From fcd2c3c35d6fde32cd1e12519b3580dfbf9fb546 Mon Sep 17 00:00:00 2001 From: deepCurse Date: Tue, 25 Mar 2025 01:18:05 -0300 Subject: [PATCH] Polishing the turd --- src/constants.rs | 72 +++++++++++++++++++----- src/databases/mod.rs | 2 +- src/main.rs | 127 ++++++++++++++++++++++++++++--------------- 3 files changed, 142 insertions(+), 59 deletions(-) diff --git a/src/constants.rs b/src/constants.rs index c589ce9..52488c9 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,11 +1,12 @@ #![allow(unused)] -pub const VERSION: u16 = 0; +/// TODO semver +pub const VERSION: u16 = 20; // setup reminder or git hook of some kind to inc this? // This variable should never change for any reason, it will be used to identify this instance of the bot //pub const BOT_INTERNAL: &str = "npobot"; /// Should be limited to lowercase alphanumeric characters with periods, dashes, and underscores only -/// If you want to stylize the bot name use the fancy version +/// If you want to stylize the bot name use `BOT_NAME_FANCY` pub const BOT_NAME: &str = "nopalmo"; pub const BOT_NAME_FANCY: &str = "Nopalmo"; @@ -29,32 +30,77 @@ Nopalmo command help page: Description: Lorem Ipsum Dolar Sit Amet... -Arguments: - nodiscord Skips connecting to discord + See Usage section for more information about how this help menu functions + +Configuration: + print_help Prints this help information + h, help, -h, --help, -help, /h, /help, /?, ? + + nodiscord Shuts down the program when the first discord connection usually takes place Eventually this will connect to a simulated discord instance + -N, --no-discord dbuser The username to be used when connecting to a database type that requires one + -U, --db-user dbpass The password used like dbuser + -P, --db-pass dbaddress The network address of the desired database + -A, --db-address dbport The port to use with dbaddress + -P, --db-port dbpath The path to a unix socket to substitute an address or the path to an SQLite database file - dbkind The kind of database we will be using + -F, --db-path + dbkind The kind of database we will be using, one of `sqlite` `mariadb` `postgresql` `access` + -D, --db-kind use_cli Enables a CLI management interface for the bot + -i, --interactive use_gui The GUI version of the CLI + -g, --gui - lua_engine_count The number of lua execution engines to create, This defaults to the number of cores the computer has - This allows executing multiple lua tasks at once, including user commands and timer tasks + lua_engine_count The number of lua execution engines to create, This defaults to the number of cores the computer has + This allows executing multiple lua tasks at once, including user commands and timer tasks + -l, --lua-engine-count - connect_remote Connect to a remote instance of the bot - host_remote Host an instance of the bot for remote connections - remote_address The remote address of the bot, or the address you wish to host the bot on - remote_port The port used like remote_address - remote_key The path or literal value of the key required to authenticate with the bot on a remote connection - remote_key_type Decides the key type for remote connections + log_level The level the logging system will use, one of `trace` `debug` `info` `warning` `error` + -L, --log-level + + connect_remote Connect to a remote instance of the bot + -C, --remote-connection + host_remote Host an instance of the bot for remote connections + -H, --host-remote-connection + + remote_address The remote address of the bot, or the address you wish to host the bot on + -a, --remote-address + remote_port The port used like remote_address + -p, --remote-port + remote_key The path of the key required to authenticate with the bot on a remote connection + The remote_key_type flag decides what this value is used for + -k, --remote-key + remote_key_type Decides the key type for remote connections, one of `database` `password` or default `file` + -K, --remote-key-type Remote Connections: Remote access is disabled by default and must be enabled via the `connect_remote` or `host_remote` flags A key is required for remote connections, if you just need something quick use the `password` key type, the password must be the same for both server and client The key can be managed by the database as well, and the key type `database` will disable the `remote_key` variable and instead load authentication information from the server + +Usage: + ./nopalmo --help + ./nopalmo -H -a 0.0.0.0 -p 2022 -K file -k ./private.key + ./nopalmo -C -a 127.0.0.1 -p 2022 -K file -k ./public.key + + The Configuration section is layout like this: + Name Description + Commandline opts Option arguments + + Each configurable value can be changed in multiple ways for ease of use by the user + The order of configuration application is like so: + 1. System Environment + 2. .env file (if enabled by your maintainer using the dot_env_file feature) + 3. Configuration file (currently not supported) + 4. Commandline options + +Environment Variables: + Environment Variables may be set in addition or in place of commandline arguments "; diff --git a/src/databases/mod.rs b/src/databases/mod.rs index 54b07db..93e56a7 100644 --- a/src/databases/mod.rs +++ b/src/databases/mod.rs @@ -11,7 +11,7 @@ pub mod mariadb; pub mod postgres; pub mod sqlite; -#[derive(Debug)] +#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)] pub enum DBKind { Access, SQLite, diff --git a/src/main.rs b/src/main.rs index 661e6f1..36aa520 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,6 +23,7 @@ use tracing::debug; use tracing::error; use tracing::info; use tracing::metadata::LevelFilter; +use tracing::trace; use tracing::warn; use constants::HELP_STRING; @@ -52,11 +53,27 @@ fn string_to_log_level(str: String) -> Option { "info" => Some(tracing_subscriber::filter::LevelFilter::INFO), "warn" => Some(tracing_subscriber::filter::LevelFilter::WARN), "error" => Some(tracing_subscriber::filter::LevelFilter::ERROR), - _ => None, + _ => { + error!("Invalid log level! {str} Choose one of `trace` `debug` `info` `warn` `error`"); + None + }, + } +} +fn string_to_db_kind(str: String) -> Option { + match str.to_lowercase().trim() { + "access" => Some(DBKind::Access), + "postgresql" => Some(DBKind::PostgreSQL), + "sqlite" => Some(DBKind::SQLite), + "mariadb" => Some(DBKind::MariaDB), + _ => { + error!("Invalid database kind! {str} Choose one of `sqlite` `mariadb` `postgresql` `access`"); + None + }, } } /// ### This macro has a "header" of sorts which is used to configure the macro +/// The variables will also be initialized with the system enviroment values, so try not to use unwrap or expect, use a sane default value /// --- /// This is the first line of an example usage /// ``` @@ -145,9 +162,10 @@ macro_rules! program_properties { } $(#[$fattr])* $fvis fn $fident() -> $sident { - info!("Initializing with env variables."); + debug!("Initializing with env variables."); let mut resval = $sident { $($itemident: { + trace!("Reading env: {}", stringify!($itemident)); #[allow(unused)] let $vname = std::env::var(stringify!($itemident)).ok(); $envfixer @@ -155,7 +173,7 @@ macro_rules! program_properties { }; #[cfg(feature = "dot_env_file")] { - info!("Grabbing .env file input."); + debug!("Grabbing .env file input."); if let Ok(cdir) = std::env::current_dir() { if let Ok(string) = std::fs::read_to_string(cdir.join(".env")) { @@ -185,6 +203,7 @@ macro_rules! program_properties { for (lhs,rhs) in eqvec { match lhs.trim() { $(stringify!($itemident) => { + trace!("Reading .env file: {}", stringify!($itemident)); #[allow(unused)] let $vname = Some(rhs.to_string()); resval.$itemident = $envfixer; @@ -202,7 +221,7 @@ macro_rules! program_properties { } - info!("Grabbing commandline options."); + debug!("Grabbing commandline options."); let mut args = std::env::args().peekable(); // The first arg is the path to the executable on all systems that i know of @@ -217,6 +236,7 @@ macro_rules! program_properties { while let Some(item) = args.next() { match item.trim() { $($($options)|* | stringify!($itemident) => { + trace!("Reading commandline options: {}", stringify!($itemident)); #[allow(unused)] let $vargsname = &mut args; resval.$itemident = $argumentfixer; @@ -247,63 +267,71 @@ program_properties! { envfixer: item.is_some_and(string_to_bool), argumentfixer: true; pub nodiscord: bool, - optnames: [], + optnames: ["-N", "--no-discord"], envfixer: item.is_some_and(string_to_bool), argumentfixer: true; pub dbuser : Option, - optnames: [], + optnames: ["-U", "--db-user"], envfixer: item, argumentfixer: arglist.next(); pub dbpass : Option, - optnames: [], + optnames: ["-P","--db-pass"], envfixer: item, argumentfixer: arglist.next(); pub dbaddress : Option, - optnames: [], + optnames: ["-A", "--db-address"], envfixer: item, argumentfixer: arglist.next(); pub dbport: Option, - optnames: [], + optnames: ["-P", "--db-port"], envfixer: item, argumentfixer: arglist.next(); pub dbpath: Option, - optnames: [], - envfixer: item, - argumentfixer: arglist.next(); - pub dbkind: Option, - optnames: [], + optnames: ["-F","--db-path"], envfixer: item, argumentfixer: arglist.next(); + pub dbkind: Option, + optnames: ["-D", "--db-kind"], + envfixer: item.and_then(string_to_db_kind), + argumentfixer: arglist.next().and_then(string_to_db_kind); pub use_cli: bool, - optnames: [], - envfixer: item.is_some_and(string_to_bool), + optnames: ["-i", "--interactive"], + envfixer: item.is_some_and(string_to_bool), + argumentfixer: true; + pub use_gui: bool, + optnames: ["-g", "--gui"], + envfixer: item.is_some_and(string_to_bool), argumentfixer: true; pub lua_engine_count: Option, - optnames: [], + optnames: ["-l", "--lua-engine-count"], envfixer: item, argumentfixer: arglist.next(); pub log_level: Option, - optnames: [], - envfixer: string_to_log_level(item.unwrap_or_default()), - argumentfixer: string_to_log_level(arglist.next().unwrap_or_default()); + optnames: ["-L", "--log-level"], + envfixer: item.and_then(string_to_log_level), + argumentfixer: arglist.next().and_then(string_to_log_level); pub connect_remote: bool, - optnames: [], + optnames: ["-C", "--remote-connection"], + envfixer: item.is_some_and(string_to_bool), + argumentfixer: true; + pub host_remote: bool, + optnames: ["-H", "--host-remote-connection"], envfixer: item.is_some_and(string_to_bool), argumentfixer: true; pub remote_address: Option, - optnames: [], + optnames: ["-a", "--remote-address"], envfixer: item, argumentfixer: arglist.next(); pub remote_port: Option, - optnames: [], + optnames: ["-p", "--remote-port"], envfixer: item, argumentfixer: arglist.next(); pub remote_key: Option, - optnames: [], + optnames: ["-k", "--remote-key"], envfixer: item, argumentfixer: arglist.next(); pub remote_key_type: Option, - optnames: [], + optnames: ["-K", "--remote-key-type"], envfixer: item, argumentfixer: arglist.next(); } @@ -328,8 +356,22 @@ pub fn shutdown_handler() { fn main() { // why do we need to set global default again? - let (level_filter, level_filter_reload_handle) = - tracing_subscriber::reload::Layer::new(if cfg!(debug_assertions) { LevelFilter::DEBUG } else { LevelFilter::INFO }); + // TODO ansi color support + + let (level_filter, level_filter_reload_handle) = tracing_subscriber::reload::Layer::new({ + if let Ok(var) = std::env::var("log_level") { + let Some(var) = string_to_log_level(var) else { + eprintln!("CRITICAL: Could not read `log_level` system environment variable for initialization phase!"); + eprintln!("CRITICAL: The value must be one of `trace` `debug` `info` `warn` `error` or it must not be present!"); + exit(-1); + }; + var + } else if cfg!(debug_assertions) { + LevelFilter::DEBUG + } else { + LevelFilter::INFO + } + }); { use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; @@ -351,17 +393,23 @@ fn main() { .init(); } + // TODO set logging output to a file via a reload handle like above, store logged data and flush to file when file is registered and opened for writing as to not lose any data + // Manually init the lazy lock here rather than later for reliability sake LazyLock::force(&PROPERTIES); + #[cfg(debug_assertions)] debug!("{:?}", *PROPERTIES); - if let Some(val) = PROPERTIES.log_level { - level_filter_reload_handle.modify(|filter| *filter = val).unwrap(); + // TODO make this a global function + if let Some(nval) = PROPERTIES.log_level { + if let Some(cval) = level_filter_reload_handle.clone_current() { + if cval != nval { + trace!("Switching from log level {cval} to {nval}"); + level_filter_reload_handle.modify(|filter| *filter = nval).unwrap(); + } + } } - // Store non fatal errors from behind this point to be logged and handled afterwards, for example ansi failures, log file failures, or invalid properties - // It is assumed there is no failure state before this point, if something goes wrong, shamble onwards - let (shutdown_tx, shutdown_rx) = std::sync::mpsc::channel(); #[allow(clippy::unwrap_used)] // it is impossible for this to be an error SHUTDOWN_SENDER.set(shutdown_tx.clone()).unwrap(); @@ -377,23 +425,12 @@ fn main() { exit(0); } - let dbkind = if let Some(arg) = &PROPERTIES.dbkind { - info!("Loading database accessor."); - match arg.to_lowercase().trim() { - "access" => DBKind::Access, - "postgresql" => DBKind::PostgreSQL, - "sqlite" => DBKind::SQLite, - "mariadb" => DBKind::MariaDB, - _ => { - error!("Invalid database kind! Choose one of `sqlite` `mariadb` `postgresql` `access`"); - exit(-1); - }, - } - } else { + let Some(dbkind) = PROPERTIES.dbkind else { warn!("You must select a database kind with `dbkind`"); exit(-1); }; + info!("Loading database accessor."); if let Err(err) = match dbkind { DBKind::Access => databases::access::create_interface(), DBKind::MariaDB => databases::mariadb::create_interface(),