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