Add apis for searching

This commit is contained in:
tezlm 2023-12-03 00:56:52 -08:00
parent 1404f50b79
commit 6e2d6ef142
Signed by: tezlm
GPG key ID: 649733FCD94AFBBA
4 changed files with 495 additions and 1 deletions

View file

@ -1,3 +1,5 @@
//! Endpoints for event searches.
pub mod search_events;
pub mod search_messages;
pub mod search_threads;
pub mod search_users;

View file

@ -0,0 +1,265 @@
//! `POST /_matrix/client/*/search/messages`
//!
//! Search for messages. This endpoint is designed specifically not to
//! be generic, unlike the vanilla `/search`.
pub mod v3 {
//! `/v3/` ([spec])
//!
//! [spec]: TODO: write
use std::collections::BTreeMap;
use js_int::{uint, UInt};
use ruma_common::{
api::{request, response, Metadata},
metadata,
serde::{Raw, StringEnum},
OwnedMxcUri, OwnedUserId,
};
use ruma_events::AnyTimelineEvent;
use serde::{Deserialize, Serialize};
use crate::PrivOwnedStr;
const METADATA: Metadata = metadata! {
method: POST,
rate_limited: true,
authentication: AccessToken,
history: {
1.0 => "/_matrix/client/v1/search/messages",
}
};
/// Request type for the `search` endpoint.
#[request(error = crate::Error)]
pub struct Request {
/// The point to return events from.
///
/// If given, this should be a `next_batch` result from a previous call to this endpoint.
#[ruma_api(query)]
pub next_batch: Option<String>,
/// Describes what to search for
pub query: String,
/// Any context to include besides messages
#[serde(default)]
pub context: EventContext,
/// In what order to return messages
#[serde(default)]
pub order: Order,
}
/// Response type for the `search` endpoint.
// NOTE: A downside to merging next_batch instead of haing categories
// means users can't paginate one category after viewing results. In
// practice, I don't think this is particularily useful - a new fine
// tuned `/search` request can be made
#[response(error = crate::Error)]
pub struct Response {
/// A grouping of search results by category.
pub chunk: Vec<SearchResult>,
/// An approximate count of the total number of results found.
pub approximate_total: UInt,
/// List of words which should be highlighted, useful for stemming which may
/// change the query terms.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub highlights: Vec<String>,
/// The point to return events from. If given, this should be a
// `next_batch` result from a previous call to this endpoint.
#[serde(skip_serializing_if = "Option::is_none")]
pub next_batch: Option<String>,
}
impl Request {
/// Creates a new `Request` with the given categories.
pub fn new(query: String) -> Self {
Self {
next_batch: None,
query,
context: EventContext::new(),
order: Order::Newest,
}
}
}
impl Response {
/// Creates a new `Response` with the given search results.
pub fn new(chunk: Vec<SearchResult>, approximate_total: UInt, highlights: Vec<String>, next_batch: Option<String>) -> Self {
Self {
chunk,
approximate_total,
highlights,
next_batch,
}
}
}
/// The order in which to search for results.
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
#[derive(Clone, Default, PartialEq, Eq, StringEnum)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[ruma_enum(rename_all = "snake_case")]
pub enum Order {
/// Prioritize recent events.
#[default]
Newest,
/// Prioritize older events.
Oldest,
/// Prioritize events by a numerical ranking of how closely they matched the search
/// criteria.
Rank,
#[doc(hidden)]
_Custom(PrivOwnedStr),
}
/// Configures whether any context for the events returned are included in the response.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct EventContext {
/// How many events before the result are returned.
#[serde(
default = "default_event_context_limit",
skip_serializing_if = "is_default_event_context_limit"
)]
pub before_limit: UInt,
/// How many events after the result are returned.
#[serde(
default = "default_event_context_limit",
skip_serializing_if = "is_default_event_context_limit"
)]
pub after_limit: UInt,
/// Requests that the server returns the historic profile information for the users that
/// sent the events that were returned.
#[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
pub include_profile: bool,
}
fn default_event_context_limit() -> UInt {
uint!(5)
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_default_event_context_limit(val: &UInt) -> bool {
*val == default_event_context_limit()
}
impl EventContext {
/// Creates an `EventContext` with all-default values.
pub fn new() -> Self {
Self {
before_limit: default_event_context_limit(),
after_limit: default_event_context_limit(),
include_profile: false,
}
}
/// Returns whether all fields have their default value.
pub fn is_default(&self) -> bool {
self.before_limit == default_event_context_limit()
&& self.after_limit == default_event_context_limit()
&& !self.include_profile
}
}
impl Default for EventContext {
fn default() -> Self {
Self::new()
}
}
/// Context for search results, if requested.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct EventContextResult {
/// Pagination token for the end of the chunk.
#[serde(skip_serializing_if = "Option::is_none")]
pub end: Option<String>,
/// Events just after the result.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub events_after: Vec<Raw<AnyTimelineEvent>>,
/// Events just before the result.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub events_before: Vec<Raw<AnyTimelineEvent>>,
/// The historic profile information of the users that sent the events returned.
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub profile_info: BTreeMap<OwnedUserId, UserProfile>,
/// Pagination token for the start of the chunk.
#[serde(skip_serializing_if = "Option::is_none")]
pub start: Option<String>,
}
impl EventContextResult {
/// Creates an empty `EventContextResult`.
pub fn new() -> Self {
Default::default()
}
/// Returns whether all fields are `None` or an empty list.
pub fn is_empty(&self) -> bool {
self.end.is_none()
&& self.events_after.is_empty()
&& self.events_before.is_empty()
&& self.profile_info.is_empty()
&& self.start.is_none()
}
}
/// A search result.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct SearchResult {
/// Context for result, if requested.
#[serde(skip_serializing_if = "EventContextResult::is_empty")]
pub context: EventContextResult,
/// The event that matched.
pub event: Raw<AnyTimelineEvent>,
}
/// A user profile.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct UserProfile {
/// The user's avatar URL, if set.
///
/// If you activate the `compat-empty-string-null` feature, this field being an empty
/// string in JSON will result in `None` here during deserialization.
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(
feature = "compat-empty-string-null",
serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none")
)]
pub avatar_url: Option<OwnedMxcUri>,
/// The user's display name, if set.
#[serde(skip_serializing_if = "Option::is_none")]
pub displayname: Option<String>,
}
impl UserProfile {
/// Creates an empty `UserProfile`.
pub fn new() -> Self {
Default::default()
}
/// Returns `true` if all fields are `None`.
pub fn is_empty(&self) -> bool {
self.avatar_url.is_none() && self.displayname.is_none()
}
}
}

