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}