From c437d4388e4ad32aab6376d9e133a9b9d20e753c Mon Sep 17 00:00:00 2001 From: Serge Bazanski Date: Mon, 30 Sep 2024 22:46:39 +0200 Subject: [PATCH] *: 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) --- dotenv.template | 5 -- fafomo.go | 170 +++++++++++++++++++++++++++++------------------- go.mod | 5 +- go.sum | 3 + 4 files changed, 109 insertions(+), 74 deletions(-) delete mode 100644 dotenv.template diff --git a/dotenv.template b/dotenv.template deleted file mode 100644 index b0719c9..0000000 --- a/dotenv.template +++ /dev/null @@ -1,5 +0,0 @@ -FAFOMO_HOMESERVER=matrix.org -FAFOMO_USERNAME=@fafomo:matrix.org -FAFOMO_PASSWORD= -FAFOMO_ROOM= -FAFOMO_BACKEND= diff --git a/fafomo.go b/fafomo.go index 66bcdaf..2c519c8 100644 --- a/fafomo.go +++ b/fafomo.go @@ -16,21 +16,20 @@ import ( "context" "encoding/json" "errors" - "log" + "flag" + "fmt" "net/http" "os" "os/signal" "strings" - "sync" "time" + "k8s.io/klog" "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 { @@ -51,31 +50,40 @@ func ircquote(s 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 { - req, err := http.NewRequestWithContext(ctx, "GET", backend, nil) +func update(ctx context.Context) (string, error) { + req, err := http.NewRequestWithContext(ctx, "GET", flagBackendURL, nil) if err != nil { - panic(err) + return "", err } res, err := http.DefaultClient.Do(req) if err != nil { - panic(err) + return "", err } defer res.Body.Close() 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 arrived []string var unchanged []string - new_folks := make(map[string]struct{}) + newFolks := make(map[string]struct{}) for _, user := range users.Users { - new_folks[user.Login] = struct{}{} + newFolks[user.Login] = struct{}{} if _, ok := folks[user.Login]; !ok { arrived = append(arrived, user.Login) } else { @@ -83,111 +91,137 @@ func update(ctx context.Context) string { } } for user, _ := range folks { - if _, ok := new_folks[user]; !ok { + if _, ok := newFolks[user]; !ok { left = append(left, user) } } - folks = new_folks + folks = newFolks // no change if len(arrived) == 0 && len(left) == 0 { - return "" + return "", nil } - message := "" + var parts []string if len(arrived) > 0 { - message += "arrived: " + listFmt(arrived) + "; " + parts = append(parts, fmt.Sprintf("arrived: %s", listFmt(arrived))) } if len(left) > 0 { - message += "left: " + listFmt(left) + "; " + parts = append(parts, fmt.Sprintf("left: %s", listFmt(left))) } 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() { - 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") + 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() - startCtx, _ := context.WithTimeout(context.Background(), 20*time.Second) - - update(startCtx) - - client, err := mautrix.NewClient(homeserver, id.UserID(username), "") - if err != nil { - panic(err) + 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) + } } - 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{ Type: mautrix.AuthTypePassword, - Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: username}, - Password: password, + 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) { - // 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") + klog.V(1).Infof("Sender: %s, type: %s, id: %s, body: %q", evt.Sender, evt.Type, evt.ID, evt.Content.AsMessage().Body) }) - if err != nil { - panic(err) - } + klog.Infof("Now running...") - 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) + 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(syncCtx) - defer syncStopWait.Done() - if err != nil && !errors.Is(err, context.Canceled) { - panic(err) + 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(syncCtx, 5*time.Second) + ctx, _ := context.WithTimeout(ctx, 5*time.Second) - message := update(ctx) - if message != "" { - log.Printf("Sent update: %s\n", message) - _, err := client.SendText(ctx, id.RoomID(room), message) + 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 { - log.Printf("Failed to send event: %v\n", err) + klog.Errorf("Failed to send event: %v\n", err) } } - case <-syncCtx.Done(): - return + case <-ctx.Done(): + break process } } - stop() - syncStopWait.Wait() - - log.Println("Exited") + klog.Infof("Waiting for graceful sync before exiting...") + <-syncDone + klog.Infof("Done.") } diff --git a/go.mod b/go.mod index 9e6cc4e..6dc1278 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,10 @@ module fa-fo.de/fafomo 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 ( filippo.io/edwards25519 v1.1.0 // indirect diff --git a/go.sum b/go.sum index 530232c..83ea584 100644 --- a/go.sum +++ b/go.sum @@ -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/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/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= 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/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= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 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/go.mod h1:qm9oDhcHxF/Xby5RUuONIGpXw1SXXqLZj/GgvMxJxu0=