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;