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