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