event: Add reply-making methods to RoomMessageEventWithoutRelation

This commit is contained in:
Jonas Platte 2023-10-09 16:12:03 +02:00
parent 533beb600f
commit 440a563355
No known key found for this signature in database
GPG Key ID: AAA7A61F696C3E0C
3 changed files with 176 additions and 96 deletions

View File

@ -6,6 +6,10 @@ Improvements:
mentioned in the original message to mentions (only the sender of the original message) mentioned in the original message to mentions (only the sender of the original message)
- Add convenience constructors like `text_plain` to `RoomMessageEventContentWithoutRelation` - Add convenience constructors like `text_plain` to `RoomMessageEventContentWithoutRelation`
- These are the same that are already available on `RoomMessageEventContent` - These are the same that are already available on `RoomMessageEventContent`
- Add methods on `RoomMessageEventWithoutRelation` that were previously only available on
`RoomMessageEventContent`:
- `make_reply_to`
- `make_reply_to_raw`
# 0.27.0 # 0.27.0

View File

@ -4,16 +4,16 @@
use std::borrow::Cow; use std::borrow::Cow;
use as_variant::as_variant;
use ruma_common::{ use ruma_common::{
serde::{JsonObject, Raw, StringEnum}, serde::{JsonObject, Raw, StringEnum},
OwnedEventId, OwnedUserId, RoomId, UserId, OwnedEventId, RoomId,
}; };
#[cfg(feature = "html")] #[cfg(feature = "html")]
use ruma_html::{sanitize_html, HtmlSanitizerMode, RemoveReplyFallback}; use ruma_html::{sanitize_html, HtmlSanitizerMode, RemoveReplyFallback};
use ruma_macros::EventContent; use ruma_macros::EventContent;
use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::Value as JsonValue; use serde_json::Value as JsonValue;
use tracing::warn;
use self::reply::OriginalEventData; use self::reply::OriginalEventData;
#[cfg(feature = "html")] #[cfg(feature = "html")]
@ -157,29 +157,12 @@ impl RoomMessageEventContent {
/// Panics if `self` has a `formatted_body` with a format other than HTML. /// Panics if `self` has a `formatted_body` with a format other than HTML.
#[track_caller] #[track_caller]
pub fn make_reply_to( pub fn make_reply_to(
mut self, self,
original_message: &OriginalRoomMessageEvent, original_message: &OriginalRoomMessageEvent,
forward_thread: ForwardThread, forward_thread: ForwardThread,
add_mentions: AddMentions, add_mentions: AddMentions,
) -> Self { ) -> Self {
self.msgtype.add_reply_fallback(original_message.into()); self.without_relation().make_reply_to(original_message, forward_thread, add_mentions)
let original_event_id = original_message.event_id.clone();
let original_thread_id = if forward_thread == ForwardThread::Yes {
original_message
.content
.relates_to
.as_ref()
.and_then(as_variant!(Relation::Thread))
.map(|thread| thread.event_id.clone())
} else {
None
};
let sender_for_mentions =
(add_mentions == AddMentions::Yes).then_some(&*original_message.sender);
self.make_reply_tweaks(original_event_id, original_thread_id, sender_for_mentions)
} }
/// Turns `self` into a reply to the given raw event. /// Turns `self` into a reply to the given raw event.
@ -201,84 +184,20 @@ impl RoomMessageEventContent {
/// Panics if `self` has a `formatted_body` with a format other than HTML. /// Panics if `self` has a `formatted_body` with a format other than HTML.
#[track_caller] #[track_caller]
pub fn make_reply_to_raw( pub fn make_reply_to_raw(
mut self, self,
original_event: &Raw<AnySyncTimelineEvent>, original_event: &Raw<AnySyncTimelineEvent>,
original_event_id: OwnedEventId, original_event_id: OwnedEventId,
room_id: &RoomId, room_id: &RoomId,
forward_thread: ForwardThread, forward_thread: ForwardThread,
add_mentions: AddMentions, add_mentions: AddMentions,
) -> Self { ) -> Self {
#[derive(Deserialize)] self.without_relation().make_reply_to_raw(
struct ContentDeHelper { original_event,
body: Option<String>, original_event_id,
#[serde(flatten)] room_id,
formatted: Option<FormattedBody>, forward_thread,
#[cfg(feature = "unstable-msc1767")] add_mentions,
#[serde(rename = "org.matrix.msc1767")] )
text: Option<String>,
#[serde(rename = "m.relates_to")]
relates_to: Option<crate::room::encrypted::Relation>,
}
let sender = original_event.get_field::<OwnedUserId>("sender").ok().flatten();
let content = original_event.get_field::<ContentDeHelper>("content").ok().flatten();
let relates_to = content.as_ref().and_then(|c| c.relates_to.as_ref());
let content_body = content.as_ref().and_then(|c| {
let body = c.body.as_deref();
#[cfg(feature = "unstable-msc1767")]
let body = body.or(c.text.as_deref());
Some((c, body?))
});
// Only apply fallback if we managed to deserialize raw event.
if let (Some(sender), Some((content, body))) = (&sender, content_body) {
let is_reply =
matches!(content.relates_to, Some(crate::room::encrypted::Relation::Reply { .. }));
let data = OriginalEventData {
body,
formatted: content.formatted.as_ref(),
is_emote: false,
is_reply,
room_id,
event_id: &original_event_id,
sender,
};
self.msgtype.add_reply_fallback(data);
}
let original_thread_id = if forward_thread == ForwardThread::Yes {
relates_to
.and_then(as_variant!(crate::room::encrypted::Relation::Thread))
.map(|thread| thread.event_id.clone())
} else {
None
};
let sender_for_mentions = sender.as_deref().filter(|_| add_mentions == AddMentions::Yes);
self.make_reply_tweaks(original_event_id, original_thread_id, sender_for_mentions)
}
fn make_reply_tweaks(
mut self,
original_event_id: OwnedEventId,
original_thread_id: Option<OwnedEventId>,
sender_for_mentions: Option<&UserId>,
) -> Self {
let relates_to = if let Some(event_id) = original_thread_id {
Relation::Thread(Thread::plain(event_id.to_owned(), original_event_id.to_owned()))
} else {
Relation::Reply { in_reply_to: InReplyTo { event_id: original_event_id.to_owned() } }
};
self.relates_to = Some(relates_to);
if let Some(sender) = sender_for_mentions {
self.mentions.get_or_insert_with(Mentions::new).user_ids.insert(sender.to_owned());
}
self
} }
/// Turns `self` into a new message for a thread, that is optionally a reply. /// Turns `self` into a new message for a thread, that is optionally a reply.
@ -485,6 +404,14 @@ impl RoomMessageEventContent {
self.msgtype.sanitize(mode, remove_reply_fallback); self.msgtype.sanitize(mode, remove_reply_fallback);
} }
fn without_relation(self) -> RoomMessageEventContentWithoutRelation {
if self.relates_to.is_some() {
warn!("Overwriting existing relates_to value");
}
self.into()
}
} }
/// Whether or not to forward a [`Relation::Thread`] when sending a reply. /// Whether or not to forward a [`Relation::Thread`] when sending a reply.

