mod result_group_map_serde;
pub mod v3 {
use std::{
collections::{BTreeMap, btree_map},
ops::Deref,
};
use as_variant::as_variant;
use js_int::{UInt, uint};
use ruma_common::{
OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedUserId,
api::{auth_scheme::AccessToken, request, response},
metadata,
serde::{Raw, StringEnum},
};
use ruma_events::{AnyStateEvent, AnyTimelineEvent};
use serde::{Deserialize, Serialize};
use crate::{PrivOwnedStr, filter::RoomEventFilter};
metadata! {
method: POST,
rate_limited: true,
authentication: AccessToken,
history: {
1.0 => "/_matrix/client/r0/search",
1.1 => "/_matrix/client/v3/search",
}
}
#[request]
pub struct Request {
#[ruma_api(query)]
pub next_batch: Option<String>,
pub search_categories: Categories,
}
#[response]
pub struct Response {
pub search_categories: ResultCategories,
}
impl Request {
pub fn new(search_categories: Categories) -> Self {
Self { next_batch: None, search_categories }
}
}
impl Response {
pub fn new(search_categories: ResultCategories) -> Self {
Self { search_categories }
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
pub struct Categories {
#[serde(skip_serializing_if = "Option::is_none")]
pub room_events: Option<Criteria>,
}
impl Categories {
pub fn new() -> Self {
Default::default()
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
pub struct Criteria {
pub search_term: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub keys: Option<Vec<SearchKeys>>,
#[serde(default, skip_serializing_if = "RoomEventFilter::is_empty")]
pub filter: RoomEventFilter,
#[serde(skip_serializing_if = "Option::is_none")]
pub order_by: Option<OrderBy>,
#[serde(default, skip_serializing_if = "EventContext::is_default")]
pub event_context: EventContext,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_state: Option<bool>,
#[serde(default, skip_serializing_if = "Groupings::is_empty")]
pub groupings: Groupings,
}
impl Criteria {
pub fn new(search_term: String) -> Self {
Self {
search_term,
keys: None,
filter: RoomEventFilter::default(),
order_by: None,
event_context: Default::default(),
include_state: None,
groupings: Default::default(),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
pub struct EventContext {
#[serde(
default = "default_event_context_limit",
skip_serializing_if = "is_default_event_context_limit"
)]
pub before_limit: UInt,
#[serde(
default = "default_event_context_limit",
skip_serializing_if = "is_default_event_context_limit"
)]
pub after_limit: UInt,
#[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 {
pub fn new() -> Self {
Self {
before_limit: default_event_context_limit(),
after_limit: default_event_context_limit(),
include_profile: false,
}
}
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()
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
pub struct EventContextResult {
#[serde(skip_serializing_if = "Option::is_none")]
pub end: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub events_after: Vec<Raw<AnyTimelineEvent>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub events_before: Vec<Raw<AnyTimelineEvent>>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub profile_info: BTreeMap<OwnedUserId, UserProfile>,
#[serde(skip_serializing_if = "Option::is_none")]
pub start: Option<String>,
}
impl EventContextResult {
pub fn new() -> Self {
Default::default()
}
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()
}
}
#[derive(Clone, Default, Debug, Deserialize, Serialize)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
pub struct Grouping {
pub key: Option<GroupingKey>,
}
impl Grouping {
pub fn new() -> Self {
Default::default()
}
pub fn is_empty(&self) -> bool {
self.key.is_none()
}
}
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
#[derive(Clone, StringEnum)]
#[ruma_enum(rename_all = "snake_case")]
#[non_exhaustive]
pub enum GroupingKey {
RoomId,
Sender,
#[doc(hidden)]
_Custom(PrivOwnedStr),
}
#[derive(Clone, Default, Debug, Deserialize, Serialize)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
pub struct Groupings {
#[serde(default, skip_serializing_if = "<[_]>::is_empty")]
pub group_by: Vec<Grouping>,
}
impl Groupings {
pub fn new() -> Self {
Default::default()
}
pub fn is_empty(&self) -> bool {
self.group_by.is_empty()
}
}
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
#[derive(Clone, StringEnum)]
#[non_exhaustive]
pub enum SearchKeys {
#[ruma_enum(rename = "content.body")]
ContentBody,
#[ruma_enum(rename = "content.name")]
ContentName,
#[ruma_enum(rename = "content.topic")]
ContentTopic,
#[doc(hidden)]
_Custom(PrivOwnedStr),
}
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
#[derive(Clone, StringEnum)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
#[ruma_enum(rename_all = "snake_case")]
pub enum OrderBy {
Recent,
Rank,
#[doc(hidden)]
_Custom(PrivOwnedStr),
}
#[derive(Clone, Default, Debug, Deserialize, Serialize)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
pub struct ResultCategories {
#[serde(default, skip_serializing_if = "ResultRoomEvents::is_empty")]
pub room_events: ResultRoomEvents,
}
impl ResultCategories {
pub fn new() -> Self {
Default::default()
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
pub struct ResultRoomEvents {
#[serde(skip_serializing_if = "Option::is_none")]
pub count: Option<UInt>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub groups: ResultGroupMapsByGroupingKey,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_batch: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub results: Vec<SearchResult>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub state: BTreeMap<OwnedRoomId, Vec<Raw<AnyStateEvent>>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub highlights: Vec<String>,
}
impl ResultRoomEvents {
pub fn new() -> Self {
Default::default()
}
pub fn is_empty(&self) -> bool {
self.count.is_none()
&& self.groups.is_empty()
&& self.next_batch.is_none()
&& self.results.is_empty()
&& self.state.is_empty()
&& self.highlights.is_empty()
}
}
#[derive(Clone, Debug, Default)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
pub struct ResultGroupMapsByGroupingKey(BTreeMap<GroupingKey, ResultGroupMap>);
impl ResultGroupMapsByGroupingKey {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, map: ResultGroupMap) -> Option<ResultGroupMap> {
self.0.insert(map.grouping_key(), map)
}
}
impl Deref for ResultGroupMapsByGroupingKey {
type Target = BTreeMap<GroupingKey, ResultGroupMap>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl FromIterator<ResultGroupMap> for ResultGroupMapsByGroupingKey {
fn from_iter<T: IntoIterator<Item = ResultGroupMap>>(iter: T) -> Self {
Self(iter.into_iter().map(|map| (map.grouping_key(), map)).collect())
}
}
impl Extend<ResultGroupMap> for ResultGroupMapsByGroupingKey {
fn extend<T: IntoIterator<Item = ResultGroupMap>>(&mut self, iter: T) {
self.0.extend(iter.into_iter().map(|map| (map.grouping_key(), map)));
}
}
impl IntoIterator for ResultGroupMapsByGroupingKey {
type Item = ResultGroupMap;
type IntoIter = btree_map::IntoValues<GroupingKey, ResultGroupMap>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_values()
}
}
#[derive(Clone, Debug)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
pub enum ResultGroupMap {
RoomId(BTreeMap<OwnedRoomId, ResultGroup>),
Sender(BTreeMap<OwnedUserId, ResultGroup>),
#[doc(hidden)]
_Custom(CustomResultGroupMap),
}
impl ResultGroupMap {
pub fn grouping_key(&self) -> GroupingKey {
match self {
Self::RoomId(_) => GroupingKey::RoomId,
Self::Sender(_) => GroupingKey::Sender,
Self::_Custom(custom) => custom.grouping_key.as_str().into(),
}
}
pub fn custom_map(&self) -> Option<&BTreeMap<String, ResultGroup>> {
as_variant!(self, Self::_Custom).map(|custom| &custom.map)
}
pub fn into_custom_map(self) -> Option<BTreeMap<String, ResultGroup>> {
as_variant!(self, Self::_Custom).map(|custom| custom.map)
}
}
#[doc(hidden)]
#[derive(Clone, Debug)]
pub struct CustomResultGroupMap {
pub(super) grouping_key: String,
pub(super) map: BTreeMap<String, ResultGroup>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
pub struct ResultGroup {
#[serde(skip_serializing_if = "Option::is_none")]
pub next_batch: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub order: Option<UInt>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub results: Vec<OwnedEventId>,
}
impl ResultGroup {
pub fn new() -> Self {
Default::default()
}
pub fn is_empty(&self) -> bool {
self.next_batch.is_none() && self.order.is_none() && self.results.is_empty()
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
pub struct SearchResult {
#[serde(default, skip_serializing_if = "EventContextResult::is_empty")]
pub context: EventContextResult,
#[serde(skip_serializing_if = "Option::is_none")]
pub rank: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<Raw<AnyTimelineEvent>>,
}
impl SearchResult {
pub fn new() -> Self {
Default::default()
}
pub fn is_empty(&self) -> bool {
self.context.is_empty() && self.rank.is_none() && self.result.is_none()
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
pub struct UserProfile {
#[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>,
#[serde(skip_serializing_if = "Option::is_none")]
pub displayname: Option<String>,
}
impl UserProfile {
pub fn new() -> Self {
Default::default()
}
pub fn is_empty(&self) -> bool {
self.avatar_url.is_none() && self.displayname.is_none()
}
}
}
#[cfg(all(test, feature = "client", feature = "server"))]
mod tests {
use std::{borrow::Cow, collections::BTreeMap};
use assert_matches2::assert_matches;
use js_int::uint;
use ruma_common::{
api::{
IncomingRequest, IncomingResponse, OutgoingRequest, OutgoingResponse,
SupportedVersions, auth_scheme::SendAccessToken,
},
event_id, room_id,
};
use serde_json::{
Value as JsonValue, from_slice as from_json_slice, json, to_vec as to_json_vec,
};
use super::v3::{GroupingKey, OrderBy, Request, Response, ResultGroupMap, SearchKeys};
#[test]
fn request_roundtrip() {
let body = json!({
"search_categories": {
"room_events": {
"groupings": {
"group_by": [
{ "key": "room_id" },
],
},
"keys": ["content.body"],
"order_by": "recent",
"search_term": "martians and men"
}
}
});
let http_request = http::Request::post("http://localhost/_matrix/client/v3/search")
.body(to_json_vec(&body).unwrap())
.unwrap();
let request = Request::try_from_http_request(http_request, &[] as &[&str]).unwrap();
let criteria = request.search_categories.room_events.as_ref().unwrap();
assert_eq!(criteria.groupings.group_by.len(), 1);
assert_eq!(criteria.groupings.group_by[0].key, Some(GroupingKey::RoomId));
let keys = criteria.keys.as_ref().unwrap();
assert_eq!(keys.len(), 1);
assert_eq!(keys[0], SearchKeys::ContentBody);
assert_eq!(criteria.order_by, Some(OrderBy::Recent));
assert_eq!(criteria.search_term, "martians and men");
let http_request = request
.try_into_http_request::<Vec<u8>>(
"http://localhost",
SendAccessToken::IfRequired("access_token"),
Cow::Owned(SupportedVersions::from_parts(&["v1.4".to_owned()], &BTreeMap::new())),
)
.unwrap();
assert_eq!(from_json_slice::<JsonValue>(http_request.body()).unwrap(), body);
}
#[test]
fn response_roundtrip() {
let body = json!({
"search_categories": {
"room_events": {
"count": 1224,
"groups": {
"room_id": {
"!qPewotXpIctQySfjSy:localhost": {
"next_batch": "BdgFsdfHSf-dsFD",
"order": 1,
"results": ["$144429830826TWwbB:localhost"],
},
},
},
"highlights": [
"martians",
"men",
],
"next_batch": "5FdgFsd234dfgsdfFD",
"results": [
{
"rank": 0.004_248_66,
"result": {
"content": {
"body": "This is an example text message",
"format": "org.matrix.custom.html",
"formatted_body": "<b>This is an example text message</b>",
"msgtype": "m.text",
},
"event_id": "$144429830826TWwbB:localhost",
"origin_server_ts": 1_735_824_653,
"room_id": "!qPewotXpIctQySfjSy:localhost",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1234,
"membership": "join",
}
}
}
]
}
}
});
let result_event_id = event_id!("$144429830826TWwbB:localhost");
let http_request = http::Response::new(to_json_vec(&body).unwrap());
let response = Response::try_from_http_response(http_request).unwrap();
let results = &response.search_categories.room_events;
assert_eq!(results.count, Some(uint!(1224)));
assert_eq!(results.groups.len(), 1);
assert_matches!(
results.groups.get(&GroupingKey::RoomId),
Some(ResultGroupMap::RoomId(room_id_group_map))
);
assert_eq!(room_id_group_map.len(), 1);
let room_id_group =
room_id_group_map.get(room_id!("!qPewotXpIctQySfjSy:localhost")).unwrap();
assert_eq!(room_id_group.results, &[result_event_id]);
assert_eq!(results.highlights, &["martians", "men"]);
assert_eq!(results.next_batch.as_deref(), Some("5FdgFsd234dfgsdfFD"));
assert_eq!(results.results.len(), 1);
assert_eq!(results.results[0].rank, Some(0.004_248_66));
let result = results.results[0].result.as_ref().unwrap().deserialize().unwrap();
assert_eq!(result.event_id(), result_event_id);
let http_response = response.try_into_http_response::<Vec<u8>>().unwrap();
assert_eq!(from_json_slice::<JsonValue>(http_response.body()).unwrap(), body);
}
}