cargo: gate telegram behind a default feature

This commit is contained in:
Lewis Wynne 2026-04-10 19:06:08 +01:00
parent de53e51c60
commit 9f0e3aae6d
6 changed files with 45 additions and 15 deletions

View file

@ -6,10 +6,14 @@ description = "A configurable, self-hosted guestbook for the web, allowing visit
license = "MIT" license = "MIT"
repository = "https://git.ily.rs/lew/guestbook" repository = "https://git.ily.rs/lew/guestbook"
[features]
default = ["telegram"]
telegram = ["dep:teloxide"]
[dependencies] [dependencies]
axum = "0.8" axum = "0.8"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "sync", "net"] } tokio = { version = "1", features = ["macros", "rt-multi-thread", "sync", "net"] }
teloxide = { version = "0.13", features = ["macros"] } teloxide = { version = "0.13", features = ["macros"], optional = true }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
toml = "0.8" toml = "0.8"
dotenvy = "0.15" dotenvy = "0.15"
@ -19,6 +23,9 @@ base64 = "0.22"
tracing = "0.1" tracing = "0.1"
tracing-subscriber = "0.3" tracing-subscriber = "0.3"
[profile.dev.package."*"]
opt-level = 1
[dev-dependencies] [dev-dependencies]
tower = { version = "0.5", features = ["util"] } tower = { version = "0.5", features = ["util"] }
http-body-util = "0.1" http-body-util = "0.1"

View file

