From 1d8ef6cb89a0eabc953142fd42c9172c4ea76d07 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 7 Dec 2021 16:02:51 +0200 Subject: [PATCH] Add support for MSC2409 --- config/config.go | 2 ++ config/registration.go | 1 + config/upgrade.go | 1 + custompuppet.go | 64 ++---------------------------------------- example-config.yaml | 5 ++++ matrix.go | 64 +++++++++++++++++++++++++++++++++++++++++- portal.go | 53 ++++++++++++++++++++++++++++++++++ puppet.go | 5 ++-- user.go | 16 +++++++++-- 9 files changed, 144 insertions(+), 67 deletions(-) diff --git a/config/config.go b/config/config.go index 9cd8cae..b35b869 100644 --- a/config/config.go +++ b/config/config.go @@ -61,6 +61,8 @@ type Config struct { Avatar string `yaml:"avatar"` } `yaml:"bot"` + EphemeralEvents bool `yaml:"ephemeral_events"` + ASToken string `yaml:"as_token"` HSToken string `yaml:"hs_token"` } `yaml:"appservice"` diff --git a/config/registration.go b/config/registration.go index c32716c..a982279 100644 --- a/config/registration.go +++ b/config/registration.go @@ -61,6 +61,7 @@ func (config *Config) copyToRegistration(registration *appservice.Registration) falseVal := false registration.RateLimited = &falseVal registration.SenderLocalpart = config.AppService.Bot.Username + registration.EphemeralEvents = config.AppService.EphemeralEvents userIDRegex, err := regexp.Compile(fmt.Sprintf("^@%s:%s$", config.Bridge.FormatUsername("[0-9]+"), diff --git a/config/upgrade.go b/config/upgrade.go index 62c2585..7ab8f78 100644 --- a/config/upgrade.go +++ b/config/upgrade.go @@ -51,6 +51,7 @@ func (helper *UpgradeHelper) doUpgrade() { helper.Copy(Str, "appservice", "bot", "username") helper.Copy(Str, "appservice", "bot", "displayname") helper.Copy(Str, "appservice", "bot", "avatar") + helper.Copy(Bool, "appservice", "ephemeral_events") helper.Copy(Str, "appservice", "as_token") helper.Copy(Str, "appservice", "hs_token") diff --git a/custompuppet.go b/custompuppet.go index e8b4e4f..e41bd45 100644 --- a/custompuppet.go +++ b/custompuppet.go @@ -24,8 +24,6 @@ import ( "fmt" "time" - "go.mau.fi/whatsmeow/types" - "maunium.net/go/mautrix" "maunium.net/go/mautrix/appservice" "maunium.net/go/mautrix/event" @@ -139,7 +137,6 @@ func (puppet *Puppet) clearCustomMXID() { puppet.CustomMXID = "" puppet.AccessToken = "" puppet.customIntent = nil - puppet.customTypingIn = nil puppet.customUser = nil } @@ -165,7 +162,6 @@ func (puppet *Puppet) StartCustomMXID(reloginOnFail bool) error { return ErrMismatchingMXID } puppet.customIntent = intent - puppet.customTypingIn = make(map[id.RoomID]bool) puppet.customUser = puppet.bridge.GetUserByMXID(puppet.CustomMXID) puppet.startSyncing() return nil @@ -210,10 +206,10 @@ func (puppet *Puppet) ProcessResponse(resp *mautrix.RespSync, _ string) error { switch evt.Type { case event.EphemeralEventReceipt: if puppet.EnableReceipts { - go puppet.handleReceiptEvent(portal, evt) + go puppet.bridge.MatrixHandler.HandleReceipt(evt) } case event.EphemeralEventTyping: - go puppet.handleTypingEvent(portal, evt) + go puppet.bridge.MatrixHandler.HandleTyping(evt) } } } @@ -226,66 +222,12 @@ func (puppet *Puppet) ProcessResponse(resp *mautrix.RespSync, _ string) error { if err != nil { continue } - go puppet.handlePresenceEvent(evt) + go puppet.bridge.MatrixHandler.HandlePresence(evt) } } return nil } -func (puppet *Puppet) handlePresenceEvent(event *event.Event) { - presence := types.PresenceAvailable - if event.Content.Raw["presence"].(string) != "online" { - presence = types.PresenceUnavailable - puppet.customUser.log.Debugln("Marking offline") - } else { - puppet.customUser.log.Debugln("Marking online") - } - puppet.customUser.lastPresence = presence - if puppet.customUser.Client.Store.PushName != "" { - err := puppet.customUser.Client.SendPresence(presence) - if err != nil { - puppet.customUser.log.Warnln("Failed to set presence:", err) - } - } -} - -func (puppet *Puppet) handleReceiptEvent(portal *Portal, event *event.Event) { - for eventID, receipts := range *event.Content.AsReceipt() { - if receipt, ok := receipts.Read[puppet.CustomMXID]; !ok { - // Ignore receipt events where this user isn't present. - } else if isDoublePuppeted, _ := receipt.Extra[doublePuppetField].(bool); isDoublePuppeted { - puppet.customUser.log.Debugfln("Ignoring double puppeted read receipt %+v", event.Content.Raw) - // Ignore double puppeted read receipts. - } else { - portal.HandleMatrixReadReceipt(puppet.customUser, eventID, time.UnixMilli(receipt.Timestamp)) - } - } -} - -func (puppet *Puppet) handleTypingEvent(portal *Portal, evt *event.Event) { - isTyping := false - for _, userID := range evt.Content.AsTyping().UserIDs { - if userID == puppet.CustomMXID { - isTyping = true - break - } - } - if puppet.customTypingIn[evt.RoomID] != isTyping { - puppet.customTypingIn[evt.RoomID] = isTyping - presence := types.ChatPresenceComposing - if !isTyping { - puppet.customUser.log.Debugfln("Marking not typing in %s/%s", portal.Key.JID, portal.MXID) - presence = types.ChatPresencePaused - } else { - puppet.customUser.log.Debugfln("Marking typing in %s/%s", portal.Key.JID, portal.MXID) - } - err := puppet.customUser.Client.SendChatPresence(presence, portal.Key.JID) - if err != nil { - puppet.customUser.log.Warnln("Error setting typing:", err) - } - } -} - func (puppet *Puppet) tryRelogin(cause error, action string) bool { if !puppet.bridge.Config.CanAutoDoublePuppet(puppet.CustomMXID) { return false diff --git a/example-config.yaml b/example-config.yaml index b6893ce..89afc1c 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -56,6 +56,11 @@ appservice: displayname: WhatsApp bridge bot avatar: mxc://maunium.net/NeXNQarUbrlYBiPCpprYsRqr + # 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: false + # Authentication tokens for AS <-> HS communication. Autogenerated; do not modify. as_token: "This value is generated when generating the registration" hs_token: "This value is generated when generating the registration" diff --git a/matrix.go b/matrix.go index 6850c0a..03fee7e 100644 --- a/matrix.go +++ b/matrix.go @@ -1,5 +1,5 @@ // mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. -// Copyright (C) 2020 Tulir Asokan +// Copyright (C) 2021 Tulir Asokan // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by @@ -22,6 +22,7 @@ import ( "strings" "time" + "go.mau.fi/whatsmeow/types" "maunium.net/go/maulogger/v2" "maunium.net/go/mautrix" @@ -57,6 +58,9 @@ func NewMatrixHandler(bridge *Bridge) *MatrixHandler { bridge.EventProcessor.On(event.StateRoomAvatar, handler.HandleRoomMetadata) bridge.EventProcessor.On(event.StateTopic, handler.HandleRoomMetadata) bridge.EventProcessor.On(event.StateEncryption, handler.HandleEncryption) + bridge.EventProcessor.On(event.EphemeralEventPresence, handler.HandlePresence) + bridge.EventProcessor.On(event.EphemeralEventReceipt, handler.HandleReceipt) + bridge.EventProcessor.On(event.EphemeralEventTyping, handler.HandleTyping) return handler } @@ -474,3 +478,61 @@ func (mx *MatrixHandler) HandleRedaction(evt *event.Event) { portal.HandleMatrixRedaction(user, evt) } } + +func (mx *MatrixHandler) HandlePresence(evt *event.Event) { + user := mx.bridge.GetUserByMXIDIfExists(evt.Sender) + if user == nil || !user.IsLoggedIn() { + return + } + customPuppet := mx.bridge.GetPuppetByCustomMXID(user.MXID) + // TODO move this flag to the user and/or portal data + if customPuppet != nil && !customPuppet.EnablePresence { + return + } + + presence := types.PresenceAvailable + if evt.Content.AsPresence().Presence != event.PresenceOnline { + presence = types.PresenceUnavailable + user.log.Debugln("Marking offline") + } else { + user.log.Debugln("Marking online") + } + user.lastPresence = presence + if user.Client.Store.PushName != "" { + err := user.Client.SendPresence(presence) + if err != nil { + user.log.Warnln("Failed to set presence:", err) + } + } +} + +func (mx *MatrixHandler) HandleReceipt(evt *event.Event) { + portal := mx.bridge.GetPortalByMXID(evt.RoomID) + if portal == nil { + return + } + + for eventID, receipts := range *evt.Content.AsReceipt() { + for userID, receipt := range receipts.Read { + if user := mx.bridge.GetUserByMXIDIfExists(userID); user == nil { + // Not a bridge user + } else if customPuppet := mx.bridge.GetPuppetByCustomMXID(user.MXID); customPuppet != nil && !customPuppet.EnableReceipts { + // TODO move this flag to the user and/or portal data + continue + } else if isDoublePuppeted, _ := receipt.Extra[doublePuppetField].(bool); isDoublePuppeted { + // Ignore double puppeted read receipts. + user.log.Debugfln("Ignoring double puppeted read receipt %+v", evt.Content.Raw) + } else { + portal.HandleMatrixReadReceipt(user, eventID, time.UnixMilli(receipt.Timestamp)) + } + } + } +} + +func (mx *MatrixHandler) HandleTyping(evt *event.Event) { + portal := mx.bridge.GetPortalByMXID(evt.RoomID) + if portal == nil { + return + } + portal.HandleMatrixTyping(evt.Content.AsTyping().UserIDs) +} diff --git a/portal.go b/portal.go index 1611d5d..7a1a8f3 100644 --- a/portal.go +++ b/portal.go @@ -198,6 +198,9 @@ type Portal struct { privateChatBackfillInvitePuppet func() + currentlyTyping []id.UserID + currentlyTypingLock sync.Mutex + messages chan PortalMessage relayUser *User @@ -2213,6 +2216,11 @@ func (portal *Portal) HandleMatrixRedaction(sender *User, evt *event.Event) { } func (portal *Portal) HandleMatrixReadReceipt(sender *User, eventID id.EventID, receiptTimestamp time.Time) { + if !sender.IsLoggedIn() { + portal.log.Debugfln("Ignoring read receipt by %s: user is not connected to WhatsApp", sender.JID) + return + } + maxTimestamp := receiptTimestamp if message := portal.bridge.DB.Message.GetByMXID(eventID); message != nil { maxTimestamp = message.Timestamp @@ -2240,6 +2248,51 @@ func (portal *Portal) HandleMatrixReadReceipt(sender *User, eventID id.EventID, } } +func typingDiff(prev, new []id.UserID) (started, stopped []id.UserID) { +OuterNew: + for _, userID := range new { + for _, previousUserID := range prev { + if userID == previousUserID { + continue OuterNew + } + } + started = append(started, userID) + } +OuterPrev: + for _, userID := range prev { + for _, previousUserID := range new { + if userID == previousUserID { + continue OuterPrev + } + } + stopped = append(stopped, userID) + } + return +} + +func (portal *Portal) setTyping(userIDs []id.UserID, state types.ChatPresence) { + for _, userID := range userIDs { + user := portal.bridge.GetUserByMXIDIfExists(userID) + if user == nil || !user.IsLoggedIn() { + continue + } + portal.log.Debugfln("Bridging typing change from %s to chat presence %s", state, user.MXID) + err := user.Client.SendChatPresence(state, portal.Key.JID) + if err != nil { + portal.log.Warnln("Error sending chat presence:", err) + } + } +} + +func (portal *Portal) HandleMatrixTyping(newTyping []id.UserID) { + portal.currentlyTypingLock.Lock() + defer portal.currentlyTypingLock.Unlock() + startedTyping, stoppedTyping := typingDiff(portal.currentlyTyping, newTyping) + portal.currentlyTyping = newTyping + portal.setTyping(startedTyping, types.ChatPresenceComposing) + portal.setTyping(stoppedTyping, types.ChatPresencePaused) +} + func (portal *Portal) canBridgeFrom(sender *User, evtType string) bool { if !sender.IsLoggedIn() { if portal.HasRelaybot() { diff --git a/puppet.go b/puppet.go index 68b31b4..3b0f2c1 100644 --- a/puppet.go +++ b/puppet.go @@ -159,9 +159,8 @@ type Puppet struct { MXID id.UserID - customIntent *appservice.IntentAPI - customTypingIn map[id.RoomID]bool - customUser *User + customIntent *appservice.IntentAPI + customUser *User syncLock sync.Mutex } diff --git a/user.go b/user.go index f0e8202..eeb759d 100644 --- a/user.go +++ b/user.go @@ -66,7 +66,7 @@ type User struct { lastPresence types.Presence } -func (bridge *Bridge) GetUserByMXID(userID id.UserID) *User { +func (bridge *Bridge) getUserByMXID(userID id.UserID, onlyIfExists bool) *User { _, isPuppet := bridge.ParsePuppetMXID(userID) if isPuppet || userID == bridge.Bot.UserID { return nil @@ -75,11 +75,23 @@ func (bridge *Bridge) GetUserByMXID(userID id.UserID) *User { defer bridge.usersLock.Unlock() user, ok := bridge.usersByMXID[userID] if !ok { - return bridge.loadDBUser(bridge.DB.User.GetByMXID(userID), &userID) + userIDPtr := &userID + if onlyIfExists { + userIDPtr = nil + } + return bridge.loadDBUser(bridge.DB.User.GetByMXID(userID), userIDPtr) } return user } +func (bridge *Bridge) GetUserByMXID(userID id.UserID) *User { + return bridge.getUserByMXID(userID, false) +} + +func (bridge *Bridge) GetUserByMXIDIfExists(userID id.UserID) *User { + return bridge.getUserByMXID(userID, true) +} + func (bridge *Bridge) GetUserByJID(jid types.JID) *User { bridge.usersLock.Lock() defer bridge.usersLock.Unlock()