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