ruma_events/room/message/
reply.rsuse std::fmt::{self, Write};
use ruma_common::{EventId, RoomId, UserId};
#[cfg(feature = "html")]
use ruma_html::Html;
use super::{
sanitize::remove_plain_reply_fallback, FormattedBody, MessageType, OriginalRoomMessageEvent,
Relation,
};
pub(super) struct OriginalEventData<'a> {
pub(super) body: &'a str,
pub(super) formatted: Option<&'a FormattedBody>,
pub(super) is_emote: bool,
pub(super) is_reply: bool,
pub(super) room_id: &'a RoomId,
pub(super) event_id: &'a EventId,
pub(super) sender: &'a UserId,
}
impl<'a> From<&'a OriginalRoomMessageEvent> for OriginalEventData<'a> {
fn from(message: &'a OriginalRoomMessageEvent) -> Self {
let OriginalRoomMessageEvent { room_id, event_id, sender, content, .. } = message;
let is_reply = matches!(content.relates_to, Some(Relation::Reply { .. }));
let (body, formatted, is_emote) = match &content.msgtype {
MessageType::Audio(_) => ("sent an audio file.", None, false),
MessageType::Emote(c) => (&*c.body, c.formatted.as_ref(), true),
MessageType::File(_) => ("sent a file.", None, false),
MessageType::Image(_) => ("sent an image.", None, false),
MessageType::Location(_) => ("sent a location.", None, false),
MessageType::Notice(c) => (&*c.body, c.formatted.as_ref(), false),
MessageType::ServerNotice(c) => (&*c.body, None, false),
MessageType::Text(c) => (&*c.body, c.formatted.as_ref(), false),
MessageType::Video(_) => ("sent a video.", None, false),
MessageType::VerificationRequest(c) => (&*c.body, None, false),
MessageType::_Custom(c) => (&*c.body, None, false),
};
Self { body, formatted, is_emote, is_reply, room_id, event_id, sender }
}
}
fn get_message_quote_fallbacks(original_event: OriginalEventData<'_>) -> (String, String) {
let OriginalEventData { body, formatted, is_emote, is_reply, room_id, event_id, sender } =
original_event;
let emote_sign = is_emote.then_some("* ").unwrap_or_default();
let body = is_reply.then(|| remove_plain_reply_fallback(body)).unwrap_or(body);
#[cfg(feature = "html")]
let html_body = FormattedOrPlainBody { formatted, body, is_reply };
#[cfg(not(feature = "html"))]
let html_body = FormattedOrPlainBody { formatted, body };
(
format!("> {emote_sign}<{sender}> {body}").replace('\n', "\n> "),
format!(
"<mx-reply>\
<blockquote>\
<a href=\"https://matrix.to/#/{room_id}/{event_id}\">In reply to</a> \
{emote_sign}<a href=\"https://matrix.to/#/{sender}\">{sender}</a>\
<br>\
{html_body}\
</blockquote>\
</mx-reply>"
),
)
}
struct EscapeHtmlEntities<'a>(&'a str);
impl fmt::Display for EscapeHtmlEntities<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for c in self.0.chars() {
match c {
'&' => f.write_str("&")?,
'<' => f.write_str("<")?,
'>' => f.write_str(">")?,
'"' => f.write_str(""")?,
'\n' => f.write_str("<br>")?,
_ => f.write_char(c)?,
}
}
Ok(())
}
}
struct FormattedOrPlainBody<'a> {
formatted: Option<&'a FormattedBody>,
body: &'a str,
#[cfg(feature = "html")]
is_reply: bool,
}
impl fmt::Display for FormattedOrPlainBody<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(formatted_body) = self.formatted {
#[cfg(feature = "html")]
if self.is_reply {
let html = Html::parse(&formatted_body.body);
html.sanitize();
write!(f, "{html}")
} else {
f.write_str(&formatted_body.body)
}
#[cfg(not(feature = "html"))]
f.write_str(&formatted_body.body)
} else {
write!(f, "{}", EscapeHtmlEntities(self.body))
}
}
}
pub(super) fn plain_and_formatted_reply_body(
body: &str,
formatted: Option<impl fmt::Display>,
original_event: OriginalEventData<'_>,
) -> (String, String) {
let (quoted, quoted_html) = get_message_quote_fallbacks(original_event);
let plain = format!("{quoted}\n\n{body}");
let html = match formatted {
Some(formatted) => format!("{quoted_html}{formatted}"),
None => format!("{quoted_html}{}", EscapeHtmlEntities(body)),
};
(plain, html)
}
#[cfg(test)]
mod tests {
use ruma_common::{owned_event_id, owned_room_id, owned_user_id, MilliSecondsSinceUnixEpoch};
use super::OriginalRoomMessageEvent;
use crate::{room::message::RoomMessageEventContent, MessageLikeUnsigned};
#[test]
fn fallback_multiline() {
let (plain_quote, html_quote) = super::get_message_quote_fallbacks(
(&OriginalRoomMessageEvent {
content: RoomMessageEventContent::text_plain("multi\nline"),
event_id: owned_event_id!("$1598361704261elfgc:localhost"),
sender: owned_user_id!("@alice:example.com"),
origin_server_ts: MilliSecondsSinceUnixEpoch::now(),
room_id: owned_room_id!("!n8f893n9:example.com"),
unsigned: MessageLikeUnsigned::new(),
})
.into(),
);
assert_eq!(plain_quote, "> <@alice:example.com> multi\n> line");
assert_eq!(
html_quote,
"<mx-reply>\
<blockquote>\
<a href=\"https://matrix.to/#/!n8f893n9:example.com/$1598361704261elfgc:localhost\">In reply to</a> \
<a href=\"https://matrix.to/#/@alice:example.com\">@alice:example.com</a>\
<br>\
multi<br>line\
</blockquote>\
</mx-reply>",
);
}
}