Skip to main content

tuwunel_api/client/
user_directory.rs

1use axum::extract::State;
2use futures::{FutureExt, StreamExt, pin_mut};
3use ruma::{
4	UserId,
5	api::client::user_directory::search_users::{self},
6	events::room::join_rules::JoinRule,
7};
8use tuwunel_core::{
9	Result,
10	utils::{
11		BoolExt, FutureBoolExt,
12		stream::{BroadbandExt, ReadyExt},
13	},
14};
15use tuwunel_service::Services;
16
17use crate::Ruma;
18
19// Tuwunel can handle a lot more results than synapse
20const LIMIT_MAX: usize = 500;
21const LIMIT_DEFAULT: usize = 10;
22
23/// # `POST /_matrix/client/r0/user_directory/search`
24///
25/// Searches all known users for a match.
26///
27/// - Hides any local users that aren't in any public rooms (i.e. those that
28///   have the join rule set to public) and don't share a room with the sender
29/// - Hides appservice senders and users in exclusive appservice user namespaces
30pub(crate) async fn search_users_route(
31	State(services): State<crate::State>,
32	body: Ruma<search_users::v3::Request>,
33) -> Result<search_users::v3::Response> {
34	let sender_user = body.sender_user();
35	let limit = usize::try_from(body.limit)
36		.unwrap_or(LIMIT_DEFAULT)
37		.min(LIMIT_MAX);
38
39	let search_term = body.search_term.to_lowercase();
40	let users = services
41		.users
42		.stream()
43		.ready_filter(|&user_id| user_id != sender_user)
44		.map(ToOwned::to_owned)
45		.broad_filter_map(async |user_id| {
46			let display_name = services.profile.displayname(&user_id).await.ok();
47
48			should_show_user(
49				&services,
50				sender_user,
51				&user_id,
52				display_name.as_deref(),
53				&search_term,
54			)
55			.await
56			.then_async(async || search_users::v3::User {
57				user_id: user_id.clone(),
58				display_name,
59				avatar_url: services.profile.avatar_url(&user_id).await.ok(),
60			})
61			.await
62		});
63
64	pin_mut!(users);
65	let results = users.by_ref().take(limit).collect().await;
66	let limited = users.next().await.is_some();
67
68	Ok(search_users::v3::Response { results, limited })
69}
70
71async fn should_show_user(
72	services: &Services,
73	sender_user: &UserId,
74	target_user: &UserId,
75	target_display_name: Option<&str>,
76	search_term: &str,
77) -> bool {
78	let user_id_matches = target_user
79		.as_str()
80		.to_lowercase()
81		.contains(search_term);
82
83	let display_name_matches = target_display_name
84		.map(str::to_lowercase)
85		.is_some_and(|display_name| display_name.contains(search_term));
86
87	if !user_id_matches && !display_name_matches {
88		return false;
89	}
90
91	if services
92		.appservice
93		.is_exclusive_user_id(target_user)
94		.await
95	{
96		return false;
97	}
98
99	if services
100		.server
101		.config
102		.show_all_local_users_in_user_directory
103	{
104		return true;
105	}
106
107	let user_in_public_room = services
108		.state_cache
109		.rooms_joined(target_user)
110		.map(ToOwned::to_owned)
111		.broad_any(async |room_id| {
112			services
113				.state_accessor
114				.get_join_rules(&room_id)
115				.map(|rule| matches!(rule, JoinRule::Public))
116				.await
117		});
118
119	let user_sees_user = services
120		.state_cache
121		.user_sees_user(sender_user, target_user);
122
123	pin_mut!(user_in_public_room, user_sees_user);
124	user_in_public_room.or(user_sees_user).await
125}