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/>.
39 "github.com/davecgh/go-spew/spew"
40 "github.com/mattermost/mattermost-server/v6/model"
41 "go.stargrave.org/mmc"
44 const CmdFile = "/FILE "
47 Newwin = flag.String("newwin", "cmd/newwin", "Path to newwin command")
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")
66 log.SetFlags(log.Lshortfile)
67 log.SetOutput(os.Stdout)
68 UmaskCur = syscall.Umask(0)
69 syscall.Umask(UmaskCur)
72 err := syscall.Mkfifo("debug", 0666)
76 DebugFd, err = os.OpenFile(
77 "debug", 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 var updateQueue []string
116 Chans := make(map[string]*model.Channel)
117 time.Sleep(mmc.SleepTime)
118 page, resp, err := c.GetChannelsForTeamForUser(Team.Id, me.Id, false, "")
121 spew.Fdump(DebugFd, resp)
126 spew.Fdump(DebugFd, page)
128 for _, ch := range page {
133 pth := path.Join("chans", strings.ReplaceAll(ch.Name, ".", "_"))
134 updateQueue = append(updateQueue, pth, ch.Id)
135 os.MkdirAll(pth, 0777)
136 rewriteIfChanged(path.Join(pth, "id"), ch.Id+"\n")
137 rewriteIfChanged(path.Join(pth, "info"), fmt.Sprintf(
143 if _, err := os.Stat(path.Join(pth, mmc.OutRec)); err != nil &&
144 errors.Is(err, fs.ErrNotExist) {
145 if _, err = os.OpenFile(
146 path.Join(pth, mmc.OutRec), os.O_WRONLY|os.O_CREATE, 0o666,
152 usersPth := path.Join(pth, "users")
154 if err := syscall.Mkfifo(usersPth, 0666); err != nil {
157 go func(ch *model.Channel) {
159 time.Sleep(mmc.SleepTime)
160 fd, err := os.OpenFile(
161 usersPth, os.O_WRONLY|os.O_APPEND, os.FileMode(0666),
164 log.Println("OpenFile:", usersPth, err)
168 users, resp, err := c.GetUsersInChannel(ch.Id, n, mmc.PerPage, "")
171 spew.Fdump(DebugFd, resp)
173 log.Println("GetUsersInChannel:", err)
178 spew.Fdump(DebugFd, users)
180 for _, u := range users {
181 fmt.Fprintf(fd, "%s\n", u.Username)
183 if len(users) < mmc.PerPage {
191 pth = path.Join(pth, "in")
193 if err := syscall.Mkfifo(pth, 0666); err != nil {
196 go func(ch *model.Channel) {
198 fd, err := os.OpenFile(pth, os.O_RDONLY, os.FileMode(0666))
202 data, err := io.ReadAll(fd)
207 if _, err = makePost(c, ch.Id, string(data)); err != nil {
208 log.Println("makePost:", err)
214 Users, err := mmc.GetUsers(c, DebugFd)
219 UsersDC := make(map[string]*model.Channel, len(Users))
220 for _, u := range Users {
221 pth := path.Join("users", strings.ReplaceAll(u.Username, ".", "_"))
222 os.MkdirAll(pth, 0777)
224 path.Join(pth, "name"),
225 fmt.Sprintf("%s %s\n", u.FirstName, u.LastName),
227 rewriteIfChanged(path.Join(pth, "email"), u.Email+"\n")
228 rewriteIfChanged(path.Join(pth, "id"), u.Id+"\n")
229 if _, err := os.Stat(path.Join(pth, mmc.OutRec)); err != nil &&
230 errors.Is(err, fs.ErrNotExist) {
231 if _, err = os.OpenFile(
232 path.Join(pth, mmc.OutRec), os.O_WRONLY|os.O_CREATE, 0o666,
238 if fi, err := os.Stat(path.Join(pth, mmc.OutRec)); err == nil && fi.Size() > 0 {
239 time.Sleep(mmc.SleepTime)
240 dc, resp, err := c.CreateDirectChannel(me.Id, u.Id)
243 spew.Fdump(DebugFd, resp)
245 log.Println("CreateDirectChannel:", err)
249 spew.Fdump(DebugFd, dc)
252 updateQueue = append(updateQueue, pth, dc.Id)
255 statusPth := path.Join(pth, "status")
257 if err := syscall.Mkfifo(statusPth, 0666); err != nil {
260 go func(u *model.User) {
262 time.Sleep(mmc.SleepTime)
263 fd, err := os.OpenFile(
264 statusPth, os.O_WRONLY|os.O_APPEND, os.FileMode(0666),
267 log.Println("OpenFile:", statusPth, err)
270 status, resp, err := c.GetUserStatus(u.Id, "")
273 spew.Fdump(DebugFd, resp)
275 log.Println("GetUserStatus:", err)
280 spew.Fdump(DebugFd, status)
282 fmt.Fprintf(fd, "%s\n", status.Status)
287 pth = path.Join(pth, "in")
289 if err := syscall.Mkfifo(pth, 0666); err != nil {
292 go func(u *model.User) {
293 var dc *model.Channel
295 fd, err := os.OpenFile(pth, os.O_RDONLY, os.FileMode(0666))
299 data, err := io.ReadAll(fd)
306 dc, resp, err = c.CreateDirectChannel(me.Id, u.Id)
309 spew.Fdump(DebugFd, resp)
311 log.Println("CreateDirectChannel:", err)
316 spew.Fdump(DebugFd, dc)
319 if _, err = makePost(c, dc.Id, string(data)); err != nil {
320 log.Println("makePost:", err)
326 UserStatus := make(map[string]string)
327 var UserStatusM sync.RWMutex
329 pth := path.Join("users", "status")
331 if err := syscall.Mkfifo(pth, 0666); err != nil {
335 time.Sleep(mmc.SleepTime)
336 fd, err := os.OpenFile(pth, os.O_WRONLY|os.O_APPEND, os.FileMode(0666))
338 log.Println("OpenFile:", pth, err)
341 statuses := make(map[string][]string)
343 for name, status := range UserStatus {
344 statuses[status] = append(statuses[status], name)
346 UserStatusM.RUnlock()
347 for status := range statuses {
348 sort.Strings(statuses[status])
349 fmt.Fprintln(fd, status+":", strings.Join(statuses[status], " "))
355 log.Println("syncing", len(updateQueue)/2, "rooms")
356 for len(updateQueue) > 0 {
357 err := updatePosts(c, Users, updateQueue[0], updateQueue[1])
359 log.Println("updatePosts:", err)
361 updateQueue = updateQueue[2:]
363 log.Println("sync done")
364 if *notifyCmd != "" {
365 exec.Command(*notifyCmd, "sync done").Run()
369 os.MkdirAll("file", 0777)
370 pthGet := path.Join("file", "get")
372 if err := syscall.Mkfifo(pthGet, 0666); err != nil {
375 pthOut := path.Join("file", "out")
377 if err := syscall.Mkfifo(pthOut, 0666); err != nil {
381 time.Sleep(mmc.SleepTime)
382 fd, err := os.OpenFile(pthGet, os.O_RDONLY, os.FileMode(0666))
386 data, err := io.ReadAll(fd)
391 fileId := strings.TrimRight(string(data), " \n")
392 if len(fileId) == 0 {
395 fi, resp, err := c.GetFileInfo(fileId)
397 spew.Fdump(DebugFd, fi, resp)
407 data, resp, err = c.GetFile(fileId)
410 spew.Fdump(DebugFd, resp)
415 fd, err = os.OpenFile(
416 pthOut, os.O_WRONLY|os.O_APPEND, os.FileMode(0666),
422 w := tar.NewWriter(fd)
423 if err = w.WriteHeader(&tar.Header{
424 Format: tar.FormatPAX,
425 Typeflag: tar.TypeReg,
427 Size: int64(len(data)),
429 ModTime: time.Unix(fi.CreateAt/1000, 0),
430 PAXRecords: map[string]string{
432 "MM.MIMEType": fi.MimeType,
439 if _, err = io.Copy(w, bytes.NewReader(data)); err != nil {
452 needsShutdown := make(chan os.Signal)
453 wc, err := model.NewWebSocketClient4("wss://"+*entrypoint, c.AuthToken)
460 t := time.NewTicker(time.Minute)
464 if wc.ListenError != nil {
465 log.Println("ListenError:", wc.ListenError)
466 needsShutdown <- syscall.SIGTERM
470 if *heartbeatCh != "" {
471 if _, _, err = c.ViewChannel(
473 &model.ChannelView{ChannelId: Chans[*heartbeatCh].Id},
475 log.Println("ChannelView:", err)
478 case <-wc.PingTimeoutChannel:
479 log.Println("PING timeout")
480 needsShutdown <- syscall.SIGTERM
482 case e := <-wc.EventChannel:
483 if e == nil || !e.IsValid() {
487 spew.Fdump(DebugFd, e)
491 if userId, ok := data["user_id"]; ok && userId.(string) != "" {
492 user = Users[userId.(string)]
494 log.Println("unknown user for event:", e)
498 switch eventType := e.EventType(); eventType {
499 case model.WebsocketEventTyping:
500 if *notifyCmd != "" {
501 exec.Command(*notifyCmd, "typing: "+user.Username).Run()
503 case model.WebsocketEventPostEdited,
504 model.WebsocketEventPostDeleted,
505 model.WebsocketEventPosted:
506 chName, ok := data["channel_name"].(string)
511 if err = json.NewDecoder(
512 strings.NewReader(data["post"].(string)),
513 ).Decode(&post); err != nil {
517 switch model.ChannelType(data["channel_type"].(string)) {
518 case model.ChannelTypeDirect:
519 userId := strings.TrimPrefix(chName, me.Id+"__")
520 userId = strings.TrimSuffix(userId, "__"+me.Id)
521 user := Users[userId]
523 log.Println("unknown user:", post)
526 recipient = path.Join("users", user.Username)
527 case model.ChannelTypeOpen:
529 case model.ChannelTypePrivate:
531 case model.ChannelTypeGroup:
532 recipient = path.Join("chans", chName)
535 recipient, Users, []mmc.Post{{P: &post, E: eventType}},
539 case model.WebsocketEventStatusChange:
540 status := data["status"].(string)
542 UserStatus[user.Username] = status
544 if *notifyCmd != "" {
545 exec.Command(*notifyCmd, fmt.Sprintf(
546 "status: %s -> %s", user.Username, status,
549 case model.WebsocketEventHello:
550 case model.WebsocketEventChannelViewed:
554 case resp := <-wc.ResponseChannel:
555 if resp == nil || !resp.IsValid() {
559 spew.Fdump(DebugFd, resp)
561 statuses := make(map[string]string)
562 for userId, status := range resp.Data {
563 status, ok := status.(string)
567 user := Users[userId]
571 statuses[user.Username] = status
573 if len(statuses) > 0 {
575 for u := range UserStatus {
576 delete(UserStatus, u)
578 for u, status := range statuses {
579 UserStatus[u] = status
587 signal.Notify(needsShutdown, syscall.SIGTERM, syscall.SIGINT)
590 log.Println("finished")