Files
continuwuity/src/api/client/directory.rs
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

428 lines
12 KiB
Rust
Raw Normal View History

2024-07-16 08:05:25 +00:00
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
2024-08-08 17:18:30 +00:00
use conduit::{info, warn, Err, Error, Result};
use futures::{StreamExt, TryFutureExt};
2020-07-30 18:14:47 +02:00
use ruma::{
api::{
client::{
2022-02-18 15:33:14 +01:00
directory::{get_public_rooms, get_public_rooms_filtered, get_room_visibility, set_room_visibility},
error::ErrorKind,
room,
2020-07-30 18:14:47 +02:00
},
federation,
2020-07-30 18:14:47 +02:00
},
2022-12-14 13:09:10 +01:00
directory::{Filter, PublicRoomJoinRule, PublicRoomsChunk, RoomNetwork},
2020-07-30 18:14:47 +02:00
events::{
room::{
join_rules::{JoinRule, RoomJoinRulesEventContent},
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
},
2022-04-06 21:31:29 +02:00
StateEventType,
2020-07-30 18:14:47 +02:00
},
2024-08-08 17:18:30 +00:00
uint, OwnedRoomId, RoomId, ServerName, UInt, UserId,
2020-07-30 18:14:47 +02:00
};
2024-07-22 07:43:51 +00:00
use service::Services;
2020-07-30 18:14:47 +02:00
2024-07-22 07:43:51 +00:00
use crate::Ruma;
2024-03-05 19:48:54 -05:00
/// # `POST /_matrix/client/v3/publicRooms`
2021-08-31 19:14:37 +02:00
///
/// Lists the public rooms on this server.
///
/// - Rooms are ordered by the number of joined members
2024-06-17 04:12:11 +00:00
#[tracing::instrument(skip_all, fields(%client), name = "publicrooms")]
2024-04-22 23:48:57 -04:00
pub(crate) async fn get_public_rooms_filtered_route(
2024-07-16 08:05:25 +00:00
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_public_rooms_filtered::v3::Request>,
2022-02-18 15:33:14 +01:00
) -> Result<get_public_rooms_filtered::v3::Response> {
if let Some(server) = &body.server {
2024-07-16 08:05:25 +00:00
if services
.globals
.forbidden_remote_room_directory_server_names()
.contains(server)
{
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"Server is banned on this homeserver.",
));
}
}
let response = get_public_rooms_filtered_helper(
2024-07-27 07:17:07 +00:00
&services,
2020-09-14 11:42:16 +02:00
body.server.as_deref(),
body.limit,
body.since.as_deref(),
&body.filter,
&body.room_network,
)
.await
.map_err(|e| {
warn!(?body.server, "Failed to return /publicRooms: {e}");
Error::BadRequest(ErrorKind::Unknown, "Failed to return the requested server's public room list.")
})?;
Ok(response)
}
/// # `GET /_matrix/client/v3/publicRooms`
2021-08-31 19:14:37 +02:00
///
/// Lists the public rooms on this server.
///
/// - Rooms are ordered by the number of joined members
2024-06-17 04:12:11 +00:00
#[tracing::instrument(skip_all, fields(%client), name = "publicrooms")]
2024-04-22 23:48:57 -04:00
pub(crate) async fn get_public_rooms_route(
2024-07-16 08:05:25 +00:00
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_public_rooms::v3::Request>,
2022-02-18 15:33:14 +01:00
) -> Result<get_public_rooms::v3::Response> {
if let Some(server) = &body.server {
2024-07-16 08:05:25 +00:00
if services
.globals
.forbidden_remote_room_directory_server_names()
.contains(server)
{
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"Server is banned on this homeserver.",
));
}
}
let response = get_public_rooms_filtered_helper(
2024-07-27 07:17:07 +00:00
&services,
body.server.as_deref(),
body.limit,
body.since.as_deref(),
2022-12-14 13:09:10 +01:00
&Filter::default(),
&RoomNetwork::Matrix,
)
.await
.map_err(|e| {
warn!(?body.server, "Failed to return /publicRooms: {e}");
Error::BadRequest(ErrorKind::Unknown, "Failed to return the requested server's public room list.")
})?;
2022-02-18 15:33:14 +01:00
Ok(get_public_rooms::v3::Response {
chunk: response.chunk,
prev_batch: response.prev_batch,
next_batch: response.next_batch,
total_room_count_estimate: response.total_room_count_estimate,
2022-01-22 16:58:32 +01:00
})
}
2021-08-31 19:14:37 +02:00
/// # `PUT /_matrix/client/r0/directory/list/room/{roomId}`
///
/// Sets the visibility of a given room in the room directory.
2024-06-17 04:12:11 +00:00
#[tracing::instrument(skip_all, fields(%client), name = "room_directory")]
2024-04-22 23:48:57 -04:00
pub(crate) async fn set_room_visibility_route(
2024-07-16 08:05:25 +00:00
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
body: Ruma<set_room_visibility::v3::Request>,
2022-02-18 15:33:14 +01:00
) -> Result<set_room_visibility::v3::Response> {
2020-11-15 12:17:21 +01:00
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
2024-08-08 17:18:30 +00:00
if !services.rooms.metadata.exists(&body.room_id).await {
2022-09-10 18:14:29 +02:00
// Return 404 if the room doesn't exist
return Err(Error::BadRequest(ErrorKind::NotFound, "Room not found"));
2022-09-10 18:14:29 +02:00
}
2024-08-08 17:18:30 +00:00
if services
.users
.is_deactivated(sender_user)
.await
.unwrap_or(false)
&& body.appservice_info.is_none()
{
return Err!(Request(Forbidden("Guests cannot publish to room directories")));
}
2024-08-08 17:18:30 +00:00
if !user_can_publish_room(&services, sender_user, &body.room_id).await? {
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"User is not allowed to publish this room",
));
}
match &body.visibility {
2020-11-15 12:17:21 +01:00
room::Visibility::Public => {
if services.globals.config.lockdown_public_room_directory
2024-08-08 17:18:30 +00:00
&& !services.users.is_admin(sender_user).await
&& body.appservice_info.is_none()
{
info!(
"Non-admin user {sender_user} tried to publish {0} to the room directory while \
\"lockdown_public_room_directory\" is enabled",
body.room_id
);
if services.globals.config.admin_room_notices {
services
.admin
.send_text(&format!(
"Non-admin user {sender_user} tried to publish {0} to the room directory while \
\"lockdown_public_room_directory\" is enabled",
body.room_id
))
.await;
}
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"Publishing rooms to the room directory is not allowed",
));
}
2024-08-08 17:18:30 +00:00
services.rooms.directory.set_public(&body.room_id);
if services.globals.config.admin_room_notices {
services
.admin
.send_text(&format!("{sender_user} made {} public to the room directory", body.room_id))
.await;
}
info!("{sender_user} made {0} public to the room directory", body.room_id);
2020-11-15 12:17:21 +01:00
},
2024-08-08 17:18:30 +00:00
room::Visibility::Private => services.rooms.directory.set_not_public(&body.room_id),
2021-07-15 19:54:04 +02:00
_ => {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Room visibility type is not supported.",
));
},
}
2022-02-18 15:33:14 +01:00
Ok(set_room_visibility::v3::Response {})
}
2021-08-31 19:14:37 +02:00
/// # `GET /_matrix/client/r0/directory/list/room/{roomId}`
///
/// Gets the visibility of a given room in the room directory.
2024-04-22 23:48:57 -04:00
pub(crate) async fn get_room_visibility_route(
2024-07-16 08:05:25 +00:00
State(services): State<crate::State>, body: Ruma<get_room_visibility::v3::Request>,
2022-02-18 15:33:14 +01:00
) -> Result<get_room_visibility::v3::Response> {
2024-08-08 17:18:30 +00:00
if !services.rooms.metadata.exists(&body.room_id).await {
2022-09-10 18:14:29 +02:00
// Return 404 if the room doesn't exist
return Err(Error::BadRequest(ErrorKind::NotFound, "Room not found"));
2022-09-10 18:14:29 +02:00
}
2022-02-18 15:33:14 +01:00
Ok(get_room_visibility::v3::Response {
2024-08-08 17:18:30 +00:00
visibility: if services.rooms.directory.is_public_room(&body.room_id).await {
room::Visibility::Public
} else {
room::Visibility::Private
},
2022-01-22 16:58:32 +01:00
})
}
2021-08-31 19:14:37 +02:00
pub(crate) async fn get_public_rooms_filtered_helper(
2024-07-16 08:05:25 +00:00
services: &Services, server: Option<&ServerName>, limit: Option<UInt>, since: Option<&str>, filter: &Filter,
_network: &RoomNetwork,
2022-02-18 15:33:14 +01:00
) -> Result<get_public_rooms_filtered::v3::Response> {
2024-07-22 07:43:51 +00:00
if let Some(other_server) = server.filter(|server_name| !services.globals.server_is_ours(server_name)) {
2024-07-16 08:05:25 +00:00
let response = services
2020-12-19 16:00:11 +01:00
.sending
.send_federation_request(
2021-01-14 14:39:56 -05:00
other_server,
2020-12-19 16:00:11 +01:00
federation::directory::get_public_rooms_filtered::v1::Request {
limit,
2022-12-14 13:09:10 +01:00
since: since.map(ToOwned::to_owned),
2020-12-19 16:00:11 +01:00
filter: Filter {
2022-12-14 13:09:10 +01:00
generic_search_term: filter.generic_search_term.clone(),
2022-10-09 17:25:06 +02:00
room_types: filter.room_types.clone(),
2020-12-19 16:00:11 +01:00
},
room_network: RoomNetwork::Matrix,
2020-09-14 16:23:15 +02:00
},
2020-12-19 16:00:11 +01:00
)
.await?;
2022-02-18 15:33:14 +01:00
return Ok(get_public_rooms_filtered::v3::Response {
2022-02-18 11:52:00 +01:00
chunk: response.chunk,
prev_batch: response.prev_batch,
next_batch: response.next_batch,
total_room_count_estimate: response.total_room_count_estimate,
2022-01-22 16:58:32 +01:00
});
}
2024-05-04 09:45:37 -04:00
// Use limit or else 10, with maximum 100
let limit = limit.map_or(10, u64::from);
2024-05-04 09:45:37 -04:00
let mut num_since: u64 = 0;
2020-07-30 18:14:47 +02:00
if let Some(s) = &since {
2020-07-30 22:09:11 +02:00
let mut characters = s.chars();
let backwards = match characters.next() {
Some('n') => false,
Some('p') => true,
_ => return Err(Error::BadRequest(ErrorKind::InvalidParam, "Invalid `since` token")),
};
2020-07-30 18:14:47 +02:00
num_since = characters
2020-07-30 22:09:11 +02:00
.collect::<String>()
.parse()
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `since` token."))?;
if backwards {
num_since = num_since.saturating_sub(limit);
2020-07-30 22:09:11 +02:00
}
2020-07-30 18:14:47 +02:00
}
2024-08-08 17:18:30 +00:00
let mut all_rooms: Vec<PublicRoomsChunk> = services
2021-10-13 10:16:45 +02:00
.rooms
2022-09-07 13:25:51 +02:00
.directory
2021-10-13 10:16:45 +02:00
.public_rooms()
2024-08-08 17:18:30 +00:00
.map(ToOwned::to_owned)
.then(|room_id| public_rooms_chunk(services, room_id))
.filter_map(|chunk| async move {
2021-10-13 10:16:45 +02:00
if let Some(query) = filter.generic_search_term.as_ref().map(|q| q.to_lowercase()) {
if let Some(name) = &chunk.name {
if name.as_str().to_lowercase().contains(&query) {
2024-08-08 17:18:30 +00:00
return Some(chunk);
2021-06-14 11:24:32 +02:00
}
2021-10-13 10:16:45 +02:00
}
2021-06-14 11:24:32 +02:00
2021-10-13 10:16:45 +02:00
if let Some(topic) = &chunk.topic {
if topic.to_lowercase().contains(&query) {
2024-08-08 17:18:30 +00:00
return Some(chunk);
2021-06-14 11:24:32 +02:00
}
2021-10-13 10:16:45 +02:00
}
2021-06-14 11:24:32 +02:00
2021-10-13 10:16:45 +02:00
if let Some(canonical_alias) = &chunk.canonical_alias {
if canonical_alias.as_str().to_lowercase().contains(&query) {
2024-08-08 17:18:30 +00:00
return Some(chunk);
2021-06-14 11:24:32 +02:00
}
2021-06-30 09:52:01 +02:00
}
2021-10-13 10:16:45 +02:00
2024-08-08 17:18:30 +00:00
return None;
2021-10-13 10:16:45 +02:00
}
2024-08-08 17:18:30 +00:00
// No search term
Some(chunk)
2021-10-13 10:16:45 +02:00
})
// We need to collect all, so we can sort by member count
2024-08-08 17:18:30 +00:00
.collect()
.await;
2020-07-30 18:14:47 +02:00
2020-07-30 22:09:11 +02:00
all_rooms.sort_by(|l, r| r.num_joined_members.cmp(&l.num_joined_members));
2020-07-30 18:14:47 +02:00
2024-05-04 09:45:37 -04:00
let total_room_count_estimate = UInt::try_from(all_rooms.len()).unwrap_or_else(|_| uint!(0));
2020-07-30 22:09:11 +02:00
2024-03-25 17:05:11 -04:00
let chunk: Vec<_> = all_rooms
.into_iter()
2024-05-04 09:45:37 -04:00
.skip(
num_since
.try_into()
.expect("num_since should not be this high"),
)
.take(limit.try_into().expect("limit should not be this high"))
2024-03-25 17:05:11 -04:00
.collect();
2020-07-30 22:09:11 +02:00
let prev_batch = if num_since == 0 {
2020-07-30 22:09:11 +02:00
None
} else {
2022-12-21 10:42:12 +01:00
Some(format!("p{num_since}"))
2020-07-30 22:09:11 +02:00
};
2024-05-04 09:45:37 -04:00
let next_batch = if chunk.len() < limit.try_into().unwrap() {
2020-07-30 22:09:11 +02:00
None
} else {
Some(format!(
"n{}",
num_since
.checked_add(limit)
.expect("num_since and limit should not be that large")
))
2020-07-30 22:09:11 +02:00
};
2020-07-30 18:14:47 +02:00
2022-02-18 15:33:14 +01:00
Ok(get_public_rooms_filtered::v3::Response {
2020-07-30 18:14:47 +02:00
chunk,
2020-07-30 22:09:11 +02:00
prev_batch,
next_batch,
2020-07-30 18:14:47 +02:00
total_room_count_estimate: Some(total_room_count_estimate),
2022-01-22 16:58:32 +01:00
})
2020-07-30 18:14:47 +02:00
}
/// Check whether the user can publish to the room directory via power levels of
/// room history visibility event or room creator
2024-08-08 17:18:30 +00:00
async fn user_can_publish_room(services: &Services, user_id: &UserId, room_id: &RoomId) -> Result<bool> {
if let Ok(event) = services
2024-07-16 08:05:25 +00:00
.rooms
.state_accessor
2024-08-08 17:18:30 +00:00
.room_state_get(room_id, &StateEventType::RoomPowerLevels, "")
.await
{
serde_json::from_str(event.content.get())
.map_err(|_| Error::bad_database("Invalid event content for m.room.power_levels"))
.map(|content: RoomPowerLevelsEventContent| {
RoomPowerLevels::from(content).user_can_send_state(user_id, StateEventType::RoomHistoryVisibility)
})
2024-08-08 17:18:30 +00:00
} else if let Ok(event) = services
.rooms
.state_accessor
.room_state_get(room_id, &StateEventType::RoomCreate, "")
.await
{
Ok(event.sender == user_id)
} else {
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"User is not allowed to publish this room",
));
}
}
2024-08-08 17:18:30 +00:00
async fn public_rooms_chunk(services: &Services, room_id: OwnedRoomId) -> PublicRoomsChunk {
PublicRoomsChunk {
canonical_alias: services
.rooms
.state_accessor
.get_canonical_alias(&room_id)
.await
.ok(),
name: services.rooms.state_accessor.get_name(&room_id).await.ok(),
num_joined_members: services
.rooms
.state_cache
.room_joined_count(&room_id)
.await
.unwrap_or(0)
.try_into()
.expect("joined count overflows ruma UInt"),
topic: services
.rooms
.state_accessor
.get_room_topic(&room_id)
.await
.ok(),
world_readable: services
.rooms
.state_accessor
.is_world_readable(&room_id)
.await,
guest_can_join: services.rooms.state_accessor.guest_can_join(&room_id).await,
avatar_url: services
.rooms
.state_accessor
.get_avatar(&room_id)
.await
.into_option()
.unwrap_or_default()
.url,
join_rule: services
.rooms
.state_accessor
.room_state_get_content(&room_id, &StateEventType::RoomJoinRules, "")
.map_ok(|c: RoomJoinRulesEventContent| match c.join_rule {
JoinRule::Public => PublicRoomJoinRule::Public,
JoinRule::Knock => PublicRoomJoinRule::Knock,
_ => "invite".into(),
})
.await
.unwrap_or_default(),
room_type: services
.rooms
.state_accessor
.get_room_type(&room_id)
.await
.ok(),
room_id,
}
}