forked from MirrorHub/mautrix-whatsapp
Add initial support for WhatsApp message edits
Sending will be disabled by default until official WhatsApp clients start rendering edits. The implementation may also be incorrect.
This commit is contained in:
parent
859355a3db
commit
1105530c9a
8 changed files with 96 additions and 15 deletions
15
CHANGELOG.md
15
CHANGELOG.md
|
@ -1,3 +1,18 @@
|
|||
# v0.7.1 (unreleased)
|
||||
|
||||
* Added support for wa.me/qr links in `!wa resolve-link`.
|
||||
* Added option to sync group members in parallel to speed up syncing large
|
||||
groups.
|
||||
* Added initial support for WhatsApp message editing.
|
||||
* Sending edits will be disabled by default until official WhatsApp clients
|
||||
start rendering edits.
|
||||
* Changed `private_chat_portal_meta` config option to be implicitly enabled in
|
||||
encrypted rooms, matching the behavior of other mautrix bridges.
|
||||
* Updated media bridging to check homeserver media size limit before
|
||||
downloading media to avoid running out of memory.
|
||||
* The bridge may still run out of ram when bridging files if your homeserver
|
||||
has a large media size limit and a low bridge memory limit.
|
||||
|
||||
# v0.7.0 (2022-09-16)
|
||||
|
||||
* Bumped minimum Go version to 1.18.
|
||||
|
|
|
@ -110,6 +110,7 @@ type BridgeConfig struct {
|
|||
FederateRooms bool `yaml:"federate_rooms"`
|
||||
URLPreviews bool `yaml:"url_previews"`
|
||||
CaptionInMessage bool `yaml:"caption_in_message"`
|
||||
SendWhatsAppEdits bool `yaml:"send_whatsapp_edits"`
|
||||
|
||||
MessageHandlingTimeout struct {
|
||||
ErrorAfterStr string `yaml:"error_after"`
|
||||
|
|
|
@ -93,6 +93,7 @@ func DoUpgrade(helper *up.Helper) {
|
|||
helper.Copy(up.Bool, "bridge", "crash_on_stream_replaced")
|
||||
helper.Copy(up.Bool, "bridge", "url_previews")
|
||||
helper.Copy(up.Bool, "bridge", "caption_in_message")
|
||||
helper.Copy(up.Bool, "bridge", "send_whatsapp_edits")
|
||||
helper.Copy(up.Str|up.Null, "bridge", "message_handling_timeout", "error_after")
|
||||
helper.Copy(up.Str|up.Null, "bridge", "message_handling_timeout", "deadline")
|
||||
|
||||
|
|
|
@ -138,6 +138,7 @@ const (
|
|||
MsgFake MessageType = "fake"
|
||||
MsgNormal MessageType = "message"
|
||||
MsgReaction MessageType = "reaction"
|
||||
MsgEdit MessageType = "edit"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
|
|
|
@ -287,6 +287,9 @@ bridge:
|
|||
# Send captions in the same message as images. This will send data compatible with both MSC2530 and MSC3552.
|
||||
# This is currently not supported in most clients.
|
||||
caption_in_message: false
|
||||
# Should Matrix edits be bridged to WhatsApp edits?
|
||||
# Official WhatsApp clients don't render edits yet, but once they do, the bridge should work with them right away.
|
||||
send_whatsapp_edits: false
|
||||
# Maximum time for handling Matrix events. Duration strings formatted for https://pkg.go.dev/time#ParseDuration
|
||||
# Null means there's no enforced timeout.
|
||||
message_handling_timeout:
|
||||
|
|
4
go.mod
4
go.mod
|
@ -11,12 +11,12 @@ require (
|
|||
github.com/prometheus/client_golang v1.13.0
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/tidwall/gjson v1.14.3
|
||||
go.mau.fi/whatsmeow v0.0.0-20220928114434-ebe489ef67ef
|
||||
go.mau.fi/whatsmeow v0.0.0-20221008133908-7f01b3072802
|
||||
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539
|
||||
golang.org/x/net v0.0.0-20220812174116-3211cb980234
|
||||
google.golang.org/protobuf v1.28.1
|
||||
maunium.net/go/maulogger/v2 v2.3.2
|
||||
maunium.net/go/mautrix v0.12.2-0.20221003070712-77198cd4cd57
|
||||
maunium.net/go/mautrix v0.12.2-0.20221008135414-78f80c20b158
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
8
go.sum
8
go.sum
|
@ -63,8 +63,8 @@ github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
|
|||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.mau.fi/libsignal v0.0.0-20220628090436-4d18b66b087e h1:ByHDg+D+dMIGuBA2n+1xOUf4xr3FJFYg8yxl06s1YBE=
|
||||
go.mau.fi/libsignal v0.0.0-20220628090436-4d18b66b087e/go.mod h1:RCdzkTWSJv0AKGqurzPXJsEGIVMuQps3E/h7CMUPous=
|
||||
go.mau.fi/whatsmeow v0.0.0-20220928114434-ebe489ef67ef h1:32Ki56jfx+tg8B8Qla/przLXJchD4Y2NtlggA1oG+cs=
|
||||
go.mau.fi/whatsmeow v0.0.0-20220928114434-ebe489ef67ef/go.mod h1:hsjqq2xLuoFew8vbsDCJcGf5EbXCRcR/yoQ+87w6m3k=
|
||||
go.mau.fi/whatsmeow v0.0.0-20221008133908-7f01b3072802 h1:dD9WVoIhSWoIu1qlM/LhsbJDBknq8K98LcKJQ2UbQeg=
|
||||
go.mau.fi/whatsmeow v0.0.0-20221008133908-7f01b3072802/go.mod h1:hsjqq2xLuoFew8vbsDCJcGf5EbXCRcR/yoQ+87w6m3k=
|
||||
golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 h1:GIAS/yBem/gq2MUqgNIzUHW7cJMmx3TGZOrnyYaNQ6c=
|
||||
golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 h1:/eM0PCrQI2xd471rI+snWuu251/+/jpBpZqir2mPdnU=
|
||||
|
@ -100,5 +100,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.12.2-0.20221003070712-77198cd4cd57 h1:AXpCOSBuF61ETOTKz+295CIpZYhIlmOBHu7XeuETHRU=
|
||||
maunium.net/go/mautrix v0.12.2-0.20221003070712-77198cd4cd57/go.mod h1:/jxQFIipObSsjZPH6o3xyUi8uoULz3Hfr/8p9loqpYE=
|
||||
maunium.net/go/mautrix v0.12.2-0.20221008135414-78f80c20b158 h1:Q56l5MDNzcmL5E0+wsGRKyjFlgSTQ73JeTYQ2LdZ8FY=
|
||||
maunium.net/go/mautrix v0.12.2-0.20221008135414-78f80c20b158/go.mod h1:/jxQFIipObSsjZPH6o3xyUi8uoULz3Hfr/8p9loqpYE=
|
||||
|
|
78
portal.go
78
portal.go
|
@ -422,6 +422,8 @@ func getMessageType(waMsg *waProto.Message) string {
|
|||
return "ignore"
|
||||
}
|
||||
return "revoke"
|
||||
case waProto.ProtocolMessage_MESSAGE_EDIT:
|
||||
return "edit"
|
||||
case waProto.ProtocolMessage_EPHEMERAL_SETTING:
|
||||
return "disappearing timer change"
|
||||
case waProto.ProtocolMessage_APP_STATE_SYNC_KEY_SHARE, waProto.ProtocolMessage_HISTORY_SYNC_NOTIFICATION, waProto.ProtocolMessage_INITIAL_SECURITY_NOTIFICATION_SETTING_SYNC:
|
||||
|
@ -703,6 +705,22 @@ func (portal *Portal) handleMessage(source *User, evt *events.Message) {
|
|||
return
|
||||
}
|
||||
}
|
||||
var editTargetMsg *database.Message
|
||||
if msgType == "edit" {
|
||||
editTargetID := evt.Message.GetProtocolMessage().GetKey().GetId()
|
||||
editTargetMsg = portal.bridge.DB.Message.GetByJID(portal.Key, editTargetID)
|
||||
if editTargetMsg == nil {
|
||||
portal.log.Warnfln("Not handling %s: couldn't find edit target %s", msgID, editTargetID)
|
||||
return
|
||||
} else if editTargetMsg.Type != database.MsgNormal {
|
||||
portal.log.Warnfln("Not handling %s: edit target %s is not a normal message (it's %s)", msgID, editTargetID, editTargetMsg.Type)
|
||||
return
|
||||
} else if editTargetMsg.Sender.User != evt.Info.Sender.User {
|
||||
portal.log.Warnfln("Not handling %s: edit target %s was sent by %s, not %s", msgID, editTargetID, editTargetMsg.Sender.User, evt.Info.Sender.User)
|
||||
return
|
||||
}
|
||||
evt.Message = evt.Message.GetProtocolMessage().GetEditedMessage()
|
||||
}
|
||||
|
||||
intent := portal.getMessageIntent(source, &evt.Info)
|
||||
if intent == nil {
|
||||
|
@ -730,16 +748,23 @@ func (portal *Portal) handleMessage(source *User, evt *events.Message) {
|
|||
} else if converted.ReplyTo != nil {
|
||||
portal.SetReply(converted.Content, converted.ReplyTo, false)
|
||||
}
|
||||
dbMsgType := database.MsgNormal
|
||||
if editTargetMsg != nil {
|
||||
dbMsgType = database.MsgEdit
|
||||
converted.Content.SetEdit(editTargetMsg.MXID)
|
||||
}
|
||||
resp, err := portal.sendMessage(converted.Intent, converted.Type, converted.Content, converted.Extra, evt.Info.Timestamp.UnixMilli())
|
||||
if err != nil {
|
||||
portal.log.Errorfln("Failed to send %s to Matrix: %v", msgID, err)
|
||||
} else {
|
||||
portal.MarkDisappearing(resp.EventID, converted.ExpiresIn, false)
|
||||
if editTargetMsg == nil {
|
||||
portal.MarkDisappearing(resp.EventID, converted.ExpiresIn, false)
|
||||
}
|
||||
eventID = resp.EventID
|
||||
lastEventID = eventID
|
||||
}
|
||||
// TODO figure out how to handle captions with undecryptable messages turning decryptable
|
||||
if converted.Caption != nil && existingMsg == nil {
|
||||
if converted.Caption != nil && existingMsg == nil && editTargetMsg == nil {
|
||||
resp, err = portal.sendMessage(converted.Intent, converted.Type, converted.Caption, nil, evt.Info.Timestamp.UnixMilli())
|
||||
if err != nil {
|
||||
portal.log.Errorfln("Failed to send caption of %s to Matrix: %v", msgID, err)
|
||||
|
@ -748,7 +773,7 @@ func (portal *Portal) handleMessage(source *User, evt *events.Message) {
|
|||
lastEventID = resp.EventID
|
||||
}
|
||||
}
|
||||
if converted.MultiEvent != nil && existingMsg == nil {
|
||||
if converted.MultiEvent != nil && existingMsg == nil && editTargetMsg == nil {
|
||||
for index, subEvt := range converted.MultiEvent {
|
||||
resp, err = portal.sendMessage(converted.Intent, converted.Type, subEvt, nil, evt.Info.Timestamp.UnixMilli())
|
||||
if err != nil {
|
||||
|
@ -759,16 +784,17 @@ func (portal *Portal) handleMessage(source *User, evt *events.Message) {
|
|||
}
|
||||
}
|
||||
}
|
||||
if source.MXID == intent.UserID {
|
||||
if source.MXID == intent.UserID && portal.bridge.Config.Homeserver.Software != bridgeconfig.SoftwareHungry {
|
||||
// There are some edge cases (like call notices) where previous messages aren't marked as read
|
||||
// when the user sends a message from another device, so just mark the new message as read to be safe.
|
||||
// Hungryserv does this automatically, so the bridge doesn't need to do it manually.
|
||||
err = intent.SetReadMarkers(portal.MXID, source.makeReadMarkerContent(lastEventID, true))
|
||||
if err != nil {
|
||||
portal.log.Warnfln("Failed to mark own message %s as read by %s: %v", lastEventID, source.MXID, err)
|
||||
}
|
||||
}
|
||||
if len(eventID) != 0 {
|
||||
portal.finishHandling(existingMsg, &evt.Info, eventID, database.MsgNormal, converted.Error)
|
||||
portal.finishHandling(existingMsg, &evt.Info, eventID, dbMsgType, converted.Error)
|
||||
}
|
||||
} else if msgType == "reaction" {
|
||||
portal.HandleMessageReaction(intent, source, &evt.Info, evt.Message.GetReactionMessage(), existingMsg)
|
||||
|
@ -3138,8 +3164,18 @@ func (portal *Portal) convertMatrixMessage(ctx context.Context, sender *User, ev
|
|||
if !ok {
|
||||
return nil, sender, fmt.Errorf("%w %T", errUnexpectedParsedContentType, evt.Content.Parsed)
|
||||
}
|
||||
var editRootMsg *database.Message
|
||||
if editEventID := content.RelatesTo.GetReplaceID(); editEventID != "" && portal.bridge.Config.Bridge.SendWhatsAppEdits {
|
||||
editRootMsg = portal.bridge.DB.Message.GetByMXID(editEventID)
|
||||
if editRootMsg == nil || editRootMsg.Type != database.MsgNormal || editRootMsg.IsFakeJID() || editRootMsg.Sender.User != sender.JID.User {
|
||||
return nil, sender, fmt.Errorf("edit rejected") // TODO more specific error message
|
||||
}
|
||||
if content.NewContent != nil {
|
||||
content = content.NewContent
|
||||
}
|
||||
}
|
||||
|
||||
var msg waProto.Message
|
||||
msg := &waProto.Message{}
|
||||
var ctxInfo waProto.ContextInfo
|
||||
replyToID := content.GetReplyTo()
|
||||
if len(replyToID) > 0 {
|
||||
|
@ -3320,7 +3356,26 @@ func (portal *Portal) convertMatrixMessage(ctx context.Context, sender *User, ev
|
|||
default:
|
||||
return nil, sender, fmt.Errorf("%w %q", errUnknownMsgType, content.MsgType)
|
||||
}
|
||||
return &msg, sender, nil
|
||||
|
||||
if editRootMsg != nil {
|
||||
msg = &waProto.Message{
|
||||
EditedMessage: &waProto.FutureProofMessage{
|
||||
Message: &waProto.Message{
|
||||
ProtocolMessage: &waProto.ProtocolMessage{
|
||||
Key: &waProto.MessageKey{
|
||||
FromMe: proto.Bool(true),
|
||||
Id: proto.String(editRootMsg.JID),
|
||||
RemoteJid: proto.String(portal.Key.JID.String()),
|
||||
},
|
||||
Type: waProto.ProtocolMessage_MESSAGE_EDIT.Enum(),
|
||||
EditedMessage: msg,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return msg, sender, nil
|
||||
}
|
||||
|
||||
func (portal *Portal) generateMessageInfo(sender *User) *types.MessageInfo {
|
||||
|
@ -3405,10 +3460,15 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *event.Event, timing
|
|||
go ms.sendMessageMetrics(evt, err, "Error converting", true)
|
||||
return
|
||||
}
|
||||
portal.MarkDisappearing(origEvtID, portal.ExpirationTime, true)
|
||||
dbMsgType := database.MsgNormal
|
||||
if msg.EditedMessage == nil {
|
||||
portal.MarkDisappearing(origEvtID, portal.ExpirationTime, true)
|
||||
} else {
|
||||
dbMsgType = database.MsgEdit
|
||||
}
|
||||
info := portal.generateMessageInfo(sender)
|
||||
if dbMsg == nil {
|
||||
dbMsg = portal.markHandled(nil, nil, info, evt.ID, false, true, database.MsgNormal, database.MsgNoError)
|
||||
dbMsg = portal.markHandled(nil, nil, info, evt.ID, false, true, dbMsgType, database.MsgNoError)
|
||||
} else {
|
||||
info.ID = dbMsg.JID
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue