1use crate::messages::decoded_message::{DecodedMessage, DeletedBy, MessageBody};
2use prost::Message;
3use std::collections::HashMap;
4use thiserror::Error;
5use xmtp_common::{ErrorCode, RetryableError};
6use xmtp_db::DbQuery;
7use xmtp_db::group_message::{
8 ContentType as DbContentType, Deletable, RelationCounts, RelationQuery, StoredGroupMessage,
9};
10use xmtp_db::message_deletion::StoredMessageDeletion;
11use xmtp_proto::xmtp::mls::message_contents::{ContentTypeId, EncodedContent};
12
13use xmtp_proto::types::GroupId;
14pub fn deleted_message_content_type() -> ContentTypeId {
16 ContentTypeId {
17 authority_id: "xmtp.org".to_string(),
18 type_id: "deletedMessage".to_string(),
19 version_major: 1,
20 version_minor: 0,
21 }
22}
23
24#[derive(Debug, Error, ErrorCode)]
25pub enum EnrichMessageError {
26 #[error("Storage error: {0}")]
27 #[error_code(inherit)]
28 Storage(#[from] xmtp_db::StorageError),
29 #[error("DB error: {0}")]
30 #[error_code(inherit)]
31 DbConnection(#[from] xmtp_db::ConnectionError),
32}
33
34impl From<xmtp_db::diesel::result::Error> for EnrichMessageError {
35 fn from(error: xmtp_db::diesel::result::Error) -> Self {
36 Self::Storage(error.into())
37 }
38}
39
40impl RetryableError for EnrichMessageError {
41 fn is_retryable(&self) -> bool {
42 match self {
43 Self::DbConnection(e) => e.is_retryable(),
44 Self::Storage(e) => e.is_retryable(),
45 }
46 }
47}
48
49type ReactionMap = HashMap<Vec<u8>, Vec<DecodedMessage>>;
51type ReferencedMessageMap = HashMap<Vec<u8>, (StoredGroupMessage, DecodedMessage)>;
53type DeletionMap = HashMap<Vec<u8>, Vec<StoredMessageDeletion>>;
55
56pub struct EnrichedStoredMessage {
57 pub delivery_cursor: Option<xmtp_db::delivery::DeliveryCursor>,
58 pub stored: StoredGroupMessage,
59 pub decoded: DecodedMessage,
60 pub parent_stored: Option<StoredGroupMessage>,
61}
62
63pub(crate) fn is_deletable_stored_message(message: &StoredGroupMessage) -> bool {
66 if !message.kind.is_deletable() {
67 return false;
68 }
69 let Ok(content) = EncodedContent::decode(message.decrypted_message_bytes.as_slice()) else {
70 return false;
71 };
72 let Some(identifier) = content.r#type else {
73 return false;
74 };
75 DbContentType::from_identifier(
76 &identifier.authority_id,
77 &identifier.type_id,
78 identifier.version_major,
79 )
80 .is_deletable()
81}
82
83pub(crate) fn is_deletion_valid(
86 deletion: &StoredMessageDeletion,
87 message: &StoredGroupMessage,
88 group_id: &GroupId,
89) -> bool {
90 if deletion.deleted_message_id != message.id {
91 return false;
92 }
93
94 if deletion.group_id != *group_id || message.group_id != *group_id {
95 return false;
96 }
97
98 if !is_deletable_stored_message(message) {
99 return false;
100 }
101
102 let is_sender = deletion.deleted_by_inbox_id == message.sender_inbox_id;
103 is_sender || deletion.is_super_admin_deletion
104}
105
106#[xmtp_common::mls_span]
107pub fn enrich_messages(
108 conn: impl DbQuery,
109 group_id: &GroupId,
110 messages: Vec<StoredGroupMessage>,
111) -> Result<Vec<DecodedMessage>, EnrichMessageError> {
112 Ok(enrich_messages_with_stored(conn, group_id, messages)?
113 .into_iter()
114 .map(|message| message.decoded)
115 .collect())
116}
117
118fn apply_deletion(
122 message: &mut DecodedMessage,
123 deletion: &StoredMessageDeletion,
124 stored: &StoredGroupMessage,
125) {
126 message.content = MessageBody::DeletedMessage {
127 deleted_by: DeletedBy::new(&deletion.deleted_by_inbox_id, &stored.sender_inbox_id),
128 };
129 message.metadata.content_type = Some(deleted_message_content_type());
130 message.fallback_text = None;
131 message.reactions = Vec::new();
132 message.num_replies = 0;
133}
134
135pub fn enrich_messages_with_stored(
139 conn: impl DbQuery,
140 group_id: &GroupId,
141 messages: Vec<StoredGroupMessage>,
142) -> Result<Vec<EnrichedStoredMessage>, EnrichMessageError> {
143 let initial_message_ids: Vec<&[u8]> = messages.iter().map(|m| m.id.as_ref()).collect();
144
145 let reference_ids: Vec<&[u8]> = messages
146 .iter()
147 .filter_map(|m| m.reference_id.as_deref())
148 .collect();
149
150 let mut relations = get_relations(conn, group_id, &initial_message_ids, &reference_ids)?;
151
152 let messages: Vec<EnrichedStoredMessage> = messages
153 .into_iter()
154 .map(|stored_message| {
155 let mut decoded = DecodedMessage::from(stored_message.clone());
156 let mut parent_stored = None;
157
158 let valid_deletion =
159 relations
160 .deletions
161 .get(&decoded.metadata.id)
162 .and_then(|deletions| {
163 deletions
164 .iter()
165 .find(|deletion| is_deletion_valid(deletion, &stored_message, group_id))
166 });
167
168 if let Some(deletion) = valid_deletion {
169 apply_deletion(&mut decoded, deletion, &stored_message);
170 } else {
171 decoded.reactions = relations
172 .reactions
173 .remove(&decoded.metadata.id)
174 .unwrap_or_default();
175
176 decoded.num_replies = relations
177 .reply_counts
178 .get(&decoded.metadata.id)
179 .cloned()
180 .unwrap_or(0);
181
182 if let MessageBody::Reply(mut reply_body) = decoded.content {
184 if let Ok(id) = hex::decode(&reply_body.reference_id) {
186 parent_stored = relations
187 .referenced_messages
188 .get(&id)
189 .map(|(stored, _)| stored.clone());
190 let mut in_reply_to = relations
191 .referenced_messages
192 .get(&id)
193 .map(|(_, decoded)| decoded.clone());
194
195 if let Some(msg) = in_reply_to.as_mut()
196 && let Some(deletions) = relations.deletions.get(&id)
197 && let Some((stored_msg, _)) = relations.referenced_messages.get(&id)
198 && let Some(deletion) = deletions
199 .iter()
200 .find(|deletion| is_deletion_valid(deletion, stored_msg, group_id))
201 {
202 apply_deletion(msg, deletion, stored_msg);
203 }
204 reply_body.in_reply_to = in_reply_to.map(Box::new);
205 }
206 decoded.content = MessageBody::Reply(reply_body);
207 }
208 }
209
210 EnrichedStoredMessage {
211 delivery_cursor: None,
212 stored: stored_message,
213 decoded,
214 parent_stored,
215 }
216 })
217 .collect();
218
219 Ok(messages)
220}
221
222fn get_relations(
223 conn: impl DbQuery,
224 group_id: &GroupId,
225 message_ids: &[&[u8]],
226 reference_ids: &[&[u8]],
227) -> Result<GetRelationsResults, EnrichMessageError> {
228 if message_ids.is_empty() {
229 return Ok(GetRelationsResults {
230 reactions: HashMap::new(),
231 referenced_messages: HashMap::new(),
232 reply_counts: HashMap::new(),
233 deletions: HashMap::new(),
234 });
235 }
236
237 let reactions_relations_query = RelationQuery::builder()
238 .content_types(Some(vec![DbContentType::Reaction]))
239 .build()
240 .unwrap_or_default();
241
242 let replies_count_query = RelationQuery::builder()
243 .content_types(Some(vec![DbContentType::Reply]))
244 .build()
245 .unwrap_or_default();
246
247 let reactions = conn.get_inbound_relations(group_id, message_ids, reactions_relations_query)?;
248 let referenced_messages = conn.get_outbound_relations(group_id, reference_ids)?;
249 let reply_counts =
250 conn.get_inbound_relation_counts(group_id, message_ids, replies_count_query)?;
251
252 let mut all_ids: Vec<Vec<u8>> = message_ids.iter().map(|id| id.to_vec()).collect();
256 all_ids.extend(reference_ids.iter().map(|id| id.to_vec()));
257 let deletions = conn.get_deletions_for_messages(all_ids)?;
258
259 Ok(GetRelationsResults {
260 reactions: get_reactions(reactions),
261 referenced_messages: get_referenced_messages(referenced_messages),
262 reply_counts,
263 deletions: get_deletions(deletions),
264 })
265}
266
267struct GetRelationsResults {
268 reactions: ReactionMap,
269 referenced_messages: ReferencedMessageMap,
270 reply_counts: RelationCounts,
271 deletions: DeletionMap,
272}
273
274fn get_referenced_messages(messages: HashMap<Vec<u8>, StoredGroupMessage>) -> ReferencedMessageMap {
275 messages
276 .into_iter()
277 .map(|(id, stored_message)| {
278 let decoded = DecodedMessage::from(stored_message.clone());
279 (id, (stored_message, decoded))
280 })
281 .collect()
282}
283
284fn get_reactions(messages: HashMap<Vec<u8>, Vec<StoredGroupMessage>>) -> ReactionMap {
288 messages
289 .into_iter()
290 .map(|(id, reaction_messages)| {
291 let mapped_reactions: Vec<DecodedMessage> = reaction_messages
292 .into_iter()
293 .map(DecodedMessage::from)
294 .filter(|decoded| matches!(decoded.content, MessageBody::Reaction(_)))
295 .collect();
296 (id, mapped_reactions)
297 })
298 .collect()
299}
300
301fn get_deletions(deletions: Vec<StoredMessageDeletion>) -> DeletionMap {
302 let mut by_message = DeletionMap::new();
303 for deletion in deletions {
304 by_message
305 .entry(deletion.deleted_message_id.clone())
306 .or_default()
307 .push(deletion);
308 }
309 for records in by_message.values_mut() {
310 records.sort_by(|a, b| a.deleted_at_ns.cmp(&b.deleted_at_ns).then(a.id.cmp(&b.id)));
311 }
312 by_message
313}