commit 385f5dda97709256d06b1eee7afa327637f575d0 Author: Leah Neukirchen Date: Thu Sep 26 22:41:03 2024 +0200 initial commit of fafomo, the FAFO Matrix Bot diff --git a/dotenv.template b/dotenv.template new file mode 100644 index 0000000..b0719c9 --- /dev/null +++ b/dotenv.template @@ -0,0 +1,5 @@ +FAFOMO_HOMESERVER=matrix.org +FAFOMO_USERNAME=@fafomo:matrix.org +FAFOMO_PASSWORD= +FAFOMO_ROOM= +FAFOMO_BACKEND= diff --git a/fafomo.go b/fafomo.go new file mode 100644 index 0000000..e1d9067 --- /dev/null +++ b/fafomo.go @@ -0,0 +1,193 @@ +// Copyright (C) 2024 Leah Neukirchen +// +// based on mautrix-go example code: +// +// Copyright (C) 2017 Tulir Asokan +// 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" + "log" + "net/http" + "os" + "os/signal" + "strings" + "sync" + "time" + + "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 { + 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 { + return "[ " + strings.Join(list, " ") + " ]" +} + +func update(ctx context.Context) string { + req, err := http.NewRequestWithContext(ctx, "GET", backend, nil) + if err != nil { + panic(err) + } + res, err := http.DefaultClient.Do(req) + if err != nil { + panic(err) + } + defer res.Body.Close() + + var users JSONTop + + json.NewDecoder(res.Body).Decode(&users) + + var left []string + var arrived []string + var unchanged []string + + new_folks := make(map[string]struct{}) + for _, user := range users.Users { + new_folks[user.Login] = struct{}{} + if _, ok := folks[user.Login]; !ok { + arrived = append(arrived, user.Login) + } else { + unchanged = append(unchanged, user.Login) + } + } + for user, _ := range folks { + if _, ok := new_folks[user]; !ok { + left = append(left, user) + } + } + + folks = new_folks + + // no change + if len(arrived) == 0 && len(left) == 0 { + return "" + } + + message := "" + if len(arrived) > 0 { + message += "arrived: " + listFmt(arrived) + "; " + } + if len(left) > 0 { + message += "left: " + listFmt(left) + "; " + } + if len(unchanged) > 0 { + message += "also there: " + listFmt(unchanged) + "; " + } + + return message +} + +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") + + startCtx, _ := context.WithTimeout(context.Background(), 20*time.Second) + + update(startCtx) + + client, err := mautrix.NewClient(homeserver, id.UserID(username), "") + if err != nil { + panic(err) + } + + log.Println("Logging in...") + + login, err := client.Login(startCtx, &mautrix.ReqLogin{ + Type: mautrix.AuthTypePassword, + Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: username}, + Password: password, + }) + + 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") + }) + + if err != nil { + panic(err) + } + + 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) + + go func() { + err = client.SyncWithContext(syncCtx) + defer syncStopWait.Done() + if err != nil && !errors.Is(err, context.Canceled) { + panic(err) + } + }() + + ticker := time.NewTicker(60 * time.Second) + for { + select { + case <-ticker.C: + ctx, _ := context.WithTimeout(syncCtx, 5*time.Second) + + message := update(ctx) + if message != "" { + log.Printf("Sent update: %s\n", message) + _, err := client.SendText(ctx, id.RoomID(room), message) + if err != nil { + log.Printf("Failed to send event: %v\n", err) + } + } + + case <-syncCtx.Done(): + return + } + } + + stop() + syncStopWait.Wait() + + log.Println("Exited") +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9e6cc4e --- /dev/null +++ b/go.mod @@ -0,0 +1,21 @@ +module fa-fo.de/fafomo + +go 1.23.0 + +require maunium.net/go/mautrix v0.21.0 + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/rs/zerolog v1.33.0 // indirect + github.com/tidwall/gjson v1.17.3 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/tidwall/sjson v1.2.5 // indirect + go.mau.fi/util v0.8.0 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect + golang.org/x/net v0.29.0 // indirect + golang.org/x/sys v0.25.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..530232c --- /dev/null +++ b/go.sum @@ -0,0 +1,45 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +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/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= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94= +github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +go.mau.fi/util v0.8.0 h1:MiSny8jgQq4XtCLAT64gDJhZVhqiDeMVIEBDFVw+M0g= +go.mau.fi/util v0.8.0/go.mod h1:1Ixb8HWoVbl3rT6nAX6nV4iMkzn7KU/KXwE0Rn5RmsQ= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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= +maunium.net/go/mautrix v0.21.0 h1:Z6nVu+clkJgj6ANwFYQQ1BtYeVXZPZ9lRgwuFN57gOY= +maunium.net/go/mautrix v0.21.0/go.mod h1:qm9oDhcHxF/Xby5RUuONIGpXw1SXXqLZj/GgvMxJxu0=