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}