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(error = crate::Error)]
42 pub struct Request {
43 #[ruma_api(query)]
47 pub next_batch: Option<String>,
48
49 pub search_categories: Categories,
51 }
52
53 #[response(error = crate::Error)]
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 self.end.is_none()
230 && self.events_after.is_empty()
231 && self.events_before.is_empty()
232 && self.profile_info.is_empty()
233 && self.start.is_none()
234 }
235 }
236
237 #[derive(Clone, Default, Debug, Deserialize, Serialize)]
239 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
240 pub struct Grouping {
241 pub key: Option<GroupingKey>,
243 }
244
245 impl Grouping {
246 pub fn new() -> Self {
248 Default::default()
249 }
250
251 pub fn is_empty(&self) -> bool {
253 self.key.is_none()
254 }
255 }
256
257 #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
259 #[derive(Clone, StringEnum)]
260 #[ruma_enum(rename_all = "snake_case")]
261 #[non_exhaustive]
262 pub enum GroupingKey {
263 RoomId,
265
266 Sender,
268
269 #[doc(hidden)]
270 _Custom(PrivOwnedStr),
271 }
272
273 #[derive(Clone, Default, Debug, Deserialize, Serialize)]
275 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
276 pub struct Groupings {
277 #[serde(default, skip_serializing_if = "<[_]>::is_empty")]
279 pub group_by: Vec<Grouping>,
280 }
281
282 impl Groupings {
283 pub fn new() -> Self {
285 Default::default()
286 }
287
288 pub fn is_empty(&self) -> bool {
290 self.group_by.is_empty()
291 }
292 }
293
294 #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
296 #[derive(Clone, StringEnum)]
297 #[non_exhaustive]
298 pub enum SearchKeys {
299 #[ruma_enum(rename = "content.body")]
301 ContentBody,
302
303 #[ruma_enum(rename = "content.name")]
305 ContentName,
306
307 #[ruma_enum(rename = "content.topic")]
309 ContentTopic,
310
311 #[doc(hidden)]
312 _Custom(PrivOwnedStr),
313 }
314
315 #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
317 #[derive(Clone, StringEnum)]
318 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
319 #[ruma_enum(rename_all = "snake_case")]
320 pub enum OrderBy {
321 Recent,
323
324 Rank,
327
328 #[doc(hidden)]
329 _Custom(PrivOwnedStr),
330 }
331
332 #[derive(Clone, Default, Debug, Deserialize, Serialize)]
334 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
335 pub struct ResultCategories {
336 #[serde(default, skip_serializing_if = "ResultRoomEvents::is_empty")]
338 pub room_events: ResultRoomEvents,
339 }
340
341 impl ResultCategories {
342 pub fn new() -> Self {
344 Default::default()
345 }
346 }
347
348 #[derive(Clone, Debug, Default, Deserialize, Serialize)]
350 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
351 pub struct ResultRoomEvents {
352 #[serde(skip_serializing_if = "Option::is_none")]
354 pub count: Option<UInt>,
355
356 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
358 pub groups: ResultGroupMapsByGroupingKey,
359
360 #[serde(skip_serializing_if = "Option::is_none")]
365 pub next_batch: Option<String>,
366
367 #[serde(default, skip_serializing_if = "Vec::is_empty")]
369 pub results: Vec<SearchResult>,
370
371 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
375 pub state: BTreeMap<OwnedRoomId, Vec<Raw<AnyStateEvent>>>,
376
377 #[serde(default, skip_serializing_if = "Vec::is_empty")]
380 pub highlights: Vec<String>,
381 }
382
383 impl ResultRoomEvents {
384 pub fn new() -> Self {
386 Default::default()
387 }
388
389 pub fn is_empty(&self) -> bool {
391 self.count.is_none()
392 && self.groups.is_empty()
393 && self.next_batch.is_none()
394 && self.results.is_empty()
395 && self.state.is_empty()
396 && self.highlights.is_empty()
397 }
398 }
399
400 #[derive(Clone, Debug, Default)]
405 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
406 pub struct ResultGroupMapsByGroupingKey(BTreeMap<GroupingKey, ResultGroupMap>);
407
408 impl ResultGroupMapsByGroupingKey {
409 pub fn new() -> Self {
411 Self::default()
412 }
413
414 pub fn insert(&mut self, map: ResultGroupMap) -> Option<ResultGroupMap> {
418 self.0.insert(map.grouping_key(), map)
419 }
420 }
421
422 impl Deref for ResultGroupMapsByGroupingKey {
423 type Target = BTreeMap<GroupingKey, ResultGroupMap>;
424
425 fn deref(&self) -> &Self::Target {
426 &self.0
427 }
428 }
429
430 impl FromIterator<ResultGroupMap> for ResultGroupMapsByGroupingKey {
431 fn from_iter<T: IntoIterator<Item = ResultGroupMap>>(iter: T) -> Self {
432 Self(iter.into_iter().map(|map| (map.grouping_key(), map)).collect())
433 }
434 }
435
436 impl Extend<ResultGroupMap> for ResultGroupMapsByGroupingKey {
437 fn extend<T: IntoIterator<Item = ResultGroupMap>>(&mut self, iter: T) {
438 self.0.extend(iter.into_iter().map(|map| (map.grouping_key(), map)));
439 }
440 }
441
442 impl IntoIterator for ResultGroupMapsByGroupingKey {
443 type Item = ResultGroupMap;
444 type IntoIter = btree_map::IntoValues<GroupingKey, ResultGroupMap>;
445
446 fn into_iter(self) -> Self::IntoIter {
447 self.0.into_values()
448 }
449 }
450
451 #[derive(Clone, Debug)]
453 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
454 pub enum ResultGroupMap {
455 RoomId(BTreeMap<OwnedRoomId, ResultGroup>),
457
458 Sender(BTreeMap<OwnedUserId, ResultGroup>),
460
461 #[doc(hidden)]
462 _Custom(CustomResultGroupMap),
463 }
464
465 impl ResultGroupMap {
466 pub fn grouping_key(&self) -> GroupingKey {
468 match self {
469 Self::RoomId(_) => GroupingKey::RoomId,
470 Self::Sender(_) => GroupingKey::Sender,
471 Self::_Custom(custom) => custom.grouping_key.as_str().into(),
472 }
473 }
474
475 pub fn custom_map(&self) -> Option<&BTreeMap<String, ResultGroup>> {
477 as_variant!(self, Self::_Custom).map(|custom| &custom.map)
478 }
479
480 pub fn into_custom_map(self) -> Option<BTreeMap<String, ResultGroup>> {
482 as_variant!(self, Self::_Custom).map(|custom| custom.map)
483 }
484 }
485
486 #[doc(hidden)]
488 #[derive(Clone, Debug)]
489 pub struct CustomResultGroupMap {
490 pub(super) grouping_key: String,
492
493 pub(super) map: BTreeMap<String, ResultGroup>,
495 }
496
497 #[derive(Clone, Debug, Default, Deserialize, Serialize)]
499 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
500 pub struct ResultGroup {
501 #[serde(skip_serializing_if = "Option::is_none")]
506 pub next_batch: Option<String>,
507
508 #[serde(skip_serializing_if = "Option::is_none")]
510 pub order: Option<UInt>,
511
512 #[serde(default, skip_serializing_if = "Vec::is_empty")]
514 pub results: Vec<OwnedEventId>,
515 }
516
517 impl ResultGroup {
518 pub fn new() -> Self {
520 Default::default()
521 }
522
523 pub fn is_empty(&self) -> bool {
525 self.next_batch.is_none() && self.order.is_none() && self.results.is_empty()
526 }
527 }
528
529 #[derive(Clone, Debug, Default, Deserialize, Serialize)]
531 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
532 pub struct SearchResult {
533 #[serde(default, skip_serializing_if = "EventContextResult::is_empty")]
535 pub context: EventContextResult,
536
537 #[serde(skip_serializing_if = "Option::is_none")]
541 pub rank: Option<f64>,
542
543 #[serde(skip_serializing_if = "Option::is_none")]
545 pub result: Option<Raw<AnyTimelineEvent>>,
546 }
547
548 impl SearchResult {
549 pub fn new() -> Self {
551 Default::default()
552 }
553
554 pub fn is_empty(&self) -> bool {
556 self.context.is_empty() && self.rank.is_none() && self.result.is_none()
557 }
558 }
559
560 #[derive(Clone, Debug, Default, Deserialize, Serialize)]
562 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
563 pub struct UserProfile {
564 #[serde(skip_serializing_if = "Option::is_none")]
569 #[cfg_attr(
570 feature = "compat-empty-string-null",
571 serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none")
572 )]
573 pub avatar_url: Option<OwnedMxcUri>,
574
575 #[serde(skip_serializing_if = "Option::is_none")]
577 pub displayname: Option<String>,
578 }
579
580 impl UserProfile {
581 pub fn new() -> Self {
583 Default::default()
584 }
585
586 pub fn is_empty(&self) -> bool {
588 self.avatar_url.is_none() && self.displayname.is_none()
589 }
590 }
591}
592
593#[cfg(all(test, feature = "client", feature = "server"))]
594mod tests {
595 use std::{borrow::Cow, collections::BTreeMap};
596
597 use assert_matches2::assert_matches;
598 use js_int::uint;
599 use ruma_common::{
600 api::{
601 IncomingRequest, IncomingResponse, OutgoingRequest, OutgoingResponse,
602 SupportedVersions, auth_scheme::SendAccessToken,
603 },
604 event_id, room_id,
605 };
606 use serde_json::{
607 Value as JsonValue, from_slice as from_json_slice, json, to_vec as to_json_vec,
608 };
609
610 use super::v3::{GroupingKey, OrderBy, Request, Response, ResultGroupMap, SearchKeys};
611
612 #[test]
613 fn request_roundtrip() {
614 let body = json!({
615 "search_categories": {
616 "room_events": {
617 "groupings": {
618 "group_by": [
619 { "key": "room_id" },
620 ],
621 },
622 "keys": ["content.body"],
623 "order_by": "recent",
624 "search_term": "martians and men"
625 }
626 }
627 });
628
629 let http_request = http::Request::post("http://localhost/_matrix/client/v3/search")
630 .body(to_json_vec(&body).unwrap())
631 .unwrap();
632 let request = Request::try_from_http_request(http_request, &[] as &[&str]).unwrap();
633
634 let criteria = request.search_categories.room_events.as_ref().unwrap();
635 assert_eq!(criteria.groupings.group_by.len(), 1);
636 assert_eq!(criteria.groupings.group_by[0].key, Some(GroupingKey::RoomId));
637 let keys = criteria.keys.as_ref().unwrap();
638 assert_eq!(keys.len(), 1);
639 assert_eq!(keys[0], SearchKeys::ContentBody);
640 assert_eq!(criteria.order_by, Some(OrderBy::Recent));
641 assert_eq!(criteria.search_term, "martians and men");
642
643 let http_request = request
644 .try_into_http_request::<Vec<u8>>(
645 "http://localhost",
646 SendAccessToken::IfRequired("access_token"),
647 Cow::Owned(SupportedVersions::from_parts(&["v1.4".to_owned()], &BTreeMap::new())),
648 )
649 .unwrap();
650 assert_eq!(from_json_slice::<JsonValue>(http_request.body()).unwrap(), body);
651 }
652
653 #[test]
654 fn response_roundtrip() {
655 let body = json!({
656 "search_categories": {
657 "room_events": {
658 "count": 1224,
659 "groups": {
660 "room_id": {
661 "!qPewotXpIctQySfjSy:localhost": {
662 "next_batch": "BdgFsdfHSf-dsFD",
663 "order": 1,
664 "results": ["$144429830826TWwbB:localhost"],
665 },
666 },
667 },
668 "highlights": [
669 "martians",
670 "men",
671 ],
672 "next_batch": "5FdgFsd234dfgsdfFD",
673 "results": [
674 {
675 "rank": 0.004_248_66,
676 "result": {
677 "content": {
678 "body": "This is an example text message",
679 "format": "org.matrix.custom.html",
680 "formatted_body": "<b>This is an example text message</b>",
681 "msgtype": "m.text",
682 },
683 "event_id": "$144429830826TWwbB:localhost",
684 "origin_server_ts": 1_735_824_653,
685 "room_id": "!qPewotXpIctQySfjSy:localhost",
686 "sender": "@example:example.org",
687 "type": "m.room.message",
688 "unsigned": {
689 "age": 1234,
690 "membership": "join",
691 }
692 }
693 }
694 ]
695 }
696 }
697 });
698 let result_event_id = event_id!("$144429830826TWwbB:localhost");
699
700 let http_request = http::Response::new(to_json_vec(&body).unwrap());
701 let response = Response::try_from_http_response(http_request).unwrap();
702
703 let results = &response.search_categories.room_events;
704 assert_eq!(results.count, Some(uint!(1224)));
705 assert_eq!(results.groups.len(), 1);
706 assert_matches!(
707 results.groups.get(&GroupingKey::RoomId),
708 Some(ResultGroupMap::RoomId(room_id_group_map))
709 );
710 assert_eq!(room_id_group_map.len(), 1);
711 let room_id_group =
712 room_id_group_map.get(room_id!("!qPewotXpIctQySfjSy:localhost")).unwrap();
713 assert_eq!(room_id_group.results, &[result_event_id]);
714 assert_eq!(results.highlights, &["martians", "men"]);
715 assert_eq!(results.next_batch.as_deref(), Some("5FdgFsd234dfgsdfFD"));
716 assert_eq!(results.results.len(), 1);
717 assert_eq!(results.results[0].rank, Some(0.004_248_66));
718 let result = results.results[0].result.as_ref().unwrap().deserialize().unwrap();
719 assert_eq!(result.event_id(), result_event_id);
720
721 let http_response = response.try_into_http_response::<Vec<u8>>().unwrap();
722 assert_eq!(from_json_slice::<JsonValue>(http_response.body()).unwrap(), body);
723 }
724}