From c5f58afe71eb601bc9fdc963f7a328d3a6ec408f Mon Sep 17 00:00:00 2001 From: Gary Kramlich Date: Wed, 20 Apr 2022 06:01:26 -0500 Subject: [PATCH 01/15] End to bridge encryption implementation So far this is passing my basic tests, but could use some testing from people that are much more familiar with how this is supposed to work. Refs #27 --- bridge/bridge.go | 31 ++ bridge/commands.go | 11 +- bridge/crypto.go | 339 ++++++++++++++++++ bridge/matrix.go | 119 ++++++ bridge/portal.go | 77 +++- bridge/user.go | 3 + config/bridge.go | 2 + config/encryption.go | 29 ++ database/cryptostore.go | 97 +++++ .../08-add-crypto-store-to-database.sql | 3 + .../09-add-account_id-to-crypto-store.sql | 3 + ...d-megolm-withheld-data-to-crypto-store.sql | 3 + ...add-cross-signing-keys-to-crypto-store.sql | 3 + ...rchar-with-text-in-the-crypto-database.sql | 4 + ...ted-and-last_decrypted-in-crypto-store.sql | 4 + ...4-add-encrypted-column-to-portal-table.sql | 1 + database/migrations/migrations.go | 109 ++++-- database/portal.go | 16 +- database/portalquery.go | 16 +- database/sqlstatestore.go | 28 ++ go.mod | 4 + go.sum | 4 + 22 files changed, 848 insertions(+), 58 deletions(-) create mode 100644 bridge/crypto.go create mode 100644 config/encryption.go create mode 100644 database/cryptostore.go create mode 100644 database/migrations/08-add-crypto-store-to-database.sql create mode 100644 database/migrations/09-add-account_id-to-crypto-store.sql create mode 100644 database/migrations/10-add-megolm-withheld-data-to-crypto-store.sql create mode 100644 database/migrations/11-add-cross-signing-keys-to-crypto-store.sql create mode 100644 database/migrations/12-replace-varchar-with-text-in-the-crypto-database.sql create mode 100644 database/migrations/13-split-last_used-into-last_encrypted-and-last_decrypted-in-crypto-store.sql create mode 100644 database/migrations/14-add-encrypted-column-to-portal-table.sql diff --git a/bridge/bridge.go b/bridge/bridge.go index c1b09a9..31c5463 100644 --- a/bridge/bridge.go +++ b/bridge/bridge.go @@ -48,6 +48,8 @@ type Bridge struct { puppetsLock sync.Mutex StateStore *database.SQLStateStore + + crypto Crypto } func New(cfg *config.Config) (*Bridge, error) { @@ -104,6 +106,8 @@ func New(cfg *config.Config) (*Bridge, error) { StateStore: stateStore, } + bridge.crypto = NewCryptoHelper(bridge) + if cfg.Appservice.Provisioning.Enabled() { bridge.provisioning = newProvisioningAPI(bridge) } @@ -151,6 +155,13 @@ func (b *Bridge) Start() error { return err } + if b.crypto != nil { + if err := b.crypto.Init(); err != nil { + b.log.Fatalln("Error initializing end-to-bridge encryption:", err) + return err + } + } + b.log.Debugln("Starting application service HTTP server") go b.as.Start() @@ -159,6 +170,10 @@ func (b *Bridge) Start() error { go b.updateBotProfile() + if b.crypto != nil { + go b.crypto.Start() + } + go b.startUsers() // Finally tell the appservice we're ready @@ -168,5 +183,21 @@ func (b *Bridge) Start() error { } func (b *Bridge) Stop() { + if b.crypto != nil { + b.crypto.Stop() + } + + b.as.Stop() + b.eventProcessor.Stop() + + for _, user := range b.usersByMXID { + if user.Session == nil { + continue + } + + b.log.Debugln("Disconnecting", user.MXID) + user.Session.Close() + } + b.log.Infoln("Bridge stopped") } diff --git a/bridge/commands.go b/bridge/commands.go index 2799e7f..83363b6 100644 --- a/bridge/commands.go +++ b/bridge/commands.go @@ -3,6 +3,7 @@ package bridge import ( "context" "fmt" + "time" "github.com/alecthomas/kong" @@ -33,11 +34,17 @@ func (g *globals) reply(msg string) { content.MsgType = event.MsgNotice intent := g.bot - if g.portal != nil && g.portal.IsPrivateChat() { + if g.portal == nil { + g.handler.log.Errorfln("we don't have a portal for this command") + + return + } + + if g.portal.IsPrivateChat() { intent = g.portal.MainIntent() } - _, err := intent.SendMessageEvent(g.roomID, event.EventMessage, content) + _, err := g.portal.sendMatrixMessage(intent, event.EventMessage, &content, nil, time.Now().UTC().UnixMilli()) if err != nil { g.handler.log.Warnfln("Failed to reply to command from %q: %v", g.user.MXID, err) } diff --git a/bridge/crypto.go b/bridge/crypto.go new file mode 100644 index 0000000..7c12eb3 --- /dev/null +++ b/bridge/crypto.go @@ -0,0 +1,339 @@ +package bridge + +import ( + "fmt" + "runtime/debug" + "time" + + "maunium.net/go/maulogger/v2" + + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/crypto" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" + + "gitlab.com/beeper/discord/database" +) + +var NoSessionFound = crypto.NoSessionFound + +var levelTrace = maulogger.Level{ + Name: "TRACE", + Severity: -10, + Color: -1, +} + +type Crypto interface { + HandleMemberEvent(*event.Event) + Decrypt(*event.Event) (*event.Event, error) + Encrypt(id.RoomID, event.Type, event.Content) (*event.EncryptedEventContent, error) + WaitForSession(id.RoomID, id.SenderKey, id.SessionID, time.Duration) bool + RequestSession(id.RoomID, id.SenderKey, id.SessionID, id.UserID, id.DeviceID) + ResetSession(id.RoomID) + Init() error + Start() + Stop() +} + +type CryptoHelper struct { + bridge *Bridge + client *mautrix.Client + mach *crypto.OlmMachine + store *database.SQLCryptoStore + log maulogger.Logger + baseLog maulogger.Logger +} + +func NewCryptoHelper(bridge *Bridge) Crypto { + if !bridge.Config.Bridge.Encryption.Allow { + bridge.log.Debugln("Bridge built with end-to-bridge encryption, but disabled in config") + return nil + } + + baseLog := bridge.log.Sub("Crypto") + return &CryptoHelper{ + bridge: bridge, + log: baseLog.Sub("Helper"), + baseLog: baseLog, + } +} + +func (helper *CryptoHelper) Init() error { + helper.log.Debugln("Initializing end-to-bridge encryption...") + + helper.store = database.NewSQLCryptoStore(helper.bridge.db, helper.bridge.as.BotMXID(), + fmt.Sprintf("@%s:%s", helper.bridge.Config.Bridge.FormatUsername("%"), helper.bridge.as.HomeserverDomain)) + + var err error + helper.client, err = helper.loginBot() + if err != nil { + return err + } + + helper.log.Debugln("Logged in as bridge bot with device ID", helper.client.DeviceID) + + logger := &cryptoLogger{helper.baseLog} + stateStore := &cryptoStateStore{helper.bridge} + helper.mach = crypto.NewOlmMachine(helper.client, logger, helper.store, stateStore) + helper.mach.AllowKeyShare = helper.allowKeyShare + + helper.client.Syncer = &cryptoSyncer{helper.mach} + helper.client.Store = &cryptoClientStore{helper.store} + + return helper.mach.Load() +} + +func (helper *CryptoHelper) allowKeyShare(device *crypto.DeviceIdentity, info event.RequestedKeyInfo) *crypto.KeyShareRejection { + cfg := helper.bridge.Config.Bridge.Encryption.KeySharing + if !cfg.Allow { + return &crypto.KeyShareRejectNoResponse + } else if device.Trust == crypto.TrustStateBlacklisted { + return &crypto.KeyShareRejectBlacklisted + } else if device.Trust == crypto.TrustStateVerified || !cfg.RequireVerification { + portal := helper.bridge.GetPortalByMXID(info.RoomID) + if portal == nil { + helper.log.Debugfln("Rejecting key request for %s from %s/%s: room is not a portal", info.SessionID, device.UserID, device.DeviceID) + + return &crypto.KeyShareRejection{Code: event.RoomKeyWithheldUnavailable, Reason: "Requested room is not a portal room"} + } + user := helper.bridge.GetUserByMXID(device.UserID) + // FIXME reimplement IsInPortal + if !user.Admin /*&& !user.IsInPortal(portal.Key)*/ { + helper.log.Debugfln("Rejecting key request for %s from %s/%s: user is not in portal", info.SessionID, device.UserID, device.DeviceID) + + return &crypto.KeyShareRejection{Code: event.RoomKeyWithheldUnauthorized, Reason: "You're not in that portal"} + } + helper.log.Debugfln("Accepting key request for %s from %s/%s", info.SessionID, device.UserID, device.DeviceID) + + return nil + } + + return &crypto.KeyShareRejectUnverified +} + +func (helper *CryptoHelper) loginBot() (*mautrix.Client, error) { + deviceID := helper.store.FindDeviceID() + if len(deviceID) > 0 { + helper.log.Debugln("Found existing device ID for bot in database:", deviceID) + } + + client, err := mautrix.NewClient(helper.bridge.as.HomeserverURL, "", "") + if err != nil { + return nil, fmt.Errorf("failed to initialize client: %w", err) + } + + client.Logger = helper.baseLog.Sub("Bot") + client.Client = helper.bridge.as.HTTPClient + client.DefaultHTTPRetries = helper.bridge.as.DefaultHTTPRetries + flows, err := client.GetLoginFlows() + if err != nil { + return nil, fmt.Errorf("failed to get supported login flows: %w", err) + } + + flow := flows.FirstFlowOfType(mautrix.AuthTypeAppservice, mautrix.AuthTypeHalfyAppservice) + if flow == nil { + return nil, fmt.Errorf("homeserver does not support appservice login") + } + + // We set the API token to the AS token here to authenticate the appservice login + // It'll get overridden after the login + client.AccessToken = helper.bridge.as.Registration.AppToken + resp, err := client.Login(&mautrix.ReqLogin{ + Type: flow.Type, + Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: string(helper.bridge.as.BotMXID())}, + DeviceID: deviceID, + InitialDeviceDisplayName: "Discord Bridge", + StoreCredentials: true, + }) + if err != nil { + return nil, fmt.Errorf("failed to log in as bridge bot: %w", err) + } + + helper.store.DeviceID = resp.DeviceID + + return client, nil +} + +func (helper *CryptoHelper) Start() { + helper.log.Debugln("Starting syncer for receiving to-device messages") + + err := helper.client.Sync() + if err != nil { + helper.log.Errorln("Fatal error syncing:", err) + } else { + helper.log.Infoln("Bridge bot to-device syncer stopped without error") + } +} + +func (helper *CryptoHelper) Stop() { + helper.log.Debugln("CryptoHelper.Stop() called, stopping bridge bot sync") + helper.client.StopSync() +} + +func (helper *CryptoHelper) Decrypt(evt *event.Event) (*event.Event, error) { + return helper.mach.DecryptMegolmEvent(evt) +} + +func (helper *CryptoHelper) Encrypt(roomID id.RoomID, evtType event.Type, content event.Content) (*event.EncryptedEventContent, error) { + encrypted, err := helper.mach.EncryptMegolmEvent(roomID, evtType, &content) + + if err != nil { + if err != crypto.SessionExpired && err != crypto.SessionNotShared && err != crypto.NoGroupSession { + return nil, err + } + + helper.log.Debugfln("Got %v while encrypting event for %s, sharing group session and trying again...", err, roomID) + users, err := helper.store.GetRoomMembers(roomID) + if err != nil { + return nil, fmt.Errorf("failed to get room member list: %w", err) + } + + err = helper.mach.ShareGroupSession(roomID, users) + if err != nil { + return nil, fmt.Errorf("failed to share group session: %w", err) + } + + encrypted, err = helper.mach.EncryptMegolmEvent(roomID, evtType, &content) + if err != nil { + return nil, fmt.Errorf("failed to encrypt event after re-sharing group session: %w", err) + } + } + + return encrypted, nil +} + +func (helper *CryptoHelper) WaitForSession(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID, timeout time.Duration) bool { + return helper.mach.WaitForSession(roomID, senderKey, sessionID, timeout) +} + +func (helper *CryptoHelper) RequestSession(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID, userID id.UserID, deviceID id.DeviceID) { + err := helper.mach.SendRoomKeyRequest(roomID, senderKey, sessionID, "", map[id.UserID][]id.DeviceID{userID: {deviceID}}) + if err != nil { + helper.log.Warnfln("Failed to send key request to %s/%s for %s in %s: %v", userID, deviceID, sessionID, roomID, err) + } else { + helper.log.Debugfln("Sent key request to %s/%s for %s in %s", userID, deviceID, sessionID, roomID) + } +} + +func (helper *CryptoHelper) ResetSession(roomID id.RoomID) { + err := helper.mach.CryptoStore.RemoveOutboundGroupSession(roomID) + if err != nil { + helper.log.Debugfln("Error manually removing outbound group session in %s: %v", roomID, err) + } +} + +func (helper *CryptoHelper) HandleMemberEvent(evt *event.Event) { + helper.mach.HandleMemberEvent(evt) +} + +type cryptoSyncer struct { + *crypto.OlmMachine +} + +func (syncer *cryptoSyncer) ProcessResponse(resp *mautrix.RespSync, since string) error { + done := make(chan struct{}) + go func() { + defer func() { + if err := recover(); err != nil { + syncer.Log.Error("Processing sync response (%s) panicked: %v\n%s", since, err, debug.Stack()) + } + done <- struct{}{} + }() + syncer.Log.Trace("Starting sync response handling (%s)", since) + syncer.ProcessSyncResponse(resp, since) + syncer.Log.Trace("Successfully handled sync response (%s)", since) + }() + + select { + case <-done: + case <-time.After(30 * time.Second): + syncer.Log.Warn("Handling sync response (%s) is taking unusually long", since) + } + + return nil +} + +func (syncer *cryptoSyncer) OnFailedSync(_ *mautrix.RespSync, err error) (time.Duration, error) { + syncer.Log.Error("Error /syncing, waiting 10 seconds: %v", err) + + return 10 * time.Second, nil +} + +func (syncer *cryptoSyncer) GetFilterJSON(_ id.UserID) *mautrix.Filter { + everything := []event.Type{{Type: "*"}} + + return &mautrix.Filter{ + Presence: mautrix.FilterPart{NotTypes: everything}, + AccountData: mautrix.FilterPart{NotTypes: everything}, + Room: mautrix.RoomFilter{ + IncludeLeave: false, + Ephemeral: mautrix.FilterPart{NotTypes: everything}, + AccountData: mautrix.FilterPart{NotTypes: everything}, + State: mautrix.FilterPart{NotTypes: everything}, + Timeline: mautrix.FilterPart{NotTypes: everything}, + }, + } +} + +type cryptoLogger struct { + int maulogger.Logger +} + +func (c *cryptoLogger) Error(message string, args ...interface{}) { + c.int.Errorfln(message, args...) +} + +func (c *cryptoLogger) Warn(message string, args ...interface{}) { + c.int.Warnfln(message, args...) +} + +func (c *cryptoLogger) Debug(message string, args ...interface{}) { + c.int.Debugfln(message, args...) +} + +func (c *cryptoLogger) Trace(message string, args ...interface{}) { + c.int.Logfln(levelTrace, message, args...) +} + +type cryptoClientStore struct { + int *database.SQLCryptoStore +} + +func (c cryptoClientStore) SaveFilterID(_ id.UserID, _ string) {} +func (c cryptoClientStore) LoadFilterID(_ id.UserID) string { return "" } +func (c cryptoClientStore) SaveRoom(_ *mautrix.Room) {} +func (c cryptoClientStore) LoadRoom(_ id.RoomID) *mautrix.Room { return nil } + +func (c cryptoClientStore) SaveNextBatch(_ id.UserID, nextBatchToken string) { + c.int.PutNextBatch(nextBatchToken) +} + +func (c cryptoClientStore) LoadNextBatch(_ id.UserID) string { + return c.int.GetNextBatch() +} + +var _ mautrix.Storer = (*cryptoClientStore)(nil) + +type cryptoStateStore struct { + bridge *Bridge +} + +var _ crypto.StateStore = (*cryptoStateStore)(nil) + +func (c *cryptoStateStore) IsEncrypted(id id.RoomID) bool { + portal := c.bridge.GetPortalByMXID(id) + if portal != nil { + return portal.Encrypted + } + + return false +} + +func (c *cryptoStateStore) FindSharedRooms(id id.UserID) []id.RoomID { + return c.bridge.StateStore.FindSharedRooms(id) +} + +func (c *cryptoStateStore) GetEncryptionEvent(id.RoomID) *event.EncryptionEventContent { + // TODO implement + return nil +} diff --git a/bridge/matrix.go b/bridge/matrix.go index 4e88f18..cb9127e 100644 --- a/bridge/matrix.go +++ b/bridge/matrix.go @@ -1,7 +1,10 @@ package bridge import ( + "errors" + "fmt" "strings" + "time" "maunium.net/go/maulogger/v2" "maunium.net/go/mautrix" @@ -29,9 +32,11 @@ func (b *Bridge) setupEvents() { } b.eventProcessor.On(event.EventMessage, b.matrixHandler.handleMessage) + b.eventProcessor.On(event.EventEncrypted, b.matrixHandler.handleEncrypted) b.eventProcessor.On(event.EventReaction, b.matrixHandler.handleReaction) b.eventProcessor.On(event.EventRedaction, b.matrixHandler.handleRedaction) b.eventProcessor.On(event.StateMember, b.matrixHandler.handleMembership) + b.eventProcessor.On(event.StateEncryption, b.matrixHandler.handleEncryption) } func (mh *matrixHandler) join(evt *event.Event, intent *appservice.IntentAPI) *mautrix.RespJoinedMembers { @@ -185,6 +190,10 @@ func (mh *matrixHandler) handleMembership(evt *event.Event) { return } + if mh.bridge.crypto != nil { + mh.bridge.crypto.HandleMemberEvent(evt) + } + // Grab the content of the event. content := evt.Content.AsMember() @@ -255,3 +264,113 @@ func (mh *matrixHandler) handleRedaction(evt *event.Event) { portal.handleMatrixRedaction(evt) } } + +func (mh *matrixHandler) handleEncryption(evt *event.Event) { + if evt.Content.AsEncryption().Algorithm != id.AlgorithmMegolmV1 { + return + } + + portal := mh.bridge.GetPortalByMXID(evt.RoomID) + if portal != nil && !portal.Encrypted { + mh.log.Debugfln("%s enabled encryption in %s", evt.Sender, evt.RoomID) + portal.Encrypted = true + portal.Update() + } +} + +const sessionWaitTimeout = 5 * time.Second + +func (mh *matrixHandler) handleEncrypted(evt *event.Event) { + if mh.ignoreEvent(evt) || mh.bridge.crypto == nil { + return + } + + decrypted, err := mh.bridge.crypto.Decrypt(evt) + decryptionRetryCount := 0 + if errors.Is(err, NoSessionFound) { + content := evt.Content.AsEncrypted() + mh.log.Debugfln("Couldn't find session %s trying to decrypt %s, waiting %d seconds...", content.SessionID, evt.ID, int(sessionWaitTimeout.Seconds())) + mh.as.SendErrorMessageSendCheckpoint(evt, appservice.StepDecrypted, err, false, decryptionRetryCount) + decryptionRetryCount++ + + if mh.bridge.crypto.WaitForSession(evt.RoomID, content.SenderKey, content.SessionID, sessionWaitTimeout) { + mh.log.Debugfln("Got session %s after waiting, trying to decrypt %s again", content.SessionID, evt.ID) + decrypted, err = mh.bridge.crypto.Decrypt(evt) + } else { + mh.as.SendErrorMessageSendCheckpoint(evt, appservice.StepDecrypted, fmt.Errorf("didn't receive encryption keys"), false, decryptionRetryCount) + + go mh.waitLongerForSession(evt) + + return + } + } + + if err != nil { + mh.as.SendErrorMessageSendCheckpoint(evt, appservice.StepDecrypted, err, true, decryptionRetryCount) + + mh.log.Warnfln("Failed to decrypt %s: %v", evt.ID, err) + _, _ = mh.bridge.bot.SendNotice(evt.RoomID, fmt.Sprintf( + "\u26a0 Your message was not bridged: %v", err)) + + return + } + + mh.as.SendMessageSendCheckpoint(decrypted, appservice.StepDecrypted, decryptionRetryCount) + mh.bridge.eventProcessor.Dispatch(decrypted) +} + +func (mh *matrixHandler) waitLongerForSession(evt *event.Event) { + const extendedTimeout = sessionWaitTimeout * 3 + + content := evt.Content.AsEncrypted() + mh.log.Debugfln("Couldn't find session %s trying to decrypt %s, waiting %d more seconds...", + content.SessionID, evt.ID, int(extendedTimeout.Seconds())) + + go mh.bridge.crypto.RequestSession(evt.RoomID, content.SenderKey, content.SessionID, evt.Sender, content.DeviceID) + + resp, err := mh.bridge.bot.SendNotice(evt.RoomID, fmt.Sprintf( + "\u26a0 Your message was not bridged: the bridge hasn't received the decryption keys. "+ + "The bridge will retry for %d seconds. If this error keeps happening, try restarting your client.", + int(extendedTimeout.Seconds()))) + if err != nil { + mh.log.Errorfln("Failed to send decryption error to %s: %v", evt.RoomID, err) + } + + update := event.MessageEventContent{MsgType: event.MsgNotice} + + if mh.bridge.crypto.WaitForSession(evt.RoomID, content.SenderKey, content.SessionID, extendedTimeout) { + mh.log.Debugfln("Got session %s after waiting more, trying to decrypt %s again", content.SessionID, evt.ID) + + decrypted, err := mh.bridge.crypto.Decrypt(evt) + if err == nil { + mh.as.SendMessageSendCheckpoint(decrypted, appservice.StepDecrypted, 2) + mh.bridge.eventProcessor.Dispatch(decrypted) + _, _ = mh.bridge.bot.RedactEvent(evt.RoomID, resp.EventID) + + return + } + + mh.log.Warnfln("Failed to decrypt %s: %v", evt.ID, err) + mh.as.SendErrorMessageSendCheckpoint(evt, appservice.StepDecrypted, err, true, 2) + update.Body = fmt.Sprintf("\u26a0 Your message was not bridged: %v", err) + } else { + mh.log.Debugfln("Didn't get %s, giving up on %s", content.SessionID, evt.ID) + mh.as.SendErrorMessageSendCheckpoint(evt, appservice.StepDecrypted, fmt.Errorf("didn't receive encryption keys"), true, 2) + update.Body = "\u26a0 Your message was not bridged: the bridge hasn't received the decryption keys. " + + "If this error keeps happening, try restarting your client." + } + + newContent := update + update.NewContent = &newContent + if resp != nil { + update.RelatesTo = &event.RelatesTo{ + Type: event.RelReplace, + EventID: resp.EventID, + } + } + + _, err = mh.bridge.bot.SendMessageEvent(evt.RoomID, event.EventMessage, &update) + if err != nil { + mh.log.Debugfln("Failed to update decryption error notice %s: %v", resp.EventID, err) + } +} diff --git a/bridge/portal.go b/bridge/portal.go index 05562d3..2eed2ef 100644 --- a/bridge/portal.go +++ b/bridge/portal.go @@ -35,6 +35,7 @@ type Portal struct { log log.Logger roomCreateLock sync.Mutex + encryptLock sync.Mutex discordMessages chan portalDiscordMessage matrixMessages chan portalMatrixMessage @@ -144,7 +145,7 @@ func (p *Portal) handleMatrixInvite(sender *User, evt *event.Event) { p.log.Infoln("no puppet for %v", sender) // Open a conversation on the discord side? } - p.log.Infoln("puppet:", puppet) + p.log.Infoln("matrixInvite: puppet:", puppet) } func (p *Portal) messageLoop() { @@ -212,14 +213,25 @@ func (p *Portal) createMatrixRoom(user *User, channel *discordgo.Channel) error var invite []id.UserID - if p.IsPrivateChat() { - invite = append(invite, p.bridge.bot.UserID) + if p.bridge.Config.Bridge.Encryption.Default { + initialState = append(initialState, &event.Event{ + Type: event.StateEncryption, + Content: event.Content{ + Parsed: event.EncryptionEventContent{Algorithm: id.AlgorithmMegolmV1}, + }, + }) + p.Encrypted = true + + if p.IsPrivateChat() { + invite = append(invite, p.bridge.bot.UserID) + } } resp, err := intent.CreateRoom(&mautrix.ReqCreateRoom{ Visibility: "private", Name: p.Name, Topic: p.Topic, + Invite: invite, Preset: "private_chat", IsDirect: p.IsPrivateChat(), InitialState: initialState, @@ -325,7 +337,7 @@ func (p *Portal) sendMediaFailedMessage(intent *appservice.IntentAPI, bridgeErr MsgType: event.MsgNotice, } - _, err := intent.SendMessageEvent(p.MXID, event.EventMessage, content) + _, err := p.sendMatrixMessage(intent, event.EventMessage, content, nil, time.Now().UTC().UnixMilli()) if err != nil { p.log.Warnfln("failed to send error message to matrix: %v", err) } @@ -379,7 +391,7 @@ func (p *Portal) handleDiscordAttachment(intent *appservice.IntentAPI, msgID str return } - resp, err := intent.SendMessageEvent(p.MXID, event.EventMessage, content) + resp, err := p.sendMatrixMessage(intent, event.EventMessage, content, nil, time.Now().UTC().UnixMilli()) if err != nil { p.log.Warnfln("failed to send media message to matrix: %v", err) } @@ -426,7 +438,7 @@ func (p *Portal) handleDiscordMessageCreate(user *User, msg *discordgo.Message) } } - resp, err := intent.SendMessageEvent(p.MXID, event.EventMessage, content) + resp, err := p.sendMatrixMessage(intent, event.EventMessage, content, nil, time.Now().UTC().UnixMilli()) if err != nil { p.log.Warnfln("failed to send message %q to matrix: %v", msg.ID, err) @@ -498,7 +510,7 @@ func (p *Portal) handleDiscordMessagesUpdate(user *User, msg *discordgo.Message) content.SetEdit(existing.MatrixID) - resp, err := intent.SendMessageEvent(p.MXID, event.EventMessage, content) + resp, err := p.sendMatrixMessage(intent, event.EventMessage, content, nil, time.Now().UTC().UnixMilli()) if err != nil { p.log.Warnfln("failed to send message %q to matrix: %v", msg.ID, err) @@ -567,6 +579,57 @@ func (p *Portal) syncParticipants(source *User, participants []*discordgo.User) } } +func (portal *Portal) encrypt(content *event.Content, eventType event.Type) (event.Type, error) { + if portal.Encrypted && portal.bridge.crypto != nil { + // TODO maybe the locking should be inside mautrix-go? + portal.encryptLock.Lock() + encrypted, err := portal.bridge.crypto.Encrypt(portal.MXID, eventType, *content) + portal.encryptLock.Unlock() + if err != nil { + return eventType, fmt.Errorf("failed to encrypt event: %w", err) + } + eventType = event.EventEncrypted + content.Parsed = encrypted + } + return eventType, nil +} + +const doublePuppetKey = "fi.mau.double_puppet_source" +const doublePuppetValue = "mautrix-discord" + +func (portal *Portal) sendMatrixMessage(intent *appservice.IntentAPI, eventType event.Type, content *event.MessageEventContent, extraContent map[string]interface{}, timestamp int64) (*mautrix.RespSendEvent, error) { + wrappedContent := event.Content{Parsed: content, Raw: extraContent} + if timestamp != 0 && intent.IsCustomPuppet { + if wrappedContent.Raw == nil { + wrappedContent.Raw = map[string]interface{}{} + } + if intent.IsCustomPuppet { + wrappedContent.Raw[doublePuppetKey] = doublePuppetValue + } + } + var err error + eventType, err = portal.encrypt(&wrappedContent, eventType) + if err != nil { + return nil, err + } + + if eventType == event.EventEncrypted { + // Clear other custom keys if the event was encrypted, but keep the double puppet identifier + if intent.IsCustomPuppet { + wrappedContent.Raw = map[string]interface{}{doublePuppetKey: doublePuppetValue} + } else { + wrappedContent.Raw = nil + } + } + + _, _ = intent.UserTyping(portal.MXID, false, 0) + if timestamp == 0 { + return intent.SendMessageEvent(portal.MXID, eventType, &wrappedContent) + } else { + return intent.SendMassagedMessageEvent(portal.MXID, eventType, &wrappedContent, timestamp) + } +} + func (p *Portal) handleMatrixMessages(msg portalMatrixMessage) { switch msg.evt.Type { case event.EventMessage: diff --git a/bridge/user.go b/bridge/user.go index 1dcb262..bf35510 100644 --- a/bridge/user.go +++ b/bridge/user.go @@ -32,6 +32,9 @@ type User struct { bridge *Bridge log log.Logger + // TODO finish implementing + Admin bool + guilds map[string]*database.Guild guildsLock sync.Mutex diff --git a/config/bridge.go b/config/bridge.go index 9d0510c..ce863db 100644 --- a/config/bridge.go +++ b/config/bridge.go @@ -30,6 +30,8 @@ type bridge struct { DoublePuppetAllowDiscovery bool `yaml:"double_puppet_allow_discovery"` LoginSharedSecretMap map[string]string `yaml:"login_shared_secret_map"` + Encryption encryption `yaml:"encryption"` + usernameTemplate *template.Template `yaml:"-"` displaynameTemplate *template.Template `yaml:"-"` channelnameTemplate *template.Template `yaml:"-"` diff --git a/config/encryption.go b/config/encryption.go new file mode 100644 index 0000000..1d57c39 --- /dev/null +++ b/config/encryption.go @@ -0,0 +1,29 @@ +package config + +type encryption struct { + Allow bool `yaml:"allow"` + Default bool `yaml:"default"` + + KeySharing struct { + Allow bool `yaml:"allow"` + RequireCrossSigning bool `yaml:"require_cross_signing"` + RequireVerification bool `yaml:"require_verification"` + } `yaml:"key_sharing"` +} + +func (e *encryption) validate() error { + return nil +} + +func (e *encryption) UnmarshalYAML(unmarshal func(interface{}) error) error { + type rawEncryption encryption + + raw := rawEncryption{} + if err := unmarshal(&raw); err != nil { + return err + } + + *e = encryption(raw) + + return e.validate() +} diff --git a/database/cryptostore.go b/database/cryptostore.go new file mode 100644 index 0000000..171c824 --- /dev/null +++ b/database/cryptostore.go @@ -0,0 +1,97 @@ +package database + +import ( + "database/sql" + + log "maunium.net/go/maulogger/v2" + + "maunium.net/go/mautrix/crypto" + "maunium.net/go/mautrix/id" +) + +type SQLCryptoStore struct { + *crypto.SQLCryptoStore + UserID id.UserID + GhostIDFormat string +} + +var _ crypto.Store = (*SQLCryptoStore)(nil) + +func NewSQLCryptoStore(db *Database, userID id.UserID, ghostIDFormat string) *SQLCryptoStore { + return &SQLCryptoStore{ + SQLCryptoStore: crypto.NewSQLCryptoStore(db.DB, db.dialect, "", "", + []byte("maunium.net/go/mautrix-whatsapp"), + &cryptoLogger{db.log.Sub("CryptoStore")}), + UserID: userID, + GhostIDFormat: ghostIDFormat, + } +} + +func (store *SQLCryptoStore) FindDeviceID() id.DeviceID { + var deviceID id.DeviceID + + query := `SELECT device_id FROM crypto_account WHERE account_id=$1` + err := store.DB.QueryRow(query, store.AccountID).Scan(&deviceID) + if err != nil && err != sql.ErrNoRows { + store.Log.Warn("Failed to scan device ID: %v", err) + } + + return deviceID +} + +func (store *SQLCryptoStore) GetRoomMembers(roomID id.RoomID) ([]id.UserID, error) { + query := ` + SELECT user_id FROM mx_user_profile + WHERE room_id=$1 + AND (membership='join' OR membership='invite') + AND user_id<>$2 + AND user_id NOT LIKE $3 + ` + + members := []id.UserID{} + + rows, err := store.DB.Query(query, roomID, store.UserID, store.GhostIDFormat) + if err != nil { + return members, err + } + + for rows.Next() { + var userID id.UserID + err := rows.Scan(&userID) + if err != nil { + store.Log.Warn("Failed to scan member in %s: %v", roomID, err) + return members, err + } + + members = append(members, userID) + } + + return members, nil +} + +// TODO merge this with the one in the parent package +type cryptoLogger struct { + int log.Logger +} + +var levelTrace = log.Level{ + Name: "TRACE", + Severity: -10, + Color: -1, +} + +func (c *cryptoLogger) Error(message string, args ...interface{}) { + c.int.Errorfln(message, args...) +} + +func (c *cryptoLogger) Warn(message string, args ...interface{}) { + c.int.Warnfln(message, args...) +} + +func (c *cryptoLogger) Debug(message string, args ...interface{}) { + c.int.Debugfln(message, args...) +} + +func (c *cryptoLogger) Trace(message string, args ...interface{}) { + c.int.Logfln(levelTrace, message, args...) +} diff --git a/database/migrations/08-add-crypto-store-to-database.sql b/database/migrations/08-add-crypto-store-to-database.sql new file mode 100644 index 0000000..c615976 --- /dev/null +++ b/database/migrations/08-add-crypto-store-to-database.sql @@ -0,0 +1,3 @@ +-- This migration is implemented in migrations.go as it comes from +-- maunium.net/go/mautrix/crypto/sql_store_upgrade. It runs upgrade at index 0 +-- which is described as "Add crypto store to database". diff --git a/database/migrations/09-add-account_id-to-crypto-store.sql b/database/migrations/09-add-account_id-to-crypto-store.sql new file mode 100644 index 0000000..03dc1cc --- /dev/null +++ b/database/migrations/09-add-account_id-to-crypto-store.sql @@ -0,0 +1,3 @@ +-- This migration is implemented in migrations.go as it comes from +-- maunium.net/go/mautrix/crypto/sql_store_upgrade. It runs upgrade at index 1 +-- which is described as "Add account_id to crypto store". diff --git a/database/migrations/10-add-megolm-withheld-data-to-crypto-store.sql b/database/migrations/10-add-megolm-withheld-data-to-crypto-store.sql new file mode 100644 index 0000000..38813b8 --- /dev/null +++ b/database/migrations/10-add-megolm-withheld-data-to-crypto-store.sql @@ -0,0 +1,3 @@ +-- This migration is implemented in migrations.go as it comes from +-- maunium.net/go/mautrix/crypto/sql_store_upgrade. It runs upgrade at index 2 +-- which is described as "Add megolm withheld data to crypto store". diff --git a/database/migrations/11-add-cross-signing-keys-to-crypto-store.sql b/database/migrations/11-add-cross-signing-keys-to-crypto-store.sql new file mode 100644 index 0000000..39f5041 --- /dev/null +++ b/database/migrations/11-add-cross-signing-keys-to-crypto-store.sql @@ -0,0 +1,3 @@ +-- This migration is implemented in migrations.go as it comes from +-- maunium.net/go/mautrix/crypto/sql_store_upgrade. It runs upgrade at index 3 +-- which is described as "Add cross-signing keys to crypto store". diff --git a/database/migrations/12-replace-varchar-with-text-in-the-crypto-database.sql b/database/migrations/12-replace-varchar-with-text-in-the-crypto-database.sql new file mode 100644 index 0000000..adb841e --- /dev/null +++ b/database/migrations/12-replace-varchar-with-text-in-the-crypto-database.sql @@ -0,0 +1,4 @@ +-- This migration is implemented in migrations.go as it comes from +-- maunium.net/go/mautrix/crypto/sql_store_upgrade. It runs upgrade at index 4 +-- which is described as "Replace VARCHAR(255) with TEXT in the crypto +-- database". diff --git a/database/migrations/13-split-last_used-into-last_encrypted-and-last_decrypted-in-crypto-store.sql b/database/migrations/13-split-last_used-into-last_encrypted-and-last_decrypted-in-crypto-store.sql new file mode 100644 index 0000000..28906bd --- /dev/null +++ b/database/migrations/13-split-last_used-into-last_encrypted-and-last_decrypted-in-crypto-store.sql @@ -0,0 +1,4 @@ +-- This migration is implemented in migrations.go as it comes from +-- maunium.net/go/mautrix/crypto/sql_store_upgrade. It runs upgrade at index 5 +-- which is described as "Split last_used into last_encrypted and +-- last_decrypted in crypto store". diff --git a/database/migrations/14-add-encrypted-column-to-portal-table.sql b/database/migrations/14-add-encrypted-column-to-portal-table.sql new file mode 100644 index 0000000..d032fee --- /dev/null +++ b/database/migrations/14-add-encrypted-column-to-portal-table.sql @@ -0,0 +1 @@ +ALTER TABLE portal ADD COLUMN encrypted BOOLEAN NOT NULL DEFAULT false; diff --git a/database/migrations/migrations.go b/database/migrations/migrations.go index 06fcce4..904b9f4 100644 --- a/database/migrations/migrations.go +++ b/database/migrations/migrations.go @@ -3,37 +3,18 @@ package migrations import ( "database/sql" "embed" - "sort" "github.com/lopezator/migrator" log "maunium.net/go/maulogger/v2" + "maunium.net/go/mautrix/crypto/sql_store_upgrade" ) //go:embed *.sql var embeddedMigrations embed.FS -var ( - commonMigrations = []string{ - "01-initial.sql", - "02-attachments.sql", - "03-emoji.sql", - "04-custom-puppet.sql", - "05-additional-puppet-fields.sql", - "07-guilds.sql", - } - - sqliteMigrations = []string{ - "06-remove-unique-user-constraint.sqlite.sql", - } - - postgresMigrations = []string{ - "06-remove-unique-user-constraint.postgres.sql", - } -) - -func migrationFromFile(filename string) *migrator.Migration { +func migrationFromFile(description, filename string) *migrator.Migration { return &migrator.Migration{ - Name: filename, + Name: description, Func: func(tx *sql.Tx) error { data, err := embeddedMigrations.ReadFile(filename) if err != nil { @@ -49,31 +30,83 @@ func migrationFromFile(filename string) *migrator.Migration { } } +func migrationFromFileWithDialect(dialect, description, sqliteFile, postgresFile string) *migrator.Migration { + switch dialect { + case "sqlite3": + return migrationFromFile(description, sqliteFile) + case "postgres": + return migrationFromFile(description, postgresFile) + default: + return nil + } +} + func Run(db *sql.DB, baseLog log.Logger, dialect string) error { subLogger := baseLog.Sub("Migrations") logger := migrator.LoggerFunc(func(msg string, args ...interface{}) { subLogger.Infof(msg, args...) }) - migrationNames := commonMigrations - switch dialect { - case "sqlite3": - migrationNames = append(migrationNames, sqliteMigrations...) - case "postgres": - migrationNames = append(migrationNames, postgresMigrations...) - } - - sort.Strings(migrationNames) - - migrations := make([]interface{}, len(migrationNames)) - for idx, name := range migrationNames { - migrations[idx] = migrationFromFile(name) - } - m, err := migrator.New( migrator.TableName("version"), migrator.WithLogger(logger), - migrator.Migrations(migrations...), + migrator.Migrations( + migrationFromFile("Initial Schema", "01-initial.sql"), + migrationFromFile("Attachments", "02-attachments.sql"), + migrationFromFile("Emoji", "03-emoji.sql"), + migrationFromFile("Custom Puppets", "04-custom-puppet.sql"), + migrationFromFile( + "Additional puppet fields", + "05-additional-puppet-fields.sql", + ), + migrationFromFileWithDialect( + dialect, + "Remove unique user constraint", + "06-remove-unique-user-constraint.sqlite.sql", + "06-remove-unique-user-constraint.postgres.sql", + ), + migrationFromFile("Guild Bridging", "07-guilds.sql"), + &migrator.Migration{ + Name: "Add crypto store to database", + Func: func(tx *sql.Tx) error { + return sql_store_upgrade.Upgrades[0](tx, dialect) + }, + }, + &migrator.Migration{ + Name: "Add account_id to crypto store", + Func: func(tx *sql.Tx) error { + return sql_store_upgrade.Upgrades[1](tx, dialect) + }, + }, + &migrator.Migration{ + Name: "Add megolm withheld data to crypto store", + Func: func(tx *sql.Tx) error { + return sql_store_upgrade.Upgrades[2](tx, dialect) + }, + }, + &migrator.Migration{ + Name: "Add cross-signing keys to crypto store", + Func: func(tx *sql.Tx) error { + return sql_store_upgrade.Upgrades[3](tx, dialect) + }, + }, + &migrator.Migration{ + Name: "Replace VARCHAR(255) with TEXT in the crypto database", + Func: func(tx *sql.Tx) error { + return sql_store_upgrade.Upgrades[4](tx, dialect) + }, + }, + &migrator.Migration{ + Name: "Split last_used into last_encrypted and last_decrypted in crypto store", + Func: func(tx *sql.Tx) error { + return sql_store_upgrade.Upgrades[5](tx, dialect) + }, + }, + migrationFromFile( + "Add encryption column to portal table", + "14-add-encrypted-column-to-portal-table.sql", + ), + ), ) if err != nil { return err diff --git a/database/portal.go b/database/portal.go index 663651b..96e4c45 100644 --- a/database/portal.go +++ b/database/portal.go @@ -19,6 +19,8 @@ type Portal struct { Name string Topic string + Encrypted bool + Avatar string AvatarURL id.ContentURI @@ -33,7 +35,8 @@ func (p *Portal) Scan(row Scannable) *Portal { var typ sql.NullInt32 err := row.Scan(&p.Key.ChannelID, &p.Key.Receiver, &mxid, &p.Name, - &p.Topic, &p.Avatar, &avatarURL, &typ, &p.DMUser, &firstEventID) + &p.Topic, &p.Avatar, &avatarURL, &typ, &p.DMUser, &firstEventID, + &p.Encrypted) if err != nil { if err != sql.ErrNoRows { @@ -62,12 +65,12 @@ func (p *Portal) mxidPtr() *id.RoomID { func (p *Portal) Insert() { query := "INSERT INTO portal" + " (channel_id, receiver, mxid, name, topic, avatar, avatar_url," + - " type, dmuser, first_event_id)" + - " VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)" + " type, dmuser, first_event_id, encrypted)" + + " VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)" _, err := p.db.Exec(query, p.Key.ChannelID, p.Key.Receiver, p.mxidPtr(), p.Name, p.Topic, p.Avatar, p.AvatarURL.String(), p.Type, p.DMUser, - p.FirstEventID.String()) + p.FirstEventID.String(), p.Encrypted) if err != nil { p.log.Warnfln("Failed to insert %s: %v", p.Key, err) @@ -77,11 +80,12 @@ func (p *Portal) Insert() { func (p *Portal) Update() { query := "UPDATE portal SET" + " mxid=$1, name=$2, topic=$3, avatar=$4, avatar_url=$5, type=$6," + - " dmuser=$7, first_event_id=$8" + - " WHERE channel_id=$9 AND receiver=$10" + " dmuser=$7, first_event_id=$8, encrypted=$9" + + " WHERE channel_id=$10 AND receiver=$11" _, err := p.db.Exec(query, p.mxidPtr(), p.Name, p.Topic, p.Avatar, p.AvatarURL.String(), p.Type, p.DMUser, p.FirstEventID.String(), + p.Encrypted, p.Key.ChannelID, p.Key.Receiver) if err != nil { diff --git a/database/portalquery.go b/database/portalquery.go index 8b4353a..d9dd1ba 100644 --- a/database/portalquery.go +++ b/database/portalquery.go @@ -6,6 +6,12 @@ import ( "maunium.net/go/mautrix/id" ) +const ( + portalSelect = "SELECT channel_id, receiver, mxid, name, topic, avatar," + + " avatar_url, type, dmuser, first_event_id, encrypted" + + " FROM portal" +) + type PortalQuery struct { db *Database log log.Logger @@ -19,23 +25,23 @@ func (pq *PortalQuery) New() *Portal { } func (pq *PortalQuery) GetAll() []*Portal { - return pq.getAll("SELECT * FROM portal") + return pq.getAll(portalSelect) } func (pq *PortalQuery) GetByID(key PortalKey) *Portal { - return pq.get("SELECT * FROM portal WHERE channel_id=$1 AND receiver=$2", key.ChannelID, key.Receiver) + return pq.get(portalSelect+" WHERE channel_id=$1 AND receiver=$2", key.ChannelID, key.Receiver) } func (pq *PortalQuery) GetByMXID(mxid id.RoomID) *Portal { - return pq.get("SELECT * FROM portal WHERE mxid=$1", mxid) + return pq.get(portalSelect+" WHERE mxid=$1", mxid) } func (pq *PortalQuery) GetAllByID(id string) []*Portal { - return pq.getAll("SELECT * FROM portal WHERE receiver=$1", id) + return pq.getAll(portalSelect+" WHERE receiver=$1", id) } func (pq *PortalQuery) FindPrivateChats(receiver string) []*Portal { - query := "SELECT * FROM portal WHERE receiver=$1 AND type=$2;" + query := portalSelect + " portal WHERE receiver=$1 AND type=$2;" return pq.getAll(query, receiver, discordgo.ChannelTypeDM) } diff --git a/database/sqlstatestore.go b/database/sqlstatestore.go index c5d800b..e316773 100644 --- a/database/sqlstatestore.go +++ b/database/sqlstatestore.go @@ -272,3 +272,31 @@ func (s *SQLStateStore) HasPowerLevel(roomID id.RoomID, userID id.UserID, eventT return s.GetPowerLevel(roomID, userID) >= s.GetPowerLevelRequirement(roomID, eventType) } + +func (store *SQLStateStore) FindSharedRooms(userID id.UserID) []id.RoomID { + query := "SELECT room_id FROM mx_user_profile" + + "LEFT JOIN portal ON portal.mxid=mx_user_profile.room_id" + + "WHERE user_id=$1 AND portal.encrypted=true" + + rooms := []id.RoomID{} + + rows, err := store.db.Query(query, userID) + if err != nil { + store.log.Warnfln("Failed to query shared rooms with %s: %v", userID, err) + + return rooms + } + + for rows.Next() { + var roomID id.RoomID + + err = rows.Scan(&roomID) + if err != nil { + store.log.Warnfln("Failed to scan room ID: %v", err) + } else { + rooms = append(rooms, roomID) + } + } + + return rooms +} diff --git a/go.mod b/go.mod index bba6b6a..536f49d 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,10 @@ require ( github.com/gorilla/mux v1.8.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/tidwall/gjson v1.14.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/tidwall/sjson v1.2.4 // indirect golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect golang.org/x/net v0.0.0-20220403103023-749bd193bc2b // indirect golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12 // indirect diff --git a/go.sum b/go.sum index 2066e59..7ff3c7f 100644 --- a/go.sum +++ b/go.sum @@ -35,9 +35,13 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.0 h1:6aeJ0bzojgWLa82gDQHcx3S0Lr/O51I9bJ5nv6JFx5w= github.com/tidwall/gjson v1.14.0/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.4 h1:cuiLzLnaMeBhRmEv00Lpk3tkYrcxpmbU81tAY4Dw0tc= github.com/tidwall/sjson v1.2.4/go.mod h1:098SZ494YoMWPmMO6ct4dcFnqxwj9r/gF0Etp19pSNM= gitlab.com/beeper/discordgo v0.23.3-0.20220219094025-13ff4cc63da7 h1:8ieR27GadHnShqhsvPrDzL1/ZOntavGGt4TXqafncYE= gitlab.com/beeper/discordgo v0.23.3-0.20220219094025-13ff4cc63da7/go.mod h1:Hwfv4M8yP/MDh47BN+4Z1WItJ1umLKUyplCH5KcQPgE= From a695b045c84190111e83b127f6fb267a453150c9 Mon Sep 17 00:00:00 2001 From: Sumner Evans Date: Mon, 25 Apr 2022 21:29:35 -0600 Subject: [PATCH 02/15] async media: add ability to upload media asynchronously Requires a homeserver with MSC2246 support --- bridge/attachments.go | 24 +++++++++++++++++++----- bridge/user.go | 2 +- config/homeserver.go | 1 + example-config.yaml | 2 ++ go.mod | 10 +++++----- go.sum | 22 ++++++++-------------- 6 files changed, 36 insertions(+), 25 deletions(-) diff --git a/bridge/attachments.go b/bridge/attachments.go index 206cf6e..c3e8569 100644 --- a/bridge/attachments.go +++ b/bridge/attachments.go @@ -9,6 +9,7 @@ import ( "github.com/bwmarrin/discordgo" + "maunium.net/go/mautrix" "maunium.net/go/mautrix/appservice" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" @@ -76,13 +77,26 @@ func (p *Portal) downloadMatrixAttachment(eventID id.EventID, content *event.Mes } func (p *Portal) uploadMatrixAttachment(intent *appservice.IntentAPI, data []byte, content *event.MessageEventContent) error { - uploaded, err := intent.UploadBytes(data, content.Info.MimeType) - if err != nil { - return err + req := mautrix.ReqUploadMedia{ + ContentBytes: data, + ContentType: content.Info.MimeType, + } + var mxc id.ContentURI + if p.bridge.Config.Homeserver.AsyncMedia { + uploaded, err := intent.UnstableUploadAsync(req) + if err != nil { + return err + } + mxc = uploaded.ContentURI + } else { + uploaded, err := intent.UploadMedia(req) + if err != nil { + return err + } + mxc = uploaded.ContentURI } - content.URL = uploaded.ContentURI.CUString() - + content.URL = mxc.CUString() content.Info.Size = len(data) if content.Info.Width == 0 && content.Info.Height == 0 && strings.HasPrefix(content.Info.MimeType, "image/") { diff --git a/bridge/user.go b/bridge/user.go index bf35510..6c2ca04 100644 --- a/bridge/user.go +++ b/bridge/user.go @@ -720,7 +720,7 @@ func (u *User) updateDirectChats(chats map[id.UserID][]id.RoomID) { var err error if u.bridge.Config.Homeserver.Asmux { - urlPath := intent.BuildBaseURL("_matrix", "client", "unstable", "com.beeper.asmux", "dms") + urlPath := intent.BuildURL(mautrix.ClientURLPath{"unstable", "com.beeper.asmux", "dms"}) _, err = intent.MakeFullRequest(mautrix.FullRequest{ Method: method, URL: urlPath, diff --git a/config/homeserver.go b/config/homeserver.go index f66864b..bb3aeb1 100644 --- a/config/homeserver.go +++ b/config/homeserver.go @@ -14,6 +14,7 @@ type homeserver struct { Domain string `yaml:"domain"` Asmux bool `yaml:"asmux"` StatusEndpoint string `yaml:"status_endpoint"` + AsyncMedia bool `yaml:"async_media"` } func (h *homeserver) validate() error { diff --git a/example-config.yaml b/example-config.yaml index 32ef879..61cb2c8 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -11,6 +11,8 @@ homeserver: # If set, the bridge will make POST requests to this URL whenever a user's whatsapp connection state changes. # The bridge will use the appservice as_token to authorize requests. status_endpoint: null + # Does the homeserver support https://github.com/matrix-org/matrix-spec-proposals/pull/2246? + async_media: false # Application service host/registration related details. # Changing these values requires regeneration of the registration. diff --git a/go.mod b/go.mod index 536f49d..cd0b037 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/alecthomas/kong v0.5.0 github.com/bwmarrin/discordgo v0.23.2 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 + github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.5.0 github.com/lib/pq v1.10.4 github.com/lopezator/migrator v0.3.0 @@ -13,19 +14,18 @@ require ( github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e gopkg.in/yaml.v2 v2.4.0 maunium.net/go/maulogger/v2 v2.3.2 - maunium.net/go/mautrix v0.10.12 + maunium.net/go/mautrix v0.10.13-0.20220417095934-0eee489b6417 ) require ( - github.com/gorilla/mux v1.8.0 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/tidwall/gjson v1.14.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/tidwall/sjson v1.2.4 // indirect - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect - golang.org/x/net v0.0.0-20220403103023-749bd193bc2b // indirect + github.com/yuin/goldmark v1.4.11 // indirect + golang.org/x/crypto v0.0.0-20220408190544-5352b0902921 // indirect + golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 // indirect golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12 // indirect ) diff --git a/go.sum b/go.sum index 7ff3c7f..9fa5517 100644 --- a/go.sum +++ b/go.sum @@ -26,8 +26,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 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/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -43,25 +41,21 @@ 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.4 h1:cuiLzLnaMeBhRmEv00Lpk3tkYrcxpmbU81tAY4Dw0tc= github.com/tidwall/sjson v1.2.4/go.mod h1:098SZ494YoMWPmMO6ct4dcFnqxwj9r/gF0Etp19pSNM= +github.com/yuin/goldmark v1.4.11 h1:i45YIzqLnUc2tGaTlJCyUxSG8TvgyGqhqOZOUKIjJ6w= +github.com/yuin/goldmark v1.4.11/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg= gitlab.com/beeper/discordgo v0.23.3-0.20220219094025-13ff4cc63da7 h1:8ieR27GadHnShqhsvPrDzL1/ZOntavGGt4TXqafncYE= gitlab.com/beeper/discordgo v0.23.3-0.20220219094025-13ff4cc63da7/go.mod h1:Hwfv4M8yP/MDh47BN+4Z1WItJ1umLKUyplCH5KcQPgE= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38= -golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220408190544-5352b0902921 h1:iU7T1X1J6yxDr0rda54sWGkHgOp5XJrqm79gcNlC2VM= +golang.org/x/crypto v0.0.0-20220408190544-5352b0902921/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220403103023-749bd193bc2b h1:vI32FkLJNAWtGD4BwkThwEy6XS7ZLLMHkSkYfF8M0W0= -golang.org/x/net v0.0.0-20220403103023-749bd193bc2b/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 h1:EN5+DfgmRMvRUrMGERW2gQl3Vc+Z7ZMnI/xdEpPSf0c= +golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86 h1:A9i04dxx7Cribqbs8jf3FQLogkL/CV2YN7hj9KWJCkc= -golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12 h1:QyVthZKMsyaQwBTJE04jdNN0Pp5Fn9Qga0mrgxyERQM= golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -81,5 +75,5 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/maulogger/v2 v2.3.2 h1:1XmIYmMd3PoQfp9J+PaHhpt80zpfmMqaShzUTC7FwY0= maunium.net/go/maulogger/v2 v2.3.2/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A= -maunium.net/go/mautrix v0.10.12 h1:GqmsksKyKrTqmLb2B6yGOawoFLPTJ3A3NtXrygAvKM8= -maunium.net/go/mautrix v0.10.12/go.mod h1:xTq6+uMCAXtQwfqjUrYd8O10oIyymbzZm02CYOMt4ek= +maunium.net/go/mautrix v0.10.13-0.20220417095934-0eee489b6417 h1:dEJ9MKQvd4v2Rk2W6EUiO1T6PrSWPsB/JQOHQn4H6X0= +maunium.net/go/mautrix v0.10.13-0.20220417095934-0eee489b6417/go.mod h1:zOor2zO/F10T/GbU67vWr0vnhLso88rlRr1HIrb1XWU= From d788565d2b4ef4f2861ca9567830187d38b0865f Mon Sep 17 00:00:00 2001 From: Gary Kramlich Date: Wed, 27 Apr 2022 13:16:11 -0500 Subject: [PATCH 03/15] Fix replies from the management room This was accidentally broken when implementing e2be. --- bridge/commands.go | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/bridge/commands.go b/bridge/commands.go index 83363b6..2799e7f 100644 --- a/bridge/commands.go +++ b/bridge/commands.go @@ -3,7 +3,6 @@ package bridge import ( "context" "fmt" - "time" "github.com/alecthomas/kong" @@ -34,17 +33,11 @@ func (g *globals) reply(msg string) { content.MsgType = event.MsgNotice intent := g.bot - if g.portal == nil { - g.handler.log.Errorfln("we don't have a portal for this command") - - return - } - - if g.portal.IsPrivateChat() { + if g.portal != nil && g.portal.IsPrivateChat() { intent = g.portal.MainIntent() } - _, err := g.portal.sendMatrixMessage(intent, event.EventMessage, &content, nil, time.Now().UTC().UnixMilli()) + _, err := intent.SendMessageEvent(g.roomID, event.EventMessage, content) if err != nil { g.handler.log.Warnfln("Failed to reply to command from %q: %v", g.user.MXID, err) } From fc51c6e94e148705dd86f5cb0e890ce7d78afe58 Mon Sep 17 00:00:00 2001 From: Gary Kramlich Date: Wed, 27 Apr 2022 13:48:36 -0500 Subject: [PATCH 04/15] Sync puppets when they send a message Fixes #30, Fixes #34 --- bridge/portal.go | 4 +++- bridge/puppet.go | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/bridge/portal.go b/bridge/portal.go index 2eed2ef..96e611e 100644 --- a/bridge/portal.go +++ b/bridge/portal.go @@ -418,7 +418,9 @@ func (p *Portal) handleDiscordMessageCreate(user *User, msg *discordgo.Message) return } - intent := p.bridge.GetPuppetByID(msg.Author.ID).IntentFor(p) + puppet := p.bridge.GetPuppetByID(msg.Author.ID) + puppet.SyncContact(user) + intent := puppet.IntentFor(p) if msg.Content != "" { content := &event.MessageEventContent{ diff --git a/bridge/puppet.go b/bridge/puppet.go index f17e8f1..c635387 100644 --- a/bridge/puppet.go +++ b/bridge/puppet.go @@ -164,7 +164,10 @@ func (p *Puppet) CustomIntent() *appservice.IntentAPI { func (p *Puppet) updatePortalMeta(meta func(portal *Portal)) { for _, portal := range p.bridge.GetAllPortalsByID(p.ID) { + // Get room create lock to prevent races between receiving contact info and room creation. + portal.roomCreateLock.Lock() meta(portal) + portal.roomCreateLock.Unlock() } } From 7630e8a7a991d8234ab5fa239d0ca8cd3e2fbfee Mon Sep 17 00:00:00 2001 From: Gary Kramlich Date: Wed, 27 Apr 2022 22:47:04 -0500 Subject: [PATCH 05/15] Add the encryption section to example-config.yaml --- example-config.yaml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/example-config.yaml b/example-config.yaml index 61cb2c8..eda5f0e 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -111,6 +111,29 @@ bridge: # Optional extra text sent when joining a management room. additional_help: "" + # End-to-bridge encryption support options. + # + # See https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html for more info. + encryption: + # Allow encryption, work in group chat rooms with e2ee enabled + allow: false + # Default to encryption, force-enable encryption in all portals the bridge creates + # This will cause the bridge bot to be in private chats for the encryption to work properly. + # It is recommended to also set private_chat_portal_meta to true when using this. + default: false + # Options for automatic key sharing. + key_sharing: + # Enable key sharing? If enabled, key requests for rooms where users are in will be fulfilled. + # You must use a client that supports requesting keys from other users to use this feature. + allow: false + # Require the requesting device to have a valid cross-signing signature? + # This doesn't require that the bridge has verified the device, only that the user has verified it. + # Not yet implemented. + require_cross_signing: false + # Require devices to be verified by the bridge? + # Verification by the bridge is not yet implemented. + require_verification: true + logging: directory: ./logs file_name_format: '{{.Date}}-{{.Index}}.log' From fdb219bb824413fa98b252ed4723a59c9274333a Mon Sep 17 00:00:00 2001 From: Sumner Evans Date: Wed, 27 Apr 2022 22:00:07 -0600 Subject: [PATCH 06/15] sqlstatestore: fix query for FindSharedRooms There were missing spaces in the string concatenation --- database/sqlstatestore.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/database/sqlstatestore.go b/database/sqlstatestore.go index e316773..9fcfd1c 100644 --- a/database/sqlstatestore.go +++ b/database/sqlstatestore.go @@ -274,9 +274,11 @@ func (s *SQLStateStore) HasPowerLevel(roomID id.RoomID, userID id.UserID, eventT } func (store *SQLStateStore) FindSharedRooms(userID id.UserID) []id.RoomID { - query := "SELECT room_id FROM mx_user_profile" + - "LEFT JOIN portal ON portal.mxid=mx_user_profile.room_id" + - "WHERE user_id=$1 AND portal.encrypted=true" + query := ` + SELECT room_id FROM mx_user_profile + LEFT JOIN portal ON portal.mxid=mx_user_profile.room_id + WHERE user_id=$1 AND portal.encrypted=true + ` rooms := []id.RoomID{} From 43f6d2eaa73f81b8621310a6ab6e8116cf0b3e58 Mon Sep 17 00:00:00 2001 From: Sumner Evans Date: Wed, 27 Apr 2022 22:01:41 -0600 Subject: [PATCH 07/15] editorconfig: add --- .editorconfig | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..02798cd --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +indent_style = tab +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{yaml,yml}] +indent_style = space + +[.gitlab-ci.yml] +indent_size = 2 From 218854a427d9b5ecbdb76b96b7ffcc06257dbea9 Mon Sep 17 00:00:00 2001 From: Gary Kramlich Date: Fri, 29 Apr 2022 01:28:51 -0500 Subject: [PATCH 08/15] Update our dependencies --- go.mod | 12 ++++++------ go.sum | 12 ++++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index cd0b037..143b0ee 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.5.0 - github.com/lib/pq v1.10.4 + github.com/lib/pq v1.10.5 github.com/lopezator/migrator v0.3.0 github.com/mattn/go-sqlite3 v1.14.12 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e @@ -19,14 +19,14 @@ require ( require ( github.com/pkg/errors v0.9.1 // indirect - github.com/tidwall/gjson v1.14.0 // indirect + github.com/tidwall/gjson v1.14.1 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/tidwall/sjson v1.2.4 // indirect - github.com/yuin/goldmark v1.4.11 // indirect - golang.org/x/crypto v0.0.0-20220408190544-5352b0902921 // indirect - golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 // indirect - golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12 // indirect + github.com/yuin/goldmark v1.4.12 // indirect + golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f // indirect + golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect + golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect ) replace github.com/bwmarrin/discordgo v0.23.2 => gitlab.com/beeper/discordgo v0.23.3-0.20220219094025-13ff4cc63da7 diff --git a/go.sum b/go.sum index 9fa5517..d2713ad 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.5 h1:J+gdV2cUmX7ZqL2B0lFcW0m+egaHC2V3lpO8nWxyYiQ= +github.com/lib/pq v1.10.5/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lopezator/migrator v0.3.0 h1:VW/rR+J8NYwPdkBxjrFdjwejpgvP59LbmANJxXuNbuk= github.com/lopezator/migrator v0.3.0/go.mod h1:bpVAVPkWSvTw8ya2Pk7E/KiNAyDWNImgivQY79o8/8I= github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0= @@ -35,6 +37,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.14.0 h1:6aeJ0bzojgWLa82gDQHcx3S0Lr/O51I9bJ5nv6JFx5w= github.com/tidwall/gjson v1.14.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.1 h1:iymTbGkQBhveq21bEvAQ81I0LEBork8BFe1CUZXdyuo= +github.com/tidwall/gjson v1.14.1/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= @@ -43,21 +47,29 @@ github.com/tidwall/sjson v1.2.4 h1:cuiLzLnaMeBhRmEv00Lpk3tkYrcxpmbU81tAY4Dw0tc= github.com/tidwall/sjson v1.2.4/go.mod h1:098SZ494YoMWPmMO6ct4dcFnqxwj9r/gF0Etp19pSNM= github.com/yuin/goldmark v1.4.11 h1:i45YIzqLnUc2tGaTlJCyUxSG8TvgyGqhqOZOUKIjJ6w= github.com/yuin/goldmark v1.4.11/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg= +github.com/yuin/goldmark v1.4.12 h1:6hffw6vALvEDqJ19dOJvJKOoAOKe4NDaTqvd2sktGN0= +github.com/yuin/goldmark v1.4.12/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= gitlab.com/beeper/discordgo v0.23.3-0.20220219094025-13ff4cc63da7 h1:8ieR27GadHnShqhsvPrDzL1/ZOntavGGt4TXqafncYE= gitlab.com/beeper/discordgo v0.23.3-0.20220219094025-13ff4cc63da7/go.mod h1:Hwfv4M8yP/MDh47BN+4Z1WItJ1umLKUyplCH5KcQPgE= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20220408190544-5352b0902921 h1:iU7T1X1J6yxDr0rda54sWGkHgOp5XJrqm79gcNlC2VM= golang.org/x/crypto v0.0.0-20220408190544-5352b0902921/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc= +golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 h1:EN5+DfgmRMvRUrMGERW2gQl3Vc+Z7ZMnI/xdEpPSf0c= golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12 h1:QyVthZKMsyaQwBTJE04jdNN0Pp5Fn9Qga0mrgxyERQM= golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc= +golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 59ea003f516d36b4f93bcadcd21c247063c13ddf Mon Sep 17 00:00:00 2001 From: Gary Kramlich Date: Fri, 29 Apr 2022 07:32:28 -0500 Subject: [PATCH 09/15] Properly handle group dm name updates. Refs #37 --- bridge/portal.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/bridge/portal.go b/bridge/portal.go index 96e611e..27f5bd4 100644 --- a/bridge/portal.go +++ b/bridge/portal.go @@ -411,6 +411,17 @@ func (p *Portal) handleDiscordMessageCreate(user *User, msg *discordgo.Message) return } + // Handle room name changes + if msg.Type == discordgo.MessageTypeChannelNameChange { + p.Name = msg.Content + p.Update() + + p.MainIntent().SetRoomName(p.MXID, msg.Content) + + return + } + + // Handle normal message existing := p.bridge.db.Message.GetByDiscordID(p.Key, msg.ID) if existing != nil { p.log.Debugln("not handling duplicate message", msg.ID) From 3a1cb2fc78ae886caf6ff76581c2d86cdd075685 Mon Sep 17 00:00:00 2001 From: Gary Kramlich Date: Mon, 2 May 2022 09:29:38 -0500 Subject: [PATCH 10/15] Make sure we've acquired the room lock before checking the mxid --- bridge/portal.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bridge/portal.go b/bridge/portal.go index 27f5bd4..0fbdae8 100644 --- a/bridge/portal.go +++ b/bridge/portal.go @@ -172,14 +172,14 @@ func (p *Portal) MainIntent() *appservice.IntentAPI { } func (p *Portal) createMatrixRoom(user *User, channel *discordgo.Channel) error { + p.roomCreateLock.Lock() + defer p.roomCreateLock.Unlock() + // If we have a matrix id the room should exist so we have nothing to do. if p.MXID != "" { return nil } - p.roomCreateLock.Lock() - defer p.roomCreateLock.Unlock() - p.Type = channel.Type if p.Type == discordgo.ChannelTypeDM { p.DMUser = channel.Recipients[0].ID From 7d9826fc2dbfa531c8efe53c31dc94e633d456dd Mon Sep 17 00:00:00 2001 From: Gary Kramlich Date: Mon, 2 May 2022 09:44:12 -0500 Subject: [PATCH 11/15] Make sure we use the configured format string on room name updates Refs #37 --- bridge/portal.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/bridge/portal.go b/bridge/portal.go index 0fbdae8..f8eb998 100644 --- a/bridge/portal.go +++ b/bridge/portal.go @@ -413,10 +413,22 @@ func (p *Portal) handleDiscordMessageCreate(user *User, msg *discordgo.Message) // Handle room name changes if msg.Type == discordgo.MessageTypeChannelNameChange { - p.Name = msg.Content + channel, err := user.Session.Channel(msg.ChannelID) + if err != nil { + p.log.Errorf("Failed to find the channel for portal %s", p.Key) + return + } + + name, err := p.bridge.Config.Bridge.FormatChannelname(channel, user.Session) + if err != nil { + p.log.Errorf("Failed to format name for portal %s", p.Key) + return + } + + p.Name = name p.Update() - p.MainIntent().SetRoomName(p.MXID, msg.Content) + p.MainIntent().SetRoomName(p.MXID, name) return } From 0afd95398b46f9f6d3d2d9d2578ab5e3f94ed808 Mon Sep 17 00:00:00 2001 From: Gary Kramlich Date: Mon, 2 May 2022 09:53:54 -0500 Subject: [PATCH 12/15] Fix the help on `guilds unbridge` --- bridge/commands.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bridge/commands.go b/bridge/commands.go index 2799e7f..845274b 100644 --- a/bridge/commands.go +++ b/bridge/commands.go @@ -306,7 +306,7 @@ func (m *pingMatrixCmd) Run(g *globals) error { type guildsCmd struct { Status guildStatusCmd `kong:"cmd,help='Show the bridge status for the guilds you are in'"` Bridge guildBridgeCmd `kong:"cmd,help='Bridge a guild'"` - Unbridge guildUnbridgeCmd `kong:"cmd,help="Unbridge a guild'"` + Unbridge guildUnbridgeCmd `kong:"cmd,help='Unbridge a guild'"` } type guildStatusCmd struct{} From fe94078194dbfaa2faa37786bbc18f3919fda004 Mon Sep 17 00:00:00 2001 From: Gary Kramlich Date: Mon, 2 May 2022 11:31:14 -0500 Subject: [PATCH 13/15] Avoid a null pointer deference --- bridge/portal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bridge/portal.go b/bridge/portal.go index f8eb998..36f0e28 100644 --- a/bridge/portal.go +++ b/bridge/portal.go @@ -455,7 +455,7 @@ func (p *Portal) handleDiscordMessageCreate(user *User, msg *discordgo.Message) key := database.PortalKey{msg.MessageReference.ChannelID, user.ID} existing := p.bridge.db.Message.GetByDiscordID(key, msg.MessageReference.MessageID) - if existing.MatrixID != "" { + if existing != nil && existing.MatrixID != "" { content.RelatesTo = &event.RelatesTo{ Type: event.RelReply, EventID: existing.MatrixID, From 2ae56ad0ed76ec0026e4f9614747acfa5241dc6d Mon Sep 17 00:00:00 2001 From: Gary Kramlich Date: Mon, 2 May 2022 11:31:51 -0500 Subject: [PATCH 14/15] Fix an issue where the bot would think e2be dms were the management rooms --- bridge/matrix.go | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/bridge/matrix.go b/bridge/matrix.go index cb9127e..536c35a 100644 --- a/bridge/matrix.go +++ b/bridge/matrix.go @@ -106,30 +106,30 @@ func (mh *matrixHandler) handleMessage(evt *event.Event) { } -func (mh *matrixHandler) joinAndCheckMembers(evt *event.Event, intent *appservice.IntentAPI) *mautrix.RespJoinedMembers { +func (mh *matrixHandler) joinAndCheckMembers(evt *event.Event, intent *appservice.IntentAPI) int { resp, err := intent.JoinRoomByID(evt.RoomID) if err != nil { mh.log.Debugfln("Failed to join room %q as %q with invite from %q: %v", evt.RoomID, intent.UserID, evt.Sender, err) - return nil + return 0 } - members, err := intent.JoinedMembers(resp.RoomID) + members, err := intent.Members(resp.RoomID) if err != nil { mh.log.Debugfln("Failed to get members in room %q with invite from %q as %q: %v", resp.RoomID, evt.Sender, intent.UserID, err) - return nil + return 0 } - if len(members.Joined) < 2 { + if len(members.Chunk) < 2 { mh.log.Debugfln("Leaving empty room %q with invite from %q as %q", resp.RoomID, evt.Sender, intent.UserID) intent.LeaveRoom(resp.RoomID) - return nil + return 0 } - return members + return len(members.Chunk) } func (mh *matrixHandler) sendNoticeWithmarkdown(roomID id.RoomID, message string) (*mautrix.RespSendEvent, error) { @@ -149,24 +149,24 @@ func (mh *matrixHandler) handleBotInvite(evt *event.Event) { } members := mh.joinAndCheckMembers(evt, intent) - if members == nil { + if members == 0 { return } // If this is a DM and the user doesn't have a management room, make this // the management room. - if len(members.Joined) == 2 && (user.ManagementRoom == "" || evt.Content.AsMember().IsDirect) { + if members == 2 && (user.ManagementRoom == "" || evt.Content.AsMember().IsDirect) { user.SetManagementRoom(evt.RoomID) intent.SendNotice(user.ManagementRoom, "This room has been registered as your bridge management/status room") mh.log.Debugfln("%q registered as management room with %q", evt.RoomID, evt.Sender) } - // Wait to send the welcome message until we're sure we're not in an empty - // room. - mh.sendNoticeWithmarkdown(evt.RoomID, mh.bridge.Config.Bridge.ManagementRoomText.Welcome) - if evt.RoomID == user.ManagementRoom { + // Wait to send the welcome message until we're sure we're not in an empty + // room. + mh.sendNoticeWithmarkdown(evt.RoomID, mh.bridge.Config.Bridge.ManagementRoomText.Welcome) + if user.Connected() { mh.sendNoticeWithmarkdown(evt.RoomID, mh.bridge.Config.Bridge.ManagementRoomText.Connected) } else { From 1a660806cb1e2915eba0934fa04483a3df552255 Mon Sep 17 00:00:00 2001 From: Gary Kramlich Date: Mon, 2 May 2022 12:40:19 -0500 Subject: [PATCH 15/15] Fix a crash caused uncached open graphs previews If the discord open graph stuff has a cached preview it'll pass it along on the initial MessageCreate message. However, if it doesn't, it'll later send a MessageUpdate with the new embed and a message that doesn't have an author set as it's coming from the server. --- bridge/portal.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/bridge/portal.go b/bridge/portal.go index 36f0e28..6eb7c42 100644 --- a/bridge/portal.go +++ b/bridge/portal.go @@ -487,6 +487,23 @@ func (p *Portal) handleDiscordMessagesUpdate(user *User, msg *discordgo.Message) return } + // There's a few scenarios where the author is nil but I haven't figured + // them all out yet. + if msg.Author == nil { + // If the server has to lookup opengraph previews it'll send the + // message through without the preview and then add the preview later + // via a message update. However, when it does this there is no author + // as it's just the server, so for the moment we'll ignore this to + // avoid a crash. + if len(msg.Embeds) > 0 { + p.log.Debugln("ignoring update for opengraph attachment") + + return + } + + p.log.Errorfln("author is nil: %#v", msg) + } + intent := p.bridge.GetPuppetByID(msg.Author.ID).IntentFor(p) existing := p.bridge.db.Message.GetByDiscordID(p.Key, msg.ID)