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::{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
19const LIMIT_MAX: usize = 500;
21const LIMIT_DEFAULT: usize = 10;
22
23pub(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}