//! Payphone's main entry point. //! //! This module is responsible for parsing command line arguments, nothing else. //! //! # Command Line Arguments //! //! The command line arguments are parsed using the [`clap`] crate, and can be found in the //! [`Args`] struct. The arguments are: //! //! - `[TOKEN]`: The Discord bot token to run the bot with. This is required. //! - `--logging` or `-l`: The minimum level logs have to be to be outputted into the console, or //! off for no logs. Defaults to `info`. //! - `--database-addr` or `-d`: The address of the database to connect to, a Scylla node. //! //! The CLI also supports setting argument values via environment variables. The environment //! variables are prefixed with `PAYPHONE_`, so for example, the `--token` argument can be set //! using the `PAYPHONE_TOKEN` environment variable. The environment variables are used to //! override the command line arguments. `.env` files are also supported by default, so you can //! create a `.env` file in the root of the project with the following content: //! //! ```env //! PAYPHONE_TOKEN="your_discord_bot_token" //! PAYPHONE_LOGGING="info" //! PAYPHONE_DATABASE_ADDR="your_database_addr" //! ``` //! //! # Parsing Arguments //! //! The arguments are parsed using the `Args::parse()` method, which returns an instance of the //! [`Args`] struct. This struct contains the parsed values of the command line arguments, and //! can be used to initialize the bot and start the event loop. //! //! ``` //! // Read the environment variables from the `.env` file, if it exists, and load them as proper //! // env variables. //! dotenvy::dotenv().ok(); //! //! let args = Args::parse(); //! ``` //! //! # Running the Bot //! //! To start the bot, run: //! //! ```shell //! $ cargo run -- start [TOKEN] //! ``` //! //! To run multiple shards, pass the `--shard` or `-s` argument to the `start` command with the //! shard ID and total amount of shards as `id:total`. For example, for a single-shard bot, you'd //! set it to `-s 0:1`. ID is zero-indexed (0 means first shard), total is one-indexed (0 means no //! shards). //! //! # Running the Documentation Server //! //! Payphone has a built-in documentation server that serves the project's documentation and //! refreshes it on changes. To start the documentation server, run the following command: //! //! ```shell //! $ cargo run -- docs --open //! ``` //! //! The `--open` flag is optional and will open the documentation in the default browser after //! starting the server. You can also use the `--bind` argument to specify the address to bind the //! server to, which defaults to `0.0.0.0:3000`. use std::{fmt, net::SocketAddr, str::FromStr}; use twilight_gateway::ShardId; /// Payphone's argument parser. #[derive(clap::Parser)] #[command(name = "Payphone CLI")] #[command(version, about = "Payphone's command line interface.")] pub struct Args { /// The minimum level logs have to be to be outputted into the console, or off for no logs. #[arg( long, short, value_enum, default_value_t = LevelFilter::Info, env = "PAYPHONE_LOGGING", value_name = "LEVEL", long_help = "The minimum level logs have to be to be outputted into the console, or off for no logs." )] pub logging: LevelFilter, #[command(subcommand)] pub command: Option, } /// An enum with the different logging levels that can be used in the CLI. /// /// This enum is used to parse the `--logging` or `-l` command line argument and then map the value /// to [`log::LevelFilter`]. #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)] pub enum LevelFilter { /// No logs at all. Off, /// The most verbose level, used for debugging purposes. Trace, /// Events that are useful to be displayed during development, but not in production. Debug, /// Used for general information about the bot's state and milestones. Info, /// Used for warnings, when something unexpected happens but the bot can still continue /// running. Warn, /// Used for errors, when something goes wrong and the bot cannot continue running. Error, } #[allow(clippy::from_over_into)] impl Into for LevelFilter { fn into(self) -> log::LevelFilter { match self { Self::Off => log::LevelFilter::Off, Self::Trace => log::LevelFilter::Trace, Self::Debug => log::LevelFilter::Debug, Self::Info => log::LevelFilter::Info, Self::Warn => log::LevelFilter::Warn, Self::Error => log::LevelFilter::Error, } } } #[derive(Debug, Clone)] pub struct Shard { pub id: u32, pub total: u32, } impl Shard { fn new(id: u32, total: u32) -> Self { Shard { id, total } } } impl Default for Shard { fn default() -> Self { Shard::new(0, 1) } } impl FromStr for Shard { type Err = &'static str; fn from_str(s: &str) -> Result { match s.split_once(":") { Some((id, total)) => { let id = id.parse().map_err(|_| "Invalid shard ID")?; let total = total.parse().map_err(|_| "Invalid shard total")?; Ok(Shard { id, total }) } None => Err("Invalid shard format, expected `id:total`"), } } } impl fmt::Display for Shard { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}:{}", self.id, self.total) } } #[allow(clippy::from_over_into)] impl Into for Shard { fn into(self) -> ShardId { ShardId::new(self.id, self.total) } } /// The available commands for the Payphone CLI and their available arguments. #[derive(clap::Subcommand, Clone)] pub enum Commands { /// Starts the bot with the provided token. Start { /// The Discord bot token to run the bot with. #[arg(env = "PAYPHONE_TOKEN")] token: String, /// The socket address of the database to connect to, a Scylla node. #[arg( long, short, env = "PAYPHONE_DATABASE_ADDR", value_name = "ADDR", default_value_t = String::from("127.0.0.1:9042")) ] database_addr: String, /// The username to use when connecting to the database. #[arg(long, env = "PAYPHONE_DATABASE_USERNAME", value_name = "USERNAME")] database_username: Option, /// The password to use when connecting to the database. #[arg(long, env = "PAYPHONE_DATABASE_PASSWORD", value_name = "PASSWORD")] database_password: Option, /// The current shard being run. #[arg( long, short, env = "PAYPHONE_SHARD", value_name = "ID:TOTAL", default_value_t = Shard::default()) ] shard: Shard, /// The Redis URLs to connect to. #[arg( long, env = "PAYPHONE_REDIS_URLS", value_name = "URLS", default_values = &["redis://127.0.0.1:6379"], )] redis_urls: Vec, /// The bot owner's ID, this user will be allowed to assign moderators. #[arg( long, env = "PAYPHONE_OWNER_ID", value_name = "ID", default_value_t = 0 )] bot_owner_id: u64, }, /// Starts a documentation server for the project which serves `cargo doc` and refreshes on /// changes. Docs { /// The address to run the documentation server on. #[arg( long, short, env = "PAYPHONE_DOCS_ADDR", value_name = "ADDR", default_value_t = SocketAddr::from(([0, 0, 0, 0], 3000)) )] bind: SocketAddr, /// When this flag is set, the documentation server will open the documentation in the /// default browser after starting the server. #[arg( long, short, env = "PAYPHONE_DOCS_OPEN", value_name = "OPEN", default_value_t = false, help = "Open the documentation in the default browser after starting the server." )] open: bool, }, }