1use ruma::{OwnedUserId, events::rtc::notification::CallIntent};
16
17use super::Room;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum CallIntentConsensus {
28 Full(CallIntent),
30 Partial {
32 intent: CallIntent,
34 agreeing_count: u64,
36 total_count: u64,
38 },
39 None,
41}
42
43impl Room {
44 pub fn has_active_room_call(&self) -> bool {
47 self.info.read().has_active_room_call()
48 }
49
50 pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
59 self.info.read().active_room_call_participants()
60 }
61
62 pub fn is_device_in_active_room_call(
66 &self,
67 user_id: &ruma::UserId,
68 device_id: &ruma::DeviceId,
69 ) -> bool {
70 self.info.read().is_device_in_active_room_call(user_id, device_id)
71 }
72
73 pub fn active_room_call_consensus_intent(&self) -> CallIntentConsensus {
76 self.info.read().active_room_call_consensus_intent()
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use std::{ops::Sub, sync::Arc, time::Duration};
83
84 use assign::assign;
85 use matrix_sdk_test::{User, event_factory::EventFactory};
86 use ruma::{
87 DeviceId, EventId, MilliSecondsSinceUnixEpoch, OwnedUserId, device_id, event_id,
88 events::{
89 AnySyncStateEvent,
90 call::member::{
91 ActiveFocus, ActiveLivekitFocus, Application, CallApplicationContent,
92 CallMemberEventContent, CallMemberStateKey, Focus, LegacyMembershipData,
93 LegacyMembershipDataInit, LivekitFocus,
94 },
95 rtc::notification::CallIntent,
96 },
97 room_id,
98 serde::Raw,
99 time::SystemTime,
100 };
101 use similar_asserts::assert_eq;
102
103 use super::{
104 super::{Room, RoomState},
105 CallIntentConsensus,
106 };
107 use crate::{
108 store::{MemoryStore, SaveLockedStateStore},
109 utils::RawStateEventWithKeys,
110 };
111
112 fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
113 let store = Arc::new(MemoryStore::new());
114 let room_id = room_id!("!test:localhost");
115 let (sender, _receiver) = tokio::sync::broadcast::channel(1);
116
117 (
118 store.clone(),
119 Room::new(
120 User::Alice.into(),
121 SaveLockedStateStore::new(store),
122 room_id,
123 room_type,
124 sender,
125 ),
126 )
127 }
128
129 fn timestamp(minutes_ago: u32) -> MilliSecondsSinceUnixEpoch {
130 MilliSecondsSinceUnixEpoch::from_system_time(
131 SystemTime::now().sub(Duration::from_secs((60 * minutes_ago).into())),
132 )
133 .expect("date out of range")
134 }
135
136 fn legacy_membership_for_my_call(
137 device_id: &DeviceId,
138 membership_id: &str,
139 minutes_ago: u32,
140 ) -> LegacyMembershipData {
141 let (application, foci) = foci_and_application();
142 assign!(
143 LegacyMembershipData::from(LegacyMembershipDataInit {
144 application,
145 device_id: device_id.to_owned(),
146 expires: Duration::from_millis(3_600_000),
147 foci_active: foci,
148 membership_id: membership_id.to_owned(),
149 }),
150 { created_ts: Some(timestamp(minutes_ago)) }
151 )
152 }
153
154 fn legacy_member_state_event(
155 memberships: Vec<LegacyMembershipData>,
156 ev_id: &EventId,
157 user: User,
158 ) -> Raw<AnySyncStateEvent> {
159 let content = CallMemberEventContent::new_legacy(memberships);
160 EventFactory::new()
161 .sender(user)
162 .event(content)
163 .state_key(CallMemberStateKey::new(user.into(), None, false).as_ref())
164 .event_id(ev_id)
165 .server_ts(timestamp(0))
168 .into()
169 }
170
171 struct InitData<'a> {
172 device_id: &'a DeviceId,
173 minutes_ago: u32,
174 }
175
176 fn session_member_state_event(
177 ev_id: &EventId,
178 user: User,
179 init_data: Option<InitData<'_>>,
180 ) -> Raw<AnySyncStateEvent> {
181 session_member_state_event_with_intent(ev_id, user, init_data, None)
182 }
183
184 fn session_member_state_event_with_intent(
185 ev_id: &EventId,
186 user: User,
187 init_data: Option<InitData<'_>>,
188 call_intent: Option<CallIntent>,
189 ) -> Raw<AnySyncStateEvent> {
190 let mut app_content = CallApplicationContent::new(
191 "my_call_id_1".to_owned(),
192 ruma::events::call::member::CallScope::Room,
193 );
194 app_content.call_intent = call_intent;
195
196 let application = Application::Call(app_content);
197 let foci_preferred = vec![Focus::Livekit(LivekitFocus::new(
198 "my_call_foci_alias".to_owned(),
199 "https://lk.org".to_owned(),
200 ))];
201 let focus_active = ActiveFocus::Livekit(ActiveLivekitFocus::new());
202
203 let (content, state_key) = match init_data {
204 Some(InitData { device_id, minutes_ago }) => {
205 let member_id = format!("{device_id}_m.call");
206 (
207 CallMemberEventContent::new(
208 application,
209 device_id.to_owned(),
210 focus_active,
211 foci_preferred,
212 Some(timestamp(minutes_ago)),
213 None,
214 ),
215 CallMemberStateKey::new(user.into(), Some(member_id), false),
216 )
217 }
218
219 None => (
220 CallMemberEventContent::new_empty(None),
221 CallMemberStateKey::new(user.into(), None, false),
222 ),
223 };
224
225 EventFactory::new()
226 .sender(user)
227 .event(content)
228 .state_key(state_key.as_ref())
229 .event_id(ev_id)
230 .server_ts(timestamp(0))
233 .into()
234 }
235
236 fn foci_and_application() -> (Application, Vec<Focus>) {
237 (
238 Application::Call(CallApplicationContent::new(
239 "my_call_id_1".to_owned(),
240 ruma::events::call::member::CallScope::Room,
241 )),
242 vec![Focus::Livekit(LivekitFocus::new(
243 "my_call_foci_alias".to_owned(),
244 "https://lk.org".to_owned(),
245 ))],
246 )
247 }
248
249 fn receive_state_events(room: &Room, events: Vec<Raw<AnySyncStateEvent>>) {
250 room.info.update_if(|info| {
251 let mut res = false;
252 for ev in events {
253 res |= info.handle_state_event(
254 &mut RawStateEventWithKeys::try_from_raw_state_event(ev)
255 .expect("generated state event should be valid"),
256 );
257 }
258 res
259 });
260 }
261
262 fn legacy_create_call_with_member_events_for_user(a: User, b: User, c: User) -> Room {
265 let (_, room) = make_room_test_helper(RoomState::Joined);
266
267 let a_empty = legacy_member_state_event(Vec::new(), event_id!("$1234"), a);
268
269 let m_init_b = legacy_membership_for_my_call(device_id!("DEVICE_0"), "0", 1);
271 let b_one = legacy_member_state_event(vec![m_init_b], event_id!("$12345"), b);
272
273 let m_init_c1 = legacy_membership_for_my_call(device_id!("DEVICE_0"), "0", 10);
275 let m_init_c2 = legacy_membership_for_my_call(device_id!("DEVICE_1"), "0", 20);
277 let c_two = legacy_member_state_event(vec![m_init_c1, m_init_c2], event_id!("$123456"), c);
278
279 receive_state_events(&room, vec![c_two, a_empty, b_one]);
281
282 room
283 }
284
285 fn session_create_call_with_member_events_for_user(a: User, b: User, c: User) -> Room {
288 let (_, room) = make_room_test_helper(RoomState::Joined);
289
290 let a_empty = session_member_state_event(event_id!("$1234"), a, None);
291
292 let b_one = session_member_state_event(
294 event_id!("$12345"),
295 b,
296 Some(InitData { device_id: "DEVICE_0".into(), minutes_ago: 1 }),
297 );
298
299 let m_c1 = session_member_state_event(
300 event_id!("$123456_0"),
301 c,
302 Some(InitData { device_id: "DEVICE_0".into(), minutes_ago: 10 }),
303 );
304 let m_c2 = session_member_state_event(
305 event_id!("$123456_1"),
306 c,
307 Some(InitData { device_id: "DEVICE_1".into(), minutes_ago: 20 }),
308 );
309 receive_state_events(&room, vec![m_c1, m_c2, a_empty, b_one]);
311
312 room
313 }
314
315 #[test]
316 fn test_show_correct_active_call_state() {
317 let room_legacy =
318 legacy_create_call_with_member_events_for_user(User::Alice, User::Bob, User::Carol);
319
320 assert_eq!(
324 room_legacy.active_room_call_participants(),
325 &[User::Carol, User::Carol, User::Bob],
326 );
327 assert!(room_legacy.has_active_room_call());
328
329 let room_session =
330 session_create_call_with_member_events_for_user(User::Alice, User::Bob, User::Carol);
331 assert_eq!(
332 room_session.active_room_call_participants(),
333 &[User::Carol, User::Carol, User::Bob],
334 );
335 assert!(room_session.has_active_room_call());
336 }
337
338 #[test]
339 fn test_active_call_is_false_when_everyone_left() {
340 let room =
341 legacy_create_call_with_member_events_for_user(User::Alice, User::Bob, User::Carol);
342
343 let b_empty_membership =
344 legacy_member_state_event(Vec::new(), event_id!("$1234_1"), User::Bob);
345 let c_empty_membership =
346 legacy_member_state_event(Vec::new(), event_id!("$12345_1"), User::Carol);
347
348 receive_state_events(&room, vec![b_empty_membership, c_empty_membership]);
349
350 assert_eq!(Vec::<OwnedUserId>::new(), room.active_room_call_participants());
352 assert!(!room.has_active_room_call());
353 }
354
355 fn consensus_setup(
356 alice_intent: Option<CallIntent>,
357 bob_intent: Option<CallIntent>,
358 carol_intent: Option<CallIntent>,
359 ) -> Vec<Raw<AnySyncStateEvent>> {
360 let alice_membership = session_member_state_event_with_intent(
361 event_id!("$1"),
362 User::Alice,
363 InitData { device_id: device_id!("AAA0"), minutes_ago: 1 }.into(),
364 alice_intent,
365 );
366 let bob_membership = session_member_state_event_with_intent(
367 event_id!("$1"),
368 User::Bob,
369 InitData { device_id: device_id!("BAA0"), minutes_ago: 1 }.into(),
370 bob_intent,
371 );
372 let carol_membership = session_member_state_event_with_intent(
373 event_id!("$2"),
374 User::Carol,
375 InitData { device_id: device_id!("CAA0"), minutes_ago: 1 }.into(),
376 carol_intent,
377 );
378 vec![alice_membership, bob_membership, carol_membership]
379 }
380
381 #[test]
382 fn test_consensus_intent() {
383 let test_cases = vec![
384 (None, None, None, CallIntentConsensus::None, "no intents"),
386 (
387 Some(CallIntent::Audio),
388 None,
389 None,
390 CallIntentConsensus::Partial {
391 intent: CallIntent::Audio,
392 agreeing_count: 1,
393 total_count: 3,
394 },
395 "one intent 1",
396 ),
397 (
398 None,
399 Some(CallIntent::Audio),
400 None,
401 CallIntentConsensus::Partial {
402 intent: CallIntent::Audio,
403 agreeing_count: 1,
404 total_count: 3,
405 },
406 "one intent 2",
407 ),
408 (
409 None,
410 None,
411 Some(CallIntent::Audio),
412 CallIntentConsensus::Partial {
413 intent: CallIntent::Audio,
414 agreeing_count: 1,
415 total_count: 3,
416 },
417 "one intent 3",
418 ),
419 (
420 None,
421 None,
422 Some(CallIntent::Video),
423 CallIntentConsensus::Partial {
424 intent: CallIntent::Video,
425 agreeing_count: 1,
426 total_count: 3,
427 },
428 "one intent 4",
429 ),
430 (
431 None,
432 Some(CallIntent::Video),
433 Some(CallIntent::Video),
434 CallIntentConsensus::Partial {
435 intent: CallIntent::Video,
436 agreeing_count: 2,
437 total_count: 3,
438 },
439 "two matching intents",
440 ),
441 (
442 Some(CallIntent::Video),
443 Some(CallIntent::Video),
444 Some(CallIntent::Video),
445 CallIntentConsensus::Full(CallIntent::Video),
446 "all agree",
447 ),
448 (
449 Some(CallIntent::Video),
450 None,
451 Some(CallIntent::Audio),
452 CallIntentConsensus::None,
453 "disagreement",
454 ),
455 (
456 Some(CallIntent::Video),
457 Some(CallIntent::Video),
458 Some(CallIntent::Audio),
459 CallIntentConsensus::None,
460 "disagreement 2",
461 ),
462 ];
463
464 for (alice, bob, carol, expected, description) in test_cases {
465 let (_, room) = make_room_test_helper(RoomState::Joined);
466 receive_state_events(&room, consensus_setup(alice, bob, carol));
467 let consensus_intent = room.active_room_call_consensus_intent();
468 assert_eq!(expected, consensus_intent, "Failed case: {}", description);
469 }
470 }
471
472 #[test]
473 fn test_is_device_in_active_room_call_matches_own_device_only() {
474 let (_, room) = make_room_test_helper(RoomState::Joined);
475
476 let alice_a = session_member_state_event(
477 event_id!("$alice_a"),
478 User::Alice,
479 Some(InitData { device_id: device_id!("DEVICE_A"), minutes_ago: 1 }),
480 );
481 let alice_b = session_member_state_event(
482 event_id!("$alice_b"),
483 User::Alice,
484 Some(InitData { device_id: device_id!("DEVICE_B"), minutes_ago: 1 }),
485 );
486 let bob_a = session_member_state_event(
487 event_id!("$bob_a"),
488 User::Bob,
489 Some(InitData { device_id: device_id!("DEVICE_A"), minutes_ago: 1 }),
490 );
491
492 receive_state_events(&room, vec![alice_a, alice_b, bob_a]);
493
494 assert!(room.is_device_in_active_room_call(User::Alice.into(), device_id!("DEVICE_A")));
495 assert!(room.is_device_in_active_room_call(User::Alice.into(), device_id!("DEVICE_B")));
496 assert!(!room.is_device_in_active_room_call(User::Alice.into(), device_id!("DEVICE_C")));
497 assert!(room.is_device_in_active_room_call(User::Bob.into(), device_id!("DEVICE_A")));
498 assert!(!room.is_device_in_active_room_call(User::Bob.into(), device_id!("DEVICE_B")));
499 }
500}