mirror of
https://github.com/tulir/mautrix-whatsapp
synced 2024-12-15 01:43:49 +01:00
parent
df99750636
commit
7f27c76659
7 changed files with 215 additions and 110 deletions
|
@ -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"`
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
2
go.mod
|
@ -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
4
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/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=
|
||||||
|
|
2
main.go
2
main.go
|
@ -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
309
portal.go
|
@ -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() {
|
||||||
|
|
Loading…
Reference in a new issue