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