botserver/src/basic/keywords/qrcode.rs
Rodrigo Rodriguez (Pragmatismo) ad311944b8 Update dependencies and remove problematic crates
Drop image (with ravif/paste), sqlx, zitadel, and related dependencies
that were causing compilation issues. Replace image processing with
direct png crate usage. Update rcgen to 0.14 with new API changes.
Refactor CA certificate generation to use Issuer pattern.
2025-12-03 16:05:30 -03:00

471 lines
18 KiB
Rust

/*****************************************************************************\
| █████ █████ ██ █ █████ █████ ████ ██ ████ █████ █████ ███ ® |
| ██ █ ███ █ █ ██ ██ ██ ██ ██ ██ █ ██ ██ █ █ |
| ██ ███ ████ █ ██ █ ████ █████ ██████ ██ ████ █ █ █ ██ |
| ██ ██ █ █ ██ █ █ ██ ██ ██ ██ ██ ██ █ ██ ██ █ █ |
| █████ █████ █ ███ █████ ██ ██ ██ ██ █████ ████ █████ █ ███ |
| |
| General Bots Copyright (c) pragmatismo.com.br. All rights reserved. |
| Licensed under the AGPL-3.0. |
| |
| According to our dual licensing model, this program can be used either |
| under the terms of the GNU Affero General Public License, version 3, |
| or under a proprietary license. |
| |
| The texts of the GNU Affero General Public License with an additional |
| permission and of our proprietary license can be found at and |
| in the LICENSE file you have received along with this program. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY, without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| "General Bots" is a registered trademark of pragmatismo.com.br. |
| The licensing of the program under the AGPLv3 does not imply a |
| trademark license. Therefore any rights, title and interest in |
| our trademarks remain entirely with us. |
| |
\*****************************************************************************/
//! QR Code generation keyword
//!
//! Provides BASIC keywords:
//! - QR CODE data -> generates QR code image, returns file path
//! - QR CODE data, size -> generates QR code with specified size
//! - QR CODE data, size, output_path -> generates QR code to specific path
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::{error, trace};
use png::{BitDepth, ColorType, Encoder};
use qrcode::QrCode;
use rhai::{Dynamic, Engine};
use std::fs::File;
use std::io::BufWriter;
use std::path::Path;
use std::sync::Arc;
use uuid::Uuid;
/// Register QR code keywords
pub fn register_qrcode_keywords(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
register_qr_code_keyword(state.clone(), user.clone(), engine);
register_qr_code_with_size_keyword(state.clone(), user.clone(), engine);
register_qr_code_full_keyword(state, user, engine);
}
/// QR CODE data
/// Generates a QR code image with default size (256x256)
pub fn register_qr_code_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let state_clone = Arc::clone(&state);
let user_clone = user.clone();
engine
.register_custom_syntax(&["QR", "CODE", "$expr$"], false, move |context, inputs| {
let data = context.eval_expression_tree(&inputs[0])?.to_string();
trace!("QR CODE: Generating QR code for data: {}", data);
let state_for_task = Arc::clone(&state_clone);
let user_for_task = user_clone.clone();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let result =
execute_qr_code_generation(&state_for_task, &user_for_task, &data, 256, None);
if tx.send(result).is_err() {
error!("Failed to send QR CODE result from thread");
}
});
match rx.recv_timeout(std::time::Duration::from_secs(30)) {
Ok(Ok(result)) => Ok(Dynamic::from(result)),
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("QR CODE failed: {}", e).into(),
rhai::Position::NONE,
))),
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"QR CODE generation timed out".into(),
rhai::Position::NONE,
)))
}
Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("QR CODE thread failed: {}", e).into(),
rhai::Position::NONE,
))),
}
})
.unwrap();
}
/// QR CODE data, size
/// Generates a QR code image with specified size
pub fn register_qr_code_with_size_keyword(
state: Arc<AppState>,
user: UserSession,
engine: &mut Engine,
) {
let state_clone = Arc::clone(&state);
let user_clone = user.clone();
engine
.register_custom_syntax(
&["QR", "CODE", "$expr$", ",", "$expr$"],
false,
move |context, inputs| {
let data = context.eval_expression_tree(&inputs[0])?.to_string();
let size = context
.eval_expression_tree(&inputs[1])?
.as_int()
.unwrap_or(256) as u32;
trace!(
"QR_CODE: Generating QR code with size {} for: {}",
size,
data
);
let state_for_task = Arc::clone(&state_clone);
let user_for_task = user_clone.clone();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let result = execute_qr_code_generation(
&state_for_task,
&user_for_task,
&data,
size,
None,
);
if tx.send(result).is_err() {
error!("Failed to send QR_CODE result from thread");
}
});
match rx.recv_timeout(std::time::Duration::from_secs(30)) {
Ok(Ok(result)) => Ok(Dynamic::from(result)),
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("QR_CODE failed: {}", e).into(),
rhai::Position::NONE,
))),
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"QR_CODE generation timed out".into(),
rhai::Position::NONE,
)))
}
Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("QR_CODE thread failed: {}", e).into(),
rhai::Position::NONE,
))),
}
},
)
.unwrap();
}
/// QR_CODE data, size, output_path
/// Generates a QR code image with specified size and output path
pub fn register_qr_code_full_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let state_clone = Arc::clone(&state);
let user_clone = user.clone();
engine
.register_custom_syntax(
&["QR_CODE", "$expr$", ",", "$expr$", ",", "$expr$"],
false,
move |context, inputs| {
let data = context.eval_expression_tree(&inputs[0])?.to_string();
let size = context
.eval_expression_tree(&inputs[1])?
.as_int()
.unwrap_or(256) as u32;
let output_path = context.eval_expression_tree(&inputs[2])?.to_string();
trace!(
"QR_CODE: Generating QR code with size {} to {} for: {}",
size,
output_path,
data
);
let state_for_task = Arc::clone(&state_clone);
let user_for_task = user_clone.clone();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let result = execute_qr_code_generation(
&state_for_task,
&user_for_task,
&data,
size,
Some(&output_path),
);
if tx.send(result).is_err() {
error!("Failed to send QR_CODE result from thread");
}
});
match rx.recv_timeout(std::time::Duration::from_secs(30)) {
Ok(Ok(result)) => Ok(Dynamic::from(result)),
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("QR_CODE failed: {}", e).into(),
rhai::Position::NONE,
))),
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"QR_CODE generation timed out".into(),
rhai::Position::NONE,
)))
}
Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("QR_CODE thread failed: {}", e).into(),
rhai::Position::NONE,
))),
}
},
)
.unwrap();
}
fn execute_qr_code_generation(
state: &AppState,
user: &UserSession,
data: &str,
size: u32,
output_path: Option<&str>,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
// Generate QR code
let code = QrCode::new(data.as_bytes())?;
// Get the QR code as a matrix of bools
let matrix = code.to_colors();
let qr_width = code.width();
// Calculate scale factor to reach target size
let scale = (size as usize) / qr_width;
let actual_size = qr_width * scale;
// Create grayscale pixel buffer
let mut pixels: Vec<u8> = Vec::with_capacity(actual_size * actual_size);
for y in 0..actual_size {
for x in 0..actual_size {
let qr_x = x / scale;
let qr_y = y / scale;
let idx = qr_y * qr_width + qr_x;
let is_dark = matrix
.get(idx)
.map(|c| *c == qrcode::Color::Dark)
.unwrap_or(false);
pixels.push(if is_dark { 0 } else { 255 });
}
}
// Determine output path
let data_dir = state
.config
.as_ref()
.map(|c| c.data_dir.as_str())
.unwrap_or("./botserver-stack/data");
let final_path = match output_path {
Some(path) => {
if Path::new(path).is_absolute() {
path.to_string()
} else {
format!("{}/bots/{}/gbdrive/{}", data_dir, user.bot_id, path)
}
}
None => {
let filename = format!("qrcode_{}.png", Uuid::new_v4());
let base_path = format!("{}/bots/{}/gbdrive", data_dir, user.bot_id);
// Ensure directory exists
std::fs::create_dir_all(&base_path)?;
format!("{}/{}", base_path, filename)
}
};
// Ensure parent directory exists
if let Some(parent) = Path::new(&final_path).parent() {
std::fs::create_dir_all(parent)?;
}
// Save as PNG using png crate directly
let file = File::create(&final_path)?;
let ref mut w = BufWriter::new(file);
let mut encoder = Encoder::new(w, actual_size as u32, actual_size as u32);
encoder.set_color(ColorType::Grayscale);
encoder.set_depth(BitDepth::Eight);
let mut writer = encoder.write_header()?;
writer.write_image_data(&pixels)?;
trace!("QR code generated: {}", final_path);
Ok(final_path)
}
/// Generate QR code with custom colors
pub fn generate_qr_code_colored(
data: &str,
size: u32,
foreground: [u8; 3],
background: [u8; 3],
output_path: &str,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let code = QrCode::new(data.as_bytes())?;
// Get the QR code as a matrix of bools
let matrix = code.to_colors();
let qr_width = code.width();
// Calculate scale factor to reach target size
let scale = (size as usize) / qr_width;
let actual_size = qr_width * scale;
// Create RGB pixel buffer (3 bytes per pixel)
let mut pixels: Vec<u8> = Vec::with_capacity(actual_size * actual_size * 3);
for y in 0..actual_size {
for x in 0..actual_size {
let qr_x = x / scale;
let qr_y = y / scale;
let idx = qr_y * qr_width + qr_x;
let is_dark = matrix
.get(idx)
.map(|c| *c == qrcode::Color::Dark)
.unwrap_or(false);
let color = if is_dark { foreground } else { background };
pixels.extend_from_slice(&color);
}
}
// Save as PNG
let file = File::create(output_path)?;
let ref mut w = BufWriter::new(file);
let mut encoder = Encoder::new(w, actual_size as u32, actual_size as u32);
encoder.set_color(ColorType::Rgb);
encoder.set_depth(BitDepth::Eight);
let mut writer = encoder.write_header()?;
writer.write_image_data(&pixels)?;
Ok(output_path.to_string())
}
/// Generate QR code with logo overlay
/// Note: Logo overlay requires the image crate. This simplified version
/// generates a QR code with a white center area where a logo can be placed manually.
pub fn generate_qr_code_with_logo(
data: &str,
size: u32,
_logo_path: &str,
output_path: &str,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
// Generate QR code with higher error correction for logo overlay
let code = QrCode::with_error_correction_level(data.as_bytes(), qrcode::EcLevel::H)?;
// Get the QR code as a matrix
let matrix = code.to_colors();
let qr_width = code.width();
// Calculate scale factor
let scale = (size as usize) / qr_width;
let actual_size = qr_width * scale;
// Calculate logo area (center 20% of the QR code)
let logo_size = actual_size / 5;
let logo_start = (actual_size - logo_size) / 2;
let logo_end = logo_start + logo_size;
// Create RGBA pixel buffer (4 bytes per pixel)
let mut pixels: Vec<u8> = Vec::with_capacity(actual_size * actual_size * 4);
for y in 0..actual_size {
for x in 0..actual_size {
// Check if we're in the logo area
if x >= logo_start && x < logo_end && y >= logo_start && y < logo_end {
// White background for logo area
pixels.extend_from_slice(&[255, 255, 255, 255]);
} else {
let qr_x = x / scale;
let qr_y = y / scale;
let idx = qr_y * qr_width + qr_x;
let is_dark = matrix
.get(idx)
.map(|c| *c == qrcode::Color::Dark)
.unwrap_or(false);
if is_dark {
pixels.extend_from_slice(&[0, 0, 0, 255]);
} else {
pixels.extend_from_slice(&[255, 255, 255, 255]);
}
}
}
}
// Save as PNG
let file = File::create(output_path)?;
let ref mut w = BufWriter::new(file);
let mut encoder = Encoder::new(w, actual_size as u32, actual_size as u32);
encoder.set_color(ColorType::Rgba);
encoder.set_depth(BitDepth::Eight);
let mut writer = encoder.write_header()?;
writer.write_image_data(&pixels)?;
// Note: Logo overlay not supported without image crate
// The QR code has a white center area where a logo can be placed manually
trace!("QR code with logo placeholder generated: {}", output_path);
Ok(output_path.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_qr_code_generation() {
// Create a mock state and user for testing
// In real tests, you'd set up proper test fixtures
let result = QrCode::new(b"https://example.com");
assert!(result.is_ok());
}
#[test]
fn test_qr_code_with_unicode() {
let result = QrCode::new("Hello 世界 🌍".as_bytes());
assert!(result.is_ok());
}
#[test]
fn test_qr_code_long_data() {
let long_data = "A".repeat(1000);
let result = QrCode::new(long_data.as_bytes());
assert!(result.is_ok());
}
#[test]
fn test_qr_code_url() {
let url = "https://example.com/path?param=value&other=123";
let result = QrCode::new(url.as_bytes());
assert!(result.is_ok());
}
#[test]
fn test_qr_code_json() {
let json = r#"{"id": 123, "name": "Test", "active": true}"#;
let result = QrCode::new(json.as_bytes());
assert!(result.is_ok());
}
}