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/>.
38 "github.com/davecgh/go-spew/spew"
39 "github.com/mattermost/mattermost-server/v6/model"
40 "go.stargrave.org/mmc"
43 const CmdFile = "/FILE "
46 Newwin = flag.String("newwin", "cmd/newwin", "Path to newwin command")
47 DebugFifo = flag.String("debug", "", "Path to debug FIFO to be created")
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 {
62 entrypoint := flag.String("entrypoint", mmc.GetEntrypoint(), "Entrypoint")
63 notifyCmd := flag.String("notify", "cmd/notify", "Path to notification handler")
64 heartbeatCh := flag.String("heartbeat-ch", "town-square", "Channel for heartbeating")
65 userStatusFifo := flag.String("user-status", "", "Path to FIFO for user statuses")
67 log.SetFlags(log.Lshortfile)
68 log.SetOutput(os.Stdout)
69 UmaskCur = syscall.Umask(0)
70 syscall.Umask(UmaskCur)
74 DebugFd, err = os.OpenFile(
75 *DebugFifo, os.O_WRONLY|os.O_APPEND, os.FileMode(0666),
83 login, password := mmc.FindInNetrc(*entrypoint)
84 if login == "" || password == "" {
85 log.Fatalln("no credentials found for:", *entrypoint)
87 c := model.NewAPIv4Client("https://" + *entrypoint)
88 c.Login(login, password)
89 me, resp, err := c.GetMe("")
92 spew.Fdump(DebugFd, resp)
97 spew.Fdump(DebugFd, me)
99 log.Println("logged in")
101 time.Sleep(mmc.SleepTime)
102 teams, resp, err := c.GetTeamsForUser(me.Id, "")
105 spew.Fdump(DebugFd, resp)
110 spew.Fdump(DebugFd, teams)
114 var updateQueue []string
115 LastSent := time.Now()
117 Users, err := mmc.GetUsers(c, DebugFd)
122 UsersDC := make(map[string]*model.Channel, len(Users))
123 userIds := make([]string, 0, len(Users))
124 for _, u := range Users {
125 userIds = append(userIds, u.Id)
126 pth := path.Join("users", strings.ReplaceAll(u.Username, ".", "_"))
127 os.MkdirAll(pth, 0777)
129 path.Join(pth, "name"),
130 fmt.Sprintf("%s %s\n", u.FirstName, u.LastName),
132 rewriteIfChanged(path.Join(pth, "email"), u.Email+"\n")
133 rewriteIfChanged(path.Join(pth, "id"), u.Id+"\n")
134 if _, err := os.Stat(path.Join(pth, mmc.OutRec)); err != nil &&
135 errors.Is(err, fs.ErrNotExist) {
136 if _, err = os.OpenFile(
137 path.Join(pth, mmc.OutRec), os.O_WRONLY|os.O_CREATE, 0o666,
143 if fi, err := os.Stat(path.Join(pth, mmc.OutRec)); err == nil && fi.Size() > 0 {
144 time.Sleep(mmc.SleepTime)
145 dc, resp, err := c.CreateDirectChannel(me.Id, u.Id)
148 spew.Fdump(DebugFd, resp)
150 log.Println("CreateDirectChannel:", err)
154 spew.Fdump(DebugFd, dc)
157 updateQueue = append(updateQueue, pth, dc.Id)
160 statusPth := path.Join(pth, "status")
162 if err := syscall.Mkfifo(statusPth, 0666); err != nil {
165 go func(u *model.User) {
167 time.Sleep(mmc.SleepTime)
168 fd, err := os.OpenFile(
169 statusPth, os.O_WRONLY|os.O_APPEND, os.FileMode(0666),
172 log.Println("OpenFile:", statusPth, err)
175 status, resp, err := c.GetUserStatus(u.Id, "")
178 spew.Fdump(DebugFd, resp)
180 log.Println("GetUserStatus:", err)
185 spew.Fdump(DebugFd, status)
187 fmt.Fprintf(fd, "%s\n", status.Status)
192 pth = path.Join(pth, "in")
194 if err := syscall.Mkfifo(pth, 0666); err != nil {
197 go func(u *model.User) {
198 var dc *model.Channel
200 fd, err := os.OpenFile(pth, os.O_RDONLY, os.FileMode(0666))
204 data, err := io.ReadAll(fd)
211 dc, resp, err = c.CreateDirectChannel(me.Id, u.Id)
214 spew.Fdump(DebugFd, resp)
216 log.Println("CreateDirectChannel:", err)
221 spew.Fdump(DebugFd, dc)
224 if _, err = makePost(c, dc.Id, string(data)); err != nil {
225 log.Println("makePost:", err)
227 LastSent = time.Now()
232 UserStatus := make(map[string]string)
233 if *userStatusFifo != "" {
234 statuses, resp, err := c.GetUsersStatusesByIds(userIds)
237 spew.Fdump(DebugFd, resp)
242 spew.Fdump(DebugFd, teams)
245 for _, s := range statuses {
246 UserStatus[Users[s.UserId].Username] = s.Status
251 time.Sleep(mmc.SleepTime)
252 fd, err := os.OpenFile(
253 *userStatusFifo, os.O_WRONLY|os.O_APPEND, os.FileMode(0666),
256 log.Println("OpenFile:", *userStatusFifo, err)
261 for name, status := range UserStatus {
264 onlines = append(onlines, name)
266 aways = append(aways, name)
269 sort.Strings(onlines)
271 fmt.Fprintln(fd, "O:", strings.Join(onlines, " "))
272 fmt.Fprintln(fd, "A:", strings.Join(aways, " "))
278 Chans := make(map[string]*model.Channel)
279 time.Sleep(mmc.SleepTime)
280 page, resp, err := c.GetChannelsForTeamForUser(Team.Id, me.Id, false, "")
283 spew.Fdump(DebugFd, resp)
288 spew.Fdump(DebugFd, page)
290 for _, ch := range page {
295 pth := path.Join("chans", strings.ReplaceAll(ch.Name, ".", "_"))
296 updateQueue = append(updateQueue, pth, ch.Id)
297 os.MkdirAll(pth, 0777)
298 rewriteIfChanged(path.Join(pth, "id"), ch.Id+"\n")
299 rewriteIfChanged(path.Join(pth, "info"), fmt.Sprintf(
305 if _, err := os.Stat(path.Join(pth, mmc.OutRec)); err != nil &&
306 errors.Is(err, fs.ErrNotExist) {
307 if _, err = os.OpenFile(
308 path.Join(pth, mmc.OutRec), os.O_WRONLY|os.O_CREATE, 0o666,
314 usersPth := path.Join(pth, "users")
316 if err := syscall.Mkfifo(usersPth, 0666); err != nil {
319 go func(ch *model.Channel) {
321 time.Sleep(mmc.SleepTime)
322 fd, err := os.OpenFile(
323 usersPth, os.O_WRONLY|os.O_APPEND, os.FileMode(0666),
326 log.Println("OpenFile:", usersPth, err)
330 users, resp, err := c.GetUsersInChannel(ch.Id, n, mmc.PerPage, "")
333 spew.Fdump(DebugFd, resp)
335 log.Println("GetUsersInChannel:", err)
340 spew.Fdump(DebugFd, users)
342 for _, u := range users {
343 fmt.Fprintf(fd, "%s\n", u.Username)
345 if len(users) < mmc.PerPage {
353 pth = path.Join(pth, "in")
355 if err := syscall.Mkfifo(pth, 0666); err != nil {
358 go func(ch *model.Channel) {
360 fd, err := os.OpenFile(pth, os.O_RDONLY, os.FileMode(0666))
364 data, err := io.ReadAll(fd)
369 if _, err = makePost(c, ch.Id, string(data)); err != nil {
370 log.Println("makePost:", err)
372 LastSent = time.Now()
377 log.Println("syncing", len(updateQueue)/2, "rooms")
378 for len(updateQueue) > 0 {
379 err := updatePosts(c, Users, updateQueue[0], updateQueue[1])
381 log.Println("updatePosts:", err)
383 updateQueue = updateQueue[2:]
385 log.Println("sync done")
386 if *notifyCmd != "" {
387 exec.Command(*notifyCmd, "sync done").Run()
391 os.MkdirAll("file", 0777)
392 pthGet := path.Join("file", "get")
394 if err := syscall.Mkfifo(pthGet, 0666); err != nil {
397 pthOut := path.Join("file", "out")
399 if err := syscall.Mkfifo(pthOut, 0666); err != nil {
403 time.Sleep(mmc.SleepTime)
404 fd, err := os.OpenFile(pthGet, os.O_RDONLY, os.FileMode(0666))
408 data, err := io.ReadAll(fd)
413 fileId := strings.TrimRight(string(data), " \n")
414 if len(fileId) == 0 {
417 fi, resp, err := c.GetFileInfo(fileId)
419 spew.Fdump(DebugFd, fi, resp)
429 data, resp, err = c.GetFile(fileId)
432 spew.Fdump(DebugFd, resp)
437 fd, err = os.OpenFile(
438 pthOut, os.O_WRONLY|os.O_APPEND, os.FileMode(0666),
444 w := tar.NewWriter(fd)
445 if err = w.WriteHeader(&tar.Header{
446 Format: tar.FormatPAX,
447 Typeflag: tar.TypeReg,
449 Size: int64(len(data)),
451 ModTime: time.Unix(fi.CreateAt/1000, 0),
452 PAXRecords: map[string]string{
454 "MM.MIMEType": fi.MimeType,
461 if _, err = io.Copy(w, bytes.NewReader(data)); err != nil {
474 needsShutdown := make(chan os.Signal)
475 wc, err := model.NewWebSocketClient4("wss://"+*entrypoint, c.AuthToken)
481 t := time.NewTicker(time.Minute)
485 if time.Now().Before(LastSent.Add(time.Minute)) {
488 wc.SendMessage("ping", nil)
489 if *heartbeatCh != "" {
490 if _, _, err = c.ViewChannel(
492 &model.ChannelView{ChannelId: Chans[*heartbeatCh].Id},
494 log.Println("ChannelView:", err)
497 case <-wc.PingTimeoutChannel:
498 log.Println("PING timeout")
499 needsShutdown <- syscall.SIGTERM
500 case e := <-wc.EventChannel:
501 if e == nil || !e.IsValid() {
506 spew.Fdump(DebugFd, e.EventType(), data)
509 if userId, ok := data["user_id"]; ok && userId.(string) != "" {
510 user = Users[userId.(string)]
512 log.Println("unknown user for event:", e)
516 switch eventType := e.EventType(); eventType {
517 case model.WebsocketEventTyping:
518 if *notifyCmd != "" {
519 exec.Command(*notifyCmd, "typing: "+user.Username).Run()
521 case model.WebsocketEventPostEdited,
522 model.WebsocketEventPostDeleted,
523 model.WebsocketEventPosted:
524 chName := data["channel_name"].(string)
526 if err = json.NewDecoder(
527 strings.NewReader(data["post"].(string)),
528 ).Decode(&post); err != nil {
532 switch model.ChannelType(data["channel_type"].(string)) {
533 case model.ChannelTypeDirect:
534 userId := strings.TrimPrefix(chName, me.Id+"__")
535 userId = strings.TrimSuffix(userId, "__"+me.Id)
536 user := Users[userId]
538 log.Println("unknown user:", post)
541 recipient = path.Join("users", user.Username)
542 case model.ChannelTypeOpen:
544 case model.ChannelTypePrivate:
546 case model.ChannelTypeGroup:
547 recipient = path.Join("chans", chName)
550 recipient, Users, []mmc.Post{{P: &post, E: eventType}},
554 case model.WebsocketEventStatusChange:
555 status := data["status"].(string)
561 log.Println(user.Username, "unknown status:", status)
563 UserStatus[user.Username] = status
564 if *notifyCmd != "" {
565 exec.Command(*notifyCmd, fmt.Sprintf(
566 "status: %s -> %s", user.Username, status,
569 case model.WebsocketEventHello:
570 case model.WebsocketEventChannelViewed:
574 case resp := <-wc.ResponseChannel:
575 if resp == nil || !resp.IsValid() {
579 spew.Fdump(DebugFd, resp)
581 if text, ok := resp.Data["text"].(string); ok && text == "pong" {
582 LastSent = time.Now()
588 signal.Notify(needsShutdown, syscall.SIGTERM, syscall.SIGINT)
591 log.Println("finished")