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/// Searches known users at `POST /_matrix/client/r0/user_directory/search`.
25///
26/// Hides local users who neither belong to rooms with public join rules nor
27/// share a room with the sender unless `directory.users.show_all_local` is
28/// enabled. Hides appservice senders and users in exclusive appservice user
29/// namespaces unless `directory.users.show_appservice` is enabled.
30pub(crate) async fn search_users_route(
31	State(services): State<crate::State>,
32	body: Ruma<Request>,
33) -> Result<Response> {
34	let sender_user = body.sender_user();
35	let limit = usize_from_ruma_bounded(body.limit, LIMIT_DEFAULT, LIMIT_MAX);
36
37	let search_term = body.search_term.to_lowercase();
38	let users = services
39		.users
40		.stream()
41		.ready_filter(|&user_id| user_id != sender_user)
42		.map(ToOwned::to_owned)
43		.wide_filter_map(async |user_id| {
44			let display_name = services.profile.displayname(&user_id).await.ok();
45
46			matches_term(&user_id, display_name.as_deref(), &search_term)
47				.then_some((user_id, display_name))
48		})
49		.wide_filter_map(async |(user_id, display_name)| {
50			should_show_user(&services, sender_user, &user_id)
51				.await
52				.then_async(async move || {
53					let avatar_url = services.profile.avatar_url(&user_id).await.ok();
54
55					User { user_id, display_name, avatar_url }
56				})
57				.await
58		});
59
60	pin_mut!(users);
61	let results = users.by_ref().take(limit).collect().await;
62	let limited = users.next().await.is_some();
63
64	Ok(Response { results, limited })
65}
66
67fn matches_term(user_id: &UserId, display_name: Option<&str>, search_term: &str) -> bool {
68	user_id
69		.as_str()
70		.to_lowercase()
71		.contains(search_term)
72		|| display_name.is_some_and(|name| name.to_lowercase().contains(search_term))
73}
74
75async fn should_show_user(
76	services: &Services,
77	sender_user: &UserId,
78	target_user: &UserId,
79) -> bool {
80	let config = &services.server.config;
81
82	if !config.directory.users.show_appservice
83		&& services
84			.appservice
85			.is_exclusive_user_id(target_user)
86			.await
87	{
88		return false;
89	}
90
91	if config.directory.users.show_all_local {
92		return true;
93	}
94
95	let user_in_public_room = services
96		.state_cache
97		.rooms_joined(target_user)
98		.map(ToOwned::to_owned)
99		.broad_any(async |room_id| {
100			services
101				.state_accessor
102				.get_join_rules(&room_id)
103				.map(|rule| matches!(rule, JoinRule::Public))
104				.await
105		});
106
107	let user_sees_user = services
108		.state_cache
109		.user_sees_user(sender_user, target_user);
110
111	user_in_public_room.or(user_sees_user).await
112}