mirror of
https://github.com/chatmail/core.git
synced 2026-05-08 09:26:29 +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);
|
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.
|
* 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")
|
.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]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_send_videochat_invitation(
|
pub unsafe extern "C" fn dc_send_videochat_invitation(
|
||||||
context: *mut dc_context_t,
|
context: *mut dc_context_t,
|
||||||
|
|||||||
@@ -3802,3 +3802,44 @@ async fn test_cannot_send_edit_request() -> Result<()> {
|
|||||||
|
|
||||||
Ok(())
|
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,
|
ChatDispositionNotificationTo,
|
||||||
ChatWebrtcRoom,
|
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.
|
/// This message obsoletes the text of the message defined here by rfc724_mid.
|
||||||
ChatEdit,
|
ChatEdit,
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use tokio::{fs, io};
|
use tokio::{fs, io};
|
||||||
|
|
||||||
use crate::blob::BlobObject;
|
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::chatlist_events;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::constants::{
|
use crate::constants::{
|
||||||
@@ -1711,20 +1711,31 @@ pub(crate) async fn delete_msgs_locally_done(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deletes requested messages
|
/// Delete messages on all devices and on IMAP.
|
||||||
/// by moving them to the trash chat
|
|
||||||
/// and scheduling for deletion on IMAP.
|
|
||||||
pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
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 modified_chat_ids = HashSet::new();
|
||||||
let mut deleted_rfc724_mid = Vec::new();
|
let mut deleted_rfc724_mid = Vec::new();
|
||||||
let mut res = Ok(());
|
let mut res = Ok(());
|
||||||
|
|
||||||
for &msg_id in msg_ids {
|
for &msg_id in msg_ids {
|
||||||
let msg = Message::load_from_db(context, msg_id).await?;
|
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);
|
modified_chat_ids.insert(msg.chat_id);
|
||||||
|
|
||||||
deleted_rfc724_mid.push(msg.rfc724_mid.clone());
|
deleted_rfc724_mid.push(msg.rfc724_mid.clone());
|
||||||
|
|
||||||
let target = context.get_delete_msgs_target().await?;
|
let target = context.get_delete_msgs_target().await?;
|
||||||
@@ -1744,13 +1755,32 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
|||||||
}
|
}
|
||||||
res?;
|
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
|
for &msg_id in msg_ids {
|
||||||
.add_sync_item(SyncData::DeleteMessages {
|
let msg = Message::load_from_db(context, msg_id).await?;
|
||||||
msgs: deleted_rfc724_mid,
|
delete_msg_locally(context, &msg).await?;
|
||||||
})
|
}
|
||||||
.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().
|
// Interrupt Inbox loop to start message deletion, run housekeeping and call send_sync_msg().
|
||||||
context.scheduler.interrupt_inbox().await;
|
context.scheduler.interrupt_inbox().await;
|
||||||
|
|||||||
@@ -734,6 +734,12 @@ impl MimeFactory {
|
|||||||
)
|
)
|
||||||
.into(),
|
.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" {
|
if header_name == "message-id" {
|
||||||
unprotected_headers.push(header.clone());
|
unprotected_headers.push(header.clone());
|
||||||
hidden_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());
|
hidden_headers.push(header.clone());
|
||||||
} else if header_name == "autocrypt"
|
} else if header_name == "autocrypt"
|
||||||
&& !context.get_config_bool(Config::ProtectAutocrypt).await?
|
&& !context.get_config_bool(Config::ProtectAutocrypt).await?
|
||||||
|
|||||||
@@ -292,6 +292,7 @@ impl MimeMessage {
|
|||||||
if !headers.contains_key(&key)
|
if !headers.contains_key(&key)
|
||||||
&& (key == "chat-user-avatar"
|
&& (key == "chat-user-avatar"
|
||||||
|| key == "chat-group-avatar"
|
|| key == "chat-group-avatar"
|
||||||
|
|| key == "chat-delete"
|
||||||
|| key == "chat-edit")
|
|| key == "chat-edit")
|
||||||
{
|
{
|
||||||
headers.insert(key.to_string(), field.get_value());
|
headers.insert(key.to_string(), field.get_value());
|
||||||
@@ -450,6 +451,7 @@ impl MimeMessage {
|
|||||||
HeaderDef::ChatGroupMemberAdded,
|
HeaderDef::ChatGroupMemberAdded,
|
||||||
HeaderDef::ChatGroupMemberTimestamps,
|
HeaderDef::ChatGroupMemberTimestamps,
|
||||||
HeaderDef::ChatGroupPastMembers,
|
HeaderDef::ChatGroupPastMembers,
|
||||||
|
HeaderDef::ChatDelete,
|
||||||
HeaderDef::ChatEdit,
|
HeaderDef::ChatEdit,
|
||||||
] {
|
] {
|
||||||
headers.remove(h.get_headername());
|
headers.remove(h.get_headername());
|
||||||
|
|||||||
@@ -207,6 +207,9 @@ pub enum Param {
|
|||||||
/// For messages: Whether [crate::message::Viewtype::Sticker] should be forced.
|
/// For messages: Whether [crate::message::Viewtype::Sticker] should be forced.
|
||||||
ForceSticker = b'X',
|
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.
|
/// For messages: Message is a text edit message. the value of this parameter is the rfc724_mid of the original message.
|
||||||
TextEditFor = b'I',
|
TextEditFor = b'I',
|
||||||
|
|
||||||
|
|||||||
@@ -1532,6 +1532,38 @@ async fn add_parts(
|
|||||||
"Edit message: rfc724_mid {rfc724_mid:?} not found."
|
"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();
|
let mut parts = mime_parser.parts.iter().peekable();
|
||||||
|
|||||||
Reference in New Issue
Block a user