xmtp_content_types/
reply.rs1use std::collections::HashMap;
2
3use crate::{CodecError, ContentCodec, text::TextCodec, utils::get_param_or_default};
4use prost::Message;
5use serde::{Deserialize, Serialize};
6
7use xmtp_proto::xmtp::mls::message_contents::{ContentTypeId, EncodedContent};
8
9pub struct ReplyCodec;
10
11impl ReplyCodec {
13 const AUTHORITY_ID: &str = "xmtp.org";
14 pub const TYPE_ID: &str = "reply";
15 pub const MAJOR_VERSION: u32 = 1;
16 pub const MINOR_VERSION: u32 = 0;
17}
18
19impl ReplyCodec {
20 fn fallback(content: &Reply) -> Option<String> {
21 let is_text = content
22 .content
23 .r#type
24 .as_ref()
25 .is_some_and(|t| t.type_id == TextCodec::TYPE_ID);
26
27 if is_text && let Ok(text) = TextCodec::decode(content.content.clone()) {
28 return Some(format!("Replied with \"{text}\" to an earlier message"));
29 }
30
31 Some("Replied to an earlier message".to_string())
32 }
33}
34
35impl ContentCodec<Reply> for ReplyCodec {
36 fn content_type() -> ContentTypeId {
37 ContentTypeId {
38 authority_id: Self::AUTHORITY_ID.to_string(),
39 type_id: Self::TYPE_ID.to_string(),
40 version_major: Self::MAJOR_VERSION,
41 version_minor: Self::MINOR_VERSION,
42 }
43 }
44
45 fn encode(data: Reply) -> Result<EncodedContent, CodecError> {
46 let fallback = Self::fallback(&data);
47 let inner_type = &data.content.r#type;
48 let mut parameters = HashMap::new();
50 parameters.insert("reference".to_string(), data.reference);
51 if let Some(content_type) = inner_type {
52 parameters.insert(
53 "contentType".to_string(),
54 format!(
55 "{}/{}:{}.{}",
56 content_type.authority_id,
57 content_type.type_id,
58 content_type.version_major,
59 content_type.version_minor
60 ),
61 );
62 }
63 if let Some(reference_inbox_id) = data.reference_inbox_id {
64 parameters.insert("referenceInboxId".to_string(), reference_inbox_id);
65 }
66
67 let content_bytes = data.content.encode_to_vec();
68
69 Ok(EncodedContent {
70 r#type: Some(Self::content_type()),
71 parameters,
72 content: content_bytes,
73 fallback,
74 ..Default::default()
75 })
76 }
77
78 fn decode(encoded: EncodedContent) -> Result<Reply, CodecError> {
79 let inner_content = EncodedContent::decode(encoded.content.as_slice())
80 .map_err(|e| CodecError::Decode(e.to_string()))?;
81
82 let reference = get_param_or_default(&encoded.parameters, "reference").to_string();
83
84 let reference_inbox_id = encoded
85 .parameters
86 .get("referenceInboxId")
87 .map(|id| id.to_string());
88
89 Ok(Reply {
90 reference,
91 reference_inbox_id,
92 content: inner_content,
93 })
94 }
95
96 fn should_push() -> bool {
97 true
98 }
99}
100
101#[derive(Debug, Serialize, Deserialize, Clone)]
103pub struct Reply {
104 pub reference: String,
106
107 #[serde(rename = "referenceInboxId", skip_serializing_if = "Option::is_none")]
109 pub reference_inbox_id: Option<String>,
110
111 pub content: EncodedContent,
113}
114
115#[cfg(test)]
116pub(crate) mod tests {
117 use crate::text::TextCodec;
118
119 use super::*;
120
121 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
122 #[cfg_attr(not(target_arch = "wasm32"), test)]
123 fn test_encode_decode_reply() {
124 let text_message = TextCodec::encode("This is a reply".to_string()).unwrap();
125 let reply = Reply {
126 reference: "msg_123".to_string(),
127 reference_inbox_id: Some("inbox_456".to_string()),
128 content: text_message,
129 };
130
131 let encoded = ReplyCodec::encode(reply.clone()).unwrap();
132 let decoded = ReplyCodec::decode(encoded).unwrap();
133
134 assert_eq!(decoded.reference, reply.reference);
135 assert_eq!(decoded.reference_inbox_id, reply.reference_inbox_id);
136 assert_eq!(decoded.content, reply.content);
137 }
138}