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"
48 Newwin = flag.String("newwin", "cmd/newwin", "Path to newwin command")
49 DebugFifo = flag.String("debug", "debug", "Path to debug FIFO to be created")
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 {
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")
68 log.SetFlags(log.Lshortfile)
69 log.SetOutput(os.Stdout)
70 UmaskCur = syscall.Umask(0)
71 syscall.Umask(UmaskCur)
75 DebugFd, err = os.OpenFile(
76 *DebugFifo, os.O_WRONLY|os.O_APPEND, os.FileMode(0666),
84 login, password := mmc.FindInNetrc(*entrypoint)
85 if login == "" || password == "" {
86 log.Fatalln("no credentials found for:", *entrypoint)
88 c := model.NewAPIv4Client("https://" + *entrypoint)
89 c.Login(login, password)
90 me, resp, err := c.GetMe("")
93 spew.Fdump(DebugFd, resp)
98 spew.Fdump(DebugFd, me)
100 log.Println("logged in")
102 time.Sleep(mmc.SleepTime)
103 teams, resp, err := c.GetTeamsForUser(me.Id, "")
106 spew.Fdump(DebugFd, resp)
111 spew.Fdump(DebugFd, teams)
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, "")
121 spew.Fdump(DebugFd, resp)
126 spew.Fdump(DebugFd, page)
128 for _, u := range page {
131 if len(page) < PerPage {
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)
142 path.Join(pth, "name"),
143 fmt.Sprintf("%s %s\n", u.FirstName, u.LastName),
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,
155 updateQueue = append(updateQueue,
156 pth, u.Id+"__"+me.Id,
157 pth, me.Id+"__"+u.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 time.Sleep(mmc.SleepTime)
201 fd, err := os.OpenFile(pth, os.O_RDONLY, os.FileMode(0666))
205 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)
220 spew.Fdump(DebugFd, dc)
223 if _, err = makePost(c, dc.Id, string(data)); err != nil {
224 log.Println("makePost:", err)
226 LastSent = time.Now()
231 Chans := make(map[string]*model.Channel)
232 time.Sleep(mmc.SleepTime)
233 page, resp, err := c.GetChannelsForTeamForUser(Team.Id, me.Id, false, "")
236 spew.Fdump(DebugFd, resp)
241 spew.Fdump(DebugFd, page)
243 for _, ch := range page {
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(
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,
267 usersPth := path.Join(pth, "users")
269 if err := syscall.Mkfifo(usersPth, 0666); err != nil {
272 go func(ch *model.Channel) {
274 time.Sleep(mmc.SleepTime)
275 fd, err := os.OpenFile(
276 usersPth, os.O_WRONLY|os.O_APPEND, os.FileMode(0666),
279 log.Println("OpenFile:", usersPth, err)
282 for pageNum := 0; ; pageNum++ {
283 users, resp, err := c.GetUsersInChannel(ch.Id, pageNum, PerPage, "")
286 spew.Fdump(DebugFd, resp)
288 log.Println("GetUsersInChannel:", err)
293 spew.Fdump(DebugFd, users)
295 for _, u := range users {
296 fmt.Fprintf(fd, "%s\n", u.Username)
298 if len(users) < PerPage {
306 pth = path.Join(pth, "in")
308 if err := syscall.Mkfifo(pth, 0666); err != nil {
311 go func(ch *model.Channel) {
313 time.Sleep(mmc.SleepTime)
314 fd, err := os.OpenFile(pth, os.O_RDONLY, os.FileMode(0666))
318 data, err := io.ReadAll(fd)
323 if _, err = makePost(c, ch.Id, string(data)); err != nil {
324 log.Println("makePost:", err)
326 LastSent = time.Now()
332 log.Println("syncing", len(Chans)+len(Users), "rooms")
333 for len(updateQueue) > 0 {
334 err := updatePosts(c, Users, updateQueue[0], updateQueue[1])
336 log.Println("updatePosts:", err)
338 updateQueue = updateQueue[2:]
340 log.Println("sync done")
344 os.MkdirAll("file", 0777)
345 pthGet := path.Join("file", "get")
347 if err := syscall.Mkfifo(pthGet, 0666); err != nil {
350 pthOut := path.Join("file", "out")
352 if err := syscall.Mkfifo(pthOut, 0666); err != nil {
356 time.Sleep(mmc.SleepTime)
357 fd, err := os.OpenFile(pthGet, os.O_RDONLY, os.FileMode(0666))
361 data, err := io.ReadAll(fd)
366 fileId := strings.TrimRight(string(data), " \n")
367 if len(fileId) == 0 {
370 fi, resp, err := c.GetFileInfo(fileId)
372 spew.Fdump(DebugFd, fi, resp)
382 data, resp, err = c.GetFile(fileId)
385 spew.Fdump(DebugFd, resp)
390 fd, err = os.OpenFile(
391 pthOut, os.O_WRONLY|os.O_APPEND, os.FileMode(0666),
397 w := tar.NewWriter(fd)
398 if err = w.WriteHeader(&tar.Header{
399 Format: tar.FormatPAX,
400 Typeflag: tar.TypeReg,
402 Size: int64(len(data)),
404 PAXRecords: map[string]string{
406 "MM.MIMEType": fi.MimeType,
413 if _, err = io.Copy(w, bytes.NewReader(data)); err != nil {
426 needsShutdown := make(chan os.Signal)
427 wc, err := model.NewWebSocketClient4("wss://"+*entrypoint, c.AuthToken)
433 t := time.NewTicker(time.Minute)
437 if time.Now().Before(LastSent.Add(time.Minute)) {
440 wc.SendMessage("ping", nil)
441 if *heartbeatCh != "" {
442 if _, _, err = c.ViewChannel(
444 &model.ChannelView{ChannelId: Chans[*heartbeatCh].Id},
446 log.Println("ChannelView:", err)
449 case <-wc.PingTimeoutChannel:
450 log.Println("PING timeout")
451 needsShutdown <- syscall.SIGTERM
452 case e := <-wc.EventChannel:
453 if e == nil || !e.IsValid() {
458 spew.Fdump(DebugFd, e.EventType(), data)
461 if userId, ok := data["user_id"]; ok && userId.(string) != "" {
462 user = Users[userId.(string)]
464 log.Println("unknown user for event:", e)
468 switch eventType := e.EventType(); eventType {
469 case model.WebsocketEventTyping:
470 if *notifyCmd != "" {
471 exec.Command(*notifyCmd, "typing: "+user.Username).Run()
473 case model.WebsocketEventPostEdited,
474 model.WebsocketEventPostDeleted,
475 model.WebsocketEventPosted:
476 chName := data["channel_name"].(string)
478 if err = json.NewDecoder(
479 strings.NewReader(data["post"].(string)),
480 ).Decode(&post); err != nil {
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]
490 log.Println("unknown user:", post)
493 recipient = path.Join("users", user.Username)
494 case model.ChannelTypeOpen:
496 case model.ChannelTypePrivate:
498 case model.ChannelTypeGroup:
499 recipient = path.Join("chans", chName)
502 recipient, Users, []mmc.Post{{P: &post, E: eventType}},
506 case model.WebsocketEventStatusChange:
507 log.Println("status change:", user.Username, "->", data["status"].(string))
508 if *notifyCmd != "" {
509 exec.Command(*notifyCmd, fmt.Sprintf(
511 user.Username, data["status"].(string),
514 case model.WebsocketEventHello:
515 case model.WebsocketEventChannelViewed:
519 case resp := <-wc.ResponseChannel:
520 if resp == nil || !resp.IsValid() {
524 spew.Fdump(DebugFd, resp)
526 if text, ok := resp.Data["text"].(string); ok && text == "pong" {
527 LastSent = time.Now()
533 signal.Notify(needsShutdown, syscall.SIGTERM, syscall.SIGINT)
536 log.Println("finished")