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