Skip to main content

matrix_sdk/
automatic_call_status.rs

1// Copyright 2026 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//! Automatic mirroring of this device's MatrixRTC participation into the
16//! [MSC4426] `m.call` profile field.
17//!
18//! [MSC4426]: https://github.com/matrix-org/matrix-spec-proposals/pull/4426
19
20use std::{
21    collections::HashSet,
22    sync::{Arc, Mutex},
23    time::SystemTime,
24};
25
26use matrix_sdk_common::executor::spawn;
27use ruma::{
28    OwnedRoomId, SecondsSinceUnixEpoch,
29    events::{OriginalSyncStateEvent, call::member::CallMemberEventContent},
30};
31use tracing::warn;
32
33use crate::{Client, Room, client::WeakClient, event_handler::EventHandlerHandle};
34
35/// Owns the `m.call.member` event handler for auto-syncing the `m.call`
36/// profile field. Dropping this struct deregisters the handler.
37/// Holds a [`WeakClient`] rather than a strong `Client` to avoid a
38/// reference cycle.
39pub(crate) struct AutomaticCallStatus {
40    handle: EventHandlerHandle,
41    client: WeakClient,
42}
43
44/// Rooms in which this device is currently participating in an active
45/// MatrixRTC call. Maintained incrementally from `m.call.member` events.
46type ActiveCallRooms = Arc<Mutex<HashSet<OwnedRoomId>>>;
47
48impl Client {
49    /// Enable or disable automatic mirroring of this device's MatrixRTC
50    /// participation into the [MSC4426] `m.call` profile field. Off by
51    /// default.
52    ///
53    /// Toggling `false -> true` registers a typed event handler for
54    /// `m.call.member` state events. Toggling `true -> false` deregisters
55    /// it.
56    ///
57    /// Toggling `true -> false` does NOT clear `m.call` on the server, you
58    /// should call [`crate::Account::clear_call`] explicitly if that is
59    /// desired.
60    ///
61    /// [MSC4426]: https://github.com/matrix-org/matrix-spec-proposals/pull/4426
62    pub fn enable_automatic_call_status(&self, enabled: bool) {
63        let mut automatic_call_status = self.inner.automatic_call_status.lock().unwrap();
64        match (enabled, automatic_call_status.is_some()) {
65            (true, false) => {
66                *automatic_call_status = Some(AutomaticCallStatus::new(self));
67            }
68            (false, true) => *automatic_call_status = None,
69            _ => {}
70        }
71    }
72}
73
74impl AutomaticCallStatus {
75    fn new(client: &Client) -> Self {
76        // Start empty: `m.call` is shared across the user's devices, so we
77        // deliberately don't reconcile from current room state on start-up
78        // to avoid stomping on a status set by another device. The
79        // trade-off is that a crash/kill while on a call leaves `m.call`
80        // set until the user clears it manually.
81        let rooms: ActiveCallRooms = Arc::new(Mutex::new(HashSet::new()));
82        let handle = client.add_event_handler(
83            async move |event: OriginalSyncStateEvent<CallMemberEventContent>,
84                        room: Room,
85                        client: Client| {
86                on_event(&rooms, event, room, client);
87            },
88        );
89        let weak_client = WeakClient::from_client(client);
90        Self { handle, client: weak_client }
91    }
92}
93
94impl Drop for AutomaticCallStatus {
95    fn drop(&mut self) {
96        if let Some(client) = self.client.get() {
97            client.remove_event_handler(self.handle.clone());
98        }
99    }
100}
101
102fn on_event(
103    rooms: &ActiveCallRooms,
104    event: OriginalSyncStateEvent<CallMemberEventContent>,
105    room: Room,
106    client: Client,
107) {
108    let Some(own_user_id) = client.user_id() else { return };
109    let Some(own_device_id) = client.device_id() else { return };
110
111    // Ignore events for other users' memberships.
112    if event.state_key.user_id() != own_user_id {
113        return;
114    }
115
116    // Update the aggregate for this room, then return early if the "in any
117    // call" boolean didn't flip.
118    let is_device_in_room_call = room.is_device_in_active_room_call(own_user_id, own_device_id);
119    let room_id = room.room_id().to_owned();
120    let (was_in_call, now_in_call) = {
121        let mut rooms = rooms.lock().unwrap();
122        let was_in_call = !rooms.is_empty();
123        if is_device_in_room_call {
124            rooms.insert(room_id);
125        } else {
126            rooms.remove(&room_id);
127        }
128        (was_in_call, !rooms.is_empty())
129    };
130    // Return early if no change.
131    if was_in_call == now_in_call {
132        return;
133    }
134    let active_call_rooms = rooms.clone();
135    spawn(async move {
136        let in_call = !active_call_rooms.lock().unwrap().is_empty();
137        let result = if in_call {
138            let joined_ts = SecondsSinceUnixEpoch::from_system_time(SystemTime::now());
139            client.account().set_call(joined_ts).await
140        } else {
141            client.account().clear_call().await
142        };
143        if let Err(error) = result {
144            warn!(?error, in_call, "m.call auto-sync request failed");
145        }
146    });
147}