Add docs server

This commit is contained in:
2025-08-10 22:50:55 -03:00
parent 1f002cb7ca
commit edeaa1335f
5 changed files with 419 additions and 27 deletions
+55 -18
View File
@@ -23,28 +23,26 @@
//! 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();
//! ```
/// Payphone's argument parser.
#[derive(clap::Parser)]
#[command(name = "Payphone CLI")]
#[command(version, about = "Payphone's command line interface.")]
pub struct Args {
/// The Discord bot token to run the bot with.
#[arg(env = "PAYPHONE_TOKEN")]
pub token: String,
/// The minimum level logs have to be to be outputted into the console, or off for no logs.
#[arg(
long,
@@ -57,28 +55,33 @@ pub struct Args {
)]
pub logging: LevelFilter,
/// 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"))
]
pub database_addr: String,
#[command(subcommand)]
pub command: Option<Commands>,
}
/// 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,
}
@@ -95,3 +98,37 @@ impl Into<log::LevelFilter> for LevelFilter {
}
}
}
#[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,
},
/// Starts a documentation server for the project which serves `cargo doc` and refreshes on
/// changes.
Docs {
/// The port to run the documentation server on.
#[arg(
long,
short,
env = "PAYPHONE_DOCS_PORT",
value_name = "PORT",
default_value_t = 3000
)]
port: u16,
},
}
+4
View File
@@ -1,3 +1,7 @@
//! A framework for building Discord bots, built with Twilight.
//!
//!
pub mod bot;
pub mod commands;
pub mod events;
+87 -3
View File
@@ -75,11 +75,17 @@
//!
//! More documentation about each group can be found in the respective submodule's documentation.
use std::{net::SocketAddr, path::Path, process::Command, sync::mpsc};
use axum::{Router, response::Redirect, routing};
use clap::Parser;
use notify::{Event, EventKind, Watcher};
use tokio::net::TcpListener;
use tower_http::services::ServeDir;
use twilight_gateway::Intents;
use crate::{
cli::Args,
cli::{Args, Commands},
framework::bot::Bot,
groups::{
about::{commands::HelpCommand, components::HelpGoBackButton},
@@ -111,13 +117,29 @@ pub async fn main() {
logging::init(args.logging.into());
if let Some(command) = args.command {
match command {
Commands::Start {
token,
database_addr,
} => {
start(token, database_addr).await;
}
Commands::Docs { port } => {
docs(port).await;
}
}
}
}
async fn start(token: String, database_addr: String) {
log::info!("Welcome to Payphone, Nyeki!");
log::info!(
"Payphone is running on version v{}",
env!("CARGO_PKG_VERSION")
);
let db = database::init(args.database_addr)
let db = database::init(database_addr)
.await
.expect("Could not connect to the database!");
@@ -140,5 +162,67 @@ pub async fn main() {
.on_server_update(ServerUpdateHandler)
.on_server_delete(ServerDeleteHandler);
bot.run(&args.token).await.expect("Could not run bot!");
bot.run(&token).await.expect("Could not run bot!");
}
async fn docs(port: u16) {
log::info!("Compiling documentation...");
Command::new("cargo")
.args(["doc", "--no-deps"])
.output()
.expect("Failed to run `cargo doc`");
tokio::spawn(async {
let (tx, rx) = mpsc::channel::<notify::Result<Event>>();
let mut watcher = notify::recommended_watcher(tx).expect("Failed to create watcher");
watcher
.watch(Path::new("src/"), notify::RecursiveMode::Recursive)
.expect("Failed to watch src directory");
for res in rx {
match res {
Ok(event) => {
match event.kind {
EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) => {
log::info!("Source files changed, recompiling...");
}
_ => continue,
}
Command::new("cargo")
.args(["doc", "--no-deps"])
.output()
.expect("Failed to run `cargo doc`");
}
Err(e) => {
log::error!("Watch error: {e:?}");
}
}
}
});
log::info!("Starting documentation server on port {port}...");
let docs: Router<()> = Router::new()
.route(
"/",
routing::get(|| async { Redirect::permanent("/payphone/") }),
)
.fallback_service(ServeDir::new(concat!(
env!("CARGO_MANIFEST_DIR"),
"/target/doc/"
)));
let listener = TcpListener::bind(SocketAddr::from(([0, 0, 0, 0], port)))
.await
.expect("Could not bind to port!");
log::info!("Documentation server is running on http://localhost:{port}/");
axum::serve(listener, docs)
.await
.expect("Could not start documentation server!");
}