1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
//! Types describing [relationships between events].
//!
//! [relationships between events]: https://spec.matrix.org/latest/client-server-api/#forming-relationships-between-events

use std::fmt::Debug;

use js_int::UInt;
use ruma_common::{
    serde::{JsonObject, Raw, StringEnum},
    OwnedEventId,
};
use serde::{Deserialize, Serialize};

use super::AnyMessageLikeEvent;
use crate::PrivOwnedStr;

mod rel_serde;

/// Information about the event a [rich reply] is replying to.
///
/// [rich reply]: https://spec.matrix.org/latest/client-server-api/#rich-replies
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct InReplyTo {
    /// The event being replied to.
    pub event_id: OwnedEventId,
}

impl InReplyTo {
    /// Creates a new `InReplyTo` with the given event ID.
    pub fn new(event_id: OwnedEventId) -> Self {
        Self { event_id }
    }
}

/// An [annotation] for an event.
///
/// [annotation]: https://spec.matrix.org/latest/client-server-api/#event-annotations-and-reactions
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(tag = "rel_type", rename = "m.annotation")]
pub struct Annotation {
    /// The event that is being annotated.
    pub event_id: OwnedEventId,

    /// A string that indicates the annotation being applied.
    ///
    /// When sending emoji reactions, this field should include the colourful variation-16 when
    /// applicable.
    ///
    /// Clients should render reactions that have a long `key` field in a sensible manner.
    pub key: String,
}

impl Annotation {
    /// Creates a new `Annotation` with the given event ID and key.
    pub fn new(event_id: OwnedEventId, key: String) -> Self {
        Self { event_id, key }
    }
}

/// The content of a [replacement] relation.
///
/// [replacement]: https://spec.matrix.org/latest/client-server-api/#event-replacements
#[derive(Clone, Debug)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct Replacement<C> {
    /// The ID of the event being replaced.
    pub event_id: OwnedEventId,

    /// New content.
    pub new_content: C,
}

impl<C> Replacement<C> {
    /// Creates a new `Replacement` with the given event ID and new content.
    pub fn new(event_id: OwnedEventId, new_content: C) -> Self {
        Self { event_id, new_content }
    }
}

/// The content of a [thread] relation.
///
/// [thread]: https://spec.matrix.org/latest/client-server-api/#threading
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(tag = "rel_type", rename = "m.thread")]
pub struct Thread {
    /// The ID of the root message in the thread.
    pub event_id: OwnedEventId,

    /// A reply relation.
    ///
    /// If this event is a reply and belongs to a thread, this points to the message that is being
    /// replied to, and `is_falling_back` must be set to `false`.
    ///
    /// If this event is not a reply, this is used as a fallback mechanism for clients that do not
    /// support threads. This should point to the latest message-like event in the thread and
    /// `is_falling_back` must be set to `true`.
    #[serde(rename = "m.in_reply_to", skip_serializing_if = "Option::is_none")]
    pub in_reply_to: Option<InReplyTo>,

    /// Whether the `m.in_reply_to` field is a fallback for older clients or a genuine reply in a
    /// thread.
    #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
    pub is_falling_back: bool,
}

impl Thread {
    /// Convenience method to create a regular `Thread` relation with the given root event ID and
    /// latest message-like event ID.
    pub fn plain(event_id: OwnedEventId, latest_event_id: OwnedEventId) -> Self {
        Self { event_id, in_reply_to: Some(InReplyTo::new(latest_event_id)), is_falling_back: true }
    }

    /// Convenience method to create a regular `Thread` relation with the given root event ID and
    /// *without* the recommended reply fallback.
    pub fn without_fallback(event_id: OwnedEventId) -> Self {
        Self { event_id, in_reply_to: None, is_falling_back: false }
    }

    /// Convenience method to create a reply `Thread` relation with the given root event ID and
    /// replied-to event ID.
    pub fn reply(event_id: OwnedEventId, reply_to_event_id: OwnedEventId) -> Self {
        Self {
            event_id,
            in_reply_to: Some(InReplyTo::new(reply_to_event_id)),
            is_falling_back: false,
        }
    }
}

/// A bundled thread.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct BundledThread {
    /// The latest event in the thread.
    pub latest_event: Raw<AnyMessageLikeEvent>,

    /// The number of events in the thread.
    pub count: UInt,

    /// Whether the current logged in user has participated in the thread.
    pub current_user_participated: bool,
}

impl BundledThread {
    /// Creates a new `BundledThread` with the given event, count and user participated flag.
    pub fn new(
        latest_event: Raw<AnyMessageLikeEvent>,
        count: UInt,
        current_user_participated: bool,
    ) -> Self {
        Self { latest_event, count, current_user_participated }
    }
}

/// A [reference] to another event.
///
/// [reference]: https://spec.matrix.org/latest/client-server-api/#reference-relations
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(tag = "rel_type", rename = "m.reference")]
pub struct Reference {
    /// The ID of the event being referenced.
    pub event_id: OwnedEventId,
}

