From bd01c661ef45c7f78c124a22c4e7d734256156dc Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 21 Aug 2023 13:52:12 +0300 Subject: [PATCH] Move double puppeting login code to mautrix-go --- commands.go | 7 +- config/bridge.go | 10 +- config/config.go | 2 +- config/upgrade.go | 1 - custompuppet.go | 250 +++++--------------------------------------- example-config.yaml | 6 +- go.mod | 2 +- go.sum | 4 +- user.go | 24 ----- 9 files changed, 38 insertions(+), 268 deletions(-) diff --git a/commands.go b/commands.go index 7025466..1dcd8da 100644 --- a/commands.go +++ b/commands.go @@ -559,12 +559,7 @@ func fnLogout(ce *WrappedCommandEvent) { return } puppet := ce.Bridge.GetPuppetByJID(ce.User.JID) - if puppet.CustomMXID != "" { - err := puppet.SwitchCustomMXID("", "") - if err != nil { - ce.User.log.Warnln("Failed to logout-matrix while logging out of WhatsApp:", err) - } - } + puppet.ClearCustomMXID() err := ce.User.Client.Logout() if err != nil { ce.User.log.Warnln("Error while logging out:", err) diff --git a/config/bridge.go b/config/bridge.go index 2393cef..4e5a8b0 100644 --- a/config/bridge.go +++ b/config/bridge.go @@ -85,18 +85,14 @@ type BridgeConfig struct { UserAvatarSync bool `yaml:"user_avatar_sync"` BridgeMatrixLeave bool `yaml:"bridge_matrix_leave"` - SyncWithCustomPuppets bool `yaml:"sync_with_custom_puppets"` SyncDirectChatList bool `yaml:"sync_direct_chat_list"` SyncManualMarkedUnread bool `yaml:"sync_manual_marked_unread"` - DefaultBridgeReceipts bool `yaml:"default_bridge_receipts"` DefaultBridgePresence bool `yaml:"default_bridge_presence"` SendPresenceOnTyping bool `yaml:"send_presence_on_typing"` ForceActiveDeliveryReceipts bool `yaml:"force_active_delivery_receipts"` - DoublePuppetServerMap map[string]string `yaml:"double_puppet_server_map"` - DoublePuppetAllowDiscovery bool `yaml:"double_puppet_allow_discovery"` - LoginSharedSecretMap map[string]string `yaml:"login_shared_secret_map"` + DoublePuppetConfig bridgeconfig.DoublePuppetConfig `yaml:",inline"` PrivateChatPortalMeta string `yaml:"private_chat_portal_meta"` ParallelMemberSync bool `yaml:"parallel_member_sync"` @@ -151,6 +147,10 @@ type BridgeConfig struct { displaynameTemplate *template.Template `yaml:"-"` } +func (bc BridgeConfig) GetDoublePuppetConfig() bridgeconfig.DoublePuppetConfig { + return bc.DoublePuppetConfig +} + func (bc BridgeConfig) GetEncryptionConfig() bridgeconfig.EncryptionConfig { return bc.Encryption } diff --git a/config/config.go b/config/config.go index bb7ec25..2aaab9a 100644 --- a/config/config.go +++ b/config/config.go @@ -42,6 +42,6 @@ type Config struct { func (config *Config) CanAutoDoublePuppet(userID id.UserID) bool { _, homeserver, _ := userID.Parse() - _, hasSecret := config.Bridge.LoginSharedSecretMap[homeserver] + _, hasSecret := config.Bridge.DoublePuppetConfig.SharedSecretMap[homeserver] return hasSecret } diff --git a/config/upgrade.go b/config/upgrade.go index 02ae8fe..b405130 100644 --- a/config/upgrade.go +++ b/config/upgrade.go @@ -61,7 +61,6 @@ func DoUpgrade(helper *up.Helper) { helper.Copy(up.List, "bridge", "history_sync", "deferred") helper.Copy(up.Bool, "bridge", "user_avatar_sync") helper.Copy(up.Bool, "bridge", "bridge_matrix_leave") - helper.Copy(up.Bool, "bridge", "sync_with_custom_puppets") helper.Copy(up.Bool, "bridge", "sync_direct_chat_list") helper.Copy(up.Bool, "bridge", "default_bridge_receipts") helper.Copy(up.Bool, "bridge", "default_bridge_presence") diff --git a/custompuppet.go b/custompuppet.go index 8fe6099..af00a28 100644 --- a/custompuppet.go +++ b/custompuppet.go @@ -17,262 +17,66 @@ package main import ( - "crypto/hmac" - "crypto/sha512" - "encoding/hex" - "errors" - "fmt" - "time" - - "maunium.net/go/mautrix" - "maunium.net/go/mautrix/appservice" - "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" ) -var ( - ErrNoCustomMXID = errors.New("no custom mxid set") - ErrMismatchingMXID = errors.New("whoami result does not match custom mxid") -) - func (puppet *Puppet) SwitchCustomMXID(accessToken string, mxid id.UserID) error { - prevCustomMXID := puppet.CustomMXID - if puppet.customIntent != nil { - puppet.stopSyncing() - } puppet.CustomMXID = mxid puppet.AccessToken = accessToken - + puppet.EnablePresence = puppet.bridge.Config.Bridge.DefaultBridgePresence + puppet.Update() err := puppet.StartCustomMXID(false) if err != nil { return err } - - if len(prevCustomMXID) > 0 { - delete(puppet.bridge.puppetsByCustomMXID, prevCustomMXID) - } - if len(puppet.CustomMXID) > 0 { - puppet.bridge.puppetsByCustomMXID[puppet.CustomMXID] = puppet - } - puppet.EnablePresence = puppet.bridge.Config.Bridge.DefaultBridgePresence - puppet.EnableReceipts = puppet.bridge.Config.Bridge.DefaultBridgeReceipts - puppet.bridge.AS.StateStore.MarkRegistered(puppet.CustomMXID) - puppet.Update() // TODO leave rooms with default puppet return nil } -func (puppet *Puppet) loginWithSharedSecret(mxid id.UserID) (string, error) { - _, homeserver, _ := mxid.Parse() - puppet.log.Debugfln("Logging into %s with shared secret", mxid) - loginSecret := puppet.bridge.Config.Bridge.LoginSharedSecretMap[homeserver] - client, err := puppet.bridge.newDoublePuppetClient(mxid, "") - if err != nil { - return "", fmt.Errorf("failed to create mautrix client to log in: %v", err) - } - req := mautrix.ReqLogin{ - Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: string(mxid)}, - DeviceID: "WhatsApp Bridge", - InitialDeviceDisplayName: "WhatsApp Bridge", - } - if loginSecret == "appservice" { - client.AccessToken = puppet.bridge.AS.Registration.AppToken - req.Type = mautrix.AuthTypeAppservice - } else { - mac := hmac.New(sha512.New, []byte(loginSecret)) - mac.Write([]byte(mxid)) - req.Password = hex.EncodeToString(mac.Sum(nil)) - req.Type = mautrix.AuthTypePassword - } - resp, err := client.Login(&req) - if err != nil { - return "", err - } - return resp.AccessToken, nil -} - -func (br *WABridge) newDoublePuppetClient(mxid id.UserID, accessToken string) (*mautrix.Client, error) { - _, homeserver, err := mxid.Parse() - if err != nil { - return nil, err - } - homeserverURL, found := br.Config.Bridge.DoublePuppetServerMap[homeserver] - if !found { - if homeserver == br.AS.HomeserverDomain { - homeserverURL = "" - } else if br.Config.Bridge.DoublePuppetAllowDiscovery { - resp, err := mautrix.DiscoverClientAPI(homeserver) - if err != nil { - return nil, fmt.Errorf("failed to find homeserver URL for %s: %v", homeserver, err) - } - homeserverURL = resp.Homeserver.BaseURL - br.Log.Debugfln("Discovered URL %s for %s to enable double puppeting for %s", homeserverURL, homeserver, mxid) - } else { - return nil, fmt.Errorf("double puppeting from %s is not allowed", homeserver) - } - } - return br.AS.NewExternalMautrixClient(mxid, accessToken, homeserverURL) -} - -func (puppet *Puppet) newCustomIntent() (*appservice.IntentAPI, error) { - if len(puppet.CustomMXID) == 0 { - return nil, ErrNoCustomMXID - } - client, err := puppet.bridge.newDoublePuppetClient(puppet.CustomMXID, puppet.AccessToken) - if err != nil { - return nil, err - } - client.Syncer = puppet - client.Store = puppet - - ia := puppet.bridge.AS.NewIntentAPI("custom") - ia.Client = client - ia.Localpart, _, _ = puppet.CustomMXID.Parse() - ia.UserID = puppet.CustomMXID - ia.IsCustomPuppet = true - return ia, nil -} - -func (puppet *Puppet) clearCustomMXID() { +func (puppet *Puppet) ClearCustomMXID() { + save := puppet.CustomMXID != "" || puppet.AccessToken != "" puppet.CustomMXID = "" puppet.AccessToken = "" puppet.customIntent = nil puppet.customUser = nil + if save { + puppet.Update() + } } func (puppet *Puppet) StartCustomMXID(reloginOnFail bool) error { - if len(puppet.CustomMXID) == 0 { - puppet.clearCustomMXID() - return nil - } - intent, err := puppet.newCustomIntent() + newIntent, newAccessToken, err := puppet.bridge.DoublePuppet.Setup(puppet.CustomMXID, puppet.AccessToken, reloginOnFail) if err != nil { - puppet.clearCustomMXID() + puppet.ClearCustomMXID() return err } - resp, err := intent.Whoami() - if err != nil { - if !reloginOnFail || (errors.Is(err, mautrix.MUnknownToken) && !puppet.tryRelogin(err, "initializing double puppeting")) { - puppet.clearCustomMXID() - return err - } - intent.AccessToken = puppet.AccessToken - } else if resp.UserID != puppet.CustomMXID { - puppet.clearCustomMXID() - return ErrMismatchingMXID + if puppet.AccessToken != newAccessToken { + puppet.AccessToken = newAccessToken + puppet.Update() } - puppet.customIntent = intent + puppet.customIntent = newIntent puppet.customUser = puppet.bridge.GetUserByMXID(puppet.CustomMXID) - puppet.startSyncing() return nil } -func (puppet *Puppet) startSyncing() { - if !puppet.bridge.Config.Bridge.SyncWithCustomPuppets { +func (user *User) tryAutomaticDoublePuppeting() { + if !user.bridge.Config.CanAutoDoublePuppet(user.MXID) { return } - go func() { - puppet.log.Debugln("Starting syncing...") - puppet.customIntent.SyncPresence = "offline" - err := puppet.customIntent.Sync() - if err != nil { - puppet.log.Errorln("Fatal error syncing:", err) - } - }() -} - -func (puppet *Puppet) stopSyncing() { - if !puppet.bridge.Config.Bridge.SyncWithCustomPuppets { + user.zlog.Debug().Msg("Checking if double puppeting needs to be enabled") + puppet := user.bridge.GetPuppetByJID(user.JID) + if len(puppet.CustomMXID) > 0 { + user.zlog.Debug().Msg("User already has double-puppeting enabled") + // Custom puppet already enabled return } - puppet.customIntent.StopSync() -} - -func (puppet *Puppet) ProcessResponse(resp *mautrix.RespSync, _ string) error { - if !puppet.customUser.IsLoggedIn() { - puppet.log.Debugln("Skipping sync processing: custom user not connected to whatsapp") - return nil - } - for roomID, events := range resp.Rooms.Join { - for _, evt := range events.Ephemeral.Events { - evt.RoomID = roomID - err := evt.Content.ParseRaw(evt.Type) - if err != nil { - continue - } - switch evt.Type { - case event.EphemeralEventReceipt: - if puppet.EnableReceipts { - go puppet.bridge.MatrixHandler.HandleReceipt(evt) - } - case event.EphemeralEventTyping: - go puppet.bridge.MatrixHandler.HandleTyping(evt) - } - } - } - if puppet.EnablePresence { - for _, evt := range resp.Presence.Events { - if evt.Sender != puppet.CustomMXID { - continue - } - err := evt.Content.ParseRaw(evt.Type) - if err != nil { - continue - } - go puppet.bridge.HandlePresence(evt) - } - } - return nil -} - -func (puppet *Puppet) tryRelogin(cause error, action string) bool { - if !puppet.bridge.Config.CanAutoDoublePuppet(puppet.CustomMXID) { - return false - } - puppet.log.Debugfln("Trying to relogin after '%v' while %s", cause, action) - accessToken, err := puppet.loginWithSharedSecret(puppet.CustomMXID) + puppet.CustomMXID = user.MXID + puppet.EnablePresence = puppet.bridge.Config.Bridge.DefaultBridgePresence + err := puppet.StartCustomMXID(true) if err != nil { - puppet.log.Errorfln("Failed to relogin after '%v' while %s: %v", cause, action, err) - return false - } - puppet.log.Infofln("Successfully relogined after '%v' while %s", cause, action) - puppet.AccessToken = accessToken - return true -} - -func (puppet *Puppet) OnFailedSync(_ *mautrix.RespSync, err error) (time.Duration, error) { - puppet.log.Warnln("Sync error:", err) - if errors.Is(err, mautrix.MUnknownToken) { - if !puppet.tryRelogin(err, "syncing") { - return 0, err - } - puppet.customIntent.AccessToken = puppet.AccessToken - return 0, nil - } - return 10 * time.Second, nil -} - -func (puppet *Puppet) GetFilterJSON(_ id.UserID) *mautrix.Filter { - everything := []event.Type{{Type: "*"}} - return &mautrix.Filter{ - Presence: mautrix.FilterPart{ - Senders: []id.UserID{puppet.CustomMXID}, - Types: []event.Type{event.EphemeralEventPresence}, - }, - AccountData: mautrix.FilterPart{NotTypes: everything}, - Room: mautrix.RoomFilter{ - Ephemeral: mautrix.FilterPart{Types: []event.Type{event.EphemeralEventTyping, event.EphemeralEventReceipt}}, - IncludeLeave: false, - AccountData: mautrix.FilterPart{NotTypes: everything}, - State: mautrix.FilterPart{NotTypes: everything}, - Timeline: mautrix.FilterPart{NotTypes: everything}, - }, + user.zlog.Warn().Err(err).Msg("Failed to login with shared secret") + } else { + // TODO leave rooms with default puppet + user.zlog.Debug().Msg("Successfully automatically enabled custom puppet") } } - -func (puppet *Puppet) SaveFilterID(_ id.UserID, _ string) {} -func (puppet *Puppet) SaveNextBatch(_ id.UserID, nbt string) { puppet.NextBatch = nbt; puppet.Update() } -func (puppet *Puppet) SaveRoom(_ *mautrix.Room) {} -func (puppet *Puppet) LoadFilterID(_ id.UserID) string { return "" } -func (puppet *Puppet) LoadNextBatch(_ id.UserID) string { return puppet.NextBatch } -func (puppet *Puppet) LoadRoom(_ id.RoomID) *mautrix.Room { return nil } diff --git a/example-config.yaml b/example-config.yaml index f5a347a..be56be3 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -65,7 +65,6 @@ appservice: # Whether or not to receive ephemeral events via appservice transactions. # Requires MSC2409 support (i.e. Synapse 1.22+). - # You should disable bridge -> sync_with_custom_puppets when this is enabled. ephemeral_events: true # Should incoming events be handled asynchronously? @@ -211,8 +210,6 @@ bridge: user_avatar_sync: true # Should Matrix users leaving groups be bridged to WhatsApp? bridge_matrix_leave: true - # Should the bridge sync with double puppeting to receive EDUs that aren't normally sent to appservices. - sync_with_custom_puppets: false # Should the bridge update the m.direct account data event when double puppeting is enabled. # Note that updating the m.direct event is not atomic (except with mautrix-asmux) # and is therefore prone to race conditions. @@ -223,9 +220,8 @@ bridge: # com.famedly.marked_unread room account data. sync_manual_marked_unread: true # When double puppeting is enabled, users can use `!wa toggle` to change whether - # presence and read receipts are bridged. These settings set the default values. + # presence is bridged. This setting sets the default value. # Existing users won't be affected when these are changed. - default_bridge_receipts: true default_bridge_presence: true # Send the presence as "available" to whatsapp when users start typing on a portal. # This works as a workaround for homeservers that do not support presence, and allows diff --git a/go.mod b/go.mod index 2fb04ad..4e92fc7 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( golang.org/x/net v0.14.0 google.golang.org/protobuf v1.31.0 maunium.net/go/maulogger/v2 v2.4.1 - maunium.net/go/mautrix v0.16.0 + maunium.net/go/mautrix v0.16.1-0.20230821105106-ac5c2c22102c ) require ( diff --git a/go.sum b/go.sum index 45b12f9..9f5cbcb 100644 --- a/go.sum +++ b/go.sum @@ -133,5 +133,5 @@ maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= maunium.net/go/maulogger/v2 v2.4.1 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8= maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho= -maunium.net/go/mautrix v0.16.0 h1:iUqCzJE2yqBC1ddAK6eAn159My8rLb4X8g4SFtQh2Dk= -maunium.net/go/mautrix v0.16.0/go.mod h1:XAjE9pTSGcr6vXaiNgQGiip7tddJ8FQV1a29u2QdBG4= +maunium.net/go/mautrix v0.16.1-0.20230821105106-ac5c2c22102c h1:oRIaFbS4ds9biwJVguT+9Zu7n5zDbKQeuGklXHQxvCU= +maunium.net/go/mautrix v0.16.1-0.20230821105106-ac5c2c22102c/go.mod h1:XAjE9pTSGcr6vXaiNgQGiip7tddJ8FQV1a29u2QdBG4= diff --git a/user.go b/user.go index 8c110b4..02c8b57 100644 --- a/user.go +++ b/user.go @@ -612,30 +612,6 @@ func (user *User) IsLoggedIn() bool { return user.IsConnected() && user.Client.IsLoggedIn() } -func (user *User) tryAutomaticDoublePuppeting() { - if !user.bridge.Config.CanAutoDoublePuppet(user.MXID) { - return - } - user.log.Debugln("Checking if double puppeting needs to be enabled") - puppet := user.bridge.GetPuppetByJID(user.JID) - if len(puppet.CustomMXID) > 0 { - user.log.Debugln("User already has double-puppeting enabled") - // Custom puppet already enabled - return - } - accessToken, err := puppet.loginWithSharedSecret(user.MXID) - if err != nil { - user.log.Warnln("Failed to login with shared secret:", err) - return - } - err = puppet.SwitchCustomMXID(accessToken, user.MXID) - if err != nil { - puppet.log.Warnln("Failed to switch to auto-logined custom puppet:", err) - return - } - user.log.Infoln("Successfully automatically enabled custom puppet") -} - func (user *User) sendMarkdownBridgeAlert(formatString string, args ...interface{}) { if user.bridge.Config.Bridge.DisableBridgeAlerts { return