Improve tracking errors in Matrix->WhatsApp bridging

Closes #231
This commit is contained in:
Tulir Asokan 2022-05-31 17:28:58 +03:00
parent df99750636
commit 7f27c76659
7 changed files with 215 additions and 110 deletions

View file

@ -48,6 +48,8 @@ type BridgeConfig struct {
PersonalFilteringSpaces bool `yaml:"personal_filtering_spaces"` PersonalFilteringSpaces bool `yaml:"personal_filtering_spaces"`
DeliveryReceipts bool `yaml:"delivery_receipts"` DeliveryReceipts bool `yaml:"delivery_receipts"`
MessageStatusEvents bool `yaml:"message_status_events"`
MessageErrorNotices bool `yaml:"message_error_notices"`
PortalMessageBuffer int `yaml:"portal_message_buffer"` PortalMessageBuffer int `yaml:"portal_message_buffer"`
CallStartNotices bool `yaml:"call_start_notices"` CallStartNotices bool `yaml:"call_start_notices"`
IdentityChangeNotices bool `yaml:"identity_change_notices"` IdentityChangeNotices bool `yaml:"identity_change_notices"`

View file

@ -39,6 +39,8 @@ func DoUpgrade(helper *up.Helper) {
helper.Copy(up.Str, "bridge", "displayname_template") helper.Copy(up.Str, "bridge", "displayname_template")
helper.Copy(up.Bool, "bridge", "personal_filtering_spaces") helper.Copy(up.Bool, "bridge", "personal_filtering_spaces")
helper.Copy(up.Bool, "bridge", "delivery_receipts") 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.Int, "bridge", "portal_message_buffer")
helper.Copy(up.Bool, "bridge", "call_start_notices") helper.Copy(up.Bool, "bridge", "call_start_notices")
helper.Copy(up.Bool, "bridge", "identity_change_notices") helper.Copy(up.Bool, "bridge", "identity_change_notices")

View file

@ -100,6 +100,10 @@ bridge:
personal_filtering_spaces: false personal_filtering_spaces: false
# Should the bridge send a read receipt from the bridge bot when a message has been sent to WhatsApp? # Should the bridge send a read receipt from the bridge bot when a message has been sent to WhatsApp?
delivery_receipts: false 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? # Should incoming calls send a message to the Matrix room?
call_start_notices: true call_start_notices: true
# Should another user's cryptographic identity changing send a message to Matrix? # Should another user's cryptographic identity changing send a message to Matrix?

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.20220530212627-b15517460fdb maunium.net/go/mautrix v0.11.1-0.20220531132903-5853dab58019
) )
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.20220530212627-b15517460fdb h1:MTY4bW0yhg8bHszMSNymTrHvIRdiDjcnQKC8qSbt5BE= maunium.net/go/mautrix v0.11.1-0.20220531132903-5853dab58019 h1:jrucVlf+iLAXB2WvOERaDhO0T3sBizJEG7Tsmkz6GY0=
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/go.mod h1:CiKpMhAx5QZFHK03jpWb0iKI3sGU8x6+LfsOjDrcO8I=

View file

