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!(
118                "Encountered a Call Member state event where the expire_ts could not be constructed."
119            );
120            false
121        }
122    }
123
124    /// The unix timestamp at which the event will expire.
125    /// This allows to determine at what time the return value of
126    /// [`MembershipData::is_expired`] will change.
127    ///
128    /// Defaults to using `created_ts` of the [`MembershipData`].
129    /// If no `origin_server_ts` is provided and the event does not contain `created_ts`
130    /// the event will be considered as not expired.
131    /// In this case, a warning will be logged.
132    ///
133    /// # Arguments
134    ///
135    /// * `origin_server_ts` - a fallback if [`MembershipData::created_ts`] is not present
136    pub fn expires_ts(
137        &self,
138        origin_server_ts: Option<MilliSecondsSinceUnixEpoch>,
139    ) -> Option<MilliSecondsSinceUnixEpoch> {
140        let expires = match &self {
141            MembershipData::Legacy(data) => data.expires,
142            MembershipData::Session(data) => data.expires,
143        };
144        let ev_created_ts = self.created_ts().or(origin_server_ts)?.to_system_time();
145        ev_created_ts.and_then(|t| MilliSecondsSinceUnixEpoch::from_system_time(t + expires))
146    }
147}
148
149/// A membership describes one of the sessions this user currently partakes.
150///
151/// The application defines the type of the session.
152#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
153#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
154pub struct LegacyMembershipData {
155    /// The type of the MatrixRTC session the membership belongs to.
156    ///
157    /// e.g. call, spacial, document...
158    #[serde(flatten)]
159    pub application: Application,
160
161    /// The device id of this membership.
162    ///
163    /// The same user can join with their phone/computer.
164    pub device_id: OwnedDeviceId,
165
166    /// The duration in milliseconds relative to the time this membership joined
167    /// during which the membership is valid.
168    ///
169    /// The time a member has joined is defined as:
170    /// `MIN(content.created_ts, event.origin_server_ts)`
171    #[serde(with = "ruma_common::serde::duration::ms")]
172    pub expires: Duration,
173
174    /// Stores a copy of the `origin_server_ts` of the initial session event.
175    ///
176    /// If the membership is updated this field will be used to track the
177    /// original `origin_server_ts`.
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub created_ts: Option<MilliSecondsSinceUnixEpoch>,
180
181    /// A list of the foci in use for this membership.
182    pub foci_active: Vec<Focus>,
183
184    /// The id of the membership.
185    ///
186    /// This is required to guarantee uniqueness of the event.
187    /// Sending the same state event twice to synapse makes the HS drop the second one and return
188    /// 200.
189    #[serde(rename = "membershipID")]
190    pub membership_id: String,
191}
192
193/// Initial set of fields of [`LegacyMembershipData`].
194#[derive(Debug)]
195#[allow(clippy::exhaustive_structs)]
196pub struct LegacyMembershipDataInit {
197    /// The type of the MatrixRTC session the membership belongs to.
198    ///
199    /// e.g. call, spacial, document...
200    pub application: Application,
201
202    /// The device id of this membership.
203    ///
204    /// The same user can join with their phone/computer.
205    pub device_id: OwnedDeviceId,
206
207    /// The duration in milliseconds relative to the time this membership joined
208    /// during which the membership is valid.
209    ///
210    /// The time a member has joined is defined as:
211    /// `MIN(content.created_ts, event.origin_server_ts)`
212    pub expires: Duration,
213
214    /// A list of the focuses (foci) in use for this membership.
215    pub foci_active: Vec<Focus>,
216
217    /// The id of the membership.
218    ///
219    /// This is required to guarantee uniqueness of the event.
220    /// Sending the same state event twice to synapse makes the HS drop the second one and return
221    /// 200.
222    pub membership_id: String,
223}
224
225impl From<LegacyMembershipDataInit> for LegacyMembershipData {
226    fn from(init: LegacyMembershipDataInit) -> Self {
227        let LegacyMembershipDataInit {
228            application,
229            device_id,
230            expires,
231            foci_active,
232            membership_id,
233        } = init;
234        Self { application, device_id, expires, created_ts: None, foci_active, membership_id }
235    }
236}
237
238/// Stores all the information for a MatrixRTC membership. (one for each device)
239#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
240#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
241pub struct SessionMembershipData {
242    /// The type of the MatrixRTC session the membership belongs to.
243    ///
244    /// e.g. call, spacial, document...
245    #[serde(flatten)]
246    pub application: Application,
247
248    /// The device id of this membership.
249    ///
250    /// The same user can join with their phone/computer.
251    pub device_id: OwnedDeviceId,
252
253    /// A list of the foci that this membership proposes to use.
254    pub foci_preferred: Vec<Focus>,
255
256    /// Data required to determine the currently used focus by this member.
257    pub focus_active: ActiveFocus,
258
259    /// Stores a copy of the `origin_server_ts` of the initial session event.
260    ///
261    /// If the membership is updated this field will be used to track the
262    /// original `origin_server_ts`.
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub created_ts: Option<MilliSecondsSinceUnixEpoch>,
265
266    /// The duration in milliseconds relative to the time this membership joined
267    /// during which the membership is valid.
268    ///
269    /// The time a member has joined is defined as:
270    /// `MIN(content.created_ts, event.origin_server_ts)`
271    #[serde(with = "ruma_common::serde::duration::ms")]
272    pub expires: Duration,
273}
274
275/// The type of the MatrixRTC session.
276///
277/// This is not the application/client used by the user but the
278/// type of MatrixRTC session e.g. calling (`m.call`), third-room, whiteboard could be
279/// possible applications.
280#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
281#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
282#[serde(tag = "application")]
283pub enum Application {
284    /// The rtc application (session type) for VoIP call.
285    #[serde(rename = "m.call")]
286    Call(CallApplicationContent),
287}
288
289/// Call specific parameters of a `m.call.member` event.
290#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
291#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
292pub struct CallApplicationContent {
293    /// An identifier for calls.
294    ///
295    /// All members using the same `call_id` will end up in the same call.
296    ///
297    /// Does not need to be a uuid.
298    ///
299    /// `""` is used for room scoped calls.
300    pub call_id: String,
301
302    /// Who owns/joins/controls (can modify) the call.
303    pub scope: CallScope,
304}
305
306impl CallApplicationContent {
307    /// Initialize a [`CallApplicationContent`].
308    ///
309    /// # Arguments
310    ///
311    /// * `call_id` - An identifier for calls. All members using the same `call_id` will end up in
312    ///   the same call. Does not need to be a uuid. `""` is used for room scoped calls.
313    /// * `scope` - Who owns/joins/controls (can modify) the call.
314    pub fn new(call_id: String, scope: CallScope) -> Self {
315        Self { call_id, scope }
316    }
317}
318
319/// The call scope defines different call ownership models.
320#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
321#[derive(Clone, StringEnum)]
322#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
323#[ruma_enum(rename_all(prefix = "m.", rule = "snake_case"))]
324pub enum CallScope {
325    /// A call which every user of a room can join and create.
326    ///
327    /// There is no particular name associated with it.
328    ///
329    /// There can only be one per room.
330    Room,
331
332    /// A user call is owned by a user.
333    ///
334    /// Each user can create one there can be multiple per room. They are started and ended by the
335    /// owning user.
336    User,
337
338    #[doc(hidden)]
339    _Custom(PrivOwnedStr),
340}