1use as_variant::as_variant;
18use ruma::{
19 OwnedEventId, UserId,
20 events::{
21 AnySyncTimelineEvent,
22 room::{
23 encrypted::Relation as EncryptedRelation,
24 message::{
25 AddMentions, ForwardThread, ReplyMetadata, ReplyWithinThread,
26 RoomMessageEventContent, RoomMessageEventContentWithoutRelation,
27 },
28 },
29 },
30};
31use thiserror::Error;
32use tracing::instrument;
33
34use super::{EventSource, Room};
35
36#[derive(Debug)]
38pub struct Reply {
39 pub event_id: OwnedEventId,
41 pub enforce_thread: EnforceThread,
43 pub add_mentions: AddMentions,
46}
47
48#[derive(Debug, Error)]
50pub enum ReplyError {
51 #[error("Couldn't fetch the remote event: {0}")]
53 Fetch(Box<crate::Error>),
54 #[error("failed to deserialize event to reply to")]
56 Deserialization,
57 #[error("tried to reply to a state event")]
59 StateEvent,
60}
61
62#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66pub enum EnforceThread {
67 Threaded(ReplyWithinThread),
70
71 MaybeThreaded,
74
75 Unthreaded,
78}
79
80impl Room {
81 #[instrument(skip(self, content), fields(room = %self.room_id()))]
93 pub async fn make_reply_event(
94 &self,
95 content: RoomMessageEventContentWithoutRelation,
96 reply: Reply,
97 ) -> Result<RoomMessageEventContent, ReplyError> {
98 make_reply_event(self, self.own_user_id(), content, reply).await
99 }
100}
101
102async fn make_reply_event<S: EventSource>(
103 source: S,
104 own_user_id: &UserId,
105 content: RoomMessageEventContentWithoutRelation,
106 reply: Reply,
107) -> Result<RoomMessageEventContent, ReplyError> {
108 let event =
109 source.get_event(&reply.event_id).await.map_err(|err| ReplyError::Fetch(Box::new(err)))?;
110
111 let raw_event = event.into_raw();
112 let event = raw_event.deserialize().map_err(|_| ReplyError::Deserialization)?;
113
114 let relation = as_variant!(&event, AnySyncTimelineEvent::MessageLike)
115 .ok_or(ReplyError::StateEvent)?
116 .original_content()
117 .and_then(|content| content.relation());
118 let thread =
119 relation.as_ref().and_then(|relation| as_variant!(relation, EncryptedRelation::Thread));
120
121 let reply_metadata = ReplyMetadata::new(event.event_id(), event.sender(), thread);
122
123 let mention_the_sender =
132 if own_user_id == event.sender() { AddMentions::No } else { reply.add_mentions };
133
134 let content = match reply.enforce_thread {
135 EnforceThread::Threaded(is_reply) => {
136 content.make_for_thread(reply_metadata, is_reply, mention_the_sender)
137 }
138 EnforceThread::MaybeThreaded => {
139 content.make_reply_to(reply_metadata, ForwardThread::Yes, mention_the_sender)
140 }
141 EnforceThread::Unthreaded => {
142 content.make_reply_to(reply_metadata, ForwardThread::No, mention_the_sender)
143 }
144 };
145
146 Ok(content)
147}
148
149#[cfg(test)]
150mod tests {
151 use std::{assert_matches, collections::BTreeMap};
152
153 use matrix_sdk_base::deserialized_responses::TimelineEvent;
154 use matrix_sdk_test::{async_test, event_factory::EventFactory};
155 use ruma::{
156 EventId, OwnedEventId, event_id,
157 events::{
158 AnySyncTimelineEvent,
159 room::message::{
160 AddMentions, Relation, ReplyWithinThread, RoomMessageEventContentWithoutRelation,
161 },
162 },
163 serde::Raw,
164 user_id,
165 };
166 use serde_json::json;
167 use strass::assert_let;
168
169 use super::{EnforceThread, EventSource, Reply, ReplyError, make_reply_event};
170 use crate::{Error, event_cache::EventCacheError};
171
172 #[derive(Default)]
173 struct TestEventCache {
174 events: BTreeMap<OwnedEventId, TimelineEvent>,
175 }
176
177 impl EventSource for TestEventCache {
178 async fn get_event(&self, event_id: &EventId) -> Result<TimelineEvent, Error> {
179 self.events
180 .get(event_id)
181 .cloned()
182 .ok_or(Error::EventCache(Box::new(EventCacheError::ClientDropped)))
183 }
184 }
185
186 #[async_test]
187 async fn test_cannot_reply_to_unknown_event() {
188 let event_id = event_id!("$1");
189 let own_user_id = user_id!("@me:saucisse.bzh");
190
191 let mut cache = TestEventCache::default();
192 let f = EventFactory::new();
193 cache.events.insert(
194 event_id.to_owned(),
195 f.text_msg("hi").event_id(event_id).sender(own_user_id).into(),
196 );
197
198 let content = RoomMessageEventContentWithoutRelation::text_plain("the reply");
199
200 assert_matches!(
201 make_reply_event(
202 cache,
203 own_user_id,
204 content,
205 Reply {
206 event_id: event_id!("$2").into(),
207 enforce_thread: EnforceThread::Unthreaded,
208 add_mentions: AddMentions::Yes,
209 },
210 )
211 .await,
212 Err(ReplyError::Fetch(_))
213 );
214 }
215
216 #[async_test]
217 async fn test_cannot_reply_to_invalid_event() {
218 let event_id = event_id!("$1");
219 let own_user_id = user_id!("@me:saucisse.bzh");
220
221 let mut cache = TestEventCache::default();
222
223 cache.events.insert(
224 event_id.to_owned(),
225 TimelineEvent::from_plaintext(
226 Raw::<AnySyncTimelineEvent>::from_json_string(
227 json!({
228 "content": {
229 "body": "hi"
230 },
231 "event_id": event_id,
232 "origin_server_ts": 1,
233 "type": "m.room.message",
234 })
236 .to_string(),
237 )
238 .unwrap(),
239 ),
240 );
241
242 let content = RoomMessageEventContentWithoutRelation::text_plain("the reply");
243
244 assert_matches!(
245 make_reply_event(
246 cache,
247 own_user_id,
248 content,
249 Reply {
250 event_id: event_id.into(),
251 enforce_thread: EnforceThread::Unthreaded,
252 add_mentions: AddMentions::Yes,
253 },
254 )
255 .await,
256 Err(ReplyError::Deserialization)
257 );
258 }
259
260 #[async_test]
261 async fn test_cannot_reply_to_state_event() {
262 let event_id = event_id!("$1");
263 let own_user_id = user_id!("@me:saucisse.bzh");
264
265 let mut cache = TestEventCache::default();
266 let f = EventFactory::new();
267 cache.events.insert(
268 event_id.to_owned(),
269 f.room_name("lobby").event_id(event_id).sender(own_user_id).into(),
270 );
271
272 let content = RoomMessageEventContentWithoutRelation::text_plain("the reply");
273
274 assert_matches!(
275 make_reply_event(
276 cache,
277 own_user_id,
278 content,
279 Reply {
280 event_id: event_id.into(),
281 enforce_thread: EnforceThread::Unthreaded,
282 add_mentions: AddMentions::Yes,
283 },
284 )
285 .await,
286 Err(ReplyError::StateEvent)
287 );
288 }
289
290 #[async_test]
291 async fn test_reply_unthreaded() {
292 let event_id = event_id!("$1");
293 let own_user_id = user_id!("@me:saucisse.bzh");
294
295 let mut cache = TestEventCache::default();
296 let f = EventFactory::new();
297 cache.events.insert(
298 event_id.to_owned(),
299 f.text_msg("hi").event_id(event_id).sender(own_user_id).into(),
300 );
301
302 let content = RoomMessageEventContentWithoutRelation::text_plain("the reply");
303
304 let reply_event = make_reply_event(
305 cache,
306 own_user_id,
307 content,
308 Reply {
309 event_id: event_id.into(),
310 enforce_thread: EnforceThread::Unthreaded,
311 add_mentions: AddMentions::Yes,
312 },
313 )
314 .await
315 .unwrap();
316
317 assert_let!(Some(Relation::Reply(reply)) = &reply_event.relates_to);
318
319 assert_eq!(reply.in_reply_to.event_id, event_id);
320 }
321
322 #[async_test]
323 async fn test_start_thread() {
324 let event_id = event_id!("$1");
325 let own_user_id = user_id!("@me:saucisse.bzh");
326
327 let mut cache = TestEventCache::default();
328 let f = EventFactory::new();
329 cache.events.insert(
330 event_id.to_owned(),
331 f.text_msg("hi").event_id(event_id).sender(own_user_id).into(),
332 );
333
334 let content = RoomMessageEventContentWithoutRelation::text_plain("the reply");
335
336 let reply_event = make_reply_event(
337 cache,
338 own_user_id,
339 content,
340 Reply {
341 event_id: event_id.into(),
342 enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No),
343 add_mentions: AddMentions::Yes,
344 },
345 )
346 .await
347 .unwrap();
348
349 assert_let!(Some(Relation::Thread(thread)) = &reply_event.relates_to);
350
351 assert_eq!(thread.event_id, event_id);
352 assert_eq!(thread.in_reply_to.as_ref().unwrap().event_id, event_id);
353 assert!(thread.is_falling_back);
354 }
355
356 #[async_test]
357 async fn test_reply_on_thread() {
358 let thread_root = event_id!("$1");
359 let event_id = event_id!("$2");
360 let own_user_id = user_id!("@me:saucisse.bzh");
361
362 let mut cache = TestEventCache::default();
363 let f = EventFactory::new();
364 cache.events.insert(
365 thread_root.to_owned(),
366 f.text_msg("hi").event_id(thread_root).sender(own_user_id).into(),
367 );
368 cache.events.insert(
369 event_id.to_owned(),
370 f.text_msg("ho")
371 .in_thread(thread_root, thread_root)
372 .event_id(event_id)
373 .sender(own_user_id)
374 .into(),
375 );
376
377 let content = RoomMessageEventContentWithoutRelation::text_plain("the reply");
378
379 let reply_event = make_reply_event(
380 cache,
381 own_user_id,
382 content,
383 Reply {
384 event_id: event_id.into(),
385 enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No),
386 add_mentions: AddMentions::Yes,
387 },
388 )
389 .await
390 .unwrap();
391
392 assert_let!(Some(Relation::Thread(thread)) = &reply_event.relates_to);
393
394 assert_eq!(thread.event_id, thread_root);
395 assert_eq!(thread.in_reply_to.as_ref().unwrap().event_id, event_id);
396 assert!(thread.is_falling_back);
397 }
398
399 #[async_test]
400 async fn test_reply_on_thread_as_reply() {
401 let thread_root = event_id!("$1");
402 let event_id = event_id!("$2");
403 let own_user_id = user_id!("@me:saucisse.bzh");
404
405 let mut cache = TestEventCache::default();
406 let f = EventFactory::new();
407 cache.events.insert(
408 thread_root.to_owned(),
409 f.text_msg("hi").event_id(thread_root).sender(own_user_id).into(),
410 );
411 cache.events.insert(
412 event_id.to_owned(),
413 f.text_msg("ho")
414 .in_thread(thread_root, thread_root)
415 .event_id(event_id)
416 .sender(own_user_id)
417 .into(),
418 );
419
420 let content = RoomMessageEventContentWithoutRelation::text_plain("the reply");
421
422 let reply_event = make_reply_event(
423 cache,
424 own_user_id,
425 content,
426 Reply {
427 event_id: event_id.into(),
428 enforce_thread: EnforceThread::Threaded(ReplyWithinThread::Yes),
429 add_mentions: AddMentions::Yes,
430 },
431 )
432 .await
433 .unwrap();
434
435 assert_let!(Some(Relation::Thread(thread)) = &reply_event.relates_to);
436
437 assert_eq!(thread.event_id, thread_root);
438 assert_eq!(thread.in_reply_to.as_ref().unwrap().event_id, event_id);
439 assert!(!thread.is_falling_back);
440 }
441
442 #[async_test]
443 async fn test_reply_forwarding_thread() {
444 let thread_root = event_id!("$1");
445 let event_id = event_id!("$2");
446 let own_user_id = user_id!("@me:saucisse.bzh");
447
448 let mut cache = TestEventCache::default();
449 let f = EventFactory::new();
450 cache.events.insert(
451 thread_root.to_owned(),
452 f.text_msg("hi").event_id(thread_root).sender(own_user_id).into(),
453 );
454 cache.events.insert(
455 event_id.to_owned(),
456 f.text_msg("ho")
457 .in_thread(thread_root, thread_root)
458 .event_id(event_id)
459 .sender(own_user_id)
460 .into(),
461 );
462
463 let content = RoomMessageEventContentWithoutRelation::text_plain("the reply");
464
465 let reply_event = make_reply_event(
466 cache,
467 own_user_id,
468 content,
469 Reply {
470 event_id: event_id.into(),
471 enforce_thread: EnforceThread::MaybeThreaded,
472 add_mentions: AddMentions::Yes,
473 },
474 )
475 .await
476 .unwrap();
477
478 assert_let!(Some(Relation::Thread(thread)) = &reply_event.relates_to);
479
480 assert_eq!(thread.event_id, thread_root);
481 assert_eq!(thread.in_reply_to.as_ref().unwrap().event_id, event_id);
482 assert!(thread.is_falling_back);
483 }
484
485 #[async_test]
486 async fn test_reply_forwarding_thread_for_poll_start() {
487 let thread_root = event_id!("$thread_root");
488 let event_id = event_id!("$thread_reply");
489 let own_user_id = user_id!("@me:saucisse.bzh");
490
491 let mut cache = TestEventCache::default();
492 let f = EventFactory::new();
493
494 cache.events.insert(
495 event_id.to_owned(),
496 f.poll_start(
497 "would you rather… A) eat a pineapple pizza, B) drink pickle juice",
498 "would you rather…",
499 vec!["eat a pineapple pizza", "drink pickle juice"],
500 )
501 .in_thread(thread_root, thread_root)
502 .event_id(event_id)
503 .sender(own_user_id)
504 .into(),
505 );
506
507 let content = RoomMessageEventContentWithoutRelation::text_plain("the reply");
508
509 let reply_event = make_reply_event(
510 cache,
511 own_user_id,
512 content,
513 Reply {
514 event_id: event_id.into(),
515 enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No),
516 add_mentions: AddMentions::Yes,
517 },
518 )
519 .await
520 .unwrap();
521
522 assert_let!(Some(Relation::Thread(thread)) = &reply_event.relates_to);
523
524 assert_eq!(thread.event_id, thread_root);
525 assert_eq!(thread.in_reply_to.as_ref().unwrap().event_id, event_id);
526 assert!(thread.is_falling_back);
527 }
528
529 #[async_test]
530 async fn test_reply_without_add_mentions() {
531 let event_id = event_id!("$1");
532 let other_user_id = user_id!("@you:saucisse.bzh");
533 let own_user_id = user_id!("@me:saucisse.bzh");
534
535 let mut cache = TestEventCache::default();
536 let f = EventFactory::new();
537 cache.events.insert(
538 event_id.to_owned(),
539 f.text_msg("hi").event_id(event_id).sender(other_user_id).into(),
540 );
541
542 let content = RoomMessageEventContentWithoutRelation::text_plain("the reply");
543
544 let reply_event = make_reply_event(
545 cache,
546 own_user_id,
547 content,
548 Reply {
549 event_id: event_id.into(),
550 enforce_thread: EnforceThread::Unthreaded,
551 add_mentions: AddMentions::No,
552 },
553 )
554 .await
555 .unwrap();
556
557 assert!(reply_event.mentions.is_none());
558 }
559
560 #[async_test]
561 async fn test_reply_with_add_mentions() {
562 let event_id = event_id!("$1");
563 let other_user_id = user_id!("@you:saucisse.bzh");
564 let own_user_id = user_id!("@me:saucisse.bzh");
565
566 let mut cache = TestEventCache::default();
567 let f = EventFactory::new();
568 cache.events.insert(
569 event_id.to_owned(),
570 f.text_msg("hi").event_id(event_id).sender(other_user_id).into(),
571 );
572
573 let content = RoomMessageEventContentWithoutRelation::text_plain("the reply");
574
575 let reply_event = make_reply_event(
576 cache,
577 own_user_id,
578 content,
579 Reply {
580 event_id: event_id.into(),
581 enforce_thread: EnforceThread::Unthreaded,
582 add_mentions: AddMentions::Yes,
583 },
584 )
585 .await
586 .unwrap();
587
588 assert!(reply_event.mentions.is_some());
589 assert!(reply_event.mentions.unwrap().user_ids.contains(user_id!("@you:saucisse.bzh")));
590 }
591}