xmtp_content_types/
attachment.rs1use 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
10impl 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#[derive(Debug, Serialize, Deserialize, Clone)]
77pub struct Attachment {
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub filename: Option<String>,
81
82 pub mime_type: String,
84
85 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}