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 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 if services.server.config.admin_room_notices {
149 services
150 .admin
151 .send_text(&format!(
152 "Non-admin user {sender_user} tried to publish {0} to the room \
153 directory while \"lockdown_public_room_directory\" is enabled",
154 body.room_id
155 ))
156 .await;
157 }
158
159 return Err!(Request(Forbidden(
160 "Publishing rooms to the room directory is not allowed",
161 )));
162 }
163
164 services.directory.set_public(&body.room_id);
165
166 if services.server.config.admin_room_notices {
167 services
168 .admin
169 .send_text(&format!(
170 "{sender_user} made {} public to the room directory",
171 body.room_id
172 ))
173 .await;
174 }
175 info!("{sender_user} made {0} public to the room directory", body.room_id);
176 },
177 | room::Visibility::Private => services.directory.set_not_public(&body.room_id),
178 | _ => {
179 return Err!(Request(InvalidParam("Room visibility type is not supported.",)));
180 },
181 }
182
183 Ok(set_room_visibility::v3::Response {})
184}
185
186pub(crate) async fn get_room_visibility_route(
190 State(services): State<crate::State>,
191 body: Ruma<get_room_visibility::v3::Request>,
192) -> Result<get_room_visibility::v3::Response> {
193 if !services.metadata.exists(&body.room_id).await {
194 return Err!(Request(NotFound("Room not found")));
196 }
197
198 Ok(get_room_visibility::v3::Response {
199 visibility: if services
200 .directory
201 .is_public_room(&body.room_id)
202 .await
203 {
204 room::Visibility::Public
205 } else {
206 room::Visibility::Private
207 },
208 })
209}
210
211pub(crate) async fn get_public_rooms_filtered_helper(
212 services: &Services,
213 server: Option<&ServerName>,
214 limit: Option<UInt>,
215 since: Option<&str>,
216 filter: &Filter,
217 _network: &RoomNetwork,
218) -> Result<get_public_rooms_filtered::v3::Response> {
219 if let Some(other_server) =
220 server.filter(|server_name| !services.globals.server_is_ours(server_name))
221 {
222 let response = services
223 .federation
224 .execute(
225 other_server,
226 federation::directory::get_public_rooms_filtered::v1::Request {
227 limit,
228 since: since.map(ToOwned::to_owned),
229 filter: Filter {
230 generic_search_term: filter.generic_search_term.clone(),
231 room_types: filter.room_types.clone(),
232 },
233 room_network: RoomNetwork::Matrix,
234 },
235 )
236 .await?;
237
238 return Ok(get_public_rooms_filtered::v3::Response {
239 chunk: response.chunk,
240 prev_batch: response.prev_batch,
241 next_batch: response.next_batch,
242 total_room_count_estimate: response.total_room_count_estimate,
243 });
244 }
245
246 let limit: usize = limit.map_or(10_u64, u64::from).try_into()?;
248 let mut num_since: usize = 0;
249
250 if let Some(s) = &since {
251 let mut characters = s.chars();
252 let backwards = match characters.next() {
253 | Some('n') => false,
254 | Some('p') => true,
255 | _ => {
256 return Err!(Request(InvalidParam("Invalid `since` token")));
257 },
258 };
259
260 num_since = characters
261 .collect::<String>()
262 .parse()
263 .map_err(|_| err!(Request(InvalidParam("Invalid `since` token."))))?;
264
265 if backwards {
266 num_since = num_since.saturating_sub(limit);
267 }
268 }
269
270 let search_term = filter
271 .generic_search_term
272 .as_deref()
273 .map(str::to_lowercase);
274
275 let search_room_id = filter
276 .generic_search_term
277 .as_deref()
278 .filter(|_| services.config.allow_public_room_search_by_id)
279 .filter(|s| s.starts_with('!'))
280 .filter(|s| s.len() > 5); let meta_public_rooms = search_room_id
283 .filter(|_| services.config.allow_unlisted_room_search_by_id)
284 .map(|prefix| services.metadata.public_ids_prefix(prefix))
285 .into_iter()
286 .stream()
287 .flatten();
288
289 let mut all_rooms: Vec<PublicRoomsChunk> = services
290 .directory
291 .public_rooms()
292 .map(ToOwned::to_owned)
293 .chain(meta_public_rooms)
294 .wide_then(|room_id| public_rooms_chunk(services, room_id))
295 .ready_filter_map(|chunk| {
296 if !filter.room_types.is_empty()
297 && !filter
298 .room_types
299 .contains(&RoomTypeFilter::from(chunk.room_type.clone()))
300 {
301 return None;
302 }
303
304 if let Some(query) = search_room_id
305 && chunk.room_id.as_str().contains(query) {
306 return Some(chunk);
307 }
308
309 if let Some(query) = search_term.as_deref() {
310 if let Some(name) = &chunk.name
311 && name.as_str().to_lowercase().contains(query) {
312 return Some(chunk);
313 }
314
315 if let Some(topic) = &chunk.topic
316 && topic.to_lowercase().contains(query) {
317 return Some(chunk);
318 }
319
320 if let Some(canonical_alias) = &chunk.canonical_alias
321 && canonical_alias.as_str().to_lowercase().contains(query) {
322 return Some(chunk);
323 }
324
325 return None;
326 }
327
328 Some(chunk)
330 })
331 .collect()
333 .await;
334
335 all_rooms.sort_by_key(|r| cmp::Reverse(r.num_joined_members));
336
337 let total_room_count_estimate = UInt::try_from(all_rooms.len())
338 .unwrap_or_else(|_| uint!(0))
339 .into();
340
341 let chunk: Vec<_> = all_rooms
342 .into_iter()
343 .skip(num_since)
344 .take(limit)
345 .collect();
346
347 let prev_batch = num_since
348 .ne(&0)
349 .then_some(format!("p{num_since}"));
350
351 let next_batch = chunk
352 .len()
353 .ge(&limit)
354 .then_some(format!("n{}", num_since.expected_add(limit)));
355
356 Ok(get_public_rooms_filtered::v3::Response {
357 chunk,
358 prev_batch,
359 next_batch,
360 total_room_count_estimate,
361 })
362}
363
364async fn user_can_publish_room(
367 services: &Services,
368 user_id: &UserId,
369 room_id: &RoomId,
370) -> Result<bool> {
371 match services
372 .state_accessor
373 .get_power_levels(room_id)
374 .await
375 {
376 | Ok(power_levels) =>
377 Ok(power_levels.user_can_send_state(user_id, StateEventType::RoomHistoryVisibility)),
378 | _ => {
379 match services
380 .state_accessor
381 .room_state_get(room_id, &StateEventType::RoomCreate, "")
382 .await
383 {
384 | Ok(event) => Ok(event.sender() == user_id),
385 | _ => Err!(Request(Forbidden("User is not allowed to publish this room"))),
386 }
387 },
388 }
389}
390
391async fn public_rooms_chunk(services: &Services, room_id: OwnedRoomId) -> PublicRoomsChunk {
392 let name = services.state_accessor.get_name(&room_id).ok();
393
394 let room_type = services
395 .state_accessor
396 .get_room_type(&room_id)
397 .ok();
398
399 let canonical_alias = services
400 .state_accessor
401 .get_canonical_alias(&room_id)
402 .ok()
403 .then(async |alias| {
404 if let Some(alias) = alias
405 && services.globals.alias_is_local(&alias)
406 && let Ok(alias_room_id) = services.alias.resolve_local_alias(&alias).await
407 && alias_room_id == room_id
408 {
409 Some(alias)
410 } else {
411 None
412 }
413 });
414
415 let avatar_url = services
416 .state_accessor
417 .get_avatar(&room_id)
418 .map_ok(|content| content.url)
419 .ok();
420
421 let topic = services
422 .state_accessor
423 .get_room_topic(&room_id)
424 .ok();
425
426 let world_readable = services
427 .state_accessor
428 .is_world_readable(&room_id);
429
430 let join_rule = services
431 .state_accessor
432 .get_join_rules(&room_id)
433 .map(|join_rule| join_rule.kind());
434
435 let guest_can_join = services.state_accessor.guest_can_join(&room_id);
436
437 let num_joined_members = services
438 .state_cache
439 .room_joined_count(&room_id)
440 .map(|x| {
441 x.ok()
442 .and_then(|x| x.try_into().ok())
443 .unwrap_or_else(|| uint!(0))
444 });
445
446 let (
447 (avatar_url, canonical_alias, guest_can_join, join_rule, name),
448 (num_joined_members, room_type, topic, world_readable),
449 ) = join(
450 join5(avatar_url, canonical_alias, guest_can_join, join_rule, name),
451 join4(num_joined_members, room_type, topic, world_readable),
452 )
453 .boxed()
454 .await;
455
456 PublicRoomsChunk {
457 avatar_url: avatar_url.flatten(),
458 canonical_alias,
459 guest_can_join,
460 join_rule,
461 name,
462 num_joined_members,
463 room_id,
464 room_type,
465 topic,
466 world_readable,
467 }
468}
469
470fn check_server_banned(services: &Services, server: Option<&ServerName>) -> Result {
471 let Some(server) = server else {
472 return Ok(());
473 };
474
475 if services
476 .config
477 .forbidden_remote_room_directory_server_names
478 .is_match(server.host())
479 || services
480 .config
481 .is_forbidden_remote_server_name(server)
482 {
483 return Err!(Request(Forbidden("Server is banned on this homeserver.")));
484 }
485
486 Ok(())
487}