Compare commits

...

2 commits

Author SHA1 Message Date
Serge Bazanski c437d4388e *: productionize
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)
2024-09-30 22:46:39 +02:00
Serge Bazanski f24ef59a41 run gofmt 2024-09-30 22:22:23 +02:00
4 changed files with 111 additions and 76 deletions

View file

@ -1,5 +0,0 @@
FAFOMO_HOMESERVER=matrix.org
FAFOMO_USERNAME=@fafomo:matrix.org
FAFOMO_PASSWORD=
FAFOMO_ROOM=
FAFOMO_BACKEND=

174
fafomo.go
View file

@ -2,7 +2,7 @@
// //
// based on mautrix-go example code: // based on mautrix-go example code:
// //
// Copyright (C) 2017 Tulir Asokan // Copyright (C) 2017 Tulir Asokan
// Copyright (C) 2018-2020 Luca Weiss // Copyright (C) 2018-2020 Luca Weiss
// Copyright (C) 2023 Tulir Asokan // Copyright (C) 2023 Tulir Asokan
// //
@ -16,21 +16,20 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"log" "flag"
"fmt"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"strings" "strings"
"sync"
"time" "time"
"k8s.io/klog"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
) )
var backend string
var folks map[string]struct{} var folks map[string]struct{}
type JSONTop struct { type JSONTop struct {
@ -51,31 +50,40 @@ func ircquote(s string) string {
} }
func listFmt(list []string) string { func listFmt(list []string) string {
return "[ " + strings.Join(list, " ") + " ]" var listQuoted []string
for _, l := range list {
listQuoted = append(listQuoted, ircquote(l))
}
return "[ " + strings.Join(listQuoted, " ") + " ]"
} }
func update(ctx context.Context) string { func update(ctx context.Context) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", backend, nil) req, err := http.NewRequestWithContext(ctx, "GET", flagBackendURL, nil)
if err != nil { if err != nil {
panic(err) return "", err
} }
res, err := http.DefaultClient.Do(req) res, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
panic(err) return "", err
} }
defer res.Body.Close() defer res.Body.Close()
var users JSONTop var users JSONTop
json.NewDecoder(res.Body).Decode(&users) 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 left []string
var arrived []string var arrived []string
var unchanged []string var unchanged []string
new_folks := make(map[string]struct{}) newFolks := make(map[string]struct{})
for _, user := range users.Users { for _, user := range users.Users {
new_folks[user.Login] = struct{}{} newFolks[user.Login] = struct{}{}
if _, ok := folks[user.Login]; !ok { if _, ok := folks[user.Login]; !ok {
arrived = append(arrived, user.Login) arrived = append(arrived, user.Login)
} else { } else {
@ -83,111 +91,137 @@ func update(ctx context.Context) string {
} }
} }
for user, _ := range folks { for user, _ := range folks {
if _, ok := new_folks[user]; !ok { if _, ok := newFolks[user]; !ok {
left = append(left, user) left = append(left, user)
} }
} }
folks = new_folks folks = newFolks
// no change // no change
if len(arrived) == 0 && len(left) == 0 { if len(arrived) == 0 && len(left) == 0 {
return "" return "", nil
} }
message := "" var parts []string
if len(arrived) > 0 { if len(arrived) > 0 {
message += "arrived: " + listFmt(arrived) + "; " parts = append(parts, fmt.Sprintf("arrived: %s", listFmt(arrived)))
} }
if len(left) > 0 { if len(left) > 0 {
message += "left: " + listFmt(left) + "; " parts = append(parts, fmt.Sprintf("left: %s", listFmt(left)))
} }
if len(unchanged) > 0 { if len(unchanged) > 0 {
message += "also there: " + listFmt(unchanged) + "; " adjective := "also"
if len(left) > 0 && len(arrived) == 0 {
adjective = "still"
}
parts = append(parts, fmt.Sprintf("%s there: %s", adjective, listFmt(unchanged)))
} }
return message return strings.Join(parts, "; "), nil
} }
var (
flagMatrixHomeserver = "matrix.org"
flagMatrixUsername = "@fafomo:matrix.org"
flagMatrixPassword string
flagMatrixRoom string
flagBackendURL string
)
func main() { func main() {
homeserver := os.Getenv("FAFOMO_HOMESERVER") flag.StringVar(&flagMatrixHomeserver, "matrix_homeserver", flagMatrixHomeserver, "Address of Matrix homeserver")
username := os.Getenv("FAFOMO_USERNAME") flag.StringVar(&flagMatrixUsername, "matrix_username", flagMatrixUsername, "Matrix login username")
password := os.Getenv("FAFOMO_PASSWORD") flag.StringVar(&flagMatrixPassword, "matrix_password", flagMatrixPassword, "Matrix login password")
room := os.Getenv("FAFOMO_ROOM") flag.StringVar(&flagMatrixRoom, "matrix_room", flagMatrixRoom, "Matrix room MXID")
backend = os.Getenv("FAFOMO_BACKEND") flag.StringVar(&flagBackendURL, "backend_url", flagBackendURL, "Checkinator (backend) addresss")
flag.Parse()
startCtx, _ := context.WithTimeout(context.Background(), 20*time.Second) for _, s := range []struct {
value string
update(startCtx) name string
}{
client, err := mautrix.NewClient(homeserver, id.UserID(username), "") {flagMatrixHomeserver, "matrix_homeserver"},
if err != nil { {flagMatrixUsername, "matrix_username"},
panic(err) {flagMatrixPassword, "matrix_password"},
{flagMatrixRoom, "matrix_room"},
{flagBackendURL, "backend_url"},
} {
if s.value == "" {
klog.Exitf("-%s must be set", s.name)
}
} }
log.Println("Logging in...") 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{ login, err := client.Login(startCtx, &mautrix.ReqLogin{
Type: mautrix.AuthTypePassword, Type: mautrix.AuthTypePassword,
Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: username}, Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: flagMatrixUsername},
Password: password, Password: flagMatrixPassword,
}) })
if err != nil {
klog.Exitf("Failed to log in: %v", err)
}
client.AccessToken = login.AccessToken client.AccessToken = login.AccessToken
syncer := client.Syncer.(*mautrix.DefaultSyncer) syncer := client.Syncer.(*mautrix.DefaultSyncer)
syncer.OnEventType(event.EventMessage, func(ctx context.Context, evt *event.Event) { syncer.OnEventType(event.EventMessage, func(ctx context.Context, evt *event.Event) {
// log.Info(). klog.V(1).Infof("Sender: %s, type: %s, id: %s, body: %q", evt.Sender, evt.Type, evt.ID, evt.Content.AsMessage().Body)
// 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 { klog.Infof("Now running...")
panic(err)
}
log.Println("Now running...") ctx, ctxC := signal.NotifyContext(context.Background(), os.Interrupt)
defer ctxC()
syncCtx, _ := context.WithCancel(context.Background())
syncCtx, stop := signal.NotifyContext(syncCtx, os.Interrupt)
defer stop()
var syncStopWait sync.WaitGroup
syncStopWait.Add(1)
// Run Matrix sync process in the background.
syncDone := make(chan struct{})
go func() { go func() {
err = client.SyncWithContext(syncCtx) err = client.SyncWithContext(ctx)
defer syncStopWait.Done() defer close(syncDone)
if err != nil && !errors.Is(err, context.Canceled) { if err != nil && !errors.Is(err, ctx.Err()) {
panic(err) klog.Exitf("Sync failed: %v", err)
} }
}() }()
// Update space members every minute.
ticker := time.NewTicker(60 * time.Second) ticker := time.NewTicker(60 * time.Second)
process:
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
ctx, _ := context.WithTimeout(syncCtx, 5*time.Second) ctx, _ := context.WithTimeout(ctx, 5*time.Second)
message := update(ctx) message, err := update(ctx)
if message != "" { if err != nil {
log.Printf("Sent update: %s\n", message) klog.Errorf("Update failed: %v", err)
_, err := client.SendText(ctx, id.RoomID(room), message) } else if message != "" {
klog.Infof("Sent update: %s\n", message)
_, err := client.SendText(ctx, id.RoomID(flagMatrixRoom), message)
if err != nil { if err != nil {
log.Printf("Failed to send event: %v\n", err) klog.Errorf("Failed to send event: %v\n", err)
} }
} }
case <-syncCtx.Done(): case <-ctx.Done():
return break process
} }
} }
stop() klog.Infof("Waiting for graceful sync before exiting...")
syncStopWait.Wait() <-syncDone
klog.Infof("Done.")
log.Println("Exited")
} }

5
go.mod
View file

@ -2,7 +2,10 @@ module fa-fo.de/fafomo
go 1.23.0 go 1.23.0
require maunium.net/go/mautrix v0.21.0 require (
k8s.io/klog v1.0.0
maunium.net/go/mautrix v0.21.0
)
require ( require (
filippo.io/edwards25519 v1.1.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect

3
go.sum
View file

@ -3,6 +3,7 @@ filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
@ -41,5 +42,7 @@ golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
maunium.net/go/mautrix v0.21.0 h1:Z6nVu+clkJgj6ANwFYQQ1BtYeVXZPZ9lRgwuFN57gOY= maunium.net/go/mautrix v0.21.0 h1:Z6nVu+clkJgj6ANwFYQQ1BtYeVXZPZ9lRgwuFN57gOY=
maunium.net/go/mautrix v0.21.0/go.mod h1:qm9oDhcHxF/Xby5RUuONIGpXw1SXXqLZj/GgvMxJxu0= maunium.net/go/mautrix v0.21.0/go.mod h1:qm9oDhcHxF/Xby5RUuONIGpXw1SXXqLZj/GgvMxJxu0=