guestbook/src/config.rs
2026-04-09 22:17:01 +01:00

283 lines
11 KiB
Rust

use std::env;
use std::path::PathBuf;
#[derive(Debug)]
pub struct Config {
pub port: u16,
pub data_dir: PathBuf,
pub site_title: String,
pub telegram_bot_token: Option<String>,
pub telegram_chat_id: Option<i64>,
pub enable_honeypot: bool,
pub max_name_length: usize,
pub max_message_length: usize,
pub max_website_length: usize,
pub enable_submissions: bool,
pub enable_website_links: bool,
pub enable_html_injection: bool,
pub enable_captcha: bool,
pub captcha_question: String,
pub captcha_answer: String,
pub captcha_exact: bool,
pub captcha_casesensitive: bool,
pub enable_drawings: bool,
pub label_drawing: String,
pub canvas_width: u32,
pub canvas_height: u32,
pub template: Option<String>,
pub separator: String,
pub style: String,
pub form_prompt: String,
pub button_text: String,
pub label_name: String,
pub label_website: String,
pub label_message: String,
pub textarea_rows: u32,
pub textarea_cols: u32,
}
impl Config {
pub fn listen_addr(&self) -> String {
format!("127.0.0.1:{}", self.port)
}
/// Maximum drawing file size: width * height * 4 (raw RGBA).
/// Any valid PNG from the configured canvas will be smaller than this.
pub fn max_drawing_bytes(&self) -> usize {
self.canvas_width as usize * self.canvas_height as usize * 4
}
pub fn from_env() -> Result<Self, String> {
Ok(Config {
port: env::var("BOOK_PORT")
.unwrap_or_else(|_| "8123".into())
.parse()
.map_err(|_| "BOOK_PORT must be a number")?,
data_dir: env::var("BOOK_DATA_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("./data")),
site_title: env::var("BOOK_SITE_TITLE").unwrap_or_else(|_| "guestbook".into()),
telegram_bot_token: env::var("BOOK_TELEGRAM_BOT_TOKEN").ok(),
telegram_chat_id: env::var("BOOK_TELEGRAM_CHAT_ID")
.ok()
.map(|v| v.parse().map_err(|_| "BOOK_TELEGRAM_CHAT_ID must be an integer"))
.transpose()?,
enable_honeypot: env::var("BOOK_ENABLE_HONEYPOT")
.map(|v| v != "false")
.unwrap_or(true),
max_name_length: env::var("BOOK_MAX_NAME_LENGTH")
.unwrap_or_else(|_| "0".into())
.parse()
.map_err(|_| "BOOK_MAX_NAME_LENGTH must be a number")?,
max_message_length: env::var("BOOK_MAX_MESSAGE_LENGTH")
.unwrap_or_else(|_| "0".into())
.parse()
.map_err(|_| "BOOK_MAX_MESSAGE_LENGTH must be a number")?,
max_website_length: env::var("BOOK_MAX_WEBSITE_LENGTH")
.unwrap_or_else(|_| "0".into())
.parse()
.map_err(|_| "BOOK_MAX_WEBSITE_LENGTH must be a number")?,
enable_submissions: env::var("BOOK_ENABLE_SUBMISSIONS")
.map(|v| v != "false")
.unwrap_or(true),
enable_website_links: env::var("BOOK_ENABLE_WEBSITE_LINKS")
.map(|v| v != "false")
.unwrap_or(true),
enable_html_injection: env::var("BOOK_ENABLE_HTML_INJECTION")
.map(|v| v != "false")
.unwrap_or(false),
enable_captcha: env::var("BOOK_ENABLE_CAPTCHA")
.map(|v| v != "false")
.unwrap_or(false),
captcha_question: env::var("BOOK_CAPTCHA_QUESTION")
.unwrap_or_default(),
captcha_answer: env::var("BOOK_CAPTCHA_ANSWER")
.unwrap_or_default(),
captcha_exact: env::var("BOOK_CAPTCHA_EXACT")
.map(|v| v != "false")
.unwrap_or(false),
captcha_casesensitive: env::var("BOOK_CAPTCHA_CASESENSITIVE")
.map(|v| v != "false")
.unwrap_or(false),
enable_drawings: env::var("BOOK_ENABLE_DRAWINGS")
.map(|v| v != "false")
.unwrap_or(false),
label_drawing: env::var("BOOK_LABEL_DRAWING")
.unwrap_or_else(|_| "Draw (optional):".into()),
canvas_width: env::var("BOOK_CANVAS_WIDTH")
.unwrap_or_else(|_| "400".into())
.parse()
.map_err(|_| "BOOK_CANVAS_WIDTH must be a number")?,
canvas_height: env::var("BOOK_CANVAS_HEIGHT")
.unwrap_or_else(|_| "200".into())
.parse()
.map_err(|_| "BOOK_CANVAS_HEIGHT must be a number")?,
separator: env::var("BOOK_SEPARATOR")
.unwrap_or_else(|_| "------------------------------------------------------------".into()),
template: env::var("BOOK_TEMPLATE").ok().map(|path| {
std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("failed to read template {path}: {e}"))
}),
style: env::var("BOOK_STYLE_FILE")
.ok()
.map(|path| {
std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("failed to read style file {path}: {e}"))
})
.or_else(|| env::var("BOOK_STYLE").ok())
.unwrap_or_default(),
form_prompt: env::var("BOOK_FORM_PROMPT")
.unwrap_or_else(|_| "Thanks for visiting. Sign the guestbook!".into()),
button_text: env::var("BOOK_BUTTON_TEXT")
.unwrap_or_else(|_| "sign".into()),
label_name: env::var("BOOK_LABEL_NAME")
.unwrap_or_else(|_| "Your name:".into()),
label_website: env::var("BOOK_LABEL_WEBSITE")
.unwrap_or_else(|_| "Your website (optional):".into()),
label_message: env::var("BOOK_LABEL_MESSAGE")
.unwrap_or_else(|_| "Your message:".into()),
textarea_rows: env::var("BOOK_TEXTAREA_ROWS")
.unwrap_or_else(|_| "8".into())
.parse()
.map_err(|_| "BOOK_TEXTAREA_ROWS must be a number")?,
textarea_cols: env::var("BOOK_TEXTAREA_COLS")
.unwrap_or_else(|_| "60".into())
.parse()
.map_err(|_| "BOOK_TEXTAREA_COLS must be a number")?,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn test_from_env() {
let _lock = ENV_LOCK.lock().unwrap();
env::set_var("BOOK_PORT", "9999");
env::set_var("BOOK_DATA_DIR", "/tmp/gb");
env::set_var("BOOK_SITE_TITLE", "test.rs");
env::set_var("BOOK_TELEGRAM_BOT_TOKEN", "123:ABC");
env::set_var("BOOK_TELEGRAM_CHAT_ID", "12345");
let config = Config::from_env().unwrap();
assert_eq!(config.port, 9999);
assert_eq!(config.listen_addr(), "127.0.0.1:9999");
assert_eq!(config.data_dir, PathBuf::from("/tmp/gb"));
assert_eq!(config.site_title, "test.rs");
assert_eq!(config.telegram_bot_token.as_deref(), Some("123:ABC"));
assert_eq!(config.telegram_chat_id, Some(12345));
// Clean up
env::remove_var("BOOK_PORT");
env::remove_var("BOOK_DATA_DIR");
env::remove_var("BOOK_SITE_TITLE");
env::remove_var("BOOK_TELEGRAM_BOT_TOKEN");
env::remove_var("BOOK_TELEGRAM_CHAT_ID");
}
#[test]
fn test_defaults() {
let _lock = ENV_LOCK.lock().unwrap();
env::set_var("BOOK_TELEGRAM_BOT_TOKEN", "123:ABC");
env::set_var("BOOK_TELEGRAM_CHAT_ID", "12345");
let config = Config::from_env().unwrap();
assert_eq!(config.port, 8123);
assert_eq!(config.data_dir, PathBuf::from("./data"));
assert_eq!(config.site_title, "guestbook");
env::remove_var("BOOK_TELEGRAM_BOT_TOKEN");
env::remove_var("BOOK_TELEGRAM_CHAT_ID");
}
#[test]
fn test_telegram_optional() {
let _lock = ENV_LOCK.lock().unwrap();
env::remove_var("BOOK_TELEGRAM_BOT_TOKEN");
env::remove_var("BOOK_TELEGRAM_CHAT_ID");
let config = Config::from_env().unwrap();
assert!(config.telegram_bot_token.is_none());
assert!(config.telegram_chat_id.is_none());
}
#[test]
fn test_enable_website_links_default() {
let _lock = ENV_LOCK.lock().unwrap();
env::set_var("BOOK_TELEGRAM_BOT_TOKEN", "123:ABC");
env::set_var("BOOK_TELEGRAM_CHAT_ID", "12345");
env::remove_var("BOOK_ENABLE_WEBSITE_LINKS");
let config = Config::from_env().unwrap();
assert!(config.enable_website_links);
env::remove_var("BOOK_TELEGRAM_BOT_TOKEN");
env::remove_var("BOOK_TELEGRAM_CHAT_ID");
}
#[test]
fn test_enable_website_links_false() {
let _lock = ENV_LOCK.lock().unwrap();
env::set_var("BOOK_TELEGRAM_BOT_TOKEN", "123:ABC");
env::set_var("BOOK_TELEGRAM_CHAT_ID", "12345");
env::set_var("BOOK_ENABLE_WEBSITE_LINKS", "false");
let config = Config::from_env().unwrap();
assert!(!config.enable_website_links);
env::remove_var("BOOK_TELEGRAM_BOT_TOKEN");
env::remove_var("BOOK_TELEGRAM_CHAT_ID");
env::remove_var("BOOK_ENABLE_WEBSITE_LINKS");
}
#[test]
fn test_enable_html_injection_default() {
let _lock = ENV_LOCK.lock().unwrap();
env::set_var("BOOK_TELEGRAM_BOT_TOKEN", "123:ABC");
env::set_var("BOOK_TELEGRAM_CHAT_ID", "12345");
env::remove_var("BOOK_ENABLE_HTML_INJECTION");
let config = Config::from_env().unwrap();
assert!(!config.enable_html_injection);
env::remove_var("BOOK_TELEGRAM_BOT_TOKEN");
env::remove_var("BOOK_TELEGRAM_CHAT_ID");
}
#[test]
fn test_enable_html_injection_true() {
let _lock = ENV_LOCK.lock().unwrap();
env::set_var("BOOK_TELEGRAM_BOT_TOKEN", "123:ABC");
env::set_var("BOOK_TELEGRAM_CHAT_ID", "12345");
env::set_var("BOOK_ENABLE_HTML_INJECTION", "true");
let config = Config::from_env().unwrap();
assert!(config.enable_html_injection);
env::remove_var("BOOK_TELEGRAM_BOT_TOKEN");
env::remove_var("BOOK_TELEGRAM_CHAT_ID");
env::remove_var("BOOK_ENABLE_HTML_INJECTION");
}
#[test]
fn test_enable_drawings_default() {
let _lock = ENV_LOCK.lock().unwrap();
env::remove_var("BOOK_ENABLE_DRAWINGS");
env::remove_var("BOOK_TELEGRAM_BOT_TOKEN");
env::remove_var("BOOK_TELEGRAM_CHAT_ID");
let config = Config::from_env().unwrap();
assert!(!config.enable_drawings);
assert_eq!(config.canvas_width, 400);
assert_eq!(config.canvas_height, 200);
assert_eq!(config.max_drawing_bytes(), 400 * 200 * 4);
assert_eq!(config.label_drawing, "Draw (optional):");
}
}