impl Reference {
    /// Creates a new `Reference` with the given event ID.
    pub fn new(event_id: OwnedEventId) -> Self {
        Self { event_id }
    }
}

/// A bundled reference.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct BundledReference {
    /// The ID of the event referencing this event.
    pub event_id: OwnedEventId,
}

impl BundledReference {
    /// Creates a new `BundledThread` with the given event ID.
    pub fn new(event_id: OwnedEventId) -> Self {
        Self { event_id }
    }
}

/// A chunk of references.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct ReferenceChunk {
    /// A batch of bundled references.
    pub chunk: Vec<BundledReference>,
}

impl ReferenceChunk {
    /// Creates a new `ReferenceChunk` with the given chunk.
    pub fn new(chunk: Vec<BundledReference>) -> Self {
        Self { chunk }
    }
}

/// [Bundled aggregations] of related child events of a message-like event.
///
/// [Bundled aggregations]: https://spec.matrix.org/latest/client-server-api/#aggregations-of-child-events
#[derive(Clone, Debug, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct BundledMessageLikeRelations<E> {
    /// Replacement relation.
    #[serde(rename = "m.replace", skip_serializing_if = "Option::is_none")]
    pub replace: Option<Box<E>>,

    /// Set when the above fails to deserialize.
    ///
    /// Intentionally *not* public.
    #[serde(skip_serializing)]
    has_invalid_replacement: bool,

    /// Thread relation.
    #[serde(rename = "m.thread", skip_serializing_if = "Option::is_none")]
    pub thread: Option<Box<BundledThread>>,

    /// Reference relations.
    #[serde(rename = "m.reference", skip_serializing_if = "Option::is_none")]
    pub reference: Option<Box<ReferenceChunk>>,
}

impl<E> BundledMessageLikeRelations<E> {
    /// Creates a new empty `BundledMessageLikeRelations`.
    pub const fn new() -> Self {
        Self { replace: None, has_invalid_replacement: false, thread: None, reference: None }
    }

    /// Whether this bundle contains a replacement relation.
    ///
    /// This may be `true` even if the `replace` field is `None`, because Matrix versions prior to
    /// 1.7 had a different incompatible format for bundled replacements. Use this method to check
    /// whether an event was replaced. If this returns `true` but `replace` is `None`, use one of
    /// the endpoints from `ruma::api::client::relations` to fetch the relation details.
    pub fn has_replacement(&self) -> bool {
        self.replace.is_some() || self.has_invalid_replacement
    }

    /// Returns `true` if all fields are empty.
    pub fn is_empty(&self) -> bool {
        self.replace.is_none() && self.thread.is_none() && self.reference.is_none()
    }

    /// Transform `BundledMessageLikeRelations<E>` to `BundledMessageLikeRelations<T>` using the
    /// given closure to convert the `replace` field if it is `Some(_)`.
    pub(crate) fn map_replace<T>(self, f: impl FnOnce(E) -> T) -> BundledMessageLikeRelations<T> {
        let Self { replace, has_invalid_replacement, thread, reference } = self;
        let replace = replace.map(|r| Box::new(f(*r)));
        BundledMessageLikeRelations { replace, has_invalid_replacement, thread, reference }
    }
}

impl<E> Default for BundledMessageLikeRelations<E> {
    fn default() -> Self {
        Self::new()
    }
}

/// [Bundled aggregations] of related child events of a state event.
///
/// [Bundled aggregations]: https://spec.matrix.org/latest/client-server-api/#aggregations-of-child-events
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct BundledStateRelations {
    /// Thread relation.
    #[serde(rename = "m.thread", skip_serializing_if = "Option::is_none")]
    pub thread: Option<Box<BundledThread>>,

    /// Reference relations.
    #[serde(rename = "m.reference", skip_serializing_if = "Option::is_none")]
    pub reference: Option<Box<ReferenceChunk>>,
}

impl BundledStateRelations {
    /// Creates a new empty `BundledStateRelations`.
    pub const fn new() -> Self {
        Self { thread: None, reference: None }
    }

    /// Returns `true` if all fields are empty.
    pub fn is_empty(&self) -> bool {
        self.thread.is_none() && self.reference.is_none()
    }
}

/// Relation types as defined in `rel_type` of an `m.relates_to` field.
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
#[derive(Clone, PartialEq, Eq, StringEnum)]
#[ruma_enum(rename_all = "m.snake_case")]
#[non_exhaustive]
pub enum RelationType {
    /// `m.annotation`, an annotation, principally used by reactions.
    Annotation,

    /// `m.replace`, a replacement.
    Replacement,

    /// `m.thread`, a participant to a thread.
    Thread,

    /// `m.reference`, a reference to another event.
    Reference,

    #[doc(hidden)]
    _Custom(PrivOwnedStr),
}

/// The payload for a custom relation.
#[doc(hidden)]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(transparent)]
pub struct CustomRelation(pub(super) JsonObject);

impl CustomRelation {
    pub(super) fn rel_type(&self) -> Option<RelationType> {
        Some(self.0.get("rel_type")?.as_str()?.into())
    }
}