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