matrix_sdk_test/
event_factory.rs

1// Copyright 2024 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![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        AnySyncTimelineEvent, AnyTimelineEvent, BundledMessageLikeRelations, EventContent,
54        RedactedMessageLikeEventContent, RedactedStateEventContent,
55    },
56    serde::Raw,
57    server_name, EventId, MilliSecondsSinceUnixEpoch, MxcUri, OwnedEventId, OwnedMxcUri,
58    OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UInt, UserId,
59};
60use serde::Serialize;
61use serde_json::json;
62
63pub trait TimestampArg {
64    fn to_milliseconds_since_unix_epoch(self) -> MilliSecondsSinceUnixEpoch;
65}
66
67impl TimestampArg for MilliSecondsSinceUnixEpoch {
68    fn to_milliseconds_since_unix_epoch(self) -> MilliSecondsSinceUnixEpoch {
69        self
70    }
71}
72
73impl TimestampArg for u64 {
74    fn to_milliseconds_since_unix_epoch(self) -> MilliSecondsSinceUnixEpoch {
75        MilliSecondsSinceUnixEpoch(UInt::try_from(self).unwrap())
76    }
77}
78
79/// A thin copy of [`ruma::events::UnsignedRoomRedactionEvent`].
80#[derive(Debug, Serialize)]
81struct RedactedBecause {
82    /// Data specific to the event type.
83    content: RoomRedactionEventContent,
84
85    /// The globally unique event identifier for the user who sent the event.
86    event_id: OwnedEventId,
87
88    /// The fully-qualified ID of the user who sent this event.
89    sender: OwnedUserId,
90
91    /// Timestamp in milliseconds on originating homeserver when this event was
92    /// sent.
93    origin_server_ts: MilliSecondsSinceUnixEpoch,
94}
95
96#[derive(Debug, Serialize)]
97struct Unsigned<C: EventContent> {
98    #[serde(skip_serializing_if = "Option::is_none")]
99    prev_content: Option<C>,
100
101    #[serde(skip_serializing_if = "Option::is_none")]
102    transaction_id: Option<OwnedTransactionId>,
103
104    #[serde(rename = "m.relations", skip_serializing_if = "Option::is_none")]
105    relations: Option<BundledMessageLikeRelations<Raw<AnySyncTimelineEvent>>>,
106
107    #[serde(skip_serializing_if = "Option::is_none")]
108    redacted_because: Option<RedactedBecause>,
109}
110
111// rustc can't derive Default because C isn't marked as `Default` 🤔 oh well.
112impl<C: EventContent> Default for Unsigned<C> {
113    fn default() -> Self {
114        Self { prev_content: None, transaction_id: None, relations: None, redacted_because: None }
115    }
116}
117
118#[derive(Debug)]
119pub struct EventBuilder<C: EventContent> {
120    sender: Option<OwnedUserId>,
121    room: Option<OwnedRoomId>,
122    event_id: Option<OwnedEventId>,
123    /// Whether the event should *not* have an event id. False by default.
124    no_event_id: bool,
125    redacts: Option<OwnedEventId>,
126    content: C,
127    server_ts: MilliSecondsSinceUnixEpoch,
128    unsigned: Option<Unsigned<C>>,
129    state_key: Option<String>,
130}
131
132impl<E: EventContent> EventBuilder<E>
133where
134    E::EventType: Serialize,
135{
136    pub fn room(mut self, room_id: &RoomId) -> Self {
137        self.room = Some(room_id.to_owned());
138        self
139    }
140
141    pub fn sender(mut self, sender: &UserId) -> Self {
142        self.sender = Some(sender.to_owned());
143        self
144    }
145
146    pub fn event_id(mut self, event_id: &EventId) -> Self {
147        self.event_id = Some(event_id.to_owned());
148        self.no_event_id = false;
149        self
150    }
151
152    pub fn no_event_id(mut self) -> Self {
153        self.event_id = None;
154        self.no_event_id = true;
155        self
156    }
157
158    pub fn server_ts(mut self, ts: impl TimestampArg) -> Self {
159        self.server_ts = ts.to_milliseconds_since_unix_epoch();
160        self
161    }
162
163    pub fn unsigned_transaction_id(mut self, transaction_id: &TransactionId) -> Self {
164        self.unsigned.get_or_insert_with(Default::default).transaction_id =
165            Some(transaction_id.to_owned());
166        self
167    }
168
169    /// Adds bundled relations to this event.
170    ///
171    /// Ideally, we'd type-check that an event passed as a relation is the same
172    /// type as this one, but it's not trivial to do so because this builder
173    /// is only generic on the event's *content*, not the event type itself;
174    /// doing so would require many changes, and this is testing code after
175    /// all.
176    pub fn bundled_relations(
177        mut self,
178        relations: BundledMessageLikeRelations<Raw<AnySyncTimelineEvent>>,
179    ) -> Self {
180        self.unsigned.get_or_insert_with(Default::default).relations = Some(relations);
181        self
182    }
183
184    /// For state events manually created, define the state key.
185    ///
186    /// For other state events created in the [`EventFactory`], this is
187    /// automatically filled upon creation or update of the events.
188    pub fn state_key(mut self, state_key: impl Into<String>) -> Self {
189        self.state_key = Some(state_key.into());
190        self
191    }
192
193    #[inline(always)]
194    fn construct_json(self, requires_room: bool) -> serde_json::Value {
195        // Use the `sender` preferably, or resort to the `redacted_because` sender if
196        // none has been set.
197        let sender = self
198            .sender
199            .or_else(|| Some(self.unsigned.as_ref()?.redacted_because.as_ref()?.sender.clone()))
200            .expect("we should have a sender user id at this point");
201
202        let mut json = json!({
203            "type": self.content.event_type(),
204            "content": self.content,
205            "sender": sender,
206            "origin_server_ts": self.server_ts,
207        });
208
209        let map = json.as_object_mut().unwrap();
210
211        let event_id = self
212            .event_id
213            .or_else(|| {
214                self.room.as_ref().map(|room_id| EventId::new(room_id.server_name().unwrap()))
215            })
216            .or_else(|| (!self.no_event_id).then(|| EventId::new(server_name!("dummy.org"))));
217        if let Some(event_id) = event_id {
218            map.insert("event_id".to_owned(), json!(event_id));
219        }
220
221        if requires_room {
222            let room_id = self.room.expect("TimelineEvent requires a room id");
223            map.insert("room_id".to_owned(), json!(room_id));
224        }
225
226        if let Some(redacts) = self.redacts {
227            map.insert("redacts".to_owned(), json!(redacts));
228        }
229
230        if let Some(unsigned) = self.unsigned {
231            map.insert("unsigned".to_owned(), json!(unsigned));
232        }
233
234        if let Some(state_key) = self.state_key {
235            map.insert("state_key".to_owned(), json!(state_key));
236        }
237
238        json
239    }
240
241    /// Build an event from the [`EventBuilder`] and convert it into a
242    /// serialized and [`Raw`] event.
243    ///
244    /// The generic argument `T` allows you to automatically cast the [`Raw`]
245    /// event into any desired type.
246    pub fn into_raw<T>(self) -> Raw<T> {
247        Raw::new(&self.construct_json(true)).unwrap().cast()
248    }
249
250    pub fn into_raw_timeline(self) -> Raw<AnyTimelineEvent> {
251        Raw::new(&self.construct_json(true)).unwrap().cast()
252    }
253
254    pub fn into_raw_sync(self) -> Raw<AnySyncTimelineEvent> {
255        Raw::new(&self.construct_json(false)).unwrap().cast()
256    }
257
258    pub fn into_event(self) -> TimelineEvent {
259        TimelineEvent::new(self.into_raw_sync())
260    }
261}
262
263impl EventBuilder<RoomEncryptedEventContent> {
264    /// Turn this event into a [`TimelineEvent`] representing a decryption
265    /// failure
266    pub fn into_utd_sync_timeline_event(self) -> TimelineEvent {
267        let session_id = as_variant!(&self.content.scheme, EncryptedEventScheme::MegolmV1AesSha2)
268            .map(|content| content.session_id.clone());
269
270        TimelineEvent::new_utd_event(
271            self.into(),
272            UnableToDecryptInfo {
273                session_id,
274                reason: UnableToDecryptReason::MissingMegolmSession { withheld_code: None },
275            },
276        )
277    }
278}
279
280impl EventBuilder<RoomMessageEventContent> {
281    /// Adds a reply relation to the current event.
282    pub fn reply_to(mut self, event_id: &EventId) -> Self {
283        self.content.relates_to =
284            Some(Relation::Reply { in_reply_to: InReplyTo::new(event_id.to_owned()) });
285        self
286    }
287
288    /// Adds a thread relation to the root event, setting the latest thread
289    /// event id too.
290    pub fn in_thread(mut self, root: &EventId, latest_thread_event: &EventId) -> Self {
291        self.content.relates_to =
292            Some(Relation::Thread(Thread::plain(root.to_owned(), latest_thread_event.to_owned())));
293        self
294    }
295
296    /// Adds a replacement relation to the current event, with the new content
297    /// passed.
298    pub fn edit(
299        mut self,
300        edited_event_id: &EventId,
301        new_content: RoomMessageEventContentWithoutRelation,
302    ) -> Self {
303        self.content.relates_to =
304            Some(Relation::Replacement(Replacement::new(edited_event_id.to_owned(), new_content)));
305        self
306    }
307
308    /// Adds a caption to a media event.
309    ///
310    /// Will crash if the event isn't a media room message.
311    pub fn caption(
312        mut self,
313        caption: Option<String>,
314        formatted_caption: Option<FormattedBody>,
315    ) -> Self {
316        match &mut self.content.msgtype {
317            MessageType::Image(image) => {
318                let filename = image.filename().to_owned();
319                if let Some(caption) = caption {
320                    image.body = caption;
321                    image.filename = Some(filename);
322                } else {
323                    image.body = filename;
324                    image.filename = None;
325                }
326                image.formatted = formatted_caption;
327            }
328
329            MessageType::Audio(_) | MessageType::Video(_) | MessageType::File(_) => {
330                unimplemented!();
331            }
332
333            _ => panic!("unexpected event type for a caption"),
334        }
335
336        self
337    }
338}
339
340impl<E: EventContent> From<EventBuilder<E>> for Raw<AnySyncTimelineEvent>
341where
342    E::EventType: Serialize,
343{
344    fn from(val: EventBuilder<E>) -> Self {
345        val.into_raw_sync()
346    }
347}
348
349impl<E: EventContent> From<EventBuilder<E>> for Raw<AnyTimelineEvent>
350where
351    E::EventType: Serialize,
352{
353    fn from(val: EventBuilder<E>) -> Self {
354        val.into_raw_timeline()
355    }
356}
357
358impl<E: EventContent> From<EventBuilder<E>> for TimelineEvent
359where
360    E::EventType: Serialize,
361{
362    fn from(val: EventBuilder<E>) -> Self {
363        val.into_event()
364    }
365}
366
367#[derive(Debug, Default)]
368pub struct EventFactory {
369    next_ts: AtomicU64,
370    sender: Option<OwnedUserId>,
371    room: Option<OwnedRoomId>,
372}
373
374impl EventFactory {
375    pub fn new() -> Self {
376        Self { next_ts: AtomicU64::new(0), sender: None, room: None }
377    }
378
379    pub fn room(mut self, room_id: &RoomId) -> Self {
380        self.room = Some(room_id.to_owned());
381        self
382    }
383
384    pub fn sender(mut self, sender: &UserId) -> Self {
385        self.sender = Some(sender.to_owned());
386        self
387    }
388
389    fn next_server_ts(&self) -> MilliSecondsSinceUnixEpoch {
390        MilliSecondsSinceUnixEpoch(
391            self.next_ts
392                .fetch_add(1, SeqCst)
393                .try_into()
394                .expect("server timestamp should fit in js_int::UInt"),
395        )
396    }
397
398    /// Create an event from any event content.
399    pub fn event<E: EventContent>(&self, content: E) -> EventBuilder<E> {
400        EventBuilder {
401            sender: self.sender.clone(),
402            room: self.room.clone(),
403            server_ts: self.next_server_ts(),
404            event_id: None,
405            no_event_id: false,
406            redacts: None,
407            content,
408            unsigned: None,
409            state_key: None,
410        }
411    }
412
413    /// Create a new plain text `m.room.message`.
414    pub fn text_msg(&self, content: impl Into<String>) -> EventBuilder<RoomMessageEventContent> {
415        self.event(RoomMessageEventContent::text_plain(content.into()))
416    }
417
418    /// Create a new plain emote `m.room.message`.
419    pub fn emote(&self, content: impl Into<String>) -> EventBuilder<RoomMessageEventContent> {
420        self.event(RoomMessageEventContent::emote_plain(content.into()))
421    }
422
423    /// Create a new `m.room.member` event for the given member.
424    ///
425    /// The given member will be used as the `sender` as well as the `state_key`
426    /// of the `m.room.member` event, unless the `sender` was already using
427    /// [`EventFactory::sender()`], in that case only the state key will be
428    /// set to the given `member`.
429    ///
430    /// The `membership` field of the content is set to
431    /// [`MembershipState::Join`].
432    ///
433    /// ```
434    /// use matrix_sdk_test::event_factory::EventFactory;
435    /// use ruma::{
436    ///     events::{
437    ///         room::member::{MembershipState, RoomMemberEventContent},
438    ///         SyncStateEvent,
439    ///     },
440    ///     room_id,
441    ///     serde::Raw,
442    ///     user_id,
443    /// };
444    ///
445    /// let factory = EventFactory::new().room(room_id!("!test:localhost"));
446    ///
447    /// let event: Raw<SyncStateEvent<RoomMemberEventContent>> = factory
448    ///     .member(user_id!("@alice:localhost"))
449    ///     .display_name("Alice")
450    ///     .into_raw();
451    /// ```
452    pub fn member(&self, member: &UserId) -> EventBuilder<RoomMemberEventContent> {
453        let mut event = self.event(RoomMemberEventContent::new(MembershipState::Join));
454
455        if self.sender.is_some() {
456            event.sender = self.sender.clone();
457        } else {
458            event.sender = Some(member.to_owned());
459        }
460
461        event.state_key = Some(member.to_string());
462
463        event
464    }
465
466    /// Create a tombstone state event for the room.
467    pub fn room_tombstone(
468        &self,
469        body: impl Into<String>,
470        replacement: &RoomId,
471    ) -> EventBuilder<RoomTombstoneEventContent> {
472        let mut event =
473            self.event(RoomTombstoneEventContent::new(body.into(), replacement.to_owned()));
474        event.state_key = Some("".to_owned());
475        event
476    }
477
478    /// Create a state event for the topic.
479    pub fn room_topic(&self, topic: impl Into<String>) -> EventBuilder<RoomTopicEventContent> {
480        let mut event = self.event(RoomTopicEventContent::new(topic.into()));
481        // The state key is empty for a room topic state event.
482        event.state_key = Some("".to_owned());
483        event
484    }
485
486    /// Create a state event for the room name.
487    pub fn room_name(&self, name: impl Into<String>) -> EventBuilder<RoomNameEventContent> {
488        let mut event = self.event(RoomNameEventContent::new(name.into()));
489        // The state key is empty for a room name state event.
490        event.state_key = Some("".to_owned());
491        event
492    }
493
494    /// Create an empty state event for the room avatar.
495    pub fn room_avatar(&self) -> EventBuilder<RoomAvatarEventContent> {
496        let mut event = self.event(RoomAvatarEventContent::new());
497        // The state key is empty for a room avatar state event.
498        event.state_key = Some("".to_owned());
499        event
500    }
501
502    /// Create a new `m.member_hints` event with the given service members.
503    ///
504    /// ```
505    /// use std::collections::BTreeSet;
506    ///
507    /// use matrix_sdk_test::event_factory::EventFactory;
508    /// use ruma::{
509    ///     events::{member_hints::MemberHintsEventContent, SyncStateEvent},
510    ///     owned_user_id, room_id,
511    ///     serde::Raw,
512    ///     user_id,
513    /// };
514    ///
515    /// let factory = EventFactory::new().room(room_id!("!test:localhost"));
516    ///
517    /// let event: Raw<SyncStateEvent<MemberHintsEventContent>> = factory
518    ///     .member_hints(BTreeSet::from([owned_user_id!("@alice:localhost")]))
519    ///     .sender(user_id!("@alice:localhost"))
520    ///     .into_raw();
521    /// ```
522    pub fn member_hints(
523        &self,
524        service_members: BTreeSet<OwnedUserId>,
525    ) -> EventBuilder<MemberHintsEventContent> {
526        // The `m.member_hints` event always has an empty state key, so let's set it.
527        self.event(MemberHintsEventContent::new(service_members)).state_key("")
528    }
529
530    /// Create a new plain/html `m.room.message`.
531    pub fn text_html(
532        &self,
533        plain: impl Into<String>,
534        html: impl Into<String>,
535    ) -> EventBuilder<RoomMessageEventContent> {
536        self.event(RoomMessageEventContent::text_html(plain, html))
537    }
538
539    /// Create a new plain notice `m.room.message`.
540    pub fn notice(&self, content: impl Into<String>) -> EventBuilder<RoomMessageEventContent> {
541        self.event(RoomMessageEventContent::notice_plain(content))
542    }
543
544    /// Add a reaction to an event.
545    pub fn reaction(
546        &self,
547        event_id: &EventId,
548        annotation: impl Into<String>,
549    ) -> EventBuilder<ReactionEventContent> {
550        self.event(ReactionEventContent::new(Annotation::new(
551            event_id.to_owned(),
552            annotation.into(),
553        )))
554    }
555
556    /// Create a live redaction for the given event id.
557    ///
558    /// Note: this is not a redacted event, but a redaction event, that will
559    /// cause another event to be redacted.
560    pub fn redaction(&self, event_id: &EventId) -> EventBuilder<RoomRedactionEventContent> {
561        let mut builder = self.event(RoomRedactionEventContent::new_v11(event_id.to_owned()));
562        builder.redacts = Some(event_id.to_owned());
563        builder
564    }
565
566    /// Create a redacted event, with extra information in the unsigned section
567    /// about the redaction itself.
568    pub fn redacted<T: RedactedMessageLikeEventContent>(
569        &self,
570        redacter: &UserId,
571        content: T,
572    ) -> EventBuilder<T> {
573        let mut builder = self.event(content);
574
575        let redacted_because = RedactedBecause {
576            content: RoomRedactionEventContent::default(),
577            event_id: EventId::new(server_name!("dummy.server")),
578            sender: redacter.to_owned(),
579            origin_server_ts: self.next_server_ts(),
580        };
581        builder.unsigned.get_or_insert_with(Default::default).redacted_because =
582            Some(redacted_because);
583
584        builder
585    }
586
587    /// Create a redacted state event, with extra information in the unsigned
588    /// section about the redaction itself.
589    pub fn redacted_state<T: RedactedStateEventContent>(
590        &self,
591        redacter: &UserId,
592        state_key: impl Into<String>,
593        content: T,
594    ) -> EventBuilder<T> {
595        let mut builder = self.event(content);
596
597        let redacted_because = RedactedBecause {
598            content: RoomRedactionEventContent::default(),
599            event_id: EventId::new(server_name!("dummy.server")),
600            sender: redacter.to_owned(),
601            origin_server_ts: self.next_server_ts(),
602        };
603        builder.unsigned.get_or_insert_with(Default::default).redacted_because =
604            Some(redacted_because);
605        builder.state_key = Some(state_key.into());
606
607        builder
608    }
609
610    /// Create a poll start event given a text, the question and the possible
611    /// answers.
612    pub fn poll_start(
613        &self,
614        content: impl Into<String>,
615        poll_question: impl Into<String>,
616        answers: Vec<impl Into<String>>,
617    ) -> EventBuilder<UnstablePollStartEventContent> {
618        // PollAnswers 'constructor' is not public, so we need to deserialize them
619        let answers: Vec<UnstablePollAnswer> = answers
620            .into_iter()
621            .enumerate()
622            .map(|(idx, answer)| UnstablePollAnswer::new(idx.to_string(), answer))
623            .collect();
624        let poll_answers = answers.try_into().unwrap();
625        let poll_start_content =
626            UnstablePollStartEventContent::New(NewUnstablePollStartEventContent::plain_text(
627                content,
628                UnstablePollStartContentBlock::new(poll_question, poll_answers),
629            ));
630        self.event(poll_start_content)
631    }
632
633    /// Create a poll edit event given the new question and possible answers.
634    pub fn poll_edit(
635        &self,
636        edited_event_id: &EventId,
637        poll_question: impl Into<String>,
638        answers: Vec<impl Into<String>>,
639    ) -> EventBuilder<ReplacementUnstablePollStartEventContent> {
640        // PollAnswers 'constructor' is not public, so we need to deserialize them
641        let answers: Vec<UnstablePollAnswer> = answers
642            .into_iter()
643            .enumerate()
644            .map(|(idx, answer)| UnstablePollAnswer::new(idx.to_string(), answer))
645            .collect();
646        let poll_answers = answers.try_into().unwrap();
647        let poll_start_content_block =
648            UnstablePollStartContentBlock::new(poll_question, poll_answers);
649        self.event(ReplacementUnstablePollStartEventContent::new(
650            poll_start_content_block,
651            edited_event_id.to_owned(),
652        ))
653    }
654
655    /// Create a poll response with the given answer id and the associated poll
656    /// start event id.
657    pub fn poll_response(
658        &self,
659        answers: Vec<impl Into<String>>,
660        poll_start_id: &EventId,
661    ) -> EventBuilder<UnstablePollResponseEventContent> {
662        self.event(UnstablePollResponseEventContent::new(
663            answers.into_iter().map(Into::into).collect(),
664            poll_start_id.to_owned(),
665        ))
666    }
667
668    /// Create a poll response with the given text and the associated poll start
669    /// event id.
670    pub fn poll_end(
671        &self,
672        content: impl Into<String>,
673        poll_start_id: &EventId,
674    ) -> EventBuilder<UnstablePollEndEventContent> {
675        self.event(UnstablePollEndEventContent::new(content.into(), poll_start_id.to_owned()))
676    }
677
678    /// Creates a plain (unencrypted) image event content referencing the given
679    /// MXC ID.
680    pub fn image(
681        &self,
682        filename: String,
683        url: OwnedMxcUri,
684    ) -> EventBuilder<RoomMessageEventContent> {
685        let image_event_content = ImageMessageEventContent::plain(filename, url);
686        self.event(RoomMessageEventContent::new(MessageType::Image(image_event_content)))
687    }
688
689    /// Create a read receipt event.
690    pub fn read_receipts(&self) -> ReadReceiptBuilder<'_> {
691        ReadReceiptBuilder { factory: self, content: ReceiptEventContent(Default::default()) }
692    }
693
694    /// Set the next server timestamp.
695    ///
696    /// Timestamps will continue to increase by 1 (millisecond) from that value.
697    pub fn set_next_ts(&self, value: u64) {
698        self.next_ts.store(value, SeqCst);
699    }
700}
701
702impl EventBuilder<RoomMemberEventContent> {
703    /// Set the `membership` of the `m.room.member` event to the given
704    /// [`MembershipState`].
705    ///
706    /// The default is [`MembershipState::Join`].
707    pub fn membership(mut self, state: MembershipState) -> Self {
708        self.content.membership = state;
709        self
710    }
711
712    /// Set that the sender of this event invited the user passed as a parameter
713    /// here.
714    pub fn invited(mut self, invited_user: &UserId) -> Self {
715        assert_ne!(
716            self.sender.as_deref().unwrap(),
717            invited_user,
718            "invited user and sender can't be the same person"
719        );
720        self.content.membership = MembershipState::Invite;
721        self.state_key = Some(invited_user.to_string());
722        self
723    }
724
725    /// Set that the sender of this event kicked the user passed as a parameter
726    /// here.
727    pub fn kicked(mut self, kicked_user: &UserId) -> Self {
728        assert_ne!(
729            self.sender.as_deref().unwrap(),
730            kicked_user,
731            "kicked user and sender can't be the same person, otherwise it's just a Leave"
732        );
733        self.content.membership = MembershipState::Leave;
734        self.state_key = Some(kicked_user.to_string());
735        self
736    }
737
738    /// Set that the sender of this event banned the user passed as a parameter
739    /// here.
740    pub fn banned(mut self, banned_user: &UserId) -> Self {
741        assert_ne!(
742            self.sender.as_deref().unwrap(),
743            banned_user,
744            "a user can't ban itself" // hopefully
745        );
746        self.content.membership = MembershipState::Ban;
747        self.state_key = Some(banned_user.to_string());
748        self
749    }
750
751    /// Set the display name of the `m.room.member` event.
752    pub fn display_name(mut self, display_name: impl Into<String>) -> Self {
753        self.content.displayname = Some(display_name.into());
754        self
755    }
756
757    /// Set the avatar URL of the `m.room.member` event.
758    pub fn avatar_url(mut self, url: &MxcUri) -> Self {
759        self.content.avatar_url = Some(url.to_owned());
760        self
761    }
762
763    /// Set the reason field of the `m.room.member` event.
764    pub fn reason(mut self, reason: impl Into<String>) -> Self {
765        self.content.reason = Some(reason.into());
766        self
767    }
768
769    /// Set the previous membership state (in the unsigned section).
770    pub fn previous(mut self, previous: impl Into<PreviousMembership>) -> Self {
771        let previous = previous.into();
772
773        let mut prev_content = RoomMemberEventContent::new(previous.state);
774        if let Some(avatar_url) = previous.avatar_url {
775            prev_content.avatar_url = Some(avatar_url);
776        }
777        if let Some(display_name) = previous.display_name {
778            prev_content.displayname = Some(display_name);
779        }
780
781        self.unsigned.get_or_insert_with(Default::default).prev_content = Some(prev_content);
782        self
783    }
784}
785
786impl EventBuilder<RoomAvatarEventContent> {
787    /// Defines the URL for the room avatar.
788    pub fn url(mut self, url: &MxcUri) -> Self {
789        self.content.url = Some(url.to_owned());
790        self
791    }
792
793    /// Defines the image info for the avatar.
794    pub fn info(mut self, image: avatar::ImageInfo) -> Self {
795        self.content.info = Some(Box::new(image));
796        self
797    }
798}
799
800pub struct ReadReceiptBuilder<'a> {
801    factory: &'a EventFactory,
802    content: ReceiptEventContent,
803}
804
805impl ReadReceiptBuilder<'_> {
806    /// Add a single read receipt to the event.
807    pub fn add(
808        mut self,
809        event_id: &EventId,
810        user_id: &UserId,
811        tyype: ReceiptType,
812        thread: ReceiptThread,
813    ) -> Self {
814        let by_event = self.content.0.entry(event_id.to_owned()).or_default();
815        let by_type = by_event.entry(tyype).or_default();
816
817        let mut receipt = Receipt::new(self.factory.next_server_ts());
818        receipt.thread = thread;
819
820        by_type.insert(user_id.to_owned(), receipt);
821        self
822    }
823
824    /// Finalize the builder into the receipt event content.
825    pub fn build(self) -> ReceiptEventContent {
826        self.content
827    }
828}
829
830pub struct PreviousMembership {
831    state: MembershipState,
832    avatar_url: Option<OwnedMxcUri>,
833    display_name: Option<String>,
834}
835
836impl PreviousMembership {
837    pub fn new(state: MembershipState) -> Self {
838        Self { state, avatar_url: None, display_name: None }
839    }
840
841    pub fn avatar_url(mut self, url: &MxcUri) -> Self {
842        self.avatar_url = Some(url.to_owned());
843        self
844    }
845
846    pub fn display_name(mut self, name: impl Into<String>) -> Self {
847        self.display_name = Some(name.into());
848        self
849    }
850}
851
852impl From<MembershipState> for PreviousMembership {
853    fn from(state: MembershipState) -> Self {
854        Self::new(state)
855    }
856}