View File

@ -1,7 +1,20 @@
use serde::Serialize; use as_variant::as_variant;
use ruma_common::{serde::Raw, OwnedEventId, OwnedUserId, RoomId, UserId};
#[cfg(doc)]
use ruma_html::{sanitize_html, RemoveReplyFallback};
use serde::{Deserialize, Serialize};
use super::{MessageType, Relation, RoomMessageEventContent}; #[cfg(doc)]
use crate::Mentions; use super::OriginalSyncRoomMessageEvent;
use super::{
AddMentions, ForwardThread, MessageType, OriginalRoomMessageEvent, Relation,
RoomMessageEventContent,
};
use crate::{
relation::{InReplyTo, Thread},
room::message::{reply::OriginalEventData, FormattedBody},
AnySyncTimelineEvent, Mentions,
};
/// Form of [`RoomMessageEventContent`] without relation. /// Form of [`RoomMessageEventContent`] without relation.
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
@ -82,6 +95,142 @@ impl RoomMessageEventContentWithoutRelation {
let Self { msgtype, mentions } = self; let Self { msgtype, mentions } = self;
RoomMessageEventContent { msgtype, relates_to, mentions } RoomMessageEventContent { msgtype, relates_to, mentions }
} }
/// 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`,
/// and optionally the `rel_type` to `m.thread` if the `original_message is in a thread and
/// thread forwarding is enabled.
#[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,
forward_thread: ForwardThread,
add_mentions: AddMentions,
) -> RoomMessageEventContent {
self.msgtype.add_reply_fallback(original_message.into());
let original_event_id = original_message.event_id.clone();
let original_thread_id = if forward_thread == ForwardThread::Yes {
original_message
.content
.relates_to
.as_ref()
.and_then(as_variant!(Relation::Thread))
.map(|thread| thread.event_id.clone())
} else {
None
};
let sender_for_mentions =
(add_mentions == AddMentions::Yes).then_some(&*original_message.sender);
self.make_reply_tweaks(original_event_id, original_thread_id, sender_for_mentions)
}
/// Turns `self` into a reply to the given raw event.
///
/// Takes the `body` / `formatted_body` (if any) in `self` for the main text and prepends a
/// quoted version of the `body` of `original_event` (if any). Also sets the `in_reply_to` field
/// inside `relates_to`, and optionally the `rel_type` to `m.thread` if the
/// `original_message is in a thread and thread forwarding is enabled.
///
/// It is recommended to use [`Self::make_reply_to()`] for replies to `m.room.message` events,
/// as the generated fallback is better for some `msgtype`s.
///
/// Note that except for the panic below, this is infallible. Which means that if a field is
/// missing when deserializing the data, the changes that require it will not be applied. It
/// will still at least apply the `m.in_reply_to` relation to this content.
///
/// # Panics
///
/// Panics if `self` has a `formatted_body` with a format other than HTML.
#[track_caller]
pub fn make_reply_to_raw(
mut self,
original_event: &Raw<AnySyncTimelineEvent>,
original_event_id: OwnedEventId,
room_id: &RoomId,
forward_thread: ForwardThread,
add_mentions: AddMentions,
) -> RoomMessageEventContent {
#[derive(Deserialize)]
struct ContentDeHelper {
body: Option<String>,
#[serde(flatten)]
formatted: Option<FormattedBody>,
#[cfg(feature = "unstable-msc1767")]
#[serde(rename = "org.matrix.msc1767")]
text: Option<String>,
#[serde(rename = "m.relates_to")]
relates_to: Option<crate::room::encrypted::Relation>,
}
let sender = original_event.get_field::<OwnedUserId>("sender").ok().flatten();
let content = original_event.get_field::<ContentDeHelper>("content").ok().flatten();
let relates_to = content.as_ref().and_then(|c| c.relates_to.as_ref());
let content_body = content.as_ref().and_then(|c| {
let body = c.body.as_deref();
#[cfg(feature = "unstable-msc1767")]
let body = body.or(c.text.as_deref());
Some((c, body?))
});
// Only apply fallback if we managed to deserialize raw event.
if let (Some(sender), Some((content, body))) = (&sender, content_body) {
let is_reply =
matches!(content.relates_to, Some(crate::room::encrypted::Relation::Reply { .. }));
let data = OriginalEventData {
body,
formatted: content.formatted.as_ref(),
is_emote: false,
is_reply,
room_id,
event_id: &original_event_id,
sender,
};
self.msgtype.add_reply_fallback(data);
}
let original_thread_id = if forward_thread == ForwardThread::Yes {
relates_to
.and_then(as_variant!(crate::room::encrypted::Relation::Thread))
.map(|thread| thread.event_id.clone())
} else {
None
};
let sender_for_mentions = sender.as_deref().filter(|_| add_mentions == AddMentions::Yes);
self.make_reply_tweaks(original_event_id, original_thread_id, sender_for_mentions)
}
fn make_reply_tweaks(
mut self,
original_event_id: OwnedEventId,
original_thread_id: Option<OwnedEventId>,
sender_for_mentions: Option<&UserId>,
) -> RoomMessageEventContent {
let relates_to = if let Some(event_id) = original_thread_id {
Relation::Thread(Thread::plain(event_id.to_owned(), original_event_id.to_owned()))
} else {
Relation::Reply { in_reply_to: InReplyTo { event_id: original_event_id.to_owned() } }
};
if let Some(sender) = sender_for_mentions {
self.mentions.get_or_insert_with(Mentions::new).user_ids.insert(sender.to_owned());
}
self.with_relation(Some(relates_to))
}
} }
impl From<MessageType> for RoomMessageEventContentWithoutRelation { impl From<MessageType> for RoomMessageEventContentWithoutRelation {