Add support for MSC2409

This commit is contained in:
Tulir Asokan 2021-12-07 16:02:51 +02:00
parent efd6e1a84f
commit 1d8ef6cb89
9 changed files with 144 additions and 67 deletions

View file

@ -61,6 +61,8 @@ type Config struct {
Avatar string `yaml:"avatar"` Avatar string `yaml:"avatar"`
} `yaml:"bot"` } `yaml:"bot"`
EphemeralEvents bool `yaml:"ephemeral_events"`
ASToken string `yaml:"as_token"` ASToken string `yaml:"as_token"`
HSToken string `yaml:"hs_token"` HSToken string `yaml:"hs_token"`
} `yaml:"appservice"` } `yaml:"appservice"`

View file

@ -61,6 +61,7 @@ func (config *Config) copyToRegistration(registration *appservice.Registration)
falseVal := false falseVal := false
registration.RateLimited = &falseVal registration.RateLimited = &falseVal
registration.SenderLocalpart = config.AppService.Bot.Username registration.SenderLocalpart = config.AppService.Bot.Username
registration.EphemeralEvents = config.AppService.EphemeralEvents
userIDRegex, err := regexp.Compile(fmt.Sprintf("^@%s:%s$", userIDRegex, err := regexp.Compile(fmt.Sprintf("^@%s:%s$",
config.Bridge.FormatUsername("[0-9]+"), config.Bridge.FormatUsername("[0-9]+"),

View file

@ -51,6 +51,7 @@ func (helper *UpgradeHelper) doUpgrade() {
helper.Copy(Str, "appservice", "bot", "username") helper.Copy(Str, "appservice", "bot", "username")
helper.Copy(Str, "appservice", "bot", "displayname") helper.Copy(Str, "appservice", "bot", "displayname")
helper.Copy(Str, "appservice", "bot", "avatar") helper.Copy(Str, "appservice", "bot", "avatar")
helper.Copy(Bool, "appservice", "ephemeral_events")
helper.Copy(Str, "appservice", "as_token") helper.Copy(Str, "appservice", "as_token")
helper.Copy(Str, "appservice", "hs_token") helper.Copy(Str, "appservice", "hs_token")

View file

@ -24,8 +24,6 @@ import (
"fmt" "fmt"
"time" "time"
"go.mau.fi/whatsmeow/types"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice" "maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
@ -139,7 +137,6 @@ func (puppet *Puppet) clearCustomMXID() {
puppet.CustomMXID = "" puppet.CustomMXID = ""
puppet.AccessToken = "" puppet.AccessToken = ""
puppet.customIntent = nil puppet.customIntent = nil
puppet.customTypingIn = nil
puppet.customUser = nil puppet.customUser = nil
} }
@ -165,7 +162,6 @@ func (puppet *Puppet) StartCustomMXID(reloginOnFail bool) error {
return ErrMismatchingMXID return ErrMismatchingMXID
} }
puppet.customIntent = intent puppet.customIntent = intent
puppet.customTypingIn = make(map[id.RoomID]bool)
puppet.customUser = puppet.bridge.GetUserByMXID(puppet.CustomMXID) puppet.customUser = puppet.bridge.GetUserByMXID(puppet.CustomMXID)
puppet.startSyncing() puppet.startSyncing()
return nil return nil
@ -210,10 +206,10 @@ func (puppet *Puppet) ProcessResponse(resp *mautrix.RespSync, _ string) error {
switch evt.Type { switch evt.Type {
case event.EphemeralEventReceipt: case event.EphemeralEventReceipt:
if puppet.EnableReceipts { if puppet.EnableReceipts {
go puppet.handleReceiptEvent(portal, evt) go puppet.bridge.MatrixHandler.HandleReceipt(evt)
} }
case event.EphemeralEventTyping: 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 { if err != nil {
continue continue
} }
go puppet.handlePresenceEvent(evt) go puppet.bridge.MatrixHandler.HandlePresence(evt)
} }
} }
return nil 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 { func (puppet *Puppet) tryRelogin(cause error, action string) bool {
if !puppet.bridge.Config.CanAutoDoublePuppet(puppet.CustomMXID) { if !puppet.bridge.Config.CanAutoDoublePuppet(puppet.CustomMXID) {
return false return false

View file

@ -56,6 +56,11 @@ appservice:
displayname: WhatsApp bridge bot displayname: WhatsApp bridge bot
avatar: mxc://maunium.net/NeXNQarUbrlYBiPCpprYsRqr 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. # Authentication tokens for AS <-> HS communication. Autogenerated; do not modify.
as_token: "This value is generated when generating the registration" as_token: "This value is generated when generating the registration"
hs_token: "This value is generated when generating the registration" hs_token: "This value is generated when generating the registration"

View file

@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. // 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 // 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 // it under the terms of the GNU Affero General Public License as published by
@ -22,6 +22,7 @@ import (
"strings" "strings"
"time" "time"
"go.mau.fi/whatsmeow/types"
"maunium.net/go/maulogger/v2" "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
@ -57,6 +58,9 @@ func NewMatrixHandler(bridge *Bridge) *MatrixHandler {
bridge.EventProcessor.On(event.StateRoomAvatar, handler.HandleRoomMetadata) bridge.EventProcessor.On(event.StateRoomAvatar, handler.HandleRoomMetadata)
bridge.EventProcessor.On(event.StateTopic, handler.HandleRoomMetadata) bridge.EventProcessor.On(event.StateTopic, handler.HandleRoomMetadata)
bridge.EventProcessor.On(event.StateEncryption, handler.HandleEncryption) 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 return handler
} }
@ -474,3 +478,61 @@ func (mx *MatrixHandler) HandleRedaction(evt *event.Event) {
portal.HandleMatrixRedaction(user, evt) 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)
}

View file

@ -198,6 +198,9 @@ type Portal struct {
privateChatBackfillInvitePuppet func() privateChatBackfillInvitePuppet func()
currentlyTyping []id.UserID
currentlyTypingLock sync.Mutex
messages chan PortalMessage messages chan PortalMessage
relayUser *User 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) { 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 maxTimestamp := receiptTimestamp
if message := portal.bridge.DB.Message.GetByMXID(eventID); message != nil { if message := portal.bridge.DB.Message.GetByMXID(eventID); message != nil {
maxTimestamp = message.Timestamp 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 { func (portal *Portal) canBridgeFrom(sender *User, evtType string) bool {
if !sender.IsLoggedIn() { if !sender.IsLoggedIn() {
if portal.HasRelaybot() { if portal.HasRelaybot() {

View file

@ -159,9 +159,8 @@ type Puppet struct {
MXID id.UserID MXID id.UserID
customIntent *appservice.IntentAPI customIntent *appservice.IntentAPI
customTypingIn map[id.RoomID]bool customUser *User
customUser *User
syncLock sync.Mutex syncLock sync.Mutex
} }

16
user.go
View file

@ -66,7 +66,7 @@ type User struct {
lastPresence types.Presence 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) _, isPuppet := bridge.ParsePuppetMXID(userID)
if isPuppet || userID == bridge.Bot.UserID { if isPuppet || userID == bridge.Bot.UserID {
return nil return nil
@ -75,11 +75,23 @@ func (bridge *Bridge) GetUserByMXID(userID id.UserID) *User {
defer bridge.usersLock.Unlock() defer bridge.usersLock.Unlock()
user, ok := bridge.usersByMXID[userID] user, ok := bridge.usersByMXID[userID]
if !ok { 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 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 { func (bridge *Bridge) GetUserByJID(jid types.JID) *User {
bridge.usersLock.Lock() bridge.usersLock.Lock()
defer bridge.usersLock.Unlock() defer bridge.usersLock.Unlock()