]> Sergey Matveev's repositories - mmc.git/blob - cmd/mmc/main.go
No hardcoded entrypoint
[mmc.git] / cmd / mmc / main.go
1 // mmc -- Mattermost client
2 // Copyright (C) 2023 Sergey Matveev <stargrave@stargrave.org>
3 //
4 // This program is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU Affero General Public License as
6 // published by the Free Software Foundation, either version 3 of the
7 // License.
8 //
9 // This program is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU Affero General Public License for more details.
13 //
14 // You should have received a copy of the GNU Affero General Public License
15 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17 package main
18
19 import (
20         "archive/tar"
21         "bytes"
22         "encoding/json"
23         "errors"
24         "flag"
25         "fmt"
26         "io"
27         "io/fs"
28         "log"
29         "os"
30         "os/exec"
31         "os/signal"
32         "path"
33         "strings"
34         "syscall"
35         "time"
36
37         "github.com/davecgh/go-spew/spew"
38         "github.com/mattermost/mattermost-server/v6/model"
39         "go.stargrave.org/mmc"
40 )
41
42 const CmdFile = "/FILE "
43
44 var (
45         Newwin    = flag.String("newwin", "cmd/newwin", "Path to newwin command")
46         DebugFifo = flag.String("debug", "debug", "Path to debug FIFO to be created")
47         DebugFd   *os.File
48         UmaskCur  int
49 )
50
51 func rewriteIfChanged(fn string, data string) {
52         if their, err := os.ReadFile(fn); err != nil ||
53                 bytes.Compare([]byte(data), their) != 0 {
54                 if err = os.WriteFile(fn, []byte(data), 0o666); err != nil {
55                         log.Fatalln(err)
56                 }
57         }
58 }
59
60 func main() {
61         entrypoint := flag.String("entrypoint", mmc.GetEntrypoint(), "Entrypoint")
62         notifyCmd := flag.String("notify", "cmd/notify", "Path to notification handler")
63         heartbeatCh := flag.String("heartbeat-ch", "town-square", "Channel for heartbeating")
64         flag.Parse()
65         log.SetFlags(log.Lshortfile)
66         log.SetOutput(os.Stdout)
67         UmaskCur = syscall.Umask(0)
68         syscall.Umask(UmaskCur)
69
70         var err error
71         if *DebugFifo != "" {
72                 DebugFd, err = os.OpenFile(
73                         *DebugFifo, os.O_WRONLY|os.O_APPEND, os.FileMode(0666),
74                 )
75                 if err != nil {
76                         log.Fatalln(err)
77                 }
78                 defer DebugFd.Close()
79         }
80
81         login, password := mmc.FindInNetrc(*entrypoint)
82         if login == "" || password == "" {
83                 log.Fatalln("no credentials found for:", *entrypoint)
84         }
85         c := model.NewAPIv4Client("https://" + *entrypoint)
86         c.Login(login, password)
87         me, resp, err := c.GetMe("")
88         if err != nil {
89                 if DebugFd != nil {
90                         spew.Fdump(DebugFd, resp)
91                 }
92                 log.Fatalln(err)
93         }
94         if DebugFd != nil {
95                 spew.Fdump(DebugFd, me)
96         }
97         log.Println("logged in")
98
99         time.Sleep(mmc.SleepTime)
100         teams, resp, err := c.GetTeamsForUser(me.Id, "")
101         if err != nil {
102                 if DebugFd != nil {
103                         spew.Fdump(DebugFd, resp)
104                 }
105                 log.Fatalln(err)
106         }
107         if DebugFd != nil {
108                 spew.Fdump(DebugFd, teams)
109         }
110         Team := teams[0]
111
112         var updateQueue []string
113         LastSent := time.Now()
114
115         Users, err := mmc.GetUsers(c, DebugFd)
116         if err != nil {
117                 log.Fatalln(err)
118         }
119         for _, u := range Users {
120                 pth := path.Join("users", strings.ReplaceAll(u.Username, ".", "_"))
121                 os.MkdirAll(pth, 0777)
122                 rewriteIfChanged(
123                         path.Join(pth, "name"),
124                         fmt.Sprintf("%s %s\n", u.FirstName, u.LastName),
125                 )
126                 rewriteIfChanged(path.Join(pth, "email"), u.Email+"\n")
127                 rewriteIfChanged(path.Join(pth, "id"), u.Id+"\n")
128                 if _, err := os.Stat(path.Join(pth, mmc.OutRec)); err != nil &&
129                         errors.Is(err, fs.ErrNotExist) {
130                         if _, err = os.OpenFile(
131                                 path.Join(pth, mmc.OutRec), os.O_WRONLY|os.O_CREATE, 0o666,
132                         ); err != nil {
133                                 log.Fatalln(err)
134                         }
135                 }
136                 updateQueue = append(updateQueue,
137                         pth, u.Id+"__"+me.Id,
138                         pth, me.Id+"__"+u.Id,
139                 )
140
141                 statusPth := path.Join(pth, "status")
142                 os.Remove(statusPth)
143                 if err := syscall.Mkfifo(statusPth, 0666); err != nil {
144                         log.Fatalln(err)
145                 }
146                 go func(u *model.User) {
147                         for {
148                                 time.Sleep(mmc.SleepTime)
149                                 fd, err := os.OpenFile(
150                                         statusPth, os.O_WRONLY|os.O_APPEND, os.FileMode(0666),
151                                 )
152                                 if err != nil {
153                                         log.Println("OpenFile:", statusPth, err)
154                                         continue
155                                 }
156                                 status, resp, err := c.GetUserStatus(u.Id, "")
157                                 if err != nil {
158                                         if DebugFd != nil {
159                                                 spew.Fdump(DebugFd, resp)
160                                         }
161                                         log.Println("GetUserStatus:", err)
162                                         fd.Close()
163                                         continue
164                                 }
165                                 if DebugFd != nil {
166                                         spew.Fdump(DebugFd, status)
167                                 }
168                                 fmt.Fprintf(fd, "%s\n", status.Status)
169                                 fd.Close()
170                         }
171                 }(u)
172
173                 pth = path.Join(pth, "in")
174                 os.Remove(pth)
175                 if err := syscall.Mkfifo(pth, 0666); err != nil {
176                         log.Fatalln(err)
177                 }
178                 go func(u *model.User) {
179                         var dc *model.Channel
180                         for {
181                                 fd, err := os.OpenFile(pth, os.O_RDONLY, os.FileMode(0666))
182                                 if err != nil {
183                                         continue
184                                 }
185                                 data, err := io.ReadAll(fd)
186                                 fd.Close()
187                                 if err != nil {
188                                         continue
189                                 }
190                                 if dc == nil {
191                                         dc, resp, err = c.CreateDirectChannel(me.Id, u.Id)
192                                         if err != nil {
193                                                 if DebugFd != nil {
194                                                         spew.Fdump(DebugFd, resp)
195                                                 }
196                                                 log.Println("CreateDirectChannel:", err)
197                                                 continue
198                                         }
199                                         if DebugFd != nil {
200                                                 spew.Fdump(DebugFd, dc)
201                                         }
202                                 }
203                                 if _, err = makePost(c, dc.Id, string(data)); err != nil {
204                                         log.Println("makePost:", err)
205                                 }
206                                 LastSent = time.Now()
207                         }
208                 }(u)
209         }
210
211         Chans := make(map[string]*model.Channel)
212         time.Sleep(mmc.SleepTime)
213         page, resp, err := c.GetChannelsForTeamForUser(Team.Id, me.Id, false, "")
214         if err != nil {
215                 if DebugFd != nil {
216                         spew.Fdump(DebugFd, resp)
217                 }
218                 log.Fatalln(err)
219         }
220         if DebugFd != nil {
221                 spew.Fdump(DebugFd, page)
222         }
223         for _, ch := range page {
224                 if ch.Type == "D" {
225                         continue
226                 }
227                 Chans[ch.Name] = ch
228                 pth := path.Join("chans", strings.ReplaceAll(ch.Name, ".", "_"))
229                 updateQueue = append(updateQueue, pth, ch.Id)
230                 os.MkdirAll(pth, 0777)
231                 rewriteIfChanged(path.Join(pth, "id"), ch.Id+"\n")
232                 rewriteIfChanged(path.Join(pth, "info"), fmt.Sprintf(
233                         "%s\n%s\n%s\n",
234                         ch.DisplayName,
235                         ch.Header,
236                         ch.Purpose,
237                 ))
238                 if _, err := os.Stat(path.Join(pth, mmc.OutRec)); err != nil &&
239                         errors.Is(err, fs.ErrNotExist) {
240                         if _, err = os.OpenFile(
241                                 path.Join(pth, mmc.OutRec), os.O_WRONLY|os.O_CREATE, 0o666,
242                         ); err != nil {
243                                 log.Fatalln(err)
244                         }
245                 }
246
247                 usersPth := path.Join(pth, "users")
248                 os.Remove(usersPth)
249                 if err := syscall.Mkfifo(usersPth, 0666); err != nil {
250                         log.Fatalln(err)
251                 }
252                 go func(ch *model.Channel) {
253                         for {
254                                 time.Sleep(mmc.SleepTime)
255                                 fd, err := os.OpenFile(
256                                         usersPth, os.O_WRONLY|os.O_APPEND, os.FileMode(0666),
257                                 )
258                                 if err != nil {
259                                         log.Println("OpenFile:", usersPth, err)
260                                         continue
261                                 }
262                                 for n := 0; ; n++ {
263                                         users, resp, err := c.GetUsersInChannel(ch.Id, n, mmc.PerPage, "")
264                                         if err != nil {
265                                                 if DebugFd != nil {
266                                                         spew.Fdump(DebugFd, resp)
267                                                 }
268                                                 log.Println("GetUsersInChannel:", err)
269                                                 fd.Close()
270                                                 continue
271                                         }
272                                         if DebugFd != nil {
273                                                 spew.Fdump(DebugFd, users)
274                                         }
275                                         for _, u := range users {
276                                                 fmt.Fprintf(fd, "%s\n", u.Username)
277                                         }
278                                         if len(users) < mmc.PerPage {
279                                                 break
280                                         }
281                                 }
282                                 fd.Close()
283                         }
284                 }(ch)
285
286                 pth = path.Join(pth, "in")
287                 os.Remove(pth)
288                 if err := syscall.Mkfifo(pth, 0666); err != nil {
289                         log.Fatalln(err)
290                 }
291                 go func(ch *model.Channel) {
292                         for {
293                                 fd, err := os.OpenFile(pth, os.O_RDONLY, os.FileMode(0666))
294                                 if err != nil {
295                                         continue
296                                 }
297                                 data, err := io.ReadAll(fd)
298                                 fd.Close()
299                                 if err != nil {
300                                         continue
301                                 }
302                                 if _, err = makePost(c, ch.Id, string(data)); err != nil {
303                                         log.Println("makePost:", err)
304                                 }
305                                 LastSent = time.Now()
306                         }
307                 }(ch)
308         }
309
310         go func() {
311                 log.Println("syncing", len(Chans)+len(Users), "rooms")
312                 for len(updateQueue) > 0 {
313                         err := updatePosts(c, Users, updateQueue[0], updateQueue[1])
314                         if err != nil {
315                                 log.Println("updatePosts:", err)
316                         }
317                         updateQueue = updateQueue[2:]
318                 }
319                 log.Println("sync done")
320         }()
321
322         go func() {
323                 os.MkdirAll("file", 0777)
324                 pthGet := path.Join("file", "get")
325                 os.Remove(pthGet)
326                 if err := syscall.Mkfifo(pthGet, 0666); err != nil {
327                         log.Fatalln(err)
328                 }
329                 pthOut := path.Join("file", "out")
330                 os.Remove(pthOut)
331                 if err := syscall.Mkfifo(pthOut, 0666); err != nil {
332                         log.Fatalln(err)
333                 }
334                 for {
335                         time.Sleep(mmc.SleepTime)
336                         fd, err := os.OpenFile(pthGet, os.O_RDONLY, os.FileMode(0666))
337                         if err != nil {
338                                 continue
339                         }
340                         data, err := io.ReadAll(fd)
341                         fd.Close()
342                         if err != nil {
343                                 continue
344                         }
345                         fileId := strings.TrimRight(string(data), " \n")
346                         if len(fileId) == 0 {
347                                 continue
348                         }
349                         fi, resp, err := c.GetFileInfo(fileId)
350                         if DebugFd != nil {
351                                 spew.Fdump(DebugFd, fi, resp)
352                         }
353                         if err != nil {
354                                 log.Println(err)
355                                 continue
356                         }
357                         if fi == nil {
358                                 fmt.Println(resp)
359                                 continue
360                         }
361                         data, resp, err = c.GetFile(fileId)
362                         if err != nil {
363                                 if DebugFd != nil {
364                                         spew.Fdump(DebugFd, resp)
365                                 }
366                                 log.Println(err)
367                                 continue
368                         }
369                         fd, err = os.OpenFile(
370                                 pthOut, os.O_WRONLY|os.O_APPEND, os.FileMode(0666),
371                         )
372                         if err != nil {
373                                 log.Println(err)
374                                 continue
375                         }
376                         w := tar.NewWriter(fd)
377                         if err = w.WriteHeader(&tar.Header{
378                                 Format:   tar.FormatPAX,
379                                 Typeflag: tar.TypeReg,
380                                 Name:     fi.Name,
381                                 Size:     int64(len(data)),
382                                 Mode:     0o666,
383                                 ModTime:  time.Unix(fi.CreateAt/1000, 0),
384                                 PAXRecords: map[string]string{
385                                         "MM.FileId":   fi.Id,
386                                         "MM.MIMEType": fi.MimeType,
387                                 },
388                         }); err != nil {
389                                 log.Println(err)
390                                 fd.Close()
391                                 continue
392                         }
393                         if _, err = io.Copy(w, bytes.NewReader(data)); err != nil {
394                                 log.Println(err)
395                                 fd.Close()
396                                 continue
397                         }
398                         err = w.Close()
399                         fd.Close()
400                         if err != nil {
401                                 log.Println(err)
402                         }
403                 }
404         }()
405
406         needsShutdown := make(chan os.Signal)
407         wc, err := model.NewWebSocketClient4("wss://"+*entrypoint, c.AuthToken)
408         if err != nil {
409                 log.Fatalln(err)
410         }
411         go func() {
412                 wc.Listen()
413                 t := time.NewTicker(time.Minute)
414                 for {
415                         select {
416                         case <-t.C:
417                                 if time.Now().Before(LastSent.Add(time.Minute)) {
418                                         continue
419                                 }
420                                 wc.SendMessage("ping", nil)
421                                 if *heartbeatCh != "" {
422                                         if _, _, err = c.ViewChannel(
423                                                 me.Id,
424                                                 &model.ChannelView{ChannelId: Chans[*heartbeatCh].Id},
425                                         ); err != nil {
426                                                 log.Println("ChannelView:", err)
427                                         }
428                                 }
429                         case <-wc.PingTimeoutChannel:
430                                 log.Println("PING timeout")
431                                 needsShutdown <- syscall.SIGTERM
432                         case e := <-wc.EventChannel:
433                                 if e == nil || !e.IsValid() {
434                                         continue
435                                 }
436                                 data := e.GetData()
437                                 if DebugFd != nil {
438                                         spew.Fdump(DebugFd, e.EventType(), data)
439                                 }
440                                 var user *model.User
441                                 if userId, ok := data["user_id"]; ok && userId.(string) != "" {
442                                         user = Users[userId.(string)]
443                                         if user == nil {
444                                                 log.Println("unknown user for event:", e)
445                                                 continue
446                                         }
447                                 }
448                                 switch eventType := e.EventType(); eventType {
449                                 case model.WebsocketEventTyping:
450                                         if *notifyCmd != "" {
451                                                 exec.Command(*notifyCmd, "typing: "+user.Username).Run()
452                                         }
453                                 case model.WebsocketEventPostEdited,
454                                         model.WebsocketEventPostDeleted,
455                                         model.WebsocketEventPosted:
456                                         chName := data["channel_name"].(string)
457                                         var post model.Post
458                                         if err = json.NewDecoder(
459                                                 strings.NewReader(data["post"].(string)),
460                                         ).Decode(&post); err != nil {
461                                                 log.Fatalln(err)
462                                         }
463                                         var recipient string
464                                         switch model.ChannelType(data["channel_type"].(string)) {
465                                         case model.ChannelTypeDirect:
466                                                 userId := strings.TrimPrefix(chName, me.Id+"__")
467                                                 userId = strings.TrimSuffix(userId, "__"+me.Id)
468                                                 user := Users[userId]
469                                                 if user == nil {
470                                                         log.Println("unknown user:", post)
471                                                         continue
472                                                 }
473                                                 recipient = path.Join("users", user.Username)
474                                         case model.ChannelTypeOpen:
475                                                 fallthrough
476                                         case model.ChannelTypePrivate:
477                                                 fallthrough
478                                         case model.ChannelTypeGroup:
479                                                 recipient = path.Join("chans", chName)
480                                         }
481                                         if err = writePosts(
482                                                 recipient, Users, []mmc.Post{{P: &post, E: eventType}},
483                                         ); err != nil {
484                                                 log.Fatalln(err)
485                                         }
486                                 case model.WebsocketEventStatusChange:
487                                         log.Println("status change:", user.Username, "->", data["status"].(string))
488                                         if *notifyCmd != "" {
489                                                 exec.Command(*notifyCmd, fmt.Sprintf(
490                                                         "status: %s -> %s",
491                                                         user.Username, data["status"].(string),
492                                                 )).Run()
493                                         }
494                                 case model.WebsocketEventHello:
495                                 case model.WebsocketEventChannelViewed:
496                                 default:
497                                         log.Println(e)
498                                 }
499                         case resp := <-wc.ResponseChannel:
500                                 if resp == nil || !resp.IsValid() {
501                                         continue
502                                 }
503                                 if DebugFd != nil {
504                                         spew.Fdump(DebugFd, resp)
505                                 }
506                                 if text, ok := resp.Data["text"].(string); ok && text == "pong" {
507                                         LastSent = time.Now()
508                                 }
509                         }
510                 }
511         }()
512
513         signal.Notify(needsShutdown, syscall.SIGTERM, syscall.SIGINT)
514         <-needsShutdown
515         c.Logout()
516         log.Println("finished")
517 }