Skip to main content

xmtp_db/encrypted_store/
message_deletion.rs

1use super::ConnectionExt;
2use crate::schema::message_deletions::dsl;
3use crate::{DbConnection, impl_store, impl_store_or_ignore, schema::message_deletions};
4use diesel::prelude::*;
5use serde::{Deserialize, Serialize};
6
7use xmtp_proto::types::GroupId;
8#[derive(
9    Debug,
10    Clone,
11    Serialize,
12    Deserialize,
13    Insertable,
14    Identifiable,
15    Queryable,
16    Eq,
17    PartialEq,
18    QueryableByName,
19)]
20#[diesel(table_name = message_deletions)]
21#[diesel(primary_key(id))]
22/// Represents a deletion record for a message in a group conversation
23pub struct StoredMessageDeletion {
24    /// The ID of the DeleteMessage in the group_messages table
25    pub id: Vec<u8>,
26    /// The group this deletion belongs to
27    pub group_id: GroupId,
28    /// The ID of the original message being deleted
29    pub deleted_message_id: Vec<u8>,
30    /// The inbox_id of who sent the delete message
31    pub deleted_by_inbox_id: String,
32    /// Whether the deleter was a super admin at deletion time
33    pub is_super_admin_deletion: bool,
34    /// Timestamp when the deletion was processed
35    pub deleted_at_ns: i64,
36}
37
38impl_store!(StoredMessageDeletion, message_deletions);
39impl_store_or_ignore!(StoredMessageDeletion, message_deletions);
40
41/// Trait for querying message deletions
42pub trait QueryMessageDeletion {
43    /// Get a deletion record by the DeleteMessage ID
44    fn get_message_deletion(
45        &self,
46        id: &[u8],
47    ) -> Result<Option<StoredMessageDeletion>, crate::ConnectionError>;
48
49    /// Get deletion record for a specific deleted message
50    fn get_deletion_by_deleted_message_id(
51        &self,
52        deleted_message_id: &[u8],
53    ) -> Result<Option<StoredMessageDeletion>, crate::ConnectionError>;
54
55    /// Get all deletions for a list of message IDs
56    fn get_deletions_for_messages(
57        &self,
58        message_ids: Vec<Vec<u8>>,
59    ) -> Result<Vec<StoredMessageDeletion>, crate::ConnectionError>;
60
61    /// Get all deletions in a group
62    fn get_group_deletions(
63        &self,
64        group_id: &GroupId,
65    ) -> Result<Vec<StoredMessageDeletion>, crate::ConnectionError>;
66
67    /// Check if a message has been deleted
68    fn is_message_deleted(&self, message_id: &[u8]) -> Result<bool, crate::ConnectionError>;
69}
70
71impl<T> QueryMessageDeletion for &T
72where
73    T: QueryMessageDeletion,
74{
75    fn get_message_deletion(
76        &self,
77        id: &[u8],
78    ) -> Result<Option<StoredMessageDeletion>, crate::ConnectionError> {
79        (**self).get_message_deletion(id)
80    }
81
82    fn get_deletion_by_deleted_message_id(
83        &self,
84        deleted_message_id: &[u8],
85    ) -> Result<Option<StoredMessageDeletion>, crate::ConnectionError> {
86        (**self).get_deletion_by_deleted_message_id(deleted_message_id)
87    }
88
89    fn get_deletions_for_messages(
90        &self,
91        message_ids: Vec<Vec<u8>>,
92    ) -> Result<Vec<StoredMessageDeletion>, crate::ConnectionError> {
93        (**self).get_deletions_for_messages(message_ids)
94    }
95
96    fn get_group_deletions(
97        &self,
98        group_id: &GroupId,
99    ) -> Result<Vec<StoredMessageDeletion>, crate::ConnectionError> {
100        (**self).get_group_deletions(group_id)
101    }
102
103    fn is_message_deleted(&self, message_id: &[u8]) -> Result<bool, crate::ConnectionError> {
104        (**self).is_message_deleted(message_id)
105    }
106}
107
108impl<C: ConnectionExt> QueryMessageDeletion for DbConnection<C> {
109    fn get_message_deletion(
110        &self,
111        id: &[u8],
112    ) -> Result<Option<StoredMessageDeletion>, crate::ConnectionError> {
113        self.raw_query(|conn| {
114            dsl::message_deletions
115                .filter(dsl::id.eq(id))
116                .first(conn)
117                .optional()
118        })
119    }
120
121    fn get_deletion_by_deleted_message_id(
122        &self,
123        deleted_message_id: &[u8],
124    ) -> Result<Option<StoredMessageDeletion>, crate::ConnectionError> {
125        self.raw_query(|conn| {
126            dsl::message_deletions
127                .filter(dsl::deleted_message_id.eq(deleted_message_id))
128                .first(conn)
129                .optional()
130        })
131    }
132
133    fn get_deletions_for_messages(
134        &self,
135        message_ids: Vec<Vec<u8>>,
136    ) -> Result<Vec<StoredMessageDeletion>, crate::ConnectionError> {
137        if message_ids.is_empty() {
138            return Ok(vec![]);
139        }
140
141        self.raw_query(|conn| {
142            dsl::message_deletions
143                .filter(dsl::deleted_message_id.eq_any(message_ids))
144                .load(conn)
145        })
146    }
147
148    fn get_group_deletions(
149        &self,
150        group_id: &GroupId,
151    ) -> Result<Vec<StoredMessageDeletion>, crate::ConnectionError> {
152        self.raw_query(|conn| {
153            dsl::message_deletions
154                .filter(dsl::group_id.eq(group_id))
155                .load(conn)
156        })
157    }
158
159    fn is_message_deleted(&self, message_id: &[u8]) -> Result<bool, crate::ConnectionError> {
160        self.raw_query(|conn| {
161            diesel::dsl::select(diesel::dsl::exists(
162                dsl::message_deletions.filter(dsl::deleted_message_id.eq(message_id)),
163            ))
164            .get_result::<bool>(conn)
165        })
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172    use crate::encrypted_store::group::{ConversationType, GroupMembershipState, StoredGroup};
173    use crate::encrypted_store::group_message::{
174        ContentType, DeliveryStatus, GroupMessageKind, StoredGroupMessage,
175    };
176    use crate::{Store, with_connection};
177
178    fn create_test_group(conn: &DbConnection<impl ConnectionExt>, group_id: GroupId) {
179        StoredGroup {
180            id: group_id,
181            created_at_ns: 0,
182            membership_state: GroupMembershipState::Allowed,
183            installations_last_checked: 0,
184            added_by_inbox_id: "test".to_string(),
185            sequence_id: Some(0),
186            rotated_at_ns: 0,
187            conversation_type: ConversationType::Group,
188            dm_id: None,
189            last_message_ns: None,
190            message_disappear_from_ns: None,
191            message_disappear_in_ns: None,
192            paused_for_version: None,
193            maybe_forked: false,
194            fork_details: "[]".to_string(),
195
196            should_publish_commit_log: false,
197            commit_log_public_key: None,
198            is_commit_log_forked: None,
199            has_pending_leave_request: None,
200            push_override: None,
201        }
202        .store(conn)
203        .unwrap();
204    }
205
206    fn create_test_message(
207        conn: &DbConnection<impl ConnectionExt>,
208        id: Vec<u8>,
209        group_id: GroupId,
210    ) {
211        StoredGroupMessage {
212            id,
213            group_id,
214            decrypted_message_bytes: vec![],
215            sent_at_ns: 1000,
216            kind: GroupMessageKind::Application,
217            sender_installation_id: vec![1, 2, 3],
218            sender_inbox_id: "sender".to_string(),
219            delivery_status: DeliveryStatus::Published,
220            content_type: ContentType::Text,
221            version_major: 1,
222            version_minor: 0,
223            authority_id: "xmtp.org".to_string(),
224            reference_id: None,
225            expire_at_ns: None,
226            sequence_id: 1,
227            envelope_hash: None,
228            expiry_ns: None,
229
230            inserted_at_ns: 0,
231            should_push: false,
232            idempotency_key: 1000.to_string(),
233        }
234        .store(conn)
235        .unwrap();
236    }
237
238    #[xmtp_common::test(unwrap_try = true)]
239    fn test_store_and_get_deletion() {
240        with_connection(|conn| {
241            let group_id = GroupId::ONE;
242            let message_id = vec![4, 5, 6];
243            let delete_message_id = vec![7, 8, 9];
244
245            create_test_group(conn, group_id);
246            create_test_message(conn, message_id.clone(), group_id);
247            create_test_message(conn, delete_message_id.clone(), group_id);
248
249            let deletion = StoredMessageDeletion {
250                id: delete_message_id.clone(),
251                group_id,
252                deleted_message_id: message_id.clone(),
253                deleted_by_inbox_id: "sender".to_string(),
254                is_super_admin_deletion: false,
255                deleted_at_ns: 2000,
256            };
257
258            deletion.store(conn)?;
259
260            // Test get by ID
261            let retrieved = conn.get_message_deletion(&delete_message_id)?;
262            assert!(retrieved.is_some());
263            assert_eq!(retrieved.unwrap().deleted_message_id, message_id);
264
265            // Test get by deleted_message_id
266            let by_deleted_id = conn.get_deletion_by_deleted_message_id(&message_id)?;
267            assert!(by_deleted_id.is_some());
268            assert_eq!(by_deleted_id.unwrap().id, delete_message_id);
269        })
270    }
271
272    #[xmtp_common::test(unwrap_try = true)]
273    fn test_is_message_deleted() {
274        with_connection(|conn| {
275            let group_id = GroupId::ONE;
276            let message_id = vec![4, 5, 6];
277            let delete_message_id = vec![7, 8, 9];
278
279            create_test_group(conn, group_id);
280            create_test_message(conn, message_id.clone(), group_id);
281            create_test_message(conn, delete_message_id.clone(), group_id);
282
283            // Initially not deleted
284            assert!(!conn.is_message_deleted(&message_id)?);
285
286            // Store deletion
287            StoredMessageDeletion {
288                id: delete_message_id.clone(),
289                group_id,
290                deleted_message_id: message_id.clone(),
291                deleted_by_inbox_id: "sender".to_string(),
292                is_super_admin_deletion: false,
293                deleted_at_ns: 2000,
294            }
295            .store(conn)?;
296
297            // Now it's deleted
298            assert!(conn.is_message_deleted(&message_id)?);
299        })
300    }
301
302    #[xmtp_common::test(unwrap_try = true)]
303    fn test_get_deletions_for_messages() {
304        with_connection(|conn| {
305            let group_id = GroupId::ONE;
306            let msg1 = vec![4, 5, 6];
307            let msg2 = vec![7, 8, 9];
308            let msg3 = vec![10, 11, 12];
309            let del1 = vec![13, 14, 15];
310            let del2 = vec![16, 17, 18];
311
312            create_test_group(conn, group_id);
313            create_test_message(conn, msg1.clone(), group_id);
314            create_test_message(conn, msg2.clone(), group_id);
315            create_test_message(conn, msg3.clone(), group_id);
316            create_test_message(conn, del1.clone(), group_id);
317            create_test_message(conn, del2.clone(), group_id);
318
319            // Delete msg1 and msg2
320            StoredMessageDeletion {
321                id: del1.clone(),
322                group_id,
323                deleted_message_id: msg1.clone(),
324                deleted_by_inbox_id: "sender".to_string(),
325                is_super_admin_deletion: false,
326                deleted_at_ns: 2000,
327            }
328            .store(conn)?;
329
330            StoredMessageDeletion {
331                id: del2.clone(),
332                group_id,
333                deleted_message_id: msg2.clone(),
334                deleted_by_inbox_id: "admin".to_string(),
335                is_super_admin_deletion: true,
336                deleted_at_ns: 3000,
337            }
338            .store(conn)?;
339
340            // Query for all three messages
341            let deletions =
342                conn.get_deletions_for_messages(vec![msg1.clone(), msg2.clone(), msg3.clone()])?;
343            assert_eq!(deletions.len(), 2);
344
345            // msg3 should not be deleted
346            assert!(!conn.is_message_deleted(&msg3)?);
347        })
348    }
349
350    #[xmtp_common::test(unwrap_try = true)]
351    fn test_get_group_deletions() {
352        with_connection(|conn| {
353            let group1 = GroupId::ONE;
354            let group2 = GroupId::TWO;
355            let msg1 = vec![7, 8, 9];
356            let msg2 = vec![10, 11, 12];
357            let del1 = vec![13, 14, 15];
358            let del2 = vec![16, 17, 18];
359
360            create_test_group(conn, group1);
361            create_test_group(conn, group2);
362            create_test_message(conn, msg1.clone(), group1);
363            create_test_message(conn, msg2.clone(), group2);
364            create_test_message(conn, del1.clone(), group1);
365            create_test_message(conn, del2.clone(), group2);
366
367            StoredMessageDeletion {
368                id: del1.clone(),
369                group_id: group1,
370                deleted_message_id: msg1.clone(),
371                deleted_by_inbox_id: "sender".to_string(),
372                is_super_admin_deletion: false,
373                deleted_at_ns: 2000,
374            }
375            .store(conn)?;
376
377            StoredMessageDeletion {
378                id: del2.clone(),
379                group_id: group2,
380                deleted_message_id: msg2.clone(),
381                deleted_by_inbox_id: "sender".to_string(),
382                is_super_admin_deletion: false,
383                deleted_at_ns: 3000,
384            }
385            .store(conn)?;
386
387            // Get deletions for group1
388            let group1_deletions = conn.get_group_deletions(&group1)?;
389            assert_eq!(group1_deletions.len(), 1);
390            assert_eq!(group1_deletions[0].deleted_message_id, msg1);
391
392            // Get deletions for group2
393            let group2_deletions = conn.get_group_deletions(&group2)?;
394            assert_eq!(group2_deletions.len(), 1);
395            assert_eq!(group2_deletions[0].deleted_message_id, msg2);
396        })
397    }
398}