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"
45 Newwin = flag.String("newwin", "cmd/newwin", "Path to newwin command")
50 func rewriteIfChanged(fn string, data string) {
51 if their, err := os.ReadFile(fn); err != nil ||
52 bytes.Compare([]byte(data), their) != 0 {
53 if err = os.WriteFile(fn, []byte(data), 0o666); err != nil {
59 func mkFifo(pth string) {
60 if _, err := os.Stat(pth); err == nil {
63 if err := syscall.Mkfifo(pth, 0666); err != nil {
69 entrypoint := flag.String("entrypoint", mmc.GetEntrypoint(), "Entrypoint")
70 notifyCmd := flag.String("notify", "cmd/notify", "Path to notification handler")
71 heartbeatCh := flag.String("heartbeat-ch", "town-square", "Channel for heartbeating")
73 log.SetFlags(log.Lshortfile)
74 log.SetOutput(os.Stdout)
75 UmaskCur = syscall.Umask(0)
76 syscall.Umask(UmaskCur)
80 DebugFd, err = os.OpenFile(
81 "debug", os.O_WRONLY|os.O_APPEND, os.FileMode(0666),
88 login, password := mmc.FindInNetrc(*entrypoint)
89 if login == "" || password == "" {
90 log.Fatalln("no credentials found for:", *entrypoint)
92 c := model.NewAPIv4Client("https://" + *entrypoint)
93 c.Login(login, password)
94 me, resp, err := c.GetMe("")
97 spew.Fdump(DebugFd, resp)
102 spew.Fdump(DebugFd, me)
104 log.Println("logged in")
106 time.Sleep(mmc.SleepTime)
107 teams, resp, err := c.GetTeamsForUser(me.Id, "")
110 spew.Fdump(DebugFd, resp)
115 spew.Fdump(DebugFd, teams)
119 var updateQueue []string
120 Chans := make(map[string]*model.Channel)
121 time.Sleep(mmc.SleepTime)
122 page, resp, err := c.GetChannelsForTeamForUser(Team.Id, me.Id, false, "")
125 spew.Fdump(DebugFd, resp)
130 spew.Fdump(DebugFd, page)
132 for _, ch := range page {
137 pth := path.Join("chans", strings.ReplaceAll(ch.Name, ".", "_"))
138 updateQueue = append(updateQueue, pth, ch.Id)
139 os.MkdirAll(pth, 0777)
140 rewriteIfChanged(path.Join(pth, "id"), ch.Id+"\n")
141 rewriteIfChanged(path.Join(pth, "info"), fmt.Sprintf(
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,
156 usersPth := path.Join(pth, "users")
158 go func(ch *model.Channel) {
160 time.Sleep(mmc.SleepTime)
161 fd, err := os.OpenFile(
162 usersPth, os.O_WRONLY|os.O_APPEND, os.FileMode(0666),
165 log.Println("OpenFile:", usersPth, err)
169 users, resp, err := c.GetUsersInChannel(ch.Id, n, mmc.PerPage, "")
172 spew.Fdump(DebugFd, resp)
174 log.Println("GetUsersInChannel:", err)
179 spew.Fdump(DebugFd, users)
181 for _, u := range users {
182 fmt.Fprintf(fd, "%s\n", u.Username)
184 if len(users) < mmc.PerPage {
192 pth = path.Join(pth, "in")
194 go func(ch *model.Channel) {
196 fd, err := os.OpenFile(pth, os.O_RDONLY, os.FileMode(0666))
200 data, err := io.ReadAll(fd)
205 if _, err = makePost(c, ch.Id, string(data)); err != nil {
206 log.Println("makePost:", err)
212 Users, err := mmc.GetUsers(c, DebugFd)
217 UsersDC := make(map[string]*model.Channel, len(Users))
218 for _, u := range Users {
219 pth := path.Join("users", strings.ReplaceAll(u.Username, ".", "_"))
220 os.MkdirAll(pth, 0777)
222 path.Join(pth, "name"),
223 fmt.Sprintf("%s %s\n", u.FirstName, u.LastName),
225 rewriteIfChanged(path.Join(pth, "email"), u.Email+"\n")
226 rewriteIfChanged(path.Join(pth, "id"), u.Id+"\n")
227 if _, err := os.Stat(path.Join(pth, mmc.OutRec)); err != nil &&
228 errors.Is(err, fs.ErrNotExist) {
229 if _, err = os.OpenFile(
230 path.Join(pth, mmc.OutRec), os.O_WRONLY|os.O_CREATE, 0o666,
236 if fi, err := os.Stat(path.Join(pth, mmc.OutRec)); err == nil && fi.Size() > 0 {
237 time.Sleep(mmc.SleepTime)
238 dc, resp, err := c.CreateDirectChannel(me.Id, u.Id)
241 spew.Fdump(DebugFd, resp)
243 log.Println("CreateDirectChannel:", err)
247 spew.Fdump(DebugFd, dc)
250 updateQueue = append(updateQueue, pth, dc.Id)
253 statusPth := path.Join(pth, "status")
255 go func(u *model.User) {
257 time.Sleep(mmc.SleepTime)
258 fd, err := os.OpenFile(
259 statusPth, os.O_WRONLY|os.O_APPEND, os.FileMode(0666),
262 log.Println("OpenFile:", statusPth, err)
265 status, resp, err := c.GetUserStatus(u.Id, "")
268 spew.Fdump(DebugFd, resp)
270 log.Println("GetUserStatus:", err)
275 spew.Fdump(DebugFd, status)
277 fmt.Fprintf(fd, "%s\n", status.Status)
282 pth = path.Join(pth, "in")
284 go func(u *model.User) {
285 var dc *model.Channel
287 fd, err := os.OpenFile(pth, os.O_RDONLY, os.FileMode(0666))
291 data, err := io.ReadAll(fd)
298 dc, resp, err = c.CreateDirectChannel(me.Id, u.Id)
301 spew.Fdump(DebugFd, resp)
303 log.Println("CreateDirectChannel:", err)
308 spew.Fdump(DebugFd, dc)
311 if _, err = makePost(c, dc.Id, string(data)); err != nil {
312 log.Println("makePost:", err)
318 UserStatus := make(map[string]string)
319 var UserStatusM sync.RWMutex
321 pth := path.Join("users", "status")
324 time.Sleep(mmc.SleepTime)
325 fd, err := os.OpenFile(pth, os.O_WRONLY|os.O_APPEND, os.FileMode(0666))
327 log.Println("OpenFile:", pth, err)
330 agg := make(map[string][]string)
332 for name, status := range UserStatus {
333 agg[status] = append(agg[status], name)
335 UserStatusM.RUnlock()
336 statuses := make([]string, 0, len(agg))
337 for status := range agg {
338 sort.Strings(agg[status])
339 statuses = append(statuses, status)
341 sort.Strings(statuses)
342 for _, status := range statuses {
343 fmt.Fprintln(fd, status+":", strings.Join(agg[status], " "))
349 log.Println("syncing", len(updateQueue)/2, "rooms")
350 for len(updateQueue) > 0 {
351 err := updatePosts(c, Users, updateQueue[0], updateQueue[1])
353 log.Println("updatePosts:", err)
355 updateQueue = updateQueue[2:]
357 log.Println("sync done")
358 if *notifyCmd != "" {
359 exec.Command(*notifyCmd, "sync done").Run()
363 os.MkdirAll("file", 0777)
364 pthGet := path.Join("file", "get")
366 pthOut := path.Join("file", "out")
369 time.Sleep(mmc.SleepTime)
370 fd, err := os.OpenFile(pthGet, os.O_RDONLY, os.FileMode(0666))
374 data, err := io.ReadAll(fd)
379 fileId := strings.TrimRight(string(data), " \n")
380 if len(fileId) == 0 {
383 fi, resp, err := c.GetFileInfo(fileId)
385 spew.Fdump(DebugFd, fi, resp)
395 data, resp, err = c.GetFile(fileId)
398 spew.Fdump(DebugFd, resp)
403 fd, err = os.OpenFile(
404 pthOut, os.O_WRONLY|os.O_APPEND, os.FileMode(0666),
410 w := tar.NewWriter(fd)
411 if err = w.WriteHeader(&tar.Header{
412 Format: tar.FormatPAX,
413 Typeflag: tar.TypeReg,
415 Size: int64(len(data)),
417 ModTime: time.Unix(fi.CreateAt/1000, 0),
418 PAXRecords: map[string]string{
420 "MM.MIMEType": fi.MimeType,
427 if _, err = io.Copy(w, bytes.NewReader(data)); err != nil {
440 needsShutdown := make(chan os.Signal)
441 wc, err := model.NewWebSocketClient4("wss://"+*entrypoint, c.AuthToken)
448 t := time.NewTicker(time.Minute)
452 if wc.ListenError != nil {
453 log.Println("ListenError:", wc.ListenError)
454 needsShutdown <- syscall.SIGTERM
458 if *heartbeatCh != "" {
459 if _, _, err = c.ViewChannel(
461 &model.ChannelView{ChannelId: Chans[*heartbeatCh].Id},
463 log.Println("ChannelView:", err)
466 case <-wc.PingTimeoutChannel:
467 log.Println("PING timeout")
468 needsShutdown <- syscall.SIGTERM
470 case e := <-wc.EventChannel:
471 if e == nil || !e.IsValid() {
475 spew.Fdump(DebugFd, e)
479 if userId, ok := data["user_id"]; ok && userId.(string) != "" {
480 user = Users[userId.(string)]
482 log.Println("unknown user for event:", e)
486 switch eventType := e.EventType(); eventType {
487 case model.WebsocketEventTyping:
488 if *notifyCmd != "" {
489 exec.Command(*notifyCmd, "typing: "+user.Username).Run()
491 case model.WebsocketEventPostEdited,
492 model.WebsocketEventPostDeleted,
493 model.WebsocketEventPosted:
494 chName, ok := data["channel_name"].(string)
499 if err = json.NewDecoder(
500 strings.NewReader(data["post"].(string)),
501 ).Decode(&post); err != nil {
505 switch model.ChannelType(data["channel_type"].(string)) {
506 case model.ChannelTypeDirect:
507 userId := strings.TrimPrefix(chName, me.Id+"__")
508 userId = strings.TrimSuffix(userId, "__"+me.Id)
509 user := Users[userId]
511 log.Println("unknown user:", post)
514 recipient = path.Join("users", user.Username)
515 case model.ChannelTypeOpen:
517 case model.ChannelTypePrivate:
519 case model.ChannelTypeGroup:
520 recipient = path.Join("chans", chName)
523 recipient, Users, []mmc.Post{{P: &post, E: eventType}},
527 case model.WebsocketEventStatusChange:
528 status := data["status"].(string)
530 UserStatus[user.Username] = status
532 if *notifyCmd != "" {
533 exec.Command(*notifyCmd, fmt.Sprintf(
534 "status: %s -> %s", user.Username, status,
537 case model.WebsocketEventHello:
538 case model.WebsocketEventChannelViewed:
542 case resp := <-wc.ResponseChannel:
543 if resp == nil || !resp.IsValid() {
547 spew.Fdump(DebugFd, resp)
549 statuses := make(map[string]string)
550 for userId, status := range resp.Data {
551 status, ok := status.(string)
555 user := Users[userId]
559 statuses[user.Username] = status
561 if len(statuses) > 0 {
563 for u := range UserStatus {
564 delete(UserStatus, u)
566 for u, status := range statuses {
567 UserStatus[u] = status
575 signal.Notify(needsShutdown, syscall.SIGTERM, syscall.SIGINT)
578 log.Println("finished")