From 1cc7ce6e27da86b11b0e166dff8563d11f8026d7 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Tue, 9 Sep 2025 10:17:53 +0200 Subject: [PATCH 1/3] api: Put the chattype into the SecurejoinInviterProgress event (#7181) Quoting @adbenitez: > I have been using the SecurejoinInviterProgress event to show a welcome message when user scan the QR/link of the bot (== starts a chat with the bot) > but this have a big problem: in that event all you know is that a contact completed the secure-join process, you don't know if it was via certain 1:1 invite link or a group invitation, then a group-invite bot would send you a help message in 1:1 every time you join a group with it Since it's easy enough to add this information to the SecurejoinInviterProgress event, I wrote a PR to do so. --- deltachat-jsonrpc/src/api/types/events.rs | 8 ++++ src/events/payload.rs | 4 ++ src/securejoin.rs | 29 ++++++++++---- src/securejoin/securejoin_tests.rs | 46 +++++++++++++++++++++++ 4 files changed, 79 insertions(+), 8 deletions(-) diff --git a/deltachat-jsonrpc/src/api/types/events.rs b/deltachat-jsonrpc/src/api/types/events.rs index 746eee72f..2b4633af4 100644 --- a/deltachat-jsonrpc/src/api/types/events.rs +++ b/deltachat-jsonrpc/src/api/types/events.rs @@ -1,4 +1,5 @@ use deltachat::{Event as CoreEvent, EventType as CoreEventType}; +use num_traits::ToPrimitive; use serde::Serialize; use typescript_type_def::TypeDef; @@ -303,6 +304,11 @@ pub enum EventType { /// ID of the contact that wants to join. contact_id: u32, + /// The type of the joined chat. + /// This can take the same values + /// as `BasicChat.chatType` ([`crate::api::types::chat::BasicChat::chat_type`]). + chat_type: u32, + /// Progress as: /// 300=vg-/vc-request received, typically shown as "bob@addr joins". /// 600=vg-/vc-request-with-auth received and verified, typically shown as "bob@addr verified". @@ -551,9 +557,11 @@ impl From for EventType { }, CoreEventType::SecurejoinInviterProgress { contact_id, + chat_type, progress, } => SecurejoinInviterProgress { contact_id: contact_id.to_u32(), + chat_type: chat_type.to_u32().unwrap_or(0), progress, }, CoreEventType::SecurejoinJoinerProgress { diff --git a/src/events/payload.rs b/src/events/payload.rs index 6bddadf79..1a6f1a679 100644 --- a/src/events/payload.rs +++ b/src/events/payload.rs @@ -5,6 +5,7 @@ use std::path::PathBuf; use crate::chat::ChatId; use crate::config::Config; +use crate::constants::Chattype; use crate::contact::ContactId; use crate::ephemeral::Timer as EphemeralTimer; use crate::message::MsgId; @@ -272,6 +273,9 @@ pub enum EventType { /// ID of the contact that wants to join. contact_id: ContactId, + /// The type of the joined chat. + chat_type: Chattype, + /// Progress as: /// 300=vg-/vc-request received, typically shown as "bob@addr joins". /// 600=vg-/vc-request-with-auth received and verified, typically shown as "bob@addr verified". diff --git a/src/securejoin.rs b/src/securejoin.rs index 888fe0556..1311262e2 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -32,16 +32,29 @@ use qrinvite::QrInvite; use crate::token::Namespace; -fn inviter_progress(context: &Context, contact_id: ContactId, progress: usize) { +fn inviter_progress( + context: &Context, + contact_id: ContactId, + step: &str, + progress: usize, +) -> Result<()> { logged_debug_assert!( context, progress <= 1000, "inviter_progress: contact {contact_id}, progress={progress}, but value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success." ); + let chat_type = match step.get(..3) { + Some("vc-") => Chattype::Single, + Some("vg-") => Chattype::Group, + _ => bail!("Unknown securejoin step {step}"), + }; context.emit_event(EventType::SecurejoinInviterProgress { contact_id, + chat_type, progress, }); + + Ok(()) } /// Generates a Secure Join QR code. @@ -310,7 +323,7 @@ pub(crate) async fn handle_securejoin_handshake( return Ok(HandshakeMessage::Ignore); } - inviter_progress(context, contact_id, 300); + inviter_progress(context, contact_id, step, 300)?; let from_addr = ContactAddress::new(&mime_message.from.addr)?; let autocrypt_fingerprint = mime_message.autocrypt_fingerprint.as_deref().unwrap_or(""); @@ -405,7 +418,7 @@ pub(crate) async fn handle_securejoin_handshake( ChatId::create_for_contact(context, contact_id).await?; } context.emit_event(EventType::ContactsChanged(Some(contact_id))); - inviter_progress(context, contact_id, 600); + inviter_progress(context, contact_id, step, 600)?; if let Some(group_chat_id) = group_chat_id { // Join group. secure_connection_established( @@ -417,8 +430,8 @@ pub(crate) async fn handle_securejoin_handshake( .await?; chat::add_contact_to_chat_ex(context, Nosync, group_chat_id, contact_id, true) .await?; - inviter_progress(context, contact_id, 800); - inviter_progress(context, contact_id, 1000); + inviter_progress(context, contact_id, step, 800)?; + inviter_progress(context, contact_id, step, 1000)?; // IMAP-delete the message to avoid handling it by another device and adding the // member twice. Another device will know the member's key from Autocrypt-Gossip. Ok(HandshakeMessage::Done) @@ -435,7 +448,7 @@ pub(crate) async fn handle_securejoin_handshake( .await .context("failed sending vc-contact-confirm message")?; - inviter_progress(context, contact_id, 1000); + inviter_progress(context, contact_id, step, 1000)?; Ok(HandshakeMessage::Ignore) // "Done" would delete the message and break multi-device (the key from Autocrypt-header is needed) } } @@ -555,10 +568,10 @@ pub(crate) async fn observe_securejoin_on_other_device( ChatId::set_protection_for_contact(context, contact_id, mime_message.timestamp_sent).await?; if step == "vg-member-added" { - inviter_progress(context, contact_id, 800); + inviter_progress(context, contact_id, step, 800)?; } if step == "vg-member-added" || step == "vc-contact-confirm" { - inviter_progress(context, contact_id, 1000); + inviter_progress(context, contact_id, step, 1000)?; } if step == "vg-request-with-auth" || step == "vc-request-with-auth" { diff --git a/src/securejoin/securejoin_tests.rs b/src/securejoin/securejoin_tests.rs index 981640640..b502ccea0 100644 --- a/src/securejoin/securejoin_tests.rs +++ b/src/securejoin/securejoin_tests.rs @@ -365,6 +365,29 @@ async fn test_setup_contact_bob_knows_alice() -> Result<()> { alice.recv_msg_trash(&sent).await; assert_eq!(contact_bob.is_verified(alice).await?, true); + // Check Alice signalled success via the SecurejoinInviterProgress event. + let event = alice + .evtracker + .get_matching(|evt| { + matches!( + evt, + EventType::SecurejoinInviterProgress { progress: 1000, .. } + ) + }) + .await; + match event { + EventType::SecurejoinInviterProgress { + contact_id, + chat_type, + progress, + } => { + assert_eq!(contact_id, contact_bob.id); + assert_eq!(chat_type, Chattype::Single); + assert_eq!(progress, 1000); + } + _ => unreachable!(), + } + let sent = alice.pop_sent_msg().await; let msg = bob.parse_msg(&sent).await; assert!(msg.was_encrypted()); @@ -515,6 +538,29 @@ async fn test_secure_join() -> Result<()> { alice.recv_msg_trash(&sent).await; assert_eq!(contact_bob.is_verified(&alice).await?, true); + // Check Alice signalled success via the SecurejoinInviterProgress event. + let event = alice + .evtracker + .get_matching(|evt| { + matches!( + evt, + EventType::SecurejoinInviterProgress { progress: 1000, .. } + ) + }) + .await; + match event { + EventType::SecurejoinInviterProgress { + contact_id, + chat_type, + progress, + } => { + assert_eq!(contact_id, contact_bob.id); + assert_eq!(chat_type, Chattype::Group); + assert_eq!(progress, 1000); + } + _ => unreachable!(), + } + let sent = alice.pop_sent_msg().await; let msg = bob.parse_msg(&sent).await; assert!(msg.was_encrypted()); From 0684810d38d0427a9635b40befc04f951b530106 Mon Sep 17 00:00:00 2001 From: iequidoo Date: Tue, 2 Sep 2025 09:45:51 -0300 Subject: [PATCH 2/3] refactor: prepare_msg_raw(): don't return MsgId The function takes `&mut Message` and updates its `id` and `chat_id` before return. --- src/chat.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index 2086e774f..7537e1c41 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1957,7 +1957,7 @@ impl Chat { } /// Adds missing values to the msg object, - /// writes the record to the database and returns its msg_id. + /// writes the record to the database. /// /// If `update_msg_id` is set, that record is reused; /// if `update_msg_id` is None, a new record is created. @@ -1966,7 +1966,7 @@ impl Chat { context: &Context, msg: &mut Message, update_msg_id: Option, - ) -> Result { + ) -> Result<()> { let mut to_id = 0; let mut location_id = 0; @@ -2244,7 +2244,7 @@ impl Chat { .await?; } context.scheduler.interrupt_ephemeral_task().await; - Ok(msg.id) + Ok(()) } /// Sends a `SyncAction` synchronising chat contacts to other devices. @@ -2972,8 +2972,7 @@ async fn prepare_send_msg( if !msg.hidden { chat_id.unarchive_if_not_muted(context, msg.state).await?; } - msg.id = chat.prepare_msg_raw(context, msg, update_msg_id).await?; - msg.chat_id = chat_id; + chat.prepare_msg_raw(context, msg, update_msg_id).await?; let row_ids = create_send_msg_jobs(context, msg) .await @@ -4464,13 +4463,13 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId) msg.state = MessageState::OutPending; msg.rfc724_mid = create_outgoing_rfc724_mid(); msg.timestamp_sort = curr_timestamp; - let new_msg_id = chat.prepare_msg_raw(context, &mut msg, None).await?; + chat.prepare_msg_raw(context, &mut msg, None).await?; curr_timestamp += 1; if !create_send_msg_jobs(context, &mut msg).await?.is_empty() { context.scheduler.interrupt_smtp().await; } - created_msgs.push(new_msg_id); + created_msgs.push(msg.id); } for msg_id in created_msgs { context.emit_msgs_changed(chat_id, msg_id); From b9ff40c6b5407d328b17040f0afccbee94a974ef Mon Sep 17 00:00:00 2001 From: iequidoo Date: Wed, 3 Sep 2025 07:18:25 -0300 Subject: [PATCH 3/3] test: Message is OutFailed if all keys are missing (#6849) Follow-up to 143ba6d5e7c48e41bc62eb1e2a26230cd7761c8c before which a message remained in `OutPending` indefinitely in such a case. --- src/chat/chat_tests.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index b2124f941..0bf5ebe6a 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -3173,6 +3173,30 @@ async fn test_chat_get_encryption_info() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_out_failed_on_all_keys_missing() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + let fiona = &tcm.fiona().await; + + let bob_chat_id = bob + .create_group_with_members(ProtectionStatus::Unprotected, "", &[alice, fiona]) + .await; + bob.send_text(bob_chat_id, "Gossiping Fiona's key").await; + alice + .recv_msg(&bob.send_text(bob_chat_id, "No key gossip").await) + .await; + SystemTime::shift(Duration::from_secs(60)); + remove_contact_from_chat(bob, bob_chat_id, ContactId::SELF).await?; + let alice_chat_id = alice.recv_msg(&bob.pop_sent_msg().await).await.chat_id; + alice_chat_id.accept(alice).await?; + let mut msg = Message::new_text("Hi".to_string()); + send_msg(alice, alice_chat_id, &mut msg).await.ok(); + assert_eq!(msg.id.get_state(alice).await?, MessageState::OutFailed); + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_get_chat_media() -> Result<()> { let t = TestContext::new_alice().await;