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