2024-09-26 20:41:03 +00:00
|
|
|
// Copyright (C) 2024 Leah Neukirchen
|
|
|
|
//
|
|
|
|
// based on mautrix-go example code:
|
|
|
|
//
|
2024-09-30 20:22:23 +00:00
|
|
|
// Copyright (C) 2017 Tulir Asokan
|
2024-09-26 20:41:03 +00:00
|
|
|
// 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"
|
2024-09-30 20:46:39 +00:00
|
|
|
"flag"
|
|
|
|
"fmt"
|
2024-09-26 20:41:03 +00:00
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"os/signal"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
2024-09-30 20:46:39 +00:00
|
|
|
"k8s.io/klog"
|
2024-09-26 20:41:03 +00:00
|
|
|
"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 {
|
2024-09-30 20:46:39 +00:00
|
|
|
var listQuoted []string
|
|
|
|
for _, l := range list {
|
|
|
|
listQuoted = append(listQuoted, ircquote(l))
|
|
|
|
}
|
|
|
|
return "[ " + strings.Join(listQuoted, " ") + " ]"
|
2024-09-26 20:41:03 +00:00
|
|
|
}
|
|
|
|
|
2024-09-30 20:46:39 +00:00
|
|
|
func update(ctx context.Context) (string, error) {
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", flagBackendURL, nil)
|
2024-09-26 20:41:03 +00:00
|
|
|
if err != nil {
|
2024-09-30 20:46:39 +00:00
|
|
|
return "", err
|
2024-09-26 20:41:03 +00:00
|
|
|
}
|
|
|
|
res, err := http.DefaultClient.Do(req)
|
|
|
|
if err != nil {
|
2024-09-30 20:46:39 +00:00
|
|
|
return "", err
|
2024-09-26 20:41:03 +00:00
|
|
|
}
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
|
|
|
var users JSONTop
|
|
|
|
|
2024-09-30 20:46:39 +00:00
|
|
|
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)
|
|
|
|
}
|
2024-09-26 20:41:03 +00:00
|
|
|
|
|
|
|
var left []string
|
|
|
|
var arrived []string
|
|
|
|
var unchanged []string
|
|
|
|
|
2024-09-30 20:46:39 +00:00
|
|
|
newFolks := make(map[string]struct{})
|
2024-09-26 20:41:03 +00:00
|
|
|
for _, user := range users.Users {
|
2024-09-30 20:46:39 +00:00
|
|
|
newFolks[user.Login] = struct{}{}
|
2024-09-26 20:41:03 +00:00
|
|
|
if _, ok := folks[user.Login]; !ok {
|
|
|
|
arrived = append(arrived, user.Login)
|
|
|
|
} else {
|
|
|
|
unchanged = append(unchanged, user.Login)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for user, _ := range folks {
|
2024-09-30 20:46:39 +00:00
|
|
|
if _, ok := newFolks[user]; !ok {
|
2024-09-26 20:41:03 +00:00
|
|
|
left = append(left, user)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-30 20:46:39 +00:00
|
|
|
folks = newFolks
|
2024-09-26 20:41:03 +00:00
|
|
|
|
|
|
|
// no change
|
|
|
|
if len(arrived) == 0 && len(left) == 0 {
|
2024-09-30 20:46:39 +00:00
|
|
|
return "", nil
|
2024-09-26 20:41:03 +00:00
|
|
|
}
|
|
|
|
|
2024-09-30 20:46:39 +00:00
|
|
|
var parts []string
|
2024-09-26 20:41:03 +00:00
|
|
|
if len(arrived) > 0 {
|
2024-09-30 20:46:39 +00:00
|
|
|
parts = append(parts, fmt.Sprintf("arrived: %s", listFmt(arrived)))
|
2024-09-26 20:41:03 +00:00
|
|
|
}
|
|
|
|
if len(left) > 0 {
|
2024-09-30 20:46:39 +00:00
|
|
|
parts = append(parts, fmt.Sprintf("left: %s", listFmt(left)))
|
2024-09-26 20:41:03 +00:00
|
|
|
}
|
|
|
|
if len(unchanged) > 0 {
|
2024-09-30 20:46:39 +00:00
|
|
|
adjective := "also"
|
|
|
|
if len(left) > 0 && len(arrived) == 0 {
|
|
|
|
adjective = "still"
|
|
|
|
}
|
|
|
|
parts = append(parts, fmt.Sprintf("%s there: %s", adjective, listFmt(unchanged)))
|
2024-09-26 20:41:03 +00:00
|
|
|
}
|
|
|
|
|
2024-09-30 20:46:39 +00:00
|
|
|
return strings.Join(parts, "; "), nil
|
2024-09-26 20:41:03 +00:00
|
|
|
}
|
|
|
|
|
2024-09-30 20:46:39 +00:00
|
|
|
var (
|
2024-09-30 21:55:37 +00:00
|
|
|
flagMatrixHomeserver = "matrix.org"
|
|
|
|
flagMatrixUsername = "@fafomo:matrix.org"
|
|
|
|
flagMatrixPasswordFile = "password.txt"
|
|
|
|
flagMatrixRoom string
|
|
|
|
flagBackendURL string
|
2024-09-30 20:46:39 +00:00
|
|
|
)
|
|
|
|
|
2024-09-26 20:41:03 +00:00
|
|
|
func main() {
|
2024-09-30 20:46:39 +00:00
|
|
|
flag.StringVar(&flagMatrixHomeserver, "matrix_homeserver", flagMatrixHomeserver, "Address of Matrix homeserver")
|
|
|
|
flag.StringVar(&flagMatrixUsername, "matrix_username", flagMatrixUsername, "Matrix login username")
|
2024-09-30 21:55:37 +00:00
|
|
|
flag.StringVar(&flagMatrixPasswordFile, "matrix_password_file", flagMatrixPasswordFile, "Path to file containing matrix login password")
|
2024-09-30 20:46:39 +00:00
|
|
|
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"},
|
2024-09-30 21:55:37 +00:00
|
|
|
{flagMatrixPasswordFile, "matrix_password_file"},
|
2024-09-30 20:46:39 +00:00
|
|
|
{flagMatrixRoom, "matrix_room"},
|
|
|
|
{flagBackendURL, "backend_url"},
|
|
|
|
} {
|
|
|
|
if s.value == "" {
|
|
|
|
klog.Exitf("-%s must be set", s.name)
|
|
|
|
}
|
|
|
|
}
|
2024-09-26 20:41:03 +00:00
|
|
|
|
2024-09-30 20:46:39 +00:00
|
|
|
startCtx, startCtxC := context.WithTimeout(context.Background(), 20*time.Second)
|
|
|
|
defer startCtxC()
|
2024-09-26 20:41:03 +00:00
|
|
|
|
2024-09-30 20:46:39 +00:00
|
|
|
if _, err := update(startCtx); err != nil {
|
|
|
|
klog.Exitf("Initial update failed: %v", err)
|
|
|
|
}
|
2024-09-26 20:41:03 +00:00
|
|
|
|
2024-09-30 20:46:39 +00:00
|
|
|
client, err := mautrix.NewClient(flagMatrixHomeserver, id.UserID(flagMatrixUsername), "")
|
2024-09-26 20:41:03 +00:00
|
|
|
if err != nil {
|
2024-09-30 20:46:39 +00:00
|
|
|
klog.Exitf("NewClient failed: %v", err)
|
2024-09-26 20:41:03 +00:00
|
|
|
}
|
|
|
|
|
2024-09-30 21:55:37 +00:00
|
|
|
passwordBytes, err := os.ReadFile(flagMatrixPasswordFile)
|
|
|
|
if err != nil {
|
|
|
|
klog.Exitf("Could not read password file: %v", err)
|
|
|
|
}
|
|
|
|
password := strings.TrimSpace(string(passwordBytes))
|
|
|
|
|
2024-09-30 20:46:39 +00:00
|
|
|
klog.Infof("Logging in...")
|
2024-09-26 20:41:03 +00:00
|
|
|
|
|
|
|
login, err := client.Login(startCtx, &mautrix.ReqLogin{
|
|
|
|
Type: mautrix.AuthTypePassword,
|
2024-09-30 20:46:39 +00:00
|
|
|
Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: flagMatrixUsername},
|
2024-09-30 21:55:37 +00:00
|
|
|
Password: password,
|
2024-09-26 20:41:03 +00:00
|
|
|
})
|
2024-09-30 20:46:39 +00:00
|
|
|
if err != nil {
|
|
|
|
klog.Exitf("Failed to log in: %v", err)
|
|
|
|
}
|
2024-09-26 20:41:03 +00:00
|
|
|
|
|
|
|
client.AccessToken = login.AccessToken
|
|
|
|
|
|
|
|
syncer := client.Syncer.(*mautrix.DefaultSyncer)
|
|
|
|
syncer.OnEventType(event.EventMessage, func(ctx context.Context, evt *event.Event) {
|
2024-09-30 20:46:39 +00:00
|
|
|
klog.V(1).Infof("Sender: %s, type: %s, id: %s, body: %q", evt.Sender, evt.Type, evt.ID, evt.Content.AsMessage().Body)
|
2024-09-26 20:41:03 +00:00
|
|
|
})
|
|
|
|
|
2024-09-30 20:46:39 +00:00
|
|
|
klog.Infof("Now running...")
|
2024-09-26 20:41:03 +00:00
|
|
|
|
2024-09-30 20:46:39 +00:00
|
|
|
ctx, ctxC := signal.NotifyContext(context.Background(), os.Interrupt)
|
|
|
|
defer ctxC()
|
2024-09-26 20:41:03 +00:00
|
|
|
|
2024-09-30 20:46:39 +00:00
|
|
|
// Run Matrix sync process in the background.
|
|
|
|
syncDone := make(chan struct{})
|
2024-09-26 20:41:03 +00:00
|
|
|
go func() {
|
2024-09-30 20:46:39 +00:00
|
|
|
err = client.SyncWithContext(ctx)
|
|
|
|
defer close(syncDone)
|
|
|
|
if err != nil && !errors.Is(err, ctx.Err()) {
|
|
|
|
klog.Exitf("Sync failed: %v", err)
|
2024-09-26 20:41:03 +00:00
|
|
|
}
|
|
|
|
}()
|
2024-09-30 20:22:23 +00:00
|
|
|
|
2024-09-30 20:46:39 +00:00
|
|
|
// Update space members every minute.
|
2024-09-26 20:41:03 +00:00
|
|
|
ticker := time.NewTicker(60 * time.Second)
|
2024-09-30 20:46:39 +00:00
|
|
|
process:
|
2024-09-26 20:41:03 +00:00
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-ticker.C:
|
2024-09-30 20:46:39 +00:00
|
|
|
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)
|
2024-09-26 20:41:03 +00:00
|
|
|
if err != nil {
|
2024-09-30 20:46:39 +00:00
|
|
|
klog.Errorf("Failed to send event: %v\n", err)
|
2024-09-26 20:41:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-30 20:46:39 +00:00
|
|
|
case <-ctx.Done():
|
|
|
|
break process
|
2024-09-26 20:41:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-30 20:46:39 +00:00
|
|
|
klog.Infof("Waiting for graceful sync before exiting...")
|
|
|
|
<-syncDone
|
|
|
|
klog.Infof("Done.")
|
2024-09-26 20:41:03 +00:00
|
|
|
}
|