From 9cc2fd555f83c1c84058cc7a8c0b7d3863311dc9 Mon Sep 17 00:00:00 2001 From: bjoern Date: Tue, 26 Apr 2022 20:59:17 +0200 Subject: [PATCH] resend messages using the same Message-ID (#3238) * add dc_resend_msgs() to ffi * add 'resend' to repl * implement resend_msgs() * allow only resending if allowed by chat-protection this means, resending is denied if a chat is protected and we cannot encrypt (normally, however, we should not arrive in that state) * allow only resending of normal, non-info-messages * allow only resending of own messages * reset sending state to OutPending on resending the resulting state is always OutDelivered first, OutMdnRcvd again would be applied when a read receipt is received. preserving old state is doable, however, maybe this simple approach is also good enough, at least for now (or maybe the simple approach is even just fine :) another thing: when we upgrade to resending foreign messages, we do not have a simple way to mark them as pending as incoming message just do not have such a state - but this is sth. for the future. --- CHANGELOG.md | 1 + deltachat-ffi/deltachat.h | 19 ++++ deltachat-ffi/src/lib.rs | 21 +++++ examples/repl/cmdline.rs | 8 ++ examples/repl/main.rs | 3 +- src/chat.rs | 181 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 232 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9d8b965e..9a22a8e9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### API-Changes - replaced stock string `DC_STR_ONE_MOMENT` by `DC_STR_NOT_CONNECTED` #3222 +- add `dc_resend_msgs()` #3238 ### Fixes diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 2f959befa..4aca4afc2 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -1801,6 +1801,25 @@ void dc_delete_msgs (dc_context_t* context, const uint3 void dc_forward_msgs (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt, uint32_t chat_id); +/** + * Resend messages and make information available for newly added chat members. + * Resending sends out the original message, however, recipients and webxdc-status may differ. + * Clients that already have the original message can still ignore the resent message as + * they have tracked the state by dedicated updates. + * + * Some messages cannot be resent, eg. info-messages, drafts, already pending messages or messages that are not sent by SELF. + * In this case, the return value indicates an error and the error string from dc_get_last_error() should be shown to the user. + * + * @memberof dc_context_t + * @param context The context object. + * @param msg_ids An array of uint32_t containing all message IDs that should be resend. + * All messages must belong to the same chat. + * @param msg_cnt The number of messages IDs in the msg_ids array. + * @return 1=all messages are queued for resending, 0=error + */ +int dc_resend_msgs (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt); + + /** * Mark messages as presented to the user. * Typically, UIs call this function on scrolling through the message list, diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index a40c4af43..d19f9d3ab 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -1751,6 +1751,27 @@ pub unsafe extern "C" fn dc_forward_msgs( }) } +#[no_mangle] +pub unsafe extern "C" fn dc_resend_msgs( + context: *mut dc_context_t, + msg_ids: *const u32, + msg_cnt: libc::c_int, +) -> libc::c_int { + if context.is_null() || msg_ids.is_null() || msg_cnt <= 0 { + eprintln!("ignoring careless call to dc_resend_msgs()"); + return 0; + } + let ctx = &*context; + let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt); + + if let Err(err) = block_on(chat::resend_msgs(ctx, &msg_ids)) { + error!(ctx, "Resending failed: {}", err); + 0 + } else { + 1 + } +} + #[no_mangle] pub unsafe extern "C" fn dc_markseen_msgs( context: *mut dc_context_t, diff --git a/examples/repl/cmdline.rs b/examples/repl/cmdline.rs index fa53afc9c..c7d45551c 100644 --- a/examples/repl/cmdline.rs +++ b/examples/repl/cmdline.rs @@ -399,6 +399,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu html \n\ listfresh\n\ forward \n\ + resend \n\ markseen \n\ delmsg \n\ ===========================Contact commands==\n\ @@ -1099,6 +1100,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu msg_ids[0] = MsgId::new(arg1.parse()?); chat::forward_msgs(&context, &msg_ids, chat_id).await?; } + "resend" => { + ensure!(!arg1.is_empty(), "Arguments expected"); + + let mut msg_ids = [MsgId::new(0); 1]; + msg_ids[0] = MsgId::new(arg1.parse()?); + chat::resend_msgs(&context, &msg_ids).await?; + } "markseen" => { ensure!(!arg1.is_empty(), "Argument missing."); let mut msg_ids = vec![MsgId::new(0)]; diff --git a/examples/repl/main.rs b/examples/repl/main.rs index 4b2f7aa63..6a82e5da1 100644 --- a/examples/repl/main.rs +++ b/examples/repl/main.rs @@ -207,11 +207,12 @@ const CHAT_COMMANDS: [&str; 36] = [ "accept", "blockchat", ]; -const MESSAGE_COMMANDS: [&str; 7] = [ +const MESSAGE_COMMANDS: [&str; 8] = [ "listmsgs", "msginfo", "listfresh", "forward", + "resend", "markseen", "delmsg", "download", diff --git a/src/chat.rs b/src/chat.rs index 60ce50776..b6bbd5f59 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -3126,6 +3126,52 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId) Ok(()) } +pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> { + let mut chat_id = None; + let mut msgs: Vec = Vec::new(); + for msg_id in msg_ids { + let msg = Message::load_from_db(context, *msg_id).await?; + if let Some(chat_id) = chat_id { + ensure!( + chat_id == msg.chat_id, + "messages to resend needs to be in the same chat" + ); + } else { + chat_id = Some(msg.chat_id); + } + ensure!( + msg.from_id == ContactId::SELF, + "can resend only own messages" + ); + ensure!(!msg.is_info(), "cannot resend info messages"); + msgs.push(msg) + } + + if let Some(chat_id) = chat_id { + let chat = Chat::load_from_db(context, chat_id).await?; + for mut msg in msgs { + if msg.get_showpadlock() && !chat.is_protected() { + msg.param.remove(Param::GuaranteeE2ee); + msg.update_param(context).await; + } + match msg.get_state() { + MessageState::OutFailed | MessageState::OutDelivered | MessageState::OutMdnRcvd => { + message::update_msg_state(context, msg.id, MessageState::OutPending).await? + } + _ => bail!("unexpected message state"), + } + context.emit_event(EventType::MsgsChanged { + chat_id: msg.chat_id, + msg_id: msg.id, + }); + if create_send_msg_job(context, msg.id).await?.is_some() { + context.interrupt_smtp(InterruptInfo::new(false)).await; + } + } + } + Ok(()) +} + pub(crate) async fn get_chat_cnt(context: &Context) -> Result { if context.sql.is_open().await { // no database, no chats - this is no error (needed eg. for information) @@ -5104,6 +5150,141 @@ mod tests { Ok(()) } + #[async_std::test] + async fn test_resend_own_message() -> Result<()> { + // Alice creates group with Bob and sends an initial message + let alice = TestContext::new_alice().await; + let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?; + add_contact_to_chat( + &alice, + alice_grp, + Contact::create(&alice, "", "bob@example.net").await?, + ) + .await?; + let sent1 = alice.send_text(alice_grp, "alice->bob").await; + + // Alice adds Claire to group and resends her own initial message + add_contact_to_chat( + &alice, + alice_grp, + Contact::create(&alice, "", "claire@example.org").await?, + ) + .await?; + let sent2 = alice.pop_sent_msg().await; + resend_msgs(&alice, &[sent1.sender_msg_id]).await?; + let sent3 = alice.pop_sent_msg().await; + + // Bob receives all messages + let bob = TestContext::new_bob().await; + bob.recv_msg(&sent1).await; + let msg = bob.get_last_msg().await; + assert_eq!(msg.get_text().unwrap(), "alice->bob"); + assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 2); + assert_eq!(get_chat_msgs(&bob, msg.chat_id, 0, None).await?.len(), 1); + bob.recv_msg(&sent2).await; + assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 3); + assert_eq!(get_chat_msgs(&bob, msg.chat_id, 0, None).await?.len(), 2); + bob.recv_msg(&sent3).await; + assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 3); + assert_eq!(get_chat_msgs(&bob, msg.chat_id, 0, None).await?.len(), 2); + + // Claire does not receive the first message, however, due to resending, she has a similar view as Alice and Bob + let claire = TestContext::new().await; + claire.configure_addr("claire@example.org").await; + claire.recv_msg(&sent2).await; + claire.recv_msg(&sent3).await; + let msg = claire.get_last_msg().await; + assert_eq!(msg.get_text().unwrap(), "alice->bob"); + assert_eq!(get_chat_contacts(&claire, msg.chat_id).await?.len(), 3); + assert_eq!(get_chat_msgs(&claire, msg.chat_id, 0, None).await?.len(), 2); + let msg_from = Contact::get_by_id(&claire, msg.get_from_id()).await?; + assert_eq!(msg_from.get_addr(), "alice@example.org"); + + Ok(()) + } + + #[async_std::test] + async fn test_resend_foreign_message_fails() -> Result<()> { + let alice = TestContext::new_alice().await; + let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?; + add_contact_to_chat( + &alice, + alice_grp, + Contact::create(&alice, "", "bob@example.net").await?, + ) + .await?; + let sent1 = alice.send_text(alice_grp, "alice->bob").await; + + let bob = TestContext::new_bob().await; + bob.recv_msg(&sent1).await; + let msg = bob.get_last_msg().await; + assert!(resend_msgs(&bob, &[msg.id]).await.is_err()); + + Ok(()) + } + + #[async_std::test] + async fn test_resend_opportunistically_encryption() -> Result<()> { + // Alice creates group with Bob and sends an initial message + let alice = TestContext::new_alice().await; + let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?; + add_contact_to_chat( + &alice, + alice_grp, + Contact::create(&alice, "", "bob@example.net").await?, + ) + .await?; + let sent1 = alice.send_text(alice_grp, "alice->bob").await; + + // Bob now can send an encrypted message + let bob = TestContext::new_bob().await; + bob.recv_msg(&sent1).await; + let msg = bob.get_last_msg().await; + assert!(!msg.get_showpadlock()); + + msg.chat_id.accept(&bob).await?; + let sent2 = bob.send_text(msg.chat_id, "bob->alice").await; + let msg = bob.get_last_msg().await; + assert!(msg.get_showpadlock()); + + // Bob adds Claire and resends his last message: this will drop encryption in opportunistic chats + add_contact_to_chat( + &bob, + msg.chat_id, + Contact::create(&bob, "", "claire@example.org").await?, + ) + .await?; + let _sent3 = bob.pop_sent_msg().await; + resend_msgs(&bob, &[sent2.sender_msg_id]).await?; + let _sent4 = bob.pop_sent_msg().await; + + Ok(()) + } + + #[async_std::test] + async fn test_resend_info_message_fails() -> Result<()> { + let alice = TestContext::new_alice().await; + let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?; + add_contact_to_chat( + &alice, + alice_grp, + Contact::create(&alice, "", "bob@example.net").await?, + ) + .await?; + alice.send_text(alice_grp, "alice->bob").await; + + add_contact_to_chat( + &alice, + alice_grp, + Contact::create(&alice, "", "claire@example.org").await?, + ) + .await?; + let sent2 = alice.pop_sent_msg().await; + assert!(resend_msgs(&alice, &[sent2.sender_msg_id]).await.is_err()); + + Ok(()) + } + #[async_std::test] async fn test_can_send_group() -> Result<()> { let alice = TestContext::new_alice().await;