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(
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}