1 // mmc -- Mattermost client
2 // Copyright (C) 2023 Sergey Matveev <stargrave@stargrave.org>
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
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.
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/>.
37 "github.com/davecgh/go-spew/spew"
38 "github.com/mattermost/mattermost-server/v6/model"
39 "go.stargrave.org/mmc"
42 const CmdFile = "/FILE "
45 Newwin = flag.String("newwin", "cmd/newwin", "Path to newwin command")
46 DebugFifo = flag.String("debug", "debug", "Path to debug FIFO to be created")
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 {
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")
65 log.SetFlags(log.Lshortfile)
66 log.SetOutput(os.Stdout)
67 UmaskCur = syscall.Umask(0)
68 syscall.Umask(UmaskCur)
72 DebugFd, err = os.OpenFile(
73 *DebugFifo, os.O_WRONLY|os.O_APPEND, os.FileMode(0666),
81 login, password := mmc.FindInNetrc(*entrypoint)
82 if login == "" || password == "" {
83 log.Fatalln("no credentials found for:", *entrypoint)
85 c := model.NewAPIv4Client("https://" + *entrypoint)
86 c.Login(login, password)
87 me, resp, err := c.GetMe("")
90 spew.Fdump(DebugFd, resp)
95 spew.Fdump(DebugFd, me)
97 log.Println("logged in")
99 time.Sleep(mmc.SleepTime)
100 teams, resp, err := c.GetTeamsForUser(me.Id, "")
103 spew.Fdump(DebugFd, resp)
108 spew.Fdump(DebugFd, teams)
112 var updateQueue []string
113 LastSent := time.Now()
115 Users, err := mmc.GetUsers(c, DebugFd)
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)
124 path.Join(pth, "name"),
125 fmt.Sprintf("%s %s\n", u.FirstName, u.LastName),
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,
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)
143 spew.Fdump(DebugFd, resp)
145 log.Println("CreateDirectChannel:", err)
149 spew.Fdump(DebugFd, dc)
152 updateQueue = append(updateQueue, pth, dc.Id)
155 statusPth := path.Join(pth, "status")
157 if err := syscall.Mkfifo(statusPth, 0666); err != nil {
160 go func(u *model.User) {
162 time.Sleep(mmc.SleepTime)
163 fd, err := os.OpenFile(
164 statusPth, os.O_WRONLY|os.O_APPEND, os.FileMode(0666),
167 log.Println("OpenFile:", statusPth, err)
170 status, resp, err := c.GetUserStatus(u.Id, "")
173 spew.Fdump(DebugFd, resp)
175 log.Println("GetUserStatus:", err)
180 spew.Fdump(DebugFd, status)
182 fmt.Fprintf(fd, "%s\n", status.Status)
187 pth = path.Join(pth, "in")
189 if err := syscall.Mkfifo(pth, 0666); err != nil {
192 go func(u *model.User) {
193 var dc *model.Channel
195 fd, err := os.OpenFile(pth, os.O_RDONLY, os.FileMode(0666))
199 data, err := io.ReadAll(fd)
206 dc, resp, err = c.CreateDirectChannel(me.Id, u.Id)
209 spew.Fdump(DebugFd, resp)
211 log.Println("CreateDirectChannel:", err)
216 spew.Fdump(DebugFd, dc)
219 if _, err = makePost(c, dc.Id, string(data)); err != nil {
220 log.Println("makePost:", err)
222 LastSent = time.Now()
227 Chans := make(map[string]*model.Channel)
228 time.Sleep(mmc.SleepTime)
229 page, resp, err := c.GetChannelsForTeamForUser(Team.Id, me.Id, false, "")
232 spew.Fdump(DebugFd, resp)
237 spew.Fdump(DebugFd, page)
239 for _, ch := range page {
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(
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,
263 usersPth := path.Join(pth, "users")
265 if err := syscall.Mkfifo(usersPth, 0666); err != nil {
268 go func(ch *model.Channel) {
270 time.Sleep(mmc.SleepTime)
271 fd, err := os.OpenFile(
272 usersPth, os.O_WRONLY|os.O_APPEND, os.FileMode(0666),
275 log.Println("OpenFile:", usersPth, err)
279 users, resp, err := c.GetUsersInChannel(ch.Id, n, mmc.PerPage, "")
282 spew.Fdump(DebugFd, resp)
284 log.Println("GetUsersInChannel:", err)
289 spew.Fdump(DebugFd, users)
291 for _, u := range users {
292 fmt.Fprintf(fd, "%s\n", u.Username)
294 if len(users) < mmc.PerPage {
302 pth = path.Join(pth, "in")
304 if err := syscall.Mkfifo(pth, 0666); err != nil {
307 go func(ch *model.Channel) {
309 fd, err := os.OpenFile(pth, os.O_RDONLY, os.FileMode(0666))
313 data, err := io.ReadAll(fd)
318 if _, err = makePost(c, ch.Id, string(data)); err != nil {
319 log.Println("makePost:", err)
321 LastSent = time.Now()
326 log.Println("syncing", len(updateQueue)/2, "rooms")
327 for len(updateQueue) > 0 {
328 err := updatePosts(c, Users, updateQueue[0], updateQueue[1])
330 log.Println("updatePosts:", err)
332 updateQueue = updateQueue[2:]
334 log.Println("sync done")
335 if *notifyCmd != "" {
336 exec.Command(*notifyCmd, "sync done").Run()
340 os.MkdirAll("file", 0777)
341 pthGet := path.Join("file", "get")
343 if err := syscall.Mkfifo(pthGet, 0666); err != nil {
346 pthOut := path.Join("file", "out")
348 if err := syscall.Mkfifo(pthOut, 0666); err != nil {
352 time.Sleep(mmc.SleepTime)
353 fd, err := os.OpenFile(pthGet, os.O_RDONLY, os.FileMode(0666))
357 data, err := io.ReadAll(fd)
362 fileId := strings.TrimRight(string(data), " \n")
363 if len(fileId) == 0 {
366 fi, resp, err := c.GetFileInfo(fileId)
368 spew.Fdump(DebugFd, fi, resp)
378 data, resp, err = c.GetFile(fileId)
381 spew.Fdump(DebugFd, resp)
386 fd, err = os.OpenFile(
387 pthOut, os.O_WRONLY|os.O_APPEND, os.FileMode(0666),
393 w := tar.NewWriter(fd)
394 if err = w.WriteHeader(&tar.Header{
395 Format: tar.FormatPAX,
396 Typeflag: tar.TypeReg,
398 Size: int64(len(data)),
400 ModTime: time.Unix(fi.CreateAt/1000, 0),
401 PAXRecords: map[string]string{
403 "MM.MIMEType": fi.MimeType,
410 if _, err = io.Copy(w, bytes.NewReader(data)); err != nil {
423 needsShutdown := make(chan os.Signal)
424 wc, err := model.NewWebSocketClient4("wss://"+*entrypoint, c.AuthToken)
430 t := time.NewTicker(time.Minute)
434 if time.Now().Before(LastSent.Add(time.Minute)) {
437 wc.SendMessage("ping", nil)
438 if *heartbeatCh != "" {
439 if _, _, err = c.ViewChannel(
441 &model.ChannelView{ChannelId: Chans[*heartbeatCh].Id},
443 log.Println("ChannelView:", err)
446 case <-wc.PingTimeoutChannel:
447 log.Println("PING timeout")
448 needsShutdown <- syscall.SIGTERM
449 case e := <-wc.EventChannel:
450 if e == nil || !e.IsValid() {
455 spew.Fdump(DebugFd, e.EventType(), data)
458 if userId, ok := data["user_id"]; ok && userId.(string) != "" {
459 user = Users[userId.(string)]
461 log.Println("unknown user for event:", e)
465 switch eventType := e.EventType(); eventType {
466 case model.WebsocketEventTyping:
467 if *notifyCmd != "" {
468 exec.Command(*notifyCmd, "typing: "+user.Username).Run()
470 case model.WebsocketEventPostEdited,
471 model.WebsocketEventPostDeleted,
472 model.WebsocketEventPosted:
473 chName := data["channel_name"].(string)
475 if err = json.NewDecoder(
476 strings.NewReader(data["post"].(string)),
477 ).Decode(&post); err != nil {
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]
487 log.Println("unknown user:", post)
490 recipient = path.Join("users", user.Username)
491 case model.ChannelTypeOpen:
493 case model.ChannelTypePrivate:
495 case model.ChannelTypeGroup:
496 recipient = path.Join("chans", chName)
499 recipient, Users, []mmc.Post{{P: &post, E: eventType}},
503 case model.WebsocketEventStatusChange:
504 log.Println("status change:", user.Username, "->", data["status"].(string))
505 if *notifyCmd != "" {
506 exec.Command(*notifyCmd, fmt.Sprintf(
508 user.Username, data["status"].(string),
511 case model.WebsocketEventHello:
512 case model.WebsocketEventChannelViewed:
516 case resp := <-wc.ResponseChannel:
517 if resp == nil || !resp.IsValid() {
521 spew.Fdump(DebugFd, resp)
523 if text, ok := resp.Data["text"].(string); ok && text == "pong" {
524 LastSent = time.Now()
530 signal.Notify(needsShutdown, syscall.SIGTERM, syscall.SIGINT)
533 log.Println("finished")