mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2025-06-29 05:04:35 +02:00
278 lines
8 KiB
Rust
278 lines
8 KiB
Rust
mod remote;
|
|
|
|
use std::sync::Arc;
|
|
|
|
use conduit::{
|
|
err,
|
|
utils::{stream::TryIgnore, ReadyExt},
|
|
Err, Error, Result,
|
|
};
|
|
use database::{Deserialized, Ignore, Interfix, Map};
|
|
use futures::{Stream, StreamExt};
|
|
use ruma::{
|
|
api::client::error::ErrorKind,
|
|
events::{
|
|
room::power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
|
|
StateEventType,
|
|
},
|
|
OwnedRoomId, OwnedServerName, OwnedUserId, RoomAliasId, RoomId, RoomOrAliasId, UserId,
|
|
};
|
|
|
|
use crate::{admin, appservice, appservice::RegistrationInfo, globals, rooms, sending, Dep};
|
|
|
|
pub struct Service {
|
|
db: Data,
|
|
services: Services,
|
|
}
|
|
|
|
struct Data {
|
|
alias_userid: Arc<Map>,
|
|
alias_roomid: Arc<Map>,
|
|
aliasid_alias: Arc<Map>,
|
|
}
|
|
|
|
struct Services {
|
|
admin: Dep<admin::Service>,
|
|
appservice: Dep<appservice::Service>,
|
|
globals: Dep<globals::Service>,
|
|
sending: Dep<sending::Service>,
|
|
state_accessor: Dep<rooms::state_accessor::Service>,
|
|
}
|
|
|
|
impl crate::Service for Service {
|
|
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
|
Ok(Arc::new(Self {
|
|
db: Data {
|
|
alias_userid: args.db["alias_userid"].clone(),
|
|
alias_roomid: args.db["alias_roomid"].clone(),
|
|
aliasid_alias: args.db["aliasid_alias"].clone(),
|
|
},
|
|
services: Services {
|
|
admin: args.depend::<admin::Service>("admin"),
|
|
appservice: args.depend::<appservice::Service>("appservice"),
|
|
globals: args.depend::<globals::Service>("globals"),
|
|
sending: args.depend::<sending::Service>("sending"),
|
|
state_accessor: args.depend::<rooms::state_accessor::Service>("rooms::state_accessor"),
|
|
},
|
|
}))
|
|
}
|
|
|
|
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
|
|
}
|
|
|
|
impl Service {
|
|
#[tracing::instrument(skip(self))]
|
|
pub fn set_alias(&self, alias: &RoomAliasId, room_id: &RoomId, user_id: &UserId) -> Result<()> {
|
|
if alias == self.services.globals.admin_alias && user_id != self.services.globals.server_user {
|
|
return Err(Error::BadRequest(
|
|
ErrorKind::forbidden(),
|
|
"Only the server user can set this alias",
|
|
));
|
|
}
|
|
|
|
// Comes first as we don't want a stuck alias
|
|
self.db
|
|
.alias_userid
|
|
.insert(alias.alias().as_bytes(), user_id.as_bytes());
|
|
|
|
self.db
|
|
.alias_roomid
|
|
.insert(alias.alias().as_bytes(), room_id.as_bytes());
|
|
|
|
let mut aliasid = room_id.as_bytes().to_vec();
|
|
aliasid.push(0xFF);
|
|
aliasid.extend_from_slice(&self.services.globals.next_count()?.to_be_bytes());
|
|
self.db.aliasid_alias.insert(&aliasid, alias.as_bytes());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tracing::instrument(skip(self))]
|
|
pub async fn remove_alias(&self, alias: &RoomAliasId, user_id: &UserId) -> Result<()> {
|
|
if !self.user_can_remove_alias(alias, user_id).await? {
|
|
return Err!(Request(Forbidden("User is not permitted to remove this alias.")));
|
|
}
|
|
|
|
let alias = alias.alias();
|
|
let Ok(room_id) = self.db.alias_roomid.get(&alias).await else {
|
|
return Err!(Request(NotFound("Alias does not exist or is invalid.")));
|
|
};
|
|
|
|
let prefix = (&room_id, Interfix);
|
|
self.db
|
|
.aliasid_alias
|
|
.keys_prefix_raw(&prefix)
|
|
.ignore_err()
|
|
.ready_for_each(|key| self.db.aliasid_alias.remove(key))
|
|
.await;
|
|
|
|
self.db.alias_roomid.remove(alias.as_bytes());
|
|
self.db.alias_userid.remove(alias.as_bytes());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn resolve(&self, room: &RoomOrAliasId) -> Result<OwnedRoomId> {
|
|
if room.is_room_id() {
|
|
let room_id: &RoomId = &RoomId::parse(room).expect("valid RoomId");
|
|
Ok(room_id.to_owned())
|
|
} else {
|
|
let alias: &RoomAliasId = &RoomAliasId::parse(room).expect("valid RoomAliasId");
|
|
Ok(self.resolve_alias(alias, None).await?.0)
|
|
}
|
|
}
|
|
|
|
#[tracing::instrument(skip(self), name = "resolve")]
|
|
pub async fn resolve_alias(
|
|
&self, room_alias: &RoomAliasId, servers: Option<&Vec<OwnedServerName>>,
|
|
) -> Result<(OwnedRoomId, Option<Vec<OwnedServerName>>)> {
|
|
if !self
|
|
.services
|
|
.globals
|
|
.server_is_ours(room_alias.server_name())
|
|
&& (!servers
|
|
.as_ref()
|
|
.is_some_and(|servers| servers.contains(&self.services.globals.server_name().to_owned()))
|
|
|| servers.as_ref().is_none())
|
|
{
|
|
return self.remote_resolve(room_alias, servers).await;
|
|
}
|
|
|
|
let room_id: Option<OwnedRoomId> = match self.resolve_local_alias(room_alias).await {
|
|
Ok(r) => Some(r),
|
|
Err(_) => self.resolve_appservice_alias(room_alias).await?,
|
|
};
|
|
|
|
room_id.map_or_else(
|
|
|| Err(Error::BadRequest(ErrorKind::NotFound, "Room with alias not found.")),
|
|
|room_id| Ok((room_id, None)),
|
|
)
|
|
}
|
|
|
|
#[tracing::instrument(skip(self), level = "debug")]
|
|
pub async fn resolve_local_alias(&self, alias: &RoomAliasId) -> Result<OwnedRoomId> {
|
|
self.db.alias_roomid.get(alias.alias()).await.deserialized()
|
|
}
|
|
|
|
#[tracing::instrument(skip(self), level = "debug")]
|
|
pub fn local_aliases_for_room<'a>(&'a self, room_id: &'a RoomId) -> impl Stream<Item = &RoomAliasId> + Send + 'a {
|
|
let prefix = (room_id, Interfix);
|
|
self.db
|
|
.aliasid_alias
|
|
.stream_prefix(&prefix)
|
|
.ignore_err()
|
|
.map(|(_, alias): (Ignore, &RoomAliasId)| alias)
|
|
}
|
|
|
|
#[tracing::instrument(skip(self), level = "debug")]
|
|
pub fn all_local_aliases<'a>(&'a self) -> impl Stream<Item = (&RoomId, &str)> + Send + 'a {
|
|
self.db
|
|
.alias_roomid
|
|
.stream()
|
|
.ignore_err()
|
|
.map(|(alias_localpart, room_id): (&str, &RoomId)| (room_id, alias_localpart))
|
|
}
|
|
|
|
async fn user_can_remove_alias(&self, alias: &RoomAliasId, user_id: &UserId) -> Result<bool> {
|
|
let room_id = self
|
|
.resolve_local_alias(alias)
|
|
.await
|
|
.map_err(|_| err!(Request(NotFound("Alias not found."))))?;
|
|
|
|
let server_user = &self.services.globals.server_user;
|
|
|
|
// The creator of an alias can remove it
|
|
if self
|
|
.who_created_alias(alias).await
|
|
.is_ok_and(|user| user == user_id)
|
|
// Server admins can remove any local alias
|
|
|| self.services.admin.user_is_admin(user_id).await
|
|
// Always allow the server service account to remove the alias, since there may not be an admin room
|
|
|| server_user == user_id
|
|
{
|
|
return Ok(true);
|
|
}
|
|
|
|
// Checking whether the user is able to change canonical aliases of the room
|
|
if let Ok(content) = self
|
|
.services
|
|
.state_accessor
|
|
.room_state_get_content::<RoomPowerLevelsEventContent>(&room_id, &StateEventType::RoomPowerLevels, "")
|
|
.await
|
|
{
|
|
return Ok(RoomPowerLevels::from(content).user_can_send_state(user_id, StateEventType::RoomCanonicalAlias));
|
|
}
|
|
|
|
// If there is no power levels event, only the room creator can change
|
|
// canonical aliases
|
|
if let Ok(event) = self
|
|
.services
|
|
.state_accessor
|
|
.room_state_get(&room_id, &StateEventType::RoomCreate, "")
|
|
.await
|
|
{
|
|
return Ok(event.sender == user_id);
|
|
}
|
|
|
|
Err!(Database("Room has no m.room.create event"))
|
|
}
|
|
|
|
async fn who_created_alias(&self, alias: &RoomAliasId) -> Result<OwnedUserId> {
|
|
self.db.alias_userid.get(alias.alias()).await.deserialized()
|
|
}
|
|
|
|
async fn resolve_appservice_alias(&self, room_alias: &RoomAliasId) -> Result<Option<OwnedRoomId>> {
|
|
use ruma::api::appservice::query::query_room_alias;
|
|
|
|
for appservice in self.services.appservice.read().await.values() {
|
|
if appservice.aliases.is_match(room_alias.as_str())
|
|
&& matches!(
|
|
self.services
|
|
.sending
|
|
.send_appservice_request(
|
|
appservice.registration.clone(),
|
|
query_room_alias::v1::Request {
|
|
room_alias: room_alias.to_owned(),
|
|
},
|
|
)
|
|
.await,
|
|
Ok(Some(_opt_result))
|
|
) {
|
|
return self
|
|
.resolve_local_alias(room_alias)
|
|
.await
|
|
.map_err(|_| err!(Request(NotFound("Room does not exist."))))
|
|
.map(Some);
|
|
}
|
|
}
|
|
|
|
Ok(None)
|
|
}
|
|
|
|
pub async fn appservice_checks(
|
|
&self, room_alias: &RoomAliasId, appservice_info: &Option<RegistrationInfo>,
|
|
) -> Result<()> {
|
|
if !self
|
|
.services
|
|
.globals
|
|
.server_is_ours(room_alias.server_name())
|
|
{
|
|
return Err(Error::BadRequest(ErrorKind::InvalidParam, "Alias is from another server."));
|
|
}
|
|
|
|
if let Some(ref info) = appservice_info {
|
|
if !info.aliases.is_match(room_alias.as_str()) {
|
|
return Err(Error::BadRequest(ErrorKind::Exclusive, "Room alias is not in namespace."));
|
|
}
|
|
} else if self
|
|
.services
|
|
.appservice
|
|
.is_exclusive_alias(room_alias)
|
|
.await
|
|
{
|
|
return Err(Error::BadRequest(ErrorKind::Exclusive, "Room alias reserved by appservice."));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|