Эх сурвалжийг харах

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.
Tulir Asokan 2 жил өмнө
parent
commit
1105530c9a
8 өөрчлөгдсөн 96 нэмэгдсэн , 15 устгасан
  1. 15 0
      CHANGELOG.md
  2. 1 0
      config/bridge.go
  3. 1 0
      config/upgrade.go
  4. 1 0
      database/message.go
  5. 3 0
      example-config.yaml
  6. 2 2
      go.mod
  7. 4 4
      go.sum
  8. 69 9
      portal.go

+ 15 - 0
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.

+ 1 - 0
config/bridge.go

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

+ 1 - 0
config/upgrade.go

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

+ 1 - 0
database/message.go

@@ -138,6 +138,7 @@ const (
 	MsgFake     MessageType = "fake"
 	MsgNormal   MessageType = "message"
 	MsgReaction MessageType = "reaction"
+	MsgEdit     MessageType = "edit"
 )
 
 type Message struct {

+ 3 - 0
example-config.yaml

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

+ 2 - 2
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 (

+ 4 - 4
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=

+ 69 - 9
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
 	}