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