mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-11-15 05:06:53 +00:00
Convert in-memory data structures in appservice, rooms::typing, and sync wakers to use SyncRwLock and SyncMutex. Drop appservice locks before making outbound network requests in room alias queries and federation invites.
273 lines
6.8 KiB
Rust
273 lines
6.8 KiB
Rust
use std::{collections::BTreeMap, sync::Arc};
|
|
|
|
use conduwuit::{
|
|
Result, Server, SyncRwLock, debug_info,
|
|
result::LogErr,
|
|
utils::{self, IterStream},
|
|
};
|
|
use futures::StreamExt;
|
|
use ruma::{
|
|
OwnedRoomId, OwnedUserId, RoomId, UserId,
|
|
api::{
|
|
appservice::event::push_events::v1::EphemeralData,
|
|
federation::transactions::edu::{Edu, TypingContent},
|
|
},
|
|
events::{EphemeralRoomEvent, SyncEphemeralRoomEvent, typing::TypingEventContent},
|
|
};
|
|
|
|
use crate::{Dep, globals, sending, sending::EduBuf, sync, users};
|
|
|
|
pub struct Service {
|
|
server: Arc<Server>,
|
|
services: Services,
|
|
/// u64 is unix timestamp of timeout
|
|
pub typing: SyncRwLock<BTreeMap<OwnedRoomId, BTreeMap<OwnedUserId, u64>>>,
|
|
/// timestamp of the last change to typing users
|
|
pub last_typing_update: SyncRwLock<BTreeMap<OwnedRoomId, u64>>,
|
|
}
|
|
|
|
struct Services {
|
|
globals: Dep<globals::Service>,
|
|
sending: Dep<sending::Service>,
|
|
sync: Dep<sync::Service>,
|
|
users: Dep<users::Service>,
|
|
}
|
|
|
|
impl crate::Service for Service {
|
|
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
|
Ok(Arc::new(Self {
|
|
server: args.server.clone(),
|
|
services: Services {
|
|
globals: args.depend::<globals::Service>("globals"),
|
|
sending: args.depend::<sending::Service>("sending"),
|
|
sync: args.depend::<sync::Service>("sync"),
|
|
users: args.depend::<users::Service>("users"),
|
|
},
|
|
typing: SyncRwLock::new(BTreeMap::new()),
|
|
last_typing_update: SyncRwLock::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()
|
|
.entry(room_id.to_owned())
|
|
.or_default()
|
|
.insert(user_id.to_owned(), timeout);
|
|
|
|
self.last_typing_update
|
|
.write()
|
|
.insert(room_id.to_owned(), self.services.globals.next_count()?);
|
|
|
|
self.services.sync.wake_all_joined(room_id).await;
|
|
|
|
// update appservices
|
|
_ = self.appservice_send(room_id).await.log_err();
|
|
|
|
// 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()
|
|
.entry(room_id.to_owned())
|
|
.or_default()
|
|
.remove(user_id);
|
|
|
|
self.last_typing_update
|
|
.write()
|
|
.insert(room_id.to_owned(), self.services.globals.next_count()?);
|
|
|
|
self.services.sync.wake_all_joined(room_id).await;
|
|
|
|
// update appservices
|
|
_ = self.appservice_send(room_id).await.log_err();
|
|
|
|
// 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();
|
|
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() {
|
|
return Ok(());
|
|
}
|
|
{
|
|
let mut typing = self.typing.write();
|
|
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()
|
|
.insert(room_id.to_owned(), self.services.globals.next_count()?);
|
|
|
|
self.services.sync.wake_all_joined(room_id).await;
|
|
|
|
// update appservices
|
|
_ = self.appservice_send(room_id).await.log_err();
|
|
|
|
// 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<u64> {
|
|
self.typings_maintain(room_id).await?;
|
|
Ok(self
|
|
.last_typing_update
|
|
.read()
|
|
.get(room_id)
|
|
.copied()
|
|
.unwrap_or(0))
|
|
}
|
|
|
|
/// Returns a new typing EDU's content.
|
|
pub fn typings_content(&self, room_id: &RoomId) -> TypingEventContent {
|
|
let room_typing_indicators = self.typing.read().get(room_id).cloned();
|
|
|
|
let Some(typing_indicators) = room_typing_indicators else {
|
|
return TypingEventContent::new(Vec::new());
|
|
};
|
|
|
|
let now = utils::millis_since_unix_epoch();
|
|
let user_ids: Vec<_> = typing_indicators
|
|
.into_iter()
|
|
.filter(|(_, timeout)| *timeout > now)
|
|
.map(|(user_id, _)| user_id)
|
|
.collect();
|
|
|
|
TypingEventContent::new(user_ids)
|
|
}
|
|
|
|
pub async fn typing_users_for_user(
|
|
&self,
|
|
room_id: &RoomId,
|
|
sender_user: &UserId,
|
|
) -> Result<Vec<OwnedUserId>> {
|
|
let room_typing_indicators = self.typing.read().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, filtered for a specific user
|
|
pub async fn typings_event_for_user(
|
|
&self,
|
|
room_id: &RoomId,
|
|
sender_user: &UserId,
|
|
) -> Result<SyncEphemeralRoomEvent<TypingEventContent>> {
|
|
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(())
|
|
}
|
|
|
|
async fn appservice_send(&self, room_id: &RoomId) -> Result<()> {
|
|
let edu = EphemeralData::Typing(EphemeralRoomEvent::new(
|
|
room_id.to_owned(),
|
|
self.typings_content(room_id),
|
|
));
|
|
|
|
let mut buf = EduBuf::new();
|
|
serde_json::to_writer(&mut buf, &edu).expect("Serialized Edu::Typing");
|
|
|
|
self.services
|
|
.sending
|
|
.send_edu_appservice_room(room_id, buf)
|
|
.await
|
|
}
|
|
}
|