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