Skip to content

Commit 8ffdd55

Browse files
r10sHocuri
andauthored
sync message deletion to other devices (#6573)
this PR synchronises deletion of messages across devices and adds a test for it --------- Co-authored-by: Hocuri <[email protected]>
1 parent 9f67d0f commit 8ffdd55

File tree

4 files changed

+133
-48
lines changed

4 files changed

+133
-48
lines changed

deltachat-ffi/deltachat.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1963,7 +1963,7 @@ char* dc_get_mime_headers (dc_context_t* context, uint32_t ms
19631963

19641964

19651965
/**
1966-
* Delete messages. The messages are deleted on the current device and
1966+
* Delete messages. The messages are deleted on all devices and
19671967
* on the IMAP server.
19681968
*
19691969
* @memberof dc_context_t

src/message.rs

Lines changed: 73 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! # Messages and their identifiers.
22
33
use std::collections::BTreeSet;
4+
use std::collections::HashSet;
45
use std::path::{Path, PathBuf};
56
use std::str;
67

@@ -31,6 +32,7 @@ use crate::pgp::split_armored_data;
3132
use crate::reaction::get_msg_reactions;
3233
use crate::sql;
3334
use crate::summary::Summary;
35+
use crate::sync::SyncData;
3436
use crate::tools::{
3537
buf_compress, buf_decompress, get_filebytes, get_filemeta, gm2local_offset, read_file,
3638
sanitize_filename, time, timestamp_to_str, truncate,
@@ -1651,35 +1653,80 @@ pub async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result<Vec<u8
16511653
Ok(headers)
16521654
}
16531655

1656+
/// Delete a single message from the database, including references in other tables.
1657+
/// This may be called in batches; the final events are emitted in delete_msgs_locally_done() then.
1658+
pub(crate) async fn delete_msg_locally(context: &Context, msg: &Message) -> Result<()> {
1659+
if msg.location_id > 0 {
1660+
delete_poi_location(context, msg.location_id).await?;
1661+
}
1662+
let on_server = true;
1663+
msg.id
1664+
.trash(context, on_server)
1665+
.await
1666+
.with_context(|| format!("Unable to trash message {}", msg.id))?;
1667+
1668+
context.emit_event(EventType::MsgDeleted {
1669+
chat_id: msg.chat_id,
1670+
msg_id: msg.id,
1671+
});
1672+
1673+
if msg.viewtype == Viewtype::Webxdc {
1674+
context.emit_event(EventType::WebxdcInstanceDeleted { msg_id: msg.id });
1675+
}
1676+
1677+
let logging_xdc_id = context
1678+
.debug_logging
1679+
.read()
1680+
.expect("RwLock is poisoned")
1681+
.as_ref()
1682+
.map(|dl| dl.msg_id);
1683+
if let Some(id) = logging_xdc_id {
1684+
if id == msg.id {
1685+
set_debug_logging_xdc(context, None).await?;
1686+
}
1687+
}
1688+
1689+
Ok(())
1690+
}
1691+
1692+
/// Do final events and jobs after batch deletion using calls to delete_msg_locally().
1693+
/// To avoid additional database queries, collecting data is up to the caller.
1694+
pub(crate) async fn delete_msgs_locally_done(
1695+
context: &Context,
1696+
msg_ids: &[MsgId],
1697+
modified_chat_ids: HashSet<ChatId>,
1698+
) -> Result<()> {
1699+
for modified_chat_id in modified_chat_ids {
1700+
context.emit_msgs_changed_without_msg_id(modified_chat_id);
1701+
chatlist_events::emit_chatlist_item_changed(context, modified_chat_id);
1702+
}
1703+
if !msg_ids.is_empty() {
1704+
context.emit_msgs_changed_without_ids();
1705+
chatlist_events::emit_chatlist_changed(context);
1706+
// Run housekeeping to delete unused blobs.
1707+
context
1708+
.set_config_internal(Config::LastHousekeeping, None)
1709+
.await?;
1710+
}
1711+
Ok(())
1712+
}
1713+
16541714
/// Deletes requested messages
16551715
/// by moving them to the trash chat
16561716
/// and scheduling for deletion on IMAP.
16571717
pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
1658-
let mut modified_chat_ids = BTreeSet::new();
1718+
let mut modified_chat_ids = HashSet::new();
1719+
let mut deleted_rfc724_mid = Vec::new();
16591720
let mut res = Ok(());
16601721

16611722
for &msg_id in msg_ids {
16621723
let msg = Message::load_from_db(context, msg_id).await?;
1663-
if msg.location_id > 0 {
1664-
delete_poi_location(context, msg.location_id).await?;
1665-
}
1666-
let on_server = true;
1667-
msg_id
1668-
.trash(context, on_server)
1669-
.await
1670-
.with_context(|| format!("Unable to trash message {msg_id}"))?;
1671-
1672-
context.emit_event(EventType::MsgDeleted {
1673-
chat_id: msg.chat_id,
1674-
msg_id,
1675-
});
1676-
1677-
if msg.viewtype == Viewtype::Webxdc {
1678-
context.emit_event(EventType::WebxdcInstanceDeleted { msg_id });
1679-
}
1724+
delete_msg_locally(context, &msg).await?;
16801725

16811726
modified_chat_ids.insert(msg.chat_id);
16821727

1728+
deleted_rfc724_mid.push(msg.rfc724_mid.clone());
1729+
16831730
let target = context.get_delete_msgs_target().await?;
16841731
let update_db = |trans: &mut rusqlite::Transaction| {
16851732
trans.execute(
@@ -1694,38 +1741,20 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
16941741
res = Err(e);
16951742
continue;
16961743
}
1697-
1698-
let logging_xdc_id = context
1699-
.debug_logging
1700-
.read()
1701-
.expect("RwLock is poisoned")
1702-
.as_ref()
1703-
.map(|dl| dl.msg_id);
1704-
1705-
if let Some(id) = logging_xdc_id {
1706-
if id == msg_id {
1707-
set_debug_logging_xdc(context, None).await?;
1708-
}
1709-
}
17101744
}
17111745
res?;
17121746

1713-
for modified_chat_id in modified_chat_ids {
1714-
context.emit_msgs_changed_without_msg_id(modified_chat_id);
1715-
chatlist_events::emit_chatlist_item_changed(context, modified_chat_id);
1716-
}
1747+
delete_msgs_locally_done(context, msg_ids, modified_chat_ids).await?;
17171748

1718-
if !msg_ids.is_empty() {
1719-
context.emit_msgs_changed_without_ids();
1720-
chatlist_events::emit_chatlist_changed(context);
1721-
// Run housekeeping to delete unused blobs.
1722-
context
1723-
.set_config_internal(Config::LastHousekeeping, None)
1724-
.await?;
1725-
}
1749+
context
1750+
.add_sync_item(SyncData::DeleteMessages {
1751+
msgs: deleted_rfc724_mid,
1752+
})
1753+
.await?;
17261754

1727-
// Interrupt Inbox loop to start message deletion and run housekeeping.
1755+
// Interrupt Inbox loop to start message deletion, run housekeeping and call send_sync_msg().
17281756
context.scheduler.interrupt_inbox().await;
1757+
17291758
Ok(())
17301759
}
17311760

src/message/message_tests.rs

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::chatlist::Chatlist;
99
use crate::config::Config;
1010
use crate::reaction::send_reaction;
1111
use crate::receive_imf::receive_imf;
12-
use crate::test_utils as test;
12+
use crate::test_utils;
1313
use crate::test_utils::{TestContext, TestContextManager};
1414

1515
#[test]
@@ -106,7 +106,7 @@ async fn test_create_webrtc_instance_noroom() {
106106

107107
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
108108
async fn test_get_width_height() {
109-
let t = test::TestContext::new().await;
109+
let t = TestContext::new().await;
110110

111111
// test that get_width() and get_height() are returning some dimensions for images;
112112
// (as the device-chat contains a welcome-images, we check that)
@@ -136,7 +136,7 @@ async fn test_get_width_height() {
136136

137137
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
138138
async fn test_quote() {
139-
let d = test::TestContext::new().await;
139+
let d = TestContext::new().await;
140140
let ctx = &d.ctx;
141141

142142
ctx.set_config(Config::ConfiguredAddr, Some("[email protected]"))
@@ -756,6 +756,37 @@ async fn test_delete_msgs_offline() -> Result<()> {
756756
Ok(())
757757
}
758758

759+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
760+
async fn test_delete_msgs_sync() -> Result<()> {
761+
let mut tcm = TestContextManager::new();
762+
let alice = &tcm.alice().await;
763+
let alice2 = &tcm.alice().await;
764+
let bob = &tcm.bob().await;
765+
let alice_chat_id = alice.create_chat(bob).await.id;
766+
767+
alice.set_config_bool(Config::SyncMsgs, true).await?;
768+
alice2.set_config_bool(Config::SyncMsgs, true).await?;
769+
bob.set_config_bool(Config::SyncMsgs, true).await?;
770+
771+
// Alice sends a messsage and receives it on the other device
772+
let sent1 = alice.send_text(alice_chat_id, "foo").await;
773+
assert_eq!(alice_chat_id.get_msg_cnt(alice).await?, 1);
774+
775+
let msg = alice2.recv_msg(&sent1).await;
776+
let alice2_chat_id = msg.chat_id;
777+
assert_eq!(alice2.get_last_msg_in(alice2_chat_id).await.id, msg.id);
778+
assert_eq!(alice2_chat_id.get_msg_cnt(alice2).await?, 1);
779+
780+
// Alice deletes the message; this should happen on both devices as well
781+
delete_msgs(alice, &[sent1.sender_msg_id]).await?;
782+
assert_eq!(alice_chat_id.get_msg_cnt(alice).await?, 0);
783+
784+
test_utils::sync(alice, alice2).await;
785+
assert_eq!(alice2_chat_id.get_msg_cnt(alice2).await?, 0);
786+
787+
Ok(())
788+
}
789+
759790
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
760791
async fn test_sanitize_filename_message() -> Result<()> {
761792
let t = &TestContext::new().await;

src/sync.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use crate::sync::SyncData::{AddQrToken, AlterChat, DeleteQrToken};
1717
use crate::token::Namespace;
1818
use crate::tools::time;
1919
use crate::{message, stock_str, token};
20+
use std::collections::HashSet;
2021

2122
/// Whether to send device sync messages. Aimed for usage in the internal API.
2223
#[derive(Debug, PartialEq)]
@@ -66,6 +67,9 @@ pub(crate) enum SyncData {
6667
src: String, // RFC724 id (i.e. "Message-Id" header)
6768
dest: String, // RFC724 id (i.e. "Message-Id" header)
6869
},
70+
DeleteMessages {
71+
msgs: Vec<String>, // RFC724 id (i.e. "Message-Id" header)
72+
},
6973
}
7074

7175
#[derive(Debug, Serialize, Deserialize)]
@@ -258,6 +262,7 @@ impl Context {
258262
AlterChat { id, action } => self.sync_alter_chat(id, action).await,
259263
SyncData::Config { key, val } => self.sync_config(key, val).await,
260264
SyncData::SaveMessage { src, dest } => self.save_message(src, dest).await,
265+
SyncData::DeleteMessages { msgs } => self.sync_message_deletion(msgs).await,
261266
},
262267
SyncDataOrUnknown::Unknown(data) => {
263268
warn!(self, "Ignored unknown sync item: {data}.");
@@ -297,6 +302,26 @@ impl Context {
297302
}
298303
Ok(())
299304
}
305+
306+
async fn sync_message_deletion(&self, msgs: &Vec<String>) -> Result<()> {
307+
let mut modified_chat_ids = HashSet::new();
308+
let mut msg_ids = Vec::new();
309+
for rfc724_mid in msgs {
310+
if let Some((msg_id, _)) = message::rfc724_mid_exists(self, rfc724_mid).await? {
311+
if let Some(msg) = Message::load_from_db_optional(self, msg_id).await? {
312+
message::delete_msg_locally(self, &msg).await?;
313+
msg_ids.push(msg.id);
314+
modified_chat_ids.insert(msg.chat_id);
315+
} else {
316+
warn!(self, "Sync message delete: Database entry does not exist.");
317+
}
318+
} else {
319+
warn!(self, "Sync message delete: {rfc724_mid:?} not found.");
320+
}
321+
}
322+
message::delete_msgs_locally_done(self, &msg_ids, modified_chat_ids).await?;
323+
Ok(())
324+
}
300325
}
301326

302327
#[cfg(test)]

0 commit comments

Comments
 (0)