feat!: Withdraw broadcast invites. Add Qr::WithdrawJoinBroadcast and Qr::ReviveJoinBroadcast QR code types. (#7439)

Add the ability to withdraw broadcast invite codes

After merging:
- [x] Create issues in iOS, Desktop and UT repositories
This commit is contained in:
Hocuri
2025-11-15 19:27:04 +01:00
committed by GitHub
parent 10b6dd1f11
commit 26f6b85ff9
5 changed files with 229 additions and 11 deletions

View File

@@ -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
/**

View File

@@ -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,

View File

@@ -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<Qr> 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<Qr> 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 },
}
}

104
src/qr.rs
View File

@@ -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<Qr> {
})
}
} 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(

View File

@@ -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();