// 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" "log" "net/http" "os" "os/signal" "strings" "sync" "time" "maunium.net/go/mautrix" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" ) var backend string 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 { return "[ " + strings.Join(list, " ") + " ]" } func update(ctx context.Context) string { req, err := http.NewRequestWithContext(ctx, "GET", backend, nil) if err != nil { panic(err) } res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() var users JSONTop json.NewDecoder(res.Body).Decode(&users) var left []string var arrived []string var unchanged []string new_folks := make(map[string]struct{}) for _, user := range users.Users { new_folks[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 := new_folks[user]; !ok { left = append(left, user) } } folks = new_folks // no change if len(arrived) == 0 && len(left) == 0 { return "" } message := "" if len(arrived) > 0 { message += "arrived: " + listFmt(arrived) + "; " } if len(left) > 0 { message += "left: " + listFmt(left) + "; " } if len(unchanged) > 0 { message += "also there: " + listFmt(unchanged) + "; " } return message } func main() { homeserver := os.Getenv("FAFOMO_HOMESERVER") username := os.Getenv("FAFOMO_USERNAME") password := os.Getenv("FAFOMO_PASSWORD") room := os.Getenv("FAFOMO_ROOM") backend = os.Getenv("FAFOMO_BACKEND") startCtx, _ := context.WithTimeout(context.Background(), 20*time.Second) update(startCtx) client, err := mautrix.NewClient(homeserver, id.UserID(username), "") if err != nil { panic(err) } log.Println("Logging in...") login, err := client.Login(startCtx, &mautrix.ReqLogin{ Type: mautrix.AuthTypePassword, Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: username}, Password: password, }) client.AccessToken = login.AccessToken syncer := client.Syncer.(*mautrix.DefaultSyncer) syncer.OnEventType(event.EventMessage, func(ctx context.Context, evt *event.Event) { // log.Info(). // Str("sender", evt.Sender.String()). // Str("type", evt.Type.String()). // Str("id", evt.ID.String()). // Str("body", evt.Content.AsMessage().Body). // Msg("Received message") }) if err != nil { panic(err) } log.Println("Now running...") syncCtx, _ := context.WithCancel(context.Background()) syncCtx, stop := signal.NotifyContext(syncCtx, os.Interrupt) defer stop() var syncStopWait sync.WaitGroup syncStopWait.Add(1) go func() { err = client.SyncWithContext(syncCtx) defer syncStopWait.Done() if err != nil && !errors.Is(err, context.Canceled) { panic(err) } }() ticker := time.NewTicker(60 * time.Second) for { select { case <-ticker.C: ctx, _ := context.WithTimeout(syncCtx, 5*time.Second) message := update(ctx) if message != "" { log.Printf("Sent update: %s\n", message) _, err := client.SendText(ctx, id.RoomID(room), message) if err != nil { log.Printf("Failed to send event: %v\n", err) } } case <-syncCtx.Done(): return } } stop() syncStopWait.Wait() log.Println("Exited") }