diff --git a/Cargo.lock b/Cargo.lock index bcafa54..19455cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -92,6 +92,21 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse 0.2.7", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstream" version = "1.0.0" @@ -99,7 +114,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", - "anstyle-parse", + "anstyle-parse 1.0.0", "anstyle-query", "anstyle-wincon", "colorchoice", @@ -113,6 +128,15 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + [[package]] name = "anstyle-parse" version = "1.0.0" @@ -535,11 +559,14 @@ dependencies = [ name = "bot" version = "0.1.0" dependencies = [ + "anyhow", "clap", "deltachat", + "env_logger", "eui48", "serde", "serde_yaml", + "tokio", ] [[package]] @@ -740,7 +767,7 @@ version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ - "anstream", + "anstream 1.0.0", "anstyle", "clap_lex", "strsim", @@ -1593,6 +1620,29 @@ dependencies = [ "syn", ] +[[package]] +name = "env_filter" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" +dependencies = [ + "anstream 0.6.21", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -2860,6 +2910,30 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jiff" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "js-sys" version = "0.3.91" @@ -3981,6 +4055,15 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +[[package]] +name = "portable-atomic-util" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +dependencies = [ + "portable-atomic", +] + [[package]] name = "portmapper" version = "0.5.0" diff --git a/Cargo.toml b/Cargo.toml index 26664e1..e8b82ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,11 @@ version = "0.1.0" edition = "2024" [dependencies] +anyhow = "1.0.102" clap = { version = "4", features = [ "derive" ] } deltachat = { path = "./chatmail-core" } +env_logger = "0.11.9" eui48 = { version = "1.1.0", features = [ "serde" ] } serde = { version = "1", features = [ "derive" ] } serde_yaml = { version = "0.9" } +tokio = { version = "1.50.0", features = ["full"] } diff --git a/src/handler.rs b/src/handler.rs new file mode 100644 index 0000000..bead3e2 --- /dev/null +++ b/src/handler.rs @@ -0,0 +1,136 @@ +use std::{collections::HashSet, sync::Arc}; + +use anyhow::Result as AnyhowResult; +use deltachat::{ + chat::{self, ChatId}, + contact::ContactId, + context::Context, + message::Message, +}; +use tokio::sync::Mutex; + +use crate::config::BotConfig; + +pub struct BotContext { + authed_contacts: HashSet, + config: BotConfig, +} + +impl BotContext { + pub fn new(config: BotConfig) -> BotContext { + BotContext { + authed_contacts: HashSet::new(), + config, + } + } +} + +pub async fn echo(dchat_ctx: Arc>, msg: Message) -> AnyhowResult<()> { + let chat_id = msg.get_chat_id(); + let dchat_ctx_lock = dchat_ctx.lock().await; + chat::send_text_msg(&dchat_ctx_lock, chat_id, msg.get_text()).await?; + Ok(()) +} + +pub async fn auth_command( + dchat_ctx: Arc>, + ctx: Arc>, + msg: Message, + args: &[&str], +) -> AnyhowResult<()> { + let chat_id = msg.get_chat_id(); + let contact_id = msg.get_from_id(); + + let mut ctx_lock = ctx.lock().await; + let dchat_ctx_lock = dchat_ctx.lock().await; + + if ctx_lock.authed_contacts.contains(&contact_id) { + chat::send_text_msg(&dchat_ctx_lock, chat_id, "Already authenticated".to_owned()).await?; + return Ok(()); + } + + let Some(password) = args.first() else { + chat::send_text_msg( + &dchat_ctx_lock, + chat_id, + "Usage: /auth ".to_owned(), + ) + .await?; + return Ok(()); + }; + + if *password == ctx_lock.config.auth.password { + ctx_lock.authed_contacts.insert(contact_id); + chat::send_text_msg( + &dchat_ctx_lock, + chat_id, + "Authentication successful!".to_owned(), + ) + .await?; + } else { + chat::send_text_msg(&dchat_ctx_lock, chat_id, "Incorrect password".to_owned()).await?; + } + + Ok(()) +} + +pub async fn wol_command( + dchat_ctx: Arc>, + ctx: Arc>, + msg: Message, + args: &[&str], +) -> AnyhowResult<()> { + let chat_id = msg.get_chat_id(); + let contact_id = msg.get_from_id(); + + let ctx_lock = ctx.lock().await; + let dchat_ctx_lock = dchat_ctx.lock().await; + + if !ensure_auth(&dchat_ctx_lock, &ctx_lock, chat_id, contact_id).await? { + return Ok(()); + } + + // TODO + + Ok(()) +} + +pub async fn ssh_unlock_disk_command( + dchat_ctx: Arc>, + ctx: Arc>, + msg: Message, + args: &[&str], +) -> AnyhowResult<()> { + let chat_id = msg.get_chat_id(); + let contact_id = msg.get_from_id(); + + let ctx_lock = ctx.lock().await; + let dchat_ctx_lock = dchat_ctx.lock().await; + + if !ensure_auth(&dchat_ctx_lock, &ctx_lock, chat_id, contact_id).await? { + return Ok(()); + } + + // TODO + + Ok(()) +} + +async fn ensure_auth( + dchat_ctx: &Context, + ctx: &BotContext, + chat_id: ChatId, + contact_id: ContactId, +) -> AnyhowResult { + if !ctx.authed_contacts.contains(&contact_id) { + chat::send_text_msg( + dchat_ctx, + chat_id, + "Authenticate yourself first with '/auth '.".to_owned(), + ) + .await?; + Ok(false) + } else { + Ok(true) + } +} diff --git a/src/main.rs b/src/main.rs index 9d838bd..071f74d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,25 @@ mod config; +mod handler; +use anyhow::{Context as _, Result as AnyhowResult}; use clap::Parser; -use std::path::PathBuf; +use deltachat::{ + EventType, Events, + config::Config, + context::Context, + message::{Message, MsgId}, + securejoin, + stock_str::StockStrings, +}; +use std::{path::PathBuf, sync::Arc}; +use tokio::sync::Mutex; + +use crate::handler::BotContext; const CONFIG_FILENAME: &str = "bot.yml"; const APP_NAME: &str = "deltachat-remotecontrol-bot"; const APP_CONFIG_DIR: &str = APP_NAME; +const APP_DATA_DIR: &str = APP_NAME; /// Delta Chat bot for remote control of local network machines. #[derive(Parser)] @@ -42,7 +56,124 @@ fn default_config_paths() -> Vec { paths } -fn main() { +fn data_path() -> PathBuf { + std::env::var("XDG_DATA_HOME") + .map(|dh| PathBuf::from(dh).join(APP_DATA_DIR)) + .or_else(|_| { + std::env::var("HOME").map(|h| { + PathBuf::from(h) + .join(".local") + .join("share") + .join(APP_DATA_DIR) + }) + }) + .unwrap_or(PathBuf::from(".")) +} + +async fn run_bot(cfg: config::BotConfig) -> AnyhowResult<()> { + let dchat_db_dir = data_path(); + std::fs::create_dir_all(&dchat_db_dir) + .with_context(|| format!("Failed to create data directory {}", dchat_db_dir.display()))?; + + let dchat_db_filename = dchat_db_dir.join("deltachat.sqlite"); + + let dchat_ctx = Context::new( + &dchat_db_filename, + 0, + Events::default(), + StockStrings::default(), + ) + .await + .context("Failed to open Delta Chat client DB")?; + + if !dchat_ctx.is_configured().await? { + dchat_ctx + .set_config(Config::Addr, Some(&cfg.delta_chat.email)) + .await?; + dchat_ctx + .set_config(Config::MailPw, Some(&cfg.delta_chat.password)) + .await?; + dchat_ctx.set_config(Config::Bot, Some("1")).await?; + dchat_ctx + .configure() + .await + .context("Failed to configure Delta Chat client")?; + } + + dchat_ctx.start_io().await; + + let invite_link = securejoin::get_securejoin_qr(&dchat_ctx, None) + .await + .context("Failed to generate invite link")?; + + println!("Add this bot to your Delta Chat contacts:"); + println!("{invite_link}"); + + let dchat_ctx = Arc::new(Mutex::new(dchat_ctx)); + + let bot_context = Arc::new(Mutex::new(BotContext::new(cfg))); + + let ev_emitter = dchat_ctx.lock().await.get_event_emitter(); + + while let Some(ev) = ev_emitter.recv().await { + match ev.typ { + EventType::IncomingMsg { chat_id: _, msg_id } => { + // let dchat_ctx = dchat_ctx.clone(); + // let bot_context = Arc::clone(&bot_context); + + if let Err(e) = handle_message(dchat_ctx.clone(), bot_context.clone(), msg_id).await + { + eprintln!("Error in message handler: {e:#}"); + } + } + _ => {} + } + } + + Ok(()) +} + +async fn handle_message( + dchat_ctx: Arc>, + bot_ctx: Arc>, + msg_id: MsgId, +) -> AnyhowResult<()> { + let dchat_ctx_lock = dchat_ctx.lock().await; + let msg = Message::load_from_db(&dchat_ctx_lock, msg_id).await?; + let text = msg.get_text(); + drop(dchat_ctx_lock); + + if !text.starts_with('/') { + handler::echo(dchat_ctx, msg).await?; + return Ok(()); + } + + let mut parts = text.split_whitespace(); + let Some(cmd) = parts.next() else { + return Ok(()); + }; + let args: Vec<&str> = parts.collect(); + + match cmd { + "/auth" => { + handler::auth_command(dchat_ctx, bot_ctx, msg, &args).await?; + } + "/wol" => { + handler::wol_command(dchat_ctx, bot_ctx, msg, &args).await?; + } + "/ssh-unlock-disk" => { + handler::ssh_unlock_disk_command(dchat_ctx, bot_ctx, msg, &args).await?; + } + _ => {} + } + + Ok(()) +} + +#[tokio::main] +async fn main() { + env_logger::init(); + let args = Args::parse(); let config_path: PathBuf = match args.config { @@ -71,6 +202,5 @@ fn main() { } }; - // TODO: start the bot using `config`. - let _ = config; + run_bot(config).await.expect("error"); }