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