Skip to main content

xmtp_content_types/
lib.rs

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