From b7b7d043f330ed083a98a2e43d5ddf73b7d99e6e Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Wed, 14 Sep 2022 00:01:16 +0200 Subject: [PATCH] events: Add RoomMessageEventContent::make_reply_to MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … and deprecate reply constructors. --- crates/ruma-common/src/doc/rich_reply.md | 2 +- crates/ruma-common/src/events/room/message.rs | 116 ++++++++++++------ .../src/events/room/message/reply.rs | 96 ++++++--------- .../ruma-common/tests/events/room_message.rs | 12 +- 4 files changed, 125 insertions(+), 101 deletions(-) diff --git a/crates/ruma-common/src/doc/rich_reply.md b/crates/ruma-common/src/doc/rich_reply.md index a47467ad..1097e0c9 100644 --- a/crates/ruma-common/src/doc/rich_reply.md +++ b/crates/ruma-common/src/doc/rich_reply.md @@ -1,5 +1,5 @@ -This constructor requires an [`OriginalRoomMessageEvent`] since it creates a permalink to +This function requires an [`OriginalRoomMessageEvent`] since it creates a permalink to the previous message, for which the room ID is required. If you want to reply to an [`OriginalSyncRoomMessageEvent`], you have to convert it first by calling [`.into_full_event()`][crate::events::OriginalSyncMessageLikeEvent::into_full_event]. diff --git a/crates/ruma-common/src/events/room/message.rs b/crates/ruma-common/src/events/room/message.rs index 0937a314..800c96b3 100644 --- a/crates/ruma-common/src/events/room/message.rs +++ b/crates/ruma-common/src/events/room/message.rs @@ -103,76 +103,116 @@ impl RoomMessageEventContent { Self::new(MessageType::Notice(NoticeMessageEventContent::markdown(body))) } + /// Turns `self` into a reply to the given message. + /// + /// Takes the `body` / `formatted_body` (if any) in `self` for the main text and prepends a + /// quoted version of `original_message`. Also sets the `in_reply_to` field inside `relates_to`. + #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))] + /// + /// # Panics + /// + /// Panics if `self` has a `formatted_body` with a format other than HTML. + #[track_caller] + pub fn make_reply_to(mut self, original_message: &OriginalRoomMessageEvent) -> Self { + let empty_formatted_body = || FormattedBody::html(String::new()); + + let (body, formatted) = { + match &mut self.msgtype { + MessageType::Emote(m) => { + (&mut m.body, Some(m.formatted.get_or_insert_with(empty_formatted_body))) + } + MessageType::Notice(m) => { + (&mut m.body, Some(m.formatted.get_or_insert_with(empty_formatted_body))) + } + MessageType::Text(m) => { + (&mut m.body, Some(m.formatted.get_or_insert_with(empty_formatted_body))) + } + MessageType::Audio(m) => (&mut m.body, None), + MessageType::File(m) => (&mut m.body, None), + MessageType::Image(m) => (&mut m.body, None), + MessageType::Location(m) => (&mut m.body, None), + MessageType::ServerNotice(m) => (&mut m.body, None), + MessageType::Video(m) => (&mut m.body, None), + MessageType::VerificationRequest(m) => (&mut m.body, None), + MessageType::_Custom(m) => (&mut m.body, None), + } + }; + + if let Some(f) = formatted { + assert_eq!( + f.format, + MessageFormat::Html, + "make_reply_to can't handle non-HTML formatted messages" + ); + + let formatted_body = &mut f.body; + + (*body, *formatted_body) = reply::plain_and_formatted_reply_body( + body.as_str(), + (!formatted_body.is_empty()).then(|| formatted_body.as_str()), + original_message, + ); + } + + self.relates_to = Some(Relation::Reply { + in_reply_to: InReplyTo { event_id: original_message.event_id.to_owned() }, + }); + + self + } + /// Creates a plain text reply to a message. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))] + #[deprecated = "\ + use [`Self::text_plain`](#method.text_plain)`(reply).`\ + [`make_reply_to`](#method.make_reply_to)`(original_message)` instead\ + "] pub fn text_reply_plain( reply: impl fmt::Display, original_message: &OriginalRoomMessageEvent, ) -> Self { - let formatted: Option<&str> = None; - let (body, html_body) = - reply::plain_and_formatted_reply_body(reply, formatted, original_message); - - Self { - relates_to: Some(Relation::Reply { - in_reply_to: InReplyTo { event_id: original_message.event_id.to_owned() }, - }), - ..Self::text_html(body, html_body) - } + Self::text_plain(reply.to_string()).make_reply_to(original_message) } /// Creates a html text reply to a message. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))] + #[deprecated = "\ + use [`Self::text_html`](#method.text_html)`(reply, html_reply).`\ + [`make_reply_to`](#method.make_reply_to)`(original_message)` instead\ + "] pub fn text_reply_html( reply: impl fmt::Display, html_reply: impl fmt::Display, original_message: &OriginalRoomMessageEvent, ) -> Self { - let (body, html_body) = - reply::plain_and_formatted_reply_body(reply, Some(html_reply), original_message); - - Self { - relates_to: Some(Relation::Reply { - in_reply_to: InReplyTo { event_id: original_message.event_id.clone() }, - }), - ..Self::text_html(body, html_body) - } + Self::text_html(reply.to_string(), html_reply.to_string()).make_reply_to(original_message) } /// Creates a plain text notice reply to a message. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))] + #[deprecated = "\ + use [`Self::notice_plain`](#method.notice_plain)`(reply).`\ + [`make_reply_to`](#method.make_reply_to)`(original_message)` instead\ + "] pub fn notice_reply_plain( reply: impl fmt::Display, original_message: &OriginalRoomMessageEvent, ) -> Self { - let formatted: Option<&str> = None; - let (body, html_body) = - reply::plain_and_formatted_reply_body(reply, formatted, original_message); - - Self { - relates_to: Some(Relation::Reply { - in_reply_to: InReplyTo { event_id: original_message.event_id.to_owned() }, - }), - ..Self::notice_html(body, html_body) - } + Self::notice_plain(reply.to_string()).make_reply_to(original_message) } /// Creates a html text notice reply to a message. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))] + #[deprecated = "\ + use [`Self::notice_html`](#method.notice_html)`(reply, html_reply).`\ + [`make_reply_to`](#method.make_reply_to)`(original_message)` instead\ + "] pub fn notice_reply_html( reply: impl fmt::Display, html_reply: impl fmt::Display, original_message: &OriginalRoomMessageEvent, ) -> Self { - let (body, html_body) = - reply::plain_and_formatted_reply_body(reply, Some(html_reply), original_message); - - Self { - relates_to: Some(Relation::Reply { - in_reply_to: InReplyTo { event_id: original_message.event_id.clone() }, - }), - ..Self::notice_html(body, html_body) - } + Self::notice_html(reply.to_string(), html_reply.to_string()).make_reply_to(original_message) } /// Create a new reply with the given message and optionally forwards the [`Relation::Thread`]. diff --git a/crates/ruma-common/src/events/room/message/reply.rs b/crates/ruma-common/src/events/room/message/reply.rs index 17832eb1..6cbb6554 100644 --- a/crates/ruma-common/src/events/room/message/reply.rs +++ b/crates/ruma-common/src/events/room/message/reply.rs @@ -8,61 +8,46 @@ use super::{ use super::{sanitize_html, HtmlSanitizerMode, RemoveReplyFallback}; fn get_message_quote_fallbacks(original_message: &OriginalRoomMessageEvent) -> (String, String) { + let get_quotes = |body: &str, formatted: Option<&FormattedBody>, is_emote: bool| { + let OriginalRoomMessageEvent { room_id, event_id, sender, content, .. } = original_message; + let is_reply = matches!(content.relates_to, Some(Relation::Reply { .. })); + let emote_sign = is_emote.then(|| "* ").unwrap_or_default(); + let body = is_reply.then(|| remove_plain_reply_fallback(body)).unwrap_or(body); + #[cfg(feature = "unstable-sanitize")] + let html_body = formatted_or_plain_body(formatted, body, is_reply); + #[cfg(not(feature = "unstable-sanitize"))] + let html_body = formatted_or_plain_body(formatted, body); + + ( + format!("> {emote_sign}<{sender}> {body}").replace('\n', "\n> "), + format!( + "\ +
\ + In reply to \ + {emote_sign}{sender}\ +
\ + {html_body}\ +
\ +
" + ), + ) + }; + match &original_message.content.msgtype { - MessageType::Audio(_) => get_quotes("sent an audio file.", None, original_message, false), - MessageType::Emote(content) => { - get_quotes(&content.body, content.formatted.as_ref(), original_message, true) - } - MessageType::File(_) => get_quotes("sent a file.", None, original_message, false), - MessageType::Image(_) => get_quotes("sent an image.", None, original_message, false), - MessageType::Location(_) => get_quotes("sent a location.", None, original_message, false), - MessageType::Notice(content) => { - get_quotes(&content.body, content.formatted.as_ref(), original_message, false) - } - MessageType::ServerNotice(content) => { - get_quotes(&content.body, None, original_message, false) - } - MessageType::Text(content) => { - get_quotes(&content.body, content.formatted.as_ref(), original_message, false) - } - MessageType::Video(_) => get_quotes("sent a video.", None, original_message, false), - MessageType::_Custom(content) => get_quotes(&content.body, None, original_message, false), - MessageType::VerificationRequest(content) => { - get_quotes(&content.body, None, original_message, false) - } + MessageType::Audio(_) => get_quotes("sent an audio file.", None, false), + MessageType::Emote(c) => get_quotes(&c.body, c.formatted.as_ref(), true), + MessageType::File(_) => get_quotes("sent a file.", None, false), + MessageType::Image(_) => get_quotes("sent an image.", None, false), + MessageType::Location(_) => get_quotes("sent a location.", None, false), + MessageType::Notice(c) => get_quotes(&c.body, c.formatted.as_ref(), false), + MessageType::ServerNotice(c) => get_quotes(&c.body, None, false), + MessageType::Text(c) => get_quotes(&c.body, c.formatted.as_ref(), false), + MessageType::Video(_) => get_quotes("sent a video.", None, false), + MessageType::VerificationRequest(content) => get_quotes(&content.body, None, false), + MessageType::_Custom(content) => get_quotes(&content.body, None, false), } } -fn get_quotes( - body: &str, - formatted: Option<&FormattedBody>, - original_message: &OriginalRoomMessageEvent, - is_emote: bool, -) -> (String, String) { - let OriginalRoomMessageEvent { room_id, event_id, sender, content, .. } = original_message; - let is_reply = matches!(content.relates_to, Some(Relation::Reply { .. })); - let emote_sign = is_emote.then(|| "* ").unwrap_or_default(); - let body = is_reply.then(|| remove_plain_reply_fallback(body)).unwrap_or(body); - #[cfg(feature = "unstable-sanitize")] - let html_body = formatted_or_plain_body(formatted, body, is_reply); - #[cfg(not(feature = "unstable-sanitize"))] - let html_body = formatted_or_plain_body(formatted, body); - - ( - format!("> {emote_sign}<{sender}> {body}").replace('\n', "\n> "), - format!( - "\ -
\ - In reply to \ - {emote_sign}{sender}\ -
\ - {html_body}\ -
\ -
" - ), - ) -} - fn formatted_or_plain_body( formatted: Option<&FormattedBody>, body: &str, @@ -119,10 +104,9 @@ pub fn plain_and_formatted_reply_body( let (quoted, quoted_html) = get_message_quote_fallbacks(original_message); let plain = format!("{quoted}\n{body}"); - let html = if let Some(formatted) = formatted { - format!("{quoted_html}{formatted}") - } else { - format!("{quoted_html}{body}") + let html = match formatted { + Some(formatted) => format!("{quoted_html}{formatted}"), + None => format!("{quoted_html}{body}"), }; (plain, html) @@ -160,7 +144,7 @@ mod tests {
\ multi
line\ \ - " + ", ); } } diff --git a/crates/ruma-common/tests/events/room_message.rs b/crates/ruma-common/tests/events/room_message.rs index c1364cae..506ab87e 100644 --- a/crates/ruma-common/tests/events/room_message.rs +++ b/crates/ruma-common/tests/events/room_message.rs @@ -500,22 +500,22 @@ fn reply_sanitize() { unsigned: MessageLikeUnsigned::default(), }; let second_message = OriginalRoomMessageEvent { - content: RoomMessageEventContent::text_reply_html( + content: RoomMessageEventContent::text_html( "This is the _second_ message", "This is the second message", - &first_message, - ), + ) + .make_reply_to(&first_message), event_id: event_id!("$143273582443PhrSn:example.org").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(10_000)), room_id: room_id!("!testroomid:example.org").to_owned(), sender: user_id!("@user:example.org").to_owned(), unsigned: MessageLikeUnsigned::default(), }; - let final_reply = RoomMessageEventContent::text_reply_html( + let final_reply = RoomMessageEventContent::text_html( "This is **my** reply", "This is my reply", - &second_message, - ); + ) + .make_reply_to(&second_message); let (body, formatted) = assert_matches!( first_message.content.msgtype,