// Copyright (C) 2024 Leah Neukirchen // // based on mautrix-go example code: // // Copyright (C) 2017 Tulir Asokan // Copyright (C) 2018-2020 Luca Weiss // Copyright (C) 2023 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package main import ( "context" "encoding/json" "errors" "flag" "fmt" "net/http" "os" "os/signal" "strings" "time" "k8s.io/klog" "maunium.net/go/mautrix" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" ) var folks map[string]struct{} type JSONTop struct { Users []JSONUser `json:"users"` } type JSONUser struct { Login string `json:"login"` } func ircquote(s string) string { if len(s) < 1 { return s // bad luck, V } ZWJ := "\u200d" return s[0:1] + ZWJ + s[1:] } func listFmt(list []string) string { var listQuoted []string for _, l := range list { listQuoted = append(listQuoted, ircquote(l)) } return "[ " + strings.Join(listQuoted, " ") + " ]" } func update(ctx context.Context) (string, error) { req, err := http.NewRequestWithContext(ctx, "GET", flagBackendURL, nil) if err != nil { return "", err } res, err := http.DefaultClient.Do(req) if err != nil { return "", err } defer res.Body.Close() var users JSONTop if res.StatusCode != 200 { return "", fmt.Errorf("unexpected API status code %d", res.StatusCode) } if err := json.NewDecoder(res.Body).Decode(&users); err != nil { return "", fmt.Errorf("could not decode API response: %v", err) } var left []string var arrived []string var unchanged []string newFolks := make(map[string]struct{}) for _, user := range users.Users { newFolks[user.Login] = struct{}{} if _, ok := folks[user.Login]; !ok { arrived = append(arrived, user.Login) } else { unchanged = append(unchanged, user.Login) } } for user, _ := range folks { if _, ok := newFolks[user]; !ok { left = append(left, user) } } folks = newFolks // no change if len(arrived) == 0 && len(left) == 0 { return "", nil } var parts []string if len(arrived) > 0 { parts = append(parts, fmt.Sprintf("arrived: %s", listFmt(arrived))) } if len(left) > 0 { parts = append(parts, fmt.Sprintf("left: %s", listFmt(left))) } if len(unchanged) > 0 { adjective := "also" if len(left) > 0 && len(arrived) == 0 { adjective = "still" } parts = append(parts, fmt.Sprintf("%s there: %s", adjective, listFmt(unchanged))) } return strings.Join(parts, "; "), nil } var ( flagMatrixHomeserver = "matrix.org" flagMatrixUsername = "@fafomo:matrix.org" flagMatrixPasswordFile = "password.txt" flagMatrixRoom string flagBackendURL string ) func main() { flag.StringVar(&flagMatrixHomeserver, "matrix_homeserver", flagMatrixHomeserver, "Address of Matrix homeserver") flag.StringVar(&flagMatrixUsername, "matrix_username", flagMatrixUsername, "Matrix login username") flag.StringVar(&flagMatrixPasswordFile, "matrix_password_file", flagMatrixPasswordFile, "Path to file containing matrix login password") flag.StringVar(&flagMatrixRoom, "matrix_room", flagMatrixRoom, "Matrix room MXID") flag.StringVar(&flagBackendURL, "backend_url", flagBackendURL, "Checkinator (backend) addresss") flag.Parse() for _, s := range []struct { value string name string }{ {flagMatrixHomeserver, "matrix_homeserver"}, {flagMatrixUsername, "matrix_username"}, {flagMatrixPasswordFile, "matrix_password_file"}, {flagMatrixRoom, "matrix_room"}, {flagBackendURL, "backend_url"}, } { if s.value == "" { klog.Exitf("-%s must be set", s.name) } } startCtx, startCtxC := context.WithTimeout(context.Background(), 20*time.Second) defer startCtxC() if _, err := update(startCtx); err != nil { klog.Exitf("Initial update failed: %v", err) } client, err := mautrix.NewClient(flagMatrixHomeserver, id.UserID(flagMatrixUsername), "") if err != nil { klog.Exitf("NewClient failed: %v", err) } passwordBytes, err := os.ReadFile(flagMatrixPasswordFile) if err != nil { klog.Exitf("Could not read password file: %v", err) } password := strings.TrimSpace(string(passwordBytes)) klog.Infof("Logging in...") login, err := client.Login(startCtx, &mautrix.ReqLogin{ Type: mautrix.AuthTypePassword, Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: flagMatrixUsername}, Password: password, }) if err != nil { klog.Exitf("Failed to log in: %v", err) } client.AccessToken = login.AccessToken syncer := client.Syncer.(*mautrix.DefaultSyncer) syncer.OnEventType(event.EventMessage, func(ctx context.Context, evt *event.Event) { klog.V(1).Infof("Sender: %s, type: %s, id: %s, body: %q", evt.Sender, evt.Type, evt.ID, evt.Content.AsMessage().Body) }) klog.Infof("Now running...") ctx, ctxC := signal.NotifyContext(context.Background(), os.Interrupt) defer ctxC() // Run Matrix sync process in the background. syncDone := make(chan struct{}) go func() { err = client.SyncWithContext(ctx) defer close(syncDone) if err != nil && !errors.Is(err, ctx.Err()) { klog.Exitf("Sync failed: %v", err) } }() // Update space members every minute. ticker := time.NewTicker(60 * time.Second) process: for { select { case <-ticker.C: ctx, _ := context.WithTimeout(ctx, 5*time.Second) message, err := update(ctx) if err != nil { klog.Errorf("Update failed: %v", err) } else if message != "" { klog.Infof("Sent update: %s\n", message) _, err := client.SendText(ctx, id.RoomID(flagMatrixRoom), message) if err != nil { klog.Errorf("Failed to send event: %v\n", err) } } case <-ctx.Done(): break process } } klog.Infof("Waiting for graceful sync before exiting...") <-syncDone klog.Infof("Done.") }