Skip to content
Snippets Groups Projects
search.rs 4.36 KiB
Newer Older
  • Learn to ignore specific revisions
  • 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,
    
    Jonathan de Jong's avatar
    Jonathan de Jong committed
    };
    
    use tracing::debug;
    
    timokoesters's avatar
    timokoesters committed
    
    
    use crate::{Error, Result, Ruma};
    
    timokoesters's avatar
    timokoesters committed
    
    
    /// # `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");
    
    timokoesters's avatar
    timokoesters committed
    
    
    	let search_criteria = body.search_categories.room_events.as_ref().unwrap();
    	let filter = &search_criteria.filter;
    
    	let include_state = &search_criteria.include_state;
    
    🥺's avatar
    🥺 committed
    	let room_ids = filter.rooms.clone().unwrap_or_else(|| {
    
    		services
    
    🥺's avatar
    🥺 committed
    			.rooms
    			.state_cache
    			.rooms_joined(sender_user)
    			.filter_map(Result::ok)
    			.collect()
    	});
    
    timokoesters's avatar
    timokoesters committed
    
    
    	// 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);
    
    timokoesters's avatar
    timokoesters committed
    
    
    	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(
    
    					"You don't have permission to view this room.",
    				));
    			}
    
    			// check if sender_user can see state events
    
    			if services
    
    🥺's avatar
    🥺 committed
    				.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(
    
    					"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(
    
    				"You don't have permission to view this room.",
    			));
    		}
    
    		if let Some(search) = services
    
    🥺's avatar
    🥺 committed
    			.rooms
    			.search
    			.search_pdus(room_id, &search_criteria.search_term)?
    		{
    
    			searches.push(search.0.peekable());
    		}
    	}
    
    timokoesters's avatar
    timokoesters committed
    
    
    	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
    	};
    
    timokoesters's avatar
    timokoesters committed
    
    
    	let mut results = Vec::new();
    
    🥺's avatar
    🥺 committed
    	let next_batch = skip.saturating_add(limit);
    
    		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);
    		}
    	}
    
    timokoesters's avatar
    timokoesters committed
    
    
    	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),
    			})
    		})
    
    		.take(limit)
    		.collect();
    
    timokoesters's avatar
    timokoesters committed
    
    
    	let more_unloaded_results = searches.iter_mut().any(|s| s.peek().is_some());
    	let next_batch = more_unloaded_results.then(|| next_batch.to_string());
    
    timokoesters's avatar
    timokoesters committed
    
    
    	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(),
    		},
    	}))
    
    timokoesters's avatar
    timokoesters committed
    }