use axum::extract::State; use axum_client_ip::InsecureClientIp; use conduwuit::{ Err, Result, debug_warn, utils::{IterStream, future::TryExtExt}, }; use futures::{ FutureExt, StreamExt, future::{OptionFuture, join3}, stream::FuturesUnordered, }; use ruma::{ OwnedServerName, RoomId, UserId, api::{ client::room::get_summary, federation::space::{SpaceHierarchyParentSummary, get_hierarchy}, }, events::room::member::MembershipState, space::SpaceRoomJoinRule::{self, *}, }; use service::Services; use crate::{Ruma, RumaResponse}; /// # `GET /_matrix/client/unstable/im.nheko.summary/rooms/{roomIdOrAlias}/summary` /// /// Returns a short description of the state of a room. /// /// This is the "wrong" endpoint that some implementations/clients may use /// according to the MSC. Request and response bodies are the same as /// `get_room_summary`. /// /// An implementation of [MSC3266](https://github.com/matrix-org/matrix-spec-proposals/pull/3266) pub(crate) async fn get_room_summary_legacy( State(services): State, InsecureClientIp(client): InsecureClientIp, body: Ruma, ) -> Result> { get_room_summary(State(services), InsecureClientIp(client), body) .boxed() .await .map(RumaResponse) } /// # `GET /_matrix/client/unstable/im.nheko.summary/summary/{roomIdOrAlias}` /// /// Returns a short description of the state of a room. /// /// An implementation of [MSC3266](https://github.com/matrix-org/matrix-spec-proposals/pull/3266) #[tracing::instrument(skip_all, fields(%client), name = "room_summary")] pub(crate) async fn get_room_summary( State(services): State, InsecureClientIp(client): InsecureClientIp, body: Ruma, ) -> Result { let (room_id, servers) = services .rooms .alias .resolve_with_servers(&body.room_id_or_alias, Some(body.via.clone())) .await?; if services.rooms.metadata.is_banned(&room_id).await { return Err!(Request(Forbidden("This room is banned on this homeserver."))); } room_summary_response(&services, &room_id, &servers, body.sender_user.as_deref()) .boxed() .await } async fn room_summary_response( services: &Services, room_id: &RoomId, servers: &[OwnedServerName], sender_user: Option<&UserId>, ) -> Result { if services.rooms.metadata.exists(room_id).await { return local_room_summary_response(services, room_id, sender_user) .boxed() .await; } let room = remote_room_summary_hierarchy_response(services, room_id, servers, sender_user).await?; Ok(get_summary::msc3266::Response { room_id: room_id.to_owned(), canonical_alias: room.canonical_alias, avatar_url: room.avatar_url, guest_can_join: room.guest_can_join, name: room.name, num_joined_members: room.num_joined_members, topic: room.topic, world_readable: room.world_readable, join_rule: room.join_rule, room_type: room.room_type, room_version: room.room_version, encryption: room.encryption, allowed_room_ids: room.allowed_room_ids, membership: sender_user.is_some().then_some(MembershipState::Leave), }) } async fn local_room_summary_response( services: &Services, room_id: &RoomId, sender_user: Option<&UserId>, ) -> Result { let join_rule = services.rooms.state_accessor.get_join_rules(room_id); let world_readable = services.rooms.state_accessor.is_world_readable(room_id); let guest_can_join = services.rooms.state_accessor.guest_can_join(room_id); let (join_rule, world_readable, guest_can_join) = join3(join_rule, world_readable, guest_can_join).await; user_can_see_summary( services, room_id, &join_rule.clone().into(), guest_can_join, world_readable, join_rule.allowed_rooms(), sender_user, ) .await?; let canonical_alias = services .rooms .state_accessor .get_canonical_alias(room_id) .ok(); let name = services.rooms.state_accessor.get_name(room_id).ok(); let topic = services.rooms.state_accessor.get_room_topic(room_id).ok(); let room_type = services.rooms.state_accessor.get_room_type(room_id).ok(); let avatar_url = services .rooms .state_accessor .get_avatar(room_id) .map(|res| res.into_option().unwrap_or_default().url); let room_version = services.rooms.state.get_room_version(room_id).ok(); let encryption = services .rooms .state_accessor .get_room_encryption(room_id) .ok(); let num_joined_members = services .rooms .state_cache .room_joined_count(room_id) .unwrap_or(0); let membership: OptionFuture<_> = sender_user .map(|sender_user| { services .rooms .state_accessor .get_member(room_id, sender_user) .map_ok_or(MembershipState::Leave, |content| content.membership) }) .into(); let ( canonical_alias, name, num_joined_members, topic, avatar_url, room_type, room_version, encryption, membership, ) = futures::join!( canonical_alias, name, num_joined_members, topic, avatar_url, room_type, room_version, encryption, membership, ); Ok(get_summary::msc3266::Response { room_id: room_id.to_owned(), canonical_alias, avatar_url, guest_can_join, name, num_joined_members: num_joined_members.try_into().unwrap_or_default(), topic, world_readable, room_type, room_version, encryption, membership, allowed_room_ids: join_rule.allowed_rooms().map(Into::into).collect(), join_rule: join_rule.into(), }) } /// used by MSC3266 to fetch a room's info if we do not know about it async fn remote_room_summary_hierarchy_response( services: &Services, room_id: &RoomId, servers: &[OwnedServerName], sender_user: Option<&UserId>, ) -> Result { if !services.config.allow_federation { return Err!(Request(Forbidden("Federation is disabled."))); } if services.rooms.metadata.is_disabled(room_id).await { return Err!(Request(Forbidden( "Federaton of room {room_id} is currently disabled on this server." ))); } let request = get_hierarchy::v1::Request::new(room_id.to_owned()); let mut requests: FuturesUnordered<_> = servers .iter() .map(|server| { services .sending .send_federation_request(server, request.clone()) }) .collect(); while let Some(Ok(response)) = requests.next().await { let room = response.room.clone(); if room.room_id != room_id { debug_warn!( "Room ID {} returned does not belong to the requested room ID {}", room.room_id, room_id ); continue; } return user_can_see_summary( services, room_id, &room.join_rule, room.guest_can_join, room.world_readable, room.allowed_room_ids.iter().map(AsRef::as_ref), sender_user, ) .await .map(|()| room); } Err!(Request(NotFound( "Room is unknown to this server and was unable to fetch over federation with the \ provided servers available" ))) } async fn user_can_see_summary<'a, I>( services: &Services, room_id: &RoomId, join_rule: &SpaceRoomJoinRule, guest_can_join: bool, world_readable: bool, allowed_room_ids: I, sender_user: Option<&UserId>, ) -> Result where I: Iterator + Send, { match sender_user { | Some(sender_user) => { let user_can_see_state_events = services .rooms .state_accessor .user_can_see_state_events(sender_user, room_id); let is_guest = services.users.is_deactivated(sender_user).unwrap_or(false); let user_in_allowed_restricted_room = allowed_room_ids .stream() .any(|room| services.rooms.state_cache.is_joined(sender_user, room)); let (user_can_see_state_events, is_guest, user_in_allowed_restricted_room) = join3(user_can_see_state_events, is_guest, user_in_allowed_restricted_room) .boxed() .await; if user_can_see_state_events || (is_guest && guest_can_join) || matches!(&join_rule, &Public | &Knock | &KnockRestricted) || user_in_allowed_restricted_room { return Ok(()); } Err!(Request(Forbidden( "Room is not world readable, not publicly accessible/joinable, restricted room \ conditions not met, and guest access is forbidden. Not allowed to see details \ of this room." ))) }, | None => { if matches!(join_rule, Public | Knock | KnockRestricted) || world_readable { return Ok(()); } Err!(Request(Forbidden( "Room is not world readable or publicly accessible/joinable, authentication is \ required" ))) }, } }