ruma_events/room/message/
without_relation.rs1use 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#[derive(Clone, Debug, Serialize)]
17#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
18pub struct RoomMessageEventContentWithoutRelation {
19 #[serde(flatten)]
23 pub msgtype: MessageType,
24
25 #[serde(rename = "m.mentions", skip_serializing_if = "Option::is_none")]
29 pub mentions: Option<Mentions>,
30}
31
32impl RoomMessageEventContentWithoutRelation {
33 pub fn new(msgtype: MessageType) -> Self {
35 Self { msgtype, mentions: None }
36 }
37
38 pub fn text_plain(body: impl Into<String>) -> Self {
40 Self::new(MessageType::text_plain(body))
41 }
42
43 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 #[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 pub fn notice_plain(body: impl Into<String>) -> Self {
56 Self::new(MessageType::notice_plain(body))
57 }
58
59 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 #[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 pub fn emote_plain(body: impl Into<String>) -> Self {
72 Self::new(MessageType::emote_plain(body))
73 }
74
75 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 #[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 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 #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))]
102 #[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 #[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 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 #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))]
222 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 #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))]
270 #[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 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 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 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 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}