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

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