diff --git a/src/main.rs b/src/main.rs index 24b8703..dd3b651 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,11 @@ mod config; mod entries; mod render; +mod telegram; mod web; use std::sync::Arc; +use teloxide::prelude::*; #[tokio::main] async fn main() { @@ -11,12 +13,28 @@ async fn main() { let config = config::Config::load("config.toml").expect("failed to load config.toml"); let listen = config.listen.clone(); + let entries_dir = config.data_dir.join("entries"); + let chat_id = ChatId(config.telegram_chat_id); - let (tx, _rx) = tokio::sync::mpsc::channel(32); + std::fs::create_dir_all(&entries_dir).ok(); + + let bot = Bot::new(&config.telegram_bot_token); + + let (tx, rx) = tokio::sync::mpsc::channel(32); let state = Arc::new(web::AppState { config, tx }); let app = web::router(state); + // Spawn telegram notification sender + let notify_bot = bot.clone(); + tokio::spawn(telegram::notification_task(notify_bot, chat_id, rx)); + + // Spawn telegram command listener + let cmd_bot = bot.clone(); + let cmd_entries_dir = entries_dir.clone(); + tokio::spawn(telegram::bot_task(cmd_bot, chat_id, cmd_entries_dir)); + + // Run web server tracing::info!("listening on {listen}"); let listener = tokio::net::TcpListener::bind(&listen).await.unwrap(); axum::serve(listener, app).await.unwrap(); diff --git a/src/telegram.rs b/src/telegram.rs new file mode 100644 index 0000000..0215cc2 --- /dev/null +++ b/src/telegram.rs @@ -0,0 +1,68 @@ +use std::path::PathBuf; + +use teloxide::prelude::*; +use tokio::sync::mpsc::Receiver; + +use crate::entries::{self, Entry, Status}; + +/// Send a notification to Telegram about a new entry. +async fn notify(bot: &Bot, chat_id: ChatId, entry: &Entry) { + let short_id = entry.id.split('-').last().unwrap_or(&entry.id); + let text = format!( + "New guestbook entry:\n\nName: {}\nWebsite: {}\n\n{}\n\n/allow_{}\n/deny_{}", + entry.meta.name, entry.meta.website, entry.body, short_id, short_id + ); + if let Err(e) = bot.send_message(chat_id, &text).await { + tracing::error!("failed to send telegram message: {e}"); + } +} + +/// Listen for new entries on the channel and send Telegram notifications. +pub async fn notification_task(bot: Bot, chat_id: ChatId, mut rx: Receiver) { + while let Some(entry) = rx.recv().await { + notify(&bot, chat_id, &entry).await; + } +} + +/// Run the Telegram bot that listens for /allow_ and /deny_ commands. +pub async fn bot_task(bot: Bot, chat_id: ChatId, entries_dir: PathBuf) { + let handler = Update::filter_message().endpoint( + |bot: Bot, msg: Message, entries_dir: PathBuf, chat_id: ChatId| async move { + let text = msg.text().unwrap_or(""); + // Only respond to the configured chat + if msg.chat.id != chat_id { + return respond(()); + } + + if let Some(id) = text.strip_prefix("/allow_") { + match entries::set_status(&entries_dir, id, Status::Approved) { + Ok(name) => { + bot.send_message(msg.chat.id, format!("Approved ({name}).")) + .await?; + } + Err(e) => { + bot.send_message(msg.chat.id, e).await?; + } + } + } else if let Some(id) = text.strip_prefix("/deny_") { + match entries::set_status(&entries_dir, id, Status::Denied) { + Ok(name) => { + bot.send_message(msg.chat.id, format!("Denied ({name}).")) + .await?; + } + Err(e) => { + bot.send_message(msg.chat.id, e).await?; + } + } + } + + Ok(()) + }, + ); + + Dispatcher::builder(bot, handler) + .dependencies(dptree::deps![entries_dir, chat_id]) + .build() + .dispatch() + .await; +}