1mod result_group_map_serde;
6
7pub mod v3 {
8 use std::{
13 collections::{BTreeMap, btree_map},
14 ops::Deref,
15 };
16
17 use as_variant::as_variant;
18 use js_int::{UInt, uint};
19 use ruma_common::{
20 OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedUserId,
21 api::{auth_scheme::AccessToken, request, response},
22 metadata,
23 serde::{Raw, StringEnum},
24 };
25 use ruma_events::{AnyStateEvent, AnyTimelineEvent};
26 use serde::{Deserialize, Serialize};
27
28 use crate::{PrivOwnedStr, filter::RoomEventFilter};
29
30 metadata! {
31 method: POST,
32 rate_limited: true,
33 authentication: AccessToken,
34 history: {
35 1.0 => "/_matrix/client/r0/search",
36 1.1 => "/_matrix/client/v3/search",
37 }
38 }
39
40 #[request]
42 pub struct Request {
43 #[ruma_api(query)]
47 pub next_batch: Option<String>,
48
49 pub search_categories: Categories,
51 }
52
53 #[response]
55 pub struct Response {
56 pub search_categories: ResultCategories,
58 }
59
60 impl Request {
61 pub fn new(search_categories: Categories) -> Self {
63 Self { next_batch: None, search_categories }
64 }
65 }
66
67 impl Response {
68 pub fn new(search_categories: ResultCategories) -> Self {
70 Self { search_categories }
71 }
72 }
73
74 #[derive(Clone, Debug, Default, Deserialize, Serialize)]
76 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
77 pub struct Categories {
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub room_events: Option<Criteria>,
81 }
82
83 impl Categories {
84 pub fn new() -> Self {
86 Default::default()
87 }
88 }
89
90 #[derive(Clone, Debug, Deserialize, Serialize)]
92 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
93 pub struct Criteria {
94 pub search_term: String,
96
97 #[serde(skip_serializing_if = "Option::is_none")]
101 pub keys: Option<Vec<SearchKeys>>,
102
103 #[serde(default, skip_serializing_if = "RoomEventFilter::is_empty")]
105 pub filter: RoomEventFilter,
106
107 #[serde(skip_serializing_if = "Option::is_none")]
109 pub order_by: Option<OrderBy>,
110
111 #[serde(default, skip_serializing_if = "EventContext::is_default")]
113 pub event_context: EventContext,
114
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub include_state: Option<bool>,
118
119 #[serde(default, skip_serializing_if = "Groupings::is_empty")]
121 pub groupings: Groupings,
122 }
123
124 impl Criteria {
125 pub fn new(search_term: String) -> Self {
127 Self {
128 search_term,
129 keys: None,
130 filter: RoomEventFilter::default(),
131 order_by: None,
132 event_context: Default::default(),
133 include_state: None,
134 groupings: Default::default(),
135 }
136 }
137 }
138
139 #[derive(Clone, Debug, Deserialize, Serialize)]
141 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
142 pub struct EventContext {
143 #[serde(
145 default = "default_event_context_limit",
146 skip_serializing_if = "is_default_event_context_limit"
147 )]
148 pub before_limit: UInt,
149
150 #[serde(
152 default = "default_event_context_limit",
153 skip_serializing_if = "is_default_event_context_limit"
154 )]
155 pub after_limit: UInt,
156
157 #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
160 pub include_profile: bool,
161 }
162
163 fn default_event_context_limit() -> UInt {
164 uint!(5)
165 }
166
167 #[allow(clippy::trivially_copy_pass_by_ref)]
168 fn is_default_event_context_limit(val: &UInt) -> bool {
169 *val == default_event_context_limit()
170 }
171
172 impl EventContext {
173 pub fn new() -> Self {
175 Self {
176 before_limit: default_event_context_limit(),
177 after_limit: default_event_context_limit(),
178 include_profile: false,
179 }
180 }
181
182 pub fn is_default(&self) -> bool {
184 self.before_limit == default_event_context_limit()
185 && self.after_limit == default_event_context_limit()
186 && !self.include_profile
187 }
188 }
189
190 impl Default for EventContext {
191 fn default() -> Self {
192 Self::new()
193 }
194 }
195
196 #[derive(Clone, Debug, Default, Deserialize, Serialize)]
198 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
199 pub struct EventContextResult {
200 #[serde(skip_serializing_if = "Option::is_none")]
202 pub end: Option<String>,
203
204 #[serde(default, skip_serializing_if = "Vec::is_empty")]
206 pub events_after: Vec<Raw<AnyTimelineEvent>>,
207
208 #[serde(default, skip_serializing_if = "Vec::is_empty")]
210 pub events_before: Vec<Raw<AnyTimelineEvent>>,
211
212 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
214 pub profile_info: BTreeMap<OwnedUserId, UserProfile>,
215
216 #[serde(skip_serializing_if = "Option::is_none")]
218 pub start: Option<String>,
219 }
220
221 impl EventContextResult {
222 pub fn new() -> Self {
224 Default::default()
225 }
226
227 pub fn is_empty(&self) -> bool {
229 let Self { end, events_after, events_before, profile_info, start } = self;
230 end.is_none()
231 && events_after.is_empty()
232 && events_before.is_empty()
233 && profile_info.is_empty()
234 && start.is_none()
235 }
236 }
237
238 #[derive(Clone, Default, Debug, Deserialize, Serialize)]
240 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
241 pub struct Grouping {
242 pub key: Option<GroupingKey>,
244 }
245
246 impl Grouping {
247 pub fn new() -> Self {
249 Default::default()
250 }
251
252 pub fn is_empty(&self) -> bool {
254 let Self { key } = self;
255 key.is_none()
256 }
257 }
258
259 #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
261 #[derive(Clone, StringEnum)]
262 #[ruma_enum(rename_all = "snake_case")]
263 #[non_exhaustive]
264 pub enum GroupingKey {
265 RoomId,
267
268 Sender,
270
271 #[doc(hidden)]
272 _Custom(PrivOwnedStr),
273 }
274
275 #[derive(Clone, Default, Debug, Deserialize, Serialize)]
277 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
278 pub struct Groupings {
279 #[serde(default, skip_serializing_if = "<[_]>::is_empty")]
281 pub group_by: Vec<Grouping>,
282 }
283
284 impl Groupings {
285 pub fn new() -> Self {
287 Default::default()
288 }
289
290 pub fn is_empty(&self) -> bool {
292 let Self { group_by } = self;
293 group_by.is_empty()
294 }
295 }
296
297 #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
299 #[derive(Clone, StringEnum)]
300 #[non_exhaustive]
301 pub enum SearchKeys {
302 #[ruma_enum(rename = "content.body")]
304 ContentBody,
305
306 #[ruma_enum(rename = "content.name")]
308 ContentName,
309
310 #[ruma_enum(rename = "content.topic")]
312 ContentTopic,
313
314 #[doc(hidden)]
315 _Custom(PrivOwnedStr),
316 }
317
318 #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
320 #[derive(Clone, StringEnum)]
321 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
322 #[ruma_enum(rename_all = "snake_case")]
323 pub enum OrderBy {
324 Recent,
326
327 Rank,
330
331 #[doc(hidden)]
332 _Custom(PrivOwnedStr),
333 }
334
335 #[derive(Clone, Default, Debug, Deserialize, Serialize)]
337 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
338 pub struct ResultCategories {
339 #[serde(default, skip_serializing_if = "ResultRoomEvents::is_empty")]
341 pub room_events: ResultRoomEvents,
342 }
343
344 impl ResultCategories {
345 pub fn new() -> Self {
347 Default::default()
348 }
349 }
350
351 #[derive(Clone, Debug, Default, Deserialize, Serialize)]
353 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
354 pub struct ResultRoomEvents {
355 #[serde(skip_serializing_if = "Option::is_none")]
357 pub count: Option<UInt>,
358
359 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
361 pub groups: ResultGroupMapsByGroupingKey,
362
363 #[serde(skip_serializing_if = "Option::is_none")]
368 pub next_batch: Option<String>,
369
370 #[serde(default, skip_serializing_if = "Vec::is_empty")]
372 pub results: Vec<SearchResult>,
373
374 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
378 pub state: BTreeMap<OwnedRoomId, Vec<Raw<AnyStateEvent>>>,
379
380 #[serde(default, skip_serializing_if = "Vec::is_empty")]
383 pub highlights: Vec<String>,
384 }
385
386 impl ResultRoomEvents {
387 pub fn new() -> Self {
389 Default::default()
390 }
391
392 pub fn is_empty(&self) -> bool {
394 let Self { count, groups, next_batch, results, state, highlights } = self;
395 count.is_none()
396 && groups.is_empty()
397 && next_batch.is_none()
398 && results.is_empty()
399 && state.is_empty()
400 && highlights.is_empty()
401 }
402 }
403
404 #[derive(Clone, Debug, Default)]
409 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
410 pub struct ResultGroupMapsByGroupingKey(BTreeMap<GroupingKey, ResultGroupMap>);
411
412 impl ResultGroupMapsByGroupingKey {
413 pub fn new() -> Self {
415 Self::default()
416 }
417
418 pub fn insert(&mut self, map: ResultGroupMap) -> Option<ResultGroupMap> {
422 self.0.insert(map.grouping_key(), map)
423 }
424 }
425
426 impl Deref for ResultGroupMapsByGroupingKey {
427 type Target = BTreeMap<GroupingKey, ResultGroupMap>;
428
429 fn deref(&self) -> &Self::Target {
430 &self.0
431 }
432 }
433
434 impl FromIterator<ResultGroupMap> for ResultGroupMapsByGroupingKey {
435 fn from_iter<T: IntoIterator<Item = ResultGroupMap>>(iter: T) -> Self {
436 Self(iter.into_iter().map(|map| (map.grouping_key(), map)).collect())
437 }
438 }
439
440 impl Extend<ResultGroupMap> for ResultGroupMapsByGroupingKey {
441 fn extend<T: IntoIterator<Item = ResultGroupMap>>(&mut self, iter: T) {
442 self.0.extend(iter.into_iter().map(|map| (map.grouping_key(), map)));
443 }
444 }
445
446 impl IntoIterator for ResultGroupMapsByGroupingKey {
447 type Item = ResultGroupMap;
448 type IntoIter = btree_map::IntoValues<GroupingKey, ResultGroupMap>;
449
450 fn into_iter(self) -> Self::IntoIter {
451 self.0.into_values()
452 }
453 }
454
455 #[derive(Clone, Debug)]
457 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
458 pub enum ResultGroupMap {
459 RoomId(BTreeMap<OwnedRoomId, ResultGroup>),
461
462 Sender(BTreeMap<OwnedUserId, ResultGroup>),
464
465 #[doc(hidden)]
466 _Custom(CustomResultGroupMap),
467 }
468
469 impl ResultGroupMap {
470 pub fn grouping_key(&self) -> GroupingKey {
472 match self {
473 Self::RoomId(_) => GroupingKey::RoomId,
474 Self::Sender(_) => GroupingKey::Sender,
475 Self::_Custom(custom) => custom.grouping_key.as_str().into(),
476 }
477 }
478
479 pub fn custom_map(&self) -> Option<&BTreeMap<String, ResultGroup>> {
481 as_variant!(self, Self::_Custom).map(|custom| &custom.map)
482 }
483
484 pub fn into_custom_map(self) -> Option<BTreeMap<String, ResultGroup>> {
486 as_variant!(self, Self::_Custom).map(|custom| custom.map)
487 }
488 }
489
490 #[doc(hidden)]
492 #[derive(Clone, Debug)]
493 pub struct CustomResultGroupMap {
494 pub(super) grouping_key: String,
496
497 pub(super) map: BTreeMap<String, ResultGroup>,
499 }
500
501 #[derive(Clone, Debug, Default, Deserialize, Serialize)]
503 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
504 pub struct ResultGroup {
505 #[serde(skip_serializing_if = "Option::is_none")]
510 pub next_batch: Option<String>,
511
512 #[serde(skip_serializing_if = "Option::is_none")]
514 pub order: Option<UInt>,
515
516 #[serde(default, skip_serializing_if = "Vec::is_empty")]
518 pub results: Vec<OwnedEventId>,
519 }
520
521 impl ResultGroup {
522 pub fn new() -> Self {
524 Default::default()
525 }
526
527 pub fn is_empty(&self) -> bool {
529 let Self { next_batch, order, results } = self;
530 next_batch.is_none() && order.is_none() && results.is_empty()
531 }
532 }
533
534 #[derive(Clone, Debug, Default, Deserialize, Serialize)]
536 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
537 pub struct SearchResult {
538 #[serde(default, skip_serializing_if = "EventContextResult::is_empty")]
540 pub context: EventContextResult,
541
542 #[serde(skip_serializing_if = "Option::is_none")]
546 pub rank: Option<f64>,
547
548 #[serde(skip_serializing_if = "Option::is_none")]
550 pub result: Option<Raw<AnyTimelineEvent>>,
551 }
552
553 impl SearchResult {
554 pub fn new() -> Self {
556 Default::default()
557 }
558
559 pub fn is_empty(&self) -> bool {
561 let Self { context, rank, result } = self;
562 context.is_empty() && rank.is_none() && result.is_none()
563 }
564 }
565
566 #[derive(Clone, Debug, Default, Deserialize, Serialize)]
568 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
569 pub struct UserProfile {
570 #[serde(skip_serializing_if = "Option::is_none")]
575 #[cfg_attr(
576 feature = "compat-empty-string-null",
577 serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none")
578 )]
579 pub avatar_url: Option<OwnedMxcUri>,
580
581 #[serde(skip_serializing_if = "Option::is_none")]
583 pub displayname: Option<String>,
584 }
585
586 impl UserProfile {
587 pub fn new() -> Self {
589 Default::default()
590 }
591
592 pub fn is_empty(&self) -> bool {
594 let Self { avatar_url, displayname } = self;
595 avatar_url.is_none() && displayname.is_none()
596 }
597 }
598}
599
600#[cfg(all(test, feature = "client", feature = "server"))]
601mod tests {
602 use std::{borrow::Cow, collections::BTreeMap};
603
604 use assert_matches2::assert_matches;
605 use js_int::uint;
606 use ruma_common::{
607 api::{
608 IncomingRequest, IncomingResponse, OutgoingRequestExt as _, OutgoingResponse,
609 SupportedVersions, auth_scheme::SendAccessToken,
610 },
611 event_id, room_id,
612 };
613 use serde_json::{
614 Value as JsonValue, from_slice as from_json_slice, json, to_vec as to_json_vec,
615 };
616
617 use super::v3::{GroupingKey, OrderBy, Request, Response, ResultGroupMap, SearchKeys};
618
619 #[test]
620 fn request_roundtrip() {
621 let body = json!({
622 "search_categories": {
623 "room_events": {
624 "groupings": {
625 "group_by": [
626 { "key": "room_id" },
627 ],
628 },
629 "keys": ["content.body"],
630 "order_by": "recent",
631 "search_term": "martians and men"
632 }
633 }
634 });
635
636 let http_request = http::Request::post("http://localhost/_matrix/client/v3/search")
637 .body(to_json_vec(&body).unwrap())
638 .unwrap();
639 let request = Request::try_from_http_request(http_request, &[] as &[&str]).unwrap();
640
641 let criteria = request.search_categories.room_events.as_ref().unwrap();
642 assert_eq!(criteria.groupings.group_by.len(), 1);
643 assert_eq!(criteria.groupings.group_by[0].key, Some(GroupingKey::RoomId));
644 let keys = criteria.keys.as_ref().unwrap();
645 assert_eq!(keys.len(), 1);
646 assert_eq!(keys[0], SearchKeys::ContentBody);
647 assert_eq!(criteria.order_by, Some(OrderBy::Recent));
648 assert_eq!(criteria.search_term, "martians and men");
649
650 let http_request = request
651 .try_into_http_request::<Vec<u8>>(
652 "http://localhost",
653 SendAccessToken::IfRequired("access_token"),
654 Cow::Owned(SupportedVersions::from_parts(&["v1.4".to_owned()], &BTreeMap::new())),
655 )
656 .unwrap();
657 assert_eq!(from_json_slice::<JsonValue>(http_request.body()).unwrap(), body);
658 }
659
660 #[test]
661 fn response_roundtrip() {
662 let body = json!({
663 "search_categories": {
664 "room_events": {
665 "count": 1224,
666 "groups": {
667 "room_id": {
668 "!qPewotXpIctQySfjSy:localhost": {
669 "next_batch": "BdgFsdfHSf-dsFD",
670 "order": 1,
671 "results": ["$144429830826TWwbB:localhost"],
672 },
673 },
674 },
675 "highlights": [
676 "martians",
677 "men",
678 ],
679 "next_batch": "5FdgFsd234dfgsdfFD",
680 "results": [
681 {
682 "rank": 0.004_248_66,
683 "result": {
684 "content": {
685 "body": "This is an example text message",
686 "format": "org.matrix.custom.html",
687 "formatted_body": "<b>This is an example text message</b>",
688 "msgtype": "m.text",
689 },
690 "event_id": "$144429830826TWwbB:localhost",
691 "origin_server_ts": 1_735_824_653,
692 "room_id": "!qPewotXpIctQySfjSy:localhost",
693 "sender": "@example:example.org",
694 "type": "m.room.message",
695 "unsigned": {
696 "age": 1234,
697 "membership": "join",
698 }
699 }
700 }
701 ]
702 }
703 }
704 });
705 let result_event_id = event_id!("$144429830826TWwbB:localhost");
706
707 let http_request = http::Response::new(to_json_vec(&body).unwrap());
708 let response = Response::try_from_http_response(http_request).unwrap();
709
710 let results = &response.search_categories.room_events;
711 assert_eq!(results.count, Some(uint!(1224)));
712 assert_eq!(results.groups.len(), 1);
713 assert_matches!(
714 results.groups.get(&GroupingKey::RoomId),
715 Some(ResultGroupMap::RoomId(room_id_group_map))
716 );
717 assert_eq!(room_id_group_map.len(), 1);
718 let room_id_group =
719 room_id_group_map.get(room_id!("!qPewotXpIctQySfjSy:localhost")).unwrap();
720 assert_eq!(room_id_group.results, &[result_event_id]);
721 assert_eq!(results.highlights, &["martians", "men"]);
722 assert_eq!(results.next_batch.as_deref(), Some("5FdgFsd234dfgsdfFD"));
723 assert_eq!(results.results.len(), 1);
724 assert_eq!(results.results[0].rank, Some(0.004_248_66));
725 let result = results.results[0].result.as_ref().unwrap().deserialize().unwrap();
726 assert_eq!(result.event_id(), result_event_id);
727
728 let http_response = response.try_into_http_response::<Vec<u8>>().unwrap();
729 assert_eq!(from_json_slice::<JsonValue>(http_response.body()).unwrap(), body);
730 }
731}