alphapapa / ement.el

A Matrix client for GNU Emacs
GNU General Public License v3.0
508 stars 45 forks source link

Replies without replied-to content in reply body #57

Open 9viz opened 2 years ago

9viz commented 2 years ago

Some clients don't have the replied to message in body or format_body fields. So one has to rely on the event_id cited in m.relates_to->m.in_reply_to. Example of such an event is (I stripped out the id),

((:id . "ID")
 (:sender . "@_discord_296842705454235650:t2bot.io")
 (:content
  (body . "sheesh u passed out at 4pm?")
  (format . "org.matrix.custom.html")
  (formatted_body . "sheesh u passed out at 4pm?")
  (m\.relates_to
   (m\.in_reply_to
    (event_id . "$TluJm8weVHXI3gwsD-kSeI1HM3UOGIzqe8b-ybmdyb4")))
  (msgtype . "m.text"))
 (:origin-server-ts . 1648191029121)
 (:type . "m.room.message")
 (:state-key)
 (:unsigned))

This is from the t2bot bridging a discord channel and a matrix room.

I can take a stab at implementing this on May.

alphapapa commented 2 years ago

Hm, I don't know about this. The spec, in section 13.2.2.6.1 Rich replies, says that the reply event's body is supposed to include the fallback content, i.e. the quoted text. That would seem to suggest that the bot (or any other such client) is not behaving according to spec. In that case, I'd rather the misbehaving client be fixed than write workarounds in this client for bugs in other clients (to a reasonable extent, anyway--there will always be some bugs in other clients, and some workarounds will be needed, but I'd like to keep them to a minimum).

alphapapa commented 2 years ago

I don't have the link handy, but a recent MSC proposal I saw would require not including the replied-to content in the reply body (in order to avoid leaks of message content to users who joined a room after a replied-to message was sent). So this may become the standard in the future.

9viz commented 2 years ago

FYI, in a couple of hours, I came up with the following super-janky patch that supports rich replies when the room uses formatted body. It hasn't gotten a lot of testing though, and most of the meat is from m.image. I followed the following two parts of the matrix spec to implement it,

diff --git a/ement-room.el b/ement-room.el
index 1c7666e..24daef8 100644
--- a/ement-room.el
+++ b/ement-room.el
@@ -3181,22 +3181,29 @@ Format defaults to `ement-room-message-format-spec', which see."
 If FORMATTED-P, return the formatted body content, when available."
   (pcase-let* (((cl-struct ement-event content
                            (unsigned (map ('redacted_by unsigned-redacted-by)))
-                           (local (map ('redacted-by local-redacted-by))))
+                           (local (map ('redacted-by local-redacted-by)))
+                           (local (map ('reply reply-event))))
                 event)
                ((map ('body main-body) msgtype ('format content-format) ('formatted_body formatted-body)
                      ('m.relates_to (map ('rel_type rel-type)))
+                     ('m.relates_to (map ('m.in_reply_to (map ('event_id reply-event-id)))))
                      ('m.new_content (map ('body new-body) ('formatted_body new-formatted-body)
                                           ('format new-content-format))))
                 content)
                (body (or new-body main-body))
                (formatted-body (or new-formatted-body formatted-body))
+               (reply-in-body-p t)
                (body (if (or (not formatted-p) (not formatted-body))
                          ;; Copy the string so as not to add face properties to the one in the struct.
                          (copy-sequence body)
                        (pcase (or new-content-format content-format)
                          ("org.matrix.custom.html"
                           (save-match-data
-                            (ement-room--render-html formatted-body)))
+                            (setq reply-in-body-p (string-match-p "<mx-reply>" formatted-body))
+                            (ement-room--render-html
+                             (if (and reply-event (null reply-in-body-p))
+                                 (ement-room--rich-reply-text ement-room reply-event formatted-body)
+                               formatted-body))))
                          (_ (format "[unknown body format: %s] %s"
                                     (or new-content-format content-format) body)))))
                (appendix (pcase msgtype
@@ -3206,10 +3213,25 @@ If FORMATTED-P, return the formatted body content, when available."
                            ("m.file" (ement-room--format-m.file event))
                            (_ (if (or local-redacted-by unsigned-redacted-by)
                                   nil
-                                (format "[unsupported msgtype: %s]" msgtype ))))))
+                                (format "[unsupported msgtype: %s]" msgtype))))))
     (when body
       ;; HACK: Once I got an error when body was nil, so let's avoid that.
       (setf body (ement-room--linkify-urls body)))
+    (when (and reply-event-id
+               (not reply-in-body-p)
+               (not reply-event))
+      ;; During initial sync, `ement-ewoc' maybe nil.
+      (if-let ((node (and ement-ewoc
+                          (ement-room--ewoc-last-matching ement-ewoc
+                            (lambda (data)
+                              (and (ement-event-p data)
+                                   (equal (ement-event-id data) reply-event-id)))))))
+          (progn
+            (message "ement: using old event for reply")
+            (setf (map-elt (ement-event-local event) 'reply) (ewoc-data node)
+                  body (ement-room--rich-reply-text ement-room (ewoc-data node) body)))
+        (ement-api ement-session (format "rooms/%s/event/%s" (ement-room-id ement-room) reply-event-id)
+          :then (apply-partially #'ement-room--rich-reply-callback ement-room event))))
     ;; HACK: Ensure body isn't nil (e.g. redacted messages can have empty bodies).
     (unless body
       (setf body (copy-sequence
@@ -3228,6 +3250,35 @@ If FORMATTED-P, return the formatted body content, when available."
       (setf body (concat body " " (propertize "[edited]" 'face 'font-lock-comment-face))))
     body))

+(defun ement-room--rich-reply-callback (room event reply-event)
+  (pcase-let* (((cl-struct ement-room (local (map buffer))) room))
+    (setf (map-elt (ement-event-local event) 'reply) (ement--make-event reply-event))
+    (when (buffer-live-p buffer)
+      (with-current-buffer buffer
+        (when-let ((node (ement-room--ewoc-last-matching ement-ewoc
+                           (lambda (data) (eq data event)))))
+          (ewoc-invalidate ement-ewoc node))))))
+
+(defun ement-room--rich-reply-text (room reply-event body)
+  (format
+   "<mx-reply><blockquote>
+    <a href=\"https://matrix.to/#/%s/%s\">In reply to</a>
+    <a href=\"https://matrix.to/#/%s\">%s</a>
+    <br />
+%s
+  </blockquote></mx-reply>
+%s"
+   (ement-room-id ement-room)
+   (ement-event-id reply-event)
+   (ement-user-id (ement-event-sender reply-event))
+   (or (ement-user-displayname (ement-event-sender reply-event))
+       (ement-user-id (ement-event-sender reply-event)))
+   (let ((content (ement-event-content reply-event)))
+     (if (equal (map-elt content 'format) "org.matrix.custom.html")
+         (map-elt content 'formatted_body)
+       (map-elt content 'body)))
+   body))
+
 (defun ement-room--render-html (string)
   "Return rendered version of HTML STRING.
 HTML is rendered to Emacs text using `shr-insert-document'."
alphapapa commented 2 years ago

@vizs Looks promising. It would be easier to review if it were a PR. :)

9viz commented 2 years ago

I will do that once I finish up the support for plain text bodies as well. I posted it here in case if it will be of help to Someone(TM).

alphapapa commented 5 months ago

For future reference, in case this MSC is merged: https://github.com/matrix-org/matrix-spec-proposals/pull/2781