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=