use std::{collections::BTreeMap, sync::Arc}; use conduwuit::{ Result, Server, debug_info, utils::{self, IterStream}, }; use futures::StreamExt; use ruma::{ OwnedRoomId, OwnedUserId, RoomId, UserId, api::federation::transactions::edu::{Edu, TypingContent}, events::{SyncEphemeralRoomEvent, typing::TypingEventContent}, }; use tokio::sync::RwLock; use crate::{Dep, globals, sending, sending::EduBuf, sync, users}; pub struct Service { server: Arc, services: Services, /// u64 is unix timestamp of timeout pub typing: RwLock>>, /// timestamp of the last change to typing users pub last_typing_update: RwLock>, } struct Services { globals: Dep, sending: Dep, sync: Dep, users: Dep, } impl crate::Service for Service { fn build(args: crate::Args<'_>) -> Result> { Ok(Arc::new(Self { server: args.server.clone(), services: Services { globals: args.depend::("globals"), sending: args.depend::("sending"), sync: args.depend::("sync"), users: args.depend::("users"), }, typing: RwLock::new(BTreeMap::new()), last_typing_update: RwLock::new(BTreeMap::new()), })) } fn name(&self) -> &str { crate::service::make_name(std::module_path!()) } } impl Service { /// Sets a user as typing until the timeout timestamp is reached or /// roomtyping_remove is called. pub async fn typing_add( &self, user_id: &UserId, room_id: &RoomId, timeout: u64, ) -> Result<()> { debug_info!("typing started {user_id:?} in {room_id:?} timeout:{timeout:?}"); // update clients self.typing .write() .await .entry(room_id.to_owned()) .or_default() .insert(user_id.to_owned(), timeout); self.last_typing_update .write() .await .insert(room_id.to_owned(), self.services.globals.next_count()?); self.services.sync.wake_all_joined(room_id).await; // update federation if self.services.globals.user_is_local(user_id) { self.federation_send(room_id, user_id, true).await?; } Ok(()) } /// Removes a user from typing before the timeout is reached. pub async fn typing_remove(&self, user_id: &UserId, room_id: &RoomId) -> Result<()> { debug_info!("typing stopped {user_id:?} in {room_id:?}"); // update clients self.typing .write() .await .entry(room_id.to_owned()) .or_default() .remove(user_id); self.last_typing_update .write() .await .insert(room_id.to_owned(), self.services.globals.next_count()?); self.services.sync.wake_all_joined(room_id).await; // update federation if self.services.globals.user_is_local(user_id) { self.federation_send(room_id, user_id, false).await?; } Ok(()) } /// Makes sure that typing events with old timestamps get removed. async fn typings_maintain(&self, room_id: &RoomId) -> Result<()> { let current_timestamp = utils::millis_since_unix_epoch(); let mut removable = Vec::new(); { let typing = self.typing.read().await; let Some(room) = typing.get(room_id) else { return Ok(()); }; for (user, timeout) in room { if *timeout < current_timestamp { removable.push(user.clone()); } } }; if !removable.is_empty() { let typing = &mut self.typing.write().await; let room = typing.entry(room_id.to_owned()).or_default(); for user in &removable { debug_info!("typing timeout {user:?} in {room_id:?}"); room.remove(user); } // update clients self.last_typing_update .write() .await .insert(room_id.to_owned(), self.services.globals.next_count()?); self.services.sync.wake_all_joined(room_id).await; // update federation for user in &removable { if self.services.globals.user_is_local(user) { self.federation_send(room_id, user, false).await?; } } } Ok(()) } /// Returns the count of the last typing update in this room. pub async fn last_typing_update(&self, room_id: &RoomId) -> Result { self.typings_maintain(room_id).await?; Ok(self .last_typing_update .read() .await .get(room_id) .copied() .unwrap_or(0)) } pub async fn typing_users_for_user( &self, room_id: &RoomId, sender_user: &UserId, ) -> Result> { let room_typing_indicators = self.typing.read().await.get(room_id).cloned(); let Some(typing_indicators) = room_typing_indicators else { return Ok(Vec::new()); }; let user_ids: Vec<_> = typing_indicators .into_keys() .stream() .filter_map(|typing_user_id| async move { (!self .services .users .user_is_ignored(&typing_user_id, sender_user) .await) .then_some(typing_user_id) }) .collect() .await; Ok(user_ids) } /// Returns a new typing EDU. pub async fn typings_event_for_user( &self, room_id: &RoomId, sender_user: &UserId, ) -> Result> { let user_ids = self.typing_users_for_user(room_id, sender_user).await?; Ok(SyncEphemeralRoomEvent::new(TypingEventContent::new(user_ids))) } async fn federation_send( &self, room_id: &RoomId, user_id: &UserId, typing: bool, ) -> Result<()> { debug_assert!( self.services.globals.user_is_local(user_id), "tried to broadcast typing status of remote user", ); if !self.server.config.allow_outgoing_typing { return Ok(()); } let content = TypingContent::new(room_id.to_owned(), user_id.to_owned(), typing); let edu = Edu::Typing(content); let mut buf = EduBuf::new(); serde_json::to_writer(&mut buf, &edu).expect("Serialized Edu::Typing"); self.services.sending.send_edu_room(room_id, buf).await?; Ok(()) } }