Skip to main content

xmtp_content_types/
reply.rs

1use 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
11/// Legacy content type id at <https://github.com/xmtp/xmtp-js/blob/main/content-types/content-type-reply/src/Reply.ts>
12impl 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        // Set the reference and reference inbox ID as parameters.
49        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/// The main content type for replies
102#[derive(Debug, Serialize, Deserialize, Clone)]
103pub struct Reply {
104    /// The message ID being replied to
105    pub reference: String,
106
107    /// The inbox ID of the user who sent the message being replied to
108    #[serde(rename = "referenceInboxId", skip_serializing_if = "Option::is_none")]
109    pub reference_inbox_id: Option<String>,
110
111    /// The content of the reply
112    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}