@ -7,7 +7,9 @@ pub struct Config {
pub data_dir: PathBuf, pub data_dir: PathBuf,
pub site_title: String, pub site_title: String,
#[cfg(feature = "telegram")]
pub telegram_bot_token: Option<String>, pub telegram_bot_token: Option<String>,
#[cfg(feature = "telegram")]
pub telegram_chat_id: Option<i64>, pub telegram_chat_id: Option<i64>,
pub enable_honeypot: bool, pub enable_honeypot: bool,
pub max_name_length: usize, pub max_name_length: usize,
@ -67,7 +69,9 @@ impl Config {
.unwrap_or_else(|_| PathBuf::from("./data")), .unwrap_or_else(|_| PathBuf::from("./data")),
site_title: env::var("BOOK_SITE_TITLE").unwrap_or_else(|_| "guestbook".into()), site_title: env::var("BOOK_SITE_TITLE").unwrap_or_else(|_| "guestbook".into()),
#[cfg(feature = "telegram")]
telegram_bot_token: env::var("BOOK_TELEGRAM_BOT_TOKEN").ok(), telegram_bot_token: env::var("BOOK_TELEGRAM_BOT_TOKEN").ok(),
#[cfg(feature = "telegram")]
telegram_chat_id: env::var("BOOK_TELEGRAM_CHAT_ID") telegram_chat_id: env::var("BOOK_TELEGRAM_CHAT_ID")
.ok() .ok()
.map(|v| v.parse().map_err(|_| "BOOK_TELEGRAM_CHAT_ID must be an integer")) .map(|v| v.parse().map_err(|_| "BOOK_TELEGRAM_CHAT_ID must be an integer"))
@ -188,7 +192,9 @@ mod tests {
assert_eq!(config.listen_addr(), "127.0.0.1:9999"); assert_eq!(config.listen_addr(), "127.0.0.1:9999");
assert_eq!(config.data_dir, PathBuf::from("/tmp/gb")); assert_eq!(config.data_dir, PathBuf::from("/tmp/gb"));
assert_eq!(config.site_title, "test.rs"); assert_eq!(config.site_title, "test.rs");
#[cfg(feature = "telegram")]
assert_eq!(config.telegram_bot_token.as_deref(), Some("123:ABC")); assert_eq!(config.telegram_bot_token.as_deref(), Some("123:ABC"));
#[cfg(feature = "telegram")]
assert_eq!(config.telegram_chat_id, Some(12345)); assert_eq!(config.telegram_chat_id, Some(12345));
// Clean up // Clean up
@ -221,7 +227,9 @@ mod tests {
env::remove_var("BOOK_TELEGRAM_CHAT_ID"); env::remove_var("BOOK_TELEGRAM_CHAT_ID");
let config = Config::from_env().unwrap(); let config = Config::from_env().unwrap();
#[cfg(feature = "telegram")]
assert!(config.telegram_bot_token.is_none()); assert!(config.telegram_bot_token.is_none());
#[cfg(feature = "telegram")]
assert!(config.telegram_chat_id.is_none()); assert!(config.telegram_chat_id.is_none());
} }

View file

@ -24,6 +24,7 @@ pub struct EntryMeta {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Entry { pub struct Entry {
#[cfg_attr(not(feature = "telegram"), allow(dead_code))]
pub id: String, pub id: String,
pub meta: EntryMeta, pub meta: EntryMeta,
pub body: String, pub body: String,
@ -49,6 +50,7 @@ impl Entry {
}) })
} }
#[cfg(any(feature = "telegram", test))]
/// Return the short ID (UUID portion after the underscore). /// Return the short ID (UUID portion after the underscore).
pub fn short_id(&self) -> &str { pub fn short_id(&self) -> &str {
self.id.split('_').last().unwrap_or(&self.id) self.id.split('_').last().unwrap_or(&self.id)
@ -103,6 +105,7 @@ pub fn read_approved(dir: &Path) -> Vec<Entry> {
read_by_status(dir, Status::Approved) read_by_status(dir, Status::Approved)
} }
#[cfg(any(feature = "telegram", test))]
/// Find a single entry by short ID (the UUID portion after the underscore). /// Find a single entry by short ID (the UUID portion after the underscore).
pub fn find_entry(dir: &Path, short_id: &str) -> Result<Entry, String> { pub fn find_entry(dir: &Path, short_id: &str) -> Result<Entry, String> {
let read_dir = std::fs::read_dir(dir).map_err(|e| e.to_string())?; let read_dir = std::fs::read_dir(dir).map_err(|e| e.to_string())?;
@ -119,6 +122,7 @@ pub fn find_entry(dir: &Path, short_id: &str) -> Result<Entry, String> {
Err("Not found.".into()) Err("Not found.".into())
} }
#[cfg(any(feature = "telegram", test))]
/// Delete an entry and its associated media files. /// Delete an entry and its associated media files.
/// `data_dir` is the parent directory containing entries/, drawings/, and voice_notes/. /// `data_dir` is the parent directory containing entries/, drawings/, and voice_notes/.
pub fn delete_entry(data_dir: &Path, short_id: &str) -> Result<String, String> { pub fn delete_entry(data_dir: &Path, short_id: &str) -> Result<String, String> {
@ -148,6 +152,7 @@ pub fn delete_entry(data_dir: &Path, short_id: &str) -> Result<String, String> {
Ok(entry.meta.name.clone()) Ok(entry.meta.name.clone())
} }
#[cfg(any(feature = "telegram", test))]
/// Find an entry file by short ID prefix and update its status. /// Find an entry file by short ID prefix and update its status.
pub fn set_status(dir: &Path, short_id: &str, status: Status) -> Result<String, String> { pub fn set_status(dir: &Path, short_id: &str, status: Status) -> Result<String, String> {
let mut entry = find_entry(dir, short_id)?; let mut entry = find_entry(dir, short_id)?;

View file

@ -1,11 +1,11 @@
mod config; mod config;
mod entries; mod entries;
mod render; mod render;
#[cfg(feature = "telegram")]
mod telegram; mod telegram;
mod web; mod web;
use std::sync::Arc; use std::sync::Arc;
use teloxide::prelude::*;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
@ -18,16 +18,18 @@ async fn main() {
std::fs::create_dir_all(&entries_dir).expect("failed to create entries directory"); std::fs::create_dir_all(&entries_dir).expect("failed to create entries directory");
let (tx, rx) = tokio::sync::mpsc::channel::<(entries::Entry, Option<Vec<u8>>, Option<Vec<u8>>)>(32); let (tx, _rx) = tokio::sync::mpsc::channel::<(entries::Entry, Option<Vec<u8>>, Option<Vec<u8>>)>(32);
// Spawn telegram tasks if configured #[cfg(feature = "telegram")]
{
use teloxide::prelude::*;
match (&config.telegram_bot_token, config.telegram_chat_id) { match (&config.telegram_bot_token, config.telegram_chat_id) {
(Some(token), Some(chat_id)) => { (Some(token), Some(chat_id)) => {
let chat_id = ChatId(chat_id); let chat_id = ChatId(chat_id);
let bot = Bot::new(token); let bot = Bot::new(token);
let notify_bot = bot.clone(); let notify_bot = bot.clone();
tokio::spawn(telegram::notification_task(notify_bot, chat_id, rx)); tokio::spawn(telegram::notification_task(notify_bot, chat_id, _rx));
let cmd_data_dir = config.data_dir.clone(); let cmd_data_dir = config.data_dir.clone();
tokio::spawn(telegram::bot_task(bot, chat_id, cmd_data_dir)); tokio::spawn(telegram::bot_task(bot, chat_id, cmd_data_dir));
@ -36,6 +38,10 @@ async fn main() {
tracing::info!("telegram not configured, moderation notifications disabled"); tracing::info!("telegram not configured, moderation notifications disabled");
} }
} }
}
#[cfg(not(feature = "telegram"))]
tracing::info!("compiled without telegram support");
let state = Arc::new(web::AppState { config, tx }); let state = Arc::new(web::AppState { config, tx });
let app = web::router(state); let app = web::router(state);

View file

@ -322,7 +322,9 @@ mod tests {
data_dir: PathBuf::from("./data"), data_dir: PathBuf::from("./data"),
site_title: "test".into(), site_title: "test".into(),
#[cfg(feature = "telegram")]
telegram_bot_token: None, telegram_bot_token: None,
#[cfg(feature = "telegram")]
telegram_chat_id: None, telegram_chat_id: None,
enable_honeypot: true, enable_honeypot: true,
max_name_length: 0, max_name_length: 0,

View file

@ -345,7 +345,9 @@ mod tests {
data_dir: dir.to_path_buf(), data_dir: dir.to_path_buf(),
site_title: "test".into(), site_title: "test".into(),
#[cfg(feature = "telegram")]
telegram_bot_token: None, telegram_bot_token: None,
#[cfg(feature = "telegram")]
telegram_chat_id: None, telegram_chat_id: None,
enable_honeypot: true, enable_honeypot: true,
max_name_length: 0, max_name_length: 0,