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::{BTreeMap, BTreeSet},
19    sync::atomic::{AtomicU64, Ordering::SeqCst},
20    time::Duration,
21};
22
23use as_variant::as_variant;
24use matrix_sdk_common::deserialized_responses::{
25    TimelineEvent, UnableToDecryptInfo, UnableToDecryptReason,
26};
27use ruma::{
28    EventId, Int, MilliSecondsSinceUnixEpoch, MxcUri, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId,
29    OwnedRoomId, OwnedTransactionId, OwnedUserId, OwnedVoipId, RoomId, RoomVersionId,
30    TransactionId, UInt, UserId, VoipVersionId,
31    events::{
32        AnyGlobalAccountDataEvent, AnyStateEvent, AnySyncMessageLikeEvent, AnySyncStateEvent,
33        AnySyncTimelineEvent, AnyTimelineEvent, BundledMessageLikeRelations, False,
34        GlobalAccountDataEventContent, Mentions, RedactedMessageLikeEventContent,
35        RedactedStateEventContent, StateEventContent, StaticEventContent,
36        beacon::BeaconEventContent,
37        call::{SessionDescription, invite::CallInviteEventContent},
38        direct::{DirectEventContent, OwnedDirectUserIdentifier},
39        ignored_user_list::IgnoredUserListEventContent,
40        member_hints::MemberHintsEventContent,
41        poll::{
42            unstable_end::UnstablePollEndEventContent,
43            unstable_response::UnstablePollResponseEventContent,
44            unstable_start::{
45                NewUnstablePollStartEventContent, ReplacementUnstablePollStartEventContent,
46                UnstablePollAnswer, UnstablePollStartContentBlock, UnstablePollStartEventContent,
47            },
48        },
49        push_rules::PushRulesEventContent,
50        reaction::ReactionEventContent,
51        receipt::{Receipt, ReceiptEventContent, ReceiptThread, ReceiptType},
52        relation::{Annotation, BundledThread, InReplyTo, Reference, Replacement, Thread},
53        room::{
54            ImageInfo,
55            avatar::{self, RoomAvatarEventContent},
56            canonical_alias::RoomCanonicalAliasEventContent,
57            create::{PreviousRoom, RoomCreateEventContent},
58            encrypted::{EncryptedEventScheme, RoomEncryptedEventContent},
59            member::{MembershipState, RoomMemberEventContent},
60            message::{
61                FormattedBody, GalleryItemType, GalleryMessageEventContent,
62                ImageMessageEventContent, MessageType, OriginalSyncRoomMessageEvent, Relation,
63                RelationWithoutReplacement, RoomMessageEventContent,
64                RoomMessageEventContentWithoutRelation,
65            },
66            name::RoomNameEventContent,
67            power_levels::RoomPowerLevelsEventContent,
68            redaction::RoomRedactionEventContent,
69            server_acl::RoomServerAclEventContent,
70            tombstone::RoomTombstoneEventContent,
71            topic::RoomTopicEventContent,
72        },
73        rtc::{
74            decline::RtcDeclineEventContent,
75            notification::{NotificationType, RtcNotificationEventContent},
76        },
77        space::{child::SpaceChildEventContent, parent::SpaceParentEventContent},
78        sticker::StickerEventContent,
79        typing::TypingEventContent,
80    },
81    push::Ruleset,
82    room::RoomType,
83    room_version_rules::AuthorizationRules,
84    serde::Raw,
85    server_name,
86};
87use serde::Serialize;
88use serde_json::json;
89
90pub trait TimestampArg {
91    fn to_milliseconds_since_unix_epoch(self) -> MilliSecondsSinceUnixEpoch;
92}
93
94impl TimestampArg for MilliSecondsSinceUnixEpoch {
95    fn to_milliseconds_since_unix_epoch(self) -> MilliSecondsSinceUnixEpoch {
96        self
97    }
98}
99
100impl TimestampArg for u64 {
101    fn to_milliseconds_since_unix_epoch(self) -> MilliSecondsSinceUnixEpoch {
102        MilliSecondsSinceUnixEpoch(UInt::try_from(self).unwrap())
103    }
104}
105
106/// A thin copy of [`ruma::events::UnsignedRoomRedactionEvent`].
107#[derive(Debug, Serialize)]
108struct RedactedBecause {
109    /// Data specific to the event type.
110    content: RoomRedactionEventContent,
111
112    /// The globally unique event identifier for the user who sent the event.
113    event_id: OwnedEventId,
114
115    /// The fully-qualified ID of the user who sent this event.
116    sender: OwnedUserId,
117
118    /// Timestamp in milliseconds on originating homeserver when this event was
119    /// sent.
120    origin_server_ts: MilliSecondsSinceUnixEpoch,
121}
122
123#[derive(Debug, Serialize)]
124struct Unsigned<C: StaticEventContent> {
125    #[serde(skip_serializing_if = "Option::is_none")]
126    prev_content: Option<C>,
127
128    #[serde(skip_serializing_if = "Option::is_none")]
129    transaction_id: Option<OwnedTransactionId>,
130
131    #[serde(rename = "m.relations", skip_serializing_if = "Option::is_none")]
132    relations: Option<BundledMessageLikeRelations<Raw<AnySyncTimelineEvent>>>,
133
134    #[serde(skip_serializing_if = "Option::is_none")]
135    redacted_because: Option<RedactedBecause>,
136
137    #[serde(skip_serializing_if = "Option::is_none")]
138    age: Option<Int>,
139}
140
141// rustc can't derive Default because C isn't marked as `Default` 🤔 oh well.
142impl<C: StaticEventContent> Default for Unsigned<C> {
143    fn default() -> Self {
144        Self {
145            prev_content: None,
146            transaction_id: None,
147            relations: None,
148            redacted_because: None,
149            age: None,
150        }
151    }
152}
153
154#[derive(Debug)]
155pub struct EventBuilder<C: StaticEventContent<IsPrefix = False>> {
156    sender: Option<OwnedUserId>,
157    /// Whether the event is an ephemeral one. As such, it doesn't require a
158    /// room id or a sender.
159    is_ephemeral: bool,
160    /// Whether the event is global account data. As such, it doesn't require a
161    /// room id.
162    is_global: bool,
163    room: Option<OwnedRoomId>,
164    event_id: Option<OwnedEventId>,
165    /// Whether the event should *not* have an event id. False by default.
166    no_event_id: bool,
167    redacts: Option<OwnedEventId>,
168    content: C,
169    server_ts: MilliSecondsSinceUnixEpoch,
170    unsigned: Option<Unsigned<C>>,
171    state_key: Option<String>,
172}
173
174impl<E: StaticEventContent<IsPrefix = False>> EventBuilder<E> {
175    pub fn room(mut self, room_id: &RoomId) -> Self {
176        self.room = Some(room_id.to_owned());
177        self
178    }
179
180    pub fn sender(mut self, sender: &UserId) -> Self {
181        self.sender = Some(sender.to_owned());
182        self
183    }
184
185    pub fn event_id(mut self, event_id: &EventId) -> Self {
186        self.event_id = Some(event_id.to_owned());
187        self.no_event_id = false;
188        self
189    }
190
191    pub fn no_event_id(mut self) -> Self {
192        self.event_id = None;
193        self.no_event_id = true;
194        self
195    }
196
197    pub fn server_ts(mut self, ts: impl TimestampArg) -> Self {
198        self.server_ts = ts.to_milliseconds_since_unix_epoch();
199        self
200    }
201
202    pub fn unsigned_transaction_id(mut self, transaction_id: &TransactionId) -> Self {
203        self.unsigned.get_or_insert_with(Default::default).transaction_id =
204            Some(transaction_id.to_owned());
205        self
206    }
207
208    /// Add age to unsigned data in this event.
209    pub fn age(mut self, age: impl Into<Int>) -> Self {
210        self.unsigned.get_or_insert_with(Default::default).age = Some(age.into());
211        self
212    }
213
214    /// Create a bundled thread summary in the unsigned bundled relations of
215    /// this event.
216    pub fn with_bundled_thread_summary(
217        mut self,
218        latest_event: Raw<AnySyncMessageLikeEvent>,
219        count: usize,
220        current_user_participated: bool,
221    ) -> Self {
222        let relations = self
223            .unsigned
224            .get_or_insert_with(Default::default)
225            .relations
226            .get_or_insert_with(BundledMessageLikeRelations::new);
227        relations.thread = Some(Box::new(BundledThread::new(
228            latest_event,
229            UInt::try_from(count).unwrap(),
230            current_user_participated,
231        )));
232        self
233    }
234
235    /// Create a bundled edit in the unsigned bundled relations of this event.
236    pub fn with_bundled_edit(mut self, replacement: impl Into<Raw<AnySyncTimelineEvent>>) -> Self {
237        let relations = self
238            .unsigned
239            .get_or_insert_with(Default::default)
240            .relations
241            .get_or_insert_with(BundledMessageLikeRelations::new);
242        relations.replace = Some(Box::new(replacement.into()));
243        self
244    }
245
246    /// For state events manually created, define the state key.
247    ///
248    /// For other state events created in the [`EventFactory`], this is
249    /// automatically filled upon creation or update of the events.
250    pub fn state_key(mut self, state_key: impl Into<String>) -> Self {
251        self.state_key = Some(state_key.into());
252        self
253    }
254}
255
256impl<E> EventBuilder<E>
257where
258    E: StaticEventContent<IsPrefix = False> + Serialize,
259{
260    #[inline(always)]
261    fn construct_json(self, requires_room: bool) -> serde_json::Value {
262        // Use the `sender` preferably, or resort to the `redacted_because` sender if
263        // none has been set.
264        let sender = self
265            .sender
266            .or_else(|| Some(self.unsigned.as_ref()?.redacted_because.as_ref()?.sender.clone()));
267
268        let mut json = json!({
269            "type": E::TYPE,
270            "content": self.content,
271            "origin_server_ts": self.server_ts,
272        });
273
274        let map = json.as_object_mut().unwrap();
275
276        if let Some(sender) = sender {
277            if !self.is_ephemeral && !self.is_global {
278                map.insert("sender".to_owned(), json!(sender));
279            }
280        } else {
281            assert!(
282                self.is_ephemeral || self.is_global,
283                "the sender must be known when building the JSON for a non read-receipt or global event"
284            );
285        }
286
287        let event_id = self
288            .event_id
289            .or_else(|| {
290                self.room.as_ref().map(|room_id| EventId::new(room_id.server_name().unwrap()))
291            })
292            .or_else(|| (!self.no_event_id).then(|| EventId::new(server_name!("dummy.org"))));
293        if let Some(event_id) = event_id {
294            map.insert("event_id".to_owned(), json!(event_id));
295        }
296
297        if requires_room && !self.is_ephemeral && !self.is_global {
298            let room_id = self.room.expect("TimelineEvent requires a room id");
299            map.insert("room_id".to_owned(), json!(room_id));
300        }
301
302        if let Some(redacts) = self.redacts {
303            map.insert("redacts".to_owned(), json!(redacts));
304        }
305
306        if let Some(unsigned) = self.unsigned {
307            map.insert("unsigned".to_owned(), json!(unsigned));
308        }
309
310        if let Some(state_key) = self.state_key {
311            map.insert("state_key".to_owned(), json!(state_key));
312        }
313
314        json
315    }
316
317    /// Build an event from the [`EventBuilder`] and convert it into a
318    /// serialized and [`Raw`] event.
319    ///
320    /// The generic argument `T` allows you to automatically cast the [`Raw`]
321    /// event into any desired type.
322    pub fn into_raw<T>(self) -> Raw<T> {
323        Raw::new(&self.construct_json(true)).unwrap().cast_unchecked()
324    }
325
326    pub fn into_raw_timeline(self) -> Raw<AnyTimelineEvent> {
327        self.into_raw()
328    }
329
330    pub fn into_any_sync_message_like_event(self) -> AnySyncMessageLikeEvent {
331        self.into_raw().deserialize().expect("expected message like event")
332    }
333
334    pub fn into_original_sync_room_message_event(self) -> OriginalSyncRoomMessageEvent {
335        self.into_raw().deserialize().expect("expected original sync room message event")
336    }
337
338    pub fn into_raw_sync(self) -> Raw<AnySyncTimelineEvent> {
339        Raw::new(&self.construct_json(false)).unwrap().cast_unchecked()
340    }
341
342    pub fn into_event(self) -> TimelineEvent {
343        TimelineEvent::from_plaintext(self.into_raw_sync())
344    }
345}
346
347impl EventBuilder<RoomEncryptedEventContent> {
348    /// Turn this event into a [`TimelineEvent`] representing a decryption
349    /// failure
350    pub fn into_utd_sync_timeline_event(self) -> TimelineEvent {
351        let session_id = as_variant!(&self.content.scheme, EncryptedEventScheme::MegolmV1AesSha2)
352            .map(|content| content.session_id.clone());
353
354        TimelineEvent::from_utd(
355            self.into(),
356            UnableToDecryptInfo {
357                session_id,
358                reason: UnableToDecryptReason::MissingMegolmSession { withheld_code: None },
359            },
360        )
361    }
362}
363
364impl EventBuilder<RoomMessageEventContent> {
365    /// Adds a reply relation to the current event.
366    pub fn reply_to(mut self, event_id: &EventId) -> Self {
367        self.content.relates_to =
368            Some(Relation::Reply { in_reply_to: InReplyTo::new(event_id.to_owned()) });
369        self
370    }
371
372    /// Adds a thread relation to the root event, setting the reply fallback to
373    /// the latest in-thread event.
374    pub fn in_thread(mut self, root: &EventId, latest_thread_event: &EventId) -> Self {
375        self.content.relates_to =
376            Some(Relation::Thread(Thread::plain(root.to_owned(), latest_thread_event.to_owned())));
377        self
378    }
379
380    /// Adds a thread relation to the root event, that's a non-fallback reply to
381    /// another thread event.
382    pub fn in_thread_reply(mut self, root: &EventId, replied_to: &EventId) -> Self {
383        self.content.relates_to =
384            Some(Relation::Thread(Thread::reply(root.to_owned(), replied_to.to_owned())));
385        self
386    }
387
388    /// Adds the given mentions to the current event.
389    pub fn mentions(mut self, mentions: Mentions) -> Self {
390        self.content.mentions = Some(mentions);
391        self
392    }
393
394    /// Adds a replacement relation to the current event, with the new content
395    /// passed.
396    pub fn edit(
397        mut self,
398        edited_event_id: &EventId,
399        new_content: RoomMessageEventContentWithoutRelation,
400    ) -> Self {
401        self.content.relates_to =
402            Some(Relation::Replacement(Replacement::new(edited_event_id.to_owned(), new_content)));
403        self
404    }
405
406    /// Adds a caption to a media event.
407    ///
408    /// Will crash if the event isn't a media room message.
409    pub fn caption(
410        mut self,
411        caption: Option<String>,
412        formatted_caption: Option<FormattedBody>,
413    ) -> Self {
414        match &mut self.content.msgtype {
415            MessageType::Image(image) => {
416                let filename = image.filename().to_owned();
417                if let Some(caption) = caption {
418                    image.body = caption;
419                    image.filename = Some(filename);
420                } else {
421                    image.body = filename;
422                    image.filename = None;
423                }
424                image.formatted = formatted_caption;
425            }
426
427            MessageType::Audio(_) | MessageType::Video(_) | MessageType::File(_) => {
428                unimplemented!();
429            }
430
431            _ => panic!("unexpected event type for a caption"),
432        }
433
434        self
435    }
436}
437
438impl EventBuilder<UnstablePollStartEventContent> {
439    /// Adds a reply relation to the current event.
440    pub fn reply_to(mut self, event_id: &EventId) -> Self {
441        if let UnstablePollStartEventContent::New(content) = &mut self.content {
442            content.relates_to = Some(RelationWithoutReplacement::Reply {
443                in_reply_to: InReplyTo::new(event_id.to_owned()),
444            });
445        }
446        self
447    }
448
449    /// Adds a thread relation to the root event, setting the reply to
450    /// event id as well.
451    pub fn in_thread(mut self, root: &EventId, reply_to_event_id: &EventId) -> Self {
452        let thread = Thread::reply(root.to_owned(), reply_to_event_id.to_owned());
453
454        if let UnstablePollStartEventContent::New(content) = &mut self.content {
455            content.relates_to = Some(RelationWithoutReplacement::Thread(thread));
456        }
457        self
458    }
459}
460
461impl EventBuilder<RoomCreateEventContent> {
462    /// Define the predecessor fields.
463    pub fn predecessor(mut self, room_id: &RoomId) -> Self {
464        self.content.predecessor = Some(PreviousRoom::new(room_id.to_owned()));
465        self
466    }
467
468    /// Erase the predecessor if any.
469    pub fn no_predecessor(mut self) -> Self {
470        self.content.predecessor = None;
471        self
472    }
473
474    /// Sets the `m.room.create` `type` field to `m.space`.
475    pub fn with_space_type(mut self) -> Self {
476        self.content.room_type = Some(RoomType::Space);
477        self
478    }
479}
480
481impl EventBuilder<StickerEventContent> {
482    /// Add reply [`Thread`] relation to root event and set replied-to event id.
483    pub fn reply_thread(mut self, root: &EventId, reply_to_event: &EventId) -> Self {
484        self.content.relates_to =
485            Some(Relation::Thread(Thread::reply(root.to_owned(), reply_to_event.to_owned())));
486        self
487    }
488}
489
490impl<E: StaticEventContent<IsPrefix = False>> From<EventBuilder<E>> for Raw<AnySyncTimelineEvent>
491where
492    E: Serialize,
493{
494    fn from(val: EventBuilder<E>) -> Self {
495        val.into_raw_sync()
496    }
497}
498
499impl<E: StaticEventContent<IsPrefix = False>> From<EventBuilder<E>> for Raw<AnyTimelineEvent>
500where
501    E: Serialize,
502{
503    fn from(val: EventBuilder<E>) -> Self {
504        val.into_raw_timeline()
505    }
506}
507
508impl<E: StaticEventContent<IsPrefix = False>> From<EventBuilder<E>>
509    for Raw<AnyGlobalAccountDataEvent>
510where
511    E: Serialize,
512{
513    fn from(val: EventBuilder<E>) -> Self {
514        val.into_raw()
515    }
516}
517
518impl<E: StaticEventContent<IsPrefix = False>> From<EventBuilder<E>> for TimelineEvent
519where
520    E: Serialize,
521{
522    fn from(val: EventBuilder<E>) -> Self {
523        val.into_event()
524    }
525}
526
527impl<E: StaticEventContent<IsPrefix = False> + StateEventContent> From<EventBuilder<E>>
528    for Raw<AnySyncStateEvent>
529{
530    fn from(val: EventBuilder<E>) -> Self {
531        Raw::new(&val.construct_json(false)).unwrap().cast_unchecked()
532    }
533}
534
535impl<E: StaticEventContent<IsPrefix = False> + StateEventContent> From<EventBuilder<E>>
536    for Raw<AnyStateEvent>
537{
538    fn from(val: EventBuilder<E>) -> Self {
539        Raw::new(&val.construct_json(true)).unwrap().cast_unchecked()
540    }
541}
542
543#[derive(Debug, Default)]
544pub struct EventFactory {
545    next_ts: AtomicU64,
546    sender: Option<OwnedUserId>,
547    room: Option<OwnedRoomId>,
548}
549
550impl EventFactory {
551    pub fn new() -> Self {
552        Self { next_ts: AtomicU64::new(0), sender: None, room: None }
553    }
554
555    pub fn room(mut self, room_id: &RoomId) -> Self {
556        self.room = Some(room_id.to_owned());
557        self
558    }
559
560    pub fn sender(mut self, sender: &UserId) -> Self {
561        self.sender = Some(sender.to_owned());
562        self
563    }
564
565    pub fn server_ts(self, ts: u64) -> Self {
566        self.next_ts.store(ts, SeqCst);
567        self
568    }
569
570    fn next_server_ts(&self) -> MilliSecondsSinceUnixEpoch {
571        MilliSecondsSinceUnixEpoch(
572            self.next_ts
573                .fetch_add(1, SeqCst)
574                .try_into()
575                .expect("server timestamp should fit in js_int::UInt"),
576        )
577    }
578
579    /// Create an event from any event content.
580    pub fn event<E: StaticEventContent<IsPrefix = False>>(&self, content: E) -> EventBuilder<E> {
581        EventBuilder {
582            sender: self.sender.clone(),
583            is_ephemeral: false,
584            is_global: false,
585            room: self.room.clone(),
586            server_ts: self.next_server_ts(),
587            event_id: None,
588            no_event_id: false,
589            redacts: None,
590            content,
591            unsigned: None,
592            state_key: None,
593        }
594    }
595
596    /// Create a new plain text `m.room.message`.
597    pub fn text_msg(&self, content: impl Into<String>) -> EventBuilder<RoomMessageEventContent> {
598        self.event(RoomMessageEventContent::text_plain(content.into()))
599    }
600
601    /// Create a new plain emote `m.room.message`.
602    pub fn emote(&self, content: impl Into<String>) -> EventBuilder<RoomMessageEventContent> {
603        self.event(RoomMessageEventContent::emote_plain(content.into()))
604    }
605
606    /// Create a new `m.room.member` event for the given member.
607    ///
608    /// The given member will be used as the `sender` as well as the `state_key`
609    /// of the `m.room.member` event, unless the `sender` was already using
610    /// [`EventFactory::sender()`], in that case only the state key will be
611    /// set to the given `member`.
612    ///
613    /// The `membership` field of the content is set to
614    /// [`MembershipState::Join`].
615    ///
616    /// ```
617    /// use matrix_sdk_test::event_factory::EventFactory;
618    /// use ruma::{
619    ///     events::{
620    ///         SyncStateEvent,
621    ///         room::member::{MembershipState, RoomMemberEventContent},
622    ///     },
623    ///     room_id,
624    ///     serde::Raw,
625    ///     user_id,
626    /// };
627    ///
628    /// let factory = EventFactory::new().room(room_id!("!test:localhost"));
629    ///
630    /// let event: Raw<SyncStateEvent<RoomMemberEventContent>> = factory
631    ///     .member(user_id!("@alice:localhost"))
632    ///     .display_name("Alice")
633    ///     .into_raw();
634    /// ```
635    pub fn member(&self, member: &UserId) -> EventBuilder<RoomMemberEventContent> {
636        let mut event = self.event(RoomMemberEventContent::new(MembershipState::Join));
637
638        if self.sender.is_some() {
639            event.sender = self.sender.clone();
640        } else {
641            event.sender = Some(member.to_owned());
642        }
643
644        event.state_key = Some(member.to_string());
645
646        event
647    }
648
649    /// Create a tombstone state event for the room.
650    pub fn room_tombstone(
651        &self,
652        body: impl Into<String>,
653        replacement: &RoomId,
654    ) -> EventBuilder<RoomTombstoneEventContent> {
655        let mut event =
656            self.event(RoomTombstoneEventContent::new(body.into(), replacement.to_owned()));
657        event.state_key = Some("".to_owned());
658        event
659    }
660
661    /// Create a state event for the topic.
662    pub fn room_topic(&self, topic: impl Into<String>) -> EventBuilder<RoomTopicEventContent> {
663        let mut event = self.event(RoomTopicEventContent::new(topic.into()));
664        // The state key is empty for a room topic state event.
665        event.state_key = Some("".to_owned());
666        event
667    }
668
669    /// Create a state event for the room name.
670    pub fn room_name(&self, name: impl Into<String>) -> EventBuilder<RoomNameEventContent> {
671        let mut event = self.event(RoomNameEventContent::new(name.into()));
672        // The state key is empty for a room name state event.
673        event.state_key = Some("".to_owned());
674        event
675    }
676
677    /// Create an empty state event for the room avatar.
678    pub fn room_avatar(&self) -> EventBuilder<RoomAvatarEventContent> {
679        let mut event = self.event(RoomAvatarEventContent::new());
680        // The state key is empty for a room avatar state event.
681        event.state_key = Some("".to_owned());
682        event
683    }
684
685    /// Create a new `m.member_hints` event with the given service members.
686    ///
687    /// ```
688    /// use std::collections::BTreeSet;
689    ///
690    /// use matrix_sdk_test::event_factory::EventFactory;
691    /// use ruma::{
692    ///     events::{SyncStateEvent, member_hints::MemberHintsEventContent},
693    ///     owned_user_id, room_id,
694    ///     serde::Raw,
695    ///     user_id,
696    /// };
697    ///
698    /// let factory = EventFactory::new().room(room_id!("!test:localhost"));
699    ///
700    /// let event: Raw<SyncStateEvent<MemberHintsEventContent>> = factory
701    ///     .member_hints(BTreeSet::from([owned_user_id!("@alice:localhost")]))
702    ///     .sender(user_id!("@alice:localhost"))
703    ///     .into_raw();
704    /// ```
705    pub fn member_hints(
706        &self,
707        service_members: BTreeSet<OwnedUserId>,
708    ) -> EventBuilder<MemberHintsEventContent> {
709        // The `m.member_hints` event always has an empty state key, so let's set it.
710        self.event(MemberHintsEventContent::new(service_members)).state_key("")
711    }
712
713    /// Create a new plain/html `m.room.message`.
714    pub fn text_html(
715        &self,
716        plain: impl Into<String>,
717        html: impl Into<String>,
718    ) -> EventBuilder<RoomMessageEventContent> {
719        self.event(RoomMessageEventContent::text_html(plain, html))
720    }
721
722    /// Create a new plain notice `m.room.message`.
723    pub fn notice(&self, content: impl Into<String>) -> EventBuilder<RoomMessageEventContent> {
724        self.event(RoomMessageEventContent::notice_plain(content))
725    }
726
727    /// Add a reaction to an event.
728    pub fn reaction(
729        &self,
730        event_id: &EventId,
731        annotation: impl Into<String>,
732    ) -> EventBuilder<ReactionEventContent> {
733        self.event(ReactionEventContent::new(Annotation::new(
734            event_id.to_owned(),
735            annotation.into(),
736        )))
737    }
738
739    /// Create a live redaction for the given event id.
740    ///
741    /// Note: this is not a redacted event, but a redaction event, that will
742    /// cause another event to be redacted.
743    pub fn redaction(&self, event_id: &EventId) -> EventBuilder<RoomRedactionEventContent> {
744        let mut builder = self.event(RoomRedactionEventContent::new_v11(event_id.to_owned()));
745        builder.redacts = Some(event_id.to_owned());
746        builder
747    }
748
749    /// Create a redacted event, with extra information in the unsigned section
750    /// about the redaction itself.
751    pub fn redacted<T: StaticEventContent<IsPrefix = False> + RedactedMessageLikeEventContent>(
752        &self,
753        redacter: &UserId,
754        content: T,
755    ) -> EventBuilder<T> {
756        let mut builder = self.event(content);
757
758        let redacted_because = RedactedBecause {
759            content: RoomRedactionEventContent::default(),
760            event_id: EventId::new(server_name!("dummy.server")),
761            sender: redacter.to_owned(),
762            origin_server_ts: self.next_server_ts(),
763        };
764        builder.unsigned.get_or_insert_with(Default::default).redacted_because =
765            Some(redacted_because);
766
767        builder
768    }
769
770    /// Create a redacted state event, with extra information in the unsigned
771    /// section about the redaction itself.
772    pub fn redacted_state<T: StaticEventContent<IsPrefix = False> + RedactedStateEventContent>(
773        &self,
774        redacter: &UserId,
775        state_key: impl Into<String>,
776        content: T,
777    ) -> EventBuilder<T> {
778        let mut builder = self.event(content);
779
780        let redacted_because = RedactedBecause {
781            content: RoomRedactionEventContent::default(),
782            event_id: EventId::new(server_name!("dummy.server")),
783            sender: redacter.to_owned(),
784            origin_server_ts: self.next_server_ts(),
785        };
786        builder.unsigned.get_or_insert_with(Default::default).redacted_because =
787            Some(redacted_because);
788        builder.state_key = Some(state_key.into());
789
790        builder
791    }
792
793    /// Create a poll start event given a text, the question and the possible
794    /// answers.
795    pub fn poll_start(
796        &self,
797        content: impl Into<String>,
798        poll_question: impl Into<String>,
799        answers: Vec<impl Into<String>>,
800    ) -> EventBuilder<UnstablePollStartEventContent> {
801        // PollAnswers 'constructor' is not public, so we need to deserialize them
802        let answers: Vec<UnstablePollAnswer> = answers
803            .into_iter()
804            .enumerate()
805            .map(|(idx, answer)| UnstablePollAnswer::new(idx.to_string(), answer))
806            .collect();
807        let poll_answers = answers.try_into().unwrap();
808        let poll_start_content =
809            UnstablePollStartEventContent::New(NewUnstablePollStartEventContent::plain_text(
810                content,
811                UnstablePollStartContentBlock::new(poll_question, poll_answers),
812            ));
813        self.event(poll_start_content)
814    }
815
816    /// Create a poll edit event given the new question and possible answers.
817    pub fn poll_edit(
818        &self,
819        edited_event_id: &EventId,
820        poll_question: impl Into<String>,
821        answers: Vec<impl Into<String>>,
822    ) -> EventBuilder<ReplacementUnstablePollStartEventContent> {
823        // PollAnswers 'constructor' is not public, so we need to deserialize them
824        let answers: Vec<UnstablePollAnswer> = answers
825            .into_iter()
826            .enumerate()
827            .map(|(idx, answer)| UnstablePollAnswer::new(idx.to_string(), answer))
828            .collect();
829        let poll_answers = answers.try_into().unwrap();
830        let poll_start_content_block =
831            UnstablePollStartContentBlock::new(poll_question, poll_answers);
832        self.event(ReplacementUnstablePollStartEventContent::new(
833            poll_start_content_block,
834            edited_event_id.to_owned(),
835        ))
836    }
837
838    /// Create a poll response with the given answer id and the associated poll
839    /// start event id.
840    pub fn poll_response(
841        &self,
842        answers: Vec<impl Into<String>>,
843        poll_start_id: &EventId,
844    ) -> EventBuilder<UnstablePollResponseEventContent> {
845        self.event(UnstablePollResponseEventContent::new(
846            answers.into_iter().map(Into::into).collect(),
847            poll_start_id.to_owned(),
848        ))
849    }
850
851    /// Create a poll response with the given text and the associated poll start
852    /// event id.
853    pub fn poll_end(
854        &self,
855        content: impl Into<String>,
856        poll_start_id: &EventId,
857    ) -> EventBuilder<UnstablePollEndEventContent> {
858        self.event(UnstablePollEndEventContent::new(content.into(), poll_start_id.to_owned()))
859    }
860
861    /// Creates a plain (unencrypted) image event content referencing the given
862    /// MXC ID.
863    pub fn image(
864        &self,
865        filename: String,
866        url: OwnedMxcUri,
867    ) -> EventBuilder<RoomMessageEventContent> {
868        let image_event_content = ImageMessageEventContent::plain(filename, url);
869        self.event(RoomMessageEventContent::new(MessageType::Image(image_event_content)))
870    }
871
872    /// Create a gallery event containing a single plain (unencrypted) image
873    /// referencing the given MXC ID.
874    pub fn gallery(
875        &self,
876        body: String,
877        filename: String,
878        url: OwnedMxcUri,
879    ) -> EventBuilder<RoomMessageEventContent> {
880        let gallery_event_content = GalleryMessageEventContent::new(
881            body,
882            None,
883            vec![GalleryItemType::Image(ImageMessageEventContent::plain(filename, url))],
884        );
885        self.event(RoomMessageEventContent::new(MessageType::Gallery(gallery_event_content)))
886    }
887
888    /// Create a typing notification event.
889    pub fn typing(&self, user_ids: Vec<&UserId>) -> EventBuilder<TypingEventContent> {
890        let mut builder = self
891            .event(TypingEventContent::new(user_ids.into_iter().map(ToOwned::to_owned).collect()));
892        builder.is_ephemeral = true;
893        builder
894    }
895
896    /// Create a read receipt event.
897    pub fn read_receipts(&self) -> ReadReceiptBuilder<'_> {
898        ReadReceiptBuilder { factory: self, content: ReceiptEventContent(Default::default()) }
899    }
900
901    /// Create a new `m.room.create` event.
902    pub fn create(
903        &self,
904        creator_user_id: &UserId,
905        room_version: RoomVersionId,
906    ) -> EventBuilder<RoomCreateEventContent> {
907        let mut event = self.event(RoomCreateEventContent::new_v1(creator_user_id.to_owned()));
908        event.content.room_version = room_version;
909
910        if self.sender.is_some() {
911            event.sender = self.sender.clone();
912        } else {
913            event.sender = Some(creator_user_id.to_owned());
914        }
915
916        event.state_key = Some("".to_owned());
917
918        event
919    }
920
921    /// Create a new `m.room.power_levels` event.
922    pub fn power_levels(
923        &self,
924        map: &mut BTreeMap<OwnedUserId, Int>,
925    ) -> EventBuilder<RoomPowerLevelsEventContent> {
926        let mut event = RoomPowerLevelsEventContent::new(&AuthorizationRules::V1);
927        event.users.append(map);
928        self.event(event)
929    }
930
931    /// Create a new `m.room.server_acl` event.
932    pub fn server_acl(
933        &self,
934        allow_ip_literals: bool,
935        allow: Vec<String>,
936        deny: Vec<String>,
937    ) -> EventBuilder<RoomServerAclEventContent> {
938        self.event(RoomServerAclEventContent::new(allow_ip_literals, allow, deny))
939    }
940
941    /// Create a new `m.room.canonical_alias` event.
942    pub fn canonical_alias(
943        &self,
944        alias: Option<OwnedRoomAliasId>,
945        alt_aliases: Vec<OwnedRoomAliasId>,
946    ) -> EventBuilder<RoomCanonicalAliasEventContent> {
947        let mut event = RoomCanonicalAliasEventContent::new();
948        event.alias = alias;
949        event.alt_aliases = alt_aliases;
950        self.event(event)
951    }
952
953    /// Create a new `org.matrix.msc3672.beacon` event.
954    ///
955    /// ```
956    /// use matrix_sdk_test::event_factory::EventFactory;
957    /// use ruma::{
958    ///     MilliSecondsSinceUnixEpoch,
959    ///     events::{MessageLikeEvent, beacon::BeaconEventContent},
960    ///     owned_event_id, room_id,
961    ///     serde::Raw,
962    ///     user_id,
963    /// };
964    ///
965    /// let factory = EventFactory::new().room(room_id!("!test:localhost"));
966    ///
967    /// let event: Raw<MessageLikeEvent<BeaconEventContent>> = factory
968    ///     .beacon(
969    ///         owned_event_id!("$123456789abc:localhost"),
970    ///         10.1,
971    ///         15.2,
972    ///         5,
973    ///         Some(MilliSecondsSinceUnixEpoch(1000u32.into())),
974    ///     )
975    ///     .sender(user_id!("@alice:localhost"))
976    ///     .into_raw();
977    /// ```
978    pub fn beacon(
979        &self,
980        beacon_info_event_id: OwnedEventId,
981        latitude: f64,
982        longitude: f64,
983        uncertainty: u32,
984        ts: Option<MilliSecondsSinceUnixEpoch>,
985    ) -> EventBuilder<BeaconEventContent> {
986        let geo_uri = format!("geo:{latitude},{longitude};u={uncertainty}");
987        self.event(BeaconEventContent::new(beacon_info_event_id, geo_uri, ts))
988    }
989
990    /// Create a new `m.sticker` event.
991    pub fn sticker(
992        &self,
993        body: impl Into<String>,
994        info: ImageInfo,
995        url: OwnedMxcUri,
996    ) -> EventBuilder<StickerEventContent> {
997        self.event(StickerEventContent::new(body.into(), info, url))
998    }
999
1000    /// Create a new `m.call.invite` event.
1001    pub fn call_invite(
1002        &self,
1003        call_id: OwnedVoipId,
1004        lifetime: UInt,
1005        offer: SessionDescription,
1006        version: VoipVersionId,
1007    ) -> EventBuilder<CallInviteEventContent> {
1008        self.event(CallInviteEventContent::new(call_id, lifetime, offer, version))
1009    }
1010
1011    /// Create a new `m.rtc.notification` event.
1012    pub fn rtc_notification(
1013        &self,
1014        notification_type: NotificationType,
1015    ) -> EventBuilder<RtcNotificationEventContent> {
1016        self.event(RtcNotificationEventContent::new(
1017            MilliSecondsSinceUnixEpoch::now(),
1018            Duration::new(30, 0),
1019            notification_type,
1020        ))
1021    }
1022
1023    // Creates a new `org.matrix.msc4310.rtc.decline` event.
1024    pub fn call_decline(
1025        &self,
1026        notification_event_id: &EventId,
1027    ) -> EventBuilder<RtcDeclineEventContent> {
1028        self.event(RtcDeclineEventContent::new(notification_event_id))
1029    }
1030
1031    /// Create a new `m.direct` global account data event.
1032    pub fn direct(&self) -> EventBuilder<DirectEventContent> {
1033        let mut builder = self.event(DirectEventContent::default());
1034        builder.is_global = true;
1035        builder
1036    }
1037
1038    /// Create a new `m.ignored_user_list` global account data event.
1039    pub fn ignored_user_list(
1040        &self,
1041        users: impl IntoIterator<Item = OwnedUserId>,
1042    ) -> EventBuilder<IgnoredUserListEventContent> {
1043        let mut builder = self.event(IgnoredUserListEventContent::users(users));
1044        builder.is_global = true;
1045        builder
1046    }
1047
1048    /// Create a new `m.push_rules` global account data event.
1049    pub fn push_rules(&self, rules: Ruleset) -> EventBuilder<PushRulesEventContent> {
1050        let mut builder = self.event(PushRulesEventContent::new(rules));
1051        builder.is_global = true;
1052        builder
1053    }
1054
1055    /// Create a new `m.space.child` state event.
1056    pub fn space_child(
1057        &self,
1058        parent: OwnedRoomId,
1059        child: OwnedRoomId,
1060    ) -> EventBuilder<SpaceChildEventContent> {
1061        let mut event = self.event(SpaceChildEventContent::new(vec![]));
1062        event.room = Some(parent);
1063        event.state_key = Some(child.to_string());
1064        event
1065    }
1066
1067    /// Create a new `m.space.parent` state event.
1068    pub fn space_parent(
1069        &self,
1070        parent: OwnedRoomId,
1071        child: OwnedRoomId,
1072    ) -> EventBuilder<SpaceParentEventContent> {
1073        let mut event = self.event(SpaceParentEventContent::new(vec![]));
1074        event.state_key = Some(parent.to_string());
1075        event.room = Some(child);
1076        event
1077    }
1078
1079    /// Set the next server timestamp.
1080    ///
1081    /// Timestamps will continue to increase by 1 (millisecond) from that value.
1082    pub fn set_next_ts(&self, value: u64) {
1083        self.next_ts.store(value, SeqCst);
1084    }
1085
1086    /// Create a new global account data event of the given `C` content type.
1087    pub fn global_account_data<C>(&self, content: C) -> EventBuilder<C>
1088    where
1089        C: GlobalAccountDataEventContent + StaticEventContent<IsPrefix = False>,
1090    {
1091        let mut event = self.event(content);
1092        event.is_global = true;
1093        event
1094    }
1095}
1096
1097impl EventBuilder<DirectEventContent> {
1098    /// Add a user/room pair to the `m.direct` event.
1099    pub fn add_user(mut self, user_id: OwnedDirectUserIdentifier, room_id: &RoomId) -> Self {
1100        self.content.0.entry(user_id).or_default().push(room_id.to_owned());
1101        self
1102    }
1103}
1104
1105impl EventBuilder<RoomMemberEventContent> {
1106    /// Set the `membership` of the `m.room.member` event to the given
1107    /// [`MembershipState`].
1108    ///
1109    /// The default is [`MembershipState::Join`].
1110    pub fn membership(mut self, state: MembershipState) -> Self {
1111        self.content.membership = state;
1112        self
1113    }
1114
1115    /// Set that the sender of this event invited the user passed as a parameter
1116    /// here.
1117    pub fn invited(mut self, invited_user: &UserId) -> Self {
1118        assert_ne!(
1119            self.sender.as_deref().unwrap(),
1120            invited_user,
1121            "invited user and sender can't be the same person"
1122        );
1123        self.content.membership = MembershipState::Invite;
1124        self.state_key = Some(invited_user.to_string());
1125        self
1126    }
1127
1128    /// Set that the sender of this event kicked the user passed as a parameter
1129    /// here.
1130    pub fn kicked(mut self, kicked_user: &UserId) -> Self {
1131        assert_ne!(
1132            self.sender.as_deref().unwrap(),
1133            kicked_user,
1134            "kicked user and sender can't be the same person, otherwise it's just a Leave"
1135        );
1136        self.content.membership = MembershipState::Leave;
1137        self.state_key = Some(kicked_user.to_string());
1138        self
1139    }
1140
1141    /// Set that the sender of this event banned the user passed as a parameter
1142    /// here.
1143    pub fn banned(mut self, banned_user: &UserId) -> Self {
1144        assert_ne!(
1145            self.sender.as_deref().unwrap(),
1146            banned_user,
1147            "a user can't ban itself" // hopefully
1148        );
1149        self.content.membership = MembershipState::Ban;
1150        self.state_key = Some(banned_user.to_string());
1151        self
1152    }
1153
1154    /// Set the display name of the `m.room.member` event.
1155    pub fn display_name(mut self, display_name: impl Into<String>) -> Self {
1156        self.content.displayname = Some(display_name.into());
1157        self
1158    }
1159
1160    /// Set the avatar URL of the `m.room.member` event.
1161    pub fn avatar_url(mut self, url: &MxcUri) -> Self {
1162        self.content.avatar_url = Some(url.to_owned());
1163        self
1164    }
1165
1166    /// Set the reason field of the `m.room.member` event.
1167    pub fn reason(mut self, reason: impl Into<String>) -> Self {
1168        self.content.reason = Some(reason.into());
1169        self
1170    }
1171
1172    /// Set the previous membership state (in the unsigned section).
1173    pub fn previous(mut self, previous: impl Into<PreviousMembership>) -> Self {
1174        let previous = previous.into();
1175
1176        let mut prev_content = RoomMemberEventContent::new(previous.state);
1177        if let Some(avatar_url) = previous.avatar_url {
1178            prev_content.avatar_url = Some(avatar_url);
1179        }
1180        if let Some(display_name) = previous.display_name {
1181            prev_content.displayname = Some(display_name);
1182        }
1183
1184        self.unsigned.get_or_insert_with(Default::default).prev_content = Some(prev_content);
1185        self
1186    }
1187}
1188
1189impl EventBuilder<RoomAvatarEventContent> {
1190    /// Defines the URL for the room avatar.
1191    pub fn url(mut self, url: &MxcUri) -> Self {
1192        self.content.url = Some(url.to_owned());
1193        self
1194    }
1195
1196    /// Defines the image info for the avatar.
1197    pub fn info(mut self, image: avatar::ImageInfo) -> Self {
1198        self.content.info = Some(Box::new(image));
1199        self
1200    }
1201}
1202
1203impl EventBuilder<RtcNotificationEventContent> {
1204    pub fn mentions(mut self, users: impl IntoIterator<Item = OwnedUserId>) -> Self {
1205        self.content.mentions = Some(Mentions::with_user_ids(users));
1206        self
1207    }
1208
1209    pub fn relates_to_membership_state_event(mut self, event_id: OwnedEventId) -> Self {
1210        self.content.relates_to = Some(Reference::new(event_id));
1211        self
1212    }
1213
1214    pub fn lifetime(mut self, time_in_seconds: u64) -> Self {
1215        self.content.lifetime = Duration::from_secs(time_in_seconds);
1216        self
1217    }
1218}
1219
1220pub struct ReadReceiptBuilder<'a> {
1221    factory: &'a EventFactory,
1222    content: ReceiptEventContent,
1223}
1224
1225impl ReadReceiptBuilder<'_> {
1226    /// Add a single read receipt to the event.
1227    pub fn add(
1228        self,
1229        event_id: &EventId,
1230        user_id: &UserId,
1231        tyype: ReceiptType,
1232        thread: ReceiptThread,
1233    ) -> Self {
1234        let ts = self.factory.next_server_ts();
1235        self.add_with_timestamp(event_id, user_id, tyype, thread, Some(ts))
1236    }
1237
1238    /// Add a single read receipt to the event, with an optional timestamp.
1239    pub fn add_with_timestamp(
1240        mut self,
1241        event_id: &EventId,
1242        user_id: &UserId,
1243        tyype: ReceiptType,
1244        thread: ReceiptThread,
1245        ts: Option<MilliSecondsSinceUnixEpoch>,
1246    ) -> Self {
1247        let by_event = self.content.0.entry(event_id.to_owned()).or_default();
1248        let by_type = by_event.entry(tyype).or_default();
1249
1250        let mut receipt = Receipt::default();
1251        if let Some(ts) = ts {
1252            receipt.ts = Some(ts);
1253        }
1254        receipt.thread = thread;
1255
1256        by_type.insert(user_id.to_owned(), receipt);
1257        self
1258    }
1259
1260    /// Finalize the builder into the receipt event content.
1261    pub fn into_content(self) -> ReceiptEventContent {
1262        self.content
1263    }
1264
1265    /// Finalize the builder into an event builder.
1266    pub fn into_event(self) -> EventBuilder<ReceiptEventContent> {
1267        let mut builder = self.factory.event(self.into_content());
1268        builder.is_ephemeral = true;
1269        builder
1270    }
1271}
1272
1273pub struct PreviousMembership {
1274    state: MembershipState,
1275    avatar_url: Option<OwnedMxcUri>,
1276    display_name: Option<String>,
1277}
1278
1279impl PreviousMembership {
1280    pub fn new(state: MembershipState) -> Self {
1281        Self { state, avatar_url: None, display_name: None }
1282    }
1283
1284    pub fn avatar_url(mut self, url: &MxcUri) -> Self {
1285        self.avatar_url = Some(url.to_owned());
1286        self
1287    }
1288
1289    pub fn display_name(mut self, name: impl Into<String>) -> Self {
1290        self.display_name = Some(name.into());
1291        self
1292    }
1293}
1294
1295impl From<MembershipState> for PreviousMembership {
1296    fn from(state: MembershipState) -> Self {
1297        Self::new(state)
1298    }
1299}