tuwunel_api_client/
user_directory.rs1use 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
20const LIMIT_MAX: usize = 500;
22const LIMIT_DEFAULT: usize = 10;
23
24pub(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}