Skip to main content

ruma_events/
relation.rs

1//! Types describing [relationships between events].
2//!
3//! [relationships between events]: https://spec.matrix.org/latest/client-server-api/#forming-relationships-between-events
4
5use std::fmt::Debug;
6
7use js_int::UInt;
8use ruma_common::{
9    OwnedEventId,
10    serde::{JsonObject, Raw, StringEnum},
11};
12use serde::{Deserialize, Serialize};
13
14use crate::{AnySyncMessageLikeEvent, PrivOwnedStr};
15
16mod rel_serde;
17
18/// A [rich reply] to an event.
19///
20/// [rich reply]: https://spec.matrix.org/latest/client-server-api/#rich-replies
21#[derive(Clone, Debug, Deserialize, Serialize)]
22#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
23pub struct Reply {
24    /// The event being replied to.
25    #[serde(rename = "m.in_reply_to")]
26    pub in_reply_to: InReplyTo,
27}
28
29impl Reply {
30    /// Creates a new `Reply` with the given reply information.
31    pub fn new(in_reply_to: InReplyTo) -> Self {
32        Self { in_reply_to }
33    }
34
35    /// Creates a new `Reply` with the given event ID.
36    pub fn with_event_id(event_id: OwnedEventId) -> Self {
37        Self { in_reply_to: InReplyTo::new(event_id) }
38    }
39}
40
41/// Information about the event a [rich reply] is replying to.
42///
43/// [rich reply]: https://spec.matrix.org/latest/client-server-api/#rich-replies
44#[derive(Clone, Debug, Deserialize, Serialize)]
45#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
46pub struct InReplyTo {
47    /// The event being replied to.
48    pub event_id: OwnedEventId,
49}
50
51impl InReplyTo {
52    /// Creates a new `InReplyTo` with the given event ID.
53    pub fn new(event_id: OwnedEventId) -> Self {
54        Self { event_id }
55    }
56}
57
58/// An [annotation] for an event.
59///
60/// [annotation]: https://spec.matrix.org/latest/client-server-api/#event-annotations-and-reactions
61#[derive(Clone, Debug, Deserialize, Serialize)]
62#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
63#[serde(tag = "rel_type", rename = "m.annotation")]
64pub struct Annotation {
65    /// The event that is being annotated.
66    pub event_id: OwnedEventId,
67
68    /// A string that indicates the annotation being applied.
69    ///
70    /// When sending emoji reactions, this field should include the colourful variation-16 when
71    /// applicable.
72    ///
73    /// Clients should render reactions that have a long `key` field in a sensible manner.
74    pub key: String,
75}
76
77impl Annotation {
78    /// Creates a new `Annotation` with the given event ID and key.
79    pub fn new(event_id: OwnedEventId, key: String) -> Self {
80        Self { event_id, key }
81    }
82}
83
84/// The content of a [replacement] relation.
85///
86/// [replacement]: https://spec.matrix.org/latest/client-server-api/#event-replacements
87#[derive(Clone, Debug)]
88#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
89pub struct Replacement<C> {
90    /// The ID of the event being replaced.
91    pub event_id: OwnedEventId,
92
93    /// New content.
94    pub new_content: C,
95}
96
97impl<C> Replacement<C> {
98    /// Creates a new `Replacement` with the given event ID and new content.
99    pub fn new(event_id: OwnedEventId, new_content: C) -> Self {
100        Self { event_id, new_content }
101    }
102}
103
104/// The content of a [thread] relation.
105///
106/// [thread]: https://spec.matrix.org/latest/client-server-api/#threading
107#[derive(Clone, Debug, Serialize, Deserialize)]
108#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
109#[serde(tag = "rel_type", rename = "m.thread")]
110pub struct Thread {
111    /// The ID of the root message in the thread.
112    pub event_id: OwnedEventId,
113
114    /// A reply relation.
115    ///
116    /// If this event is a reply and belongs to a thread, this points to the message that is being
117    /// replied to, and `is_falling_back` must be set to `false`.
118    ///
119    /// If this event is not a reply, this is used as a fallback mechanism for clients that do not
120    /// support threads. This should point to the latest message-like event in the thread and
121    /// `is_falling_back` must be set to `true`.
122    #[serde(rename = "m.in_reply_to", skip_serializing_if = "Option::is_none")]
123    pub in_reply_to: Option<InReplyTo>,
124
125    /// Whether the `m.in_reply_to` field is a fallback for older clients or a genuine reply in a
126    /// thread.
127    #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
128    pub is_falling_back: bool,
129}
130
131impl Thread {
132    /// Convenience method to create a regular `Thread` relation with the given root event ID and
133    /// latest message-like event ID.
134    pub fn plain(event_id: OwnedEventId, latest_event_id: OwnedEventId) -> Self {
135        Self { event_id, in_reply_to: Some(InReplyTo::new(latest_event_id)), is_falling_back: true }
136    }
137
138    /// Convenience method to create a regular `Thread` relation with the given root event ID and
139    /// *without* the recommended reply fallback.
140    pub fn without_fallback(event_id: OwnedEventId) -> Self {
141        Self { event_id, in_reply_to: None, is_falling_back: false }
142    }
143
144    /// Convenience method to create a reply `Thread` relation with the given root event ID and
145    /// replied-to event ID.
146    pub fn reply(event_id: OwnedEventId, reply_to_event_id: OwnedEventId) -> Self {
147        Self {
148            event_id,
149            in_reply_to: Some(InReplyTo::new(reply_to_event_id)),
150            is_falling_back: false,
151        }
152    }
153}
154
155/// A bundled thread.
156#[derive(Clone, Debug, Deserialize, Serialize)]
157#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
158pub struct BundledThread {
159    /// The latest event in the thread.
160    pub latest_event: Raw<AnySyncMessageLikeEvent>,
161
162    /// The number of events in the thread.
163    pub count: UInt,
164
165    /// Whether the current logged in user has participated in the thread.
166    pub current_user_participated: bool,
167}
168
169impl BundledThread {
170    /// Creates a new `BundledThread` with the given event, count and user participated flag.
171    pub fn new(
172        latest_event: Raw<AnySyncMessageLikeEvent>,
173        count: UInt,
174        current_user_participated: bool,
175    ) -> Self {
176        Self { latest_event, count, current_user_participated }
177    }
178}
179
180/// A [reference] to another event.
181///
182/// [reference]: https://spec.matrix.org/latest/client-server-api/#reference-relations
183#[derive(Clone, Debug, Deserialize, Serialize)]
184#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
185#[serde(tag = "rel_type", rename = "m.reference")]
186pub struct Reference {
187    /// The ID of the event being referenced.
188    pub event_id: OwnedEventId,
189}
190
191impl Reference {
192    /// Creates a new `Reference` with the given event ID.
193    pub fn new(event_id: OwnedEventId) -> Self {
194        Self { event_id }
195    }
196}
197
198/// A bundled reference.
199#[derive(Clone, Debug, Deserialize, Serialize)]
200#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
201pub struct BundledReference {
202    /// The ID of the event referencing this event.
203    pub event_id: OwnedEventId,
204}
205
206impl BundledReference {
207    /// Creates a new `BundledThread` with the given event ID.
208    pub fn new(event_id: OwnedEventId) -> Self {
209        Self { event_id }
210    }
211}
212
213/// A chunk of references.
214#[derive(Clone, Debug, Default, Deserialize, Serialize)]
215#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
216pub struct ReferenceChunk {
217    /// A batch of bundled references.
218    pub chunk: Vec<BundledReference>,
219}
220
221impl ReferenceChunk {
222    /// Creates a new `ReferenceChunk` with the given chunk.
223    pub fn new(chunk: Vec<BundledReference>) -> Self {
224        Self { chunk }
225    }
226}
227
228/// [Bundled aggregations] of related child events of a message-like event.
229///
230/// [Bundled aggregations]: https://spec.matrix.org/latest/client-server-api/#aggregations-of-child-events
231#[derive(Clone, Debug, Serialize)]
232#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
233pub struct BundledMessageLikeRelations<E> {
234    /// Replacement relation.
235    #[serde(rename = "m.replace", skip_serializing_if = "Option::is_none")]
236    pub replace: Option<Box<E>>,
237
238    /// Set when the above fails to deserialize.
239    ///
240    /// Intentionally *not* public.
241    #[serde(skip_serializing)]
242    has_invalid_replacement: bool,
243
244    /// Thread relation.
245    #[serde(rename = "m.thread", skip_serializing_if = "Option::is_none")]
246    pub thread: Option<Box<BundledThread>>,
247
248    /// Reference relations.
249    #[serde(rename = "m.reference", skip_serializing_if = "Option::is_none")]
250    pub reference: Option<Box<ReferenceChunk>>,
251}
252
253impl<E> BundledMessageLikeRelations<E> {
254    /// Creates a new empty `BundledMessageLikeRelations`.
255    pub const fn new() -> Self {
256        Self { replace: None, has_invalid_replacement: false, thread: None, reference: None }
257    }
258
259    /// Whether this bundle contains a replacement relation.
260    ///
261    /// This may be `true` even if the `replace` field is `None`, because Matrix versions prior to
262    /// 1.7 had a different incompatible format for bundled replacements. Use this method to check
263    /// whether an event was replaced. If this returns `true` but `replace` is `None`, use one of
264    /// the endpoints from `ruma::api::client::relations` to fetch the relation details.
265    pub fn has_replacement(&self) -> bool {
266        self.replace.is_some() || self.has_invalid_replacement
267    }
268
269    /// Returns `true` if all fields are empty.
270    pub fn is_empty(&self) -> bool {
271        self.replace.is_none() && self.thread.is_none() && self.reference.is_none()
272    }
273
274    /// Transform `BundledMessageLikeRelations<E>` to `BundledMessageLikeRelations<T>` using the
275    /// given closure to convert the `replace` field if it is `Some(_)`.
276    pub(crate) fn map_replace<T>(self, f: impl FnOnce(E) -> T) -> BundledMessageLikeRelations<T> {
277        let Self { replace, has_invalid_replacement, thread, reference } = self;
278        let replace = replace.map(|r| Box::new(f(*r)));
279        BundledMessageLikeRelations { replace, has_invalid_replacement, thread, reference }
280    }
281}
282
283impl<E> Default for BundledMessageLikeRelations<E> {
284    fn default() -> Self {
285        Self::new()
286    }
287}
288
289/// [Bundled aggregations] of related child events of a state event.
290///
291/// [Bundled aggregations]: https://spec.matrix.org/latest/client-server-api/#aggregations-of-child-events
292#[derive(Clone, Debug, Default, Deserialize, Serialize)]
293#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
294pub struct BundledStateRelations {
295    /// Thread relation.
296    #[serde(rename = "m.thread", skip_serializing_if = "Option::is_none")]
297    pub thread: Option<Box<BundledThread>>,
298
299    /// Reference relations.
300    #[serde(rename = "m.reference", skip_serializing_if = "Option::is_none")]
301    pub reference: Option<Box<ReferenceChunk>>,
302}
303
304impl BundledStateRelations {
305    /// Creates a new empty `BundledStateRelations`.
306    pub const fn new() -> Self {
307        Self { thread: None, reference: None }
308    }
309
310    /// Returns `true` if all fields are empty.
311    pub fn is_empty(&self) -> bool {
312        self.thread.is_none() && self.reference.is_none()
313    }
314}
315
316/// Relation types as defined in `rel_type` of an `m.relates_to` field.
317#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
318#[derive(Clone, StringEnum)]
319#[ruma_enum(rename_all(prefix = "m.", rule = "snake_case"))]
320#[non_exhaustive]
321pub enum RelationType {
322    /// `m.annotation`, an annotation, principally used by reactions.
323    Annotation,
324
325    /// `m.replace`, a replacement.
326    #[ruma_enum(rename = "m.replace")]
327    Replacement,
328
329    /// `m.thread`, a participant to a thread.
330    Thread,
331
332    /// `m.reference`, a reference to another event.
333    Reference,
334
335    #[doc(hidden)]
336    _Custom(PrivOwnedStr),
337}
338
339/// The payload for a custom relation.
340#[doc(hidden)]
341#[derive(Clone, Debug, Deserialize, Serialize)]
342#[serde(transparent)]
343pub struct CustomRelation(pub(super) JsonObject);
344
345impl CustomRelation {
346    pub(super) fn rel_type(&self) -> Option<RelationType> {
347        Some(self.0.get("rel_type")?.as_str()?.into())
348    }
349}