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