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