mod namespace_regex; mod registration_info; use std::{ collections::{BTreeMap, HashSet}, iter::IntoIterator, sync::Arc, }; use async_trait::async_trait; use futures::{Future, FutureExt, Stream, StreamExt, TryStreamExt}; use ruma::{RoomAliasId, RoomId, UserId, api::appservice::Registration}; use tokio::sync::{RwLock, RwLockReadGuard}; use tuwunel_core::{Err, Result, debug, err, utils::stream::IterStream}; use tuwunel_database::Map; pub use self::{namespace_regex::NamespaceRegex, registration_info::RegistrationInfo}; pub struct Service { registration_info: RwLock, services: Arc, db: Data, } struct Data { id_appserviceregistrations: Arc, } type Registrations = BTreeMap; #[async_trait] impl crate::Service for Service { fn build(args: crate::Args<'_>) -> Result> { Ok(Arc::new(Self { registration_info: RwLock::new(BTreeMap::new()), services: args.services.clone(), db: Data { id_appserviceregistrations: args.db["id_appserviceregistrations"].clone(), }, })) } async fn worker(self: Arc) -> Result { self.init_registrations().await?; self.check_registrations().await?; Ok(()) } fn name(&self) -> &str { crate::service::make_name(std::module_path!()) } } impl Service { #[tracing::instrument(name = "init", skip(self))] async fn init_registrations(&self) -> Result { // Registrations from configuration file let confs = self .services .server .config .appservice .clone() .into_iter() .stream() .map(|(id, mut reg)| { reg.id.clone_from(&id); reg.sender_localpart .get_or_insert_with(|| id.clone()); Ok((id, reg)) }); // Registrations from database self.iter_db_ids() .chain(confs.map_ok(|(id, reg)| (id, reg.into()))) .try_for_each(async |(id, reg): (_, Registration)| { debug!(?id, ?reg, "appservice registration"); self.registration_info .write() .await .insert(id.clone(), reg.try_into()?) .map_or(Ok(()), |_| Err!("Conflicting Appservice ID: {id:?}")) }) .await } #[tracing::instrument(name = "check", skip(self))] async fn check_registrations(&self) -> Result { let regs = self.registration_info.read().await; let num_as_tokens = regs .values() .map(|info| &info.registration.as_token) .collect::>() .len(); if num_as_tokens < regs.len() { return Err!( "Conflicting Appservice registrations: each must have a unique as_token." ); } Ok(()) } /// Registers an appservice and returns the ID to the caller pub async fn register_appservice( &self, registration: &Registration, appservice_config_body: &str, ) -> Result { //TODO: Check for collisions between exclusive appservice namespaces self.registration_info .write() .await .insert(registration.id.clone(), registration.clone().try_into()?); self.db .id_appserviceregistrations .insert(®istration.id, appservice_config_body); Ok(()) } /// Remove an appservice registration /// /// # Arguments /// /// * `service_name` - the registration ID of the appservice pub async fn unregister_appservice(&self, appservice_id: &str) -> Result { // removes the appservice registration info self.registration_info .write() .await .remove(appservice_id) .ok_or_else(|| err!("Appservice not found"))?; // remove the appservice from the database self.db .id_appserviceregistrations .del(appservice_id); // deletes all active requests for the appservice if there are any so we stop // sending to the URL self.services .sending .cleanup_events(Some(appservice_id), None, None) .await } pub async fn get_registration(&self, id: &str) -> Option { self.registration_info .read() .await .get(id) .cloned() .map(|info| info.registration) } pub async fn find_from_access_token(&self, token: &str) -> Result { self.read() .await .values() .find(|info| info.registration.as_token == token) .cloned() .ok_or_else(|| err!(Request(NotFound("Missing or invalid appservice token")))) } /// Checks if a given user id matches any exclusive appservice regex pub async fn is_exclusive_user_id(&self, user_id: &UserId) -> bool { self.read() .await .values() .any(|info| info.is_exclusive_user_match(user_id)) } /// Checks if a given room alias matches any exclusive appservice regex pub async fn is_exclusive_alias(&self, alias: &RoomAliasId) -> bool { self.read() .await .values() .any(|info| info.aliases.is_exclusive_match(alias.as_str())) } /// Checks if a given room id matches any exclusive appservice regex /// /// TODO: use this? #[allow(dead_code)] pub async fn is_exclusive_room_id(&self, room_id: &RoomId) -> bool { self.read() .await .values() .any(|info| info.rooms.is_exclusive_match(room_id.as_str())) } pub fn iter_ids(&self) -> impl Stream + Send { self.read() .map(|info| info.keys().cloned().collect::>()) .map(IntoIterator::into_iter) .map(IterStream::stream) .flatten_stream() } pub fn iter_db_ids(&self) -> impl Stream> + Send { self.db .id_appserviceregistrations .keys() .and_then(async move |id: &str| { Ok((id.to_owned(), self.get_db_registration(id).await?)) }) } pub async fn get_db_registration(&self, id: &str) -> Result { self.db .id_appserviceregistrations .get(id) .await .and_then(|ref bytes| serde_yaml::from_slice(bytes).map_err(Into::into)) .map_err(|e| err!(Database("Invalid appservice {id:?} registration: {e:?}"))) } pub fn read(&self) -> impl Future> + Send { self.registration_info.read() } }