Skip to main content

xmtp_mls/messages/
decoded_message.rs

1use crate::groups::GroupError;
2use crate::messages::enrichment::EnrichMessageError;
3use prost::Message;
4use xmtp_content_types::actions::{Actions, ActionsCodec};
5use xmtp_content_types::group_updated::GroupUpdatedCodec;
6use xmtp_content_types::intent::{Intent, IntentCodec};
7use xmtp_content_types::leave_request::LeaveRequestCodec;
8use xmtp_content_types::multi_remote_attachment::MultiRemoteAttachmentCodec;
9use xmtp_content_types::reaction::{LegacyReactionCodec, ReactionCodec};
10use xmtp_content_types::read_receipt::ReadReceiptCodec;
11use xmtp_content_types::remote_attachment::RemoteAttachmentCodec;
12use xmtp_content_types::reply::ReplyCodec;
13use xmtp_content_types::transaction_reference::TransactionReferenceCodec;
14use xmtp_content_types::wallet_send_calls::{WalletSendCalls, WalletSendCallsCodec};
15use xmtp_content_types::{CodecError, ContentCodec};
16use xmtp_content_types::{
17    attachment::{Attachment, AttachmentCodec},
18    markdown::MarkdownCodec,
19    read_receipt::ReadReceipt,
20    remote_attachment::RemoteAttachment,
21    text::TextCodec,
22    transaction_reference::TransactionReference,
23};
24use xmtp_db::group_message::StoredGroupMessage;
25use xmtp_db::group_message::{DeliveryStatus, GroupMessageKind};
26use xmtp_proto::types::GroupId;
27use xmtp_proto::xmtp::mls::message_contents::{
28    ContentTypeId, EncodedContent, GroupUpdated,
29    content_types::{LeaveRequest, MultiRemoteAttachment, ReactionV2},
30};
31
32#[derive(Debug, Clone)]
33pub struct Reply {
34    // The original message that this reply is in reply to.
35    // This goes at most one level deep from the original message, and won't happen recursively if there are replies to replies to replies
36    pub in_reply_to: Option<Box<DecodedMessage>>,
37    pub content: Box<MessageBody>,
38    pub reference_id: String,
39}
40
41// Wrap text content in a struct to be consistent with other content types
42#[derive(Debug, Clone)]
43pub struct Text {
44    pub content: String,
45}
46
47// Wrap markdown content in a struct to be consistent with other content types
48#[derive(Debug, Clone)]
49pub struct Markdown {
50    pub content: String,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum DeletedBy {
55    /// Deleted by the original sender
56    Sender,
57    /// Deleted by a super admin
58    Admin(String), // inbox_id of the admin who deleted the message
59}
60
61#[derive(Debug, Clone)]
62pub enum MessageBody {
63    Text(Text),
64    Markdown(Markdown),
65    Reply(Reply),
66    Reaction(ReactionV2),
67    Attachment(Attachment),
68    RemoteAttachment(RemoteAttachment),
69    MultiRemoteAttachment(MultiRemoteAttachment),
70    TransactionReference(TransactionReference),
71    GroupUpdated(GroupUpdated),
72    ReadReceipt(ReadReceipt),
73    WalletSendCalls(WalletSendCalls),
74    Intent(Option<Intent>),
75    Actions(Option<Actions>),
76    LeaveRequest(LeaveRequest),
77    /// Placeholder for a message that has been deleted (shown in message lists)
78    DeletedMessage {
79        deleted_by: DeletedBy,
80    },
81    Custom(EncodedContent),
82}
83
84#[derive(Debug, Clone)]
85pub struct DecodedMessageMetadata {
86    // The message ID
87    pub id: Vec<u8>,
88    // The group ID
89    pub group_id: GroupId,
90    // The timestamp of the message in nanoseconds
91    pub sent_at_ns: i64,
92    // The kind of message
93    pub kind: GroupMessageKind,
94    // The installation ID of the sender
95    pub sender_installation_id: Vec<u8>,
96    // The inbox ID of the sender
97    pub sender_inbox_id: String,
98    // The delivery status of the message
99    pub delivery_status: DeliveryStatus,
100    // The content type of the message
101    pub content_type: ContentTypeId,
102    // Time in nanoseconds the message was inserted into the database
103    pub inserted_at_ns: i64,
104    // Timestamp (in NS) after which the message must be deleted
105    pub expires_at_ns: Option<i64>,
106}
107
108#[derive(Debug, Clone)]
109pub struct DecodedMessage {
110    pub metadata: DecodedMessageMetadata,
111    // The content of the message
112    pub content: MessageBody,
113    // Fallback text for the message
114    pub fallback_text: Option<String>,
115    // A list of reactions
116    pub reactions: Vec<DecodedMessage>,
117    // The number of replies to the message available
118    pub num_replies: usize,
119}
120
121impl TryFrom<EncodedContent> for MessageBody {
122    type Error = GroupError;
123
124    fn try_from(value: EncodedContent) -> Result<Self, Self::Error> {
125        let content_type = match value.r#type.as_ref() {
126            Some(content_type) => content_type,
127            None => return Err(CodecError::InvalidContentType.into()),
128        };
129
130        match (content_type.type_id.as_str(), content_type.version_major) {
131            (TextCodec::TYPE_ID, TextCodec::MAJOR_VERSION) => {
132                let text = TextCodec::decode(value)?;
133                Ok(MessageBody::Text(Text { content: text }))
134            }
135            (MarkdownCodec::TYPE_ID, MarkdownCodec::MAJOR_VERSION) => {
136                let markdown = MarkdownCodec::decode(value)?;
137                Ok(MessageBody::Markdown(Markdown { content: markdown }))
138            }
139            (AttachmentCodec::TYPE_ID, AttachmentCodec::MAJOR_VERSION) => {
140                let attachment = AttachmentCodec::decode(value)?;
141                Ok(MessageBody::Attachment(attachment))
142            }
143            (RemoteAttachmentCodec::TYPE_ID, RemoteAttachmentCodec::MAJOR_VERSION) => {
144                let remote_attachment = RemoteAttachmentCodec::decode(value)?;
145                Ok(MessageBody::RemoteAttachment(remote_attachment))
146            }
147            (ReplyCodec::TYPE_ID, ReplyCodec::MAJOR_VERSION) => {
148                let reply = ReplyCodec::decode(value)?;
149                // if the inner content uses a custom content type, try_into
150                // will fail. in that case, wrap it as custom content.
151                let content: MessageBody = reply
152                    .content
153                    .clone()
154                    .try_into()
155                    .unwrap_or(MessageBody::Custom(reply.content));
156                Ok(MessageBody::Reply(Reply {
157                    in_reply_to: None,
158                    content: Box::new(content),
159                    reference_id: reply.reference,
160                }))
161            }
162            (ReactionCodec::TYPE_ID, ReactionCodec::MAJOR_VERSION) => {
163                let reaction = ReactionCodec::decode(value)?;
164                Ok(MessageBody::Reaction(reaction))
165            }
166            (LegacyReactionCodec::TYPE_ID, LegacyReactionCodec::MAJOR_VERSION) => {
167                let reaction = LegacyReactionCodec::decode(value)?;
168                Ok(MessageBody::Reaction(reaction.into()))
169            }
170            (MultiRemoteAttachmentCodec::TYPE_ID, MultiRemoteAttachmentCodec::MAJOR_VERSION) => {
171                let multi_remote_attachment = MultiRemoteAttachmentCodec::decode(value)?;
172                Ok(MessageBody::MultiRemoteAttachment(multi_remote_attachment))
173            }
174            (TransactionReferenceCodec::TYPE_ID, TransactionReferenceCodec::MAJOR_VERSION) => {
175                let transaction_reference = TransactionReferenceCodec::decode(value)?;
176                Ok(MessageBody::TransactionReference(transaction_reference))
177            }
178            (GroupUpdatedCodec::TYPE_ID, GroupUpdatedCodec::MAJOR_VERSION) => {
179                let group_updated = GroupUpdatedCodec::decode(value)?;
180                Ok(MessageBody::GroupUpdated(group_updated))
181            }
182            (ReadReceiptCodec::TYPE_ID, ReadReceiptCodec::MAJOR_VERSION) => {
183                let read_receipt = ReadReceiptCodec::decode(value)?;
184                Ok(MessageBody::ReadReceipt(read_receipt))
185            }
186            (WalletSendCallsCodec::TYPE_ID, WalletSendCallsCodec::MAJOR_VERSION) => {
187                let wallet_send_calls = WalletSendCallsCodec::decode(value)?;
188                Ok(MessageBody::WalletSendCalls(wallet_send_calls))
189            }
190            (IntentCodec::TYPE_ID, IntentCodec::MAJOR_VERSION) => {
191                let intent = IntentCodec::decode(value)?;
192                Ok(MessageBody::Intent(Some(intent)))
193            }
194            (ActionsCodec::TYPE_ID, ActionsCodec::MAJOR_VERSION) => {
195                let actions = ActionsCodec::decode(value)?;
196                Ok(MessageBody::Actions(Some(actions)))
197            }
198            (LeaveRequestCodec::TYPE_ID, LeaveRequestCodec::MAJOR_VERSION) => {
199                let leave_request = LeaveRequestCodec::decode(value)?;
200                Ok(MessageBody::LeaveRequest(leave_request))
201            }
202
203            _ => Err(CodecError::CodecNotFound(content_type.clone()).into()),
204        }
205    }
206}
207
208impl TryFrom<StoredGroupMessage> for DecodedMessage {
209    type Error = EnrichMessageError;
210
211    fn try_from(value: StoredGroupMessage) -> Result<Self, Self::Error> {
212        // Decode the message content from the stored bytes
213        // If we can't get past this part, we return an error
214        let encoded_content = EncodedContent::decode(&mut value.decrypted_message_bytes.as_slice())
215            .map_err(|_| CodecError::InvalidContentType)?;
216        let content_type_id = encoded_content.r#type.clone().unwrap_or_default();
217        let fallback = encoded_content.fallback.clone();
218
219        let content = match encoded_content.try_into() {
220            Ok(content) => content,
221            // TODO:(nm)
222            // Rather than clone the encoded content by default, I am re-decoding the bytes
223            // That feels dumb and wrong. Will figure out a better solution.
224            Err(_) => MessageBody::Custom(
225                EncodedContent::decode(&mut value.decrypted_message_bytes.as_slice())
226                    .map_err(|e| CodecError::Decode(e.to_string()))?,
227            ),
228        };
229
230        // Create the metadata
231        let metadata = DecodedMessageMetadata {
232            id: value.id,
233            group_id: value.group_id,
234            sent_at_ns: value.sent_at_ns,
235            kind: value.kind,
236            sender_installation_id: value.sender_installation_id,
237            sender_inbox_id: value.sender_inbox_id,
238            delivery_status: value.delivery_status,
239            content_type: content_type_id,
240            inserted_at_ns: value.inserted_at_ns,
241            expires_at_ns: value.expire_at_ns,
242        };
243
244        // For now, we'll set default values for reactions and replies
245        // These could be populated later if needed
246        let reactions = Vec::new();
247        let num_replies = 0;
248
249        Ok(DecodedMessage {
250            metadata,
251            content,
252            fallback_text: fallback,
253            reactions,
254            num_replies,
255        })
256    }
257}