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"`
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"`

View file

@ -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")

View file

@ -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?

2
go.mod
View file

@ -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 (

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/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=

View file

@ -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,

309
portal.go
View file

@ -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() {