Add apis for searching
This commit is contained in:
parent
1404f50b79
commit
6e2d6ef142
4 changed files with 495 additions and 1 deletions
|
@ -1,3 +1,5 @@
|
|||
//! Endpoints for event searches.
|
||||
|
||||
pub mod search_events;
|
||||
pub mod search_messages;
|
||||
pub mod search_threads;
|
||||
pub mod search_users;
|
||||
|
|
265
crates/ruma-client-api/src/search/search_messages.rs
Normal file
265
crates/ruma-client-api/src/search/search_messages.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
122
crates/ruma-client-api/src/search/search_threads.rs
Normal file
122
crates/ruma-client-api/src/search/search_threads.rs
Normal 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>,
|
||||
}
|
||||
}
|
105
crates/ruma-client-api/src/search/search_users.rs
Normal file
105
crates/ruma-client-api/src/search/search_users.rs
Normal 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()
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue