matrix_sdk_base/room/
call.rs

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
15use ruma::OwnedUserId;
16
17use super::Room;
18
19impl 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)]
40mod tests {
41    use std::{ops::Sub, sync::Arc, time::Duration};
42
43    use assign::assign;
44    use matrix_sdk_test::{ALICE, BOB, CAROL};
45    use ruma::{
46        device_id, event_id,
47        events::{
48            call::member::{
49                ActiveFocus, ActiveLivekitFocus, Application, CallApplicationContent,
50                CallMemberEventContent, CallMemberStateKey, Focus, LegacyMembershipData,
51                LegacyMembershipDataInit, LivekitFocus, OriginalSyncCallMemberEvent,
52            },
53            AnySyncStateEvent, StateUnsigned, SyncStateEvent,
54        },
55        room_id,
56        time::SystemTime,
57        user_id, DeviceId, EventId, MilliSecondsSinceUnixEpoch, OwnedUserId, UserId,
58    };
59    use similar_asserts::assert_eq;
60
61    use super::super::{Room, RoomState};
62    use crate::store::MemoryStore;
63
64    fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
65        let store = Arc::new(MemoryStore::new());
66        let user_id = user_id!("@me:example.org");
67        let room_id = room_id!("!test:localhost");
68        let (sender, _receiver) = tokio::sync::broadcast::channel(1);
69
70        (store.clone(), Room::new(user_id, store, room_id, room_type, sender))
71    }
72
73    fn timestamp(minutes_ago: u32) -> MilliSecondsSinceUnixEpoch {
74        MilliSecondsSinceUnixEpoch::from_system_time(
75            SystemTime::now().sub(Duration::from_secs((60 * minutes_ago).into())),
76        )
77        .expect("date out of range")
78    }
79
80    fn legacy_membership_for_my_call(
81        device_id: &DeviceId,
82        membership_id: &str,
83        minutes_ago: u32,
84    ) -> LegacyMembershipData {
85        let (application, foci) = foci_and_application();
86        assign!(
87            LegacyMembershipData::from(LegacyMembershipDataInit {
88                application,
89                device_id: device_id.to_owned(),
90                expires: Duration::from_millis(3_600_000),
91                foci_active: foci,
92                membership_id: membership_id.to_owned(),
93            }),
94            { created_ts: Some(timestamp(minutes_ago)) }
95        )
96    }
97
98    fn legacy_member_state_event(
99        memberships: Vec<LegacyMembershipData>,
100        ev_id: &EventId,
101        user_id: &UserId,
102    ) -> AnySyncStateEvent {
103        let content = CallMemberEventContent::new_legacy(memberships);
104
105        AnySyncStateEvent::CallMember(SyncStateEvent::Original(OriginalSyncCallMemberEvent {
106            content,
107            event_id: ev_id.to_owned(),
108            sender: user_id.to_owned(),
109            // we can simply use now here since this will be dropped when using a MinimalStateEvent
110            // in the roomInfo
111            origin_server_ts: timestamp(0),
112            state_key: CallMemberStateKey::new(user_id.to_owned(), None, false),
113            unsigned: StateUnsigned::new(),
114        }))
115    }
116
117    struct InitData<'a> {
118        device_id: &'a DeviceId,
119        minutes_ago: u32,
120    }
121
122    fn session_member_state_event(
123        ev_id: &EventId,
124        user_id: &UserId,
125        init_data: Option<InitData<'_>>,
126    ) -> AnySyncStateEvent {
127        let application = Application::Call(CallApplicationContent::new(
128            "my_call_id_1".to_owned(),
129            ruma::events::call::member::CallScope::Room,
130        ));
131        let foci_preferred = vec![Focus::Livekit(LivekitFocus::new(
132            "my_call_foci_alias".to_owned(),
133            "https://lk.org".to_owned(),
134        ))];
135        let focus_active = ActiveFocus::Livekit(ActiveLivekitFocus::new());
136        let (content, state_key) = match init_data {
137            Some(InitData { device_id, minutes_ago }) => (
138                CallMemberEventContent::new(
139                    application,
140                    device_id.to_owned(),
141                    focus_active,
142                    foci_preferred,
143                    Some(timestamp(minutes_ago)),
144                ),
145                CallMemberStateKey::new(user_id.to_owned(), Some(device_id.to_owned()), false),
146            ),
147            None => (
148                CallMemberEventContent::new_empty(None),
149                CallMemberStateKey::new(user_id.to_owned(), None, false),
150            ),
151        };
152
153        AnySyncStateEvent::CallMember(SyncStateEvent::Original(OriginalSyncCallMemberEvent {
154            content,
155            event_id: ev_id.to_owned(),
156            sender: user_id.to_owned(),
157            // we can simply use now here since this will be dropped when using a MinimalStateEvent
158            // in the roomInfo
159            origin_server_ts: timestamp(0),
160            state_key,
161            unsigned: StateUnsigned::new(),
162        }))
163    }
164
165    fn foci_and_application() -> (Application, Vec<Focus>) {
166        (
167            Application::Call(CallApplicationContent::new(
168                "my_call_id_1".to_owned(),
169                ruma::events::call::member::CallScope::Room,
170            )),
171            vec![Focus::Livekit(LivekitFocus::new(
172                "my_call_foci_alias".to_owned(),
173                "https://lk.org".to_owned(),
174            ))],
175        )
176    }
177
178    fn receive_state_events(room: &Room, events: Vec<&AnySyncStateEvent>) {
179        room.inner.update_if(|info| {
180            let mut res = false;
181            for ev in events {
182                res |= info.handle_state_event(ev);
183            }
184            res
185        });
186    }
187
188    /// `user_a`: empty memberships
189    /// `user_b`: one membership
190    /// `user_c`: two memberships (two devices)
191    fn legacy_create_call_with_member_events_for_user(a: &UserId, b: &UserId, c: &UserId) -> Room {
192        let (_, room) = make_room_test_helper(RoomState::Joined);
193
194        let a_empty = legacy_member_state_event(Vec::new(), event_id!("$1234"), a);
195
196        // make b 10min old
197        let m_init_b = legacy_membership_for_my_call(device_id!("DEVICE_0"), "0", 1);
198        let b_one = legacy_member_state_event(vec![m_init_b], event_id!("$12345"), b);
199
200        // c1 1min old
201        let m_init_c1 = legacy_membership_for_my_call(device_id!("DEVICE_0"), "0", 10);
202        // c2 20min old
203        let m_init_c2 = legacy_membership_for_my_call(device_id!("DEVICE_1"), "0", 20);
204        let c_two = legacy_member_state_event(vec![m_init_c1, m_init_c2], event_id!("$123456"), c);
205
206        // Intentionally use a non time sorted receive order.
207        receive_state_events(&room, vec![&c_two, &a_empty, &b_one]);
208
209        room
210    }
211
212    /// `user_a`: empty memberships
213    /// `user_b`: one membership
214    /// `user_c`: two memberships (two devices)
215    fn session_create_call_with_member_events_for_user(a: &UserId, b: &UserId, c: &UserId) -> Room {
216        let (_, room) = make_room_test_helper(RoomState::Joined);
217
218        let a_empty = session_member_state_event(event_id!("$1234"), a, None);
219
220        // make b 10min old
221        let b_one = session_member_state_event(
222            event_id!("$12345"),
223            b,
224            Some(InitData { device_id: "DEVICE_0".into(), minutes_ago: 1 }),
225        );
226
227        let m_c1 = session_member_state_event(
228            event_id!("$123456_0"),
229            c,
230            Some(InitData { device_id: "DEVICE_0".into(), minutes_ago: 10 }),
231        );
232        let m_c2 = session_member_state_event(
233            event_id!("$123456_1"),
234            c,
235            Some(InitData { device_id: "DEVICE_1".into(), minutes_ago: 20 }),
236        );
237        // Intentionally use a non time sorted receive order1
238        receive_state_events(&room, vec![&m_c1, &m_c2, &a_empty, &b_one]);
239
240        room
241    }
242
243    #[test]
244    fn test_show_correct_active_call_state() {
245        let room_legacy = legacy_create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL);
246
247        // This check also tests the ordering.
248        // We want older events to be in the front.
249        // user_b (Bob) is 1min old, c1 (CAROL) 10min old, c2 (CAROL) 20min old
250        assert_eq!(
251            vec![CAROL.to_owned(), CAROL.to_owned(), BOB.to_owned()],
252            room_legacy.active_room_call_participants()
253        );
254        assert!(room_legacy.has_active_room_call());
255
256        let room_session = session_create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL);
257        assert_eq!(
258            vec![CAROL.to_owned(), CAROL.to_owned(), BOB.to_owned()],
259            room_session.active_room_call_participants()
260        );
261        assert!(room_session.has_active_room_call());
262    }
263
264    #[test]
265    fn test_active_call_is_false_when_everyone_left() {
266        let room = legacy_create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL);
267
268        let b_empty_membership = legacy_member_state_event(Vec::new(), event_id!("$1234_1"), &BOB);
269        let c_empty_membership =
270            legacy_member_state_event(Vec::new(), event_id!("$12345_1"), &CAROL);
271
272        receive_state_events(&room, vec![&b_empty_membership, &c_empty_membership]);
273
274        // We have no active call anymore after emptying the memberships
275        assert_eq!(Vec::<OwnedUserId>::new(), room.active_room_call_participants());
276        assert!(!room.has_active_room_call());
277    }
278}