forked from MirrorHub/mautrix-whatsapp
Add support for MSC2409
This commit is contained in:
parent
efd6e1a84f
commit
1d8ef6cb89
9 changed files with 144 additions and 67 deletions
|
@ -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"`
|
||||||
|
|
|
@ -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]+"),
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
64
matrix.go
64
matrix.go
|
@ -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)
|
||||||
|
}
|
||||||
|
|
53
portal.go
53
portal.go
|
@ -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() {
|
||||||
|
|
|
@ -160,7 +160,6 @@ 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
16
user.go
|
@ -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()
|
||||||
|
|
Loading…
Reference in a new issue