mirror of
https://github.com/chatmail/core.git
synced 2026-04-17 21:46:35 +03:00
message deletion request API (#6576)
this PR adds an API allowing users to delete their messages on other member's devices this PR is build on top of https://github.com/deltachat/deltachat-core-rust/pull/6573 which should be merged first a test is missing, otherwise ready for review; it is working already in https://github.com/deltachat/deltachat-ios/pull/2611
This commit is contained in:
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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::{
|
||||
@@ -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?;
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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());
|
||||
@@ -450,6 +451,7 @@ impl MimeMessage {
|
||||
HeaderDef::ChatGroupMemberAdded,
|
||||
HeaderDef::ChatGroupMemberTimestamps,
|
||||
HeaderDef::ChatGroupPastMembers,
|
||||
HeaderDef::ChatDelete,
|
||||
HeaderDef::ChatEdit,
|
||||
] {
|
||||
headers.remove(h.get_headername());
|
||||
|
||||
@@ -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',
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user