Move Matrix event and command handling to mautrix-go

This commit is contained in:
Tulir Asokan 2022-05-22 16:15:54 +03:00
parent a8c72a57f9
commit 73304cd400
10 changed files with 516 additions and 882 deletions

File diff suppressed because it is too large Load diff

View file

@ -112,12 +112,7 @@ type BridgeConfig struct {
CommandPrefix string `yaml:"command_prefix"` CommandPrefix string `yaml:"command_prefix"`
ManagementRoomText struct { ManagementRoomText bridgeconfig.ManagementRoomTexts `yaml:"management_room_text"`
Welcome string `yaml:"welcome"`
WelcomeConnected string `yaml:"welcome_connected"`
WelcomeUnconnected string `yaml:"welcome_unconnected"`
AdditionalHelp string `yaml:"additional_help"`
} `yaml:"management_room_text"`
Encryption bridgeconfig.EncryptionConfig `yaml:"encryption"` Encryption bridgeconfig.EncryptionConfig `yaml:"encryption"`
@ -138,6 +133,14 @@ func (bc BridgeConfig) GetEncryptionConfig() bridgeconfig.EncryptionConfig {
return bc.Encryption return bc.Encryption
} }
func (bc BridgeConfig) GetCommandPrefix() string {
return bc.CommandPrefix
}
func (bc BridgeConfig) GetManagementRoomTexts() bridgeconfig.ManagementRoomTexts {
return bc.ManagementRoomText
}
type umBridgeConfig BridgeConfig type umBridgeConfig BridgeConfig
func (bc *BridgeConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { func (bc *BridgeConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {

View file

@ -219,7 +219,7 @@ func (puppet *Puppet) ProcessResponse(resp *mautrix.RespSync, _ string) error {
if err != nil { if err != nil {
continue continue
} }
go puppet.bridge.MatrixHandler.HandlePresence(evt) go puppet.bridge.HandlePresence(evt)
} }
} }
return nil return nil

2
go.mod
View file

@ -15,7 +15,7 @@ require (
golang.org/x/net v0.0.0-20220513224357-95641704303c golang.org/x/net v0.0.0-20220513224357-95641704303c
google.golang.org/protobuf v1.28.0 google.golang.org/protobuf v1.28.0
maunium.net/go/maulogger/v2 v2.3.2 maunium.net/go/maulogger/v2 v2.3.2
maunium.net/go/mautrix v0.11.1-0.20220521221850-b3037c19004a maunium.net/go/mautrix v0.11.1-0.20220522131515-87f89ec33247
) )
require ( require (

4
go.sum
View file

@ -107,5 +107,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/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
maunium.net/go/maulogger/v2 v2.3.2 h1:1XmIYmMd3PoQfp9J+PaHhpt80zpfmMqaShzUTC7FwY0= maunium.net/go/maulogger/v2 v2.3.2 h1:1XmIYmMd3PoQfp9J+PaHhpt80zpfmMqaShzUTC7FwY0=
maunium.net/go/maulogger/v2 v2.3.2/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A= maunium.net/go/maulogger/v2 v2.3.2/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
maunium.net/go/mautrix v0.11.1-0.20220521221850-b3037c19004a h1:eaC/oCwiQl0G/ybPEWpzel0jwB/FKSsw86POz4dw3ss= maunium.net/go/mautrix v0.11.1-0.20220522131515-87f89ec33247 h1:Hi90NviwsdYIY9/N6S4uZQuqPuHf/a2jYud4kC9tTcc=
maunium.net/go/mautrix v0.11.1-0.20220521221850-b3037c19004a/go.mod h1:oma8o6Y/5jcViBlDbX7tp1ajP2XP+b78h8twdI+zKI0= maunium.net/go/mautrix v0.11.1-0.20220522131515-87f89ec33247/go.mod h1:oma8o6Y/5jcViBlDbX7tp1ajP2XP+b78h8twdI+zKI0=

11
main.go
View file

@ -33,6 +33,8 @@ import (
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
"maunium.net/go/mautrix/bridge" "maunium.net/go/mautrix/bridge"
"maunium.net/go/mautrix/bridge/commands"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/util/configupgrade" "maunium.net/go/mautrix/util/configupgrade"
@ -53,7 +55,6 @@ var ExampleConfig string
type WABridge struct { type WABridge struct {
bridge.Bridge bridge.Bridge
MatrixHandler *MatrixHandler
Config *config.Config Config *config.Config
DB *database.Database DB *database.Database
Provisioning *ProvisioningAPI Provisioning *ProvisioningAPI
@ -78,6 +79,12 @@ type WABridge struct {
} }
func (br *WABridge) Init() { func (br *WABridge) Init() {
br.CommandProcessor = commands.NewProcessor(&br.Bridge)
br.RegisterCommands()
// TODO this is a weird place for this
br.EventProcessor.On(event.EphemeralEventPresence, br.HandlePresence)
Segment.log = br.Log.Sub("Segment") Segment.log = br.Log.Sub("Segment")
Segment.key = br.Config.SegmentKey Segment.key = br.Config.SegmentKey
if Segment.IsEnabled() { if Segment.IsEnabled() {
@ -93,8 +100,6 @@ func (br *WABridge) Init() {
br.Provisioning = &ProvisioningAPI{bridge: br} br.Provisioning = &ProvisioningAPI{bridge: br}
} }
br.Log.Debugln("Initializing Matrix event handler")
br.MatrixHandler = NewMatrixHandler(br)
br.Formatter = NewFormatter(br) br.Formatter = NewFormatter(br)
br.Metrics = NewMetricsHandler(br.Config.Metrics.Listen, br.Log.Sub("Metrics"), br.DB) br.Metrics = NewMetricsHandler(br.Config.Metrics.Listen, br.Log.Sub("Metrics"), br.DB)

496
matrix.go
View file

@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. // mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2021 Tulir Asokan // Copyright (C) 2022 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
@ -17,17 +17,11 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"strings"
"time"
"maunium.net/go/maulogger/v2"
"go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/bridge" "maunium.net/go/mautrix/bridge"
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format" "maunium.net/go/mautrix/format"
@ -36,149 +30,32 @@ import (
"maunium.net/go/mautrix-whatsapp/database" "maunium.net/go/mautrix-whatsapp/database"
) )
type MatrixHandler struct { func (br *WABridge) CreatePrivatePortal(roomID id.RoomID, brInviter bridge.User, brGhost bridge.Ghost) {
bridge *WABridge inviter := brInviter.(*User)
as *appservice.AppService puppet := brGhost.(*Puppet)
log maulogger.Logger key := database.NewPortalKey(puppet.JID, inviter.JID)
cmd *CommandHandler portal := br.GetPortalByJID(key)
}
func NewMatrixHandler(bridge *WABridge) *MatrixHandler {
handler := &MatrixHandler{
bridge: bridge,
as: bridge.AS,
log: bridge.Log.Sub("Matrix"),
cmd: NewCommandHandler(bridge),
}
bridge.EventProcessor.On(event.EventMessage, handler.HandleMessage)
bridge.EventProcessor.On(event.EventEncrypted, handler.HandleEncrypted)
bridge.EventProcessor.On(event.EventSticker, handler.HandleMessage)
bridge.EventProcessor.On(event.EventReaction, handler.HandleReaction)
bridge.EventProcessor.On(event.EventRedaction, handler.HandleRedaction)
bridge.EventProcessor.On(event.StateMember, handler.HandleMembership)
bridge.EventProcessor.On(event.StateRoomName, handler.HandleRoomMetadata)
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
}
func (mx *MatrixHandler) HandleEncryption(evt *event.Event) {
defer mx.bridge.Metrics.TrackMatrixEvent(evt.Type)()
if evt.Content.AsEncryption().Algorithm != id.AlgorithmMegolmV1 {
return
}
portal := mx.bridge.GetPortalByMXID(evt.RoomID)
if portal != nil && !portal.Encrypted {
mx.log.Debugfln("%s enabled encryption in %s", evt.Sender, evt.RoomID)
portal.Encrypted = true
portal.Update(nil)
if portal.IsPrivateChat() {
err := mx.as.BotIntent().EnsureJoined(portal.MXID, appservice.EnsureJoinedParams{BotOverride: portal.MainIntent().Client})
if err != nil {
mx.log.Errorfln("Failed to join bot to %s after encryption was enabled: %v", evt.RoomID, err)
}
}
}
}
func (mx *MatrixHandler) joinAndCheckMembers(evt *event.Event, intent *appservice.IntentAPI) *mautrix.RespJoinedMembers {
resp, err := intent.JoinRoomByID(evt.RoomID)
if err != nil {
mx.log.Debugfln("Failed to join room %s as %s with invite from %s: %v", evt.RoomID, intent.UserID, evt.Sender, err)
return nil
}
members, err := intent.JoinedMembers(resp.RoomID)
if err != nil {
mx.log.Debugfln("Failed to get members in room %s after accepting invite from %s as %s: %v", resp.RoomID, evt.Sender, intent.UserID, err)
_, _ = intent.LeaveRoom(resp.RoomID)
return nil
}
if len(members.Joined) < 2 {
mx.log.Debugln("Leaving empty room", resp.RoomID, "after accepting invite from", evt.Sender, "as", intent.UserID)
_, _ = intent.LeaveRoom(resp.RoomID)
return nil
}
return members
}
func (mx *MatrixHandler) sendNoticeWithMarkdown(roomID id.RoomID, message string) (*mautrix.RespSendEvent, error) {
intent := mx.as.BotIntent()
content := format.RenderMarkdown(message, true, false)
content.MsgType = event.MsgNotice
return intent.SendMessageEvent(roomID, event.EventMessage, content)
}
func (mx *MatrixHandler) HandleBotInvite(evt *event.Event) {
intent := mx.as.BotIntent()
user := mx.bridge.GetUserByMXID(evt.Sender)
if user == nil {
return
}
members := mx.joinAndCheckMembers(evt, intent)
if members == nil {
return
}
if !user.Whitelisted {
_, _ = intent.SendNotice(evt.RoomID, "You are not whitelisted to use this bridge.\n"+
"If you're the owner of this bridge, see the bridge.permissions section in your config file.")
_, _ = intent.LeaveRoom(evt.RoomID)
return
}
_, _ = mx.sendNoticeWithMarkdown(evt.RoomID, mx.bridge.Config.Bridge.ManagementRoomText.Welcome)
if len(members.Joined) == 2 && (len(user.ManagementRoom) == 0 || evt.Content.AsMember().IsDirect) {
user.SetManagementRoom(evt.RoomID)
_, _ = intent.SendNotice(user.ManagementRoom, "This room has been registered as your bridge management/status room.")
mx.log.Debugln(evt.RoomID, "registered as a management room with", evt.Sender)
}
if evt.RoomID == user.ManagementRoom {
if user.HasSession() {
_, _ = mx.sendNoticeWithMarkdown(evt.RoomID, mx.bridge.Config.Bridge.ManagementRoomText.WelcomeConnected)
} else {
_, _ = mx.sendNoticeWithMarkdown(evt.RoomID, mx.bridge.Config.Bridge.ManagementRoomText.WelcomeUnconnected)
}
additionalHelp := mx.bridge.Config.Bridge.ManagementRoomText.AdditionalHelp
if len(additionalHelp) > 0 {
_, _ = mx.sendNoticeWithMarkdown(evt.RoomID, additionalHelp)
}
}
}
func (mx *MatrixHandler) handlePrivatePortal(roomID id.RoomID, inviter *User, puppet *Puppet, key database.PortalKey) {
portal := mx.bridge.GetPortalByJID(key)
if len(portal.MXID) == 0 { if len(portal.MXID) == 0 {
mx.createPrivatePortalFromInvite(roomID, inviter, puppet, portal) br.createPrivatePortalFromInvite(roomID, inviter, puppet, portal)
return return
} }
err := portal.MainIntent().EnsureInvited(portal.MXID, inviter.MXID) err := portal.MainIntent().EnsureInvited(portal.MXID, inviter.MXID)
if err != nil { if err != nil {
mx.log.Warnfln("Failed to invite %s to existing private chat portal %s with %s: %v. Redirecting portal to new room...", inviter.MXID, portal.MXID, puppet.JID, err) br.Log.Warnfln("Failed to invite %s to existing private chat portal %s with %s: %v. Redirecting portal to new room...", inviter.MXID, portal.MXID, puppet.JID, err)
mx.createPrivatePortalFromInvite(roomID, inviter, puppet, portal) br.createPrivatePortalFromInvite(roomID, inviter, puppet, portal)
return return
} }
intent := puppet.DefaultIntent() intent := puppet.DefaultIntent()
errorMessage := fmt.Sprintf("You already have a private chat portal with me at [%[1]s](https://matrix.to/#/%[1]s)", portal.MXID) errorMessage := fmt.Sprintf("You already have a private chat portal with me at [%[1]s](https://matrix.to/#/%[1]s)", portal.MXID)
errorContent := format.RenderMarkdown(errorMessage, true, false) errorContent := format.RenderMarkdown(errorMessage, true, false)
_, _ = intent.SendMessageEvent(roomID, event.EventMessage, errorContent) _, _ = intent.SendMessageEvent(roomID, event.EventMessage, errorContent)
mx.log.Debugfln("Leaving private chat room %s as %s after accepting invite from %s as we already have chat with the user", roomID, puppet.MXID, inviter.MXID) br.Log.Debugfln("Leaving private chat room %s as %s after accepting invite from %s as we already have chat with the user", roomID, puppet.MXID, inviter.MXID)
_, _ = intent.LeaveRoom(roomID) _, _ = intent.LeaveRoom(roomID)
} }
func (mx *MatrixHandler) createPrivatePortalFromInvite(roomID id.RoomID, inviter *User, puppet *Puppet, portal *Portal) { func (br *WABridge) createPrivatePortalFromInvite(roomID id.RoomID, inviter *User, puppet *Puppet, portal *Portal) {
portal.MXID = roomID portal.MXID = roomID
portal.Topic = PrivateChatTopic portal.Topic = PrivateChatTopic
_, _ = portal.MainIntent().SetRoomTopic(portal.MXID, portal.Topic) _, _ = portal.MainIntent().SetRoomTopic(portal.MXID, portal.Topic)
@ -194,12 +71,12 @@ func (mx *MatrixHandler) createPrivatePortalFromInvite(roomID id.RoomID, inviter
portal.log.Infofln("Created private chat portal in %s after invite from %s", roomID, inviter.MXID) portal.log.Infofln("Created private chat portal in %s after invite from %s", roomID, inviter.MXID)
intent := puppet.DefaultIntent() intent := puppet.DefaultIntent()
if mx.bridge.Config.Bridge.Encryption.Default { if br.Config.Bridge.Encryption.Default {
_, err := intent.InviteUser(roomID, &mautrix.ReqInviteUser{UserID: mx.bridge.Bot.UserID}) _, err := intent.InviteUser(roomID, &mautrix.ReqInviteUser{UserID: br.Bot.UserID})
if err != nil { if err != nil {
portal.log.Warnln("Failed to invite bridge bot to enable e2be:", err) portal.log.Warnln("Failed to invite bridge bot to enable e2be:", err)
} }
err = mx.bridge.Bot.EnsureJoined(roomID) err = br.Bot.EnsureJoined(roomID)
if err != nil { if err != nil {
portal.log.Warnln("Failed to join as bridge bot to enable e2be:", err) portal.log.Warnln("Failed to join as bridge bot to enable e2be:", err)
} }
@ -207,9 +84,9 @@ func (mx *MatrixHandler) createPrivatePortalFromInvite(roomID id.RoomID, inviter
if err != nil { if err != nil {
portal.log.Warnln("Failed to enable e2be:", err) portal.log.Warnln("Failed to enable e2be:", err)
} }
mx.as.StateStore.SetMembership(roomID, inviter.MXID, event.MembershipJoin) br.AS.StateStore.SetMembership(roomID, inviter.MXID, event.MembershipJoin)
mx.as.StateStore.SetMembership(roomID, puppet.MXID, event.MembershipJoin) br.AS.StateStore.SetMembership(roomID, puppet.MXID, event.MembershipJoin)
mx.as.StateStore.SetMembership(roomID, mx.bridge.Bot.UserID, event.MembershipJoin) br.AS.StateStore.SetMembership(roomID, br.Bot.UserID, event.MembershipJoin)
portal.Encrypted = true portal.Encrypted = true
} }
portal.Update(nil) portal.Update(nil)
@ -217,312 +94,12 @@ func (mx *MatrixHandler) createPrivatePortalFromInvite(roomID id.RoomID, inviter
_, _ = intent.SendNotice(roomID, "Private chat portal created") _, _ = intent.SendNotice(roomID, "Private chat portal created")
} }
func (mx *MatrixHandler) HandlePuppetInvite(evt *event.Event, inviter *User, puppet *Puppet) { func (br *WABridge) HandlePresence(evt *event.Event) {
intent := puppet.DefaultIntent() user := br.GetUserByMXIDIfExists(evt.Sender)
if !inviter.Whitelisted {
puppet.log.Debugfln("Rejecting invite from %s to %s: user is not whitelisted", evt.Sender, evt.RoomID)
_, err := intent.LeaveRoom(evt.RoomID, &mautrix.ReqLeave{
Reason: "You're not whitelisted to use this bridge",
})
if err != nil {
puppet.log.Warnfln("Failed to reject invite from %s to %s: %v", evt.Sender, evt.RoomID, err)
}
return
} else if !inviter.IsLoggedIn() {
puppet.log.Debugfln("Rejecting invite from %s to %s: user is not logged in", evt.Sender, evt.RoomID)
_, err := intent.LeaveRoom(evt.RoomID, &mautrix.ReqLeave{
Reason: "You're not logged into this bridge",
})
if err != nil {
puppet.log.Warnfln("Failed to reject invite from %s to %s: %v", evt.Sender, evt.RoomID, err)
}
return
}
members := mx.joinAndCheckMembers(evt, intent)
if members == nil {
return
}
var hasBridgeBot, hasOtherUsers bool
for mxid, _ := range members.Joined {
if mxid == intent.UserID || mxid == inviter.MXID {
continue
} else if mxid == mx.bridge.Bot.UserID {
hasBridgeBot = true
} else {
hasOtherUsers = true
}
}
if !hasBridgeBot && !hasOtherUsers {
key := database.NewPortalKey(puppet.JID, inviter.JID)
mx.handlePrivatePortal(evt.RoomID, inviter, puppet, key)
} else if !hasBridgeBot {
mx.log.Debugln("Leaving multi-user room", evt.RoomID, "as", puppet.MXID, "after accepting invite from", evt.Sender)
_, _ = intent.SendNotice(evt.RoomID, "Please invite the bridge bot first if you want to bridge to a WhatsApp group.")
_, _ = intent.LeaveRoom(evt.RoomID)
} else {
_, _ = intent.SendNotice(evt.RoomID, "This puppet will remain inactive until this room is bridged to a WhatsApp group.")
}
}
func (mx *MatrixHandler) HandleMembership(evt *event.Event) {
if _, isPuppet := mx.bridge.ParsePuppetMXID(evt.Sender); evt.Sender == mx.bridge.Bot.UserID || isPuppet {
return
}
defer mx.bridge.Metrics.TrackMatrixEvent(evt.Type)()
if mx.bridge.Crypto != nil {
mx.bridge.Crypto.HandleMemberEvent(evt)
}
content := evt.Content.AsMember()
if content.Membership == event.MembershipInvite && id.UserID(evt.GetStateKey()) == mx.as.BotMXID() {
mx.HandleBotInvite(evt)
return
}
if mx.shouldIgnoreEvent(evt) {
return
}
user := mx.bridge.GetUserByMXID(evt.Sender)
if user == nil {
return
}
isSelf := id.UserID(evt.GetStateKey()) == evt.Sender
puppet := mx.bridge.GetPuppetByMXID(id.UserID(evt.GetStateKey()))
portal := mx.bridge.GetPortalByMXID(evt.RoomID)
if portal == nil {
if puppet != nil && content.Membership == event.MembershipInvite {
mx.HandlePuppetInvite(evt, user, puppet)
}
return
} else if !user.Whitelisted || !user.IsLoggedIn() {
return
}
if content.Membership == event.MembershipLeave {
if evt.Unsigned.PrevContent != nil {
_ = evt.Unsigned.PrevContent.ParseRaw(evt.Type)
prevContent, ok := evt.Unsigned.PrevContent.Parsed.(*event.MemberEventContent)
if ok && prevContent.Membership != "join" {
return
}
}
if isSelf {
portal.HandleMatrixLeave(user)
} else if puppet != nil {
portal.HandleMatrixKick(user, puppet)
}
} else if content.Membership == event.MembershipInvite && !isSelf && puppet != nil {
portal.HandleMatrixInvite(user, puppet)
}
}
func (mx *MatrixHandler) HandleRoomMetadata(evt *event.Event) {
defer mx.bridge.Metrics.TrackMatrixEvent(evt.Type)()
if mx.shouldIgnoreEvent(evt) {
return
}
user := mx.bridge.GetUserByMXID(evt.Sender)
if user == nil || !user.Whitelisted || !user.IsLoggedIn() {
return
}
portal := mx.bridge.GetPortalByMXID(evt.RoomID)
if portal == nil || portal.IsPrivateChat() {
return
}
portal.HandleMatrixMeta(user, evt)
}
func (mx *MatrixHandler) shouldIgnoreEvent(evt *event.Event) bool {
if _, isPuppet := mx.bridge.ParsePuppetMXID(evt.Sender); evt.Sender == mx.bridge.Bot.UserID || isPuppet {
return true
}
if val, ok := evt.Content.Raw[doublePuppetKey]; ok && val == doublePuppetValue && mx.bridge.GetPuppetByCustomMXID(evt.Sender) != nil {
return true
}
user := mx.bridge.GetUserByMXID(evt.Sender)
if !user.RelayWhitelisted {
return true
}
return false
}
const sessionWaitTimeout = 5 * time.Second
func (mx *MatrixHandler) HandleEncrypted(evt *event.Event) {
defer mx.bridge.Metrics.TrackMatrixEvent(evt.Type)()
if mx.shouldIgnoreEvent(evt) || mx.bridge.Crypto == nil {
return
}
decrypted, err := mx.bridge.Crypto.Decrypt(evt)
decryptionRetryCount := 0
if errors.Is(err, bridge.NoSessionFound) {
content := evt.Content.AsEncrypted()
mx.log.Debugfln("Couldn't find session %s trying to decrypt %s, waiting %d seconds...", content.SessionID, evt.ID, int(sessionWaitTimeout.Seconds()))
mx.as.SendErrorMessageSendCheckpoint(evt, appservice.StepDecrypted, err, false, decryptionRetryCount)
decryptionRetryCount++
if mx.bridge.Crypto.WaitForSession(evt.RoomID, content.SenderKey, content.SessionID, sessionWaitTimeout) {
mx.log.Debugfln("Got session %s after waiting, trying to decrypt %s again", content.SessionID, evt.ID)
decrypted, err = mx.bridge.Crypto.Decrypt(evt)
} else {
mx.as.SendErrorMessageSendCheckpoint(evt, appservice.StepDecrypted, fmt.Errorf("didn't receive encryption keys"), false, decryptionRetryCount)
go mx.waitLongerForSession(evt)
return
}
}
if err != nil {
mx.as.SendErrorMessageSendCheckpoint(evt, appservice.StepDecrypted, err, true, decryptionRetryCount)
mx.log.Warnfln("Failed to decrypt %s: %v", evt.ID, err)
_, _ = mx.bridge.Bot.SendNotice(evt.RoomID, fmt.Sprintf(
"\u26a0 Your message was not bridged: %v", err))
return
}
mx.as.SendMessageSendCheckpoint(decrypted, appservice.StepDecrypted, decryptionRetryCount)
mx.bridge.EventProcessor.Dispatch(decrypted)
}
func (mx *MatrixHandler) waitLongerForSession(evt *event.Event) {
const extendedTimeout = sessionWaitTimeout * 3
content := evt.Content.AsEncrypted()
mx.log.Debugfln("Couldn't find session %s trying to decrypt %s, waiting %d more seconds...",
content.SessionID, evt.ID, int(extendedTimeout.Seconds()))
go mx.bridge.Crypto.RequestSession(evt.RoomID, content.SenderKey, content.SessionID, evt.Sender, content.DeviceID)
resp, err := mx.bridge.Bot.SendNotice(evt.RoomID, fmt.Sprintf(
"\u26a0 Your message was not bridged: the bridge hasn't received the decryption keys. "+
"The bridge will retry for %d seconds. If this error keeps happening, try restarting your client.",
int(extendedTimeout.Seconds())))
if err != nil {
mx.log.Errorfln("Failed to send decryption error to %s: %v", evt.RoomID, err)
}
update := event.MessageEventContent{MsgType: event.MsgNotice}
if mx.bridge.Crypto.WaitForSession(evt.RoomID, content.SenderKey, content.SessionID, extendedTimeout) {
mx.log.Debugfln("Got session %s after waiting more, trying to decrypt %s again", content.SessionID, evt.ID)
decrypted, err := mx.bridge.Crypto.Decrypt(evt)
if err == nil {
mx.as.SendMessageSendCheckpoint(decrypted, appservice.StepDecrypted, 2)
mx.bridge.EventProcessor.Dispatch(decrypted)
_, _ = mx.bridge.Bot.RedactEvent(evt.RoomID, resp.EventID)
return
}
mx.log.Warnfln("Failed to decrypt %s: %v", evt.ID, err)
mx.as.SendErrorMessageSendCheckpoint(evt, appservice.StepDecrypted, err, true, 2)
update.Body = fmt.Sprintf("\u26a0 Your message was not bridged: %v", err)
} else {
mx.log.Debugfln("Didn't get %s, giving up on %s", content.SessionID, evt.ID)
mx.as.SendErrorMessageSendCheckpoint(evt, appservice.StepDecrypted, fmt.Errorf("didn't receive encryption keys"), true, 2)
update.Body = "\u26a0 Your message was not bridged: the bridge hasn't received the decryption keys. " +
"If this error keeps happening, try restarting your client."
}
newContent := update
update.NewContent = &newContent
if resp != nil {
update.RelatesTo = &event.RelatesTo{
Type: event.RelReplace,
EventID: resp.EventID,
}
}
_, err = mx.bridge.Bot.SendMessageEvent(evt.RoomID, event.EventMessage, &update)
if err != nil {
mx.log.Debugfln("Failed to update decryption error notice %s: %v", resp.EventID, err)
}
}
func (mx *MatrixHandler) HandleMessage(evt *event.Event) {
defer mx.bridge.Metrics.TrackMatrixEvent(evt.Type)()
if mx.shouldIgnoreEvent(evt) {
return
}
user := mx.bridge.GetUserByMXID(evt.Sender)
if user == nil {
return
}
content := evt.Content.AsMessage()
content.RemoveReplyFallback()
if user.Whitelisted && content.MsgType == event.MsgText {
commandPrefix := mx.bridge.Config.Bridge.CommandPrefix
hasCommandPrefix := strings.HasPrefix(content.Body, commandPrefix)
if hasCommandPrefix {
content.Body = strings.TrimLeft(content.Body[len(commandPrefix):], " ")
}
if hasCommandPrefix || evt.RoomID == user.ManagementRoom {
mx.cmd.Handle(evt.RoomID, evt.ID, user, content.Body, content.GetReplyTo())
return
}
}
portal := mx.bridge.GetPortalByMXID(evt.RoomID)
if portal != nil && (user.Whitelisted || portal.HasRelaybot()) {
portal.matrixMessages <- PortalMatrixMessage{user: user, evt: evt}
}
}
func (mx *MatrixHandler) HandleReaction(evt *event.Event) {
defer mx.bridge.Metrics.TrackMatrixEvent(evt.Type)()
if mx.shouldIgnoreEvent(evt) {
return
}
user := mx.bridge.GetUserByMXID(evt.Sender)
if user == nil || !user.Whitelisted || !user.IsLoggedIn() {
return
}
portal := mx.bridge.GetPortalByMXID(evt.RoomID)
if portal == nil {
return
} else if portal.IsPrivateChat() && user.JID.User != portal.Key.Receiver.User {
// One user can only react once, so we don't use the relay user for reactions
return
}
content := evt.Content.AsReaction()
if strings.Contains(content.RelatesTo.Key, "retry") || strings.HasPrefix(content.RelatesTo.Key, "\u267b") { // ♻️
if retryRequested, _ := portal.requestMediaRetry(user, content.RelatesTo.EventID, nil); retryRequested {
_, _ = portal.MainIntent().RedactEvent(portal.MXID, evt.ID, mautrix.ReqRedact{
Reason: "requested media from phone",
})
// Errored media, don't try to send as reaction
return
}
}
portal.HandleMatrixReaction(user, evt)
}
func (mx *MatrixHandler) HandleRedaction(evt *event.Event) {
defer mx.bridge.Metrics.TrackMatrixEvent(evt.Type)()
user := mx.bridge.GetUserByMXID(evt.Sender)
if user == nil {
return
}
portal := mx.bridge.GetPortalByMXID(evt.RoomID)
if portal != nil && (user.Whitelisted || portal.HasRelaybot()) {
portal.matrixMessages <- PortalMatrixMessage{user: user, evt: evt}
}
}
func (mx *MatrixHandler) HandlePresence(evt *event.Event) {
user := mx.bridge.GetUserByMXIDIfExists(evt.Sender)
if user == nil || !user.IsLoggedIn() { if user == nil || !user.IsLoggedIn() {
return return
} }
customPuppet := mx.bridge.GetPuppetByCustomMXID(user.MXID) customPuppet := br.GetPuppetByCustomMXID(user.MXID)
// TODO move this flag to the user and/or portal data // TODO move this flag to the user and/or portal data
if customPuppet != nil && !customPuppet.EnablePresence { if customPuppet != nil && !customPuppet.EnablePresence {
return return
@ -543,36 +120,3 @@ func (mx *MatrixHandler) HandlePresence(evt *event.Event) {
} }
} }
} }
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 val, ok := receipt.Extra[doublePuppetKey].(string); ok && customPuppet != nil && val == doublePuppetValue {
// Ignore double puppeted read receipts.
user.log.Debugfln("Ignoring double puppeted read receipt %+v", evt.Content.Raw)
// But do start disappearing messages, because the user read the chat
portal.ScheduleDisappearing()
} else {
portal.HandleMatrixReadReceipt(user, eventID, time.UnixMilli(receipt.Timestamp), true)
}
}
}
}
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

@ -80,8 +80,27 @@ func (br *WABridge) GetPortalByMXID(mxid id.RoomID) *Portal {
return portal return portal
} }
func (br *WABridge) GetIPortalByMXID(mxid id.RoomID) bridge.Portal { func (br *WABridge) GetIPortal(mxid id.RoomID) bridge.Portal {
return br.GetPortalByMXID(mxid) p := br.GetPortalByMXID(mxid)
if p == nil {
return nil
}
return p
}
func (portal *Portal) IsEncrypted() bool {
return portal.Encrypted
}
func (portal *Portal) MarkEncrypted() {
portal.Encrypted = true
portal.Update(nil)
}
func (portal *Portal) ReceiveMatrixEvent(user bridge.User, evt *event.Event) {
if user.GetPermissionLevel() >= bridge.PermissionUser || portal.HasRelaybot() {
portal.matrixMessages <- PortalMatrixMessage{user: user.(*User), evt: evt}
}
} }
func (br *WABridge) GetPortalByJID(key database.PortalKey) *Portal { func (br *WABridge) GetPortalByJID(key database.PortalKey) *Portal {
@ -234,10 +253,6 @@ type Portal struct {
relayUser *User relayUser *User
} }
func (portal *Portal) IsEncrypted() bool {
return portal.Encrypted
}
func (portal *Portal) handleMessageLoopItem(msg PortalMessage) { func (portal *Portal) handleMessageLoopItem(msg PortalMessage) {
if len(portal.MXID) == 0 { if len(portal.MXID) == 0 {
if msg.fake == nil && msg.undecryptable == nil && (msg.evt == nil || !containsSupportedMessage(msg.evt.Message)) { if msg.fake == nil && msg.undecryptable == nil && (msg.evt == nil || !containsSupportedMessage(msg.evt.Message)) {
@ -266,12 +281,14 @@ func (portal *Portal) handleMessageLoopItem(msg PortalMessage) {
} }
func (portal *Portal) handleMatrixMessageLoopItem(msg PortalMatrixMessage) { func (portal *Portal) handleMatrixMessageLoopItem(msg PortalMatrixMessage) {
portal.HandleMatrixReadReceipt(msg.user, "", time.UnixMilli(msg.evt.Timestamp), false) portal.handleMatrixReadReceipt(msg.user, "", time.UnixMilli(msg.evt.Timestamp), false)
switch msg.evt.Type { switch msg.evt.Type {
case event.EventMessage, event.EventSticker: case event.EventMessage, event.EventSticker:
portal.HandleMatrixMessage(msg.user, msg.evt) portal.HandleMatrixMessage(msg.user, msg.evt)
case event.EventRedaction: case event.EventRedaction:
portal.HandleMatrixRedaction(msg.user, msg.evt) portal.HandleMatrixRedaction(msg.user, msg.evt)
case event.EventReaction:
portal.HandleMatrixReaction(msg.user, msg.evt)
default: default:
portal.log.Warnln("Unsupported event type %+v in portal message channel", msg.evt.Type) portal.log.Warnln("Unsupported event type %+v in portal message channel", msg.evt.Type)
} }
@ -2897,6 +2914,21 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *event.Event) {
} }
func (portal *Portal) HandleMatrixReaction(sender *User, evt *event.Event) { func (portal *Portal) HandleMatrixReaction(sender *User, evt *event.Event) {
if portal.IsPrivateChat() && sender.JID.User != portal.Key.Receiver.User {
return
}
content, ok := evt.Content.Parsed.(*event.ReactionEventContent)
if ok && strings.Contains(content.RelatesTo.Key, "retry") || strings.HasPrefix(content.RelatesTo.Key, "\u267b") { // ♻️
if retryRequested, _ := portal.requestMediaRetry(sender, content.RelatesTo.EventID, nil); retryRequested {
_, _ = portal.MainIntent().RedactEvent(portal.MXID, evt.ID, mautrix.ReqRedact{
Reason: "requested media from phone",
})
// Errored media, don't try to send as reaction
return
}
}
portal.log.Debugfln("Received reaction event %s from %s", evt.ID, evt.Sender) portal.log.Debugfln("Received reaction event %s from %s", evt.ID, evt.Sender)
err := portal.handleMatrixReaction(sender, evt) err := portal.handleMatrixReaction(sender, evt)
if err != nil { if err != nil {
@ -3033,7 +3065,11 @@ func (portal *Portal) HandleMatrixRedaction(sender *User, evt *event.Event) {
} }
} }
func (portal *Portal) HandleMatrixReadReceipt(sender *User, eventID id.EventID, receiptTimestamp time.Time, isExplicit bool) { func (portal *Portal) HandleMatrixReadReceipt(sender bridge.User, eventID id.EventID, receiptTimestamp time.Time) {
portal.handleMatrixReadReceipt(sender.(*User), eventID, receiptTimestamp, true)
}
func (portal *Portal) handleMatrixReadReceipt(sender *User, eventID id.EventID, receiptTimestamp time.Time, isExplicit bool) {
if !sender.IsLoggedIn() { if !sender.IsLoggedIn() {
if isExplicit { if isExplicit {
portal.log.Debugfln("Ignoring read receipt by %s: user is not connected to WhatsApp", sender.JID) portal.log.Debugfln("Ignoring read receipt by %s: user is not connected to WhatsApp", sender.JID)
@ -3247,7 +3283,8 @@ func (portal *Portal) Cleanup(puppetsOnly bool) {
} }
} }
func (portal *Portal) HandleMatrixLeave(sender *User) { func (portal *Portal) HandleMatrixLeave(brSender bridge.User) {
sender := brSender.(*User)
if portal.IsPrivateChat() { if portal.IsPrivateChat() {
portal.log.Debugln("User left private chat portal, cleaning up and deleting...") portal.log.Debugln("User left private chat portal, cleaning up and deleting...")
portal.Delete() portal.Delete()
@ -3264,7 +3301,9 @@ func (portal *Portal) HandleMatrixLeave(sender *User) {
portal.CleanupIfEmpty() portal.CleanupIfEmpty()
} }
func (portal *Portal) HandleMatrixKick(sender *User, target *Puppet) { func (portal *Portal) HandleMatrixKick(brSender bridge.User, brTarget bridge.Ghost) {
sender := brSender.(*User)
target := brTarget.(*Puppet)
_, err := sender.Client.UpdateGroupParticipants(portal.Key.JID, map[types.JID]whatsmeow.ParticipantChange{ _, err := sender.Client.UpdateGroupParticipants(portal.Key.JID, map[types.JID]whatsmeow.ParticipantChange{
target.JID: whatsmeow.ParticipantChangeRemove, target.JID: whatsmeow.ParticipantChangeRemove,
}) })
@ -3275,7 +3314,9 @@ func (portal *Portal) HandleMatrixKick(sender *User, target *Puppet) {
//portal.log.Infoln("Kick %s response: %s", puppet.JID, <-resp) //portal.log.Infoln("Kick %s response: %s", puppet.JID, <-resp)
} }
func (portal *Portal) HandleMatrixInvite(sender *User, target *Puppet) { func (portal *Portal) HandleMatrixInvite(brSender bridge.User, brTarget bridge.Ghost) {
sender := brSender.(*User)
target := brTarget.(*Puppet)
_, err := sender.Client.UpdateGroupParticipants(portal.Key.JID, map[types.JID]whatsmeow.ParticipantChange{ _, err := sender.Client.UpdateGroupParticipants(portal.Key.JID, map[types.JID]whatsmeow.ParticipantChange{
target.JID: whatsmeow.ParticipantChangeAdd, target.JID: whatsmeow.ParticipantChangeAdd,
}) })
@ -3286,7 +3327,12 @@ func (portal *Portal) HandleMatrixInvite(sender *User, target *Puppet) {
//portal.log.Infofln("Add %s response: %s", puppet.JID, <-resp) //portal.log.Infofln("Add %s response: %s", puppet.JID, <-resp)
} }
func (portal *Portal) HandleMatrixMeta(sender *User, evt *event.Event) { func (portal *Portal) HandleMatrixMeta(brSender bridge.User, evt *event.Event) {
sender := brSender.(*User)
if !sender.Whitelisted || !sender.IsLoggedIn() {
return
}
switch content := evt.Content.Parsed.(type) { switch content := evt.Content.Parsed.(type) {
case *event.RoomNameEventContent: case *event.RoomNameEventContent:
if content.Name == portal.Name { if content.Name == portal.Name {

View file

@ -31,6 +31,7 @@ import (
log "maunium.net/go/maulogger/v2" log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/appservice" "maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/bridge"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
"maunium.net/go/mautrix-whatsapp/config" "maunium.net/go/mautrix-whatsapp/config"
@ -104,6 +105,31 @@ func (br *WABridge) GetPuppetByCustomMXID(mxid id.UserID) *Puppet {
return puppet return puppet
} }
func (user *User) GetIDoublePuppet() bridge.DoublePuppet {
p := user.bridge.GetPuppetByCustomMXID(user.MXID)
if p == nil {
return nil
}
return p
}
func (br *WABridge) IsGhost(id id.UserID) bool {
_, ok := br.ParsePuppetMXID(id)
return ok
}
func (br *WABridge) GetIGhost(id id.UserID) bridge.Ghost {
p := br.GetPuppetByMXID(id)
if p == nil {
return nil
}
return p
}
func (p *Puppet) GetMXID() id.UserID {
return p.MXID
}
func (br *WABridge) GetAllPuppetsWithCustomMXID() []*Puppet { func (br *WABridge) GetAllPuppetsWithCustomMXID() []*Puppet {
return br.dbPuppetsToPuppets(br.DB.Puppet.GetAllWithCustomMXID()) return br.dbPuppetsToPuppets(br.DB.Puppet.GetAllWithCustomMXID())
} }

27
user.go
View file

@ -63,6 +63,7 @@ type User struct {
Admin bool Admin bool
Whitelisted bool Whitelisted bool
RelayWhitelisted bool RelayWhitelisted bool
PermissionLevel bridge.PermissionLevel
mgmtCreateLock sync.Mutex mgmtCreateLock sync.Mutex
spaceCreateLock sync.Mutex spaceCreateLock sync.Mutex
@ -107,12 +108,28 @@ func (br *WABridge) GetUserByMXID(userID id.UserID) *User {
return br.getUserByMXID(userID, false) return br.getUserByMXID(userID, false)
} }
func (br *WABridge) GetIUserByMXID(userID id.UserID) bridge.User { func (br *WABridge) GetIUser(userID id.UserID, create bool) bridge.User {
return br.getUserByMXID(userID, false) u := br.getUserByMXID(userID, !create)
if u == nil {
return nil
}
return u
} }
func (user *User) IsAdmin() bool { func (user *User) GetPermissionLevel() bridge.PermissionLevel {
return user.Admin return user.PermissionLevel
}
func (user *User) GetManagementRoomID() id.RoomID {
return user.ManagementRoom
}
func (user *User) GetMXID() id.UserID {
return user.MXID
}
func (user *User) GetCommandState() map[string]interface{} {
return nil
} }
func (br *WABridge) GetUserByMXIDIfExists(userID id.UserID) *User { func (br *WABridge) GetUserByMXIDIfExists(userID id.UserID) *User {
@ -201,9 +218,11 @@ func (br *WABridge) NewUser(dbUser *database.User) *User {
historySyncs: make(chan *events.HistorySync, 32), historySyncs: make(chan *events.HistorySync, 32),
lastPresence: types.PresenceUnavailable, lastPresence: types.PresenceUnavailable,
} }
user.RelayWhitelisted = user.bridge.Config.Bridge.Permissions.IsRelayWhitelisted(user.MXID) user.RelayWhitelisted = user.bridge.Config.Bridge.Permissions.IsRelayWhitelisted(user.MXID)
user.Whitelisted = user.bridge.Config.Bridge.Permissions.IsWhitelisted(user.MXID) user.Whitelisted = user.bridge.Config.Bridge.Permissions.IsWhitelisted(user.MXID)
user.Admin = user.bridge.Config.Bridge.Permissions.IsAdmin(user.MXID) user.Admin = user.bridge.Config.Bridge.Permissions.IsAdmin(user.MXID)
user.PermissionLevel = bridge.PermissionLevel(user.bridge.Config.Bridge.Permissions.GetPermissionLevel(user.MXID))
if len(user.bridge.Config.Homeserver.StatusEndpoint) > 0 { if len(user.bridge.Config.Homeserver.StatusEndpoint) > 0 {
user.bridgeStateQueue = make(chan BridgeState, 10) user.bridgeStateQueue = make(chan BridgeState, 10)
go user.bridgeStateLoop() go user.bridgeStateLoop()