matrix_sdk/room/
calls.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
15//!  Facilities to handle incoming calls.
16
17use ruma::{
18    EventId, OwnedUserId, UserId,
19    events::{
20        AnySyncMessageLikeEvent, AnySyncTimelineEvent,
21        rtc::decline::{RtcDeclineEventContent, SyncRtcDeclineEvent},
22    },
23};
24use thiserror::Error;
25use tokio::sync::broadcast;
26use tracing::instrument;
27
28use crate::{Room, event_handler::EventHandlerDropGuard, room::EventSource};
29
30/// An error occurring while interacting with a call/rtc event.
31#[derive(Debug, Error)]
32pub enum CallError {
33    /// We couldn't fetch the remote notification event.
34    #[error("Couldn't fetch the remote event: {0}")]
35    Fetch(Box<crate::Error>),
36
37    /// We tried to decline an event which is not of type m.rtc.notification.
38    #[error("You cannot decline this event type.")]
39    BadEventType,
40
41    /// We tried to decline a call started by ourselves.
42    #[error("You cannot decline your own call.")]
43    DeclineOwnCall,
44
45    /// We couldn't properly deserialize the target event.
46    #[error(transparent)]
47    Deserialize(#[from] serde_json::Error),
48}
49
50impl Room {
51    /// Create a new decline call event for the target notification event id .
52    ///
53    /// The event can then be sent with [`Room::send`] or a
54    /// [`crate::send_queue::RoomSendQueue`].
55    #[instrument(skip(self), fields(room = %self.room_id()))]
56    pub async fn make_decline_call_event(
57        &self,
58        notification_event_id: &EventId,
59    ) -> Result<RtcDeclineEventContent, CallError> {
60        make_call_decline_event(self, self.own_user_id(), notification_event_id).await
61    }
62
63    /// Subscribe to decline call event for this room.
64    ///
65    /// The returned receiver will receive the sender UserID for each decline
66    /// for the matching notify event.
67    /// Example:
68    /// - A push is received for an `m.rtc.notification` event.
69    /// - The app starts ringing on this device.
70    /// - The app subscribes to decline events for that notify event and stops
71    ///   ringing if another device declines the call.
72    ///
73    /// In case of outgoing call, you can subscribe to see if your call was
74    /// denied from the other side.
75    ///
76    /// ```rust
77    /// # async fn start_ringing() {}
78    /// # async fn stop_ringing() {}
79    /// # async fn show_incoming_call_ui() {}
80    /// # async fn dismiss_incoming_call_ui() {}
81    /// #
82    /// # async fn on_push_for_call_notify(room: matrix_sdk::Room, notify_event_id: &ruma::EventId) {
83    ///     // 1) We just received a push for an `m.rtc.notification` in `room`.
84    ///     show_incoming_call_ui().await;
85    ///     start_ringing().await;
86    ///
87    ///     // 2) Subscribe to declines for this notify event, in case the call is declined from another device.
88    ///     let (drop_guard, mut declines) = room.subscribe_to_call_decline_events(notify_event_id);
89    ///
90    ///     // Keep the subscription alive while we wait for a decline.
91    ///     // You might store `drop_guard` alongside your call state.
92    ///     tokio::spawn(async move {
93    ///         loop {
94    ///             match declines.recv().await {
95    ///                 Ok(_decliner) => {
96    ///                     // 3) Check the mxID -> I declined this call from another device.
97    ///                     stop_ringing().await;
98    ///                     dismiss_incoming_call_ui().await;
99    ///                     // Exiting ends the task; dropping the guard unsubscribes the handler.
100    ///                     drop(drop_guard);
101    ///                     break;
102    ///                 }
103    ///                 Err(broadcast_err) => {
104    ///                     // Channel closed or lagged; stop waiting.
105    ///                     // In practice you might want to handle Lagged specifically.
106    ///                     eprintln!("decline subscription ended: {broadcast_err}");
107    ///                     drop(drop_guard);
108    ///                     break;
109    ///                 }
110    ///             }
111    ///         }
112    ///     });
113    /// # }
114    /// ```
115    pub fn subscribe_to_call_decline_events(
116        &self,
117        notification_event_id: &EventId,
118    ) -> (EventHandlerDropGuard, broadcast::Receiver<OwnedUserId>) {
119        let (sender, receiver) = broadcast::channel(16);
120
121        let decline_call_event_handler_handle =
122            self.client.add_room_event_handler(self.room_id(), {
123                let own_notification_event_id = notification_event_id.to_owned();
124                move |event: SyncRtcDeclineEvent| async move {
125                    // Ignore decline for other unrelated notification events.
126                    if let Some(declined_event_id) =
127                        event.as_original().map(|ev| ev.content.relates_to.event_id.clone())
128                        && declined_event_id == own_notification_event_id
129                    {
130                        let _ = sender.send(event.sender().to_owned());
131                    }
132                }
133            });
134        let drop_guard = self.client().event_handler_drop_guard(decline_call_event_handler_handle);
135        (drop_guard, receiver)
136    }
137}
138
139async fn make_call_decline_event(
140    room: &Room,
141    own_user_id: &UserId,
142    notification_event_id: &EventId,
143) -> Result<RtcDeclineEventContent, CallError> {
144    let target = room
145        .get_event(notification_event_id)
146        .await
147        .map_err(|err| CallError::Fetch(Box::new(err)))?;
148
149    let event = target.raw().deserialize().map_err(CallError::Deserialize)?;
150
151    // The event must be RtcNotification-like.
152    if let AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RtcNotification(notify)) =
153        event
154    {
155        if notify.sender() == own_user_id {
156            // Cannot decline own call.
157            Err(CallError::DeclineOwnCall)
158        } else {
159            Ok(RtcDeclineEventContent::new(notification_event_id))
160        }
161    } else {
162        Err(CallError::BadEventType)
163    }
164}