1use std::cmp;
2
3use axum::extract::State;
4use futures::{
5 FutureExt, StreamExt, TryFutureExt,
6 future::{join, join4, join5},
7};
8use ruma::{
9 OwnedRoomAliasId, OwnedRoomId, RoomAliasId, RoomId, ServerName, UInt, UserId,
10 api::{
11 client::{
12 directory::{
13 get_public_rooms, get_public_rooms_filtered, get_room_visibility,
14 set_room_visibility,
15 },
16 room,
17 },
18 federation,
19 },
20 directory::{Filter, PublicRoomsChunk, RoomNetwork, RoomTypeFilter},
21 events::StateEventType,
22 uint,
23};
24use tuwunel_core::{
25 Err, Error, Result, err, info,
26 matrix::Event,
27 utils::{
28 TryFutureExtExt,
29 math::Expected,
30 stream::{IterStream, ReadyExt, WidebandExt},
31 },
32 warn,
33};
34use tuwunel_service::Services;
35
36use crate::{ClientIp, Ruma};
37
38#[tracing::instrument(skip_all, fields(%client), name = "publicrooms")]
44pub(crate) async fn get_public_rooms_filtered_route(
45 State(services): State<crate::State>,
46 ClientIp(client): ClientIp,
47 body: Ruma<get_public_rooms_filtered::v3::Request>,
48) -> Result<get_public_rooms_filtered::v3::Response> {
49 check_server_banned(&services, body.server.as_deref())?;
50
51 get_public_rooms_filtered_helper(
52 &services,
53 body.server.as_deref(),
54 body.limit,
55 body.since.as_deref(),
56 &body.filter,
57 &body.room_network,
58 )
59 .map_err(|e| mask_remote_failure(&services, body.server.as_deref(), e))
60 .await
61}
62
63#[tracing::instrument(skip_all, fields(%client), name = "publicrooms")]
69pub(crate) async fn get_public_rooms_route(
70 State(services): State<crate::State>,
71 ClientIp(client): ClientIp,
72 body: Ruma<get_public_rooms::v3::Request>,
73) -> Result<get_public_rooms::v3::Response> {
74 check_server_banned(&services, body.server.as_deref())?;
75
76 let response = get_public_rooms_filtered_helper(
77 &services,
78 body.server.as_deref(),
79 body.limit,
80 body.since.as_deref(),
81 &Filter::default(),
82 &RoomNetwork::Matrix,
83 )
84 .map_err(|e| mask_remote_failure(&services, body.server.as_deref(), e))
85 .await?;
86
87 Ok(get_public_rooms::v3::Response {
88 chunk: response.chunk,
89 prev_batch: response.prev_batch,
90 next_batch: response.next_batch,
91 total_room_count_estimate: response.total_room_count_estimate,
92 })
93}
94
95#[tracing::instrument(skip_all, fields(%client), name = "room_directory")]
99pub(crate) async fn set_room_visibility_route(
100 State(services): State<crate::State>,
101 ClientIp(client): ClientIp,
102 body: Ruma<set_room_visibility::v3::Request>,
103) -> Result<set_room_visibility::v3::Response> {
104 let sender_user = body.sender_user();
105
106 if !services.metadata.exists(&body.room_id).await {
107 return Err!(Request(NotFound("Room not found")));
109 }
110
111 if services
112 .users
113 .is_deactivated(sender_user)
114 .await
115 .unwrap_or(false)
116 && body.appservice_info.is_none()
117 {
118 return Err!(Request(Forbidden("Guests cannot publish to room directories")));
119 }
120
121 if !user_can_publish_room(&services, sender_user, &body.room_id).await? {
122 return Err!(Request(Forbidden("User is not allowed to publish this room")));
123 }
124
125 match &body.visibility {
126 | room::Visibility::Public => {
127 if services
128 .server
129 .config
130 .lockdown_public_room_directory
131 && !services.admin.user_is_admin(sender_user).await
132 && body.appservice_info.is_none()
133 {
134 info!(
135 "Non-admin user {sender_user} tried to publish {0} to the room directory \
136 while \"lockdown_public_room_directory\" is enabled",
137 body.room_id
138 );
139
140 services
141 .admin
142 .notify_loud(&format!(
143 "Non-admin user {sender_user} tried to publish {0} to the room \
144 directory while \"lockdown_public_room_directory\" is enabled",
145 body.room_id
146 ))
147 .await;
148
149 return Err!(Request(Forbidden(
150 "Publishing rooms to the room directory is not allowed",
151 )));
152 }
153
154 let published_alias = services
156 .directory
157 .published_alias(&body.room_id)
158 .await
159 .ok();
160
161 services
162 .directory
163 .set_public(&body.room_id, published_alias.as_deref());
164
165 services
166 .admin
167 .notify_loud(&format!(
168 "{sender_user} made {} public to the room directory",
169 body.room_id
170 ))
171 .await;
172
173 info!("{sender_user} made {0} public to the room directory", body.room_id);
174 },
175 | room::Visibility::Private => services.directory.set_not_public(&body.room_id),
176 | _ => {
177 return Err!(Request(InvalidParam("Room visibility type is not supported.",)));
178 },
179 }
180
181 Ok(set_room_visibility::v3::Response {})
182}
183
184pub(crate) async fn get_room_visibility_route(
188 State(services): State<crate::State>,
189 body: Ruma<get_room_visibility::v3::Request>,
190) -> Result<get_room_visibility::v3::Response> {
191 if !services.metadata.exists(&body.room_id).await {
192 return Err!(Request(NotFound("Room not found")));
194 }
195
196 Ok(get_room_visibility::v3::Response {
197 visibility: if services
198 .directory
199 .is_public_room(&body.room_id)
200 .await
201 {
202 room::Visibility::Public
203 } else {
204 room::Visibility::Private
205 },
206 })
207}
208
209pub(crate) async fn get_public_rooms_filtered_helper(
210 services: &Services,
211 server: Option<&ServerName>,
212 limit: Option<UInt>,
213 since: Option<&str>,
214 filter: &Filter,
215 _network: &RoomNetwork,
216) -> Result<get_public_rooms_filtered::v3::Response> {
217 if let Some(other_server) = remote_server(services, server) {
218 let response = services
219 .federation
220 .execute(
221 other_server,
222 federation::directory::get_public_rooms_filtered::v1::Request {
223 limit,
224 since: since.map(ToOwned::to_owned),
225 filter: Filter {
226 generic_search_term: filter.generic_search_term.clone(),
227 room_types: filter.room_types.clone(),
228 },
229 room_network: RoomNetwork::Matrix,
230 },
231 )
232 .await?;
233
234 return Ok(get_public_rooms_filtered::v3::Response {
235 chunk: response.chunk,
236 prev_batch: response.prev_batch,
237 next_batch: response.next_batch,
238 total_room_count_estimate: response.total_room_count_estimate,
239 });
240 }
241
242 let limit: usize = limit.map_or(10_u64, u64::from).try_into()?;
244 let mut num_since: usize = 0;
245
246 if let Some(s) = &since {
247 let mut characters = s.chars();
248 let backwards = match characters.next() {
249 | Some('n') => false,
250 | Some('p') => true,
251 | _ => {
252 return Err!(Request(InvalidParam("Invalid `since` token")));
253 },
254 };
255
256 num_since = characters
257 .collect::<String>()
258 .parse()
259 .map_err(|_| err!(Request(InvalidParam("Invalid `since` token."))))?;
260
261 if backwards {
262 num_since = num_since.saturating_sub(limit);
263 }
264 }
265
266 let search_term = filter
267 .generic_search_term
268 .as_deref()
269 .map(str::to_lowercase);
270
271 let search_room_id = filter
272 .generic_search_term
273 .as_deref()
274 .filter(|_| services.config.allow_public_room_search_by_id)
275 .filter(|s| s.starts_with('!'))
276 .filter(|s| s.len() > 5); let meta_public_rooms = search_room_id
279 .filter(|_| services.config.allow_unlisted_room_search_by_id)
280 .map(|prefix| services.metadata.public_ids_prefix(prefix))
281 .into_iter()
282 .stream()
283 .flatten();
284
285 let mut all_rooms: Vec<PublicRoomsChunk> = services
286 .directory
287 .public_rooms()
288 .map(ToOwned::to_owned)
289 .chain(meta_public_rooms)
290 .wide_then(|room_id| public_rooms_chunk(services, room_id))
291 .ready_filter_map(|chunk| {
292 if !filter.room_types.is_empty()
293 && !filter
294 .room_types
295 .contains(&RoomTypeFilter::from(chunk.room_type.clone()))
296 {
297 return None;
298 }
299
300 if let Some(query) = search_room_id
301 && chunk.room_id.as_str().contains(query) {
302 return Some(chunk);
303 }
304
305 if let Some(query) = search_term.as_deref() {
306 if let Some(name) = &chunk.name
307 && name.as_str().to_lowercase().contains(query) {
308 return Some(chunk);
309 }
310
311 if let Some(topic) = &chunk.topic
312 && topic.to_lowercase().contains(query) {
313 return Some(chunk);
314 }
315
316 if let Some(canonical_alias) = &chunk.canonical_alias
317 && canonical_alias.as_str().to_lowercase().contains(query) {
318 return Some(chunk);
319 }
320
321 return None;
322 }
323
324 Some(chunk)
326 })
327 .collect()
329 .await;
330
331 all_rooms.sort_by_key(|r| cmp::Reverse(r.num_joined_members));
332
333 let total_room_count_estimate = UInt::try_from(all_rooms.len())
334 .unwrap_or_else(|_| uint!(0))
335 .into();
336
337 let chunk: Vec<_> = all_rooms
338 .into_iter()
339 .skip(num_since)
340 .take(limit)
341 .collect();
342
343 let prev_batch = num_since
344 .ne(&0)
345 .then_some(format!("p{num_since}"));
346
347 let next_batch = chunk
348 .len()
349 .ge(&limit)
350 .then_some(format!("n{}", num_since.expected_add(limit)));
351
352 Ok(get_public_rooms_filtered::v3::Response {
353 chunk,
354 prev_batch,
355 next_batch,
356 total_room_count_estimate,
357 })
358}
359
360async fn user_can_publish_room(
363 services: &Services,
364 user_id: &UserId,
365 room_id: &RoomId,
366) -> Result<bool> {
367 match services
368 .state_accessor
369 .get_power_levels(room_id)
370 .await
371 {
372 | Ok(power_levels) =>
373 Ok(power_levels.user_can_send_state(user_id, StateEventType::RoomHistoryVisibility)),
374 | _ => {
375 match services
376 .state_accessor
377 .room_state_get(room_id, &StateEventType::RoomCreate, "")
378 .await
379 {
380 | Ok(event) => Ok(event.sender() == user_id),
381 | _ => Err!(Request(Forbidden("User is not allowed to publish this room"))),
382 }
383 },
384 }
385}
386
387async fn public_rooms_chunk(services: &Services, room_id: OwnedRoomId) -> PublicRoomsChunk {
388 let name = services.state_accessor.get_name(&room_id).ok();
389
390 let room_type = services
391 .state_accessor
392 .get_room_type(&room_id)
393 .ok();
394
395 let canonical_alias = directory_alias(services, &room_id);
396
397 let avatar_url = services
398 .state_accessor
399 .get_avatar(&room_id)
400 .map_ok(|content| content.url)
401 .ok();
402
403 let topic = services
404 .state_accessor
405 .get_room_topic(&room_id)
406 .ok();
407
408 let world_readable = services
409 .state_accessor
410 .is_world_readable(&room_id);
411
412 let join_rule = services
413 .state_accessor
414 .get_join_rules(&room_id)
415 .map(|join_rule| join_rule.kind());
416
417 let guest_can_join = services.state_accessor.guest_can_join(&room_id);
418
419 let num_joined_members = services
420 .state_cache
421 .room_joined_count(&room_id)
422 .map(|x| {
423 x.ok()
424 .and_then(|x| x.try_into().ok())
425 .unwrap_or_else(|| uint!(0))
426 });
427
428 let (
429 (avatar_url, canonical_alias, guest_can_join, join_rule, name),
430 (num_joined_members, room_type, topic, world_readable),
431 ) = join(
432 join5(avatar_url, canonical_alias, guest_can_join, join_rule, name),
433 join4(num_joined_members, room_type, topic, world_readable),
434 )
435 .boxed()
436 .await;
437
438 PublicRoomsChunk {
439 avatar_url: avatar_url.flatten(),
440 canonical_alias,
441 guest_can_join,
442 join_rule,
443 name,
444 num_joined_members,
445 room_id,
446 room_type,
447 topic,
448 world_readable,
449 }
450}
451
452async fn directory_alias(services: &Services, room_id: &RoomId) -> Option<OwnedRoomAliasId> {
455 if let Ok(alias) = services.directory.published_alias(room_id).await
456 && alias_resolves_to(services, &alias, room_id).await
457 {
458 return Some(alias);
459 }
460
461 let alias = services
462 .state_accessor
463 .get_canonical_alias(room_id)
464 .await
465 .ok()?;
466
467 alias_resolves_to(services, &alias, room_id)
468 .await
469 .then_some(alias)
470}
471
472async fn alias_resolves_to(services: &Services, alias: &RoomAliasId, room_id: &RoomId) -> bool {
473 services.globals.alias_is_local(alias)
474 && services
475 .alias
476 .resolve_local_alias(alias)
477 .await
478 .is_ok_and(|resolved| resolved == room_id)
479}
480
481fn check_server_banned(services: &Services, server: Option<&ServerName>) -> Result {
482 let Some(server) = server else {
483 return Ok(());
484 };
485
486 if services
487 .config
488 .forbidden_remote_room_directory_server_names
489 .is_match(server.host())
490 || services
491 .config
492 .is_forbidden_remote_server_name(server)
493 {
494 return Err!(Request(Forbidden("Server is banned on this homeserver.")));
495 }
496
497 Ok(())
498}
499
500fn mask_remote_failure(services: &Services, server: Option<&ServerName>, error: Error) -> Error {
507 let Some(server) = remote_server(services, server) else {
508 return error;
509 };
510
511 warn!(%server, %error, "Failed to query remote public rooms directory");
512
513 err!(Request(ConnectionFailed("Unable to query the remote public rooms directory.")))
514}
515
516fn remote_server<'a>(
522 services: &Services,
523 server: Option<&'a ServerName>,
524) -> Option<&'a ServerName> {
525 server.filter(|server| !services.globals.server_is_ours(server))
526}