Skip to content

Commit bd9a895

Browse files
committed
chore(base): Move Room::*room_call* methods in the new call module.
This patch moves the `Room::has_active_room_call` and `Room::active_room_call_participants` methods into the new `call` module. This patch also moves the associated tests in this new module.
1 parent a71e792 commit bd9a895

File tree

2 files changed

+282
-262
lines changed

2 files changed

+282
-262
lines changed
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
// Copyright 2025 The Matrix.org Foundation C.I.C.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use ruma::OwnedUserId;
16+
17+
use super::Room;
18+
19+
impl Room {
20+
/// Is there a non expired membership with application `m.call` and scope
21+
/// `m.room` in this room.
22+
pub fn has_active_room_call(&self) -> bool {
23+
self.inner.read().has_active_room_call()
24+
}
25+
26+
/// Returns a `Vec` of `OwnedUserId`'s that participate in the room call.
27+
///
28+
/// MatrixRTC memberships with application `m.call` and scope `m.room` are
29+
/// considered. A user can occur twice if they join with two devices.
30+
/// Convert to a set depending if the different users are required or the
31+
/// amount of sessions.
32+
///
33+
/// The vector is ordered by oldest membership user to newest.
34+
pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
35+
self.inner.read().active_room_call_participants()
36+
}
37+
}
38+
39+
#[cfg(test)]
40+
mod tests {
41+
use std::{
42+
ops::Sub,
43+
sync::Arc,
44+
time::{Duration, SystemTime},
45+
};
46+
47+
use assign::assign;
48+
use matrix_sdk_test::{ALICE, BOB, CAROL};
49+
use ruma::{
50+
device_id, event_id,
51+
events::{
52+
call::member::{
53+
ActiveFocus, ActiveLivekitFocus, Application, CallApplicationContent,
54+
CallMemberEventContent, CallMemberStateKey, Focus, LegacyMembershipData,
55+
LegacyMembershipDataInit, LivekitFocus, OriginalSyncCallMemberEvent,
56+
},
57+
AnySyncStateEvent, StateUnsigned, SyncStateEvent,
58+
},
59+
room_id, user_id, DeviceId, EventId, MilliSecondsSinceUnixEpoch, OwnedUserId, UserId,
60+
};
61+
62+
use similar_asserts::assert_eq;
63+
64+
use super::super::{Room, RoomState};
65+
use crate::store::MemoryStore;
66+
67+
fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
68+
let store = Arc::new(MemoryStore::new());
69+
let user_id = user_id!("@me:example.org");
70+
let room_id = room_id!("!test:localhost");
71+
let (sender, _receiver) = tokio::sync::broadcast::channel(1);
72+
73+
(store.clone(), Room::new(user_id, store, room_id, room_type, sender))
74+
}
75+
76+
fn timestamp(minutes_ago: u32) -> MilliSecondsSinceUnixEpoch {
77+
MilliSecondsSinceUnixEpoch::from_system_time(
78+
SystemTime::now().sub(Duration::from_secs((60 * minutes_ago).into())),
79+
)
80+
.expect("date out of range")
81+
}
82+
83+
fn legacy_membership_for_my_call(
84+
device_id: &DeviceId,
85+
membership_id: &str,
86+
minutes_ago: u32,
87+
) -> LegacyMembershipData {
88+
let (application, foci) = foci_and_application();
89+
assign!(
90+
LegacyMembershipData::from(LegacyMembershipDataInit {
91+
application,
92+
device_id: device_id.to_owned(),
93+
expires: Duration::from_millis(3_600_000),
94+
foci_active: foci,
95+
membership_id: membership_id.to_owned(),
96+
}),
97+
{ created_ts: Some(timestamp(minutes_ago)) }
98+
)
99+
}
100+
101+
fn legacy_member_state_event(
102+
memberships: Vec<LegacyMembershipData>,
103+
ev_id: &EventId,
104+
user_id: &UserId,
105+
) -> AnySyncStateEvent {
106+
let content = CallMemberEventContent::new_legacy(memberships);
107+
108+
AnySyncStateEvent::CallMember(SyncStateEvent::Original(OriginalSyncCallMemberEvent {
109+
content,
110+
event_id: ev_id.to_owned(),
111+
sender: user_id.to_owned(),
112+
// we can simply use now here since this will be dropped when using a MinimalStateEvent
113+
// in the roomInfo
114+
origin_server_ts: timestamp(0),
115+
state_key: CallMemberStateKey::new(user_id.to_owned(), None, false),
116+
unsigned: StateUnsigned::new(),
117+
}))
118+
}
119+
120+
struct InitData<'a> {
121+
device_id: &'a DeviceId,
122+
minutes_ago: u32,
123+
}
124+
125+
fn session_member_state_event(
126+
ev_id: &EventId,
127+
user_id: &UserId,
128+
init_data: Option<InitData<'_>>,
129+
) -> AnySyncStateEvent {
130+
let application = Application::Call(CallApplicationContent::new(
131+
"my_call_id_1".to_owned(),
132+
ruma::events::call::member::CallScope::Room,
133+
));
134+
let foci_preferred = vec![Focus::Livekit(LivekitFocus::new(
135+
"my_call_foci_alias".to_owned(),
136+
"https://lk.org".to_owned(),
137+
))];
138+
let focus_active = ActiveFocus::Livekit(ActiveLivekitFocus::new());
139+
let (content, state_key) = match init_data {
140+
Some(InitData { device_id, minutes_ago }) => (
141+
CallMemberEventContent::new(
142+
application,
143+
device_id.to_owned(),
144+
focus_active,
145+
foci_preferred,
146+
Some(timestamp(minutes_ago)),
147+
),
148+
CallMemberStateKey::new(user_id.to_owned(), Some(device_id.to_owned()), false),
149+
),
150+
None => (
151+
CallMemberEventContent::new_empty(None),
152+
CallMemberStateKey::new(user_id.to_owned(), None, false),
153+
),
154+
};
155+
156+
AnySyncStateEvent::CallMember(SyncStateEvent::Original(OriginalSyncCallMemberEvent {
157+
content,
158+
event_id: ev_id.to_owned(),
159+
sender: user_id.to_owned(),
160+
// we can simply use now here since this will be dropped when using a MinimalStateEvent
161+
// in the roomInfo
162+
origin_server_ts: timestamp(0),
163+
state_key,
164+
unsigned: StateUnsigned::new(),
165+
}))
166+
}
167+
168+
fn foci_and_application() -> (Application, Vec<Focus>) {
169+
(
170+
Application::Call(CallApplicationContent::new(
171+
"my_call_id_1".to_owned(),
172+
ruma::events::call::member::CallScope::Room,
173+
)),
174+
vec![Focus::Livekit(LivekitFocus::new(
175+
"my_call_foci_alias".to_owned(),
176+
"https://lk.org".to_owned(),
177+
))],
178+
)
179+
}
180+
181+
fn receive_state_events(room: &Room, events: Vec<&AnySyncStateEvent>) {
182+
room.inner.update_if(|info| {
183+
let mut res = false;
184+
for ev in events {
185+
res |= info.handle_state_event(ev);
186+
}
187+
res
188+
});
189+
}
190+
191+
/// `user_a`: empty memberships
192+
/// `user_b`: one membership
193+
/// `user_c`: two memberships (two devices)
194+
fn legacy_create_call_with_member_events_for_user(a: &UserId, b: &UserId, c: &UserId) -> Room {
195+
let (_, room) = make_room_test_helper(RoomState::Joined);
196+
197+
let a_empty = legacy_member_state_event(Vec::new(), event_id!("$1234"), a);
198+
199+
// make b 10min old
200+
let m_init_b = legacy_membership_for_my_call(device_id!("DEVICE_0"), "0", 1);
201+
let b_one = legacy_member_state_event(vec![m_init_b], event_id!("$12345"), b);
202+
203+
// c1 1min old
204+
let m_init_c1 = legacy_membership_for_my_call(device_id!("DEVICE_0"), "0", 10);
205+
// c2 20min old
206+
let m_init_c2 = legacy_membership_for_my_call(device_id!("DEVICE_1"), "0", 20);
207+
let c_two = legacy_member_state_event(vec![m_init_c1, m_init_c2], event_id!("$123456"), c);
208+
209+
// Intentionally use a non time sorted receive order.
210+
receive_state_events(&room, vec![&c_two, &a_empty, &b_one]);
211+
212+
room
213+
}
214+
215+
/// `user_a`: empty memberships
216+
/// `user_b`: one membership
217+
/// `user_c`: two memberships (two devices)
218+
fn session_create_call_with_member_events_for_user(a: &UserId, b: &UserId, c: &UserId) -> Room {
219+
let (_, room) = make_room_test_helper(RoomState::Joined);
220+
221+
let a_empty = session_member_state_event(event_id!("$1234"), a, None);
222+
223+
// make b 10min old
224+
let b_one = session_member_state_event(
225+
event_id!("$12345"),
226+
b,
227+
Some(InitData { device_id: "DEVICE_0".into(), minutes_ago: 1 }),
228+
);
229+
230+
let m_c1 = session_member_state_event(
231+
event_id!("$123456_0"),
232+
c,
233+
Some(InitData { device_id: "DEVICE_0".into(), minutes_ago: 10 }),
234+
);
235+
let m_c2 = session_member_state_event(
236+
event_id!("$123456_1"),
237+
c,
238+
Some(InitData { device_id: "DEVICE_1".into(), minutes_ago: 20 }),
239+
);
240+
// Intentionally use a non time sorted receive order1
241+
receive_state_events(&room, vec![&m_c1, &m_c2, &a_empty, &b_one]);
242+
243+
room
244+
}
245+
246+
#[test]
247+
fn test_show_correct_active_call_state() {
248+
let room_legacy = legacy_create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL);
249+
250+
// This check also tests the ordering.
251+
// We want older events to be in the front.
252+
// user_b (Bob) is 1min old, c1 (CAROL) 10min old, c2 (CAROL) 20min old
253+
assert_eq!(
254+
vec![CAROL.to_owned(), CAROL.to_owned(), BOB.to_owned()],
255+
room_legacy.active_room_call_participants()
256+
);
257+
assert!(room_legacy.has_active_room_call());
258+
259+
let room_session = session_create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL);
260+
assert_eq!(
261+
vec![CAROL.to_owned(), CAROL.to_owned(), BOB.to_owned()],
262+
room_session.active_room_call_participants()
263+
);
264+
assert!(room_session.has_active_room_call());
265+
}
266+
267+
#[test]
268+
fn test_active_call_is_false_when_everyone_left() {
269+
let room = legacy_create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL);
270+
271+
let b_empty_membership = legacy_member_state_event(Vec::new(), event_id!("$1234_1"), &BOB);
272+
let c_empty_membership =
273+
legacy_member_state_event(Vec::new(), event_id!("$12345_1"), &CAROL);
274+
275+
receive_state_events(&room, vec![&b_empty_membership, &c_empty_membership]);
276+
277+
// We have no active call anymore after emptying the memberships
278+
assert_eq!(Vec::<OwnedUserId>::new(), room.active_room_call_participants());
279+
assert!(!room.has_active_room_call());
280+
}
281+
}

0 commit comments

Comments
 (0)