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