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