mirror of
https://github.com/chatmail/core.git
synced 2026-05-16 21:36:30 +03:00
generate qr codes
This commit is contained in:
@@ -26,6 +26,7 @@ anyhow = "1"
|
|||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
rand = "0.7"
|
rand = "0.7"
|
||||||
once_cell = "1.16.0"
|
once_cell = "1.16.0"
|
||||||
|
iroh-share = { git = "https://github.com/n0-computer/iroh", branch = "iroh-share" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["vendored"]
|
default = ["vendored"]
|
||||||
|
|||||||
@@ -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]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_imex_has_backup(
|
pub unsafe extern "C" fn dc_imex_has_backup(
|
||||||
context: *mut dc_context_t,
|
context: *mut dc_context_t,
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ impl Lot {
|
|||||||
Qr::FprMismatch { .. } => None,
|
Qr::FprMismatch { .. } => None,
|
||||||
Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint),
|
Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint),
|
||||||
Qr::Account { domain } => Some(domain),
|
Qr::Account { domain } => Some(domain),
|
||||||
|
Qr::Backup { .. } => None,
|
||||||
Qr::WebrtcInstance { domain, .. } => Some(domain),
|
Qr::WebrtcInstance { domain, .. } => Some(domain),
|
||||||
Qr::Addr { draft, .. } => draft.as_deref(),
|
Qr::Addr { draft, .. } => draft.as_deref(),
|
||||||
Qr::Url { url } => Some(url),
|
Qr::Url { url } => Some(url),
|
||||||
@@ -101,6 +102,7 @@ impl Lot {
|
|||||||
Qr::FprMismatch { .. } => LotState::QrFprMismatch,
|
Qr::FprMismatch { .. } => LotState::QrFprMismatch,
|
||||||
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
|
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
|
||||||
Qr::Account { .. } => LotState::QrAccount,
|
Qr::Account { .. } => LotState::QrAccount,
|
||||||
|
Qr::Backup { .. } => LotState::QrBackup,
|
||||||
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
|
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
|
||||||
Qr::Addr { .. } => LotState::QrAddr,
|
Qr::Addr { .. } => LotState::QrAddr,
|
||||||
Qr::Url { .. } => LotState::QrUrl,
|
Qr::Url { .. } => LotState::QrUrl,
|
||||||
@@ -125,6 +127,7 @@ impl Lot {
|
|||||||
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default().to_u32(),
|
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default().to_u32(),
|
||||||
Qr::FprWithoutAddr { .. } => Default::default(),
|
Qr::FprWithoutAddr { .. } => Default::default(),
|
||||||
Qr::Account { .. } => Default::default(),
|
Qr::Account { .. } => Default::default(),
|
||||||
|
Qr::Backup { .. } => Default::default(),
|
||||||
Qr::WebrtcInstance { .. } => Default::default(),
|
Qr::WebrtcInstance { .. } => Default::default(),
|
||||||
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
|
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
|
||||||
Qr::Url { .. } => Default::default(),
|
Qr::Url { .. } => Default::default(),
|
||||||
@@ -173,6 +176,9 @@ pub enum LotState {
|
|||||||
/// text1=domain
|
/// text1=domain
|
||||||
QrAccount = 250,
|
QrAccount = 250,
|
||||||
|
|
||||||
|
/// TODO
|
||||||
|
QrBackup = 251,
|
||||||
|
|
||||||
/// text1=domain, text2=instance pattern
|
/// text1=domain, text2=instance pattern
|
||||||
QrWebrtcInstance = 260,
|
QrWebrtcInstance = 260,
|
||||||
|
|
||||||
|
|||||||
@@ -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 dir = dirs::home_dir().unwrap_or_default();
|
||||||
let (sender, transfer) =
|
let (sender, transfer) =
|
||||||
send_backup(&context, dir.as_ref(), Some(arg2.to_string())).await?;
|
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!(
|
println!(
|
||||||
"Ticket: {}",
|
"Ticket: {}",
|
||||||
multibase::encode(multibase::Base::Base64, &ticket_bytes)
|
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;
|
tokio::time::sleep(std::time::Duration::from_secs(100)).await;
|
||||||
sender.close().await?;
|
sender.close().await?;
|
||||||
}
|
}
|
||||||
|
|||||||
18
src/qr.rs
18
src/qr.rs
@@ -32,6 +32,7 @@ const VCARD_SCHEME: &str = "BEGIN:VCARD";
|
|||||||
const SMTP_SCHEME: &str = "SMTP:";
|
const SMTP_SCHEME: &str = "SMTP:";
|
||||||
const HTTP_SCHEME: &str = "http://";
|
const HTTP_SCHEME: &str = "http://";
|
||||||
const HTTPS_SCHEME: &str = "https://";
|
const HTTPS_SCHEME: &str = "https://";
|
||||||
|
pub const DCBACKUP_SCHEME: &str = "DCBACKUP:";
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum Qr {
|
pub enum Qr {
|
||||||
@@ -61,6 +62,9 @@ pub enum Qr {
|
|||||||
Account {
|
Account {
|
||||||
domain: String,
|
domain: String,
|
||||||
},
|
},
|
||||||
|
Backup {
|
||||||
|
ticket: iroh_share::Ticket,
|
||||||
|
},
|
||||||
WebrtcInstance {
|
WebrtcInstance {
|
||||||
domain: String,
|
domain: String,
|
||||||
instance_pattern: String,
|
instance_pattern: String,
|
||||||
@@ -127,6 +131,8 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
|
|||||||
decode_account(qr)?
|
decode_account(qr)?
|
||||||
} else if starts_with_ignore_case(qr, DCLOGIN_SCHEME) {
|
} else if starts_with_ignore_case(qr, DCLOGIN_SCHEME) {
|
||||||
dclogin_scheme::decode_login(qr)?
|
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) {
|
} else if starts_with_ignore_case(qr, DCWEBRTC_SCHEME) {
|
||||||
decode_webrtc_instance(context, qr)?
|
decode_webrtc_instance(context, qr)?
|
||||||
} else if qr.starts_with(MAILTO_SCHEME) {
|
} else if qr.starts_with(MAILTO_SCHEME) {
|
||||||
@@ -332,6 +338,18 @@ fn decode_account(qr: &str) -> Result<Qr> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// scheme: `DCBACKUP:<multibase + bincode encode ticket>`
|
||||||
|
fn decode_backup(qr: &str) -> Result<Qr> {
|
||||||
|
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`
|
/// scheme: `DCWEBRTC:https://meet.jit.si/$ROOM`
|
||||||
fn decode_webrtc_instance(_context: &Context, qr: &str) -> Result<Qr> {
|
fn decode_webrtc_instance(_context: &Context, qr: &str) -> Result<Qr> {
|
||||||
let payload = qr
|
let payload = qr
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use crate::{
|
|||||||
config::Config,
|
config::Config,
|
||||||
contact::{Contact, ContactId},
|
contact::{Contact, ContactId},
|
||||||
context::Context,
|
context::Context,
|
||||||
|
qr::DCBACKUP_SCHEME,
|
||||||
securejoin, stock_str,
|
securejoin, stock_str,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -262,6 +263,93 @@ fn inner_generate_secure_join_qr_code(
|
|||||||
Ok(svg)
|
Ok(svg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn generate_backup_qr_code(ticket: &iroh_share::Ticket) -> Result<String> {
|
||||||
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
Reference in New Issue
Block a user