diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index e54c12048..829aed592 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -2578,8 +2578,10 @@ void dc_stop_ongoing_process (dc_context_t* context); #define DC_QR_ERROR 400 // text1=error string #define DC_QR_WITHDRAW_VERIFYCONTACT 500 #define DC_QR_WITHDRAW_VERIFYGROUP 502 // text1=groupname +#define DC_QR_WITHDRAW_JOINBROADCAST 504 // text1=broadcast name #define DC_QR_REVIVE_VERIFYCONTACT 510 #define DC_QR_REVIVE_VERIFYGROUP 512 // text1=groupname +#define DC_QR_REVIVE_JOINBROADCAST 514 // text1=broadcast name #define DC_QR_LOGIN 520 // text1=email_address /** diff --git a/deltachat-ffi/src/lot.rs b/deltachat-ffi/src/lot.rs index fdea2c6d4..f6dda66c6 100644 --- a/deltachat-ffi/src/lot.rs +++ b/deltachat-ffi/src/lot.rs @@ -58,8 +58,10 @@ impl Lot { Qr::Text { text } => Some(Cow::Borrowed(text)), Qr::WithdrawVerifyContact { .. } => None, Qr::WithdrawVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)), + Qr::WithdrawJoinBroadcast { name, .. } => Some(Cow::Borrowed(name)), Qr::ReviveVerifyContact { .. } => None, Qr::ReviveVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)), + Qr::ReviveJoinBroadcast { name, .. } => Some(Cow::Borrowed(name)), Qr::Login { address, .. } => Some(Cow::Borrowed(address)), }, Self::Error(err) => Some(Cow::Borrowed(err)), @@ -112,8 +114,10 @@ impl Lot { Qr::Text { .. } => LotState::QrText, Qr::WithdrawVerifyContact { .. } => LotState::QrWithdrawVerifyContact, Qr::WithdrawVerifyGroup { .. } => LotState::QrWithdrawVerifyGroup, + Qr::WithdrawJoinBroadcast { .. } => LotState::QrWithdrawJoinBroadcast, Qr::ReviveVerifyContact { .. } => LotState::QrReviveVerifyContact, Qr::ReviveVerifyGroup { .. } => LotState::QrReviveVerifyGroup, + Qr::ReviveJoinBroadcast { .. } => LotState::QrReviveJoinBroadcast, Qr::Login { .. } => LotState::QrLogin, }, Self::Error(_err) => LotState::QrError, @@ -138,9 +142,11 @@ impl Lot { Qr::Url { .. } => Default::default(), Qr::Text { .. } => Default::default(), Qr::WithdrawVerifyContact { contact_id, .. } => contact_id.to_u32(), - Qr::WithdrawVerifyGroup { .. } => Default::default(), + Qr::WithdrawVerifyGroup { .. } | Qr::WithdrawJoinBroadcast { .. } => { + Default::default() + } Qr::ReviveVerifyContact { contact_id, .. } => contact_id.to_u32(), - Qr::ReviveVerifyGroup { .. } => Default::default(), + Qr::ReviveVerifyGroup { .. } | Qr::ReviveJoinBroadcast { .. } => Default::default(), Qr::Login { .. } => Default::default(), }, Self::Error(_) => Default::default(), @@ -207,11 +213,15 @@ pub enum LotState { /// text1=groupname QrWithdrawVerifyGroup = 502, + /// text1=broadcast channel name + QrWithdrawJoinBroadcast = 504, QrReviveVerifyContact = 510, /// text1=groupname QrReviveVerifyGroup = 512, + /// text1=groupname + QrReviveJoinBroadcast = 514, /// text1=email_address QrLogin = 520, diff --git a/deltachat-jsonrpc/src/api/types/qr.rs b/deltachat-jsonrpc/src/api/types/qr.rs index b40c06784..de05e79c6 100644 --- a/deltachat-jsonrpc/src/api/types/qr.rs +++ b/deltachat-jsonrpc/src/api/types/qr.rs @@ -157,6 +157,21 @@ pub enum QrObject { /// Authentication code. authcode: String, }, + /// Ask the user if they want to withdraw their own broadcast channel invite QR code. + WithdrawJoinBroadcast { + /// Broadcast name. + name: String, + /// ID, uniquely identifying this chat. Called grpid for historic reasons. + grpid: String, + /// Contact ID. Always `ContactId::SELF`. + contact_id: u32, + /// Fingerprint of the contact key as scanned from the QR code. + fingerprint: String, + /// Invite number. + invitenumber: String, + /// Authentication code. + authcode: String, + }, /// Ask the user if they want to revive their own QR code. ReviveVerifyContact { /// Contact ID. @@ -183,6 +198,21 @@ pub enum QrObject { /// Authentication code. authcode: String, }, + /// Ask the user if they want to revive their own broadcast channel invite QR code. + ReviveJoinBroadcast { + /// Broadcast name. + name: String, + /// Globally unique chat ID. Called grpid for historic reasons. + grpid: String, + /// Contact ID. Always `ContactId::SELF`. + contact_id: u32, + /// Fingerprint of the contact key as scanned from the QR code. + fingerprint: String, + /// Invite number. + invitenumber: String, + /// Authentication code. + authcode: String, + }, /// `dclogin:` scheme parameters. /// /// Ask the user if they want to login with the email address. @@ -306,6 +336,25 @@ impl From for QrObject { authcode, } } + Qr::WithdrawJoinBroadcast { + name, + grpid, + contact_id, + fingerprint, + invitenumber, + authcode, + } => { + let contact_id = contact_id.to_u32(); + let fingerprint = fingerprint.to_string(); + QrObject::WithdrawJoinBroadcast { + name, + grpid, + contact_id, + fingerprint, + invitenumber, + authcode, + } + } Qr::ReviveVerifyContact { contact_id, fingerprint, @@ -340,6 +389,25 @@ impl From for QrObject { authcode, } } + Qr::ReviveJoinBroadcast { + name, + grpid, + contact_id, + fingerprint, + invitenumber, + authcode, + } => { + let contact_id = contact_id.to_u32(); + let fingerprint = fingerprint.to_string(); + QrObject::ReviveJoinBroadcast { + name, + grpid, + contact_id, + fingerprint, + invitenumber, + authcode, + } + } Qr::Login { address, .. } => QrObject::Login { address }, } } diff --git a/src/qr.rs b/src/qr.rs index 61816b76c..3c1bdb59f 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -233,6 +233,31 @@ pub enum Qr { authcode: String, }, + /// Ask the user if they want to withdraw their own broadcast channel invite QR code. + WithdrawJoinBroadcast { + /// The user-visible name of this broadcast channel + name: String, + + /// A string of random characters, + /// uniquely identifying this broadcast channel across all databases/clients. + /// Called `grpid` for historic reasons: + /// The id of multi-user chats is always called `grpid` in the database + /// because groups were once the only multi-user chats. + grpid: String, + + /// Contact ID. Always `ContactId::SELF`. + contact_id: ContactId, + + /// Fingerprint of the contact's key as scanned from the QR code. + fingerprint: Fingerprint, + + /// Invite number. + invitenumber: String, + + /// Authentication code. + authcode: String, + }, + /// Ask the user if they want to revive their own QR code. ReviveVerifyContact { /// Contact ID. @@ -269,6 +294,31 @@ pub enum Qr { authcode: String, }, + /// Ask the user if they want to revive their own broadcast channel invite QR code. + ReviveJoinBroadcast { + /// The user-visible name of this broadcast channel + name: String, + + /// A string of random characters, + /// uniquely identifying this broadcast channel across all databases/clients. + /// Called `grpid` for historic reasons: + /// The id of multi-user chats is always called `grpid` in the database + /// because groups were once the only multi-user chats. + grpid: String, + + /// Contact ID. Always `ContactId::SELF`. + contact_id: ContactId, + + /// Fingerprint of the contact's key as scanned from the QR code. + fingerprint: Fingerprint, + + /// Invite number. + invitenumber: String, + + /// Authentication code. + authcode: String, + }, + /// `dclogin:` scheme parameters. /// /// Ask the user if they want to login with the email address. @@ -500,14 +550,40 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result { }) } } else if let (Some(grpid), Some(name)) = (grpid, broadcast_name) { - Ok(Qr::AskJoinBroadcast { - name, - grpid, - contact_id, - fingerprint, - invitenumber, - authcode, - }) + if context + .is_self_addr(&addr) + .await + .with_context(|| format!("Can't check if {addr:?} is our address"))? + { + if token::exists(context, token::Namespace::InviteNumber, &invitenumber).await? { + Ok(Qr::WithdrawJoinBroadcast { + name, + grpid, + contact_id, + fingerprint, + invitenumber, + authcode, + }) + } else { + Ok(Qr::ReviveJoinBroadcast { + name, + grpid, + contact_id, + fingerprint, + invitenumber, + authcode, + }) + } + } else { + Ok(Qr::AskJoinBroadcast { + name, + grpid, + contact_id, + fingerprint, + invitenumber, + authcode, + }) + } } else if context.is_self_addr(&addr).await? { if token::exists(context, token::Namespace::InviteNumber, &invitenumber).await? { Ok(Qr::WithdrawVerifyContact { @@ -800,6 +876,12 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> { invitenumber, authcode, .. + } + | Qr::WithdrawJoinBroadcast { + grpid, + invitenumber, + authcode, + .. } => { token::delete(context, &grpid).await?; context @@ -829,6 +911,12 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> { authcode, grpid, .. + } + | Qr::ReviveJoinBroadcast { + invitenumber, + authcode, + grpid, + .. } => { let timestamp = time(); token::save( diff --git a/src/qr/qr_tests.rs b/src/qr/qr_tests.rs index b5839a404..d6dc88f72 100644 --- a/src/qr/qr_tests.rs +++ b/src/qr/qr_tests.rs @@ -1,5 +1,5 @@ use super::*; -use crate::chat::create_group; +use crate::chat::{Chat, create_broadcast, create_group, get_chat_contacts}; use crate::config::Config; use crate::login_param::EnteredCertificateChecks; use crate::provider::Socket; @@ -511,6 +511,56 @@ async fn test_withdraw_verifygroup() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_withdraw_joinbroadcast() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + let chat_id = create_broadcast(alice, "foo".to_string()).await?; + let qr = get_securejoin_qr(alice, Some(chat_id)).await?; + + // scanning own verify-group code offers withdrawing + if let Qr::WithdrawJoinBroadcast { name, .. } = check_qr(alice, &qr).await? { + assert_eq!(name, "foo"); + } else { + bail!("Wrong QR type, expected WithdrawJoinBroadcast"); + } + set_config_from_qr(alice, &qr).await?; + + // scanning withdrawn verify-group code offers reviving + if let Qr::ReviveJoinBroadcast { name, .. } = check_qr(alice, &qr).await? { + assert_eq!(name, "foo"); + } else { + bail!("Wrong QR type, expected ReviveJoinBroadcast"); + } + + // someone else always scans as ask-verify-group + if let Qr::AskJoinBroadcast { name, .. } = check_qr(bob, &qr).await? { + assert_eq!(name, "foo"); + } else { + bail!("Wrong QR type, expected AskJoinBroadcast"); + } + assert!(set_config_from_qr(bob, &qr).await.is_err()); + + // Bob can't join using this QR code, since it's still withdrawn + let bob_chat_id = tcm.exec_securejoin_qr(bob, alice, &qr).await; + let bob_chat = Chat::load_from_db(bob, bob_chat_id).await?; + assert_eq!(bob_chat.is_self_in_chat(bob).await?, false); + assert_eq!(get_chat_contacts(alice, chat_id).await?.len(), 0); + + // Revive + set_config_from_qr(alice, &qr).await?; + + // Now Bob can join + let bob_chat_id2 = tcm.exec_securejoin_qr(bob, alice, &qr).await; + assert_eq!(bob_chat_id, bob_chat_id2); + let bob_chat = Chat::load_from_db(bob, bob_chat_id).await?; + assert_eq!(bob_chat.is_self_in_chat(bob).await?, true); + assert_eq!(get_chat_contacts(alice, chat_id).await?.len(), 1); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_withdraw_multidevice() -> Result<()> { let mut tcm = TestContextManager::new();