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