Skip to main content

xmtp_content_types/
attachment.rs

1use std::collections::HashMap;
2
3use crate::{CodecError, ContentCodec, utils::get_param_or_default};
4use serde::{Deserialize, Serialize};
5
6use xmtp_proto::xmtp::mls::message_contents::{ContentTypeId, EncodedContent};
7
8pub struct AttachmentCodec {}
9
10/// Legacy content type id at <https://github.com/xmtp/xmtp-js/blob/main/content-types/content-type-remote-attachment/src/Attachment.ts>
11impl AttachmentCodec {
12    const AUTHORITY_ID: &'static str = "xmtp.org";
13    pub const TYPE_ID: &'static str = "attachment";
14    pub const MAJOR_VERSION: u32 = 1;
15    pub const MINOR_VERSION: u32 = 0;
16}
17
18impl AttachmentCodec {
19    fn fallback(content: &Attachment) -> Option<String> {
20        Some(format!(
21            "Can't display {}. This app doesn't support attachments.",
22            content
23                .filename
24                .clone()
25                .unwrap_or("this content".to_string())
26        ))
27    }
28}
29
30impl ContentCodec<Attachment> for AttachmentCodec {
31    fn content_type() -> ContentTypeId {
32        ContentTypeId {
33            authority_id: Self::AUTHORITY_ID.to_string(),
34            type_id: Self::TYPE_ID.to_string(),
35            version_major: AttachmentCodec::MAJOR_VERSION,
36            version_minor: AttachmentCodec::MINOR_VERSION,
37        }
38    }
39
40    fn encode(data: Attachment) -> Result<EncodedContent, CodecError> {
41        let fallback = Self::fallback(&data);
42        let mut parameters = [("mimeType", data.mime_type)]
43            .into_iter()
44            .map(|(k, v)| (k.to_string(), v))
45            .collect::<HashMap<_, _>>();
46
47        if let Some(filename) = data.filename {
48            parameters.insert("filename".to_string(), filename);
49        }
50
51        Ok(EncodedContent {
52            r#type: Some(Self::content_type()),
53            parameters,
54            fallback,
55            compression: None,
56            content: data.content,
57        })
58    }
59
60    fn decode(encoded: EncodedContent) -> Result<Attachment, CodecError> {
61        let parameters: &HashMap<String, String> = &encoded.parameters;
62
63        Ok(Attachment {
64            filename: parameters.get("filename").map(|f| f.to_string()),
65            mime_type: get_param_or_default(parameters, "mimeType").to_string(),
66            content: encoded.content,
67        })
68    }
69
70    fn should_push() -> bool {
71        true
72    }
73}
74
75/// The main content type for attachments
76#[derive(Debug, Serialize, Deserialize, Clone)]
77pub struct Attachment {
78    /// The filename of the attachment
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub filename: Option<String>,
81
82    /// The MIME type of the attachment
83    pub mime_type: String,
84
85    /// The content of the attachment (base64 encoded)
86    pub content: Vec<u8>,
87}
88
89#[cfg(test)]
90pub(crate) mod tests {
91    use super::*;
92
93    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
94    #[cfg_attr(not(target_arch = "wasm32"), test)]
95    fn test_encode_decode_attachment() {
96        let attachment = Attachment {
97            filename: Some("test.txt".to_string()),
98            mime_type: "text/plain".to_string(),
99            content: vec![1, 2, 3, 4],
100        };
101
102        let encoded = AttachmentCodec::encode(attachment.clone()).unwrap();
103        let decoded = AttachmentCodec::decode(encoded).unwrap();
104
105        assert_eq!(decoded.filename, attachment.filename);
106        assert_eq!(decoded.mime_type, attachment.mime_type);
107        assert_eq!(decoded.content, attachment.content);
108    }
109}