From 63872fc271cf94587b191f51dcee0ca1a4404d3b Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Thu, 7 Jul 2022 20:24:16 +0200 Subject: [PATCH] generate qr codes --- deltachat-ffi/Cargo.toml | 1 + deltachat-ffi/src/lib.rs | 61 ++++++++++++++++++++++++++++ deltachat-ffi/src/lot.rs | 6 +++ examples/repl/cmdline.rs | 7 +++- src/qr.rs | 18 ++++++++ src/qr_code_generator.rs | 88 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 180 insertions(+), 1 deletion(-) diff --git a/deltachat-ffi/Cargo.toml b/deltachat-ffi/Cargo.toml index 76fd067fe..5ecee8872 100644 --- a/deltachat-ffi/Cargo.toml +++ b/deltachat-ffi/Cargo.toml @@ -26,6 +26,7 @@ anyhow = "1" thiserror = "1" rand = "0.7" once_cell = "1.16.0" +iroh-share = { git = "https://github.com/n0-computer/iroh", branch = "iroh-share" } [features] default = ["vendored"] diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 68c7c8d27..f78db33c8 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -2204,6 +2204,67 @@ pub unsafe extern "C" fn dc_imex( } } +#[no_mangle] +pub unsafe extern "C" fn dc_send_backup( + context: *mut dc_context_t, + folder: *const libc::c_char, + passphrase: *const libc::c_char, +) -> *mut dc_backup_sender { + if context.is_null() { + eprintln!("ignoring careless call to dc_send_backup()"); + return ptr::null_mut(); + } + + let passphrase = to_opt_string_lossy(passphrase); + let ctx = &*context; + + if let Some(folder) = to_opt_string_lossy(folder) { + block_on(async move { + imex::send_backup(ctx, folder.as_ref(), passphrase) + .await + .map(|(sender, transfer)| { + Box::into_raw(Box::new(dc_backup_sender { sender, transfer })) + }) + .log_err(ctx, "send_backup failed") + .unwrap_or_else(|_| ptr::null_mut()) + }) + } else { + eprintln!("dc_imex called without a valid directory"); + ptr::null_mut() + } +} + +pub struct dc_backup_sender { + sender: iroh_share::Sender, + transfer: iroh_share::SenderTransfer, +} + +#[no_mangle] +pub unsafe extern "C" fn dc_backup_sender_qr( + ctx: *mut dc_context_t, + bs: *const dc_backup_sender, +) -> *mut libc::c_char { + if ctx.is_null() || bs.is_null() { + eprintln!("ignoring careless call to dc_backup_sender_qr"); + return ptr::null_mut(); + } + let ctx = &*ctx; + let bs = &*bs; + let ticket = bs.transfer.ticket(); + + qr_code_generator::generate_backup_qr_code(&ticket) + .map(|s| s.strdup()) + .log_err(ctx, "generate_backup_qr_code failed") + .unwrap_or_else(|_| ptr::null_mut()) +} + +#[no_mangle] +pub unsafe extern "C" fn dc_backup_sender_unref(bs: *mut dc_backup_sender) { + if !bs.is_null() { + let _ = Box::from_raw(bs); + } +} + #[no_mangle] pub unsafe extern "C" fn dc_imex_has_backup( context: *mut dc_context_t, diff --git a/deltachat-ffi/src/lot.rs b/deltachat-ffi/src/lot.rs index 0f3dadc3b..78b21df52 100644 --- a/deltachat-ffi/src/lot.rs +++ b/deltachat-ffi/src/lot.rs @@ -50,6 +50,7 @@ impl Lot { Qr::FprMismatch { .. } => None, Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint), Qr::Account { domain } => Some(domain), + Qr::Backup { .. } => None, Qr::WebrtcInstance { domain, .. } => Some(domain), Qr::Addr { draft, .. } => draft.as_deref(), Qr::Url { url } => Some(url), @@ -101,6 +102,7 @@ impl Lot { Qr::FprMismatch { .. } => LotState::QrFprMismatch, Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr, Qr::Account { .. } => LotState::QrAccount, + Qr::Backup { .. } => LotState::QrBackup, Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance, Qr::Addr { .. } => LotState::QrAddr, Qr::Url { .. } => LotState::QrUrl, @@ -125,6 +127,7 @@ impl Lot { Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default().to_u32(), Qr::FprWithoutAddr { .. } => Default::default(), Qr::Account { .. } => Default::default(), + Qr::Backup { .. } => Default::default(), Qr::WebrtcInstance { .. } => Default::default(), Qr::Addr { contact_id, .. } => contact_id.to_u32(), Qr::Url { .. } => Default::default(), @@ -173,6 +176,9 @@ pub enum LotState { /// text1=domain QrAccount = 250, + /// TODO + QrBackup = 251, + /// text1=domain, text2=instance pattern QrWebrtcInstance = 260, diff --git a/examples/repl/cmdline.rs b/examples/repl/cmdline.rs index 14fbe88f0..e0ffe30ec 100644 --- a/examples/repl/cmdline.rs +++ b/examples/repl/cmdline.rs @@ -483,12 +483,17 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu let dir = dirs::home_dir().unwrap_or_default(); let (sender, transfer) = send_backup(&context, dir.as_ref(), Some(arg2.to_string())).await?; - let ticket_bytes = transfer.ticket().as_bytes(); + let ticket = transfer.ticket(); + let ticket_bytes = ticket.as_bytes(); println!( "Ticket: {}", multibase::encode(multibase::Base::Base64, &ticket_bytes) ); + let qr_code = deltachat::qr_code_generator::generate_backup_qr_code(&ticket)?; + let file = dir.join("qr.svg"); + tokio::fs::write(file, qr_code.as_bytes()).await?; + tokio::time::sleep(std::time::Duration::from_secs(100)).await; sender.close().await?; } diff --git a/src/qr.rs b/src/qr.rs index 800eb4872..541d7c4ce 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -32,6 +32,7 @@ const VCARD_SCHEME: &str = "BEGIN:VCARD"; const SMTP_SCHEME: &str = "SMTP:"; const HTTP_SCHEME: &str = "http://"; const HTTPS_SCHEME: &str = "https://"; +pub const DCBACKUP_SCHEME: &str = "DCBACKUP:"; #[derive(Debug, Clone, PartialEq, Eq)] pub enum Qr { @@ -61,6 +62,9 @@ pub enum Qr { Account { domain: String, }, + Backup { + ticket: iroh_share::Ticket, + }, WebrtcInstance { domain: String, instance_pattern: String, @@ -127,6 +131,8 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result { decode_account(qr)? } else if starts_with_ignore_case(qr, DCLOGIN_SCHEME) { dclogin_scheme::decode_login(qr)? + } else if starts_with_ignore_case(qr, DCBACKUP_SCHEME) { + decode_backup(qr)? } else if starts_with_ignore_case(qr, DCWEBRTC_SCHEME) { decode_webrtc_instance(context, qr)? } else if qr.starts_with(MAILTO_SCHEME) { @@ -332,6 +338,18 @@ fn decode_account(qr: &str) -> Result { } } +/// scheme: `DCBACKUP:` +fn decode_backup(qr: &str) -> Result { + let payload = qr + .get(DCBACKUP_SCHEME.len()..) + .context("invalid DCBACKUP payload")?; + + let (_, ticket_bytes) = multibase::decode(payload)?; + let ticket = iroh_share::Ticket::from_bytes(&ticket_bytes)?; + + Ok(Qr::Backup { ticket }) +} + /// scheme: `DCWEBRTC:https://meet.jit.si/$ROOM` fn decode_webrtc_instance(_context: &Context, qr: &str) -> Result { let payload = qr diff --git a/src/qr_code_generator.rs b/src/qr_code_generator.rs index b3b4aef11..22749daa0 100644 --- a/src/qr_code_generator.rs +++ b/src/qr_code_generator.rs @@ -8,6 +8,7 @@ use crate::{ config::Config, contact::{Contact, ContactId}, context::Context, + qr::DCBACKUP_SCHEME, securejoin, stock_str, }; @@ -262,6 +263,93 @@ fn inner_generate_secure_join_qr_code( Ok(svg) } +pub fn generate_backup_qr_code(ticket: &iroh_share::Ticket) -> Result { + let ticket_bytes = ticket.as_bytes(); + let ticket_str = multibase::encode(multibase::Base::Base64, &ticket_bytes); + let ticket_str = format!("{}{}", DCBACKUP_SCHEME, ticket_str); + // config + let width = 515.0; + let height = 630.0; + let qr_code_size = 400.0; + let qr_translate_up = 40.0; + let card_roundness = 40.0; + let card_border_size = 2.0; + + let qr = QrCode::encode_text(&ticket_str, QrCodeEcc::Medium)?; + let mut svg = String::with_capacity(28000); + let mut w = tagger::new(&mut svg); + + w.elem("svg", |d| { + d.attr("xmlns", "http://www.w3.org/2000/svg")?; + d.attr("viewBox", format_args!("0 0 {} {}", width, height))?; + Ok(()) + })? + .build(|w| { + // White Background apears like a card + w.single("rect", |d| { + d.attr("x", card_border_size)?; + d.attr("y", card_border_size)?; + d.attr("rx", card_roundness)?; + d.attr("stroke", "#c6c6c6")?; + d.attr("stroke-width", card_border_size)?; + d.attr("width", width - (card_border_size * 2.0))?; + d.attr("height", height - (card_border_size * 2.0))?; + d.attr("style", "fill:#f2f2f2")?; + Ok(()) + })?; + // Qrcode + w.elem("g", |d| { + d.attr( + "transform", + format!( + "translate({},{})", + (width - qr_code_size) / 2.0, + ((height - qr_code_size) / 2.0) - qr_translate_up + ), + ) + // If the qr code should be in the wrong place, + // we could also translate and scale the points in the path already, + // but that would make the resulting svg way bigger in size and might bring up rounding issues, + // so better avoid doing it manually if possible + })? + .build(|w| { + w.single("path", |d| { + let mut path_data = String::with_capacity(0); + let scale = qr_code_size / qr.size() as f32; + + for y in 0..qr.size() { + for x in 0..qr.size() { + if qr.get_module(x, y) { + path_data += &format!("M{},{}h1v1h-1z", x, y); + } + } + } + + d.attr("style", "fill:#000000")?; + d.attr("d", path_data)?; + d.attr("transform", format!("scale({})", scale)) + }) + })?; + + // Footer logo + const FOOTER_HEIGHT: f32 = 35.0; + const FOOTER_WIDTH: f32 = 198.0; + w.elem("g", |d| { + d.attr( + "transform", + format!( + "translate({},{})", + (width - FOOTER_WIDTH) / 2.0, + height - FOOTER_HEIGHT + ), + ) + })? + .build(|w| w.put_raw(include_str!("../assets/qrcode_logo_footer.svg"))) + })?; + + Ok(svg) +} + #[cfg(test)] mod tests { use super::*;