@ -271,6 +271,8 @@ func main() {
Version: "0.4.0", Version: "0.4.0",
ProtocolName: "WhatsApp", ProtocolName: "WhatsApp",
CryptoPickleKey: "maunium.net/go/mautrix-whatsapp",
ConfigUpgrader: &configupgrade.StructUpgrader{ ConfigUpgrader: &configupgrade.StructUpgrader{
SimpleUpgrader: configupgrade.SimpleUpgrader(config.DoUpgrade), SimpleUpgrader: configupgrade.SimpleUpgrader(config.DoUpgrade),
Blocks: config.SpacedBlocks, Blocks: config.SpacedBlocks,

309
portal.go
View file

@ -1207,6 +1207,10 @@ func (portal *Portal) RestrictMetadataChanges(restrict bool) id.EventID {
return "" return ""
} }
func (portal *Portal) getBridgeInfoStateKey() string {
return fmt.Sprintf("net.maunium.whatsapp://whatsapp/%s", portal.Key.JID)
}
func (portal *Portal) getBridgeInfo() (string, event.BridgeEventContent) { func (portal *Portal) getBridgeInfo() (string, event.BridgeEventContent) {
bridgeInfo := event.BridgeEventContent{ bridgeInfo := event.BridgeEventContent{
BridgeBot: portal.bridge.Bot.UserID, BridgeBot: portal.bridge.Bot.UserID,
@ -1223,8 +1227,7 @@ func (portal *Portal) getBridgeInfo() (string, event.BridgeEventContent) {
AvatarURL: portal.AvatarURL.CUString(), AvatarURL: portal.AvatarURL.CUString(),
}, },
} }
bridgeInfoStateKey := fmt.Sprintf("net.maunium.whatsapp://whatsapp/%s", portal.Key.JID) return portal.getBridgeInfoStateKey(), bridgeInfo
return bridgeInfoStateKey, bridgeInfo
} }
func (portal *Portal) UpdateBridgeInfo() { func (portal *Portal) UpdateBridgeInfo() {
@ -2546,7 +2549,28 @@ func (portal *Portal) convertWebPtoPNG(webpImage []byte) ([]byte, error) {
return pngBuffer.Bytes(), nil 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 caption string
var mentionedJIDs []string var mentionedJIDs []string
if relaybotFormatted { if relaybotFormatted {
@ -2561,19 +2585,16 @@ func (portal *Portal) preprocessMatrixMedia(sender *User, relaybotFormatted bool
} }
mxc, err := rawMXC.Parse() mxc, err := rawMXC.Parse()
if err != nil { if err != nil {
portal.log.Errorln("Malformed content URL in %s: %v", eventID, err) return nil, err
return nil
} }
data, err := portal.MainIntent().DownloadBytes(mxc) data, err := portal.MainIntent().DownloadBytes(mxc)
if err != nil { if err != nil {
portal.log.Errorfln("Failed to download media in %s: %v", eventID, err) return nil, NewDualError(errMediaDownloadFailed, err)
return nil
} }
if file != nil { if file != nil {
err = file.DecryptInPlace(data) err = file.DecryptInPlace(data)
if err != nil { if err != nil {
portal.log.Errorfln("Failed to decrypt media in %s: %v", eventID, err) return nil, NewDualError(errMediaDecryptFailed, err)
return nil
} }
} }
if mediaType == whatsmeow.MediaVideo && content.GetInfo().MimeType == "image/gif" { 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'", "-filter:v", "crop='floor(in_w/2)*2:floor(in_h/2)*2'",
}, content.GetInfo().MimeType) }, content.GetInfo().MimeType)
if err != nil { if err != nil {
portal.log.Errorfln("Failed to convert gif to mp4 in %s: %v", eventID, err) return nil, NewDualError(fmt.Errorf("%w (gif to mp4)", errMediaConvertFailed), err)
return nil
} }
content.Info.MimeType = "video/mp4" content.Info.MimeType = "video/mp4"
} }
if mediaType == whatsmeow.MediaImage && content.GetInfo().MimeType == "image/webp" { if mediaType == whatsmeow.MediaImage && content.GetInfo().MimeType == "image/webp" {
data, err = portal.convertWebPtoPNG(data) data, err = portal.convertWebPtoPNG(data)
if err != nil { if err != nil {
portal.log.Errorfln("Failed to convert webp to png in %s: %v", eventID, err) return nil, NewDualError(fmt.Errorf("%w (webp to png)", errMediaConvertFailed), err)
return nil
} }
content.Info.MimeType = "image/png" content.Info.MimeType = "image/png"
} }
uploadResp, err := sender.Client.Upload(context.Background(), data, mediaType) uploadResp, err := sender.Client.Upload(context.Background(), data, mediaType)
if err != nil { if err != nil {
portal.log.Errorfln("Failed to upload media in %s: %v", eventID, err) return nil, NewDualError(errMediaWhatsAppUploadFailed, err)
return nil
} }
// Audio doesn't have thumbnails // 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) thumbnail, err = portal.downloadThumbnail(data, content.GetInfo().ThumbnailURL, eventID)
// Ignore format errors for non-image files, we don't care about those thumbnails // 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) { 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, MentionedJIDs: mentionedJIDs,
Thumbnail: thumbnail, Thumbnail: thumbnail,
FileLength: len(data), FileLength: len(data),
} }, nil
} }
type MediaUpload struct { type MediaUpload struct {
@ -2695,11 +2713,10 @@ func getUnstableWaveform(content map[string]interface{}) []byte {
return output 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) content, ok := evt.Content.Parsed.(*event.MessageEventContent)
if !ok { if !ok {
portal.log.Debugfln("Failed to handle event %s: unexpected parsed content type %T", evt.ID, evt.Content.Parsed) return nil, sender, fmt.Errorf("%w %T", errUnexpectedParsedContentType, evt.Content.Parsed)
return nil, sender
} }
var msg waProto.Message var msg waProto.Message
@ -2724,8 +2741,7 @@ func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waP
relaybotFormatted := false relaybotFormatted := false
if !sender.IsLoggedIn() || (portal.IsPrivateChat() && sender.JID.User != portal.Key.Receiver.User) { if !sender.IsLoggedIn() || (portal.IsPrivateChat() && sender.JID.User != portal.Key.Receiver.User) {
if !portal.HasRelaybot() { if !portal.HasRelaybot() {
portal.log.Warnln("Ignoring message from", sender.MXID, "in chat with no relaybot (convertMatrixMessage)") return nil, sender, errUserNotLoggedIn
return nil, sender
} }
relaybotFormatted = portal.addRelaybotFormat(sender, content) relaybotFormatted = portal.addRelaybotFormat(sender, content)
sender = portal.GetRelayUser() sender = portal.GetRelayUser()
@ -2741,7 +2757,7 @@ func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waP
case event.MsgText, event.MsgEmote, event.MsgNotice: case event.MsgText, event.MsgEmote, event.MsgNotice:
text := content.Body text := content.Body
if content.MsgType == event.MsgNotice && !portal.bridge.Config.Bridge.BridgeNotices { if content.MsgType == event.MsgNotice && !portal.bridge.Config.Bridge.BridgeNotices {
return nil, sender return nil, sender, errMNoticeDisabled
} }
if content.Format == event.FormatHTML { if content.Format == event.FormatHTML {
text, ctxInfo.MentionedJid = portal.bridge.Formatter.ParseMatrix(content.FormattedBody) 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 msg.Conversation = &text
} }
case event.MsgImage: 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 { if media == nil {
return nil, sender return nil, sender, err
} }
ctxInfo.MentionedJid = media.MentionedJIDs ctxInfo.MentionedJid = media.MentionedJIDs
msg.ImageMessage = &waProto.ImageMessage{ msg.ImageMessage = &waProto.ImageMessage{
@ -2778,9 +2794,9 @@ func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waP
} }
case event.MsgVideo: case event.MsgVideo:
gifPlayback := content.GetInfo().MimeType == "image/gif" 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 { if media == nil {
return nil, sender return nil, sender, err
} }
duration := uint32(content.GetInfo().Duration / 1000) duration := uint32(content.GetInfo().Duration / 1000)
ctxInfo.MentionedJid = media.MentionedJIDs ctxInfo.MentionedJid = media.MentionedJIDs
@ -2798,9 +2814,9 @@ func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waP
FileLength: proto.Uint64(uint64(media.FileLength)), FileLength: proto.Uint64(uint64(media.FileLength)),
} }
case event.MsgAudio: 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 { if media == nil {
return nil, sender return nil, sender, err
} }
duration := uint32(content.GetInfo().Duration / 1000) duration := uint32(content.GetInfo().Duration / 1000)
msg.AudioMessage = &waProto.AudioMessage{ 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")) msg.AudioMessage.Mimetype = proto.String(addCodecToMime(content.GetInfo().MimeType, "opus"))
} }
case event.MsgFile: 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 { if media == nil {
return nil, sender return nil, sender, err
} }
msg.DocumentMessage = &waProto.DocumentMessage{ msg.DocumentMessage = &waProto.DocumentMessage{
ContextInfo: &ctxInfo, ContextInfo: &ctxInfo,
@ -2840,8 +2856,7 @@ func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waP
case event.MsgLocation: case event.MsgLocation:
lat, long, err := parseGeoURI(content.GeoURI) lat, long, err := parseGeoURI(content.GeoURI)
if err != nil { if err != nil {
portal.log.Debugfln("Invalid geo URI on Matrix event %s: %v", evt.ID, err) return nil, sender, fmt.Errorf("%w: %v", errInvalidGeoURI, err)
return nil, sender
} }
msg.LocationMessage = &waProto.LocationMessage{ msg.LocationMessage = &waProto.LocationMessage{
DegreesLatitude: &lat, DegreesLatitude: &lat,
@ -2850,13 +2865,15 @@ func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waP
ContextInfo: &ctxInfo, ContextInfo: &ctxInfo,
} }
default: default:
portal.log.Debugfln("Unhandled Matrix event %s: unknown msgtype %s", evt.ID, content.MsgType) return nil, sender, fmt.Errorf("%w %q", errUnknownMsgType, content.MsgType)
return nil, sender
} }
return &msg, sender return &msg, sender, nil
} }
func (portal *Portal) sendErrorMessage(message string, confirmed bool) id.EventID { func (portal *Portal) sendErrorMessage(message string, confirmed bool) id.EventID {
if !portal.bridge.Config.Bridge.MessageErrorNotices {
return ""
}
certainty := "may not have been" certainty := "may not have been"
if confirmed { if confirmed {
certainty = "was not" certainty = "was not"
@ -2872,6 +2889,87 @@ func (portal *Portal) sendErrorMessage(message string, confirmed bool) id.EventI
return resp.EventID 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) { func (portal *Portal) sendDeliveryReceipt(eventID id.EventID) {
if portal.bridge.Config.Bridge.DeliveryReceipts { if portal.bridge.Config.Bridge.DeliveryReceipts {
err := portal.bridge.Bot.MarkRead(portal.MXID, eventID) 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) { 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 return
} }
portal.log.Debugfln("Received event %s from %s", evt.ID, evt.Sender) portal.log.Debugfln("Received message %s from %s", evt.ID, evt.Sender)
msg, sender := portal.convertMatrixMessage(sender, evt) msg, sender, err := portal.convertMatrixMessage(sender, evt)
if msg == nil { if msg == nil {
go portal.sendMessageMetrics(evt, err, "Error converting")
return return
} }
portal.MarkDisappearing(evt.ID, portal.ExpirationTime, true) 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) 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) portal.log.Debugln("Sending event", evt.ID, "to WhatsApp", info.ID)
ts, err := sender.Client.SendMessage(portal.Key.JID, info.ID, msg) ts, err := sender.Client.SendMessage(portal.Key.JID, info.ID, msg)
if err != nil { go portal.sendMessageMetrics(evt, err, "Error sending")
portal.log.Errorfln("Error sending message: %v", err) if err == nil {
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)
dbMsg.MarkSent(ts) dbMsg.MarkSent(ts)
} }
} }
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 { if err := portal.canBridgeFrom(sender, false); err != nil {
go portal.sendMessageMetrics(evt, err, "Ignoring")
return 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) 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 { go portal.sendMessageMetrics(evt, err, "Error sending")
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)
}
} }
func (portal *Portal) handleMatrixReaction(sender *User, evt *event.Event) error { 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.upsertReaction(nil, target.JID, sender.JID, evt.ID, info.ID)
portal.log.Debugln("Sending reaction", evt.ID, "to WhatsApp", 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) ts, err := portal.sendReactionToWhatsApp(sender, info.ID, target, content.RelatesTo.Key, evt.Timestamp)
if err != nil { if err == nil {
dbMsg.MarkSent(ts) dbMsg.MarkSent(ts)
} }
return err return err
@ -3022,7 +3143,8 @@ func (portal *Portal) upsertReaction(intent *appservice.IntentAPI, targetJID typ
} }
func (portal *Portal) HandleMatrixRedaction(sender *User, evt *event.Event) { 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 return
} }
portal.log.Debugfln("Received redaction %s from %s", evt.ID, evt.Sender) 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) msg := portal.bridge.DB.Message.GetByMXID(evt.Redacts)
if msg == nil { if msg == nil {
portal.log.Debugfln("Ignoring redaction %s of unknown event by %s", evt.ID, senderLogIdentifier) go portal.sendMessageMetrics(evt, errTargetNotFound, "Ignoring")
portal.bridge.SendMessageErrorCheckpoint(evt, bridge.MsgStepRemote, errors.New("target not found"), true, 0)
return
} else if msg.IsFakeJID() { } else if msg.IsFakeJID() {
portal.log.Debugfln("Ignoring redaction %s of fake event by %s", evt.ID, senderLogIdentifier) go portal.sendMessageMetrics(evt, errTargetIsFake, "Ignoring")
portal.bridge.SendMessageErrorCheckpoint(evt, bridge.MsgStepRemote, errors.New("target is a fake event"), true, 0)
return
} else if msg.Sender.User != sender.JID.User { } 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) go portal.sendMessageMetrics(evt, errTargetSentBySomeoneElse, "Ignoring")
portal.bridge.SendMessageErrorCheckpoint(evt, bridge.MsgStepRemote, errors.New("message was sent by someone else"), true, 0) } else if msg.Type == database.MsgReaction {
return
}
var err error
if msg.Type == database.MsgReaction {
if reaction := portal.bridge.DB.Reaction.GetByMXID(evt.Redacts); reaction == nil { 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) go portal.sendMessageMetrics(evt, errReactionDatabaseNotFound, "Ignoring")
portal.bridge.SendMessageErrorCheckpoint(evt, bridge.MsgStepRemote, errors.New("reaction database entry not found"), true, 0)
return
} else if reactionTarget := reaction.GetTarget(); reactionTarget == nil { } else if reactionTarget := reaction.GetTarget(); reactionTarget == nil {
portal.log.Debugfln("Ignoring redaction of reaction %s: reaction target message not found", evt.ID) go portal.sendMessageMetrics(evt, errReactionTargetNotFound, "Ignoring")
portal.bridge.SendMessageErrorCheckpoint(evt, bridge.MsgStepRemote, errors.New("reaction target message not found"), true, 0)
return
} else { } else {
portal.log.Debugfln("Sending redaction reaction %s of %s/%s to WhatsApp", evt.ID, msg.MXID, msg.JID) 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 { } else {
portal.log.Debugfln("Sending redaction %s of %s/%s to WhatsApp", evt.ID, msg.MXID, msg.JID) 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) _, err := sender.Client.RevokeMessage(portal.Key.JID, msg.JID)
} go portal.sendMessageMetrics(evt, err, "Error sending")
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)
} }
} }
@ -3192,27 +3295,19 @@ func (portal *Portal) HandleMatrixTyping(newTyping []id.UserID) {
portal.setTyping(stoppedTyping, types.ChatPresencePaused) 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 !sender.IsLoggedIn() {
if portal.HasRelaybot() { if allowRelay && portal.HasRelaybot() {
return true return nil
} else if sender.Session != nil { } else if sender.Session != nil {
portal.log.Debugfln("Ignoring %s from %s as user is not connected", evtType, sender.MXID) return errUserNotConnected
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)
}
} else { } 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 && (!allowRelay || !portal.HasRelaybot()) {
} else if portal.IsPrivateChat() && sender.JID.User != portal.Key.Receiver.User && !portal.HasRelaybot() { return errDifferentUser
portal.log.Debugfln("Ignoring %s from different user %s/%s in private chat with no relay user", evtType, sender.MXID, sender.JID)
return false
} }
return true return nil
} }
func (portal *Portal) Delete() { func (portal *Portal) Delete() {