View file

@ -0,0 +1,122 @@
//! `POST /_matrix/client/*/search/threads`
//!
//! Search for threads.
pub mod v3 {
//! `/v3/` ([spec])
//!
//! [spec]: TODO: write
use js_int::UInt;
use ruma_common::{
api::{request, response, Metadata},
metadata,
serde::{Raw, StringEnum},
};
use ruma_events::AnyTimelineEvent;
use serde::{Deserialize, Serialize};
use crate::PrivOwnedStr;
const METADATA: Metadata = metadata! {
method: POST,
rate_limited: true,
authentication: AccessToken,
history: {
1.0 => "/_matrix/client/v1/search/threads",
}
};
/// Request type for the `search` endpoint.
#[request(error = crate::Error)]
pub struct Request {
/// The point to return events from.
///
/// If given, this should be a `next_batch` result from a previous call to this endpoint.
#[ruma_api(query)]
pub next_batch: Option<String>,
/// Describes what to search for
pub query: String,
/// In what order to return threads
#[serde(default)]
pub order: Order,
}
/// Response type for the `search` endpoint.
// NOTE: A downside to merging next_batch instead of haing categories
// means users can't paginate one category after viewing results. In
// practice, I don't think this is particularily useful - a new fine
// tuned `/search` request can be made
#[response(error = crate::Error)]
pub struct Response {
/// A grouping of search results by category.
pub chunk: Vec<SearchResult>,
/// An approximate count of the total number of results found.
pub approximate_total: UInt,
/// List of words which should be highlighted, useful for stemming which may
/// change the query terms.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub highlights: Vec<String>,
/// The point to return events from. If given, this should be a
// `next_batch` result from a previous call to this endpoint.
#[serde(skip_serializing_if = "Option::is_none")]
pub next_batch: Option<String>,
}
impl Request {
/// Creates a new `Request` with the given categories.
pub fn new(query: String) -> Self {
Self {
next_batch: None,
query,
order: Order::default(),
}
}
}
impl Response {
/// Creates a new `Response` with the given search results.
pub fn new(chunk: Vec<SearchResult>, approximate_total: UInt, highlights: Vec<String>, next_batch: Option<String>) -> Self {
Self {
chunk,
approximate_total,
highlights,
next_batch,
}
}
}
/// The order in which to search for results.
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
#[derive(Clone, Default, PartialEq, Eq, StringEnum)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[ruma_enum(rename_all = "snake_case")]
pub enum Order {
/// Prioritize recent events.
#[default]
Newest,
/// Prioritize older events.
Oldest,
/// Prioritize events by a numerical ranking of how closely they matched the search
/// criteria.
Rank,
#[doc(hidden)]
_Custom(PrivOwnedStr),
}
/// A search result.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct SearchResult {
/// The event that matched.
pub event: Raw<AnyTimelineEvent>,
}
}

