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