From 7f27c766591c3cfa29adb556e619732f1d79e397 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 31 May 2022 17:28:58 +0300 Subject: [PATCH] Improve tracking errors in Matrix->WhatsApp bridging Closes #231 --- config/bridge.go | 2 + config/upgrade.go | 2 + example-config.yaml | 4 + go.mod | 2 +- go.sum | 4 +- main.go | 2 + portal.go | 309 +++++++++++++++++++++++++++++--------------- 7 files changed, 215 insertions(+), 110 deletions(-) diff --git a/config/bridge.go b/config/bridge.go index 30b3687..c9c067b 100644 --- a/config/bridge.go +++ b/config/bridge.go @@ -48,6 +48,8 @@ type BridgeConfig struct { PersonalFilteringSpaces bool `yaml:"personal_filtering_spaces"` DeliveryReceipts bool `yaml:"delivery_receipts"` + MessageStatusEvents bool `yaml:"message_status_events"` + MessageErrorNotices bool `yaml:"message_error_notices"` PortalMessageBuffer int `yaml:"portal_message_buffer"` CallStartNotices bool `yaml:"call_start_notices"` IdentityChangeNotices bool `yaml:"identity_change_notices"` diff --git a/config/upgrade.go b/config/upgrade.go index 49b30fc..193296b 100644 --- a/config/upgrade.go +++ b/config/upgrade.go @@ -39,6 +39,8 @@ func DoUpgrade(helper *up.Helper) { helper.Copy(up.Str, "bridge", "displayname_template") helper.Copy(up.Bool, "bridge", "personal_filtering_spaces") helper.Copy(up.Bool, "bridge", "delivery_receipts") + helper.Copy(up.Bool, "bridge", "message_status_events") + helper.Copy(up.Bool, "bridge", "message_error_notices") helper.Copy(up.Int, "bridge", "portal_message_buffer") helper.Copy(up.Bool, "bridge", "call_start_notices") helper.Copy(up.Bool, "bridge", "identity_change_notices") diff --git a/example-config.yaml b/example-config.yaml index c460893..7d7a43d 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -100,6 +100,10 @@ bridge: personal_filtering_spaces: false # Should the bridge send a read receipt from the bridge bot when a message has been sent to WhatsApp? delivery_receipts: false + # Whether the bridge should send the message status as a custom com.beeper.message_send_status event. + message_status_events: false + # Whether the bridge should send error notices via m.notice events when a message fails to bridge. + message_error_notices: true # Should incoming calls send a message to the Matrix room? call_start_notices: true # Should another user's cryptographic identity changing send a message to Matrix? diff --git a/go.mod b/go.mod index 3044803..8994926 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( golang.org/x/net v0.0.0-20220513224357-95641704303c google.golang.org/protobuf v1.28.0 maunium.net/go/maulogger/v2 v2.3.2 - maunium.net/go/mautrix v0.11.1-0.20220530212627-b15517460fdb + maunium.net/go/mautrix v0.11.1-0.20220531132903-5853dab58019 ) require ( diff --git a/go.sum b/go.sum index ac5083c..6e52056 100644 --- a/go.sum +++ b/go.sum @@ -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/maulogger/v2 v2.3.2 h1:1XmIYmMd3PoQfp9J+PaHhpt80zpfmMqaShzUTC7FwY0= maunium.net/go/maulogger/v2 v2.3.2/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A= -maunium.net/go/mautrix v0.11.1-0.20220530212627-b15517460fdb h1:MTY4bW0yhg8bHszMSNymTrHvIRdiDjcnQKC8qSbt5BE= -maunium.net/go/mautrix v0.11.1-0.20220530212627-b15517460fdb/go.mod h1:CiKpMhAx5QZFHK03jpWb0iKI3sGU8x6+LfsOjDrcO8I= +maunium.net/go/mautrix v0.11.1-0.20220531132903-5853dab58019 h1:jrucVlf+iLAXB2WvOERaDhO0T3sBizJEG7Tsmkz6GY0= +maunium.net/go/mautrix v0.11.1-0.20220531132903-5853dab58019/go.mod h1:CiKpMhAx5QZFHK03jpWb0iKI3sGU8x6+LfsOjDrcO8I= diff --git a/main.go b/main.go index 6b1bcc2..bd74f83 100644 --- a/main.go +++ b/main.go @@ -271,6 +271,8 @@ func main() { Version: "0.4.0", ProtocolName: "WhatsApp", + CryptoPickleKey: "maunium.net/go/mautrix-whatsapp", + ConfigUpgrader: &configupgrade.StructUpgrader{ SimpleUpgrader: configupgrade.SimpleUpgrader(config.DoUpgrade), Blocks: config.SpacedBlocks, diff --git a/portal.go b/portal.go index e9fb727..86906c4 100644 --- a/portal.go +++ b/portal.go @@ -1207,6 +1207,10 @@ func (portal *Portal) RestrictMetadataChanges(restrict bool) id.EventID { return "" } +func (portal *Portal) getBridgeInfoStateKey() string { + return fmt.Sprintf("net.maunium.whatsapp://whatsapp/%s", portal.Key.JID) +} + func (portal *Portal) getBridgeInfo() (string, event.BridgeEventContent) { bridgeInfo := event.BridgeEventContent{ BridgeBot: portal.bridge.Bot.UserID, @@ -1223,8 +1227,7 @@ func (portal *Portal) getBridgeInfo() (string, event.BridgeEventContent) { AvatarURL: portal.AvatarURL.CUString(), }, } - bridgeInfoStateKey := fmt.Sprintf("net.maunium.whatsapp://whatsapp/%s", portal.Key.JID) - return bridgeInfoStateKey, bridgeInfo + return portal.getBridgeInfoStateKey(), bridgeInfo } func (portal *Portal) UpdateBridgeInfo() { @@ -2546,7 +2549,28 @@ func (portal *Portal) convertWebPtoPNG(webpImage []byte) ([]byte, error) { return pngBuffer.Bytes(), nil } -func (portal *Portal) preprocessMatrixMedia(sender *User, relaybotFormatted bool, content *event.MessageEventContent, eventID id.EventID, mediaType whatsmeow.MediaType) *MediaUpload { +type DualError struct { + High error + Low error +} + +func NewDualError(high, low error) DualError { + return DualError{high, low} +} + +func (err DualError) Is(other error) bool { + return errors.Is(other, err.High) || errors.Is(other, err.Low) +} + +func (err DualError) Unwrap() error { + return err.Low +} + +func (err DualError) Error() string { + return fmt.Sprintf("%v: %v", err.High, err.Low) +} + +func (portal *Portal) preprocessMatrixMedia(sender *User, relaybotFormatted bool, content *event.MessageEventContent, eventID id.EventID, mediaType whatsmeow.MediaType) (*MediaUpload, error) { var caption string var mentionedJIDs []string if relaybotFormatted { @@ -2561,19 +2585,16 @@ func (portal *Portal) preprocessMatrixMedia(sender *User, relaybotFormatted bool } mxc, err := rawMXC.Parse() if err != nil { - portal.log.Errorln("Malformed content URL in %s: %v", eventID, err) - return nil + return nil, err } data, err := portal.MainIntent().DownloadBytes(mxc) if err != nil { - portal.log.Errorfln("Failed to download media in %s: %v", eventID, err) - return nil + return nil, NewDualError(errMediaDownloadFailed, err) } if file != nil { err = file.DecryptInPlace(data) if err != nil { - portal.log.Errorfln("Failed to decrypt media in %s: %v", eventID, err) - return nil + return nil, NewDualError(errMediaDecryptFailed, err) } } if mediaType == whatsmeow.MediaVideo && content.GetInfo().MimeType == "image/gif" { @@ -2582,23 +2603,20 @@ func (portal *Portal) preprocessMatrixMedia(sender *User, relaybotFormatted bool "-filter:v", "crop='floor(in_w/2)*2:floor(in_h/2)*2'", }, content.GetInfo().MimeType) if err != nil { - portal.log.Errorfln("Failed to convert gif to mp4 in %s: %v", eventID, err) - return nil + return nil, NewDualError(fmt.Errorf("%w (gif to mp4)", errMediaConvertFailed), err) } content.Info.MimeType = "video/mp4" } if mediaType == whatsmeow.MediaImage && content.GetInfo().MimeType == "image/webp" { data, err = portal.convertWebPtoPNG(data) if err != nil { - portal.log.Errorfln("Failed to convert webp to png in %s: %v", eventID, err) - return nil + return nil, NewDualError(fmt.Errorf("%w (webp to png)", errMediaConvertFailed), err) } content.Info.MimeType = "image/png" } uploadResp, err := sender.Client.Upload(context.Background(), data, mediaType) if err != nil { - portal.log.Errorfln("Failed to upload media in %s: %v", eventID, err) - return nil + return nil, NewDualError(errMediaWhatsAppUploadFailed, err) } // Audio doesn't have thumbnails @@ -2607,7 +2625,7 @@ func (portal *Portal) preprocessMatrixMedia(sender *User, relaybotFormatted bool thumbnail, err = portal.downloadThumbnail(data, content.GetInfo().ThumbnailURL, eventID) // Ignore format errors for non-image files, we don't care about those thumbnails if err != nil && (!errors.Is(err, image.ErrFormat) || mediaType == whatsmeow.MediaImage) { - portal.log.Errorfln("Failed to generate thumbnail for %s: %v", eventID, err) + portal.log.Warnfln("Failed to generate thumbnail for %s: %v", eventID, err) } } @@ -2617,7 +2635,7 @@ func (portal *Portal) preprocessMatrixMedia(sender *User, relaybotFormatted bool MentionedJIDs: mentionedJIDs, Thumbnail: thumbnail, FileLength: len(data), - } + }, nil } type MediaUpload struct { @@ -2695,11 +2713,10 @@ func getUnstableWaveform(content map[string]interface{}) []byte { return output } -func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waProto.Message, *User) { +func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waProto.Message, *User, error) { content, ok := evt.Content.Parsed.(*event.MessageEventContent) if !ok { - portal.log.Debugfln("Failed to handle event %s: unexpected parsed content type %T", evt.ID, evt.Content.Parsed) - return nil, sender + return nil, sender, fmt.Errorf("%w %T", errUnexpectedParsedContentType, evt.Content.Parsed) } var msg waProto.Message @@ -2724,8 +2741,7 @@ func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waP relaybotFormatted := false if !sender.IsLoggedIn() || (portal.IsPrivateChat() && sender.JID.User != portal.Key.Receiver.User) { if !portal.HasRelaybot() { - portal.log.Warnln("Ignoring message from", sender.MXID, "in chat with no relaybot (convertMatrixMessage)") - return nil, sender + return nil, sender, errUserNotLoggedIn } relaybotFormatted = portal.addRelaybotFormat(sender, content) sender = portal.GetRelayUser() @@ -2741,7 +2757,7 @@ func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waP case event.MsgText, event.MsgEmote, event.MsgNotice: text := content.Body if content.MsgType == event.MsgNotice && !portal.bridge.Config.Bridge.BridgeNotices { - return nil, sender + return nil, sender, errMNoticeDisabled } if content.Format == event.FormatHTML { text, ctxInfo.MentionedJid = portal.bridge.Formatter.ParseMatrix(content.FormattedBody) @@ -2760,9 +2776,9 @@ func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waP msg.Conversation = &text } case event.MsgImage: - media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsmeow.MediaImage) + media, err := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsmeow.MediaImage) if media == nil { - return nil, sender + return nil, sender, err } ctxInfo.MentionedJid = media.MentionedJIDs msg.ImageMessage = &waProto.ImageMessage{ @@ -2778,9 +2794,9 @@ func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waP } case event.MsgVideo: gifPlayback := content.GetInfo().MimeType == "image/gif" - media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsmeow.MediaVideo) + media, err := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsmeow.MediaVideo) if media == nil { - return nil, sender + return nil, sender, err } duration := uint32(content.GetInfo().Duration / 1000) ctxInfo.MentionedJid = media.MentionedJIDs @@ -2798,9 +2814,9 @@ func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waP FileLength: proto.Uint64(uint64(media.FileLength)), } case event.MsgAudio: - media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsmeow.MediaAudio) + media, err := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsmeow.MediaAudio) if media == nil { - return nil, sender + return nil, sender, err } duration := uint32(content.GetInfo().Duration / 1000) msg.AudioMessage = &waProto.AudioMessage{ @@ -2821,9 +2837,9 @@ func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waP msg.AudioMessage.Mimetype = proto.String(addCodecToMime(content.GetInfo().MimeType, "opus")) } case event.MsgFile: - media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsmeow.MediaDocument) + media, err := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsmeow.MediaDocument) if media == nil { - return nil, sender + return nil, sender, err } msg.DocumentMessage = &waProto.DocumentMessage{ ContextInfo: &ctxInfo, @@ -2840,8 +2856,7 @@ func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waP case event.MsgLocation: lat, long, err := parseGeoURI(content.GeoURI) if err != nil { - portal.log.Debugfln("Invalid geo URI on Matrix event %s: %v", evt.ID, err) - return nil, sender + return nil, sender, fmt.Errorf("%w: %v", errInvalidGeoURI, err) } msg.LocationMessage = &waProto.LocationMessage{ DegreesLatitude: &lat, @@ -2850,13 +2865,15 @@ func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waP ContextInfo: &ctxInfo, } default: - portal.log.Debugfln("Unhandled Matrix event %s: unknown msgtype %s", evt.ID, content.MsgType) - return nil, sender + return nil, sender, fmt.Errorf("%w %q", errUnknownMsgType, content.MsgType) } - return &msg, sender + return &msg, sender, nil } func (portal *Portal) sendErrorMessage(message string, confirmed bool) id.EventID { + if !portal.bridge.Config.Bridge.MessageErrorNotices { + return "" + } certainty := "may not have been" if confirmed { certainty = "was not" @@ -2872,6 +2889,87 @@ func (portal *Portal) sendErrorMessage(message string, confirmed bool) id.EventI return resp.EventID } +var ( + errUserNotConnected = errors.New("you are not connected to WhatsApp") + errDifferentUser = errors.New("user is not the recipient of this private chat portal") + errUserNotLoggedIn = errors.New("user is not logged in and chat has no relay bot") + errMNoticeDisabled = errors.New("bridging m.notice messages is disabled") + errUnexpectedParsedContentType = errors.New("unexpected parsed content type") + errInvalidGeoURI = errors.New("invalid `geo:` URI in message") + errUnknownMsgType = errors.New("unknown msgtype") + errMediaDownloadFailed = errors.New("failed to download media") + errMediaDecryptFailed = errors.New("failed to decrypt media") + errMediaConvertFailed = errors.New("failed to convert media") + errMediaWhatsAppUploadFailed = errors.New("failed to upload media to WhatsApp") + errTargetNotFound = errors.New("target event not found") + errReactionDatabaseNotFound = errors.New("reaction database entry not found") + errReactionTargetNotFound = errors.New("reaction target message not found") + errTargetIsFake = errors.New("target is a fake event") + errTargetSentBySomeoneElse = errors.New("target is a fake event") + + errMessageDisconnected = &whatsmeow.DisconnectedError{Action: "message send"} + errMessageRetryDisconnected = &whatsmeow.DisconnectedError{Action: "message send (retry)"} +) + +func errorToStatusReason(err error) (reason event.MessageStatusReason, isCertain, canRetry, sendNotice bool) { + switch { + case errors.Is(err, whatsmeow.ErrBroadcastListUnsupported), + errors.Is(err, errUnexpectedParsedContentType), + errors.Is(err, errUnknownMsgType), + errors.Is(err, errInvalidGeoURI), + errors.Is(err, whatsmeow.ErrUnknownServer), + errors.Is(err, whatsmeow.ErrRecipientADJID): + return event.MessageStatusUnsupported, true, false, true + case errors.Is(err, errTargetNotFound), + errors.Is(err, errTargetIsFake), + errors.Is(err, errReactionDatabaseNotFound), + errors.Is(err, errReactionTargetNotFound), + errors.Is(err, errTargetSentBySomeoneElse): + return event.MessageStatusGenericError, true, false, false + case errors.Is(err, whatsmeow.ErrNotConnected), + errors.Is(err, errUserNotConnected): + return event.MessageStatusGenericError, true, true, true + case errors.Is(err, errUserNotLoggedIn), + errors.Is(err, errDifferentUser): + return event.MessageStatusGenericError, true, true, false + case errors.Is(err, errMessageDisconnected), + errors.Is(err, errMessageRetryDisconnected): + return event.MessageStatusGenericError, false, true, true + default: + return event.MessageStatusGenericError, false, true, true + } +} + +func (portal *Portal) sendStatusEvent(evtID id.EventID, err error) { + if !portal.bridge.Config.Bridge.MessageStatusEvents { + return + } + intent := portal.bridge.Bot + if !portal.Encrypted { + // Bridge bot isn't present in unencrypted DMs + intent = portal.MainIntent() + } + content := event.BeeperMessageStatusEventContent{ + Network: portal.getBridgeInfoStateKey(), + RelatesTo: event.RelatesTo{ + Type: event.RelReference, + EventID: evtID, + }, + Success: err == nil, + } + if !content.Success { + reason, isCertain, canRetry, _ := errorToStatusReason(err) + content.Reason = reason + content.IsCertain = &isCertain + content.CanRetry = &canRetry + content.Error = err.Error() + } + _, err = intent.SendMessageEvent(portal.MXID, event.BeeperMessageStatus, &content) + if err != nil { + portal.log.Warnln("Failed to send message status event:", err) + } +} + func (portal *Portal) sendDeliveryReceipt(eventID id.EventID) { if portal.bridge.Config.Bridge.DeliveryReceipts { err := portal.bridge.Bot.MarkRead(portal.MXID, eventID) @@ -2894,13 +2992,52 @@ func (portal *Portal) generateMessageInfo(sender *User) *types.MessageInfo { } } +func (portal *Portal) sendMessageMetrics(evt *event.Event, err error, part string) { + var msgType string + switch evt.Type { + case event.EventMessage: + msgType = "message" + case event.EventReaction: + msgType = "reaction" + case event.EventRedaction: + msgType = "redaction" + default: + msgType = "unknown event" + } + evtDescription := evt.ID.String() + if evt.Type == event.EventRedaction { + evtDescription += fmt.Sprintf(" of %s", evt.Redacts) + } + if err != nil { + level := log.LevelError + if part == "Ignoring" { + level = log.LevelDebug + } + portal.log.Logfln(level, "%s %s %s from %s: %v", part, msgType, evtDescription, evt.Sender, err) + reason, isCertain, _, sendNotice := errorToStatusReason(err) + status := bridge.ReasonToCheckpointStatus(reason) + portal.bridge.SendMessageCheckpoint(evt, bridge.MsgStepRemote, err, status, 0) + if sendNotice { + portal.sendErrorMessage(err.Error(), isCertain) + } + portal.sendStatusEvent(evt.ID, err) + } else { + portal.log.Debugfln("Handled Matrix %s %s", msgType, evtDescription) + portal.sendDeliveryReceipt(evt.ID) + portal.bridge.SendMessageSuccessCheckpoint(evt, bridge.MsgStepRemote, 0) + portal.sendStatusEvent(evt.ID, nil) + } +} + func (portal *Portal) HandleMatrixMessage(sender *User, evt *event.Event) { - if !portal.canBridgeFrom(sender, "message") { + if err := portal.canBridgeFrom(sender, true); err != nil { + go portal.sendMessageMetrics(evt, err, "Ignoring") return } - portal.log.Debugfln("Received event %s from %s", evt.ID, evt.Sender) - msg, sender := portal.convertMatrixMessage(sender, evt) + portal.log.Debugfln("Received message %s from %s", evt.ID, evt.Sender) + msg, sender, err := portal.convertMatrixMessage(sender, evt) if msg == nil { + go portal.sendMessageMetrics(evt, err, "Error converting") return } portal.MarkDisappearing(evt.ID, portal.ExpirationTime, true) @@ -2908,24 +3045,15 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *event.Event) { dbMsg := portal.markHandled(nil, nil, info, evt.ID, false, true, database.MsgNormal, database.MsgNoError) portal.log.Debugln("Sending event", evt.ID, "to WhatsApp", info.ID) ts, err := sender.Client.SendMessage(portal.Key.JID, info.ID, msg) - if err != nil { - portal.log.Errorfln("Error sending message: %v", err) - portal.sendErrorMessage(err.Error(), true) - status := bridge.MsgStatusPermFailure - if errors.Is(err, whatsmeow.ErrBroadcastListUnsupported) { - status = bridge.MsgStatusUnsupported - } - portal.bridge.SendMessageCheckpoint(evt, bridge.MsgStepRemote, err, status, 0) - } else { - portal.log.Debugfln("Handled Matrix event %s", evt.ID) - portal.bridge.SendMessageSuccessCheckpoint(evt, bridge.MsgStepRemote, 0) - portal.sendDeliveryReceipt(evt.ID) + go portal.sendMessageMetrics(evt, err, "Error sending") + if err == nil { dbMsg.MarkSent(ts) } } func (portal *Portal) HandleMatrixReaction(sender *User, evt *event.Event) { - if portal.IsPrivateChat() && sender.JID.User != portal.Key.Receiver.User { + if err := portal.canBridgeFrom(sender, false); err != nil { + go portal.sendMessageMetrics(evt, err, "Ignoring") return } @@ -2942,14 +3070,7 @@ func (portal *Portal) HandleMatrixReaction(sender *User, evt *event.Event) { portal.log.Debugfln("Received reaction event %s from %s", evt.ID, evt.Sender) err := portal.handleMatrixReaction(sender, evt) - if err != nil { - portal.log.Errorfln("Error sending reaction %s: %v", evt.ID, err) - portal.bridge.SendMessageErrorCheckpoint(evt, bridge.MsgStepRemote, err, true, 0) - } else { - portal.log.Debugfln("Handled Matrix reaction %s", evt.ID) - portal.bridge.SendMessageSuccessCheckpoint(evt, bridge.MsgStepRemote, 0) - portal.sendDeliveryReceipt(evt.ID) - } + go portal.sendMessageMetrics(evt, err, "Error sending") } func (portal *Portal) handleMatrixReaction(sender *User, evt *event.Event) error { @@ -2966,7 +3087,7 @@ func (portal *Portal) handleMatrixReaction(sender *User, evt *event.Event) error portal.upsertReaction(nil, target.JID, sender.JID, evt.ID, info.ID) portal.log.Debugln("Sending reaction", evt.ID, "to WhatsApp", info.ID) ts, err := portal.sendReactionToWhatsApp(sender, info.ID, target, content.RelatesTo.Key, evt.Timestamp) - if err != nil { + if err == nil { dbMsg.MarkSent(ts) } return err @@ -3022,7 +3143,8 @@ func (portal *Portal) upsertReaction(intent *appservice.IntentAPI, targetJID typ } func (portal *Portal) HandleMatrixRedaction(sender *User, evt *event.Event) { - if !portal.canBridgeFrom(sender, "redaction") { + if err := portal.canBridgeFrom(sender, true); err != nil { + go portal.sendMessageMetrics(evt, err, "Ignoring") return } portal.log.Debugfln("Received redaction %s from %s", evt.ID, evt.Sender) @@ -3035,44 +3157,25 @@ func (portal *Portal) HandleMatrixRedaction(sender *User, evt *event.Event) { msg := portal.bridge.DB.Message.GetByMXID(evt.Redacts) if msg == nil { - portal.log.Debugfln("Ignoring redaction %s of unknown event by %s", evt.ID, senderLogIdentifier) - portal.bridge.SendMessageErrorCheckpoint(evt, bridge.MsgStepRemote, errors.New("target not found"), true, 0) - return + go portal.sendMessageMetrics(evt, errTargetNotFound, "Ignoring") } else if msg.IsFakeJID() { - portal.log.Debugfln("Ignoring redaction %s of fake event by %s", evt.ID, senderLogIdentifier) - portal.bridge.SendMessageErrorCheckpoint(evt, bridge.MsgStepRemote, errors.New("target is a fake event"), true, 0) - return + go portal.sendMessageMetrics(evt, errTargetIsFake, "Ignoring") } else if msg.Sender.User != sender.JID.User { - portal.log.Debugfln("Ignoring redaction %s of %s/%s by %s: message was sent by someone else (%s, not %s)", evt.ID, msg.MXID, msg.JID, senderLogIdentifier, msg.Sender, sender.JID) - portal.bridge.SendMessageErrorCheckpoint(evt, bridge.MsgStepRemote, errors.New("message was sent by someone else"), true, 0) - return - } - - var err error - if msg.Type == database.MsgReaction { + go portal.sendMessageMetrics(evt, errTargetSentBySomeoneElse, "Ignoring") + } else if msg.Type == database.MsgReaction { if reaction := portal.bridge.DB.Reaction.GetByMXID(evt.Redacts); reaction == nil { - portal.log.Debugfln("Ignoring redaction of reaction %s: reaction database entry not found", evt.ID) - portal.bridge.SendMessageErrorCheckpoint(evt, bridge.MsgStepRemote, errors.New("reaction database entry not found"), true, 0) - return + go portal.sendMessageMetrics(evt, errReactionDatabaseNotFound, "Ignoring") } else if reactionTarget := reaction.GetTarget(); reactionTarget == nil { - portal.log.Debugfln("Ignoring redaction of reaction %s: reaction target message not found", evt.ID) - portal.bridge.SendMessageErrorCheckpoint(evt, bridge.MsgStepRemote, errors.New("reaction target message not found"), true, 0) - return + go portal.sendMessageMetrics(evt, errReactionTargetNotFound, "Ignoring") } else { portal.log.Debugfln("Sending redaction reaction %s of %s/%s to WhatsApp", evt.ID, msg.MXID, msg.JID) - _, err = portal.sendReactionToWhatsApp(sender, "", reactionTarget, "", evt.Timestamp) + _, err := portal.sendReactionToWhatsApp(sender, "", reactionTarget, "", evt.Timestamp) + go portal.sendMessageMetrics(evt, err, "Error sending") } } else { portal.log.Debugfln("Sending redaction %s of %s/%s to WhatsApp", evt.ID, msg.MXID, msg.JID) - _, err = sender.Client.RevokeMessage(portal.Key.JID, msg.JID) - } - if err != nil { - portal.log.Errorfln("Error handling Matrix redaction %s: %v", evt.ID, err) - portal.bridge.SendMessageErrorCheckpoint(evt, bridge.MsgStepRemote, err, true, 0) - } else { - portal.log.Debugfln("Handled Matrix redaction %s of %s", evt.ID, evt.Redacts) - portal.bridge.SendMessageSuccessCheckpoint(evt, bridge.MsgStepRemote, 0) - portal.sendDeliveryReceipt(evt.ID) + _, err := sender.Client.RevokeMessage(portal.Key.JID, msg.JID) + go portal.sendMessageMetrics(evt, err, "Error sending") } } @@ -3192,27 +3295,19 @@ func (portal *Portal) HandleMatrixTyping(newTyping []id.UserID) { portal.setTyping(stoppedTyping, types.ChatPresencePaused) } -func (portal *Portal) canBridgeFrom(sender *User, evtType string) bool { +func (portal *Portal) canBridgeFrom(sender *User, allowRelay bool) error { if !sender.IsLoggedIn() { - if portal.HasRelaybot() { - return true + if allowRelay && portal.HasRelaybot() { + return nil } else if sender.Session != nil { - portal.log.Debugfln("Ignoring %s from %s as user is not connected", evtType, sender.MXID) - msg := format.RenderMarkdown(fmt.Sprintf("\u26a0 You are not connected to WhatsApp, so your %s was not bridged.", evtType), true, false) - msg.MsgType = event.MsgNotice - _, err := portal.sendMainIntentMessage(&msg) - if err != nil { - portal.log.Errorln("Failed to send bridging failure message:", err) - } + return errUserNotConnected } else { - portal.log.Debugfln("Ignoring %s from non-logged-in user %s in chat with no relay user", evtType, sender.MXID) + return errUserNotLoggedIn } - return false - } else if portal.IsPrivateChat() && sender.JID.User != portal.Key.Receiver.User && !portal.HasRelaybot() { - portal.log.Debugfln("Ignoring %s from different user %s/%s in private chat with no relay user", evtType, sender.MXID, sender.JID) - return false + } else if portal.IsPrivateChat() && sender.JID.User != portal.Key.Receiver.User && (!allowRelay || !portal.HasRelaybot()) { + return errDifferentUser } - return true + return nil } func (portal *Portal) Delete() {