use std::{ env, fs, path::{Path, PathBuf}, }; use anyhow::{bail, Context, Result}; use clap::Parser; use const_format::{formatcp, map_ascii_case, Case}; use log::info; use serde::Deserialize; use crate::{ actions::AnyActionConfig, blockchain::ElectrumConfig, message::MessageConfig, wallets::WalletConfig, }; #[derive(Parser, Debug)] #[command(version, about)] pub struct Args { /// Path to toml configuration file config: Option, /// Perform configured actions on a test notification #[arg(short, long)] test: bool, /// Notify for every past transaction (careful: if you have a long transaction history, this /// can SPAM your configured actions #[arg(short, long)] notify_past_txs: bool, } impl Args { pub fn config(&self) -> Option<&str> { self.config.as_deref() } pub fn test(&self) -> bool { self.test } pub fn notify_past_txs(&self) -> bool { self.notify_past_txs } } fn get_config_filename() -> &'static str { formatcp!("{}.toml", env!("CARGO_PKG_NAME")) } fn get_config_env_var() -> &'static str { formatcp!( "{}_CONFIG", map_ascii_case!(Case::Upper, env!("CARGO_PKG_NAME")) ) } fn get_cwd_config_path() -> PathBuf { PathBuf::from(".").join(get_config_filename()) } fn get_config_path_impl(user_config_dir: &Path) -> PathBuf { user_config_dir .join(env!("CARGO_PKG_NAME")) .join(get_config_filename()) } fn get_user_config_path() -> Option { dirs::config_dir().map(|p| get_config_path_impl(&p)) } fn get_system_config_path() -> PathBuf { get_config_path_impl(&systemd_directories::config_dir().unwrap_or(PathBuf::from("/etc"))) } fn get_config_path(maybe_arg_config: &Option<&str>) -> Result { if let Some(arg_path) = maybe_arg_config { return Ok(PathBuf::from(arg_path)); } if let Ok(env_path) = env::var(get_config_env_var()) { return Ok(PathBuf::from(env_path)); } let cwd_config_path = get_cwd_config_path(); if cwd_config_path.try_exists().is_ok_and(|x| x) { return Ok(cwd_config_path); } if let Some(user_config_path) = get_user_config_path() { if user_config_path.try_exists().is_ok_and(|x| x) { return Ok(user_config_path); } } let system_config_path = get_system_config_path(); if system_config_path.try_exists().is_ok_and(|x| x) { return Ok(system_config_path); } bail!( "no configuration file was passed as first argument, nor by the '{}' environment variable, nor did one exist in the default search paths: '{}', '{}', '{}'", get_config_env_var(), get_cwd_config_path().display(), get_user_config_path().unwrap_or_default().display(), get_system_config_path().display() ); } #[derive(Deserialize, Debug)] pub struct Config { wallets: Vec, #[serde(default)] electrum: ElectrumConfig, #[serde(default)] message: MessageConfig, #[serde(default)] actions: Vec, } impl Config { pub fn electrum(&self) -> &ElectrumConfig { &self.electrum } pub fn wallets(&self) -> &[WalletConfig] { &self.wallets } pub fn message(&self) -> &MessageConfig { &self.message } pub fn actions(&self) -> &[AnyActionConfig] { &self.actions } } pub fn get_config(maybe_arg_config: &Option<&str>) -> Result { let config_path = get_config_path(maybe_arg_config)?; info!("reading configuration from '{}'", config_path.display()); let config_content = fs::read_to_string(&config_path) .with_context(|| format!("could not read config file '{}'", config_path.display()))?; toml::from_str(&config_content) .with_context(|| format!("could not parse config file '{}'", config_path.display(),)) }