ruma_events/call/member/
member_data.rs

1//! Types for MatrixRTC `m.call.member` state event content data ([MSC3401])
2//!
3//! [MSC3401]: https://github.com/matrix-org/matrix-spec-proposals/pull/3401
4
5use std::time::Duration;
6
7use as_variant::as_variant;
8use ruma_common::{DeviceId, MilliSecondsSinceUnixEpoch, OwnedDeviceId};
9use ruma_macros::StringEnum;
10use serde::{Deserialize, Serialize};
11use tracing::warn;
12
13use super::focus::{ActiveFocus, ActiveLivekitFocus, Focus};
14use crate::PrivOwnedStr;
15
16/// The data object that contains the information for one membership.
17///
18/// It can be a legacy or a normal MatrixRTC Session membership.
19///
20/// The legacy format contains time information to compute if it is expired or not.
21/// SessionMembershipData does not have the concept of timestamp based expiration anymore.
22/// The state event will reliably be set to empty when the user disconnects.
23#[derive(Clone, Debug)]
24#[cfg_attr(test, derive(PartialEq))]
25#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
26pub enum MembershipData<'a> {
27    /// The legacy format (using an array of memberships for each device -> one event per user)
28    Legacy(&'a LegacyMembershipData),
29    /// One event per device. `SessionMembershipData` contains all the information required to
30    /// represent the current membership state of one device.
31    Session(&'a SessionMembershipData),
32}
33
34impl MembershipData<'_> {
35    /// The application this RTC membership participates in (the session type, can be `m.call`...)
36    pub fn application(&self) -> &Application {
37        match self {
38            MembershipData::Legacy(data) => &data.application,
39            MembershipData::Session(data) => &data.application,
40        }
41    }
42
43    /// The device id of this membership.
44    pub fn device_id(&self) -> &DeviceId {
45        match self {
46            MembershipData::Legacy(data) => &data.device_id,
47            MembershipData::Session(data) => &data.device_id,
48        }
49    }
50
51    /// The active focus is a FocusType specific object that describes how this user
52    /// is currently connected.
53    ///
54    /// It can use the foci_preferred list to choose one of the available (preferred)
55    /// foci or specific information on how to connect to this user.
56    ///
57    /// Every user needs to converge to use the same focus_active type.
58    pub fn focus_active(&self) -> &ActiveFocus {
59        match self {
60            MembershipData::Legacy(_) => &ActiveFocus::Livekit(ActiveLivekitFocus {
61                focus_selection: super::focus::FocusSelection::OldestMembership,
62            }),
63            MembershipData::Session(data) => &data.focus_active,
64        }
65    }
66
67    /// The list of available/preferred options this user provides to connect to the call.
68    pub fn foci_preferred(&self) -> &Vec<Focus> {
69        match self {
70            MembershipData::Legacy(data) => &data.foci_active,
71            MembershipData::Session(data) => &data.foci_preferred,
72        }
73    }
74
75    /// The application of the membership is "m.call" and the scope is "m.room".
76    pub fn is_room_call(&self) -> bool {
77        as_variant!(self.application(), Application::Call)
78            .is_some_and(|call| call.scope == CallScope::Room)
79    }
80
81    /// The application of the membership is "m.call".
82    pub fn is_call(&self) -> bool {
83        as_variant!(self.application(), Application::Call).is_some()
84    }
85
86    /// Gets the created_ts of the event.
87    ///
88    /// This is the `origin_server_ts` for session data.
89    /// For legacy events this can either be the origin server ts or a copy from the
90    /// `origin_server_ts` since we expect legacy events to get updated (when a new device
91    /// joins/leaves).
92    pub fn created_ts(&self) -> Option<MilliSecondsSinceUnixEpoch> {
93        match self {
94            MembershipData::Legacy(data) => data.created_ts,
95            MembershipData::Session(data) => data.created_ts,
96        }
97    }
98
99    /// Checks if the event is expired.
100    ///
101    /// Defaults to using `created_ts` of the [`MembershipData`].
102    /// If no `origin_server_ts` is provided and the event does not contain `created_ts`
103    /// the event will be considered as not expired.
104    /// In this case, a warning will be logged.
105    ///
106    /// This method needs to be called periodically to check if the event is still valid.
107    ///
108    /// # Arguments
109    ///
110    /// * `origin_server_ts` - a fallback if [`MembershipData::created_ts`] is not present
111    pub fn is_expired(&self, origin_server_ts: Option<MilliSecondsSinceUnixEpoch>) -> bool {
112        if let Some(expire_ts) = self.expires_ts(origin_server_ts) {
113            MilliSecondsSinceUnixEpoch::now() > expire_ts
114        } else {
115            // This should not be reached since we only allow events that have copied over
116            // the origin server ts. `set_created_ts_if_none`
117            warn!("Encountered a Call Member state event where the expire_ts could not be constructed.");
118            false
119        }
120    }
121
122    /// The unix timestamp at which the event will expire.
123    /// This allows to determine at what time the return value of
124    /// [`MembershipData::is_expired`] will change.
125    ///
126    /// Defaults to using `created_ts` of the [`MembershipData`].
127    /// If no `origin_server_ts` is provided and the event does not contain `created_ts`
128    /// the event will be considered as not expired.
129    /// In this case, a warning will be logged.
130    ///
131    /// # Arguments
132    ///
133    /// * `origin_server_ts` - a fallback if [`MembershipData::created_ts`] is not present
134    pub fn expires_ts(
135        &self,
136        origin_server_ts: Option<MilliSecondsSinceUnixEpoch>,
137    ) -> Option<MilliSecondsSinceUnixEpoch> {
138        let expires = match &self {
139            MembershipData::Legacy(data) => data.expires,
140            MembershipData::Session(data) => data.expires,
141        };
142        let ev_created_ts = self.created_ts().or(origin_server_ts)?.to_system_time();
143        ev_created_ts.and_then(|t| MilliSecondsSinceUnixEpoch::from_system_time(t + expires))
144    }
145}
146
147/// A membership describes one of the sessions this user currently partakes.
148///
149/// The application defines the type of the session.
150#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
151#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
152pub struct LegacyMembershipData {
153    /// The type of the MatrixRTC session the membership belongs to.
154    ///
155    /// e.g. call, spacial, document...
156    #[serde(flatten)]
157    pub application: Application,
158
159    /// The device id of this membership.
160    ///
161    /// The same user can join with their phone/computer.
162    pub device_id: OwnedDeviceId,
163
164    /// The duration in milliseconds relative to the time this membership joined
165    /// during which the membership is valid.
166    ///
167    /// The time a member has joined is defined as:
168    /// `MIN(content.created_ts, event.origin_server_ts)`
169    #[serde(with = "ruma_common::serde::duration::ms")]
170    pub expires: Duration,
171
172    /// Stores a copy of the `origin_server_ts` of the initial session event.
173    ///
174    /// If the membership is updated this field will be used to track the
175    /// original `origin_server_ts`.
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub created_ts: Option<MilliSecondsSinceUnixEpoch>,
178
179    /// A list of the foci in use for this membership.
180    pub foci_active: Vec<Focus>,
181
182    /// The id of the membership.
183    ///
184    /// This is required to guarantee uniqueness of the event.
185    /// Sending the same state event twice to synapse makes the HS drop the second one and return
186    /// 200.
187    #[serde(rename = "membershipID")]
188    pub membership_id: String,
189}
190
191/// Initial set of fields of [`LegacyMembershipData`].
192#[derive(Debug)]
193#[allow(clippy::exhaustive_structs)]
194pub struct LegacyMembershipDataInit {
195    /// The type of the MatrixRTC session the membership belongs to.
196    ///
197    /// e.g. call, spacial, document...
198    pub application: Application,
199
200    /// The device id of this membership.
201    ///
202    /// The same user can join with their phone/computer.
203    pub device_id: OwnedDeviceId,
204
205    /// The duration in milliseconds relative to the time this membership joined
206    /// during which the membership is valid.
207    ///
208    /// The time a member has joined is defined as:
209    /// `MIN(content.created_ts, event.origin_server_ts)`
210    pub expires: Duration,
211
212    /// A list of the focuses (foci) in use for this membership.
213    pub foci_active: Vec<Focus>,
214
215    /// The id of the membership.
216    ///
217    /// This is required to guarantee uniqueness of the event.
218    /// Sending the same state event twice to synapse makes the HS drop the second one and return
219    /// 200.
220    pub membership_id: String,
221}
222
223impl From<LegacyMembershipDataInit> for LegacyMembershipData {
224    fn from(init: LegacyMembershipDataInit) -> Self {
225        let LegacyMembershipDataInit {
226            application,
227            device_id,
228            expires,
229            foci_active,
230            membership_id,
231        } = init;
232        Self { application, device_id, expires, created_ts: None, foci_active, membership_id }
233    }
234}
235
236/// Stores all the information for a MatrixRTC membership. (one for each device)
237#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
238#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
239pub struct SessionMembershipData {
240    /// The type of the MatrixRTC session the membership belongs to.
241    ///
242    /// e.g. call, spacial, document...
243    #[serde(flatten)]
244    pub application: Application,
245
246    /// The device id of this membership.
247    ///
248    /// The same user can join with their phone/computer.
249    pub device_id: OwnedDeviceId,
250
251    /// A list of the foci that this membership proposes to use.
252    pub foci_preferred: Vec<Focus>,
253
254    /// Data required to determine the currently used focus by this member.
255    pub focus_active: ActiveFocus,
256
257    /// Stores a copy of the `origin_server_ts` of the initial session event.
258    ///
259    /// If the membership is updated this field will be used to track the
260    /// original `origin_server_ts`.
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub created_ts: Option<MilliSecondsSinceUnixEpoch>,
263
264    /// The duration in milliseconds relative to the time this membership joined
265    /// during which the membership is valid.
266    ///
267    /// The time a member has joined is defined as:
268    /// `MIN(content.created_ts, event.origin_server_ts)`
269    #[serde(with = "ruma_common::serde::duration::ms")]
270    pub expires: Duration,
271}
272
273/// The type of the MatrixRTC session.
274///
275/// This is not the application/client used by the user but the
276/// type of MatrixRTC session e.g. calling (`m.call`), third-room, whiteboard could be
277/// possible applications.
278#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
279#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
280#[serde(tag = "application")]
281pub enum Application {
282    /// The rtc application (session type) for VoIP call.
283    #[serde(rename = "m.call")]
284    Call(CallApplicationContent),
285}
286
287/// Call specific parameters of a `m.call.member` event.
288#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
289#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
290pub struct CallApplicationContent {
291    /// An identifier for calls.
292    ///
293    /// All members using the same `call_id` will end up in the same call.
294    ///
295    /// Does not need to be a uuid.
296    ///
297    /// `""` is used for room scoped calls.
298    pub call_id: String,
299
300    /// Who owns/joins/controls (can modify) the call.
301    pub scope: CallScope,
302}
303
304impl CallApplicationContent {
305    /// Initialize a [`CallApplicationContent`].
306    ///
307    /// # Arguments
308    ///
309    /// * `call_id` - An identifier for calls. All members using the same `call_id` will end up in
310    ///   the same call. Does not need to be a uuid. `""` is used for room scoped calls.
311    /// * `scope` - Who owns/joins/controls (can modify) the call.
312    pub fn new(call_id: String, scope: CallScope) -> Self {
313        Self { call_id, scope }
314    }
315}
316
317/// The call scope defines different call ownership models.
318#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
319#[derive(Clone, PartialEq, StringEnum)]
320#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
321#[ruma_enum(rename_all = "m.snake_case")]
322pub enum CallScope {
323    /// A call which every user of a room can join and create.
324    ///
325    /// There is no particular name associated with it.
326    ///
327    /// There can only be one per room.
328    Room,
329
330    /// A user call is owned by a user.
331    ///
332    /// Each user can create one there can be multiple per room. They are started and ended by the
333    /// owning user.
334    User,
335
336    #[doc(hidden)]
337    _Custom(PrivOwnedStr),
338}