tuwunel_service/rooms/typing/
mod.rs1use std::{collections::BTreeMap, sync::Arc};
2
3use futures::{FutureExt, TryStreamExt, future::try_join};
4use ruma::{
5 OwnedRoomId, OwnedUserId, RoomId, UserId,
6 api::{
7 appservice::event::push_events::v1::EphemeralData,
8 federation::transactions::edu::{Edu, TypingContent},
9 },
10 events::{
11 EphemeralRoomEvent, GlobalAccountDataEventType, ignored_user_list::IgnoredUserListEvent,
12 typing::TypingEventContent,
13 },
14};
15use tokio::sync::{RwLock, broadcast};
16use tuwunel_core::{
17 Result, Server,
18 debug::INFO_SPAN_LEVEL,
19 debug_info, trace,
20 utils::{BoolExt, IterStream, millis_since_unix_epoch},
21};
22
23use crate::sending::EduBuf;
24
25pub struct Service {
26 server: Arc<Server>,
27 services: Arc<crate::services::OnceServices>,
28 typing: RwLock<BTreeMap<OwnedRoomId, RoomTyping>>,
29 pub typing_update_sender: broadcast::Sender<OwnedRoomId>,
30}
31
32#[derive(Default)]
33struct RoomTyping {
34 users: BTreeMap<OwnedUserId, u64>,
36 update: u64,
39}
40
41impl crate::Service for Service {
42 fn build(args: &crate::Args<'_>) -> Result<Arc<Self>> {
43 Ok(Arc::new(Self {
44 server: args.server.clone(),
45 services: args.services.clone(),
46 typing: RwLock::new(BTreeMap::new()),
47 typing_update_sender: broadcast::channel(100).0,
48 }))
49 }
50
51 fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
52}
53
54impl Service {
55 #[tracing::instrument(
58 name = "typing_start"
59 level = INFO_SPAN_LEVEL,
60 skip_all,
61 fields(
62 %room_id,
63 %user_id,
64 %timeout,
65 )
66 )]
67 pub async fn typing_add(&self, user_id: &UserId, room_id: &RoomId, timeout: u64) -> Result {
68 debug_info!("typing started {user_id:?} in {room_id:?} timeout:{timeout:?}");
69
70 let mut typing = self.typing.write().await;
72 let room = typing.entry(room_id.to_owned()).or_default();
73 room.users.insert(user_id.to_owned(), timeout);
74
75 let count = self.services.globals.next_count();
76
77 room.update = *count;
78
79 drop(typing);
80 drop(count);
81
82 if self
83 .typing_update_sender
84 .send(room_id.to_owned())
85 .is_err()
86 {
87 trace!("receiver found what it was looking for and is no longer interested");
88 }
89
90 let appservice_send = self.appservice_send(room_id);
92
93 let federation_send = self
95 .services
96 .globals
97 .user_is_local(user_id)
98 .then_async(|| self.federation_send(room_id, user_id, true))
99 .map(Option::transpose);
100
101 try_join(appservice_send, federation_send)
102 .await
103 .map(|_| ())
104 }
105
106 #[tracing::instrument(
108 name = "typing_stop"
109 level = INFO_SPAN_LEVEL,
110 skip_all,
111 fields(
112 %room_id,
113 %user_id,
114 )
115 )]
116 pub async fn typing_remove(&self, user_id: &UserId, room_id: &RoomId) -> Result {
117 debug_info!("typing stopped {user_id:?} in {room_id:?}");
118
119 let mut typing = self.typing.write().await;
121 let room = typing.entry(room_id.to_owned()).or_default();
122 room.users.remove(user_id);
123
124 let count = self.services.globals.next_count();
125
126 room.update = *count;
127
128 drop(typing);
129 drop(count);
130
131 if self
132 .typing_update_sender
133 .send(room_id.to_owned())
134 .is_err()
135 {
136 trace!("receiver found what it was looking for and is no longer interested");
137 }
138
139 let appservice_send = self.appservice_send(room_id);
141
142 let federation_send = self
144 .services
145 .globals
146 .user_is_local(user_id)
147 .then_async(|| self.federation_send(room_id, user_id, false))
148 .map(Option::transpose);
149
150 try_join(appservice_send, federation_send)
151 .await
152 .map(|_| ())
153 }
154
155 pub async fn wait_for_update(&self, room_id: &RoomId) {
156 let mut receiver = self.typing_update_sender.subscribe();
157 while let Ok(next) = receiver.recv().await {
158 if next == room_id {
159 break;
160 }
161 }
162 }
163
164 async fn typings_maintain(&self, room_id: &RoomId) -> Result {
166 let current_timestamp = millis_since_unix_epoch();
167 let typing = self.typing.read().await;
168 let has_expired = typing.get(room_id).is_some_and(|room| {
169 room.users
170 .values()
171 .any(|timeout| *timeout < current_timestamp)
172 });
173
174 drop(typing);
175
176 if !has_expired {
177 return Ok(());
178 }
179
180 let current_timestamp = millis_since_unix_epoch();
181 let mut removable = Vec::new();
182 let mut typing = self.typing.write().await;
183 let Some(room) = typing.get_mut(room_id) else {
184 return Ok(());
185 };
186
187 room.users.retain(|user, timeout| {
188 let expired = *timeout < current_timestamp;
189 if expired {
190 removable.push(user.clone());
191 }
192
193 expired.is_false()
194 });
195
196 if removable.is_empty() {
197 return Ok(());
198 }
199
200 let count = self.services.globals.next_count();
202
203 room.update = *count;
204
205 drop(typing);
206 drop(count);
207
208 for user in &removable {
209 debug_info!("typing timeout {user:?} in {room_id:?}");
210 }
211
212 if self
213 .typing_update_sender
214 .send(room_id.to_owned())
215 .is_err()
216 {
217 trace!("receiver found what it was looking for and is no longer interested");
218 }
219
220 let appservice_send = self.appservice_send(room_id);
222
223 let federation_sends = removable
225 .iter()
226 .filter(|user_id| self.services.globals.user_is_local(user_id))
227 .try_stream()
228 .try_for_each(|user_id| self.federation_send(room_id, user_id, false));
229
230 try_join(appservice_send, federation_sends)
231 .boxed()
232 .await
233 .map(|_| ())
234 }
235
236 pub async fn last_typing_update(&self, room_id: &RoomId) -> Result<u64> {
238 self.typings_maintain(room_id).await?;
239
240 self.typing
241 .read()
242 .await
243 .get(room_id)
244 .map(|room| room.update)
245 .map(Ok)
246 .unwrap_or(Ok(0))
247 }
248
249 async fn typings_content(&self, room_id: &RoomId) -> TypingEventContent {
251 let typing = self.typing.read().await;
252 let user_ids = typing
253 .get(room_id)
254 .into_iter()
255 .flat_map(|room| room.users.keys().cloned())
256 .collect();
257
258 TypingEventContent { user_ids }
259 }
260
261 async fn appservice_send(&self, room_id: &RoomId) -> Result {
263 let content = self.typings_content(room_id).await;
264
265 self.services
266 .sending
267 .send_edu_room_appservices(room_id, |buf| {
268 let edu = EphemeralData::Typing(EphemeralRoomEvent {
269 room_id: room_id.to_owned(),
270 content: content.clone(),
271 });
272
273 Ok(serde_json::to_writer(buf, &edu)?)
274 })
275 .await
276 }
277
278 pub async fn typing_users_for_user(
280 &self,
281 room_id: &RoomId,
282 sender_user: &UserId,
283 ) -> Result<Vec<OwnedUserId>> {
284 let typing = self.typing.read().await;
285 let user_ids = typing
286 .get(room_id)
287 .into_iter()
288 .flat_map(|room| room.users.keys().cloned())
289 .collect();
290 drop(typing);
291
292 Ok(self
293 .filter_typing_users(user_ids, sender_user)
294 .await)
295 }
296
297 pub async fn typing_snapshot_for_user<Select>(
303 &self,
304 room_id: &RoomId,
305 sender_user: &UserId,
306 select: Select,
307 ) -> Result<Option<(u64, Vec<OwnedUserId>)>>
308 where
309 Select: FnOnce(u64) -> bool + Send,
310 {
311 self.typings_maintain(room_id).await?;
312
313 let typing = self.typing.read().await;
314 let room = typing.get(room_id);
315 let update = room.map_or(0, |room| room.update);
316
317 if !select(update) {
318 return Ok(None);
319 }
320
321 let user_ids = room
322 .into_iter()
323 .flat_map(|room| room.users.keys().cloned())
324 .collect();
325
326 drop(typing);
327
328 let user_ids = self
329 .filter_typing_users(user_ids, sender_user)
330 .await;
331
332 Ok(Some((update, user_ids)))
333 }
334
335 async fn filter_typing_users(
336 &self,
337 user_ids: Vec<OwnedUserId>,
338 sender_user: &UserId,
339 ) -> Vec<OwnedUserId> {
340 if user_ids.is_empty() {
341 return user_ids;
342 }
343
344 let ignored: Option<IgnoredUserListEvent> = self
345 .services
346 .account_data
347 .get_global(sender_user, GlobalAccountDataEventType::IgnoredUserList)
348 .await
349 .ok();
350
351 user_ids
352 .into_iter()
353 .filter(|user_id| {
354 ignored.as_ref().is_none_or(|ignored| {
355 !ignored
356 .content
357 .ignored_users
358 .contains_key::<UserId>(user_id.as_ref())
359 })
360 })
361 .collect()
362 }
363
364 async fn federation_send(&self, room_id: &RoomId, user_id: &UserId, typing: bool) -> Result {
365 debug_assert!(
366 self.services.globals.user_is_local(user_id),
367 "tried to broadcast typing status of remote user",
368 );
369
370 if !self.server.config.allow_outgoing_typing {
371 return Ok(());
372 }
373
374 let content = TypingContent::new(room_id.to_owned(), user_id.to_owned(), typing);
375 let edu = Edu::Typing(content);
376
377 let mut buf = EduBuf::new();
378 serde_json::to_writer(&mut buf, &edu).expect("Serialized Edu::Typing");
379
380 self.services
381 .sending
382 .send_edu_room(room_id, buf)
383 .await?;
384
385 Ok(())
386 }
387}