Skip to content

Commit

Permalink
message deletion request API (#6576)
Browse files Browse the repository at this point in the history
this PR adds an API allowing users to delete their messages on other
member's devices

this PR is build on top of
#6573 which should
be merged first

a test is missing, otherwise ready for review; it is working already in
deltachat/deltachat-ios#2611
  • Loading branch information
r10s authored Feb 26, 2025
1 parent a4e478a commit c58f610
Show file tree
Hide file tree
Showing 9 changed files with 167 additions and 13 deletions.
15 changes: 15 additions & 0 deletions deltachat-ffi/deltachat.h
Original file line number Diff line number Diff line change
Expand Up @@ -1056,6 +1056,21 @@ uint32_t dc_send_text_msg (dc_context_t* context, uint32_t ch
void dc_send_edit_request (dc_context_t* context, uint32_t msg_id, const char* new_text);


/**
* Send chat members a request to delete the given messages.
*
* Only outgoing messages can be deleted this way
* and all messages must be in the same chat.
* No tombstone or sth. like that is left.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param msg_ids An array of uint32_t containing all message IDs to delete.
* @param msg_cnt The number of messages IDs in the msg_ids array.
*/
void dc_send_delete_request (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt);


/**
* Send invitation to a videochat.
*
Expand Down
19 changes: 19 additions & 0 deletions deltachat-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,25 @@ pub unsafe extern "C" fn dc_send_edit_request(
.unwrap_or_log_default(ctx, "Failed to send text edit")
}

#[no_mangle]
pub unsafe extern "C" fn dc_send_delete_request(
context: *mut dc_context_t,
msg_ids: *const u32,
msg_cnt: libc::c_int,
) {
if context.is_null() || msg_ids.is_null() || msg_cnt <= 0 {
eprintln!("ignoring careless call to dc_send_delete_request()");
return;
}
let ctx = &*context;
let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt);

block_on(message::delete_msgs_ex(ctx, &msg_ids, true))
.context("failed dc_send_delete_request() call")
.log_err(ctx)
.ok();
}

#[no_mangle]
pub unsafe extern "C" fn dc_send_videochat_invitation(
context: *mut dc_context_t,
Expand Down
41 changes: 41 additions & 0 deletions src/chat/chat_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3802,3 +3802,44 @@ async fn test_cannot_send_edit_request() -> Result<()> {

Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_delete_request() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_chat = alice.create_chat(bob).await;
let bob_chat = bob.create_chat(alice).await;

// Bobs sends a message to Alice, so Alice learns Bob's key
let sent0 = bob.send_text(bob_chat.id, "¡ola!").await;
alice.recv_msg(&sent0).await;

// Alice sends a message, then sends a deletion request
let sent1 = alice.send_text(alice_chat.id, "wtf").await;
let alice_msg = sent1.load_from_db().await;
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, 2);

message::delete_msgs_ex(alice, &[alice_msg.id], true).await?;
let sent2 = alice.pop_sent_msg().await;
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, 1);

// Bob receives both messages and has nothing the end
let bob_msg = bob.recv_msg(&sent1).await;
assert_eq!(bob_msg.text, "wtf");
assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, 2);

bob.recv_msg_opt(&sent2).await;
assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, 1);

// Alice has another device, and there is also nothing at the end
let alice2 = &tcm.alice().await;
alice2.recv_msg(&sent0).await;
let alice2_msg = alice2.recv_msg(&sent1).await;
assert_eq!(alice2_msg.chat_id.get_msg_cnt(alice2).await?, 2);

alice2.recv_msg_opt(&sent2).await;
assert_eq!(alice2_msg.chat_id.get_msg_cnt(alice2).await?, 1);

Ok(())
}
3 changes: 3 additions & 0 deletions src/headerdef.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ pub enum HeaderDef {
ChatDispositionNotificationTo,
ChatWebrtcRoom,

/// This message deletes the messages listed in the value by rfc724_mid.
ChatDelete,

/// This message obsoletes the text of the message defined here by rfc724_mid.
ChatEdit,

Expand Down
54 changes: 42 additions & 12 deletions src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize};
use tokio::{fs, io};

use crate::blob::BlobObject;
use crate::chat::{Chat, ChatId, ChatIdBlocked, ChatVisibility};
use crate::chat::{send_msg, Chat, ChatId, ChatIdBlocked, ChatVisibility};
use crate::chatlist_events;
use crate::config::Config;
use crate::constants::{
Expand Down Expand Up @@ -1711,20 +1711,31 @@ pub(crate) async fn delete_msgs_locally_done(
Ok(())
}

/// Deletes requested messages
/// by moving them to the trash chat
/// and scheduling for deletion on IMAP.
/// Delete messages on all devices and on IMAP.
pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
delete_msgs_ex(context, msg_ids, false).await
}

/// Delete messages on all devices, on IMAP and optionally for all chat members.
/// Deleted messages are moved to the trash chat and scheduling for deletion on IMAP.
/// When deleting messages for others, all messages must be self-sent and in the same chat.
pub async fn delete_msgs_ex(
context: &Context,
msg_ids: &[MsgId],
delete_for_all: bool,
) -> Result<()> {
let mut modified_chat_ids = HashSet::new();
let mut deleted_rfc724_mid = Vec::new();
let mut res = Ok(());

for &msg_id in msg_ids {
let msg = Message::load_from_db(context, msg_id).await?;
delete_msg_locally(context, &msg).await?;
ensure!(
!delete_for_all || msg.from_id == ContactId::SELF,
"Can delete only own messages for others"
);

modified_chat_ids.insert(msg.chat_id);

deleted_rfc724_mid.push(msg.rfc724_mid.clone());

let target = context.get_delete_msgs_target().await?;
Expand All @@ -1744,13 +1755,32 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
}
res?;

delete_msgs_locally_done(context, msg_ids, modified_chat_ids).await?;
if delete_for_all {
ensure!(
modified_chat_ids.len() == 1,
"Can delete only from same chat."
);
if let Some(chat_id) = modified_chat_ids.iter().next() {
let mut msg = Message::new_text("🚮".to_owned());
msg.param.set_int(Param::GuaranteeE2ee, 1);
msg.param
.set(Param::DeleteRequestFor, deleted_rfc724_mid.join(" "));
msg.hidden = true;
send_msg(context, *chat_id, &mut msg).await?;
}
} else {
context
.add_sync_item(SyncData::DeleteMessages {
msgs: deleted_rfc724_mid,
})
.await?;
}

context
.add_sync_item(SyncData::DeleteMessages {
msgs: deleted_rfc724_mid,
})
.await?;
for &msg_id in msg_ids {
let msg = Message::load_from_db(context, msg_id).await?;
delete_msg_locally(context, &msg).await?;
}
delete_msgs_locally_done(context, msg_ids, modified_chat_ids).await?;

// Interrupt Inbox loop to start message deletion, run housekeeping and call send_sync_msg().
context.scheduler.interrupt_inbox().await;
Expand Down
11 changes: 10 additions & 1 deletion src/mimefactory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,12 @@ impl MimeFactory {
)
.into(),
));
} else if let Some(rfc724_mid_list) = msg.param.get(Param::DeleteRequestFor) {
headers.push((
"Chat-Delete",
mail_builder::headers::message_id::MessageId::new(rfc724_mid_list.to_string())
.into(),
));
}
}

Expand Down Expand Up @@ -861,7 +867,10 @@ impl MimeFactory {
if header_name == "message-id" {
unprotected_headers.push(header.clone());
hidden_headers.push(header.clone());
} else if header_name == "chat-user-avatar" || header_name == "chat-edit" {
} else if header_name == "chat-user-avatar"
|| header_name == "chat-delete"
|| header_name == "chat-edit"
{
hidden_headers.push(header.clone());
} else if header_name == "autocrypt"
&& !context.get_config_bool(Config::ProtectAutocrypt).await?
Expand Down
2 changes: 2 additions & 0 deletions src/mimeparser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ impl MimeMessage {
if !headers.contains_key(&key)
&& (key == "chat-user-avatar"
|| key == "chat-group-avatar"
|| key == "chat-delete"
|| key == "chat-edit")
{
headers.insert(key.to_string(), field.get_value());
Expand Down Expand Up @@ -450,6 +451,7 @@ impl MimeMessage {
HeaderDef::ChatGroupMemberAdded,
HeaderDef::ChatGroupMemberTimestamps,
HeaderDef::ChatGroupPastMembers,
HeaderDef::ChatDelete,
HeaderDef::ChatEdit,
] {
headers.remove(h.get_headername());
Expand Down
3 changes: 3 additions & 0 deletions src/param.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,9 @@ pub enum Param {
/// For messages: Whether [crate::message::Viewtype::Sticker] should be forced.
ForceSticker = b'X',

/// For messages: Message is a deletion request. The value is a list of rfc724_mid of the messages to delete.
DeleteRequestFor = b'M',

/// For messages: Message is a text edit message. the value of this parameter is the rfc724_mid of the original message.
TextEditFor = b'I',

Expand Down
32 changes: 32 additions & 0 deletions src/receive_imf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1532,6 +1532,38 @@ async fn add_parts(
"Edit message: rfc724_mid {rfc724_mid:?} not found."
);
}
} else if let Some(rfc724_mid_list) = mime_parser.get_header(HeaderDef::ChatDelete) {
chat_id = DC_CHAT_ID_TRASH;
if let Some(part) = mime_parser.parts.first() {
if part.param.get_bool(Param::GuaranteeE2ee).unwrap_or(false) {
let mut modified_chat_ids = HashSet::new();
let mut msg_ids = Vec::new();

let rfc724_mid_vec: Vec<&str> = rfc724_mid_list.split_whitespace().collect();
for rfc724_mid in rfc724_mid_vec {
if let Some((msg_id, _)) =
message::rfc724_mid_exists(context, rfc724_mid).await?
{
if let Some(msg) = Message::load_from_db_optional(context, msg_id).await? {
if msg.from_id == from_id {
message::delete_msg_locally(context, &msg).await?;
msg_ids.push(msg.id);
modified_chat_ids.insert(msg.chat_id);
} else {
warn!(context, "Delete message: Bad sender.");
}
} else {
warn!(context, "Delete message: Database entry does not exist.");
}
} else {
warn!(context, "Delete message: {rfc724_mid:?} not found.");
}
}
message::delete_msgs_locally_done(context, &msg_ids, modified_chat_ids).await?;
} else {
warn!(context, "Delete message: Not encrypted.");
}
}
}

let mut parts = mime_parser.parts.iter().peekable();
Expand Down

0 comments on commit c58f610

Please sign in to comment.