1#![allow(missing_docs)]
16
17use std::{
18 collections::BTreeSet,
19 sync::atomic::{AtomicU64, Ordering::SeqCst},
20};
21
22use as_variant::as_variant;
23use matrix_sdk_common::deserialized_responses::{
24 TimelineEvent, UnableToDecryptInfo, UnableToDecryptReason,
25};
26use ruma::{
27 events::{
28 member_hints::MemberHintsEventContent,
29 poll::{
30 unstable_end::UnstablePollEndEventContent,
31 unstable_response::UnstablePollResponseEventContent,
32 unstable_start::{
33 NewUnstablePollStartEventContent, ReplacementUnstablePollStartEventContent,
34 UnstablePollAnswer, UnstablePollStartContentBlock, UnstablePollStartEventContent,
35 },
36 },
37 reaction::ReactionEventContent,
38 receipt::{Receipt, ReceiptEventContent, ReceiptThread, ReceiptType},
39 relation::{Annotation, InReplyTo, Replacement, Thread},
40 room::{
41 avatar::{self, RoomAvatarEventContent},
42 encrypted::{EncryptedEventScheme, RoomEncryptedEventContent},
43 member::{MembershipState, RoomMemberEventContent},
44 message::{
45 FormattedBody, ImageMessageEventContent, MessageType, Relation,
46 RoomMessageEventContent, RoomMessageEventContentWithoutRelation,
47 },
48 name::RoomNameEventContent,
49 redaction::RoomRedactionEventContent,
50 tombstone::RoomTombstoneEventContent,
51 topic::RoomTopicEventContent,
52 },
53 typing::TypingEventContent,
54 AnySyncTimelineEvent, AnyTimelineEvent, BundledMessageLikeRelations, EventContent,
55 RedactedMessageLikeEventContent, RedactedStateEventContent,
56 },
57 serde::Raw,
58 server_name, EventId, MilliSecondsSinceUnixEpoch, MxcUri, OwnedEventId, OwnedMxcUri,
59 OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UInt, UserId,
60};
61use serde::Serialize;
62use serde_json::json;
63
64pub trait TimestampArg {
65 fn to_milliseconds_since_unix_epoch(self) -> MilliSecondsSinceUnixEpoch;
66}
67
68impl TimestampArg for MilliSecondsSinceUnixEpoch {
69 fn to_milliseconds_since_unix_epoch(self) -> MilliSecondsSinceUnixEpoch {
70 self
71 }
72}
73
74impl TimestampArg for u64 {
75 fn to_milliseconds_since_unix_epoch(self) -> MilliSecondsSinceUnixEpoch {
76 MilliSecondsSinceUnixEpoch(UInt::try_from(self).unwrap())
77 }
78}
79
80#[derive(Debug, Serialize)]
82struct RedactedBecause {
83 content: RoomRedactionEventContent,
85
86 event_id: OwnedEventId,
88
89 sender: OwnedUserId,
91
92 origin_server_ts: MilliSecondsSinceUnixEpoch,
95}
96
97#[derive(Debug, Serialize)]
98struct Unsigned<C: EventContent> {
99 #[serde(skip_serializing_if = "Option::is_none")]
100 prev_content: Option<C>,
101
102 #[serde(skip_serializing_if = "Option::is_none")]
103 transaction_id: Option<OwnedTransactionId>,
104
105 #[serde(rename = "m.relations", skip_serializing_if = "Option::is_none")]
106 relations: Option<BundledMessageLikeRelations<Raw<AnySyncTimelineEvent>>>,
107
108 #[serde(skip_serializing_if = "Option::is_none")]
109 redacted_because: Option<RedactedBecause>,
110}
111
112impl<C: EventContent> Default for Unsigned<C> {
114 fn default() -> Self {
115 Self { prev_content: None, transaction_id: None, relations: None, redacted_because: None }
116 }
117}
118
119#[derive(Debug)]
120pub struct EventBuilder<C: EventContent> {
121 sender: Option<OwnedUserId>,
122 is_ephemeral: bool,
125 room: Option<OwnedRoomId>,
126 event_id: Option<OwnedEventId>,
127 no_event_id: bool,
129 redacts: Option<OwnedEventId>,
130 content: C,
131 server_ts: MilliSecondsSinceUnixEpoch,
132 unsigned: Option<Unsigned<C>>,
133 state_key: Option<String>,
134}
135
136impl<E: EventContent> EventBuilder<E>
137where
138 E::EventType: Serialize,
139{
140 pub fn room(mut self, room_id: &RoomId) -> Self {
141 self.room = Some(room_id.to_owned());
142 self
143 }
144
145 pub fn sender(mut self, sender: &UserId) -> Self {
146 self.sender = Some(sender.to_owned());
147 self
148 }
149
150 pub fn event_id(mut self, event_id: &EventId) -> Self {
151 self.event_id = Some(event_id.to_owned());
152 self.no_event_id = false;
153 self
154 }
155
156 pub fn no_event_id(mut self) -> Self {
157 self.event_id = None;
158 self.no_event_id = true;
159 self
160 }
161
162 pub fn server_ts(mut self, ts: impl TimestampArg) -> Self {
163 self.server_ts = ts.to_milliseconds_since_unix_epoch();
164 self
165 }
166
167 pub fn unsigned_transaction_id(mut self, transaction_id: &TransactionId) -> Self {
168 self.unsigned.get_or_insert_with(Default::default).transaction_id =
169 Some(transaction_id.to_owned());
170 self
171 }
172
173 pub fn bundled_relations(
181 mut self,
182 relations: BundledMessageLikeRelations<Raw<AnySyncTimelineEvent>>,
183 ) -> Self {
184 self.unsigned.get_or_insert_with(Default::default).relations = Some(relations);
185 self
186 }
187
188 pub fn state_key(mut self, state_key: impl Into<String>) -> Self {
193 self.state_key = Some(state_key.into());
194 self
195 }
196
197 #[inline(always)]
198 fn construct_json(self, requires_room: bool) -> serde_json::Value {
199 let sender = self
202 .sender
203 .or_else(|| Some(self.unsigned.as_ref()?.redacted_because.as_ref()?.sender.clone()));
204
205 if sender.is_none() {
206 assert!(
207 self.is_ephemeral,
208 "the sender must be known when building the JSON for a non read-receipt event"
209 );
210 } else {
211 assert!(
212 !self.is_ephemeral,
213 "event builder set is_ephemeral, but also has a sender field"
214 );
215 }
216
217 let mut json = json!({
218 "type": self.content.event_type(),
219 "content": self.content,
220 "origin_server_ts": self.server_ts,
221 });
222
223 let map = json.as_object_mut().unwrap();
224
225 if let Some(sender) = sender {
226 map.insert("sender".to_owned(), json!(sender));
227 }
228
229 let event_id = self
230 .event_id
231 .or_else(|| {
232 self.room.as_ref().map(|room_id| EventId::new(room_id.server_name().unwrap()))
233 })
234 .or_else(|| (!self.no_event_id).then(|| EventId::new(server_name!("dummy.org"))));
235 if let Some(event_id) = event_id {
236 map.insert("event_id".to_owned(), json!(event_id));
237 }
238
239 if requires_room && !self.is_ephemeral {
240 let room_id = self.room.expect("TimelineEvent requires a room id");
241 map.insert("room_id".to_owned(), json!(room_id));
242 }
243
244 if let Some(redacts) = self.redacts {
245 map.insert("redacts".to_owned(), json!(redacts));
246 }
247
248 if let Some(unsigned) = self.unsigned {
249 map.insert("unsigned".to_owned(), json!(unsigned));
250 }
251
252 if let Some(state_key) = self.state_key {
253 map.insert("state_key".to_owned(), json!(state_key));
254 }
255
256 json
257 }
258
259 pub fn into_raw<T>(self) -> Raw<T> {
265 Raw::new(&self.construct_json(true)).unwrap().cast()
266 }
267
268 pub fn into_raw_timeline(self) -> Raw<AnyTimelineEvent> {
269 Raw::new(&self.construct_json(true)).unwrap().cast()
270 }
271
272 pub fn into_raw_sync(self) -> Raw<AnySyncTimelineEvent> {
273 Raw::new(&self.construct_json(false)).unwrap().cast()
274 }
275
276 pub fn into_event(self) -> TimelineEvent {
277 TimelineEvent::new(self.into_raw_sync())
278 }
279}
280
281impl EventBuilder<RoomEncryptedEventContent> {
282 pub fn into_utd_sync_timeline_event(self) -> TimelineEvent {
285 let session_id = as_variant!(&self.content.scheme, EncryptedEventScheme::MegolmV1AesSha2)
286 .map(|content| content.session_id.clone());
287
288 TimelineEvent::new_utd_event(
289 self.into(),
290 UnableToDecryptInfo {
291 session_id,
292 reason: UnableToDecryptReason::MissingMegolmSession { withheld_code: None },
293 },
294 )
295 }
296}
297
298impl EventBuilder<RoomMessageEventContent> {
299 pub fn reply_to(mut self, event_id: &EventId) -> Self {
301 self.content.relates_to =
302 Some(Relation::Reply { in_reply_to: InReplyTo::new(event_id.to_owned()) });
303 self
304 }
305
306 pub fn in_thread(mut self, root: &EventId, latest_thread_event: &EventId) -> Self {
309 self.content.relates_to =
310 Some(Relation::Thread(Thread::plain(root.to_owned(), latest_thread_event.to_owned())));
311 self
312 }
313
314 pub fn edit(
317 mut self,
318 edited_event_id: &EventId,
319 new_content: RoomMessageEventContentWithoutRelation,
320 ) -> Self {
321 self.content.relates_to =
322 Some(Relation::Replacement(Replacement::new(edited_event_id.to_owned(), new_content)));
323 self
324 }
325
326 pub fn caption(
330 mut self,
331 caption: Option<String>,
332 formatted_caption: Option<FormattedBody>,
333 ) -> Self {
334 match &mut self.content.msgtype {
335 MessageType::Image(image) => {
336 let filename = image.filename().to_owned();
337 if let Some(caption) = caption {
338 image.body = caption;
339 image.filename = Some(filename);
340 } else {
341 image.body = filename;
342 image.filename = None;
343 }
344 image.formatted = formatted_caption;
345 }
346
347 MessageType::Audio(_) | MessageType::Video(_) | MessageType::File(_) => {
348 unimplemented!();
349 }
350
351 _ => panic!("unexpected event type for a caption"),
352 }
353
354 self
355 }
356}
357
358impl<E: EventContent> From<EventBuilder<E>> for Raw<AnySyncTimelineEvent>
359where
360 E::EventType: Serialize,
361{
362 fn from(val: EventBuilder<E>) -> Self {
363 val.into_raw_sync()
364 }
365}
366
367impl<E: EventContent> From<EventBuilder<E>> for Raw<AnyTimelineEvent>
368where
369 E::EventType: Serialize,
370{
371 fn from(val: EventBuilder<E>) -> Self {
372 val.into_raw_timeline()
373 }
374}
375
376impl<E: EventContent> From<EventBuilder<E>> for TimelineEvent
377where
378 E::EventType: Serialize,
379{
380 fn from(val: EventBuilder<E>) -> Self {
381 val.into_event()
382 }
383}
384
385#[derive(Debug, Default)]
386pub struct EventFactory {
387 next_ts: AtomicU64,
388 sender: Option<OwnedUserId>,
389 room: Option<OwnedRoomId>,
390}
391
392impl EventFactory {
393 pub fn new() -> Self {
394 Self { next_ts: AtomicU64::new(0), sender: None, room: None }
395 }
396
397 pub fn room(mut self, room_id: &RoomId) -> Self {
398 self.room = Some(room_id.to_owned());
399 self
400 }
401
402 pub fn sender(mut self, sender: &UserId) -> Self {
403 self.sender = Some(sender.to_owned());
404 self
405 }
406
407 fn next_server_ts(&self) -> MilliSecondsSinceUnixEpoch {
408 MilliSecondsSinceUnixEpoch(
409 self.next_ts
410 .fetch_add(1, SeqCst)
411 .try_into()
412 .expect("server timestamp should fit in js_int::UInt"),
413 )
414 }
415
416 pub fn event<E: EventContent>(&self, content: E) -> EventBuilder<E> {
418 EventBuilder {
419 sender: self.sender.clone(),
420 is_ephemeral: false,
421 room: self.room.clone(),
422 server_ts: self.next_server_ts(),
423 event_id: None,
424 no_event_id: false,
425 redacts: None,
426 content,
427 unsigned: None,
428 state_key: None,
429 }
430 }
431
432 pub fn text_msg(&self, content: impl Into<String>) -> EventBuilder<RoomMessageEventContent> {
434 self.event(RoomMessageEventContent::text_plain(content.into()))
435 }
436
437 pub fn emote(&self, content: impl Into<String>) -> EventBuilder<RoomMessageEventContent> {
439 self.event(RoomMessageEventContent::emote_plain(content.into()))
440 }
441
442 pub fn member(&self, member: &UserId) -> EventBuilder<RoomMemberEventContent> {
472 let mut event = self.event(RoomMemberEventContent::new(MembershipState::Join));
473
474 if self.sender.is_some() {
475 event.sender = self.sender.clone();
476 } else {
477 event.sender = Some(member.to_owned());
478 }
479
480 event.state_key = Some(member.to_string());
481
482 event
483 }
484
485 pub fn room_tombstone(
487 &self,
488 body: impl Into<String>,
489 replacement: &RoomId,
490 ) -> EventBuilder<RoomTombstoneEventContent> {
491 let mut event =
492 self.event(RoomTombstoneEventContent::new(body.into(), replacement.to_owned()));
493 event.state_key = Some("".to_owned());
494 event
495 }
496
497 pub fn room_topic(&self, topic: impl Into<String>) -> EventBuilder<RoomTopicEventContent> {
499 let mut event = self.event(RoomTopicEventContent::new(topic.into()));
500 event.state_key = Some("".to_owned());
502 event
503 }
504
505 pub fn room_name(&self, name: impl Into<String>) -> EventBuilder<RoomNameEventContent> {
507 let mut event = self.event(RoomNameEventContent::new(name.into()));
508 event.state_key = Some("".to_owned());
510 event
511 }
512
513 pub fn room_avatar(&self) -> EventBuilder<RoomAvatarEventContent> {
515 let mut event = self.event(RoomAvatarEventContent::new());
516 event.state_key = Some("".to_owned());
518 event
519 }
520
521 pub fn member_hints(
542 &self,
543 service_members: BTreeSet<OwnedUserId>,
544 ) -> EventBuilder<MemberHintsEventContent> {
545 self.event(MemberHintsEventContent::new(service_members)).state_key("")
547 }
548
549 pub fn text_html(
551 &self,
552 plain: impl Into<String>,
553 html: impl Into<String>,
554 ) -> EventBuilder<RoomMessageEventContent> {
555 self.event(RoomMessageEventContent::text_html(plain, html))
556 }
557
558 pub fn notice(&self, content: impl Into<String>) -> EventBuilder<RoomMessageEventContent> {
560 self.event(RoomMessageEventContent::notice_plain(content))
561 }
562
563 pub fn reaction(
565 &self,
566 event_id: &EventId,
567 annotation: impl Into<String>,
568 ) -> EventBuilder<ReactionEventContent> {
569 self.event(ReactionEventContent::new(Annotation::new(
570 event_id.to_owned(),
571 annotation.into(),
572 )))
573 }
574
575 pub fn redaction(&self, event_id: &EventId) -> EventBuilder<RoomRedactionEventContent> {
580 let mut builder = self.event(RoomRedactionEventContent::new_v11(event_id.to_owned()));
581 builder.redacts = Some(event_id.to_owned());
582 builder
583 }
584
585 pub fn redacted<T: RedactedMessageLikeEventContent>(
588 &self,
589 redacter: &UserId,
590 content: T,
591 ) -> EventBuilder<T> {
592 let mut builder = self.event(content);
593
594 let redacted_because = RedactedBecause {
595 content: RoomRedactionEventContent::default(),
596 event_id: EventId::new(server_name!("dummy.server")),
597 sender: redacter.to_owned(),
598 origin_server_ts: self.next_server_ts(),
599 };
600 builder.unsigned.get_or_insert_with(Default::default).redacted_because =
601 Some(redacted_because);
602
603 builder
604 }
605
606 pub fn redacted_state<T: RedactedStateEventContent>(
609 &self,
610 redacter: &UserId,
611 state_key: impl Into<String>,
612 content: T,
613 ) -> EventBuilder<T> {
614 let mut builder = self.event(content);
615
616 let redacted_because = RedactedBecause {
617 content: RoomRedactionEventContent::default(),
618 event_id: EventId::new(server_name!("dummy.server")),
619 sender: redacter.to_owned(),
620 origin_server_ts: self.next_server_ts(),
621 };
622 builder.unsigned.get_or_insert_with(Default::default).redacted_because =
623 Some(redacted_because);
624 builder.state_key = Some(state_key.into());
625
626 builder
627 }
628
629 pub fn poll_start(
632 &self,
633 content: impl Into<String>,
634 poll_question: impl Into<String>,
635 answers: Vec<impl Into<String>>,
636 ) -> EventBuilder<UnstablePollStartEventContent> {
637 let answers: Vec<UnstablePollAnswer> = answers
639 .into_iter()
640 .enumerate()
641 .map(|(idx, answer)| UnstablePollAnswer::new(idx.to_string(), answer))
642 .collect();
643 let poll_answers = answers.try_into().unwrap();
644 let poll_start_content =
645 UnstablePollStartEventContent::New(NewUnstablePollStartEventContent::plain_text(
646 content,
647 UnstablePollStartContentBlock::new(poll_question, poll_answers),
648 ));
649 self.event(poll_start_content)
650 }
651
652 pub fn poll_edit(
654 &self,
655 edited_event_id: &EventId,
656 poll_question: impl Into<String>,
657 answers: Vec<impl Into<String>>,
658 ) -> EventBuilder<ReplacementUnstablePollStartEventContent> {
659 let answers: Vec<UnstablePollAnswer> = answers
661 .into_iter()
662 .enumerate()
663 .map(|(idx, answer)| UnstablePollAnswer::new(idx.to_string(), answer))
664 .collect();
665 let poll_answers = answers.try_into().unwrap();
666 let poll_start_content_block =
667 UnstablePollStartContentBlock::new(poll_question, poll_answers);
668 self.event(ReplacementUnstablePollStartEventContent::new(
669 poll_start_content_block,
670 edited_event_id.to_owned(),
671 ))
672 }
673
674 pub fn poll_response(
677 &self,
678 answers: Vec<impl Into<String>>,
679 poll_start_id: &EventId,
680 ) -> EventBuilder<UnstablePollResponseEventContent> {
681 self.event(UnstablePollResponseEventContent::new(
682 answers.into_iter().map(Into::into).collect(),
683 poll_start_id.to_owned(),
684 ))
685 }
686
687 pub fn poll_end(
690 &self,
691 content: impl Into<String>,
692 poll_start_id: &EventId,
693 ) -> EventBuilder<UnstablePollEndEventContent> {
694 self.event(UnstablePollEndEventContent::new(content.into(), poll_start_id.to_owned()))
695 }
696
697 pub fn image(
700 &self,
701 filename: String,
702 url: OwnedMxcUri,
703 ) -> EventBuilder<RoomMessageEventContent> {
704 let image_event_content = ImageMessageEventContent::plain(filename, url);
705 self.event(RoomMessageEventContent::new(MessageType::Image(image_event_content)))
706 }
707
708 pub fn typing(&self, user_ids: Vec<&UserId>) -> EventBuilder<TypingEventContent> {
710 let mut builder = self
711 .event(TypingEventContent::new(user_ids.into_iter().map(ToOwned::to_owned).collect()));
712 builder.is_ephemeral = true;
713 builder
714 }
715
716 pub fn read_receipts(&self) -> ReadReceiptBuilder<'_> {
718 ReadReceiptBuilder { factory: self, content: ReceiptEventContent(Default::default()) }
719 }
720
721 pub fn set_next_ts(&self, value: u64) {
725 self.next_ts.store(value, SeqCst);
726 }
727}
728
729impl EventBuilder<RoomMemberEventContent> {
730 pub fn membership(mut self, state: MembershipState) -> Self {
735 self.content.membership = state;
736 self
737 }
738
739 pub fn invited(mut self, invited_user: &UserId) -> Self {
742 assert_ne!(
743 self.sender.as_deref().unwrap(),
744 invited_user,
745 "invited user and sender can't be the same person"
746 );
747 self.content.membership = MembershipState::Invite;
748 self.state_key = Some(invited_user.to_string());
749 self
750 }
751
752 pub fn kicked(mut self, kicked_user: &UserId) -> Self {
755 assert_ne!(
756 self.sender.as_deref().unwrap(),
757 kicked_user,
758 "kicked user and sender can't be the same person, otherwise it's just a Leave"
759 );
760 self.content.membership = MembershipState::Leave;
761 self.state_key = Some(kicked_user.to_string());
762 self
763 }
764
765 pub fn banned(mut self, banned_user: &UserId) -> Self {
768 assert_ne!(
769 self.sender.as_deref().unwrap(),
770 banned_user,
771 "a user can't ban itself" );
773 self.content.membership = MembershipState::Ban;
774 self.state_key = Some(banned_user.to_string());
775 self
776 }
777
778 pub fn display_name(mut self, display_name: impl Into<String>) -> Self {
780 self.content.displayname = Some(display_name.into());
781 self
782 }
783
784 pub fn avatar_url(mut self, url: &MxcUri) -> Self {
786 self.content.avatar_url = Some(url.to_owned());
787 self
788 }
789
790 pub fn reason(mut self, reason: impl Into<String>) -> Self {
792 self.content.reason = Some(reason.into());
793 self
794 }
795
796 pub fn previous(mut self, previous: impl Into<PreviousMembership>) -> Self {
798 let previous = previous.into();
799
800 let mut prev_content = RoomMemberEventContent::new(previous.state);
801 if let Some(avatar_url) = previous.avatar_url {
802 prev_content.avatar_url = Some(avatar_url);
803 }
804 if let Some(display_name) = previous.display_name {
805 prev_content.displayname = Some(display_name);
806 }
807
808 self.unsigned.get_or_insert_with(Default::default).prev_content = Some(prev_content);
809 self
810 }
811}
812
813impl EventBuilder<RoomAvatarEventContent> {
814 pub fn url(mut self, url: &MxcUri) -> Self {
816 self.content.url = Some(url.to_owned());
817 self
818 }
819
820 pub fn info(mut self, image: avatar::ImageInfo) -> Self {
822 self.content.info = Some(Box::new(image));
823 self
824 }
825}
826
827pub struct ReadReceiptBuilder<'a> {
828 factory: &'a EventFactory,
829 content: ReceiptEventContent,
830}
831
832impl ReadReceiptBuilder<'_> {
833 pub fn add(
835 self,
836 event_id: &EventId,
837 user_id: &UserId,
838 tyype: ReceiptType,
839 thread: ReceiptThread,
840 ) -> Self {
841 let ts = self.factory.next_server_ts();
842 self.add_with_timestamp(event_id, user_id, tyype, thread, Some(ts))
843 }
844
845 pub fn add_with_timestamp(
847 mut self,
848 event_id: &EventId,
849 user_id: &UserId,
850 tyype: ReceiptType,
851 thread: ReceiptThread,
852 ts: Option<MilliSecondsSinceUnixEpoch>,
853 ) -> Self {
854 let by_event = self.content.0.entry(event_id.to_owned()).or_default();
855 let by_type = by_event.entry(tyype).or_default();
856
857 let mut receipt = Receipt::default();
858 if let Some(ts) = ts {
859 receipt.ts = Some(ts);
860 }
861 receipt.thread = thread;
862
863 by_type.insert(user_id.to_owned(), receipt);
864 self
865 }
866
867 pub fn into_content(self) -> ReceiptEventContent {
869 self.content
870 }
871
872 pub fn into_event(self) -> EventBuilder<ReceiptEventContent> {
874 let mut builder = self.factory.event(self.into_content());
875 builder.is_ephemeral = true;
876 builder
877 }
878}
879
880pub struct PreviousMembership {
881 state: MembershipState,
882 avatar_url: Option<OwnedMxcUri>,
883 display_name: Option<String>,
884}
885
886impl PreviousMembership {
887 pub fn new(state: MembershipState) -> Self {
888 Self { state, avatar_url: None, display_name: None }
889 }
890
891 pub fn avatar_url(mut self, url: &MxcUri) -> Self {
892 self.avatar_url = Some(url.to_owned());
893 self
894 }
895
896 pub fn display_name(mut self, name: impl Into<String>) -> Self {
897 self.display_name = Some(name.into());
898 self
899 }
900}
901
902impl From<MembershipState> for PreviousMembership {
903 fn from(state: MembershipState) -> Self {
904 Self::new(state)
905 }
906}