Skip to main content

xmtp_content_types/
lib.rs

1pub mod actions;
2pub mod attachment;
3pub mod compression;
4pub mod delete_message;
5pub mod encryption;
6#[cfg(not(target_arch = "wasm32"))]
7pub mod file_encryption;
8pub mod group_updated;
9pub mod intent;
10pub mod leave_request;
11pub mod markdown;
12pub mod membership_change;
13pub mod multi_remote_attachment;
14pub mod reaction;
15pub mod read_receipt;
16pub mod remote_attachment;
17pub mod reply;
18pub mod text;
19pub mod transaction_reference;
20mod utils;
21pub mod wallet_send_calls;
22
23pub use xmtp_common::types::ContentTypeId;
24
25use prost::Message;
26use thiserror::Error;
27use xmtp_common::ErrorCode;
28use xmtp_proto::xmtp::mls::message_contents::{
29    ContentTypeId as ProtoContentTypeId, EncodedContent,
30};
31
32#[cfg(test)]
33mod compatibility_test;
34#[cfg(any(test, feature = "test-utils"))]
35pub mod test_utils;
36
37#[derive(Debug, Error, ErrorCode)]
38#[error_code(internal)]
39pub enum CodecError {
40    /// Encode error.
41    ///
42    /// Content type encoding failed. Not retryable.
43    #[error("encode error {0}")]
44    Encode(String),
45    /// Decode error.
46    ///
47    /// Content type decoding failed. Not retryable.
48    #[error("decode error {0}")]
49    Decode(String),
50    /// Codec not found.
51    ///
52    /// No codec registered for content type. Not retryable.
53    #[error("codec not found for {0:?}")]
54    CodecNotFound(ProtoContentTypeId),
55    /// Invalid content type.
56    ///
57    /// Content type identifier is invalid. Not retryable.
58    #[error("invalid content type")]
59    InvalidContentType,
60}
61
62pub enum ContentType {
63    Text,
64    Markdown,
65    GroupMembershipChange,
66    GroupUpdated,
67    Reaction,
68    ReadReceipt,
69    Reply,
70    Actions,
71    Attachment,
72    Intent,
73    RemoteAttachment,
74    MultiRemoteAttachment,
75    TransactionReference,
76    WalletSendCalls,
77    DeviceSyncMessage,
78    LeaveRequest,
79    DeleteMessage,
80}
81
82impl TryFrom<&str> for ContentType {
83    type Error = String;
84
85    fn try_from(type_id: &str) -> Result<Self, Self::Error> {
86        match type_id {
87            text::TextCodec::TYPE_ID => Ok(Self::Text),
88            markdown::MarkdownCodec::TYPE_ID => Ok(Self::Markdown),
89            membership_change::GroupMembershipChangeCodec::TYPE_ID => {
90                Ok(Self::GroupMembershipChange)
91            }
92            group_updated::GroupUpdatedCodec::TYPE_ID => Ok(Self::GroupUpdated),
93            reaction::ReactionCodec::TYPE_ID => Ok(Self::Reaction),
94            read_receipt::ReadReceiptCodec::TYPE_ID => Ok(Self::ReadReceipt),
95            reply::ReplyCodec::TYPE_ID => Ok(Self::Reply),
96            attachment::AttachmentCodec::TYPE_ID => Ok(Self::Attachment),
97            remote_attachment::RemoteAttachmentCodec::TYPE_ID => Ok(Self::RemoteAttachment),
98            multi_remote_attachment::MultiRemoteAttachmentCodec::TYPE_ID => {
99                Ok(Self::MultiRemoteAttachment)
100            }
101            transaction_reference::TransactionReferenceCodec::TYPE_ID => {
102                Ok(Self::TransactionReference)
103            }
104            wallet_send_calls::WalletSendCallsCodec::TYPE_ID => Ok(Self::WalletSendCalls),
105            leave_request::LeaveRequestCodec::TYPE_ID => Ok(Self::LeaveRequest),
106            actions::ActionsCodec::TYPE_ID => Ok(Self::Actions),
107            intent::IntentCodec::TYPE_ID => Ok(Self::Intent),
108            delete_message::DeleteMessageCodec::TYPE_ID => Ok(Self::DeleteMessage),
109            _ => Err(format!("Unknown content type ID: {type_id}")),
110        }
111    }
112}
113
114pub trait ContentCodec<T> {
115    fn content_type() -> ProtoContentTypeId;
116    fn encode(content: T) -> Result<EncodedContent, CodecError>;
117    fn decode(content: EncodedContent) -> Result<T, CodecError>;
118    fn should_push() -> bool;
119}
120
121pub fn encoded_content_to_bytes(content: EncodedContent) -> Vec<u8> {
122    let mut buf = Vec::new();
123    content.encode(&mut buf).unwrap();
124    buf
125}
126
127pub fn bytes_to_encoded_content(bytes: Vec<u8>) -> EncodedContent {
128    EncodedContent::decode(&mut bytes.as_slice()).unwrap()
129}
130
131#[cfg(test)]
132mod tests {
133    use std::collections::HashMap;
134
135    use super::*;
136
137    #[test]
138    fn test_encoded_content_conversion() {
139        // Create a sample EncodedContent
140        let original = EncodedContent {
141            r#type: Some(ProtoContentTypeId {
142                authority_id: "".to_string(),
143                type_id: "test".to_string(),
144                version_major: 0,
145                version_minor: 0,
146            }),
147            parameters: HashMap::new(),
148            compression: None,
149            content: vec![1, 2, 3, 4],
150            fallback: Some("test".to_string()),
151        };
152
153        // Convert to bytes
154        let bytes = encoded_content_to_bytes(original.clone());
155
156        // Convert back to EncodedContent
157        let recovered = bytes_to_encoded_content(bytes);
158
159        // Verify the recovered content matches the original
160        assert_eq!(recovered.content, original.content);
161        assert_eq!(recovered.fallback, original.fallback);
162    }
163}