*: 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)
This commit is contained in:
parent
f24ef59a41
commit
c437d4388e
|
@ -1,5 +0,0 @@
|
||||||
FAFOMO_HOMESERVER=matrix.org
|
|
||||||
FAFOMO_USERNAME=@fafomo:matrix.org
|
|
||||||
FAFOMO_PASSWORD=
|
|
||||||
FAFOMO_ROOM=
|
|
||||||
FAFOMO_BACKEND=
|
|
170
fafomo.go
170
fafomo.go
|
@ -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
5
go.mod
|
@ -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
3
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/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=
|
||||||
|
|
Loading…
Reference in a new issue