diff --git a/config/bridge.go b/config/bridge.go index 625e860..4168929 100644 --- a/config/bridge.go +++ b/config/bridge.go @@ -60,6 +60,7 @@ type BridgeConfig struct { SyncChatMaxAge uint64 `yaml:"sync_max_chat_age"` SyncWithCustomPuppets bool `yaml:"sync_with_custom_puppets"` + SyncDirectChatList bool `yaml:"sync_direct_chat_list"` DefaultBridgeReceipts bool `yaml:"default_bridge_receipts"` DefaultBridgePresence bool `yaml:"default_bridge_presence"` LoginSharedSecret string `yaml:"login_shared_secret"` diff --git a/config/config.go b/config/config.go index 8abb8ba..3956e40 100644 --- a/config/config.go +++ b/config/config.go @@ -28,6 +28,7 @@ type Config struct { Homeserver struct { Address string `yaml:"address"` Domain string `yaml:"domain"` + Asmux bool `yaml:"asmux"` } `yaml:"homeserver"` AppService struct { diff --git a/crypto.go b/crypto.go index e2d6157..3df27ce 100644 --- a/crypto.go +++ b/crypto.go @@ -128,8 +128,8 @@ func (helper *CryptoHelper) loginBot() (*mautrix.Client, error) { return nil, err } resp, err := client.Login(&mautrix.ReqLogin{ - Type: "m.login.password", - Identifier: mautrix.UserIdentifier{Type: "m.id.user", User: string(helper.bridge.AS.BotMXID())}, + Type: mautrix.AuthTypePassword, + Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: string(helper.bridge.AS.BotMXID())}, Password: hex.EncodeToString(mac.Sum(nil)), DeviceID: deviceID, InitialDeviceDisplayName: "WhatsApp Bridge", diff --git a/custompuppet.go b/custompuppet.go index 58d2dab..39218d5 100644 --- a/custompuppet.go +++ b/custompuppet.go @@ -68,8 +68,8 @@ func (puppet *Puppet) loginWithSharedSecret(mxid id.UserID) (string, error) { mac := hmac.New(sha512.New, []byte(puppet.bridge.Config.Bridge.LoginSharedSecret)) mac.Write([]byte(mxid)) resp, err := puppet.bridge.AS.BotClient().Login(&mautrix.ReqLogin{ - Type: "m.login.password", - Identifier: mautrix.UserIdentifier{Type: "m.id.user", User: string(mxid)}, + Type: mautrix.AuthTypePassword, + Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: string(mxid)}, Password: hex.EncodeToString(mac.Sum(nil)), DeviceID: "WhatsApp Bridge", InitialDeviceDisplayName: "WhatsApp Bridge", diff --git a/database/portal.go b/database/portal.go index 194d9f3..fc235a3 100644 --- a/database/portal.go +++ b/database/portal.go @@ -84,6 +84,10 @@ func (pq *PortalQuery) GetAllByJID(jid types.WhatsAppID) []*Portal { return pq.getAll("SELECT * FROM portal WHERE jid=$1", jid) } +func (pq *PortalQuery) FindPrivateChats(receiver types.WhatsAppID) []*Portal { + return pq.getAll("SELECT * FROM portal WHERE receiver=$1 AND jid LIKE '%@s.whatsapp.net'", receiver) +} + func (pq *PortalQuery) getAll(query string, args ...interface{}) (portals []*Portal) { rows, err := pq.db.Query(query, args...) if err != nil || rows == nil { diff --git a/example-config.yaml b/example-config.yaml index 0f5da51..d496ae4 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -138,6 +138,10 @@ bridge: # Whether or not to sync with custom puppets to receive EDUs that # are not normally sent to appservices. sync_with_custom_puppets: true + # Whether or not to 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. + sync_direct_chat_list: false # When double puppeting is enabled, users can use `!wa toggle` to change whether or not # presence and read receipts are bridged. These settings set the default values. # Existing users won't be affected when these are changed. diff --git a/go.mod b/go.mod index fac1f93..ea7e286 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( gopkg.in/yaml.v2 v2.3.0 maunium.net/go/mauflag v1.0.0 maunium.net/go/maulogger/v2 v2.1.1 - maunium.net/go/mautrix v0.7.0 + maunium.net/go/mautrix v0.7.2 ) replace github.com/Rhymen/go-whatsapp => github.com/tulir/go-whatsapp v0.3.7 diff --git a/go.sum b/go.sum index d7771d7..c1882fd 100644 --- a/go.sum +++ b/go.sum @@ -221,3 +221,5 @@ maunium.net/go/mautrix v0.7.0-rc.3 h1:GVmrVvY5vDASMyZ2xJ9kNynWsgqKl1yerKP7c6RsM7 maunium.net/go/mautrix v0.7.0-rc.3/go.mod h1:Va/74MijqaS0DQ3aUqxmFO54/PMfr1LVsCOcGRHbYmo= maunium.net/go/mautrix v0.7.0 h1:9Wxs5S4Wl4S99dbBwfLZYAe/sP7VKaFikw9Ocf88kfk= maunium.net/go/mautrix v0.7.0/go.mod h1:Va/74MijqaS0DQ3aUqxmFO54/PMfr1LVsCOcGRHbYmo= +maunium.net/go/mautrix v0.7.2 h1:ru//jj7Y5Xj9CXBpeNyWCoxjq8iT0d+a2lNeSiN9P/o= +maunium.net/go/mautrix v0.7.2/go.mod h1:Va/74MijqaS0DQ3aUqxmFO54/PMfr1LVsCOcGRHbYmo= diff --git a/portal.go b/portal.go index 89c2b58..aa082e7 100644 --- a/portal.go +++ b/portal.go @@ -1038,6 +1038,8 @@ func (portal *Portal) CreateMatrixRoom(user *User) error { portal.log.Errorln("Failed to join created portal with bridge bot for e2be:", err) } } + + user.UpdateDirectChats(map[id.UserID][]id.RoomID{puppet.MXID: {portal.MXID}}) } err = portal.FillInitialHistory(user) if err != nil { diff --git a/puppet.go b/puppet.go index dd6880d..0c3d82c 100644 --- a/puppet.go +++ b/puppet.go @@ -124,18 +124,22 @@ func (bridge *Bridge) dbPuppetsToPuppets(dbPuppets []*database.Puppet) []*Puppet return output } +func (bridge *Bridge) FormatPuppetMXID(jid types.WhatsAppID) id.UserID { + return id.NewUserID( + bridge.Config.Bridge.FormatUsername( + strings.Replace( + jid, + whatsappExt.NewUserSuffix, "", 1)), + bridge.Config.Homeserver.Domain) +} + func (bridge *Bridge) NewPuppet(dbPuppet *database.Puppet) *Puppet { return &Puppet{ Puppet: dbPuppet, bridge: bridge, log: bridge.Log.Sub(fmt.Sprintf("Puppet/%s", dbPuppet.JID)), - MXID: id.NewUserID( - bridge.Config.Bridge.FormatUsername( - strings.Replace( - dbPuppet.JID, - whatsappExt.NewUserSuffix, "", 1)), - bridge.Config.Homeserver.Domain), + MXID: bridge.FormatPuppetMXID(dbPuppet.JID), } } diff --git a/user.go b/user.go index d0fad27..73153e9 100644 --- a/user.go +++ b/user.go @@ -19,6 +19,7 @@ package main import ( "encoding/json" "fmt" + "net/http" "sort" "strconv" "strings" @@ -28,12 +29,12 @@ import ( "github.com/pkg/errors" "github.com/skip2/go-qrcode" log "maunium.net/go/maulogger/v2" - "maunium.net/go/mautrix" "github.com/Rhymen/go-whatsapp" waBinary "github.com/Rhymen/go-whatsapp/binary" waProto "github.com/Rhymen/go-whatsapp/binary/proto" + "maunium.net/go/mautrix" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/format" "maunium.net/go/mautrix/id" @@ -573,6 +574,7 @@ func (user *User) syncPortals(chatMap map[string]whatsapp.Chat, createAll bool) } } } + user.UpdateDirectChats(nil) user.log.Infoln("Finished syncing portals") select { case user.syncPortalsDone <- struct{}{}: @@ -580,6 +582,61 @@ func (user *User) syncPortals(chatMap map[string]whatsapp.Chat, createAll bool) } } +func (user *User) getDirectChats() map[id.UserID][]id.RoomID { + res := make(map[id.UserID][]id.RoomID) + privateChats := user.bridge.DB.Portal.FindPrivateChats(user.JID) + for _, portal := range privateChats { + if len(portal.MXID) > 0 { + res[user.bridge.FormatPuppetMXID(portal.Key.JID)] = []id.RoomID{portal.MXID} + } + } + return res +} + +func (user *User) UpdateDirectChats(chats map[id.UserID][]id.RoomID) { + if !user.bridge.Config.Bridge.SyncDirectChatList { + return + } + puppet := user.bridge.GetPuppetByCustomMXID(user.MXID) + if puppet == nil || puppet.CustomIntent() == nil { + return + } + intent := puppet.CustomIntent() + method := http.MethodPatch + if chats == nil { + chats = user.getDirectChats() + method = http.MethodPut + } + user.log.Debugln("Updating m.direct list on homeserver") + var err error + if user.bridge.Config.Homeserver.Asmux { + urlPath := intent.BuildBaseURL("_matrix", "client", "unstable", "net.maunium.asmux", "dms") + _, err = intent.MakeFullRequest(method, urlPath, http.Header{ + "X-Asmux-Auth": {user.bridge.AS.Registration.AppToken}, + }, chats, nil) + } else { + existingChats := make(map[id.UserID][]id.RoomID) + err = intent.GetAccountData(event.AccountDataDirectChats.Type, &existingChats) + if err != nil { + user.log.Warnln("Failed to get m.direct list to update it:", err) + return + } + for userID, rooms := range existingChats { + if _, ok := user.bridge.ParsePuppetMXID(userID); !ok { + // This is not a ghost user, include it in the new list + chats[userID] = rooms + } else if _, ok := chats[userID]; !ok && method == http.MethodPatch { + // This is a ghost user, but we're not replacing the whole list, so include it too + chats[userID] = rooms + } + } + err = intent.SetAccountData(event.AccountDataDirectChats.Type, &chats) + } + if err != nil { + user.log.Warnln("Failed to update m.direct list:", err) + } +} + func (user *User) HandleContactList(contacts []whatsapp.Contact) { contactMap := make(map[string]whatsapp.Contact) for _, contact := range contacts {