diff --git a/api/api.go b/api/api.go index 01ca8c1..2b2f6de 100644 --- a/api/api.go +++ b/api/api.go @@ -63,6 +63,8 @@ type ClientConfig struct { HomeserverURL string // The matrix access token to authenticate the requests with. AccessToken string + // The device ID for this access token. + DeviceID id.DeviceID // True to start a sync stream for this user, making this a "syncing client". If false, no // /sync goroutine will be created and this client won't listen for new events from Matrix. For services // which only SEND events into Matrix, it may be desirable to set Sync to false to reduce the diff --git a/api/handlers/service.go b/api/handlers/service.go index d2450d9..cedcaef 100644 --- a/api/handlers/service.go +++ b/api/handlers/service.go @@ -16,7 +16,6 @@ import ( "github.com/matrix-org/go-neb/types" "github.com/matrix-org/util" log "github.com/sirupsen/logrus" - "maunium.net/go/mautrix" ) // ConfigureService represents an HTTP handler which can process /admin/configureService requests. @@ -225,7 +224,7 @@ func (h *GetService) OnIncomingRequest(req *http.Request) util.JSONResponse { } } -func checkClientForService(service types.Service, client *mautrix.Client) error { +func checkClientForService(service types.Service, client *clients.BotClient) error { // If there are any commands or expansions for this Service then the service user ID // MUST be a syncing client or else the Service will never get the incoming command/expansion! cmds := service.Commands(client) diff --git a/clients/bot_client.go b/clients/bot_client.go new file mode 100644 index 0000000..bcae08d --- /dev/null +++ b/clients/bot_client.go @@ -0,0 +1,143 @@ +package clients + +import ( + "time" + + "github.com/matrix-org/go-neb/api" + "github.com/matrix-org/go-neb/database" + "github.com/matrix-org/go-neb/matrix" + log "github.com/sirupsen/logrus" + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/crypto" + mevt "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" +) + +// BotClient represents one of the bot's sessions, with a specific User and Device ID. +// It can be used for sending messages and retrieving information about the rooms that +// the client has joined. +type BotClient struct { + *mautrix.Client + config api.ClientConfig + olmMachine *crypto.OlmMachine + stateStore *NebStateStore +} + +// InitOlmMachine initializes a BotClient's internal OlmMachine given a client object and a Neb store, +// which will be used to store room information. +func (botClient *BotClient) InitOlmMachine(client *mautrix.Client, nebStore *matrix.NEBStore) (err error) { + + var cryptoStore crypto.Store + cryptoLogger := CryptoMachineLogger{} + if sdb, ok := database.GetServiceDB().(*database.ServiceDB); ok { + // Create an SQL crypto store based on the ServiceDB used + db, dialect := sdb.GetSQLDb() + sqlCryptoStore := crypto.NewSQLCryptoStore(db, dialect, client.DeviceID, []byte(client.DeviceID.String()), cryptoLogger) + // Try to create the tables if they are missing + if err = sqlCryptoStore.CreateTables(); err != nil { + return + } + cryptoStore = sqlCryptoStore + cryptoLogger.Debug("Using SQL backend as the crypto store") + } else { + deviceID := client.DeviceID.String() + if deviceID == "" { + deviceID = "_empty_device_id" + } + cryptoStore, err = crypto.NewGobStore(deviceID + ".gob") + if err != nil { + return + } + cryptoLogger.Debug("Using gob storage as the crypto store") + } + + botClient.stateStore = &NebStateStore{&nebStore.InMemoryStore} + olmMachine := crypto.NewOlmMachine(client, cryptoLogger, cryptoStore, botClient.stateStore) + if err = olmMachine.Load(); err != nil { + return + } + botClient.olmMachine = olmMachine + + return nil +} + +// Register registers a BotClient's Sync and StateMember event callbacks to update its internal state +// when new events arrive. +func (botClient *BotClient) Register(syncer mautrix.ExtensibleSyncer) { + syncer.OnEventType(mevt.StateMember, func(_ mautrix.EventSource, evt *mevt.Event) { + botClient.olmMachine.HandleMemberEvent(evt) + }) + syncer.OnSync(botClient.syncCallback) +} + +func (botClient *BotClient) syncCallback(resp *mautrix.RespSync, since string) bool { + botClient.stateStore.UpdateStateStore(resp) + botClient.olmMachine.ProcessSyncResponse(resp, since) + if err := botClient.olmMachine.CryptoStore.Flush(); err != nil { + log.WithError(err).Error("Could not flush crypto store") + } + return true +} + +// DecryptMegolmEvent attempts to decrypt an incoming m.room.encrypted message using the session information +// already present in the OlmMachine. The corresponding decrypted event is then returned. +// If it fails, usually because the session is not known, an error is returned. +func (botClient *BotClient) DecryptMegolmEvent(evt *mevt.Event) (*mevt.Event, error) { + return botClient.olmMachine.DecryptMegolmEvent(evt) +} + +// SendMessageEvent sends the given content to the given room ID using this BotClient as a message event. +// If the target room has enabled encryption, a megolm session is created if one doesn't already exist +// and the message is sent after being encrypted. +func (botClient *BotClient) SendMessageEvent(roomID id.RoomID, evtType mevt.Type, content interface{}, + extra ...mautrix.ReqSendEvent) (*mautrix.RespSendEvent, error) { + + olmMachine := botClient.olmMachine + if olmMachine.StateStore.IsEncrypted(roomID) { + // Check if there is already a megolm session + if sess, err := olmMachine.CryptoStore.GetOutboundGroupSession(roomID); err != nil { + return nil, err + } else if sess == nil || sess.Expired() || !sess.Shared { + // No error but valid, shared session does not exist + memberIDs, err := botClient.stateStore.GetJoinedMembers(roomID) + if err != nil { + return nil, err + } + // Share group session with room members + if err = olmMachine.ShareGroupSession(roomID, memberIDs); err != nil { + return nil, err + } + } + enc, err := olmMachine.EncryptMegolmEvent(roomID, mevt.EventMessage, content) + if err != nil { + return nil, err + } + content = enc + evtType = mevt.EventEncrypted + } + return botClient.Client.SendMessageEvent(roomID, evtType, content, extra...) +} + +// Sync loops to keep syncing the client with the homeserver by calling the /sync endpoint. +func (botClient *BotClient) Sync() { + // Get the state store up to date + resp, err := botClient.SyncRequest(30000, "", "", true, mevt.PresenceOnline) + if err != nil { + log.WithError(err).Error("Error performing initial sync") + return + } + botClient.stateStore.UpdateStateStore(resp) + + for { + if e := botClient.Client.Sync(); e != nil { + log.WithFields(log.Fields{ + log.ErrorKey: e, + "user_id": botClient.config.UserID, + }).Error("Fatal Sync() error") + time.Sleep(10 * time.Second) + } else { + log.WithField("user_id", botClient.config.UserID).Info("Stopping Sync()") + return + } + } +} diff --git a/clients/clients.go b/clients/clients.go index fc2c285..8167cda 100644 --- a/clients/clients.go +++ b/clients/clients.go @@ -6,7 +6,6 @@ import ( "net/http" "strings" "sync" - "time" "github.com/matrix-org/go-neb/api" "github.com/matrix-org/go-neb/database" @@ -26,7 +25,7 @@ type Clients struct { httpClient *http.Client dbMutex sync.Mutex mapMutex sync.Mutex - clients map[id.UserID]clientEntry + clients map[id.UserID]BotClient } // New makes a new collection of matrix clients @@ -34,19 +33,19 @@ func New(db database.Storer, cli *http.Client) *Clients { clients := &Clients{ db: db, httpClient: cli, - clients: make(map[id.UserID]clientEntry), // user_id => clientEntry + clients: make(map[id.UserID]BotClient), // user_id => BotClient } return clients } // Client gets a client for the userID -func (c *Clients) Client(userID id.UserID) (*mautrix.Client, error) { +func (c *Clients) Client(userID id.UserID) (*BotClient, error) { entry := c.getClient(userID) - if entry.client != nil { - return entry.client, nil + if entry.Client != nil { + return &entry, nil } entry, err := c.loadClientFromDB(userID) - return entry.client, err + return &entry, err } // Update updates the config for a matrix client @@ -71,29 +70,24 @@ func (c *Clients) Start() error { return nil } -type clientEntry struct { - config api.ClientConfig - client *mautrix.Client -} - -func (c *Clients) getClient(userID id.UserID) clientEntry { +func (c *Clients) getClient(userID id.UserID) BotClient { c.mapMutex.Lock() defer c.mapMutex.Unlock() return c.clients[userID] } -func (c *Clients) setClient(client clientEntry) { +func (c *Clients) setClient(client BotClient) { c.mapMutex.Lock() defer c.mapMutex.Unlock() c.clients[client.config.UserID] = client } -func (c *Clients) loadClientFromDB(userID id.UserID) (entry clientEntry, err error) { +func (c *Clients) loadClientFromDB(userID id.UserID) (entry BotClient, err error) { c.dbMutex.Lock() defer c.dbMutex.Unlock() entry = c.getClient(userID) - if entry.client != nil { + if entry.Client != nil { return } @@ -104,7 +98,7 @@ func (c *Clients) loadClientFromDB(userID id.UserID) (entry clientEntry, err err return } - if entry.client, err = c.newClient(entry.config); err != nil { + if err = c.initClient(&entry); err != nil { return } @@ -112,12 +106,12 @@ func (c *Clients) loadClientFromDB(userID id.UserID) (entry clientEntry, err err return } -func (c *Clients) updateClientInDB(newConfig api.ClientConfig) (new clientEntry, old clientEntry, err error) { +func (c *Clients) updateClientInDB(newConfig api.ClientConfig) (new, old BotClient, err error) { c.dbMutex.Lock() defer c.dbMutex.Unlock() old = c.getClient(newConfig.UserID) - if old.client != nil && old.config == newConfig { + if old.Client != nil && old.config == newConfig { // Already have a client with that config. new = old return @@ -125,13 +119,13 @@ func (c *Clients) updateClientInDB(newConfig api.ClientConfig) (new clientEntry, new.config = newConfig - if new.client, err = c.newClient(new.config); err != nil { + if err = c.initClient(&new); err != nil { return } // set the new display name if they differ if old.config.DisplayName != new.config.DisplayName { - if err := new.client.SetDisplayName(new.config.DisplayName); err != nil { + if err := new.SetDisplayName(new.config.DisplayName); err != nil { // whine about it but don't stop: this isn't fatal. log.WithFields(log.Fields{ log.ErrorKey: err, @@ -142,12 +136,12 @@ func (c *Clients) updateClientInDB(newConfig api.ClientConfig) (new clientEntry, } if old.config, err = c.db.StoreMatrixClientConfig(new.config); err != nil { - new.client.StopSync() + new.StopSync() return } - if old.client != nil { - old.client.StopSync() + if old.Client != nil { + old.Client.StopSync() return } @@ -155,13 +149,13 @@ func (c *Clients) updateClientInDB(newConfig api.ClientConfig) (new clientEntry, return } -func (c *Clients) onMessageEvent(client *mautrix.Client, event *mevt.Event) { - services, err := c.db.LoadServicesForUser(client.UserID) +func (c *Clients) onMessageEvent(botClient *BotClient, event *mevt.Event) { + services, err := c.db.LoadServicesForUser(botClient.UserID) if err != nil { log.WithFields(log.Fields{ log.ErrorKey: err, "room_id": event.RoomID, - "service_user_id": client.UserID, + "service_user_id": botClient.UserID, }).Warn("Error loading services") } @@ -196,23 +190,22 @@ func (c *Clients) onMessageEvent(client *mautrix.Client, event *mevt.Event) { args = strings.Split(body[1:], " ") } - if response := runCommandForService(service.Commands(client), event, args); response != nil { + if response := runCommandForService(service.Commands(botClient), event, args); response != nil { responses = append(responses, response) } } else { // message isn't a command, it might need expanding - expansions := runExpansionsForService(service.Expansions(client), event, body) + expansions := runExpansionsForService(service.Expansions(botClient), event, body) responses = append(responses, expansions...) } } for _, content := range responses { - if _, err := client.SendMessageEvent(event.RoomID, mevt.EventMessage, content); err != nil { + if _, err := botClient.SendMessageEvent(event.RoomID, mevt.EventMessage, content); err != nil { log.WithFields(log.Fields{ - log.ErrorKey: err, - "room_id": event.RoomID, - "user_id": event.Sender, - "content": content, - }).Print("Failed to send command response") + "room_id": event.RoomID, + "content": content, + "sender": event.Sender, + }).WithError(err).Error("Failed to send command response") } } } @@ -341,61 +334,102 @@ func (c *Clients) onRoomMemberEvent(client *mautrix.Client, event *mevt.Event) { } } -func (c *Clients) newClient(config api.ClientConfig) (*mautrix.Client, error) { +func (c *Clients) initClient(botClient *BotClient) error { + config := botClient.config client, err := mautrix.NewClient(config.HomeserverURL, config.UserID, config.AccessToken) if err != nil { - return nil, err + return err } + client.Client = c.httpClient + client.DeviceID = config.DeviceID + if client.DeviceID == "" { + log.Warn("Device ID is not set which will result in E2E encryption/decryption not working") + } + botClient.Client = client + syncer := client.Syncer.(*mautrix.DefaultSyncer) + nebStore := &matrix.NEBStore{ InMemoryStore: *mautrix.NewInMemoryStore(), Database: c.db, ClientConfig: config, } client.Store = nebStore - syncer.Store = nebStore // TODO: Check that the access token is valid for the userID by peforming // a request against the server. - syncer.OnEventType(mevt.EventMessage, func(event *mevt.Event) { - c.onMessageEvent(client, event) + if err = botClient.InitOlmMachine(client, nebStore); err != nil { + return err + } + + // Register sync callback for maintaining the state store and Olm machine state + botClient.Register(syncer) + + syncer.OnEventType(mevt.EventMessage, func(_ mautrix.EventSource, event *mevt.Event) { + c.onMessageEvent(botClient, event) }) - syncer.OnEventType(mevt.Type{Type: "m.room.bot.options", Class: mevt.UnknownEventType}, func(event *mevt.Event) { - c.onBotOptionsEvent(client, event) + syncer.OnEventType(mevt.Type{Type: "m.room.bot.options", Class: mevt.UnknownEventType}, func(_ mautrix.EventSource, event *mevt.Event) { + c.onBotOptionsEvent(botClient.Client, event) }) if config.AutoJoinRooms { - syncer.OnEventType(mevt.StateMember, func(event *mevt.Event) { + syncer.OnEventType(mevt.StateMember, func(_ mautrix.EventSource, event *mevt.Event) { c.onRoomMemberEvent(client, event) }) } + // When receiving an encrypted event, attempt to decrypt it using the BotClient's capabilities. + // If successfully decrypted propagate the decrypted event to the clients. + syncer.OnEventType(mevt.EventEncrypted, func(source mautrix.EventSource, evt *mevt.Event) { + if err := evt.Content.ParseRaw(mevt.EventEncrypted); err != nil { + log.WithError(err).Error("Failed to parse encrypted message") + return + } + encContent := evt.Content.AsEncrypted() + decrypted, err := botClient.DecryptMegolmEvent(evt) + if err != nil { + log.WithFields(log.Fields{ + "user_id": config.UserID, + "device_id": encContent.DeviceID, + "session_id": encContent.SessionID, + "sender_key": encContent.SenderKey, + }).WithError(err).Error("Failed to decrypt message") + } else { + if decrypted.Type == mevt.EventMessage { + err = decrypted.Content.ParseRaw(mevt.EventMessage) + if err != nil { + log.WithError(err).Error("Could not parse decrypted message event") + } else { + c.onMessageEvent(botClient, decrypted) + } + } + log.WithFields(log.Fields{ + "type": evt.Type, + "sender": evt.Sender, + "room_id": evt.RoomID, + "state_key": evt.StateKey, + }).Trace("Decrypted event successfully") + } + }) + + // Ignore events before neb's join event. + eventIgnorer := mautrix.OldEventIgnorer{UserID: config.UserID} + eventIgnorer.Register(syncer) + log.WithFields(log.Fields{ "user_id": config.UserID, + "device_id": config.DeviceID, "sync": config.Sync, "auto_join_rooms": config.AutoJoinRooms, "since": nebStore.LoadNextBatch(config.UserID), }).Info("Created new client") if config.Sync { - go func() { - for { - if e := client.Sync(); e != nil { - log.WithFields(log.Fields{ - log.ErrorKey: e, - "user_id": config.UserID, - }).Error("Fatal Sync() error") - time.Sleep(10 * time.Second) - } else { - log.WithField("user_id", config.UserID).Info("Stopping Sync()") - return - } - } - }() + go botClient.Sync() } - return client, nil + return nil } diff --git a/clients/clients_test.go b/clients/clients_test.go index d000f4e..d133fe8 100644 --- a/clients/clients_test.go +++ b/clients/clients_test.go @@ -30,7 +30,7 @@ type MockService struct { commands []types.Command } -func (s *MockService) Commands(cli *mautrix.Client) []types.Command { +func (s *MockService) Commands(cli types.MatrixClient) []types.Command { return s.commands } @@ -76,6 +76,7 @@ func TestCommandParsing(t *testing.T) { clients := New(&store, cli) mxCli, _ := mautrix.NewClient("https://someplace.somewhere", "@service:user", "token") mxCli.Client = cli + botClient := BotClient{Client: mxCli} for _, input := range commandParseTests { executedCmdArgs = []string{} @@ -94,7 +95,7 @@ func TestCommandParsing(t *testing.T) { RoomID: "!foo:bar", Content: content, } - clients.onMessageEvent(mxCli, &event) + clients.onMessageEvent(&botClient, &event) if !reflect.DeepEqual(executedCmdArgs, input.expectArgs) { t.Errorf("TestCommandParsing want %s, got %s", input.expectArgs, executedCmdArgs) } diff --git a/clients/crypto_logger.go b/clients/crypto_logger.go new file mode 100644 index 0000000..75026e7 --- /dev/null +++ b/clients/crypto_logger.go @@ -0,0 +1,28 @@ +package clients + +import ( + log "github.com/sirupsen/logrus" +) + +// CryptoMachineLogger wraps around the usual logger, implementing the Logger interface needed by OlmMachine. +type CryptoMachineLogger struct{} + +// Error formats and logs an error message. +func (CryptoMachineLogger) Error(message string, args ...interface{}) { + log.Errorf(message, args...) +} + +// Warn formats and logs a warning message. +func (CryptoMachineLogger) Warn(message string, args ...interface{}) { + log.Warnf(message, args...) +} + +// Debug formats and logs a debug message. +func (CryptoMachineLogger) Debug(message string, args ...interface{}) { + log.Debugf(message, args...) +} + +// Trace formats and logs a trace message. +func (CryptoMachineLogger) Trace(message string, args ...interface{}) { + log.Tracef(message, args...) +} diff --git a/clients/state_store.go b/clients/state_store.go new file mode 100644 index 0000000..fa95a74 --- /dev/null +++ b/clients/state_store.go @@ -0,0 +1,79 @@ +package clients + +import ( + "errors" + + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" +) + +// NebStateStore implements the StateStore interface for OlmMachine. +// It is used to determine which rooms are encrypted and which rooms are shared with a user. +// The state is updated by /sync responses. +type NebStateStore struct { + Storer *mautrix.InMemoryStore +} + +// IsEncrypted returns whether a room has been encrypted. +func (ss *NebStateStore) IsEncrypted(roomID id.RoomID) bool { + room := ss.Storer.LoadRoom(roomID) + if room == nil { + return false + } + _, ok := room.State[event.StateEncryption] + return ok +} + +// FindSharedRooms returns a list of room IDs that the given user ID is also a member of. +func (ss *NebStateStore) FindSharedRooms(userID id.UserID) []id.RoomID { + sharedRooms := make([]id.RoomID, 0) + for roomID, room := range ss.Storer.Rooms { + if room.GetMembershipState(userID) != event.MembershipLeave { + sharedRooms = append(sharedRooms, roomID) + } + } + return sharedRooms +} + +// UpdateStateStore updates the internal state of NebStateStore from a /sync response. +func (ss *NebStateStore) UpdateStateStore(resp *mautrix.RespSync) { + for roomID, evts := range resp.Rooms.Join { + room := ss.Storer.LoadRoom(roomID) + if room == nil { + room = mautrix.NewRoom(roomID) + ss.Storer.SaveRoom(room) + } + for _, i := range evts.State.Events { + room.UpdateState(i) + } + for _, i := range evts.Timeline.Events { + if i.Type.IsState() { + room.UpdateState(i) + } + } + } +} + +// GetJoinedMembers returns a list of members that are currently in a room. +func (ss *NebStateStore) GetJoinedMembers(roomID id.RoomID) ([]id.UserID, error) { + joinedMembers := make([]id.UserID, 0) + room := ss.Storer.LoadRoom(roomID) + if room == nil { + return nil, errors.New("unknown roomID") + } + memberEvents := room.State[event.StateMember] + if memberEvents == nil { + return nil, errors.New("no state member events found") + } + for stateKey, stateEvent := range memberEvents { + if stateEvent == nil { + continue + } + stateEvent.Content.ParseRaw(event.StateMember) + if stateEvent.Content.AsMember().Membership == event.MembershipJoin { + joinedMembers = append(joinedMembers, id.UserID(stateKey)) + } + } + return joinedMembers, nil +} diff --git a/database/db.go b/database/db.go index 6551650..a050dfd 100644 --- a/database/db.go +++ b/database/db.go @@ -13,7 +13,8 @@ import ( // A ServiceDB stores the configuration for the services type ServiceDB struct { - db *sql.DB + db *sql.DB + dialect string } // A single global instance of the service DB. @@ -44,7 +45,7 @@ func Open(databaseType, databaseURL string) (serviceDB *ServiceDB, err error) { // https://github.com/mattn/go-sqlite3/issues/274 db.SetMaxOpenConns(1) } - serviceDB = &ServiceDB{db: db} + serviceDB = &ServiceDB{db: db, dialect: databaseType} return } @@ -327,6 +328,11 @@ func (d *ServiceDB) InsertFromConfig(cfg *api.ConfigFile) error { return nil } +// GetSQLDb retrieves the SQL database instance of a ServiceDB and the dialect it uses (sqlite3 or postgres). +func (d *ServiceDB) GetSQLDb() (*sql.DB, string) { + return d.db, d.dialect +} + func runTransaction(db *sql.DB, fn func(txn *sql.Tx) error) (err error) { txn, err := db.Begin() if err != nil { diff --git a/go.mod b/go.mod index 24d8f26..6ef508e 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/json-iterator/go v1.1.9 // indirect github.com/julienschmidt/httprouter v1.2.0 // indirect github.com/kr/pretty v0.1.0 // indirect - github.com/lib/pq v1.3.0 + github.com/lib/pq v1.7.0 github.com/matrix-org/dugong v0.0.0-20180820122854-51a565b5666b github.com/matrix-org/util v0.0.0-20190711121626-527ce5ddefc7 github.com/mattn/go-shellwords v1.0.10 @@ -45,12 +45,11 @@ require ( github.com/sirupsen/logrus v1.4.2 github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect golang.org/dl v0.0.0-20200601221412-a954fa24b3e5 // indirect - golang.org/x/net v0.0.0-20200505041828-1ed23360d12c + golang.org/x/net v0.0.0-20200602114024-627f9648deb9 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d golang.org/x/tools v0.0.0-20200311090712-aafaee8bce8c // indirect gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect - gopkg.in/yaml.v2 v2.2.8 - maunium.net/go/gomuks v0.1.0 - maunium.net/go/mautrix v0.4.7 + gopkg.in/yaml.v2 v2.3.0 + maunium.net/go/mautrix v0.5.5 ) diff --git a/go.sum b/go.sum index 982e162..d896bb6 100644 --- a/go.sum +++ b/go.sum @@ -79,6 +79,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kyokomi/emoji v2.2.2+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA= github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.7.0 h1:h93mCPfUSkaul3Ka/VG8uZdmW1uMHDGxzu0NWHuJmHY= +github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lithammer/fuzzysearch v1.1.0/go.mod h1:Bqx4wo8lTOFcJr3ckpY6HA9lEIOO0H5HrkJ5CsN56HQ= github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/matrix-org/dugong v0.0.0-20180820122854-51a565b5666b h1:xpcmnpfUImRC4O2SAS/dmTcJENDXvGmLUzey76V1R3Q= @@ -94,6 +96,7 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw= github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.13.0 h1:LnJI81JidiW9r7pS/hXe6cFeO5EXNq7KbfvoJLRI69c= +github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= @@ -173,6 +176,7 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tidwall/gjson v1.6.0 h1:9VEQWz6LLMUsUl6PueE49ir4Ka6CzLymOAZDxpFsTDc= github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc= @@ -207,8 +211,11 @@ golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200505041828-1ed23360d12c h1:zJ0mtu4jCalhKg6Oaukv6iIkb+cOvDrajDH9DH46Q4M= golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM= +golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -224,6 +231,7 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3 h1:5B6i6EAiSYyejWfvc5Rc9BbI3rzIsrrXfAQBWnYfn+w= golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -251,11 +259,16 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -maunium.net/go/gomuks v0.1.0 h1:Ra0But5Cr3UfCSzd456x8MzV6jXLisZG0gW7RZY1xnk= -maunium.net/go/gomuks v0.1.0/go.mod h1:UWB7mC4OcKINQ2+ygalRSsSvnROoiwdSxdVLrWTeco4= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/maulogger/v2 v2.1.1/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A= maunium.net/go/mautrix v0.4.2/go.mod h1:8Y+NqmROJyWYvvP4yPfX9tLM59VCfgE/kcQ0SeX68ho= maunium.net/go/mautrix v0.4.7 h1:jpclbeGcuiHPIWZFZhQJoxgZKP9f+9OLBPtcDNMFV/o= maunium.net/go/mautrix v0.4.7/go.mod h1:8Y+NqmROJyWYvvP4yPfX9tLM59VCfgE/kcQ0SeX68ho= +maunium.net/go/mautrix v0.5.0-rc.1 h1:Ux2rwQfgf044lXjzCXmaY6VVJPcLioNzdN3okbOVIlg= +maunium.net/go/mautrix v0.5.0-rc.1/go.mod h1:LnkFnB1yjCbb8V+upoEHDGvI/F38NHSTWYCe2RRJgSY= +maunium.net/go/mautrix v0.5.5 h1:e0Pql1FdxoNUudx2oXo1gZHMrqIh5MC72cdXEPIrYLA= +maunium.net/go/mautrix v0.5.5/go.mod h1:FLbMANzwqlsX2Fgm7SDe+E4I3wSa4UxJRKqS5wGkCwA= maunium.net/go/mauview v0.1.1/go.mod h1:3QBUiuLct9moP1LgDhCGIg0Ovxn38Bd2sGndnUOuj4o= maunium.net/go/tcell v0.2.0/go.mod h1:9Apcb3lNNS6C6lCqKT9UFp7BTRzHXfWE+/tgufsAMho= diff --git a/goneb.go b/goneb.go index ed3ea67..8812af6 100644 --- a/goneb.go +++ b/goneb.go @@ -139,7 +139,7 @@ func insertServicesFromConfig(clis *clients.Clients, serviceReqs []api.Configure } func loadDatabase(databaseType, databaseURL, configYAML string) (*database.ServiceDB, error) { - if configYAML != "" { + if databaseType == "" && databaseURL == "" { databaseType = "sqlite3" databaseURL = ":memory:?_busy_timeout=5000" } diff --git a/services/alertmanager/alertmanager.go b/services/alertmanager/alertmanager.go index a02e969..9f12fe5 100644 --- a/services/alertmanager/alertmanager.go +++ b/services/alertmanager/alertmanager.go @@ -13,7 +13,6 @@ import ( "github.com/matrix-org/go-neb/database" "github.com/matrix-org/go-neb/types" log "github.com/sirupsen/logrus" - "maunium.net/go/mautrix" mevt "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" ) @@ -78,7 +77,7 @@ type WebhookNotification struct { } // OnReceiveWebhook receives requests from Alertmanager and sends requests to Matrix as a result. -func (s *Service) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli *mautrix.Client) { +func (s *Service) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli types.MatrixClient) { decoder := json.NewDecoder(req.Body) var notif WebhookNotification if err := decoder.Decode(¬if); err != nil { @@ -144,7 +143,7 @@ func (s *Service) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli } // Register makes sure the Config information supplied is valid. -func (s *Service) Register(oldService types.Service, client *mautrix.Client) error { +func (s *Service) Register(oldService types.Service, client types.MatrixClient) error { s.WebhookURL = s.webhookEndpointURL for _, templates := range s.Rooms { // validate that we have at least a plain text template @@ -191,13 +190,12 @@ func (s *Service) PostRegister(oldService types.Service) { } } -func (s *Service) joinRooms(client *mautrix.Client) { +func (s *Service) joinRooms(client types.MatrixClient) { for roomID := range s.Rooms { if _, err := client.JoinRoom(roomID.String(), "", nil); err != nil { log.WithFields(log.Fields{ log.ErrorKey: err, "room_id": roomID, - "user_id": client.UserID, }).Error("Failed to join room") } } diff --git a/services/alertmanager/alertmanager_test.go b/services/alertmanager/alertmanager_test.go index e752144..d0a9983 100644 --- a/services/alertmanager/alertmanager_test.go +++ b/services/alertmanager/alertmanager_test.go @@ -93,7 +93,7 @@ func TestNotify(t *testing.T) { } } -func buildTestClient(msgs *[]mevt.MessageEventContent) *mautrix.Client { +func buildTestClient(msgs *[]mevt.MessageEventContent) types.MatrixClient { matrixTrans := struct{ testutils.MockTransport }{} matrixTrans.RT = func(req *http.Request) (*http.Response, error) { if !strings.Contains(req.URL.String(), "/send/m.room.message") { diff --git a/services/echo/echo.go b/services/echo/echo.go index ca9b3a0..daf45f6 100644 --- a/services/echo/echo.go +++ b/services/echo/echo.go @@ -5,7 +5,6 @@ import ( "strings" "github.com/matrix-org/go-neb/types" - "maunium.net/go/mautrix" mevt "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" ) @@ -21,9 +20,9 @@ type Service struct { // Commands supported: // !echo some message // Responds with a notice of "some message". -func (e *Service) Commands(cli *mautrix.Client) []types.Command { +func (e *Service) Commands(cli types.MatrixClient) []types.Command { return []types.Command{ - types.Command{ + { Path: []string{"echo"}, Command: func(roomID id.RoomID, userID id.UserID, args []string) (interface{}, error) { return &mevt.MessageEventContent{ diff --git a/services/giphy/giphy.go b/services/giphy/giphy.go index 0fd58ce..583b0fb 100644 --- a/services/giphy/giphy.go +++ b/services/giphy/giphy.go @@ -11,7 +11,6 @@ import ( "github.com/matrix-org/go-neb/types" log "github.com/sirupsen/logrus" - "maunium.net/go/mautrix" "maunium.net/go/mautrix/event" mevt "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" @@ -61,7 +60,7 @@ type Service struct { // Commands supported: // !giphy some search query without quotes // Responds with a suitable GIF into the same room as the command. -func (s *Service) Commands(client *mautrix.Client) []types.Command { +func (s *Service) Commands(client types.MatrixClient) []types.Command { return []types.Command{ types.Command{ Path: []string{"giphy"}, @@ -72,7 +71,7 @@ func (s *Service) Commands(client *mautrix.Client) []types.Command { } } -func (s *Service) cmdGiphy(client *mautrix.Client, roomID id.RoomID, userID id.UserID, args []string) (interface{}, error) { +func (s *Service) cmdGiphy(client types.MatrixClient, roomID id.RoomID, userID id.UserID, args []string) (interface{}, error) { // only 1 arg which is the text to search for. query := strings.Join(args, " ") gifResult, err := s.searchGiphy(query) diff --git a/services/github/github.go b/services/github/github.go index 702eda6..91b1515 100644 --- a/services/github/github.go +++ b/services/github/github.go @@ -22,7 +22,6 @@ import ( "github.com/matrix-org/go-neb/services/github/client" "github.com/matrix-org/go-neb/types" log "github.com/sirupsen/logrus" - "maunium.net/go/mautrix" mevt "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" ) @@ -563,7 +562,7 @@ func (s *Service) expandCommit(roomID id.RoomID, userID id.UserID, owner, repo, // Responds with the outcome of the issue comment creation request. This command requires // a Github account to be linked to the Matrix user ID issuing the command. If there // is no link, it will return a Starter Link instead. -func (s *Service) Commands(cli *mautrix.Client) []types.Command { +func (s *Service) Commands(cli types.MatrixClient) []types.Command { return []types.Command{ { Path: []string{"github", "search"}, @@ -632,7 +631,7 @@ func (s *Service) Commands(cli *mautrix.Client) []types.Command { // it will also expand strings of the form: // #12 // using the default repository. -func (s *Service) Expansions(cli *mautrix.Client) []types.Expansion { +func (s *Service) Expansions(cli types.MatrixClient) []types.Expansion { return []types.Expansion{ types.Expansion{ Regexp: ownerRepoIssueRegex, @@ -718,7 +717,7 @@ func (s *Service) Expansions(cli *mautrix.Client) []types.Expansion { } // Register makes sure that the given realm ID maps to a github realm. -func (s *Service) Register(oldService types.Service, client *mautrix.Client) error { +func (s *Service) Register(oldService types.Service, client types.MatrixClient) error { if s.RealmID == "" { return fmt.Errorf("RealmID is required") } diff --git a/services/github/github_webhook.go b/services/github/github_webhook.go index 4c54d2e..6e43b00 100644 --- a/services/github/github_webhook.go +++ b/services/github/github_webhook.go @@ -13,7 +13,6 @@ import ( "github.com/matrix-org/go-neb/services/github/webhook" "github.com/matrix-org/go-neb/types" log "github.com/sirupsen/logrus" - "maunium.net/go/mautrix" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" ) @@ -82,7 +81,7 @@ type WebhookService struct { // // If the "owner/repo" string doesn't exist in this Service config, then the webhook will be deleted from // Github. -func (s *WebhookService) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli *mautrix.Client) { +func (s *WebhookService) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli types.MatrixClient) { evType, repo, msg, err := webhook.OnReceiveRequest(req, s.SecretToken) if err != nil { w.WriteHeader(err.Code) @@ -146,7 +145,7 @@ func (s *WebhookService) OnReceiveWebhook(w http.ResponseWriter, req *http.Reque // // Hooks can get out of sync if a user manually deletes a hook in the Github UI. In this case, toggling the repo configuration will // force NEB to recreate the hook. -func (s *WebhookService) Register(oldService types.Service, client *mautrix.Client) error { +func (s *WebhookService) Register(oldService types.Service, client types.MatrixClient) error { if s.RealmID == "" || s.ClientUserID == "" { return fmt.Errorf("RealmID and ClientUserID is required") } @@ -252,7 +251,7 @@ func (s *WebhookService) PostRegister(oldService types.Service) { } } -func (s *WebhookService) joinWebhookRooms(client *mautrix.Client) error { +func (s *WebhookService) joinWebhookRooms(client types.MatrixClient) error { for roomID := range s.Rooms { if _, err := client.JoinRoom(roomID.String(), "", nil); err != nil { // TODO: Leave the rooms we successfully joined? diff --git a/services/google/google.go b/services/google/google.go index 6d456f2..64719b9 100644 --- a/services/google/google.go +++ b/services/google/google.go @@ -13,7 +13,6 @@ import ( "github.com/matrix-org/go-neb/types" log "github.com/sirupsen/logrus" - "maunium.net/go/mautrix" mevt "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" ) @@ -70,7 +69,7 @@ type Service struct { // Commands supported: // !google image some_search_query_without_quotes // Responds with a suitable image into the same room as the command. -func (s *Service) Commands(client *mautrix.Client) []types.Command { +func (s *Service) Commands(client types.MatrixClient) []types.Command { return []types.Command{ { Path: []string{"google", "image"}, @@ -101,7 +100,8 @@ func usageMessage() *mevt.MessageEventContent { } } -func (s *Service) cmdGoogleImgSearch(client *mautrix.Client, roomID id.RoomID, userID id.UserID, args []string) (interface{}, error) { +func (s *Service) cmdGoogleImgSearch(client types.MatrixClient, roomID id.RoomID, userID id.UserID, + args []string) (interface{}, error) { if len(args) < 1 { return usageMessage(), nil diff --git a/services/guggy/guggy.go b/services/guggy/guggy.go index 33b8020..6c1b9c7 100644 --- a/services/guggy/guggy.go +++ b/services/guggy/guggy.go @@ -12,7 +12,6 @@ import ( "github.com/matrix-org/go-neb/types" log "github.com/sirupsen/logrus" - "maunium.net/go/mautrix" mevt "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" ) @@ -51,7 +50,7 @@ type Service struct { // Commands supported: // !guggy some search query without quotes // Responds with a suitable GIF into the same room as the command. -func (s *Service) Commands(client *mautrix.Client) []types.Command { +func (s *Service) Commands(client types.MatrixClient) []types.Command { return []types.Command{ { Path: []string{"guggy"}, @@ -61,7 +60,7 @@ func (s *Service) Commands(client *mautrix.Client) []types.Command { }, } } -func (s *Service) cmdGuggy(client *mautrix.Client, roomID id.RoomID, userID id.UserID, args []string) (interface{}, error) { +func (s *Service) cmdGuggy(client types.MatrixClient, roomID id.RoomID, userID id.UserID, args []string) (interface{}, error) { // only 1 arg which is the text to search for. querySentence := strings.Join(args, " ") gifResult, err := s.text2gifGuggy(querySentence) diff --git a/services/imgur/imgur.go b/services/imgur/imgur.go index 4193cdb..af3dc71 100644 --- a/services/imgur/imgur.go +++ b/services/imgur/imgur.go @@ -12,7 +12,6 @@ import ( "github.com/matrix-org/go-neb/types" log "github.com/sirupsen/logrus" - "maunium.net/go/mautrix" mevt "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" ) @@ -116,7 +115,7 @@ type Service struct { // Commands supported: // !imgur some_search_query_without_quotes // Responds with a suitable image into the same room as the command. -func (s *Service) Commands(client *mautrix.Client) []types.Command { +func (s *Service) Commands(client types.MatrixClient) []types.Command { return []types.Command{ { Path: []string{"imgur", "help"}, @@ -142,7 +141,7 @@ func usageMessage() *mevt.MessageEventContent { } // Search Imgur for a relevant image and upload it to matrix -func (s *Service) cmdImgSearch(client *mautrix.Client, roomID id.RoomID, userID id.UserID, args []string) (interface{}, error) { +func (s *Service) cmdImgSearch(client types.MatrixClient, roomID id.RoomID, userID id.UserID, args []string) (interface{}, error) { // Check for query text if len(args) < 1 { return usageMessage(), nil diff --git a/services/jira/jira.go b/services/jira/jira.go index 9d1b0f6..91e3400 100644 --- a/services/jira/jira.go +++ b/services/jira/jira.go @@ -21,7 +21,6 @@ import ( "github.com/matrix-org/go-neb/services/utils" "github.com/matrix-org/go-neb/types" log "github.com/sirupsen/logrus" - "maunium.net/go/mautrix" mevt "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" ) @@ -76,7 +75,7 @@ type Service struct { // Register ensures that the given realm IDs are valid JIRA realms and registers webhooks // with those JIRA endpoints. -func (s *Service) Register(oldService types.Service, client *mautrix.Client) error { +func (s *Service) Register(oldService types.Service, client types.MatrixClient) error { // We only ever make 1 JIRA webhook which listens for all projects and then filter // on receive. So we simply need to know if we need to make a webhook or not. We // need to do this for each unique realm. @@ -242,7 +241,7 @@ func (s *Service) expandIssue(roomID id.RoomID, userID id.UserID, issueKeyGroups // same project key, which project is chosen is undefined. If there // is no JIRA account linked to the Matrix user ID, it will return a Starter Link // if there is a known public project with that project key. -func (s *Service) Commands(cli *mautrix.Client) []types.Command { +func (s *Service) Commands(cli types.MatrixClient) []types.Command { return []types.Command{ types.Command{ Path: []string{"jira", "create"}, @@ -259,9 +258,9 @@ func (s *Service) Commands(cli *mautrix.Client) []types.Command { // to map the project key to a realm, and subsequently the JIRA endpoint to hit. // If there are multiple projects with the same project key in the Service Config, one will // be chosen arbitrarily. -func (s *Service) Expansions(cli *mautrix.Client) []types.Expansion { +func (s *Service) Expansions(cli types.MatrixClient) []types.Expansion { return []types.Expansion{ - types.Expansion{ + { Regexp: issueKeyRegex, Expand: func(roomID id.RoomID, userID id.UserID, issueKeyGroups []string) interface{} { return s.expandIssue(roomID, userID, issueKeyGroups) @@ -271,7 +270,7 @@ func (s *Service) Expansions(cli *mautrix.Client) []types.Expansion { } // OnReceiveWebhook receives requests from JIRA and possibly sends requests to Matrix as a result. -func (s *Service) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli *mautrix.Client) { +func (s *Service) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli types.MatrixClient) { eventProjectKey, event, httpErr := webhook.OnReceiveRequest(req) if httpErr != nil { log.Print("Failed to handle JIRA webhook") diff --git a/services/rssbot/rssbot.go b/services/rssbot/rssbot.go index 251c282..c339975 100644 --- a/services/rssbot/rssbot.go +++ b/services/rssbot/rssbot.go @@ -19,7 +19,6 @@ import ( "github.com/mmcdole/gofeed" "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" - "maunium.net/go/mautrix" mevt "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" ) @@ -102,7 +101,7 @@ type Service struct { } // Register will check the liveness of each RSS feed given. If all feeds check out okay, no error is returned. -func (s *Service) Register(oldService types.Service, client *mautrix.Client) error { +func (s *Service) Register(oldService types.Service, client types.MatrixClient) error { if len(s.Feeds) == 0 { // this is an error UNLESS the old service had some feeds in which case they are deleting us :( var numOldFeeds int @@ -131,7 +130,7 @@ func (s *Service) Register(oldService types.Service, client *mautrix.Client) err return nil } -func (s *Service) joinRooms(client *mautrix.Client) { +func (s *Service) joinRooms(client types.MatrixClient) { roomSet := make(map[id.RoomID]bool) for _, feedInfo := range s.Feeds { for _, roomID := range feedInfo.Rooms { @@ -144,7 +143,6 @@ func (s *Service) joinRooms(client *mautrix.Client) { log.WithFields(log.Fields{ log.ErrorKey: err, "room_id": roomID, - "user_id": client.UserID, }).Error("Failed to join room") } } @@ -175,7 +173,7 @@ func (s *Service) PostRegister(oldService types.Service) { // - Else if there is a Title field, use it as the GUID. // // Returns a timestamp representing when this Service should have OnPoll called again. -func (s *Service) OnPoll(cli *mautrix.Client) time.Time { +func (s *Service) OnPoll(cli types.MatrixClient) time.Time { logger := log.WithFields(log.Fields{ "service_id": s.ServiceID(), "service_type": s.ServiceType(), @@ -408,7 +406,7 @@ func (s *Service) newItems(feedURL string, allItems []*gofeed.Item) (items []gof return } -func (s *Service) sendToRooms(cli *mautrix.Client, feedURL string, feed *gofeed.Feed, item gofeed.Item) error { +func (s *Service) sendToRooms(cli types.MatrixClient, feedURL string, feed *gofeed.Feed, item gofeed.Item) error { logger := log.WithFields(log.Fields{ "feed_url": feedURL, "title": item.Title, diff --git a/services/slackapi/slackapi.go b/services/slackapi/slackapi.go index 327d4ad..75eb3b7 100644 --- a/services/slackapi/slackapi.go +++ b/services/slackapi/slackapi.go @@ -6,7 +6,6 @@ import ( "github.com/matrix-org/go-neb/types" log "github.com/sirupsen/logrus" - "maunium.net/go/mautrix" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" ) @@ -37,7 +36,7 @@ type Service struct { // to Matrix as a result. // // This requires that the WebhookURL is given to an outgoing slack webhook (see https://api.slack.com/outgoing-webhooks) -func (s *Service) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli *mautrix.Client) { +func (s *Service) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli types.MatrixClient) { segments := strings.Split(req.URL.Path, "/") if len(segments) < 2 { @@ -72,13 +71,12 @@ func (s *Service) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli } // Register joins the configured room and sets the public WebhookURL -func (s *Service) Register(oldService types.Service, client *mautrix.Client) error { +func (s *Service) Register(oldService types.Service, client types.MatrixClient) error { s.WebhookURL = s.webhookEndpointURL if _, err := client.JoinRoom(s.RoomID.String(), "", nil); err != nil { log.WithFields(log.Fields{ log.ErrorKey: err, "room_id": s.RoomID, - "user_id": client.UserID, }).Error("Failed to join room") } return nil diff --git a/services/travisci/travisci.go b/services/travisci/travisci.go index 837389c..b6b7b24 100644 --- a/services/travisci/travisci.go +++ b/services/travisci/travisci.go @@ -13,7 +13,6 @@ import ( "github.com/matrix-org/go-neb/database" "github.com/matrix-org/go-neb/types" log "github.com/sirupsen/logrus" - "maunium.net/go/mautrix" mevt "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" ) @@ -180,7 +179,7 @@ func outputForTemplate(travisTmpl string, tmpl map[string]string) (out string) { // webhooks: http://go-neb-endpoint.com/notifications // // See https://docs.travis-ci.com/user/notifications#Webhook-notifications for more information. -func (s *Service) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli *mautrix.Client) { +func (s *Service) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli types.MatrixClient) { if err := req.ParseForm(); err != nil { log.WithError(err).Error("Failed to read incoming Travis-CI webhook form") w.WriteHeader(400) @@ -243,7 +242,7 @@ func (s *Service) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli } // Register makes sure the Config information supplied is valid. -func (s *Service) Register(oldService types.Service, client *mautrix.Client) error { +func (s *Service) Register(oldService types.Service, client types.MatrixClient) error { s.WebhookURL = s.webhookEndpointURL for _, roomData := range s.Rooms { for repo := range roomData.Repos { @@ -275,13 +274,12 @@ func (s *Service) PostRegister(oldService types.Service) { } } -func (s *Service) joinRooms(client *mautrix.Client) { +func (s *Service) joinRooms(client types.MatrixClient) { for roomID := range s.Rooms { if _, err := client.JoinRoom(roomID.String(), "", nil); err != nil { log.WithFields(log.Fields{ log.ErrorKey: err, "room_id": roomID, - "user_id": client.UserID, }).Error("Failed to join room") } } diff --git a/services/wikipedia/wikipedia.go b/services/wikipedia/wikipedia.go index ae3fa2f..b99138b 100644 --- a/services/wikipedia/wikipedia.go +++ b/services/wikipedia/wikipedia.go @@ -12,7 +12,6 @@ import ( "github.com/jaytaylor/html2text" "github.com/matrix-org/go-neb/types" log "github.com/sirupsen/logrus" - "maunium.net/go/mautrix" mevt "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" ) @@ -51,7 +50,7 @@ type Service struct { // Commands supported: // !wikipedia some_search_query_without_quotes // Responds with a suitable article extract and link to the referenced page into the same room as the command. -func (s *Service) Commands(client *mautrix.Client) []types.Command { +func (s *Service) Commands(client types.MatrixClient) []types.Command { return []types.Command{ { Path: []string{"wikipedia"}, @@ -70,7 +69,7 @@ func usageMessage() *mevt.MessageEventContent { } } -func (s *Service) cmdWikipediaSearch(client *mautrix.Client, roomID id.RoomID, userID id.UserID, args []string) (interface{}, error) { +func (s *Service) cmdWikipediaSearch(client types.MatrixClient, roomID id.RoomID, userID id.UserID, args []string) (interface{}, error) { // Check for query text if len(args) < 1 { return usageMessage(), nil diff --git a/types/service.go b/types/service.go index a4e1105..5f208d0 100644 --- a/types/service.go +++ b/types/service.go @@ -9,6 +9,7 @@ import ( "time" "maunium.net/go/mautrix" + "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" ) @@ -24,7 +25,18 @@ type BotOptions struct { type Poller interface { // OnPoll is called when the poller should poll. Return the timestamp when you want to be polled again. // Return 0 to never be polled again. - OnPoll(client *mautrix.Client) time.Time + OnPoll(client MatrixClient) time.Time +} + +// MatrixClient represents an object that can communicate with a Matrix server in certain ways that services require. +type MatrixClient interface { + // Join a room by ID or alias. Content can optionally specify the request body. + JoinRoom(roomIDorAlias, serverName string, content interface{}) (resp *mautrix.RespJoinRoom, err error) + // Send a message event to a room. + SendMessageEvent(roomID id.RoomID, eventType event.Type, contentJSON interface{}, + extra ...mautrix.ReqSendEvent) (resp *mautrix.RespSendEvent, err error) + // Upload an HTTP URL. + UploadLink(link string) (*mautrix.RespMediaUpload, error) } // A Service is the configuration for a bot service. @@ -35,14 +47,14 @@ type Service interface { ServiceID() string // Return the type of service. This string MUST NOT change. ServiceType() string - Commands(cli *mautrix.Client) []Command - Expansions(cli *mautrix.Client) []Expansion - OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli *mautrix.Client) + Commands(cli MatrixClient) []Command + Expansions(cli MatrixClient) []Expansion + OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli MatrixClient) // A lifecycle function which is invoked when the service is being registered. The old service, if one exists, is provided, // along with a Client instance for ServiceUserID(). If this function returns an error, the service will not be registered // or persisted to the database, and the user's request will fail. This can be useful if you depend on external factors // such as registering webhooks. - Register(oldService Service, client *mautrix.Client) error + Register(oldService Service, client MatrixClient) error // A lifecycle function which is invoked after the service has been successfully registered and persisted to the database. // This function is invoked within the critical section for configuring services, guaranteeing that there will not be // concurrent modifications to this service whilst this function executes. This lifecycle hook should be used to clean @@ -83,23 +95,23 @@ func (s *DefaultService) ServiceType() string { } // Commands returns no commands. -func (s *DefaultService) Commands(cli *mautrix.Client) []Command { +func (s *DefaultService) Commands(cli MatrixClient) []Command { return []Command{} } // Expansions returns no expansions. -func (s *DefaultService) Expansions(cli *mautrix.Client) []Expansion { +func (s *DefaultService) Expansions(cli MatrixClient) []Expansion { return []Expansion{} } // Register does nothing and returns no error. -func (s *DefaultService) Register(oldService Service, client *mautrix.Client) error { return nil } +func (s *DefaultService) Register(oldService Service, cli MatrixClient) error { return nil } // PostRegister does nothing. func (s *DefaultService) PostRegister(oldService Service) {} // OnReceiveWebhook does nothing but 200 OK the request. -func (s *DefaultService) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli *mautrix.Client) { +func (s *DefaultService) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli MatrixClient) { w.WriteHeader(200) // Do nothing }