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