mautrix-go/bridge/matrix.go

756 lines
25 KiB
Go

// Copyright (c) 2023 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package bridge
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/rs/zerolog"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/bridge/bridgeconfig"
"maunium.net/go/mautrix/bridge/status"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
)
type CommandProcessor interface {
Handle(ctx context.Context, roomID id.RoomID, eventID id.EventID, user User, message string, replyTo id.EventID)
}
type MatrixHandler struct {
bridge *Bridge
as *appservice.AppService
log *zerolog.Logger
TrackEventDuration func(event.Type) func()
}
func noop() {}
func noopTrack(_ event.Type) func() {
return noop
}
func NewMatrixHandler(br *Bridge) *MatrixHandler {
handler := &MatrixHandler{
bridge: br,
as: br.AS,
log: br.ZLog,
TrackEventDuration: noopTrack,
}
for evtType := range status.CheckpointTypes {
br.EventProcessor.On(evtType, handler.sendBridgeCheckpoint)
}
br.EventProcessor.On(event.EventMessage, handler.HandleMessage)
br.EventProcessor.On(event.EventEncrypted, handler.HandleEncrypted)
br.EventProcessor.On(event.EventSticker, handler.HandleMessage)
br.EventProcessor.On(event.EventReaction, handler.HandleReaction)
br.EventProcessor.On(event.EventRedaction, handler.HandleRedaction)
br.EventProcessor.On(event.StateMember, handler.HandleMembership)
br.EventProcessor.On(event.StateRoomName, handler.HandleRoomMetadata)
br.EventProcessor.On(event.StateRoomAvatar, handler.HandleRoomMetadata)
br.EventProcessor.On(event.StateTopic, handler.HandleRoomMetadata)
br.EventProcessor.On(event.StateEncryption, handler.HandleEncryption)
br.EventProcessor.On(event.EphemeralEventReceipt, handler.HandleReceipt)
br.EventProcessor.On(event.EphemeralEventTyping, handler.HandleTyping)
br.EventProcessor.On(event.StatePowerLevels, handler.HandlePowerLevels)
br.EventProcessor.On(event.StateJoinRules, handler.HandleJoinRule)
return handler
}
func (mx *MatrixHandler) sendBridgeCheckpoint(_ context.Context, evt *event.Event) {
if !evt.Mautrix.CheckpointSent {
go mx.bridge.SendMessageSuccessCheckpoint(evt, status.MsgStepBridge, 0)
}
}
func (mx *MatrixHandler) HandleEncryption(ctx context.Context, evt *event.Event) {
defer mx.TrackEventDuration(evt.Type)()
if evt.Content.AsEncryption().Algorithm != id.AlgorithmMegolmV1 {
return
}
portal := mx.bridge.Child.GetIPortal(evt.RoomID)
if portal != nil && !portal.IsEncrypted() {
mx.log.Debug().
Str("user_id", evt.Sender.String()).
Str("room_id", evt.RoomID.String()).
Msg("Encryption was enabled in room")
portal.MarkEncrypted()
if portal.IsPrivateChat() {
err := mx.as.BotIntent().EnsureJoined(ctx, evt.RoomID, appservice.EnsureJoinedParams{BotOverride: portal.MainIntent().Client})
if err != nil {
mx.log.Err(err).
Str("room_id", evt.RoomID.String()).
Msg("Failed to join bot to room after encryption was enabled")
}
}
}
}
func (mx *MatrixHandler) joinAndCheckMembers(ctx context.Context, evt *event.Event, intent *appservice.IntentAPI) *mautrix.RespJoinedMembers {
log := zerolog.Ctx(ctx)
resp, err := intent.JoinRoomByID(ctx, evt.RoomID)
if err != nil {
log.Warn().Err(err).Msg("Failed to join room with invite")
return nil
}
members, err := intent.JoinedMembers(ctx, resp.RoomID)
if err != nil {
log.Warn().Err(err).Msg("Failed to get members in room after accepting invite, leaving room")
_, _ = intent.LeaveRoom(ctx, resp.RoomID)
return nil
}
if len(members.Joined) < 2 {
log.Debug().Msg("Leaving empty room after accepting invite")
_, _ = intent.LeaveRoom(ctx, resp.RoomID)
return nil
}
return members
}
func (mx *MatrixHandler) sendNoticeWithMarkdown(ctx context.Context, 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(ctx, roomID, event.EventMessage, content)
}
func (mx *MatrixHandler) HandleBotInvite(ctx context.Context, evt *event.Event) {
intent := mx.as.BotIntent()
user := mx.bridge.Child.GetIUser(evt.Sender, true)
if user == nil {
return
}
members := mx.joinAndCheckMembers(ctx, evt, intent)
if members == nil {
return
}
if user.GetPermissionLevel() < bridgeconfig.PermissionLevelUser {
_, _ = intent.SendNotice(ctx, 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(ctx, evt.RoomID)
return
}
texts := mx.bridge.Config.Bridge.GetManagementRoomTexts()
_, _ = mx.sendNoticeWithMarkdown(ctx, evt.RoomID, texts.Welcome)
if len(members.Joined) == 2 && (len(user.GetManagementRoomID()) == 0 || evt.Content.AsMember().IsDirect) {
user.SetManagementRoom(evt.RoomID)
_, _ = intent.SendNotice(ctx, user.GetManagementRoomID(), "This room has been registered as your bridge management/status room.")
zerolog.Ctx(ctx).Debug().Msg("Registered room as management room with inviter")
}
if evt.RoomID == user.GetManagementRoomID() {
if user.IsLoggedIn() {
_, _ = mx.sendNoticeWithMarkdown(ctx, evt.RoomID, texts.WelcomeConnected)
} else {
_, _ = mx.sendNoticeWithMarkdown(ctx, evt.RoomID, texts.WelcomeUnconnected)
}
additionalHelp := texts.AdditionalHelp
if len(additionalHelp) > 0 {
_, _ = mx.sendNoticeWithMarkdown(ctx, evt.RoomID, additionalHelp)
}
}
}
func (mx *MatrixHandler) HandleGhostInvite(ctx context.Context, evt *event.Event, inviter User, ghost Ghost) {
log := zerolog.Ctx(ctx)
intent := ghost.DefaultIntent()
if inviter.GetPermissionLevel() < bridgeconfig.PermissionLevelUser {
log.Debug().Msg("Rejecting invite: inviter is not whitelisted")
_, err := intent.LeaveRoom(ctx, evt.RoomID, &mautrix.ReqLeave{
Reason: "You're not whitelisted to use this bridge",
})
if err != nil {
log.Error().Err(err).Msg("Failed to reject invite")
}
return
} else if !inviter.IsLoggedIn() {
log.Debug().Msg("Rejecting invite: inviter is not logged in")
_, err := intent.LeaveRoom(ctx, evt.RoomID, &mautrix.ReqLeave{
Reason: "You're not logged into this bridge",
})
if err != nil {
log.Error().Err(err).Msg("Failed to reject invite")
}
return
}
members := mx.joinAndCheckMembers(ctx, evt, intent)
if members == nil {
return
}
var createEvent event.CreateEventContent
if err := intent.StateEvent(ctx, evt.RoomID, event.StateCreate, "", &createEvent); err != nil {
log.Warn().Err(err).Msg("Failed to check m.room.create event in room")
} else if createEvent.Type != "" {
log.Warn().Str("room_type", string(createEvent.Type)).Msg("Non-standard room type, leaving room")
_, err = intent.LeaveRoom(ctx, evt.RoomID, &mautrix.ReqLeave{
Reason: "Unsupported room type",
})
if err != nil {
log.Error().Err(err).Msg("Failed to leave room")
}
return
}
var hasBridgeBot, hasOtherUsers bool
for mxid, _ := range members.Joined {
if mxid == intent.UserID || mxid == inviter.GetMXID() {
continue
} else if mxid == mx.bridge.Bot.UserID {
hasBridgeBot = true
} else {
hasOtherUsers = true
}
}
if !hasBridgeBot && !hasOtherUsers && evt.Content.AsMember().IsDirect {
mx.bridge.Child.CreatePrivatePortal(evt.RoomID, inviter, ghost)
} else if !hasBridgeBot {
log.Debug().Msg("Leaving multi-user room after accepting invite")
_, _ = intent.SendNotice(ctx, evt.RoomID, "Please invite the bridge bot first if you want to bridge to a remote chat.")
_, _ = intent.LeaveRoom(ctx, evt.RoomID)
} else {
_, _ = intent.SendNotice(ctx, evt.RoomID, "This puppet will remain inactive until this room is bridged to a remote chat.")
}
}
func (mx *MatrixHandler) HandleMembership(ctx context.Context, evt *event.Event) {
if evt.Sender == mx.bridge.Bot.UserID || mx.bridge.Child.IsGhost(evt.Sender) {
return
}
defer mx.TrackEventDuration(evt.Type)()
if mx.bridge.Crypto != nil {
mx.bridge.Crypto.HandleMemberEvent(ctx, evt)
}
log := mx.log.With().
Str("sender", evt.Sender.String()).
Str("target", evt.GetStateKey()).
Str("room_id", evt.RoomID.String()).
Logger()
ctx = log.WithContext(ctx)
content := evt.Content.AsMember()
if content.Membership == event.MembershipInvite && id.UserID(evt.GetStateKey()) == mx.as.BotMXID() {
mx.HandleBotInvite(ctx, evt)
return
}
if mx.shouldIgnoreEvent(evt) {
return
}
user := mx.bridge.Child.GetIUser(evt.Sender, true)
if user == nil {
return
}
isSelf := id.UserID(evt.GetStateKey()) == evt.Sender
ghost := mx.bridge.Child.GetIGhost(id.UserID(evt.GetStateKey()))
portal := mx.bridge.Child.GetIPortal(evt.RoomID)
if portal == nil {
if ghost != nil && content.Membership == event.MembershipInvite {
mx.HandleGhostInvite(ctx, evt, user, ghost)
}
return
} else if user.GetPermissionLevel() < bridgeconfig.PermissionLevelUser || !user.IsLoggedIn() {
return
}
bhp, bhpOk := portal.(BanHandlingPortal)
mhp, mhpOk := portal.(MembershipHandlingPortal)
khp, khpOk := portal.(KnockHandlingPortal)
ihp, ihpOk := portal.(InviteHandlingPortal)
if !(mhpOk || bhpOk || khpOk) {
return
}
prevContent := &event.MemberEventContent{Membership: event.MembershipLeave}
if evt.Unsigned.PrevContent != nil {
_ = evt.Unsigned.PrevContent.ParseRaw(evt.Type)
prevContent, _ = evt.Unsigned.PrevContent.Parsed.(*event.MemberEventContent)
}
if ihpOk && prevContent.Membership == event.MembershipInvite && content.Membership != event.MembershipBan {
if content.Membership == event.MembershipJoin {
ihp.HandleMatrixAcceptInvite(user, evt)
}
if content.Membership == event.MembershipLeave {
if isSelf {
ihp.HandleMatrixRejectInvite(user, evt)
} else if ghost != nil {
ihp.HandleMatrixRetractInvite(user, ghost, evt)
}
}
}
if bhpOk && ghost != nil {
if content.Membership == event.MembershipBan {
bhp.HandleMatrixBan(user, ghost, evt)
} else if content.Membership == event.MembershipLeave && prevContent.Membership == event.MembershipBan {
bhp.HandleMatrixUnban(user, ghost, evt)
}
}
if khpOk {
if content.Membership == event.MembershipKnock {
khp.HandleMatrixKnock(user, evt)
} else if prevContent.Membership == event.MembershipKnock {
if content.Membership == event.MembershipInvite && ghost != nil {
khp.HandleMatrixAcceptKnock(user, ghost, evt)
} else if content.Membership == event.MembershipLeave {
if isSelf {
khp.HandleMatrixRetractKnock(user, evt)
} else if ghost != nil {
khp.HandleMatrixRejectKnock(user, ghost, evt)
}
}
}
}
if mhpOk {
if content.Membership == event.MembershipLeave && prevContent.Membership == event.MembershipJoin {
if isSelf {
mhp.HandleMatrixLeave(user, evt)
} else if ghost != nil {
mhp.HandleMatrixKick(user, ghost, evt)
}
} else if content.Membership == event.MembershipInvite && !isSelf && ghost != nil {
mhp.HandleMatrixInvite(user, ghost, evt)
}
}
// TODO kicking/inviting non-ghost users users
}
func (mx *MatrixHandler) HandleRoomMetadata(ctx context.Context, evt *event.Event) {
defer mx.TrackEventDuration(evt.Type)()
if mx.shouldIgnoreEvent(evt) {
return
}
user := mx.bridge.Child.GetIUser(evt.Sender, true)
if user == nil {
return
}
portal := mx.bridge.Child.GetIPortal(evt.RoomID)
if portal == nil || portal.IsPrivateChat() {
return
}
metaPortal, ok := portal.(MetaHandlingPortal)
if !ok {
return
}
metaPortal.HandleMatrixMeta(user, evt)
}
func (mx *MatrixHandler) shouldIgnoreEvent(evt *event.Event) bool {
if evt.Sender == mx.bridge.Bot.UserID || mx.bridge.Child.IsGhost(evt.Sender) {
return true
}
user := mx.bridge.Child.GetIUser(evt.Sender, true)
if user == nil || user.GetPermissionLevel() <= 0 {
return true
} else if val, ok := evt.Content.Raw[appservice.DoublePuppetKey]; ok && val == mx.bridge.Name && user.GetIDoublePuppet() != nil {
return true
}
return false
}
const initialSessionWaitTimeout = 3 * time.Second
const extendedSessionWaitTimeout = 22 * time.Second
func (mx *MatrixHandler) sendCryptoStatusError(ctx context.Context, evt *event.Event, editEvent id.EventID, err error, retryCount int, isFinal bool) id.EventID {
mx.bridge.SendMessageErrorCheckpoint(evt, status.MsgStepDecrypted, err, isFinal, retryCount)
if mx.bridge.Config.Bridge.EnableMessageStatusEvents() {
statusEvent := &event.BeeperMessageStatusEventContent{
// TODO: network
RelatesTo: event.RelatesTo{
Type: event.RelReference,
EventID: evt.ID,
},
Status: event.MessageStatusRetriable,
Reason: event.MessageStatusUndecryptable,
Error: err.Error(),
Message: errorToHumanMessage(err),
}
if !isFinal {
statusEvent.Status = event.MessageStatusPending
}
_, sendErr := mx.bridge.Bot.SendMessageEvent(ctx, evt.RoomID, event.BeeperMessageStatus, statusEvent)
if sendErr != nil {
zerolog.Ctx(ctx).Error().Err(err).Msg("Failed to send message status event")
}
}
if mx.bridge.Config.Bridge.EnableMessageErrorNotices() {
update := event.MessageEventContent{
MsgType: event.MsgNotice,
Body: fmt.Sprintf("\u26a0 Your message was not bridged: %v.", err),
}
if errors.Is(err, errNoCrypto) {
update.Body = "🔒 This bridge has not been configured to support encryption"
}
relatable, ok := evt.Content.Parsed.(event.Relatable)
if editEvent != "" {
update.SetEdit(editEvent)
} else if ok && relatable.OptionalGetRelatesTo().GetThreadParent() != "" {
update.GetRelatesTo().SetThread(relatable.OptionalGetRelatesTo().GetThreadParent(), evt.ID)
}
resp, sendErr := mx.bridge.Bot.SendMessageEvent(ctx, evt.RoomID, event.EventMessage, &update)
if sendErr != nil {
zerolog.Ctx(ctx).Error().Err(sendErr).Msg("Failed to send decryption error notice")
} else if resp != nil {
return resp.EventID
}
}
return ""
}
var (
errDeviceNotTrusted = errors.New("your device is not trusted")
errMessageNotEncrypted = errors.New("unencrypted message")
errNoDecryptionKeys = errors.New("the bridge hasn't received the decryption keys")
errNoCrypto = errors.New("this bridge has not been configured to support encryption")
)
func errorToHumanMessage(err error) string {
var withheld *event.RoomKeyWithheldEventContent
switch {
case errors.Is(err, errDeviceNotTrusted), errors.Is(err, errNoDecryptionKeys):
return err.Error()
case errors.Is(err, UnknownMessageIndex):
return "the keys received by the bridge can't decrypt the message"
case errors.Is(err, DuplicateMessageIndex):
return "your client encrypted multiple messages with the same key"
case errors.As(err, &withheld):
if withheld.Code == event.RoomKeyWithheldBeeperRedacted {
return "your client used an outdated encryption session"
}
return "your client refused to share decryption keys with the bridge"
case errors.Is(err, errMessageNotEncrypted):
return "the message is not encrypted"
default:
return "the bridge failed to decrypt the message"
}
}
func deviceUnverifiedErrorWithExplanation(trust id.TrustState) error {
var explanation string
switch trust {
case id.TrustStateBlacklisted:
explanation = "device is blacklisted"
case id.TrustStateUnset:
explanation = "unverified"
case id.TrustStateUnknownDevice:
explanation = "device info not found"
case id.TrustStateForwarded:
explanation = "keys were forwarded from an unknown device"
case id.TrustStateCrossSignedUntrusted:
explanation = "cross-signing keys changed after setting up the bridge"
default:
return errDeviceNotTrusted
}
return fmt.Errorf("%w (%s)", errDeviceNotTrusted, explanation)
}
func copySomeKeys(original, decrypted *event.Event) {
isScheduled, _ := original.Content.Raw["com.beeper.scheduled"].(bool)
_, alreadyExists := decrypted.Content.Raw["com.beeper.scheduled"]
if isScheduled && !alreadyExists {
decrypted.Content.Raw["com.beeper.scheduled"] = true
}
}
func (mx *MatrixHandler) postDecrypt(ctx context.Context, original, decrypted *event.Event, retryCount int, errorEventID id.EventID, duration time.Duration) {
log := zerolog.Ctx(ctx)
minLevel := mx.bridge.Config.Bridge.GetEncryptionConfig().VerificationLevels.Send
if decrypted.Mautrix.TrustState < minLevel {
logEvt := log.Warn().
Str("user_id", decrypted.Sender.String()).
Bool("forwarded_keys", decrypted.Mautrix.ForwardedKeys).
Stringer("device_trust", decrypted.Mautrix.TrustState).
Stringer("min_trust", minLevel)
if decrypted.Mautrix.TrustSource != nil {
dev := decrypted.Mautrix.TrustSource
logEvt.
Str("device_id", dev.DeviceID.String()).
Str("device_signing_key", dev.SigningKey.String())
} else {
logEvt.Str("device_id", "unknown")
}
logEvt.Msg("Dropping event due to insufficient verification level")
err := deviceUnverifiedErrorWithExplanation(decrypted.Mautrix.TrustState)
go mx.sendCryptoStatusError(ctx, decrypted, errorEventID, err, retryCount, true)
return
}
copySomeKeys(original, decrypted)
mx.bridge.SendMessageSuccessCheckpoint(decrypted, status.MsgStepDecrypted, retryCount)
decrypted.Mautrix.CheckpointSent = true
decrypted.Mautrix.DecryptionDuration = duration
decrypted.Mautrix.EventSource |= event.SourceDecrypted
mx.bridge.EventProcessor.Dispatch(ctx, decrypted)
if errorEventID != "" {
_, _ = mx.bridge.Bot.RedactEvent(ctx, decrypted.RoomID, errorEventID)
}
}
func (mx *MatrixHandler) HandleEncrypted(ctx context.Context, evt *event.Event) {
defer mx.TrackEventDuration(evt.Type)()
if mx.shouldIgnoreEvent(evt) {
return
}
content := evt.Content.AsEncrypted()
log := zerolog.Ctx(ctx).With().
Str("event_id", evt.ID.String()).
Str("session_id", content.SessionID.String()).
Logger()
ctx = log.WithContext(ctx)
if mx.bridge.Crypto == nil {
go mx.sendCryptoStatusError(ctx, evt, "", errNoCrypto, 0, true)
return
}
log.Debug().Msg("Decrypting received event")
decryptionStart := time.Now()
decrypted, err := mx.bridge.Crypto.Decrypt(ctx, evt)
decryptionRetryCount := 0
if errors.Is(err, NoSessionFound) {
decryptionRetryCount = 1
log.Debug().
Int("wait_seconds", int(initialSessionWaitTimeout.Seconds())).
Msg("Couldn't find session, waiting for keys to arrive...")
mx.bridge.SendMessageErrorCheckpoint(evt, status.MsgStepDecrypted, err, false, 0)
if mx.bridge.Crypto.WaitForSession(ctx, evt.RoomID, content.SenderKey, content.SessionID, initialSessionWaitTimeout) {
log.Debug().Msg("Got keys after waiting, trying to decrypt event again")
decrypted, err = mx.bridge.Crypto.Decrypt(ctx, evt)
} else {
go mx.waitLongerForSession(ctx, evt, decryptionStart)
return
}
}
if err != nil {
mx.bridge.SendMessageErrorCheckpoint(evt, status.MsgStepDecrypted, err, true, decryptionRetryCount)
log.Warn().Err(err).Msg("Failed to decrypt event")
go mx.sendCryptoStatusError(ctx, evt, "", err, decryptionRetryCount, true)
return
}
mx.postDecrypt(ctx, evt, decrypted, decryptionRetryCount, "", time.Since(decryptionStart))
}
func (mx *MatrixHandler) waitLongerForSession(ctx context.Context, evt *event.Event, decryptionStart time.Time) {
log := zerolog.Ctx(ctx)
content := evt.Content.AsEncrypted()
log.Debug().
Int("wait_seconds", int(extendedSessionWaitTimeout.Seconds())).
Msg("Couldn't find session, requesting keys and waiting longer...")
go mx.bridge.Crypto.RequestSession(ctx, evt.RoomID, content.SenderKey, content.SessionID, evt.Sender, content.DeviceID)
errorEventID := mx.sendCryptoStatusError(ctx, evt, "", fmt.Errorf("%w. The bridge will retry for %d seconds", errNoDecryptionKeys, int(extendedSessionWaitTimeout.Seconds())), 1, false)
if !mx.bridge.Crypto.WaitForSession(ctx, evt.RoomID, content.SenderKey, content.SessionID, extendedSessionWaitTimeout) {
log.Debug().Msg("Didn't get session, giving up trying to decrypt event")
mx.sendCryptoStatusError(ctx, evt, errorEventID, errNoDecryptionKeys, 2, true)
return
}
log.Debug().Msg("Got keys after waiting longer, trying to decrypt event again")
decrypted, err := mx.bridge.Crypto.Decrypt(ctx, evt)
if err != nil {
log.Error().Err(err).Msg("Failed to decrypt event")
mx.sendCryptoStatusError(ctx, evt, errorEventID, err, 2, true)
return
}
mx.postDecrypt(ctx, evt, decrypted, 2, errorEventID, time.Since(decryptionStart))
}
func (mx *MatrixHandler) HandleMessage(ctx context.Context, evt *event.Event) {
defer mx.TrackEventDuration(evt.Type)()
log := zerolog.Ctx(ctx).With().
Str("event_id", evt.ID.String()).
Str("room_id", evt.RoomID.String()).
Str("sender", evt.Sender.String()).
Logger()
ctx = log.WithContext(ctx)
if mx.shouldIgnoreEvent(evt) {
return
} else if !evt.Mautrix.WasEncrypted && mx.bridge.Config.Bridge.GetEncryptionConfig().Require {
log.Warn().Msg("Dropping unencrypted event")
mx.sendCryptoStatusError(ctx, evt, "", errMessageNotEncrypted, 0, true)
return
}
user := mx.bridge.Child.GetIUser(evt.Sender, true)
if user == nil {
return
}
content := evt.Content.AsMessage()
content.RemoveReplyFallback()
if user.GetPermissionLevel() >= bridgeconfig.PermissionLevelUser && content.MsgType == event.MsgText {
commandPrefix := mx.bridge.Config.Bridge.GetCommandPrefix()
hasCommandPrefix := strings.HasPrefix(content.Body, commandPrefix)
if hasCommandPrefix {
content.Body = strings.TrimLeft(strings.TrimPrefix(content.Body, commandPrefix), " ")
}
if hasCommandPrefix || evt.RoomID == user.GetManagementRoomID() {
go mx.bridge.CommandProcessor.Handle(ctx, evt.RoomID, evt.ID, user, content.Body, content.RelatesTo.GetReplyTo())
go mx.bridge.SendMessageSuccessCheckpoint(evt, status.MsgStepCommand, 0)
if mx.bridge.Config.Bridge.EnableMessageStatusEvents() {
statusEvent := &event.BeeperMessageStatusEventContent{
// TODO: network
RelatesTo: event.RelatesTo{
Type: event.RelReference,
EventID: evt.ID,
},
Status: event.MessageStatusSuccess,
}
_, sendErr := mx.bridge.Bot.SendMessageEvent(ctx, evt.RoomID, event.BeeperMessageStatus, statusEvent)
if sendErr != nil {
log.Warn().Err(sendErr).Msg("Failed to send message status event for command")
}
}
return
}
}
portal := mx.bridge.Child.GetIPortal(evt.RoomID)
if portal != nil {
portal.ReceiveMatrixEvent(user, evt)
} else {
mx.bridge.SendMessageErrorCheckpoint(evt, status.MsgStepRemote, fmt.Errorf("unknown room"), true, 0)
}
}
func (mx *MatrixHandler) HandleReaction(_ context.Context, evt *event.Event) {
defer mx.TrackEventDuration(evt.Type)()
if mx.shouldIgnoreEvent(evt) {
return
}
user := mx.bridge.Child.GetIUser(evt.Sender, true)
if user == nil || user.GetPermissionLevel() < bridgeconfig.PermissionLevelUser || !user.IsLoggedIn() {
return
}
portal := mx.bridge.Child.GetIPortal(evt.RoomID)
if portal != nil {
portal.ReceiveMatrixEvent(user, evt)
} else {
mx.bridge.SendMessageErrorCheckpoint(evt, status.MsgStepRemote, fmt.Errorf("unknown room"), true, 0)
}
}
func (mx *MatrixHandler) HandleRedaction(_ context.Context, evt *event.Event) {
defer mx.TrackEventDuration(evt.Type)()
if mx.shouldIgnoreEvent(evt) {
return
}
user := mx.bridge.Child.GetIUser(evt.Sender, true)
if user == nil {
return
}
portal := mx.bridge.Child.GetIPortal(evt.RoomID)
if portal != nil {
portal.ReceiveMatrixEvent(user, evt)
} else {
mx.bridge.SendMessageErrorCheckpoint(evt, status.MsgStepRemote, fmt.Errorf("unknown room"), true, 0)
}
}
func (mx *MatrixHandler) HandleReceipt(_ context.Context, evt *event.Event) {
portal := mx.bridge.Child.GetIPortal(evt.RoomID)
if portal == nil {
return
}
rrPortal, ok := portal.(ReadReceiptHandlingPortal)
if !ok {
return
}
for eventID, receipts := range *evt.Content.AsReceipt() {
for userID, receipt := range receipts[event.ReceiptTypeRead] {
user := mx.bridge.Child.GetIUser(userID, false)
if user == nil {
// Not a bridge user
continue
}
customPuppet := user.GetIDoublePuppet()
if val, ok := receipt.Extra[appservice.DoublePuppetKey].(string); ok && customPuppet != nil && val == mx.bridge.Name {
// Ignore double puppeted read receipts.
mx.log.Debug().Interface("content", evt.Content.Raw).Msg("Ignoring double-puppeted read receipt")
// But do start disappearing messages, because the user read the chat
dp, ok := portal.(DisappearingPortal)
if ok {
dp.ScheduleDisappearing()
}
} else {
rrPortal.HandleMatrixReadReceipt(user, eventID, receipt)
}
}
}
}
func (mx *MatrixHandler) HandleTyping(_ context.Context, evt *event.Event) {
portal := mx.bridge.Child.GetIPortal(evt.RoomID)
if portal == nil {
return
}
typingPortal, ok := portal.(TypingPortal)
if !ok {
return
}
typingPortal.HandleMatrixTyping(evt.Content.AsTyping().UserIDs)
}
func (mx *MatrixHandler) HandlePowerLevels(_ context.Context, evt *event.Event) {
if mx.shouldIgnoreEvent(evt) {
return
}
portal := mx.bridge.Child.GetIPortal(evt.RoomID)
if portal == nil {
return
}
powerLevelPortal, ok := portal.(PowerLevelHandlingPortal)
if ok {
user := mx.bridge.Child.GetIUser(evt.Sender, true)
powerLevelPortal.HandleMatrixPowerLevels(user, evt)
}
}
func (mx *MatrixHandler) HandleJoinRule(_ context.Context, evt *event.Event) {
if mx.shouldIgnoreEvent(evt) {
return
}
portal := mx.bridge.Child.GetIPortal(evt.RoomID)
if portal == nil {
return
}
joinRulePortal, ok := portal.(JoinRuleHandlingPortal)
if ok {
user := mx.bridge.Child.GetIUser(evt.Sender, true)
joinRulePortal.HandleMatrixJoinRule(user, evt)
}
}