diff --git a/Cargo.toml b/Cargo.toml index 3874a9f..c708564 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ uuid = { version = "1.6.1", features = ["v4", "fast-rng"] } md-5 = "0.10.6" # telegram provider teloxide = { version = "0.12.2", features = ["macros"], optional = true } +toml = "0.8.8" [features] default = [] diff --git a/src/main.rs b/src/main.rs index dfaa490..625b34a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,14 @@ use std::net::SocketAddr; use clap::{Parser, Subcommand}; -use crate::{storage::JsonFileStorageStrategy, routes::Context, notifications::console::ConsoleTracingNotifier}; +use crate::{storage::JsonFileStorageStrategy, routes::Context, notifications::console::ConsoleTracingNotifier, config::{Config, ConfigNotifier}}; mod notifications; mod routes; mod model; mod storage; +mod config; #[derive(Debug, Clone, Parser)] @@ -18,6 +19,10 @@ struct CliArgs { #[clap(subcommand)] action: CliAction, + /// connection string of storage database + #[arg(long, default_value = "./storage.json")] // file://./guestbook.db + db: String, + #[arg(long, default_value_t = false)] /// increase log verbosity to DEBUG level debug: bool, @@ -25,24 +30,19 @@ struct CliArgs { #[derive(Debug, Clone, Subcommand)] enum CliAction { + /// serve guestbook api Serve { - #[arg(long, short, default_value = "127.0.0.1:37812")] + #[arg(default_value = "127.0.0.1:37812")] /// host to bind onto addr: String, - #[arg(long)] - /// force public field content - public: Option, + #[arg(long, short)] + /// path to config file for overrides and notifiers + config: Option, + }, - #[arg(long)] - /// force author field content - author: Option, - } -} - -struct CliServeOverrides { - author: Option, - public: Option, + /// print a sample configuration, redirect to file and customize + Default, } #[tokio::main] @@ -51,20 +51,46 @@ async fn main() { tracing_subscriber::fmt::fmt() .with_max_level(if args.debug { tracing::Level::DEBUG } else { tracing::Level::INFO }) - .pretty() - .finish(); + .init(); + + // TODO more (and better) storage solutions! sqlx to the rescue... + let storage = Box::new(JsonFileStorageStrategy::new(&args.db)); match args.action { - CliAction::Serve { addr, public, author } => { + CliAction::Default => { + let mut cfg = Config::default(); + cfg.notifiers.push(ConfigNotifier::ConsoleNotifier); + #[cfg(feature = "telegram")] + cfg.notifiers.push(ConfigNotifier::TelegramNotifier { token: "asd".into(), chat_id: -1 }); + println!("{}", toml::to_string(&cfg).unwrap()); + }, + CliAction::Serve { addr, config } => { let addr : SocketAddr = addr.parse().expect("invalid host provided"); - let storage = Box::new(JsonFileStorageStrategy::new("./storage.json")); + let config = match config { + None => Config::default(), + Some(path) => { + let cfg_file = std::fs::read_to_string(path).unwrap(); + toml::from_str(&cfg_file).unwrap() + } + }; - let overrides = CliServeOverrides { author, public }; + let mut state = Context::new(storage, config.overrides); - let mut state = Context::new(storage, overrides); + for notifier in config.notifiers { + match notifier { + ConfigNotifier::ConsoleNotifier => { + state.register(Box::new(ConsoleTracingNotifier {})); + }, - state.register(Box::new(ConsoleTracingNotifier {})); + #[cfg(feature = "telegram")] + ConfigNotifier::TelegramNotifier { token, chat_id } => { + state.register(Box::new( + notifications::telegram::TGNotifier::new(&token, chat_id) + )); + }, + } + } let router = routes::create_router_with_app_routes(state); diff --git a/src/model.rs b/src/model.rs index d969dea..91319bd 100644 --- a/src/model.rs +++ b/src/model.rs @@ -3,6 +3,8 @@ use serde::{Serialize, Deserialize}; use chrono::{DateTime, Utc}; use uuid::Uuid; +use crate::config::ConfigOverrides; + const AUTHOR_MAX_CHARS: usize = 25; const CONTACT_MAX_CHARS: usize = 50; const BODY_MAX_CHARS: usize = 4096; @@ -70,6 +72,10 @@ pub struct PageInsertion { pub contact: Option, pub body: String, + + pub public: Option, + + pub date: Option>, } impl PageInsertion { @@ -79,15 +85,15 @@ impl PageInsertion { self.body = html_escape::encode_safe(&self.body.chars().take(BODY_MAX_CHARS).collect::()).to_string(); } - pub fn convert(mut self, overrides: &crate::CliServeOverrides) -> Page { + pub fn convert(mut self, overrides: &ConfigOverrides) -> Page { self.sanitize(); let mut page = Page { author: self.author.unwrap_or("".into()), contact: self.contact, body: self.body, - date: Utc::now(), - public: true, + date: self.date.unwrap_or(Utc::now()), + public: self.public.unwrap_or(true), }; if let Some(author) = &overrides.author { @@ -96,6 +102,13 @@ impl PageInsertion { if let Some(public) = overrides.public { page.public = public; } + if let Some(date) = &overrides.date { + if date.to_lowercase() == "now" { + page.date = Utc::now(); + } else { + page.date = DateTime::parse_from_rfc3339(date).unwrap().into(); + } + } page } diff --git a/src/routes.rs b/src/routes.rs index b1d9798..f758285 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use axum::{Json, Form, Router, routing::{put, post, get}, extract::{State, Query}, response::Redirect}; -use crate::{notifications::NotificationProcessor, model::{Page, PageOptions, PageInsertion}, storage::StorageStrategy, CliServeOverrides}; +use crate::{notifications::NotificationProcessor, model::{Page, PageOptions, PageInsertion}, storage::StorageStrategy, config::ConfigOverrides}; pub fn create_router_with_app_routes(state: Context) -> Router { Router::new() @@ -15,11 +15,11 @@ pub fn create_router_with_app_routes(state: Context) -> Router { pub struct Context { providers: Vec>>, storage: Box>, - overrides: CliServeOverrides, + overrides: ConfigOverrides, } impl Context { - pub fn new(storage: Box>, overrides: CliServeOverrides) -> Self { + pub fn new(storage: Box>, overrides: ConfigOverrides) -> Self { Context { providers: Vec::new(), storage, overrides } }