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