use 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!(
"\
\
In reply to \
{emote_sign}{sender}\
\
{html_body}\
\
"
),
)
}
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() {
// Escape reserved HTML entities and new lines.
//
match c {
'&' => f.write_str("&")?,
'<' => f.write_str("<")?,
'>' => f.write_str(">")?,
'"' => f.write_str(""")?,
'\n' => f.write_str("
")?,
_ => 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 mut 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))
}
}
}
/// Get the plain and formatted body for a rich reply.
///
/// Returns a `(plain, html)` tuple.
///
/// With the `sanitize` feature, [HTML tags and attributes] that are not allowed in the Matrix
/// spec and previous [rich reply fallbacks] are removed from the previous message in the new rich
/// reply fallback.
///
/// [HTML tags and attributes]: https://spec.matrix.org/latest/client-server-api/#mroommessage-msgtypes
/// [rich reply fallbacks]: https://spec.matrix.org/latest/client-server-api/#fallbacks-for-rich-replies
pub(super) fn plain_and_formatted_reply_body(
body: &str,
formatted: Option,
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,
"\
\
In reply to \
@alice:example.com\
\
multi
line\
\
",
);
}
}