1use std::collections::BTreeMap;
2
3use axum::extract::State;
4use futures::{
5 FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt,
6 future::{
7 Either::{Left, Right},
8 join,
9 },
10};
11use itertools::Itertools;
12use ruma::{
13 OwnedRoomId, RoomId, UInt, UserId,
14 api::client::search::search_events::{
15 self,
16 v3::{
17 Criteria, EventContext, EventContextResult, ResultCategories, ResultRoomEvents,
18 SearchResult,
19 },
20 },
21 events::AnyStateEvent,
22 serde::Raw,
23};
24use search_events::v3::{Request, Response};
25use tuwunel_core::{
26 Err, Result, at, is_true,
27 utils::{
28 IterStream,
29 math::usize_from_ruma_bounded,
30 option::OptionExt,
31 stream::{ReadyExt, TryIgnore, WidebandExt},
32 },
33};
34use tuwunel_matrix::{Event, PduCount};
35use tuwunel_service::{
36 Services,
37 rooms::{search::RoomQuery, timeline::PdusIterItem},
38};
39
40use super::visibility_filter;
41use crate::Ruma;
42
43type RoomStates = BTreeMap<OwnedRoomId, RoomState>;
44type RoomState = Vec<Raw<AnyStateEvent>>;
45
46const LIMIT_DEFAULT: usize = 10;
47const LIMIT_MAX: usize = 100;
48const CONTEXT_MAX: usize = 20;
49
50pub(crate) async fn search_events_route(
57 State(services): State<crate::State>,
58 body: Ruma<Request>,
59) -> Result<Response> {
60 let sender_user = body.sender_user();
61 let next_batch = body.next_batch.as_deref();
62 let room_events = body
63 .search_categories
64 .room_events
65 .as_ref()
66 .map_async(|criteria| category_room_events(&services, sender_user, next_batch, criteria))
67 .await
68 .transpose()?;
69
70 Ok(Response {
71 search_categories: ResultCategories {
72 room_events: room_events.unwrap_or_default(),
73 },
74 })
75}
76
77#[expect(clippy::map_unwrap_or)]
78async fn category_room_events(
79 services: &Services,
80 sender_user: &UserId,
81 next_batch: Option<&str>,
82 criteria: &Criteria,
83) -> Result<ResultRoomEvents> {
84 let filter = &criteria.filter;
85
86 let limit = filter
87 .limit
88 .map_or(LIMIT_DEFAULT, |limit| usize_from_ruma_bounded(limit, LIMIT_DEFAULT, LIMIT_MAX));
89
90 let before: Option<PduCount> = next_batch.map(str::parse).transpose()?;
91
92 let rooms = filter
93 .rooms
94 .clone()
95 .map(IntoIterator::into_iter)
96 .map(IterStream::stream)
97 .map(Left)
98 .unwrap_or_else(|| {
99 let joined = services
100 .state_cache
101 .rooms_joined(sender_user)
102 .map(ToOwned::to_owned);
103
104 Right(joined)
105 });
106
107 let results: Vec<_> = rooms
108 .filter_map(async |room_id| {
109 check_room_visible(services, sender_user, &room_id, criteria)
110 .await
111 .is_ok()
112 .then_some(room_id)
113 })
114 .filter_map(async |room_id| {
115 let query = RoomQuery {
116 room_id: &room_id,
117 user_id: Some(sender_user),
118 criteria,
119 before,
120 limit,
121 };
122
123 let (count, results) = services.search.search_pdus(&query).await.ok()?;
124
125 results
126 .collect::<Vec<_>>()
127 .map(|results| (room_id.clone(), count, results))
128 .map(Some)
129 .await
130 })
131 .collect()
132 .await;
133
134 let total: UInt = results
135 .iter()
136 .fold(0, |a: usize, (_, count, _)| a.saturating_add(*count))
137 .try_into()?;
138
139 let state: RoomStates = results
140 .iter()
141 .stream()
142 .ready_filter(|_| criteria.include_state.is_some_and(is_true!()))
143 .filter_map(async |(room_id, ..)| {
144 procure_room_state(services, sender_user, room_id)
145 .map_ok(|state| (room_id.clone(), state))
146 .await
147 .ok()
148 })
149 .collect()
150 .await;
151
152 let page: Vec<_> = results
153 .into_iter()
154 .map(at!(2))
155 .kmerge_by(|(a, _), (b, _)| a > b)
156 .take(limit)
157 .collect();
158
159 let next_batch = page
160 .last()
161 .filter(|_| page.len() >= limit)
162 .map(|(count, _)| count.to_string());
163
164 let results: Vec<SearchResult> = page
165 .into_iter()
166 .map(at!(1))
167 .stream()
168 .map(Event::into_pdu)
169 .wide_then(async |pdu| {
170 let context =
171 event_context(services, sender_user, &pdu, &criteria.event_context).await;
172
173 let pdu = services
174 .pdu_metadata
175 .bundle_aggregations(sender_user, pdu)
176 .await;
177
178 SearchResult {
179 rank: None,
180 result: Some(pdu.into_format()),
181 context,
182 }
183 })
184 .collect()
185 .await;
186
187 let highlights = criteria
188 .search_term
189 .split_terminator(|c: char| !c.is_alphanumeric())
190 .map(str::to_lowercase)
191 .collect();
192
193 Ok(ResultRoomEvents {
194 count: Some(total),
195 next_batch,
196 results,
197 state,
198 highlights,
199 groups: Default::default(), })
201}
202
203async fn event_context<E>(
204 services: &Services,
205 sender_user: &UserId,
206 pdu: &E,
207 event_context: &EventContext,
208) -> EventContextResult
209where
210 E: Event,
211{
212 if event_context.is_default() {
215 return EventContextResult::default();
216 }
217
218 let Ok(base_count) = services
219 .timeline
220 .get_pdu_count(pdu.event_id())
221 .await
222 else {
223 return EventContextResult::default();
224 };
225
226 let room_id = pdu.room_id();
227 let before_limit = usize_from_ruma_bounded(event_context.before_limit, 0, CONTEXT_MAX);
228 let after_limit = usize_from_ruma_bounded(event_context.after_limit, 0, CONTEXT_MAX);
229
230 let events_before = collect_context_half(
231 services,
232 services
233 .timeline
234 .pdus_rev(Some(sender_user), room_id, Some(base_count)),
235 sender_user,
236 before_limit,
237 );
238
239 let events_after = collect_context_half(
240 services,
241 services
242 .timeline
243 .pdus(Some(sender_user), room_id, Some(base_count)),
244 sender_user,
245 after_limit,
246 );
247
248 let (events_before, events_after) = join(events_before, events_after).await;
249
250 let start = events_before
251 .last()
252 .map(at!(0))
253 .or(Some(base_count))
254 .as_ref()
255 .map(ToString::to_string);
256
257 let end = events_after
258 .last()
259 .map(at!(0))
260 .or_else(|| Some(base_count.saturating_add(1)))
261 .as_ref()
262 .map(ToString::to_string);
263
264 let events_before = events_before
265 .into_iter()
266 .map(at!(1))
267 .map(Event::into_format)
268 .collect();
269
270 let events_after = events_after
271 .into_iter()
272 .map(at!(1))
273 .map(Event::into_format)
274 .collect();
275
276 EventContextResult {
277 start,
278 end,
279 events_before,
280 events_after,
281 profile_info: BTreeMap::new(),
282 }
283}
284
285async fn collect_context_half<'a, S>(
286 services: &'a Services,
287 pdus: S,
288 sender_user: &'a UserId,
289 take: usize,
290) -> Vec<PdusIterItem>
291where
292 S: Stream<Item = Result<PdusIterItem>> + Send + 'a,
293{
294 pdus.ignore_err()
295 .wide_filter_map(|item| visibility_filter(services, item, sender_user))
296 .take(take)
297 .wide_then(async |(count, pdu)| {
298 let pdu = services
299 .pdu_metadata
300 .bundle_aggregations(sender_user, pdu)
301 .await;
302
303 (count, pdu)
304 })
305 .collect()
306 .await
307}
308
309async fn procure_room_state(
310 services: &Services,
311 sender_user: &UserId,
312 room_id: &RoomId,
313) -> Result<RoomState> {
314 let state = services
315 .state_accessor
316 .room_state_full_pdus(room_id)
317 .map_ok(Event::into_pdu)
318 .map_ok(|pdu| pdu.without_private_unless_sender(Some(sender_user)))
319 .map_ok(Event::into_format)
320 .try_collect()
321 .await?;
322
323 Ok(state)
324}
325
326async fn check_room_visible(
327 services: &Services,
328 user_id: &UserId,
329 room_id: &RoomId,
330 search: &Criteria,
331) -> Result {
332 let check_visible = search.filter.rooms.is_some();
333 let check_state = check_visible && search.include_state.is_some_and(is_true!());
334
335 let is_joined = !check_visible
336 || services
337 .state_cache
338 .is_joined(user_id, room_id)
339 .await;
340
341 let state_visible = !check_state
342 || services
343 .state_accessor
344 .user_can_see_state_events(user_id, room_id)
345 .await;
346
347 if !is_joined || !state_visible {
348 return Err!(Request(Forbidden("You don't have permission to view {room_id:?}")));
349 }
350
351 Ok(())
352}