1use crate::context::XmtpSharedContext;
2use crate::groups::MlsGroup;
3use crate::messages::decoded_message::DecodedMessage;
4use crate::messages::enrichment::{
5 EnrichMessageError, EnrichedStoredMessage, enrich_messages, enrich_messages_with_stored,
6};
7use xmtp_db::DbQuery;
8use xmtp_db::group_message::{ContentType as DbContentType, MsgQueryArgs};
9use xmtp_db::prelude::QueryGroupMessage;
10
11mod page;
12pub use page::{EnrichedHistoryPage, EnrichedRecoveryPage};
13
14impl<Context> MlsGroup<Context>
15where
16 Context: XmtpSharedContext,
17{
18 #[xmtp_common::mls_span]
19 pub fn find_messages_v2(
20 &self,
21 query: &MsgQueryArgs,
22 ) -> Result<Vec<DecodedMessage>, EnrichMessageError> {
23 let conn = self.context.db();
24 self.find_messages_v2_with_conn(query, conn)
25 }
26
27 #[xmtp_common::mls_span]
28 pub fn find_messages_v2_with_stored(
29 &self,
30 query: &MsgQueryArgs,
31 ) -> Result<Vec<EnrichedStoredMessage>, EnrichMessageError> {
32 let conn = self.context.db();
33 use xmtp_db::delivery::QueryDelivery;
34 let rows = conn
35 .app_visible_message_rows(
36 &self.group_id,
37 &filter_out_hidden_message_types_from_query(query),
38 )
39 .map_err(EnrichMessageError::Storage)?;
40 let cursors: std::collections::HashMap<_, _> = rows
41 .iter()
42 .map(|row| (row.stored.id.clone(), row.cursor))
43 .collect();
44 let mut enriched = enrich_messages_with_stored(
45 conn,
46 &self.group_id,
47 rows.into_iter().map(|row| row.stored).collect(),
48 )?;
49 for message in &mut enriched {
50 message.delivery_cursor = cursors.get(&message.stored.id).copied().flatten();
51 }
52 Ok(enriched)
53 }
54
55 #[xmtp_common::mls_span]
56 pub fn find_messages_v2_with_conn<C>(
57 &self,
58 query: &MsgQueryArgs,
59 conn: C,
60 ) -> Result<Vec<DecodedMessage>, EnrichMessageError>
61 where
62 C: QueryGroupMessage + DbQuery,
63 {
64 let initial_messages = conn.get_group_messages(
65 &self.group_id,
66 &filter_out_hidden_message_types_from_query(query),
67 )?;
68
69 enrich_messages(conn, &self.group_id, initial_messages)
70 }
71}
72
73fn filter_out_hidden_message_types_from_query(query: &MsgQueryArgs) -> MsgQueryArgs {
74 let mut new_query = query.clone();
75 let hidden_message_types = vec![
76 DbContentType::Reaction,
77 DbContentType::ReadReceipt,
78 DbContentType::DeleteMessage,
79 ];
80
81 let excluded_content_types = match &query.exclude_content_types {
82 Some(excluded) => {
83 let mut content_types = excluded.clone();
84 content_types.extend(
85 hidden_message_types
86 .into_iter()
87 .filter(|t| !excluded.contains(t)),
88 );
89 content_types
90 }
91 None => hidden_message_types,
92 };
93 new_query.exclude_content_types = Some(excluded_content_types);
94 new_query
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use crate::builder::ClientBuilder;
101 use crate::groups::QueryableContentFields;
102 use crate::messages::decoded_message::{ContentDecodeFailureKind, MessageBody};
103 use hex::ToHexExt;
104 use xmtp_common::time::now_ns;
105 use xmtp_content_types::ContentCodec;
106 use xmtp_content_types::test_utils::TestContentGenerator;
107 use xmtp_content_types::text::TextCodec;
108 use xmtp_cryptography::utils::generate_local_wallet;
109 use xmtp_db::Store;
110 use xmtp_db::group_message::{
111 ContentType as DbContentType, DeliveryStatus, GroupMessageKind, StoredGroupMessage,
112 };
113 use xmtp_proto::types::GroupId;
114 use xmtp_proto::xmtp::mls::message_contents::content_types::ReactionAction;
115 use xmtp_proto::xmtp::mls::message_contents::{ContentTypeId, EncodedContent};
116
117 async fn setup_test_group() -> (MlsGroup<impl XmtpSharedContext>, impl XmtpSharedContext) {
118 let client = ClientBuilder::new_test_client(&generate_local_wallet()).await;
119 let group = client.create_group(None, Default::default()).unwrap();
120
121 (group, client.context.clone())
122 }
123
124 fn create_test_message(
125 group_id: &GroupId,
126 message_id: Vec<u8>,
127 encoded_content: EncodedContent,
128 sent_at_ns: i64,
129 sender_inbox_id: String,
130 ) -> StoredGroupMessage {
131 let content_bytes = xmtp_content_types::encoded_content_to_bytes(encoded_content.clone());
132 let queryable_fields =
133 QueryableContentFields::try_from(encoded_content).unwrap_or_default();
134
135 StoredGroupMessage {
136 id: message_id,
137 group_id: *group_id,
138 decrypted_message_bytes: content_bytes,
139 sent_at_ns,
140 kind: GroupMessageKind::Application,
141 sender_installation_id: vec![1, 2, 3],
142 sender_inbox_id,
143 delivery_status: DeliveryStatus::Published,
144 content_type: queryable_fields.content_type,
145 version_major: queryable_fields.version_major,
146 version_minor: queryable_fields.version_minor,
147 authority_id: queryable_fields.authority_id,
148 reference_id: queryable_fields.reference_id,
149 sequence_id: 0,
150 envelope_hash: None,
151 expiry_ns: None,
152 expire_at_ns: None,
153 inserted_at_ns: 0,
154 should_push: true,
155 idempotency_key: sent_at_ns.to_string(),
156 }
157 }
158
159 fn create_test_message_raw(
161 group_id: &GroupId,
162 message_id: Vec<u8>,
163 content: Vec<u8>,
164 sent_at_ns: i64,
165 sender_inbox_id: String,
166 content_type_id: Option<ContentTypeId>,
167 reference_id: Option<Vec<u8>>,
168 ) -> StoredGroupMessage {
169 let queryable_fields = content_type_id
170 .as_ref()
171 .map(|ct| {
172 (
173 DbContentType::from_identifier(&ct.authority_id, &ct.type_id, ct.version_major),
174 ct.version_major as i32,
175 ct.version_minor as i32,
176 ct.authority_id.clone(),
177 )
178 })
179 .unwrap_or((DbContentType::Text, 1, 0, "xmtp.org".to_string()));
180
181 StoredGroupMessage {
182 id: message_id,
183 group_id: *group_id,
184 decrypted_message_bytes: content,
185 sent_at_ns,
186 kind: GroupMessageKind::Application,
187 sender_installation_id: vec![1, 2, 3],
188 sender_inbox_id,
189 delivery_status: DeliveryStatus::Published,
190 content_type: queryable_fields.0,
191 version_major: queryable_fields.1,
192 version_minor: queryable_fields.2,
193 authority_id: queryable_fields.3,
194 reference_id,
195 sequence_id: 0,
196 envelope_hash: None,
197 expiry_ns: None,
198 expire_at_ns: None,
199 inserted_at_ns: 0,
200 should_push: true,
201 idempotency_key: sent_at_ns.to_string(),
202 }
203 }
204
205 fn assert_message_count(messages: &[DecodedMessage], expected: usize) {
208 assert_eq!(
209 messages.len(),
210 expected,
211 "Expected {} messages, got {}",
212 expected,
213 messages.len()
214 );
215 }
216
217 fn assert_text_content(message: &DecodedMessage, expected: &str) {
218 match &message.content {
219 MessageBody::Text(text) => assert_eq!(text.content, expected),
220 _ => panic!("Expected text message, got {:?}", message.content),
221 }
222 }
223
224 fn assert_reaction_count(message: &DecodedMessage, expected: usize) {
225 assert_eq!(
226 message.reactions.len(),
227 expected,
228 "Expected {} reactions, got {}",
229 expected,
230 message.reactions.len()
231 );
232 }
233
234 fn assert_has_reaction(message: &DecodedMessage, emoji: &str) {
235 assert!(
236 message.reactions.iter().any(|r| {
237 if let MessageBody::Reaction(reaction) = &r.content {
238 reaction.content == emoji
239 } else {
240 false
241 }
242 }),
243 "Expected reaction '{}' not found",
244 emoji
245 );
246 }
247
248 fn assert_reply_references(reply: &DecodedMessage, expected_ref_id: &[u8]) {
249 match &reply.content {
250 MessageBody::Reply(reply_body) => {
251 assert!(
252 reply_body.in_reply_to.is_some(),
253 "Reply should have in_reply_to populated"
254 );
255 let referenced = reply_body.in_reply_to.as_ref().unwrap();
256 assert_eq!(referenced.metadata.id, expected_ref_id);
257 }
258 _ => panic!("Expected reply message, got {:?}", reply.content),
259 }
260 }
261
262 fn assert_reply_has_no_reference(reply: &DecodedMessage) {
263 match &reply.content {
264 MessageBody::Reply(reply_body) => {
265 assert!(
266 reply_body.in_reply_to.is_none(),
267 "Reply should not have in_reply_to populated"
268 );
269 }
270 _ => panic!("Expected reply message, got {:?}", reply.content),
271 }
272 }
273
274 fn find_message_by_id<'a>(messages: &'a [DecodedMessage], id: &[u8]) -> &'a DecodedMessage {
275 messages
276 .iter()
277 .find(|m| m.metadata.id == id)
278 .unwrap_or_else(|| panic!("Message with id {:?} not found", id))
279 }
280
281 fn create_and_store_message<S>(
282 conn: &S,
283 group_id: &GroupId,
284 message_id: Vec<u8>,
285 content: EncodedContent,
286 timestamp_offset: i64,
287 sender: &str,
288 ) -> Vec<u8>
289 where
290 StoredGroupMessage: Store<S>,
291 {
292 let msg = create_test_message(
293 group_id,
294 message_id.clone(),
295 content,
296 now_ns() + timestamp_offset,
297 sender.to_string(),
298 );
299 msg.store(conn).unwrap();
300 message_id
301 }
302
303 #[xmtp_common::test]
306 async fn test_exclude_content_types_with_custom_exclusions() {
307 let (group, context) = setup_test_group().await;
308 let conn = context.db();
309
310 create_and_store_message(
312 &conn,
313 &group.group_id,
314 vec![1],
315 TestContentGenerator::text_content("Hello World"),
316 0,
317 "sender1",
318 );
319
320 create_and_store_message(
322 &conn,
323 &group.group_id,
324 vec![2],
325 TestContentGenerator::group_updated_content(vec!["inbox1".to_string()]),
326 1000,
327 "sender2",
328 );
329
330 let messages = group.find_messages_v2(&MsgQueryArgs::default()).unwrap();
332 assert_message_count(&messages, 2);
333
334 let query = MsgQueryArgs {
336 exclude_content_types: Some(vec![DbContentType::Text]),
337 ..Default::default()
338 };
339 let messages = group.find_messages_v2(&query).unwrap();
340
341 assert_message_count(&messages, 1);
342 if let MessageBody::GroupUpdated(_) = &messages[0].content {
343 } else {
345 panic!(
346 "Expected GroupUpdated message, got {:?}",
347 messages[0].content
348 );
349 }
350 }
351
352 #[xmtp_common::test]
353 async fn test_find_messages_no_reactions_or_replies() {
354 let (group, context) = setup_test_group().await;
355 let conn = context.db();
356
357 create_and_store_message(
359 &conn,
360 &group.group_id,
361 vec![1],
362 TestContentGenerator::text_content("Hello World"),
363 0,
364 "sender1",
365 );
366
367 create_and_store_message(
368 &conn,
369 &group.group_id,
370 vec![2],
371 TestContentGenerator::text_content("Another message"),
372 1000,
373 "sender2",
374 );
375
376 let messages = group.find_messages_v2(&MsgQueryArgs::default()).unwrap();
378 assert_message_count(&messages, 2);
379 assert_text_content(&messages[0], "Hello World");
380 assert_text_content(&messages[1], "Another message");
381 assert_reaction_count(&messages[0], 0);
382 assert_reaction_count(&messages[1], 0);
383 }
384
385 #[xmtp_common::test]
386 async fn test_find_messages_v2_with_reactions() {
387 let (group, context) = setup_test_group().await;
388 let conn = context.db();
389
390 let msg_id = vec![1, 2, 3];
392 let msg_id_hex = msg_id.encode_hex();
393 create_and_store_message(
394 &conn,
395 &group.group_id,
396 msg_id.clone(),
397 TestContentGenerator::text_content("Hello World"),
398 0,
399 "sender1",
400 );
401
402 create_and_store_message(
404 &conn,
405 &group.group_id,
406 vec![4],
407 TestContentGenerator::reaction_content(&msg_id_hex, "👍", ReactionAction::Added),
408 1000,
409 "reactor1",
410 );
411
412 create_and_store_message(
413 &conn,
414 &group.group_id,
415 vec![5],
416 TestContentGenerator::reaction_content(&msg_id_hex, "❤️", ReactionAction::Added),
417 2000,
418 "reactor2",
419 );
420
421 let messages = group.find_messages_v2(&MsgQueryArgs::default()).unwrap();
423
424 assert_message_count(&messages, 1);
426
427 let original_msg = find_message_by_id(&messages, &msg_id);
429 assert_reaction_count(original_msg, 2);
430 assert_has_reaction(original_msg, "👍");
431 assert_has_reaction(original_msg, "❤️");
432 }
433
434 #[xmtp_common::test]
435 async fn test_find_messages_with_replies() {
436 let (group, context) = setup_test_group().await;
437 let conn = context.db();
438
439 let msg_id = vec![1, 2, 3];
441 let msg_id_hex = msg_id.encode_hex();
442 create_and_store_message(
443 &conn,
444 &group.group_id,
445 msg_id.clone(),
446 TestContentGenerator::text_content("Original message"),
447 0,
448 "sender1",
449 );
450
451 let reply_id = create_and_store_message(
453 &conn,
454 &group.group_id,
455 vec![4, 5, 6],
456 TestContentGenerator::reply_content(
457 &msg_id_hex,
458 TextCodec::content_type(),
459 b"This is a reply".to_vec(),
460 ),
461 1000,
462 "replier1",
463 );
464
465 let messages = group.find_messages_v2(&MsgQueryArgs::default()).unwrap();
467
468 assert_message_count(&messages, 2);
469
470 let reply_msg = find_message_by_id(&messages, &reply_id);
472 assert_reply_references(reply_msg, &msg_id);
473
474 if let MessageBody::Reply(reply) = &reply_msg.content {
476 let referenced_msg = reply.in_reply_to.as_ref().unwrap();
477 assert_text_content(referenced_msg, "Original message");
478 }
479 }
480
481 #[xmtp_common::test]
482 async fn test_find_messages_invalid_reply_reference() {
483 let (group, context) = setup_test_group().await;
484 let conn = context.db();
485
486 create_and_store_message(
488 &conn,
489 &group.group_id,
490 vec![1],
491 TestContentGenerator::reply_content(
492 "not-valid-hex!@#",
493 TextCodec::content_type(),
494 b"This is a reply".to_vec(),
495 ),
496 0,
497 "replier1",
498 );
499
500 let messages = group.find_messages_v2(&MsgQueryArgs::default()).unwrap();
503 assert_message_count(&messages, 1);
504 assert_reply_has_no_reference(&messages[0]);
505 if let MessageBody::Reply(reply) = &messages[0].content {
506 assert_eq!(reply.reference_id, "not-valid-hex!@#");
507 }
508 }
509
510 #[xmtp_common::test]
511 async fn test_find_messages_missing_reply_reference() {
512 let (group, context) = setup_test_group().await;
513 let conn = context.db();
514
515 let non_existent_id = vec![99, 99, 99];
517 create_and_store_message(
518 &conn,
519 &group.group_id,
520 vec![1],
521 TestContentGenerator::reply_content(
522 &non_existent_id.encode_hex(),
523 TextCodec::content_type(),
524 b"This is a reply".to_vec(),
525 ),
526 0,
527 "replier1",
528 );
529
530 let messages = group.find_messages_v2(&MsgQueryArgs::default()).unwrap();
532 assert_message_count(&messages, 1);
533 assert_reply_has_no_reference(&messages[0]);
534
535 if let MessageBody::Reply(reply) = &messages[0].content {
536 assert_eq!(reply.reference_id, non_existent_id.encode_hex());
537 }
538 }
539
540 #[xmtp_common::test]
542 async fn test_find_messages_undecodable_messages() {
543 let (group, context) = setup_test_group().await;
544 let conn = context.db();
545
546 create_and_store_message(
548 &conn,
549 &group.group_id,
550 vec![1],
551 TestContentGenerator::text_content("Valid message"),
552 0,
553 "sender1",
554 );
555
556 let malformed = vec![0xff, 0x00, 0x80];
559 create_test_message_raw(
560 &group.group_id,
561 vec![2],
562 malformed.clone(),
563 now_ns() + 1000,
564 "sender2".into(),
565 None,
566 None,
567 )
568 .store(&conn)
569 .unwrap();
570
571 create_and_store_message(
573 &conn,
574 &group.group_id,
575 vec![3],
576 TestContentGenerator::invalid_content(),
577 2000,
578 "sender3",
579 );
580
581 let messages = group.find_messages_v2(&MsgQueryArgs::default()).unwrap();
582 assert_message_count(&messages, 3);
583 assert_text_content(&messages[0], "Valid message");
584
585 assert_eq!(messages[1].metadata.id, vec![2]);
586 assert_eq!(messages[1].metadata.content_type, None);
587 let MessageBody::Undecodable(undecodable) = &messages[1].content else {
588 panic!(
589 "expected undecodable content, got {:?}",
590 messages[1].content
591 );
592 };
593 assert_eq!(undecodable.raw_bytes, malformed);
594 assert_eq!(
595 undecodable.failure.kind,
596 ContentDecodeFailureKind::MalformedEnvelope
597 );
598
599 let MessageBody::Custom(content) = &messages[2].content else {
600 panic!("expected custom content for unknown type message");
601 };
602 assert_eq!(
603 content.encoded.fallback,
604 Some("Invalid message".to_string())
605 );
606 assert_eq!(
607 messages[2].fallback_text,
608 Some("Invalid message".to_string())
609 );
610 }
611
612 #[xmtp_common::test(unwrap_try = true)]
616 async fn malformed_rows_are_kept_on_history_lookup_and_parent_paths() {
617 let client = ClientBuilder::new_test_client(&generate_local_wallet()).await;
618 let group = client.create_group(None, Default::default())?;
619 let conn = client.context.db();
620
621 let malformed = vec![0xff, 0x00, 0x80];
622 let malformed_id = vec![7, 7, 7];
623 create_test_message_raw(
624 &group.group_id,
625 malformed_id.clone(),
626 malformed.clone(),
627 now_ns(),
628 "sender1".into(),
629 None,
630 None,
631 )
632 .store(&conn)?;
633 let reply_id = create_and_store_message(
634 &conn,
635 &group.group_id,
636 vec![8, 8, 8],
637 TestContentGenerator::reply_content(
638 &malformed_id.encode_hex(),
639 TextCodec::content_type(),
640 b"replying to bytes".to_vec(),
641 ),
642 1000,
643 "sender2",
644 );
645 create_and_store_message(
646 &conn,
647 &group.group_id,
648 vec![9, 9, 9],
649 TestContentGenerator::text_content("later"),
650 2000,
651 "sender3",
652 );
653
654 let messages = group.find_messages_v2(&MsgQueryArgs::default())?;
655 assert_eq!(
656 messages
657 .iter()
658 .map(|message| message.metadata.id.clone())
659 .collect::<Vec<_>>(),
660 vec![malformed_id.clone(), reply_id.clone(), vec![9, 9, 9]]
661 );
662 let is_kept = |message: &DecodedMessage| {
663 matches!(&message.content, MessageBody::Undecodable(undecodable)
664 if undecodable.raw_bytes == malformed
665 && undecodable.failure.kind == ContentDecodeFailureKind::MalformedEnvelope)
666 };
667 assert!(is_kept(&messages[0]), "history: {:?}", messages[0].content);
668 assert_text_content(&messages[2], "later");
669
670 let MessageBody::Reply(reply) = &messages[1].content else {
671 panic!("expected reply, got {:?}", messages[1].content);
672 };
673 let parent = reply.in_reply_to.as_ref().expect("reply parent");
674 assert_eq!(parent.metadata.id, malformed_id);
675 assert!(is_kept(parent), "parent: {:?}", parent.content);
676
677 let looked_up = client.message_v2(malformed_id.clone())?;
678 assert_eq!(looked_up.metadata.id, malformed_id);
679 assert!(is_kept(&looked_up), "lookup: {:?}", looked_up.content);
680 }
681
682 #[xmtp_common::test]
683 async fn test_find_messages_invalid_reactions() {
684 let (group, context) = setup_test_group().await;
685 let conn = context.db();
686
687 let msg_id = vec![1, 2, 3];
689 let msg_id_hex = msg_id.encode_hex();
690 create_and_store_message(
691 &conn,
692 &group.group_id,
693 msg_id.clone(),
694 TestContentGenerator::text_content("Hello World"),
695 0,
696 "sender1",
697 );
698
699 create_and_store_message(
701 &conn,
702 &group.group_id,
703 vec![4],
704 TestContentGenerator::reaction_content(&msg_id_hex, "👍", ReactionAction::Added),
705 1000,
706 "reactor1",
707 );
708
709 let reaction_type = xmtp_content_types::reaction::ReactionCodec::content_type();
711 let invalid_reaction = create_test_message_raw(
712 &group.group_id,
713 vec![5],
714 b"invalid reaction bytes".to_vec(),
715 now_ns() + 2000,
716 "reactor2".to_string(),
717 Some(reaction_type),
718 Some(msg_id.clone()),
719 );
720 invalid_reaction.store(&conn).unwrap();
721
722 let messages = group.find_messages_v2(&MsgQueryArgs::default()).unwrap();
724
725 assert_message_count(&messages, 1);
727
728 let original_msg = find_message_by_id(&messages, &msg_id);
730
731 assert_reaction_count(original_msg, 1);
733 assert_has_reaction(original_msg, "👍");
734 }
735
736 #[xmtp_common::test]
737 async fn test_hidden_message_types_are_filtered() {
738 let (group, context) = setup_test_group().await;
739 let conn = context.db();
740
741 create_and_store_message(
743 &conn,
744 &group.group_id,
745 vec![1],
746 TestContentGenerator::text_content("Hello World"),
747 0,
748 "sender1",
749 );
750
751 let msg_id_hex = vec![1].encode_hex();
753 create_and_store_message(
754 &conn,
755 &group.group_id,
756 vec![2],
757 TestContentGenerator::reaction_content(&msg_id_hex, "👍", ReactionAction::Added),
758 1000,
759 "reactor1",
760 );
761
762 create_and_store_message(
764 &conn,
765 &group.group_id,
766 vec![3],
767 TestContentGenerator::read_receipt_content(),
768 2000,
769 "reader1",
770 );
771
772 create_and_store_message(
774 &conn,
775 &group.group_id,
776 vec![4],
777 TestContentGenerator::delete_message_content(&msg_id_hex),
778 3000,
779 "deleter1",
780 );
781
782 create_and_store_message(
784 &conn,
785 &group.group_id,
786 vec![5],
787 TestContentGenerator::text_content("Second message"),
788 4000,
789 "sender2",
790 );
791
792 let messages = group.find_messages_v2(&MsgQueryArgs::default()).unwrap();
795
796 assert_message_count(&messages, 2);
799 assert_text_content(&messages[0], "Hello World");
800 assert_text_content(&messages[1], "Second message");
801
802 assert_reaction_count(&messages[0], 1);
804 assert_has_reaction(&messages[0], "👍");
805 }
806
807 #[xmtp_common::test]
808 async fn test_find_messages_chain_of_replies() {
809 let (group, context) = setup_test_group().await;
810 let conn = context.db();
811
812 let msg1_id = vec![1];
814 let msg1_id_hex = msg1_id.encode_hex();
815 create_and_store_message(
816 &conn,
817 &group.group_id,
818 msg1_id.clone(),
819 TestContentGenerator::text_content("Original message"),
820 0,
821 "sender1",
822 );
823
824 let msg2_id = vec![2];
826 let msg2_id_hex = msg2_id.encode_hex();
827 create_and_store_message(
828 &conn,
829 &group.group_id,
830 msg2_id.clone(),
831 TestContentGenerator::reply_content(
832 &msg1_id_hex,
833 TextCodec::content_type(),
834 b"First reply".to_vec(),
835 ),
836 1000,
837 "replier1",
838 );
839
840 let msg3_id = vec![3];
842 create_and_store_message(
843 &conn,
844 &group.group_id,
845 msg3_id.clone(),
846 TestContentGenerator::reply_content(
847 &msg2_id_hex,
848 TextCodec::content_type(),
849 b"Second reply - reply to reply".to_vec(),
850 ),
851 2000,
852 "replier2",
853 );
854
855 let messages = group.find_messages_v2(&MsgQueryArgs::default()).unwrap();
857
858 assert_message_count(&messages, 3);
859
860 let first_reply = find_message_by_id(&messages, &msg2_id);
862 if let MessageBody::Reply(reply) = &first_reply.content {
863 assert!(reply.in_reply_to.is_some());
865 let referenced = reply.in_reply_to.as_ref().unwrap();
866 assert_eq!(referenced.metadata.id, msg1_id);
867
868 if let MessageBody::Text(_) = &referenced.content {
870 } else {
872 panic!("Expected text in first layer reference");
873 }
874 } else {
875 panic!("Expected reply message");
876 }
877
878 let second_reply = find_message_by_id(&messages, &msg3_id);
880 if let MessageBody::Reply(reply) = &second_reply.content {
881 assert!(reply.in_reply_to.is_some());
883 let referenced = reply.in_reply_to.as_ref().unwrap();
884 assert_eq!(referenced.metadata.id, msg2_id);
885
886 if let MessageBody::Reply(inner_reply) = &referenced.content {
888 assert!(
890 inner_reply.in_reply_to.is_none(),
891 "Should not recursively resolve replies - got {:?}",
892 inner_reply.in_reply_to
893 );
894 } else {
895 panic!("Expected reply in second layer reference");
896 }
897 } else {
898 panic!("Expected reply message");
899 }
900 }
901
902 #[xmtp_common::test]
903 async fn test_reply_with_custom_inner_content() {
904 let (group, context) = setup_test_group().await;
905 let conn = context.db();
906
907 let msg_id = vec![1, 2, 3];
909 let msg_id_hex = msg_id.encode_hex();
910 create_and_store_message(
911 &conn,
912 &group.group_id,
913 msg_id.clone(),
914 TestContentGenerator::text_content("Original message"),
915 0,
916 "sender1",
917 );
918
919 let custom_content_type = ContentTypeId {
921 authority_id: "custom.org".to_string(),
922 type_id: "custom/payload".to_string(),
923 version_major: 1,
924 version_minor: 0,
925 };
926
927 let reply_id = create_and_store_message(
929 &conn,
930 &group.group_id,
931 vec![4, 5, 6],
932 TestContentGenerator::reply_content(
933 &msg_id_hex,
934 custom_content_type,
935 b"custom payload data".to_vec(),
936 ),
937 1000,
938 "replier1",
939 );
940
941 let messages = group.find_messages_v2(&MsgQueryArgs::default()).unwrap();
943 assert_message_count(&messages, 2);
944
945 let reply_msg = find_message_by_id(&messages, &reply_id);
947
948 if let MessageBody::Reply(reply) = &reply_msg.content {
950 assert!(reply.in_reply_to.is_some());
952 let referenced = reply.in_reply_to.as_ref().unwrap();
953 assert_eq!(referenced.metadata.id, msg_id);
954
955 if let MessageBody::Custom(custom) = reply.content.as_ref() {
957 assert_eq!(
958 custom.encoded.r#type.as_ref().unwrap().type_id,
959 "custom/payload"
960 );
961 assert_eq!(custom.encoded.content, b"custom payload data");
962 } else {
963 panic!(
964 "Expected Custom inner content in Reply, got {:?}",
965 reply.content
966 );
967 }
968 } else {
969 panic!("Expected Reply message, got {:?}", reply_msg.content);
970 }
971 }
972}