Serge Bazanski
c437d4388e
1. Use klog instead of plain log (leveled logging is better logging) 2. Use flags instead of env vars (less change of leaks, stronger typing) 3. Rename contexts around to be more canonically named 4. Use ZWJ quoting 5. Clean up message rendering (no trailing semicolons, adjective pick)
228 lines
5.5 KiB
Go
228 lines
5.5 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"
|
|
flagMatrixPassword string
|
|
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(&flagMatrixPassword, "matrix_password", flagMatrixPassword, "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"},
|
|
{flagMatrixPassword, "matrix_password"},
|
|
{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)
|
|
}
|
|
|
|
klog.Infof("Logging in...")
|
|
|
|
login, err := client.Login(startCtx, &mautrix.ReqLogin{
|
|
Type: mautrix.AuthTypePassword,
|
|
Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: flagMatrixUsername},
|
|
Password: flagMatrixPassword,
|
|
})
|
|
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.")
|
|
}
|