use std::collections::BTreeMap; use axum::extract::State; use ruma::{ api::client::{ error::ErrorKind, search::search_events::{ self, v3::{EventContextResult, ResultCategories, ResultRoomEvents, SearchResult}, }, }, events::AnyStateEvent, serde::Raw, uint, OwnedRoomId, }; use tracing::debug; use crate::{Error, Result, Ruma}; /// # `POST /_matrix/client/r0/search` /// /// Searches rooms for messages. /// /// - Only works if the user is currently joined to the room (TODO: Respect /// history visibility) pub(crate) async fn search_events_route( State(services): State<crate::State>, body: Ruma<search_events::v3::Request>, ) -> Result<search_events::v3::Response> { let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let search_criteria = body.search_categories.room_events.as_ref().unwrap(); let filter = &search_criteria.filter; let include_state = &search_criteria.include_state; let room_ids = filter.rooms.clone().unwrap_or_else(|| { services .rooms .state_cache .rooms_joined(sender_user) .filter_map(Result::ok) .collect() }); // Use limit or else 10, with maximum 100 let limit: usize = filter .limit .unwrap_or_else(|| uint!(10)) .try_into() .unwrap_or(10) .min(100); let mut room_states: BTreeMap<OwnedRoomId, Vec<Raw<AnyStateEvent>>> = BTreeMap::new(); if include_state.is_some_and(|include_state| include_state) { for room_id in &room_ids { if !services.rooms.state_cache.is_joined(sender_user, room_id)? { return Err(Error::BadRequest( ErrorKind::forbidden(), "You don't have permission to view this room.", )); } // check if sender_user can see state events if services .rooms .state_accessor .user_can_see_state_events(sender_user, room_id)? { let room_state = services .rooms .state_accessor .room_state_full(room_id) .await? .values() .map(|pdu| pdu.to_state_event()) .collect::<Vec<_>>(); debug!("Room state: {:?}", room_state); room_states.insert(room_id.clone(), room_state); } else { return Err(Error::BadRequest( ErrorKind::forbidden(), "You don't have permission to view this room.", )); } } } let mut searches = Vec::new(); for room_id in &room_ids { if !services.rooms.state_cache.is_joined(sender_user, room_id)? { return Err(Error::BadRequest( ErrorKind::forbidden(), "You don't have permission to view this room.", )); } if let Some(search) = services .rooms .search .search_pdus(room_id, &search_criteria.search_term)? { searches.push(search.0.peekable()); } } let skip: usize = match body.next_batch.as_ref().map(|s| s.parse()) { Some(Ok(s)) => s, Some(Err(_)) => return Err(Error::BadRequest(ErrorKind::InvalidParam, "Invalid next_batch token.")), None => 0, // Default to the start }; let mut results = Vec::new(); let next_batch = skip.saturating_add(limit); for _ in 0..next_batch { if let Some(s) = searches .iter_mut() .map(|s| (s.peek().cloned(), s)) .max_by_key(|(peek, _)| peek.clone()) .and_then(|(_, i)| i.next()) { results.push(s); } } let results: Vec<_> = results .iter() .skip(skip) .filter_map(|result| { services .rooms .timeline .get_pdu_from_id(result) .ok()? .filter(|pdu| { !pdu.is_redacted() && services .rooms .state_accessor .user_can_see_event(sender_user, &pdu.room_id, &pdu.event_id) .unwrap_or(false) }) .map(|pdu| pdu.to_room_event()) }) .map(|result| { Ok::<_, Error>(SearchResult { context: EventContextResult { end: None, events_after: Vec::new(), events_before: Vec::new(), profile_info: BTreeMap::new(), start: None, }, rank: None, result: Some(result), }) }) .filter_map(Result::ok) .take(limit) .collect(); let more_unloaded_results = searches.iter_mut().any(|s| s.peek().is_some()); let next_batch = more_unloaded_results.then(|| next_batch.to_string()); Ok(search_events::v3::Response::new(ResultCategories { room_events: ResultRoomEvents { count: Some(results.len().try_into().unwrap_or_else(|_| uint!(0))), groups: BTreeMap::new(), // TODO next_batch, results, state: room_states, highlights: search_criteria .search_term .split_terminator(|c: char| !c.is_alphanumeric()) .map(str::to_lowercase) .collect(), }, })) }