View file

@ -0,0 +1,105 @@
//! `POST /_matrix/client/*/search/users`
//!
//! Search for users. Replaces the user directory.
pub mod v3 {
//! `/v3/` ([spec])
//!
//! [spec]: TODO: write
use js_int::{uint, UInt};
use ruma_common::{
api::{request, response, Metadata},
metadata,
};
use serde::{Deserialize, Serialize};
use ruma_common::OwnedMxcUri;
const METADATA: Metadata = metadata! {
method: POST,
rate_limited: true,
authentication: AccessToken,
history: {
1.0 => "/_matrix/client/v1/search/users",
}
};
/// Request type for the `search` endpoint.
#[request(error = crate::Error)]
pub struct Request {
/// The point to return events from.
///
/// If given, this should be a `next_batch` result from a previous call to this endpoint.
#[ruma_api(query)]
pub next_batch: Option<String>,
/// The maximum number of results to return.
///
/// Defaults to 10.
#[serde(default = "default_limit", skip_serializing_if = "is_default_limit")]
pub limit: UInt,
/// The term to search for.
pub query: String,
}
/// Response type for the `search` endpoint.
#[response(error = crate::Error)]
pub struct Response {
/// Ordered by rank and then whether or not profile info is available.
pub chunk: Vec<User>,
/// A grouping of search results by category.
#[serde(skip_serializing_if = "Option::is_none")]
pub next_batch: Option<String>,
}
impl Request {
/// Creates a new `Request` with the given categories.
pub fn new(query: String) -> Self {
Self { next_batch: None, query, limit: default_limit() }
}
}
/// A user which really should be moved into its own api chunk
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct User {
/// The user's avatar URL, if set.
///
/// If you activate the `compat-empty-string-null` feature, this field being an empty
/// string in JSON will result in `None` here during deserialization.
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(
feature = "compat-empty-string-null",
serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none")
)]
pub avatar_url: Option<OwnedMxcUri>,
/// The user's display name, if set.
#[serde(skip_serializing_if = "Option::is_none")]
pub displayname: Option<String>,
/// The [BlurHash](https://blurha.sh) for the avatar pointed to by `avatar_url`.
///
/// This uses the unstable prefix in
/// [MSC2448](https://github.com/matrix-org/matrix-spec-proposals/pull/2448).
#[cfg(feature = "unstable-msc2448")]
#[serde(rename = "xyz.amorgan.blurhash", skip_serializing_if = "Option::is_none")]
pub blurhash: Option<String>,
}
impl Response {
/// Creates a new `Response` with the given search results.
pub fn new(chunk: Vec<User>, next_batch: Option<String>) -> Self {
Self { chunk, next_batch }
}
}
fn default_limit() -> UInt {
uint!(10)
}
fn is_default_limit(limit: &UInt) -> bool {
limit == &default_limit()
}
}