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        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/// A thin copy of [`ruma::events::UnsignedRoomRedactionEvent`].
81#[derive(Debug, Serialize)]
82struct RedactedBecause {
83    /// Data specific to the event type.
84    content: RoomRedactionEventContent,
85
86    /// The globally unique event identifier for the user who sent the event.
87    event_id: OwnedEventId,
88
89    /// The fully-qualified ID of the user who sent this event.
90    sender: OwnedUserId,
91
92    /// Timestamp in milliseconds on originating homeserver when this event was
93    /// sent.
94    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
112// rustc can't derive Default because C isn't marked as `Default` 🤔 oh well.
113impl<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    /// Whether the event is an ephemeral one. As such, it doesn't require a
123    /// room id or a sender.
124    is_ephemeral: bool,
125    room: Option<OwnedRoomId>,
126    event_id: Option<OwnedEventId>,
127    /// Whether the event should *not* have an event id. False by default.
128    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    /// Adds bundled relations to this event.
174    ///
175    /// Ideally, we'd type-check that an event passed as a relation is the same
176    /// type as this one, but it's not trivial to do so because this builder
177    /// is only generic on the event's *content*, not the event type itself;
178    /// doing so would require many changes, and this is testing code after
179    /// all.
180    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    /// For state events manually created, define the state key.
189    ///
190    /// For other state events created in the [`EventFactory`], this is
191    /// automatically filled upon creation or update of the events.
192    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        // Use the `sender` preferably, or resort to the `redacted_because` sender if
200        // none has been set.
201        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    /// Build an event from the [`EventBuilder`] and convert it into a
260    /// serialized and [`Raw`] event.
261    ///
262    /// The generic argument `T` allows you to automatically cast the [`Raw`]
263    /// event into any desired type.
264    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    /// Turn this event into a [`TimelineEvent`] representing a decryption
283    /// failure
284    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    /// Adds a reply relation to the current event.
300    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    /// Adds a thread relation to the root event, setting the latest thread
307    /// event id too.
308    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    /// Adds a replacement relation to the current event, with the new content
315    /// passed.
316    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    /// Adds a caption to a media event.
327    ///
328    /// Will crash if the event isn't a media room message.
329    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    /// Create an event from any event content.
417    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    /// Create a new plain text `m.room.message`.
433    pub fn text_msg(&self, content: impl Into<String>) -> EventBuilder<RoomMessageEventContent> {
434        self.event(RoomMessageEventContent::text_plain(content.into()))
435    }
436
437    /// Create a new plain emote `m.room.message`.
438    pub fn emote(&self, content: impl Into<String>) -> EventBuilder<RoomMessageEventContent> {
439        self.event(RoomMessageEventContent::emote_plain(content.into()))
440    }
441
442    /// Create a new `m.room.member` event for the given member.
443    ///
444    /// The given member will be used as the `sender` as well as the `state_key`
445    /// of the `m.room.member` event, unless the `sender` was already using
446    /// [`EventFactory::sender()`], in that case only the state key will be
447    /// set to the given `member`.
448    ///
449    /// The `membership` field of the content is set to
450    /// [`MembershipState::Join`].
451    ///
452    /// ```
453    /// use matrix_sdk_test::event_factory::EventFactory;
454    /// use ruma::{
455    ///     events::{
456    ///         room::member::{MembershipState, RoomMemberEventContent},
457    ///         SyncStateEvent,
458    ///     },
459    ///     room_id,
460    ///     serde::Raw,
461    ///     user_id,
462    /// };
463    ///
464    /// let factory = EventFactory::new().room(room_id!("!test:localhost"));
465    ///
466    /// let event: Raw<SyncStateEvent<RoomMemberEventContent>> = factory
467    ///     .member(user_id!("@alice:localhost"))
468    ///     .display_name("Alice")
469    ///     .into_raw();
470    /// ```
471    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    /// Create a tombstone state event for the room.
486    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    /// Create a state event for the topic.
498    pub fn room_topic(&self, topic: impl Into<String>) -> EventBuilder<RoomTopicEventContent> {
499        let mut event = self.event(RoomTopicEventContent::new(topic.into()));
500        // The state key is empty for a room topic state event.
501        event.state_key = Some("".to_owned());
502        event
503    }
504
505    /// Create a state event for the room name.
506    pub fn room_name(&self, name: impl Into<String>) -> EventBuilder<RoomNameEventContent> {
507        let mut event = self.event(RoomNameEventContent::new(name.into()));
508        // The state key is empty for a room name state event.
509        event.state_key = Some("".to_owned());
510        event
511    }
512
513    /// Create an empty state event for the room avatar.
514    pub fn room_avatar(&self) -> EventBuilder<RoomAvatarEventContent> {
515        let mut event = self.event(RoomAvatarEventContent::new());
516        // The state key is empty for a room avatar state event.
517        event.state_key = Some("".to_owned());
518        event
519    }
520
521    /// Create a new `m.member_hints` event with the given service members.
522    ///
523    /// ```
524    /// use std::collections::BTreeSet;
525    ///
526    /// use matrix_sdk_test::event_factory::EventFactory;
527    /// use ruma::{
528    ///     events::{member_hints::MemberHintsEventContent, SyncStateEvent},
529    ///     owned_user_id, room_id,
530    ///     serde::Raw,
531    ///     user_id,
532    /// };
533    ///
534    /// let factory = EventFactory::new().room(room_id!("!test:localhost"));
535    ///
536    /// let event: Raw<SyncStateEvent<MemberHintsEventContent>> = factory
537    ///     .member_hints(BTreeSet::from([owned_user_id!("@alice:localhost")]))
538    ///     .sender(user_id!("@alice:localhost"))
539    ///     .into_raw();
540    /// ```
541    pub fn member_hints(
542        &self,
543        service_members: BTreeSet<OwnedUserId>,
544    ) -> EventBuilder<MemberHintsEventContent> {
545        // The `m.member_hints` event always has an empty state key, so let's set it.
546        self.event(MemberHintsEventContent::new(service_members)).state_key("")
547    }
548
549    /// Create a new plain/html `m.room.message`.
550    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    /// Create a new plain notice `m.room.message`.
559    pub fn notice(&self, content: impl Into<String>) -> EventBuilder<RoomMessageEventContent> {
560        self.event(RoomMessageEventContent::notice_plain(content))
561    }
562
563    /// Add a reaction to an event.
564    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    /// Create a live redaction for the given event id.
576    ///
577    /// Note: this is not a redacted event, but a redaction event, that will
578    /// cause another event to be redacted.
579    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    /// Create a redacted event, with extra information in the unsigned section
586    /// about the redaction itself.
587    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    /// Create a redacted state event, with extra information in the unsigned
607    /// section about the redaction itself.
608    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    /// Create a poll start event given a text, the question and the possible
630    /// answers.
631    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        // PollAnswers 'constructor' is not public, so we need to deserialize them
638        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    /// Create a poll edit event given the new question and possible answers.
653    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        // PollAnswers 'constructor' is not public, so we need to deserialize them
660        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    /// Create a poll response with the given answer id and the associated poll
675    /// start event id.
676    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    /// Create a poll response with the given text and the associated poll start
688    /// event id.
689    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    /// Creates a plain (unencrypted) image event content referencing the given
698    /// MXC ID.
699    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    /// Create a typing notification event.
709    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    /// Create a read receipt event.
717    pub fn read_receipts(&self) -> ReadReceiptBuilder<'_> {
718        ReadReceiptBuilder { factory: self, content: ReceiptEventContent(Default::default()) }
719    }
720
721    /// Set the next server timestamp.
722    ///
723    /// Timestamps will continue to increase by 1 (millisecond) from that value.
724    pub fn set_next_ts(&self, value: u64) {
725        self.next_ts.store(value, SeqCst);
726    }
727}
728
729impl EventBuilder<RoomMemberEventContent> {
730    /// Set the `membership` of the `m.room.member` event to the given
731    /// [`MembershipState`].
732    ///
733    /// The default is [`MembershipState::Join`].
734    pub fn membership(mut self, state: MembershipState) -> Self {
735        self.content.membership = state;
736        self
737    }
738
739    /// Set that the sender of this event invited the user passed as a parameter
740    /// here.
741    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    /// Set that the sender of this event kicked the user passed as a parameter
753    /// here.
754    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    /// Set that the sender of this event banned the user passed as a parameter
766    /// here.
767    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" // hopefully
772        );
773        self.content.membership = MembershipState::Ban;
774        self.state_key = Some(banned_user.to_string());
775        self
776    }
777
778    /// Set the display name of the `m.room.member` event.
779    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    /// Set the avatar URL of the `m.room.member` event.
785    pub fn avatar_url(mut self, url: &MxcUri) -> Self {
786        self.content.avatar_url = Some(url.to_owned());
787        self
788    }
789
790    /// Set the reason field of the `m.room.member` event.
791    pub fn reason(mut self, reason: impl Into<String>) -> Self {
792        self.content.reason = Some(reason.into());
793        self
794    }
795
796    /// Set the previous membership state (in the unsigned section).
797    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    /// Defines the URL for the room avatar.
815    pub fn url(mut self, url: &MxcUri) -> Self {
816        self.content.url = Some(url.to_owned());
817        self
818    }
819
820    /// Defines the image info for the avatar.
821    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    /// Add a single read receipt to the event.
834    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    /// Add a single read receipt to the event, with an optional timestamp.
846    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    /// Finalize the builder into the receipt event content.
868    pub fn into_content(self) -> ReceiptEventContent {
869        self.content
870    }
871
872    /// Finalize the builder into an event builder.
873    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}