1use crate::context::XmtpSharedContext;
2use crate::groups::MlsGroup;
3use crate::messages::decoded_message::DecodedMessage;
4use crate::messages::enrichment::{EnrichMessageError, enrich_messages};
5use xmtp_db::DbQuery;
6use xmtp_db::group_message::{ContentType as DbContentType, MsgQueryArgs};
7use xmtp_db::prelude::QueryGroupMessage;
8
9impl<Context> MlsGroup<Context>
10where
11 Context: XmtpSharedContext,
12{
13 #[xmtp_common::mls_span]
14 pub fn find_messages_v2(
15 &self,
16 query: &MsgQueryArgs,
17 ) -> Result<Vec<DecodedMessage>, EnrichMessageError> {
18 let conn = self.context.db();
19 self.find_messages_v2_with_conn(query, conn)
20 }
21
22 #[xmtp_common::mls_span]
23 pub fn find_messages_v2_with_conn<C>(
24 &self,
25 query: &MsgQueryArgs,
26 conn: C,
27 ) -> Result<Vec<DecodedMessage>, EnrichMessageError>
28 where
29 C: QueryGroupMessage + DbQuery,
30 {
31 let initial_messages = conn.get_group_messages(
32 &self.group_id,
33 &filter_out_hidden_message_types_from_query(query),
34 )?;
35
36 enrich_messages(conn, &self.group_id, initial_messages)
37 }
38}
39
40fn filter_out_hidden_message_types_from_query(query: &MsgQueryArgs) -> MsgQueryArgs {
41 let mut new_query = query.clone();
42 let hidden_message_types = vec![
43 DbContentType::Reaction,
44 DbContentType::ReadReceipt,
45 DbContentType::DeleteMessage,
46 ];
47
48 let excluded_content_types = match &query.exclude_content_types {
49 Some(excluded) => {
50 let mut content_types = excluded.clone();
51 content_types.extend(
52 hidden_message_types
53 .into_iter()
54 .filter(|t| !excluded.contains(t)),
55 );
56 content_types
57 }
58 None => hidden_message_types,
59 };
60 new_query.exclude_content_types = Some(excluded_content_types);
61 new_query
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67 use crate::builder::ClientBuilder;
68 use crate::groups::QueryableContentFields;
69 use crate::messages::decoded_message::MessageBody;
70 use hex::ToHexExt;
71 use xmtp_common::time::now_ns;
72 use xmtp_content_types::ContentCodec;
73 use xmtp_content_types::test_utils::TestContentGenerator;
74 use xmtp_content_types::text::TextCodec;
75 use xmtp_cryptography::utils::generate_local_wallet;
76 use xmtp_db::Store;
77 use xmtp_db::group_message::{
78 ContentType as DbContentType, DeliveryStatus, GroupMessageKind, StoredGroupMessage,
79 };
80 use xmtp_proto::types::GroupId;
81 use xmtp_proto::xmtp::mls::message_contents::content_types::ReactionAction;
82 use xmtp_proto::xmtp::mls::message_contents::{ContentTypeId, EncodedContent};
83
84 async fn setup_test_group() -> (MlsGroup<impl XmtpSharedContext>, impl XmtpSharedContext) {
85 let client = ClientBuilder::new_test_client(&generate_local_wallet()).await;
86 let group = client.create_group(None, Default::default()).unwrap();
87
88 (group, client.context.clone())
89 }
90
91 fn create_test_message(
92 group_id: &GroupId,
93 message_id: Vec<u8>,
94 encoded_content: EncodedContent,
95 sent_at_ns: i64,
96 sender_inbox_id: String,
97 ) -> StoredGroupMessage {
98 let content_bytes = xmtp_content_types::encoded_content_to_bytes(encoded_content.clone());
99 let queryable_fields =
100 QueryableContentFields::try_from(encoded_content).unwrap_or_default();
101
102 StoredGroupMessage {
103 id: message_id,
104 group_id: *group_id,
105 decrypted_message_bytes: content_bytes,
106 sent_at_ns,
107 kind: GroupMessageKind::Application,
108 sender_installation_id: vec![1, 2, 3],
109 sender_inbox_id,
110 delivery_status: DeliveryStatus::Published,
111 content_type: queryable_fields.content_type,
112 version_major: queryable_fields.version_major,
113 version_minor: queryable_fields.version_minor,
114 authority_id: queryable_fields.authority_id,
115 reference_id: queryable_fields.reference_id,
116 sequence_id: 0,
117 envelope_hash: None,
118 expiry_ns: None,
119 expire_at_ns: None,
120 inserted_at_ns: 0,
121 should_push: true,
122 idempotency_key: sent_at_ns.to_string(),
123 }
124 }
125
126 fn create_test_message_raw(
128 group_id: &GroupId,
129 message_id: Vec<u8>,
130 content: Vec<u8>,
131 sent_at_ns: i64,
132 sender_inbox_id: String,
133 content_type_id: Option<ContentTypeId>,
134 reference_id: Option<Vec<u8>>,
135 ) -> StoredGroupMessage {
136 let queryable_fields = content_type_id
137 .as_ref()
138 .map(|ct| {
139 (
140 DbContentType::from(ct.type_id.clone()),
141 ct.version_major as i32,
142 ct.version_minor as i32,
143 ct.authority_id.clone(),
144 )
145 })
146 .unwrap_or((DbContentType::Text, 1, 0, "xmtp.org".to_string()));
147
148 StoredGroupMessage {
149 id: message_id,
150 group_id: *group_id,
151 decrypted_message_bytes: content,
152 sent_at_ns,
153 kind: GroupMessageKind::Application,
154 sender_installation_id: vec![1, 2, 3],
155 sender_inbox_id,
156 delivery_status: DeliveryStatus::Published,
157 content_type: queryable_fields.0,
158 version_major: queryable_fields.1,
159 version_minor: queryable_fields.2,
160 authority_id: queryable_fields.3,
161 reference_id,
162 sequence_id: 0,
163 envelope_hash: None,
164 expiry_ns: None,
165 expire_at_ns: None,
166 inserted_at_ns: 0,
167 should_push: true,
168 idempotency_key: sent_at_ns.to_string(),
169 }
170 }
171
172 fn assert_message_count(messages: &[DecodedMessage], expected: usize) {
175 assert_eq!(
176 messages.len(),
177 expected,
178 "Expected {} messages, got {}",
179 expected,
180 messages.len()
181 );
182 }
183
184 fn assert_text_content(message: &DecodedMessage, expected: &str) {
185 match &message.content {
186 MessageBody::Text(text) => assert_eq!(text.content, expected),
187 _ => panic!("Expected text message, got {:?}", message.content),
188 }
189 }
190
191 fn assert_reaction_count(message: &DecodedMessage, expected: usize) {
192 assert_eq!(
193 message.reactions.len(),
194 expected,
195 "Expected {} reactions, got {}",
196 expected,
197 message.reactions.len()
198 );
199 }
200
201 fn assert_has_reaction(message: &DecodedMessage, emoji: &str) {
202 assert!(
203 message.reactions.iter().any(|r| {
204 if let MessageBody::Reaction(reaction) = &r.content {
205 reaction.content == emoji
206 } else {
207 false
208 }
209 }),
210 "Expected reaction '{}' not found",
211 emoji
212 );
213 }
214
215 fn assert_reply_references(reply: &DecodedMessage, expected_ref_id: &[u8]) {
216 match &reply.content {
217 MessageBody::Reply(reply_body) => {
218 assert!(
219 reply_body.in_reply_to.is_some(),
220 "Reply should have in_reply_to populated"
221 );
222 let referenced = reply_body.in_reply_to.as_ref().unwrap();
223 assert_eq!(referenced.metadata.id, expected_ref_id);
224 }
225 _ => panic!("Expected reply message, got {:?}", reply.content),
226 }
227 }
228
229 fn assert_reply_has_no_reference(reply: &DecodedMessage) {
230 match &reply.content {
231 MessageBody::Reply(reply_body) => {
232 assert!(
233 reply_body.in_reply_to.is_none(),
234 "Reply should not have in_reply_to populated"
235 );
236 }
237 _ => panic!("Expected reply message, got {:?}", reply.content),
238 }
239 }
240
241 fn find_message_by_id<'a>(messages: &'a [DecodedMessage], id: &[u8]) -> &'a DecodedMessage {
242 messages
243 .iter()
244 .find(|m| m.metadata.id == id)
245 .unwrap_or_else(|| panic!("Message with id {:?} not found", id))
246 }
247
248 fn create_and_store_message<S>(
249 conn: &S,
250 group_id: &GroupId,
251 message_id: Vec<u8>,
252 content: EncodedContent,
253 timestamp_offset: i64,
254 sender: &str,
255 ) -> Vec<u8>
256 where
257 StoredGroupMessage: Store<S>,
258 {
259 let msg = create_test_message(
260 group_id,
261 message_id.clone(),
262 content,
263 now_ns() + timestamp_offset,
264 sender.to_string(),
265 );
266 msg.store(conn).unwrap();
267 message_id
268 }
269
270 #[xmtp_common::test]
273 async fn test_exclude_content_types_with_custom_exclusions() {
274 let (group, context) = setup_test_group().await;
275 let conn = context.db();
276
277 create_and_store_message(
279 &conn,
280 &group.group_id,
281 vec![1],
282 TestContentGenerator::text_content("Hello World"),
283 0,
284 "sender1",
285 );
286
287 create_and_store_message(
289 &conn,
290 &group.group_id,
291 vec![2],
292 TestContentGenerator::group_updated_content(vec!["inbox1".to_string()]),
293 1000,
294 "sender2",
295 );
296
297 let messages = group.find_messages_v2(&MsgQueryArgs::default()).unwrap();
299 assert_message_count(&messages, 2);
300
301 let query = MsgQueryArgs {
303 exclude_content_types: Some(vec![DbContentType::Text]),
304 ..Default::default()
305 };
306 let messages = group.find_messages_v2(&query).unwrap();
307
308 assert_message_count(&messages, 1);
309 if let MessageBody::GroupUpdated(_) = &messages[0].content {
310 } else {
312 panic!(
313 "Expected GroupUpdated message, got {:?}",
314 messages[0].content
315 );
316 }
317 }
318
319 #[xmtp_common::test]
320 async fn test_find_messages_no_reactions_or_replies() {
321 let (group, context) = setup_test_group().await;
322 let conn = context.db();
323
324 create_and_store_message(
326 &conn,
327 &group.group_id,
328 vec![1],
329 TestContentGenerator::text_content("Hello World"),
330 0,
331 "sender1",
332 );
333
334 create_and_store_message(
335 &conn,
336 &group.group_id,
337 vec![2],
338 TestContentGenerator::text_content("Another message"),
339 1000,
340 "sender2",
341 );
342
343 let messages = group.find_messages_v2(&MsgQueryArgs::default()).unwrap();
345 assert_message_count(&messages, 2);
346 assert_text_content(&messages[0], "Hello World");
347 assert_text_content(&messages[1], "Another message");
348 assert_reaction_count(&messages[0], 0);
349 assert_reaction_count(&messages[1], 0);
350 }
351
352 #[xmtp_common::test]
353 async fn test_find_messages_with_reactions() {
354 let (group, context) = setup_test_group().await;
355 let conn = context.db();
356
357 let msg_id = vec![1, 2, 3];
359 let msg_id_hex = msg_id.encode_hex();
360 create_and_store_message(
361 &conn,
362 &group.group_id,
363 msg_id.clone(),
364 TestContentGenerator::text_content("Hello World"),
365 0,
366 "sender1",
367 );
368
369 create_and_store_message(
371 &conn,
372 &group.group_id,
373 vec![4],
374 TestContentGenerator::reaction_content(&msg_id_hex, "👍", ReactionAction::Added),
375 1000,
376 "reactor1",
377 );
378
379 create_and_store_message(
380 &conn,
381 &group.group_id,
382 vec![5],
383 TestContentGenerator::reaction_content(&msg_id_hex, "❤️", ReactionAction::Added),
384 2000,
385 "reactor2",
386 );
387
388 let messages = group.find_messages_v2(&MsgQueryArgs::default()).unwrap();
390
391 assert_message_count(&messages, 1);
393
394 let original_msg = find_message_by_id(&messages, &msg_id);
396 assert_reaction_count(original_msg, 2);
397 assert_has_reaction(original_msg, "👍");
398 assert_has_reaction(original_msg, "❤️");
399 }
400
401 #[xmtp_common::test]
402 async fn test_find_messages_with_replies() {
403 let (group, context) = setup_test_group().await;
404 let conn = context.db();
405
406 let msg_id = vec![1, 2, 3];
408 let msg_id_hex = msg_id.encode_hex();
409 create_and_store_message(
410 &conn,
411 &group.group_id,
412 msg_id.clone(),
413 TestContentGenerator::text_content("Original message"),
414 0,
415 "sender1",
416 );
417
418 let reply_id = create_and_store_message(
420 &conn,
421 &group.group_id,
422 vec![4, 5, 6],
423 TestContentGenerator::reply_content(
424 &msg_id_hex,
425 TextCodec::content_type(),
426 b"This is a reply".to_vec(),
427 ),
428 1000,
429 "replier1",
430 );
431
432 let messages = group.find_messages_v2(&MsgQueryArgs::default()).unwrap();
434
435 assert_message_count(&messages, 2);
436
437 let reply_msg = find_message_by_id(&messages, &reply_id);
439 assert_reply_references(reply_msg, &msg_id);
440
441 if let MessageBody::Reply(reply) = &reply_msg.content {
443 let referenced_msg = reply.in_reply_to.as_ref().unwrap();
444 assert_text_content(referenced_msg, "Original message");
445 }
446 }
447
448 #[xmtp_common::test]
449 async fn test_find_messages_invalid_reply_reference() {
450 let (group, context) = setup_test_group().await;
451 let conn = context.db();
452
453 create_and_store_message(
455 &conn,
456 &group.group_id,
457 vec![1],
458 TestContentGenerator::reply_content(
459 "not-valid-hex!@#",
460 TextCodec::content_type(),
461 b"This is a reply".to_vec(),
462 ),
463 0,
464 "replier1",
465 );
466
467 let messages = group.find_messages_v2(&MsgQueryArgs::default()).unwrap();
469 assert_message_count(&messages, 1);
470
471 assert_reply_has_no_reference(&messages[0]);
472 if let MessageBody::Reply(reply) = &messages[0].content {
473 assert_eq!(reply.reference_id, "not-valid-hex!@#");
474 }
475 }
476
477 #[xmtp_common::test]
478 async fn test_find_messages_missing_reply_reference() {
479 let (group, context) = setup_test_group().await;
480 let conn = context.db();
481
482 let non_existent_id = vec![99, 99, 99];
484 create_and_store_message(
485 &conn,
486 &group.group_id,
487 vec![1],
488 TestContentGenerator::reply_content(
489 &non_existent_id.encode_hex(),
490 TextCodec::content_type(),
491 b"This is a reply".to_vec(),
492 ),
493 0,
494 "replier1",
495 );
496
497 let messages = group.find_messages_v2(&MsgQueryArgs::default()).unwrap();
499 assert_message_count(&messages, 1);
500 assert_reply_has_no_reference(&messages[0]);
501
502 if let MessageBody::Reply(reply) = &messages[0].content {
503 assert_eq!(reply.reference_id, non_existent_id.encode_hex());
504 }
505 }
506
507 #[xmtp_common::test]
508 async fn test_find_messages_undecodable_messages() {
509 let (group, context) = setup_test_group().await;
510 let conn = context.db();
511
512 create_and_store_message(
514 &conn,
515 &group.group_id,
516 vec![1],
517 TestContentGenerator::text_content("Valid message"),
518 0,
519 "sender1",
520 );
521
522 create_and_store_message(
524 &conn,
525 &group.group_id,
526 vec![2],
527 TestContentGenerator::malformed_content_with_type(TextCodec::content_type()),
528 1000,
529 "sender2",
530 );
531
532 create_and_store_message(
534 &conn,
535 &group.group_id,
536 vec![3],
537 TestContentGenerator::invalid_content(),
538 2000,
539 "sender3",
540 );
541
542 let messages = group.find_messages_v2(&MsgQueryArgs::default()).unwrap();
544
545 assert_message_count(&messages, 3);
547
548 assert_text_content(&messages[0], "Valid message");
550
551 assert_text_content(&messages[1], "malformed content for a known type");
553
554 if let MessageBody::Custom(content) = &messages[2].content {
555 assert_eq!(content.fallback, Some("Invalid message".to_string()));
556 } else {
557 panic!("Expected custom content for unknown type message");
558 }
559 }
560
561 #[xmtp_common::test]
562 async fn test_find_messages_invalid_reactions() {
563 let (group, context) = setup_test_group().await;
564 let conn = context.db();
565
566 let msg_id = vec![1, 2, 3];
568 let msg_id_hex = msg_id.encode_hex();
569 create_and_store_message(
570 &conn,
571 &group.group_id,
572 msg_id.clone(),
573 TestContentGenerator::text_content("Hello World"),
574 0,
575 "sender1",
576 );
577
578 create_and_store_message(
580 &conn,
581 &group.group_id,
582 vec![4],
583 TestContentGenerator::reaction_content(&msg_id_hex, "👍", ReactionAction::Added),
584 1000,
585 "reactor1",
586 );
587
588 let reaction_type = xmtp_content_types::reaction::ReactionCodec::content_type();
590 let invalid_reaction = create_test_message_raw(
591 &group.group_id,
592 vec![5],
593 b"invalid reaction bytes".to_vec(),
594 now_ns() + 2000,
595 "reactor2".to_string(),
596 Some(reaction_type),
597 Some(msg_id.clone()),
598 );
599 invalid_reaction.store(&conn).unwrap();
600
601 let messages = group.find_messages_v2(&MsgQueryArgs::default()).unwrap();
603
604 assert_message_count(&messages, 1);
606
607 let original_msg = find_message_by_id(&messages, &msg_id);
609
610 assert_reaction_count(original_msg, 1);
612 assert_has_reaction(original_msg, "👍");
613 }
614
615 #[xmtp_common::test]
616 async fn test_hidden_message_types_are_filtered() {
617 let (group, context) = setup_test_group().await;
618 let conn = context.db();
619
620 create_and_store_message(
622 &conn,
623 &group.group_id,
624 vec![1],
625 TestContentGenerator::text_content("Hello World"),
626 0,
627 "sender1",
628 );
629
630 let msg_id_hex = vec![1].encode_hex();
632 create_and_store_message(
633 &conn,
634 &group.group_id,
635 vec![2],
636 TestContentGenerator::reaction_content(&msg_id_hex, "👍", ReactionAction::Added),
637 1000,
638 "reactor1",
639 );
640
641 create_and_store_message(
643 &conn,
644 &group.group_id,
645 vec![3],
646 TestContentGenerator::read_receipt_content(),
647 2000,
648 "reader1",
649 );
650
651 create_and_store_message(
653 &conn,
654 &group.group_id,
655 vec![4],
656 TestContentGenerator::delete_message_content(&msg_id_hex),
657 3000,
658 "deleter1",
659 );
660
661 create_and_store_message(
663 &conn,
664 &group.group_id,
665 vec![5],
666 TestContentGenerator::text_content("Second message"),
667 4000,
668 "sender2",
669 );
670
671 let messages = group.find_messages_v2(&MsgQueryArgs::default()).unwrap();
674
675 assert_message_count(&messages, 2);
678 assert_text_content(&messages[0], "Hello World");
679 assert_text_content(&messages[1], "Second message");
680
681 assert_reaction_count(&messages[0], 1);
683 assert_has_reaction(&messages[0], "👍");
684 }
685
686 #[xmtp_common::test]
687 async fn test_find_messages_chain_of_replies() {
688 let (group, context) = setup_test_group().await;
689 let conn = context.db();
690
691 let msg1_id = vec![1];
693 let msg1_id_hex = msg1_id.encode_hex();
694 create_and_store_message(
695 &conn,
696 &group.group_id,
697 msg1_id.clone(),
698 TestContentGenerator::text_content("Original message"),
699 0,
700 "sender1",
701 );
702
703 let msg2_id = vec![2];
705 let msg2_id_hex = msg2_id.encode_hex();
706 create_and_store_message(
707 &conn,
708 &group.group_id,
709 msg2_id.clone(),
710 TestContentGenerator::reply_content(
711 &msg1_id_hex,
712 TextCodec::content_type(),
713 b"First reply".to_vec(),
714 ),
715 1000,
716 "replier1",
717 );
718
719 let msg3_id = vec![3];
721 create_and_store_message(
722 &conn,
723 &group.group_id,
724 msg3_id.clone(),
725 TestContentGenerator::reply_content(
726 &msg2_id_hex,
727 TextCodec::content_type(),
728 b"Second reply - reply to reply".to_vec(),
729 ),
730 2000,
731 "replier2",
732 );
733
734 let messages = group.find_messages_v2(&MsgQueryArgs::default()).unwrap();
736
737 assert_message_count(&messages, 3);
738
739 let first_reply = find_message_by_id(&messages, &msg2_id);
741 if let MessageBody::Reply(reply) = &first_reply.content {
742 assert!(reply.in_reply_to.is_some());
744 let referenced = reply.in_reply_to.as_ref().unwrap();
745 assert_eq!(referenced.metadata.id, msg1_id);
746
747 if let MessageBody::Text(_) = &referenced.content {
749 } else {
751 panic!("Expected text in first layer reference");
752 }
753 } else {
754 panic!("Expected reply message");
755 }
756
757 let second_reply = find_message_by_id(&messages, &msg3_id);
759 if let MessageBody::Reply(reply) = &second_reply.content {
760 assert!(reply.in_reply_to.is_some());
762 let referenced = reply.in_reply_to.as_ref().unwrap();
763 assert_eq!(referenced.metadata.id, msg2_id);
764
765 if let MessageBody::Reply(inner_reply) = &referenced.content {
767 assert!(
769 inner_reply.in_reply_to.is_none(),
770 "Should not recursively resolve replies - got {:?}",
771 inner_reply.in_reply_to
772 );
773 } else {
774 panic!("Expected reply in second layer reference");
775 }
776 } else {
777 panic!("Expected reply message");
778 }
779 }
780
781 #[xmtp_common::test]
782 async fn test_reply_with_custom_inner_content() {
783 let (group, context) = setup_test_group().await;
784 let conn = context.db();
785
786 let msg_id = vec![1, 2, 3];
788 let msg_id_hex = msg_id.encode_hex();
789 create_and_store_message(
790 &conn,
791 &group.group_id,
792 msg_id.clone(),
793 TestContentGenerator::text_content("Original message"),
794 0,
795 "sender1",
796 );
797
798 let custom_content_type = ContentTypeId {
800 authority_id: "custom.org".to_string(),
801 type_id: "custom/payload".to_string(),
802 version_major: 1,
803 version_minor: 0,
804 };
805
806 let reply_id = create_and_store_message(
808 &conn,
809 &group.group_id,
810 vec![4, 5, 6],
811 TestContentGenerator::reply_content(
812 &msg_id_hex,
813 custom_content_type,
814 b"custom payload data".to_vec(),
815 ),
816 1000,
817 "replier1",
818 );
819
820 let messages = group.find_messages_v2(&MsgQueryArgs::default()).unwrap();
822 assert_message_count(&messages, 2);
823
824 let reply_msg = find_message_by_id(&messages, &reply_id);
826
827 if let MessageBody::Reply(reply) = &reply_msg.content {
829 assert!(reply.in_reply_to.is_some());
831 let referenced = reply.in_reply_to.as_ref().unwrap();
832 assert_eq!(referenced.metadata.id, msg_id);
833
834 if let MessageBody::Custom(custom) = reply.content.as_ref() {
836 assert_eq!(custom.r#type.as_ref().unwrap().type_id, "custom/payload");
837 assert_eq!(custom.content, b"custom payload data");
838 } else {
839 panic!(
840 "Expected Custom inner content in Reply, got {:?}",
841 reply.content
842 );
843 }
844 } else {
845 panic!("Expected Reply message, got {:?}", reply_msg.content);
846 }
847 }
848}