1use axum::extract::State;
2use futures::{FutureExt, StreamExt, TryFutureExt, future::join3, stream::FuturesUnordered};
3use ruma::{
4 OwnedServerName, RoomId, UserId,
5 api::{
6 client::room::get_summary,
7 federation::space::{SpaceHierarchyParentSummary, get_hierarchy},
8 },
9 events::room::member::MembershipState,
10 room::{JoinRuleSummary, RoomSummary},
11};
12use tuwunel_core::{
13 Err, Result, debug_warn, trace,
14 utils::{IterStream, future::TryExtExt, option::OptionExt},
15};
16use tuwunel_service::Services;
17
18use crate::{ClientIp, Ruma, RumaResponse};
19
20pub(crate) async fn get_room_summary_legacy(
30 State(services): State<crate::State>,
31 ClientIp(client): ClientIp,
32 body: Ruma<get_summary::v1::Request>,
33) -> Result<RumaResponse<get_summary::v1::Response>> {
34 get_room_summary(State(services), ClientIp(client), body)
35 .boxed()
36 .await
37 .map(RumaResponse)
38}
39
40#[tracing::instrument(skip_all, fields(%client), name = "room_summary")]
46pub(crate) async fn get_room_summary(
47 State(services): State<crate::State>,
48 ClientIp(client): ClientIp,
49 body: Ruma<get_summary::v1::Request>,
50) -> Result<get_summary::v1::Response> {
51 let (room_id, servers) = services
52 .alias
53 .maybe_resolve_with_servers(&body.room_id_or_alias, Some(&body.via))
54 .await?;
55
56 if services.metadata.is_banned(&room_id).await {
57 return Err!(Request(Forbidden("This room is banned on this homeserver.")));
58 }
59
60 room_summary_response(&services, &room_id, &servers, body.sender_user.as_deref())
61 .boxed()
62 .await
63}
64
65async fn room_summary_response(
66 services: &Services,
67 room_id: &RoomId,
68 servers: &[OwnedServerName],
69 sender_user: Option<&UserId>,
70) -> Result<get_summary::v1::Response> {
71 if services
72 .state_cache
73 .server_in_room(services.globals.server_name(), room_id)
74 .await
75 {
76 return local_room_summary_response(services, room_id, sender_user)
77 .boxed()
78 .await;
79 }
80
81 let summary = remote_room_summary_hierarchy_response(services, room_id, servers, sender_user)
82 .await?
83 .summary;
84
85 Ok(get_summary::v1::Response {
86 summary,
87 membership: sender_user
88 .is_some()
89 .then_some(MembershipState::Leave),
90 })
91}
92
93async fn local_room_summary_response(
94 services: &Services,
95 room_id: &RoomId,
96 sender_user: Option<&UserId>,
97) -> Result<get_summary::v1::Response> {
98 trace!(?sender_user, "Sending local room summary response for {room_id:?}");
99 let join_rule = services.state_accessor.get_join_rules(room_id);
100
101 let world_readable = services.state_accessor.is_world_readable(room_id);
102
103 let guest_can_join = services.state_accessor.guest_can_join(room_id);
104
105 let (join_rule, world_readable, guest_can_join) =
106 join3(join_rule, world_readable, guest_can_join).await;
107
108 trace!("{join_rule:?}, {world_readable:?}, {guest_can_join:?}");
109 user_can_see_summary(
110 services,
111 room_id,
112 &join_rule.clone().into(),
113 guest_can_join,
114 world_readable,
115 join_rule.allowed_room_ids(),
116 sender_user,
117 )
118 .await?;
119
120 let canonical_alias = services
121 .state_accessor
122 .get_canonical_alias(room_id)
123 .ok();
124
125 let name = services.state_accessor.get_name(room_id).ok();
126
127 let topic = services
128 .state_accessor
129 .get_room_topic(room_id)
130 .ok();
131
132 let room_type = services
133 .state_accessor
134 .get_room_type(room_id)
135 .ok();
136
137 let avatar_url = services
138 .state_accessor
139 .get_avatar(room_id)
140 .map_ok(|content| content.url)
141 .ok()
142 .map(Option::flatten);
143
144 let room_version = services.state.get_room_version(room_id).ok();
145
146 let encryption = services
147 .state_accessor
148 .get_room_encryption(room_id)
149 .ok();
150
151 let num_joined_members = services
152 .state_cache
153 .room_joined_count(room_id)
154 .unwrap_or(0);
155
156 let membership = sender_user.map_async(|sender_user| {
157 services
158 .state_accessor
159 .get_member(room_id, sender_user)
160 .map_ok_or(MembershipState::Leave, |content| content.membership)
161 });
162
163 let (
164 canonical_alias,
165 name,
166 num_joined_members,
167 topic,
168 avatar_url,
169 room_type,
170 room_version,
171 encryption,
172 membership,
173 ) = futures::join!(
174 canonical_alias,
175 name,
176 num_joined_members,
177 topic,
178 avatar_url,
179 room_type,
180 room_version,
181 encryption,
182 membership,
183 );
184
185 Ok(get_summary::v1::Response {
186 summary: RoomSummary {
187 room_id: room_id.to_owned(),
188 canonical_alias,
189 avatar_url,
190 guest_can_join,
191 name,
192 num_joined_members: num_joined_members.try_into().unwrap_or_default(),
193 topic,
194 world_readable,
195 room_type,
196 room_version,
197 encryption,
198 join_rule: join_rule.into(),
199 },
200 membership,
201 })
202}
203
204async fn remote_room_summary_hierarchy_response(
206 services: &Services,
207 room_id: &RoomId,
208 servers: &[OwnedServerName],
209 sender_user: Option<&UserId>,
210) -> Result<SpaceHierarchyParentSummary> {
211 trace!(?sender_user, ?servers, "Sending remote room summary response for {room_id:?}");
212 if !services.config.allow_federation {
213 return Err!(Request(Forbidden("Federation is disabled.")));
214 }
215
216 if services.metadata.is_disabled(room_id).await {
217 return Err!(Request(Forbidden(
218 "Federaton of room {room_id} is currently disabled on this server."
219 )));
220 }
221
222 let request = get_hierarchy::v1::Request::new(room_id.to_owned());
223
224 let mut requests: FuturesUnordered<_> = servers
225 .iter()
226 .map(|server| {
227 services
228 .federation
229 .execute(server, request.clone())
230 })
231 .collect();
232
233 while let Some(Ok(response)) = requests.next().await {
234 trace!("{response:?}");
235 let room = response.room.clone();
236 let summary = &room.summary;
237 if summary.room_id != room_id {
238 debug_warn!(
239 "Room ID {} returned does not belong to the requested room ID {}",
240 summary.room_id,
241 room_id
242 );
243 continue;
244 }
245
246 return user_can_see_summary(
247 services,
248 room_id,
249 &summary.join_rule,
250 summary.guest_can_join,
251 summary.world_readable,
252 summary.join_rule.allowed_room_ids(),
253 sender_user,
254 )
255 .await
256 .map(|()| room);
257 }
258
259 Err!(Request(NotFound(
260 "Room is unknown to this server and was unable to fetch over federation with the \
261 provided servers available"
262 )))
263}
264
265async fn user_can_see_summary<'a, I>(
266 services: &Services,
267 room_id: &RoomId,
268 join_rule: &JoinRuleSummary,
269 guest_can_join: bool,
270 world_readable: bool,
271 allowed_room_ids: I,
272 sender_user: Option<&UserId>,
273) -> Result
274where
275 I: Iterator<Item = &'a RoomId> + Send,
276{
277 let is_public_room = matches!(
278 join_rule,
279 JoinRuleSummary::Public | JoinRuleSummary::Knock | JoinRuleSummary::KnockRestricted(_)
280 );
281
282 match sender_user {
283 | Some(sender_user) => {
284 let user_can_see_state_events = services
285 .state_accessor
286 .user_can_see_state_events(sender_user, room_id);
287
288 let is_guest = services
289 .users
290 .is_deactivated(sender_user)
291 .unwrap_or(false);
292
293 let user_in_allowed_restricted_room = allowed_room_ids
294 .stream()
295 .any(|room| services.state_cache.is_joined(sender_user, room));
296
297 let (user_can_see_state_events, is_guest, user_in_allowed_restricted_room) =
298 join3(user_can_see_state_events, is_guest, user_in_allowed_restricted_room)
299 .boxed()
300 .await;
301
302 if user_can_see_state_events
303 || (is_guest && guest_can_join)
304 || is_public_room
305 || user_in_allowed_restricted_room
306 {
307 return Ok(());
308 }
309
310 Err!(Request(Forbidden(
311 "Room is not world readable, not publicly accessible/joinable, restricted room \
312 conditions not met, and guest access is forbidden. Not allowed to see details \
313 of this room."
314 )))
315 },
316 | None => {
317 if is_public_room || world_readable {
318 return Ok(());
319 }
320
321 Err!(Request(Forbidden(
322 "Room is not world readable or publicly accessible/joinable, authentication is \
323 required"
324 )))
325 },
326 }
327}