Skip to main content

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