ruma_events/room/message/
without_relation.rs

1use as_variant::as_variant;
2use ruma_common::{serde::Raw, OwnedEventId, OwnedUserId, RoomId, UserId};
3use serde::{Deserialize, Serialize};
4
5use super::{
6    AddMentions, ForwardThread, MessageType, OriginalRoomMessageEvent, Relation,
7    ReplacementMetadata, ReplyWithinThread, RoomMessageEventContent,
8};
9use crate::{
10    relation::{InReplyTo, Replacement, Thread},
11    room::message::{reply::OriginalEventData, FormattedBody},
12    AnySyncTimelineEvent, Mentions,
13};
14
15/// Form of [`RoomMessageEventContent`] without relation.
16#[derive(Clone, Debug, Serialize)]
17#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
18pub struct RoomMessageEventContentWithoutRelation {
19    /// A key which identifies the type of message being sent.
20    ///
21    /// This also holds the specific content of each message.
22    #[serde(flatten)]
23    pub msgtype: MessageType,
24
25    /// The [mentions] of this event.
26    ///
27    /// [mentions]: https://spec.matrix.org/latest/client-server-api/#user-and-room-mentions
28    #[serde(rename = "m.mentions", skip_serializing_if = "Option::is_none")]
29    pub mentions: Option<Mentions>,
30}
31
32impl RoomMessageEventContentWithoutRelation {
33    /// Creates a new `RoomMessageEventContentWithoutRelation` with the given `MessageType`.
34    pub fn new(msgtype: MessageType) -> Self {
35        Self { msgtype, mentions: None }
36    }
37
38    /// A constructor to create a plain text message.
39    pub fn text_plain(body: impl Into<String>) -> Self {
40        Self::new(MessageType::text_plain(body))
41    }
42
43    /// A constructor to create an html message.
44    pub fn text_html(body: impl Into<String>, html_body: impl Into<String>) -> Self {
45        Self::new(MessageType::text_html(body, html_body))
46    }
47
48    /// A constructor to create a markdown message.
49    #[cfg(feature = "markdown")]
50    pub fn text_markdown(body: impl AsRef<str> + Into<String>) -> Self {
51        Self::new(MessageType::text_markdown(body))
52    }
53
54    /// A constructor to create a plain text notice.
55    pub fn notice_plain(body: impl Into<String>) -> Self {
56        Self::new(MessageType::notice_plain(body))
57    }
58
59    /// A constructor to create an html notice.
60    pub fn notice_html(body: impl Into<String>, html_body: impl Into<String>) -> Self {
61        Self::new(MessageType::notice_html(body, html_body))
62    }
63
64    /// A constructor to create a markdown notice.
65    #[cfg(feature = "markdown")]
66    pub fn notice_markdown(body: impl AsRef<str> + Into<String>) -> Self {
67        Self::new(MessageType::notice_markdown(body))
68    }
69
70    /// A constructor to create a plain text emote.
71    pub fn emote_plain(body: impl Into<String>) -> Self {
72        Self::new(MessageType::emote_plain(body))
73    }
74
75    /// A constructor to create an html emote.
76    pub fn emote_html(body: impl Into<String>, html_body: impl Into<String>) -> Self {
77        Self::new(MessageType::emote_html(body, html_body))
78    }
79
80    /// A constructor to create a markdown emote.
81    #[cfg(feature = "markdown")]
82    pub fn emote_markdown(body: impl AsRef<str> + Into<String>) -> Self {
83        Self::new(MessageType::emote_markdown(body))
84    }
85
86    /// Transform `self` into a `RoomMessageEventContent` with the given relation.
87    pub fn with_relation(
88        self,
89        relates_to: Option<Relation<RoomMessageEventContentWithoutRelation>>,
90    ) -> RoomMessageEventContent {
91        let Self { msgtype, mentions } = self;
92        RoomMessageEventContent { msgtype, relates_to, mentions }
93    }
94
95    /// Turns `self` into a reply to the given message.
96    ///
97    /// Takes the `body` / `formatted_body` (if any) in `self` for the main text and prepends a
98    /// quoted version of `original_message`. Also sets the `in_reply_to` field inside `relates_to`,
99    /// and optionally the `rel_type` to `m.thread` if the `original_message is in a thread and
100    /// thread forwarding is enabled.
101    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))]
102    ///
103    /// # Panics
104    ///
105    /// Panics if `self` has a `formatted_body` with a format other than HTML.
106    #[track_caller]
107    pub fn make_reply_to(
108        mut self,
109        original_message: &OriginalRoomMessageEvent,
110        forward_thread: ForwardThread,
111        add_mentions: AddMentions,
112    ) -> RoomMessageEventContent {
113        self.msgtype.add_reply_fallback(original_message.into());
114        let original_event_id = original_message.event_id.clone();
115
116        let original_thread_id = if forward_thread == ForwardThread::Yes {
117            original_message
118                .content
119                .relates_to
120                .as_ref()
121                .and_then(as_variant!(Relation::Thread))
122                .map(|thread| thread.event_id.clone())
123        } else {
124            None
125        };
126
127        let sender_for_mentions =
128            (add_mentions == AddMentions::Yes).then_some(&*original_message.sender);
129
130        self.make_reply_tweaks(original_event_id, original_thread_id, sender_for_mentions)
131    }
132
133    /// Turns `self` into a reply to the given raw event.
134    ///
135    /// Takes the `body` / `formatted_body` (if any) in `self` for the main text and prepends a
136    /// quoted version of the `body` of `original_event` (if any). Also sets the `in_reply_to` field
137    /// inside `relates_to`, and optionally the `rel_type` to `m.thread` if the
138    /// `original_message is in a thread and thread forwarding is enabled.
139    ///
140    /// It is recommended to use [`Self::make_reply_to()`] for replies to `m.room.message` events,
141    /// as the generated fallback is better for some `msgtype`s.
142    ///
143    /// Note that except for the panic below, this is infallible. Which means that if a field is
144    /// missing when deserializing the data, the changes that require it will not be applied. It
145    /// will still at least apply the `m.in_reply_to` relation to this content.
146    ///
147    /// # Panics
148    ///
149    /// Panics if `self` has a `formatted_body` with a format other than HTML.
150    #[track_caller]
151    pub fn make_reply_to_raw(
152        mut self,
153        original_event: &Raw<AnySyncTimelineEvent>,
154        original_event_id: OwnedEventId,
155        room_id: &RoomId,
156        forward_thread: ForwardThread,
157        add_mentions: AddMentions,
158    ) -> RoomMessageEventContent {
159        #[derive(Deserialize)]
160        struct ContentDeHelper {
161            body: Option<String>,
162            #[serde(flatten)]
163            formatted: Option<FormattedBody>,
164            #[cfg(feature = "unstable-msc1767")]
165            #[serde(rename = "org.matrix.msc1767.text")]
166            text: Option<String>,
167            #[serde(rename = "m.relates_to")]
168            relates_to: Option<crate::room::encrypted::Relation>,
169        }
170
171        let sender = original_event.get_field::<OwnedUserId>("sender").ok().flatten();
172        let content = original_event.get_field::<ContentDeHelper>("content").ok().flatten();
173        let relates_to = content.as_ref().and_then(|c| c.relates_to.as_ref());
174
175        let content_body = content.as_ref().and_then(|c| {
176            let body = c.body.as_deref();
177            #[cfg(feature = "unstable-msc1767")]
178            let body = body.or(c.text.as_deref());
179
180            Some((c, body?))
181        });
182
183        // Only apply fallback if we managed to deserialize raw event.
184        if let (Some(sender), Some((content, body))) = (&sender, content_body) {
185            let is_reply =
186                matches!(content.relates_to, Some(crate::room::encrypted::Relation::Reply { .. }));
187            let data = OriginalEventData {
188                body,
189                formatted: content.formatted.as_ref(),
190                is_emote: false,
191                is_reply,
192                room_id,
193                event_id: &original_event_id,
194                sender,
195            };
196
197            self.msgtype.add_reply_fallback(data);
198        }
199
200        let original_thread_id = if forward_thread == ForwardThread::Yes {
201            relates_to
202                .and_then(as_variant!(crate::room::encrypted::Relation::Thread))
203                .map(|thread| thread.event_id.clone())
204        } else {
205            None
206        };
207
208        let sender_for_mentions = sender.as_deref().filter(|_| add_mentions == AddMentions::Yes);
209        self.make_reply_tweaks(original_event_id, original_thread_id, sender_for_mentions)
210    }
211
212    /// Turns `self` into a new message for a thread, that is optionally a reply.
213    ///
214    /// Looks for a [`Relation::Thread`] in `previous_message`. If it exists, this message will be
215    /// in the same thread. If it doesn't, a new thread with `previous_message` as the root is
216    /// created.
217    ///
218    /// If this is a reply within the thread, takes the `body` / `formatted_body` (if any) in `self`
219    /// for the main text and prepends a quoted version of `previous_message`. Also sets the
220    /// `in_reply_to` field inside `relates_to`.
221    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))]
222    ///
223    /// # Panics
224    ///
225    /// Panics if this is a reply within the thread and `self` has a `formatted_body` with a format
226    /// other than HTML.
227    pub fn make_for_thread(
228        self,
229        previous_message: &OriginalRoomMessageEvent,
230        is_reply: ReplyWithinThread,
231        add_mentions: AddMentions,
232    ) -> RoomMessageEventContent {
233        let mut content = if is_reply == ReplyWithinThread::Yes {
234            self.make_reply_to(previous_message, ForwardThread::No, add_mentions)
235        } else {
236            self.into()
237        };
238
239        let thread_root = if let Some(Relation::Thread(Thread { event_id, .. })) =
240            &previous_message.content.relates_to
241        {
242            event_id.clone()
243        } else {
244            previous_message.event_id.clone()
245        };
246
247        content.relates_to = Some(Relation::Thread(Thread {
248            event_id: thread_root,
249            in_reply_to: Some(InReplyTo { event_id: previous_message.event_id.clone() }),
250            is_falling_back: is_reply == ReplyWithinThread::No,
251        }));
252
253        content
254    }
255
256    /// Turns `self` into a [replacement] (or edit) for a given message.
257    ///
258    /// The first argument after `self` can be `&OriginalRoomMessageEvent` or
259    /// `&OriginalSyncRoomMessageEvent` if you don't want to create `ReplacementMetadata` separately
260    /// before calling this function.
261    ///
262    /// This takes the content and sets it in `m.new_content`, and modifies the `content` to include
263    /// a fallback.
264    ///
265    /// If the message that is replaced is a reply to another message, the latter should also be
266    /// provided to be able to generate a rich reply fallback that takes the `body` /
267    /// `formatted_body` (if any) in `self` for the main text and prepends a quoted version of
268    /// `original_message`.
269    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))]
270    ///
271    /// If this message contains [`Mentions`], they are copied into `m.new_content` to keep the same
272    /// mentions, but the ones in `content` are filtered with the ones in the
273    /// [`ReplacementMetadata`] so only new mentions will trigger a notification.
274    ///
275    /// # Panics
276    ///
277    /// Panics if `self` has a `formatted_body` with a format other than HTML.
278    ///
279    /// [replacement]: https://spec.matrix.org/latest/client-server-api/#event-replacements
280    #[track_caller]
281    pub fn make_replacement(
282        mut self,
283        metadata: impl Into<ReplacementMetadata>,
284        replied_to_message: Option<&OriginalRoomMessageEvent>,
285    ) -> RoomMessageEventContent {
286        let metadata = metadata.into();
287
288        let mentions = self.mentions.take();
289
290        // Only set mentions that were not there before.
291        if let Some(mentions) = &mentions {
292            let new_mentions = metadata
293                .mentions
294                .map(|old_mentions| {
295                    let mut new_mentions = Mentions::new();
296
297                    new_mentions.user_ids = mentions
298                        .user_ids
299                        .iter()
300                        .filter(|u| !old_mentions.user_ids.contains(*u))
301                        .cloned()
302                        .collect();
303
304                    new_mentions.room = mentions.room && !old_mentions.room;
305
306                    new_mentions
307                })
308                .unwrap_or_else(|| mentions.clone());
309
310            self.mentions = Some(new_mentions);
311        };
312
313        // Prepare relates_to with the untouched msgtype.
314        let relates_to = Relation::Replacement(Replacement {
315            event_id: metadata.event_id,
316            new_content: RoomMessageEventContentWithoutRelation {
317                msgtype: self.msgtype.clone(),
318                mentions,
319            },
320        });
321
322        self.msgtype.make_replacement_body();
323
324        // Add reply fallback if needed.
325        let mut content = if let Some(original_message) = replied_to_message {
326            self.make_reply_to(original_message, ForwardThread::No, AddMentions::No)
327        } else {
328            self.into()
329        };
330
331        content.relates_to = Some(relates_to);
332
333        content
334    }
335
336    /// Add the given [mentions] to this event.
337    ///
338    /// If no [`Mentions`] was set on this events, this sets it. Otherwise, this updates the current
339    /// mentions by extending the previous `user_ids` with the new ones, and applies a logical OR to
340    /// the values of `room`.
341    ///
342    /// [mentions]: https://spec.matrix.org/latest/client-server-api/#user-and-room-mentions
343    pub fn add_mentions(mut self, mentions: Mentions) -> Self {
344        self.mentions.get_or_insert_with(Mentions::new).add(mentions);
345        self
346    }
347
348    fn make_reply_tweaks(
349        mut self,
350        original_event_id: OwnedEventId,
351        original_thread_id: Option<OwnedEventId>,
352        sender_for_mentions: Option<&UserId>,
353    ) -> RoomMessageEventContent {
354        let relates_to = if let Some(event_id) = original_thread_id {
355            Relation::Thread(Thread::plain(event_id.to_owned(), original_event_id.to_owned()))
356        } else {
357            Relation::Reply { in_reply_to: InReplyTo { event_id: original_event_id.to_owned() } }
358        };
359
360        if let Some(sender) = sender_for_mentions {
361            self.mentions.get_or_insert_with(Mentions::new).user_ids.insert(sender.to_owned());
362        }
363
364        self.with_relation(Some(relates_to))
365    }
366}
367
368impl From<MessageType> for RoomMessageEventContentWithoutRelation {
369    fn from(msgtype: MessageType) -> Self {
370        Self::new(msgtype)
371    }
372}
373
374impl From<RoomMessageEventContent> for RoomMessageEventContentWithoutRelation {
375    fn from(value: RoomMessageEventContent) -> Self {
376        let RoomMessageEventContent { msgtype, mentions, .. } = value;
377        Self { msgtype, mentions }
378    }
379}
380
381impl From<RoomMessageEventContentWithoutRelation> for RoomMessageEventContent {
382    fn from(value: RoomMessageEventContentWithoutRelation) -> Self {
383        let RoomMessageEventContentWithoutRelation { msgtype, mentions } = value;
384        Self { msgtype, relates_to: None, mentions }
385    }
386}