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}