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