fafomo/fafomo.go

234 lines
5.7 KiB
Go

// 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.")
}