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