diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3997bea --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.db \ No newline at end of file diff --git a/bot/bot.go b/bot/bot.go new file mode 100644 index 0000000..684d756 --- /dev/null +++ b/bot/bot.go @@ -0,0 +1,138 @@ +package bot + +import ( + _ "github.com/mattn/go-sqlite3" + "golang.org/x/exp/slog" + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/crypto/cryptohelper" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/format" + "maunium.net/go/mautrix/id" +) + +type MatrixConfig struct { + Homeserver string + UserID string + UserAccessKey string + UserPassword string + RoomID string + DBPath string + Pickle string + AcceptInvites bool +} + +type Bot struct { + config MatrixConfig + client *mautrix.Client + cryptoHelper *cryptohelper.CryptoHelper + kagiCient *Kagi + logger *slog.Logger +} + +func NewBot(cfg MatrixConfig, kagi *Kagi, logger *slog.Logger) *Bot { + return &Bot{ + config: cfg, + kagiCient: kagi, + logger: logger, + } +} + +func (m *Bot) Init() error { + client, err := mautrix.NewClient(m.config.Homeserver, id.UserID(m.config.UserID), m.config.UserAccessKey) + if err != nil { + return err + } + var oei mautrix.OldEventIgnorer + oei.Register(client.Syncer.(mautrix.ExtensibleSyncer)) + m.client = client + m.cryptoHelper, err = cryptohelper.NewCryptoHelper(client, []byte(m.config.Pickle), m.config.DBPath) + if err != nil { + return err + } + m.cryptoHelper.LoginAs = &mautrix.ReqLogin{ + Type: mautrix.AuthTypePassword, + Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: m.config.UserID}, + Password: m.config.UserPassword, + } + if err := m.cryptoHelper.Init(); err != nil { + return err + } + m.client.Crypto = m.cryptoHelper + + if m.config.AcceptInvites { + m.AddEventHandler(m.InviteHandler()) + } + + return nil +} + +func (m *Bot) Run() error { + if err := m.client.Sync(); err != nil { + return err + } + + return nil +} + +func (m *Bot) Close() error { + if err := m.client.Sync(); err != nil { + return err + } + if err := m.cryptoHelper.Close(); err != nil { + return err + } + + return nil +} + +func (m *Bot) AddEventHandler(eventType event.Type, handler mautrix.EventHandler) { + syncer := m.client.Syncer.(*mautrix.DefaultSyncer) + syncer.OnEventType(eventType, handler) +} + +func (m *Bot) InviteHandler() (event.Type, mautrix.EventHandler) { + return event.StateMember, func(source mautrix.EventSource, evt *event.Event) { + if evt.GetStateKey() == m.client.UserID.String() && evt.Content.AsMember().Membership == event.MembershipInvite && evt.RoomID.String() == m.config.RoomID { + _, err := m.client.JoinRoomByID(evt.RoomID) + if err != nil { + m.logger.Error("failed to join room after invite", slog.String("err", err.Error()), slog.String("room_id", evt.RoomID.String()), slog.String("inviter", evt.Sender.String())) + return + } + + m.logger.Info("joined room after invite", slog.String("room_id", evt.RoomID.String()), slog.String("inviter", evt.Sender.String())) + } + } +} + +func (m *Bot) ResponseHandler() (event.Type, mautrix.EventHandler) { + return event.EventMessage, func(source mautrix.EventSource, evt *event.Event) { + content := evt.Content.AsMessage() + eventID := evt.ID + m.logger.Info("received message", slog.String("content", content.Body)) + + // get reply from GPT + //reply, err := m.gptClient.Complete(conv) + //if err != nil { + // m.logger.Error("failed to get reply from openai", slog.String("err", err.Error()), slog.String("bot", m.config.UserDisplayName)) + // return + //} + + reply := "the summary" + formattedReply := format.RenderMarkdown(reply, true, false) + formattedReply.RelatesTo = &event.RelatesTo{ + InReplyTo: &event.InReplyTo{ + EventID: eventID, + }, + } + _, err := m.client.SendMessageEvent(evt.RoomID, event.EventMessage, &formattedReply) + if err != nil { + m.logger.Error("failed to send message", slog.String("err", err.Error())) + return + } + + if len(reply) > 30 { + reply = reply[:30] + "..." + } + m.logger.Info("sent reply", slog.String("parent_id", eventID.String()), slog.String("content", reply)) + } +} diff --git a/kagi.go b/bot/kagi.go similarity index 98% rename from kagi.go rename to bot/kagi.go index 7c9a829..16d2b1d 100644 --- a/kagi.go +++ b/bot/kagi.go @@ -1,4 +1,4 @@ -package main +package bot import ( "fmt" diff --git a/go.mod b/go.mod index 663084a..ba50f9f 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,20 @@ module ewintr.nl/matrix-kagisum go 1.20 -require golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect +require ( + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mattn/go-sqlite3 v1.14.17 // indirect + github.com/rs/zerolog v1.29.1 // indirect + github.com/tidwall/gjson v1.14.4 // 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 + github.com/yuin/goldmark v1.5.4 // indirect + golang.org/x/crypto v0.10.0 // indirect + golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect + golang.org/x/net v0.11.0 // indirect + golang.org/x/sys v0.9.0 // indirect + maunium.net/go/maulogger/v2 v2.4.1 // indirect + maunium.net/go/mautrix v0.15.3 // indirect +) diff --git a/go.sum b/go.sum index 001575f..42e77a0 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,45 @@ +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.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +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.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc= +github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/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= +github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU= +github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= +golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= +golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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/maulogger/v2 v2.4.1 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8= +maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho= +maunium.net/go/mautrix v0.15.3 h1:C9BHSUM0gYbuZmAtopuLjIcH5XHLb/ZjTEz7nN+0jN0= +maunium.net/go/mautrix v0.15.3/go.mod h1:zLrQqdxJlLkurRCozTc9CL6FySkgZlO/kpCYxBILSLE= diff --git a/main.go b/main.go index 0552d04..a6ca888 100644 --- a/main.go +++ b/main.go @@ -1,23 +1,52 @@ package main import ( + "ewintr.nl/matrix-kagisum/bot" "fmt" + "golang.org/x/exp/slog" "os" + "os/signal" ) func main() { - //logger := slog.New(slog.NewTextHandler(os.Stderr)) + logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) apiKey := getParam("KAGI_API_KEY", "") fmt.Println(apiKey) - k := NewKagi("https://kagi.com/api/v0", apiKey) + kagi := bot.NewKagi("https://kagi.com/api/v0", apiKey) - res, err := k.Summarize("https://ewintr.nl/shitty-ssg/why-i-built-my-own-shitty-static-site-generator/") - if err != nil { - fmt.Println(err) + //res, err := kagi.Summarize("https://ewintr.nl/shitty-ssg/why-i-built-my-own-shitty-static-site-generator/") + //if err != nil { + // fmt.Println(err) + //} + + mtrxConf := bot.MatrixConfig{ + Homeserver: getParam("MATRIX_HOMESERVER", "http://localhost/"), + UserID: getParam("MATRIX_USER_ID", "@user:localhost"), + UserAccessKey: getParam("MATRIX_USER_ACCESS_KEY", "secret"), + UserPassword: getParam("MATRIX_USER_PASSWORD", "secret"), + RoomID: getParam("MATRIX_ROOM_ID", "!room:localhost"), + DBPath: getParam("MATRIX_DB_PATH", "matrix.db"), + Pickle: getParam("MATRIX_PICKLE", "matrix.pickle"), + AcceptInvites: getParam("MATRIX_ACCEPT_INVITES", "false") == "true", } - fmt.Println(res) + bot := bot.NewBot(mtrxConf, kagi, logger) + if err := bot.Init(); err != nil { + logger.Error("error running matrix bot: %v", slog.String("error", err.Error())) + os.Exit(1) + } + + go bot.Run() + defer bot.Close() + logger.Info("matrix bot started") + + done := make(chan os.Signal) + signal.Notify(done, os.Interrupt) + <-done + + logger.Info("matrix bot stopped") + } func getParam(param, def string) string {