Files
payphone/src/main.rs
T
2025-08-10 22:50:55 -03:00

229 lines
7.3 KiB
Rust

//! Payphone is a Discord bot that allows users to make calls to each other, similar to a phone
//! system.
//!
//! # Setting Up
//!
//! First of all, clone the Git repository and navigate to the project directory:
//!
//! ```shell
//! $ git clone --branch rewrite https://git.nyeki.dev/payphone/payphone.git
//! $ cd payphone
//! ```
//!
//! To boot up Payphone, you'll need both a Discord bot token and a Scylla database cluster with
//! at least 3 nodes.
//!
//! - To get a Discord bot token, create a new bot application on the
//! [Discord Developer Portal](https://discord.com/developers/applications).
//!
//! - To set up a Scylla database cluster, make sure to have Docker and Docker Compose installed
//! and run `docker compose up -d`. A development cluster will be created with 3 nodes, with one
//! of them listening at `127.0.0.1:9042`.
//!
//! To stop the database cluster at any time, run `docker compose down`. To stop it and delete the
//! volumes (resetting the database), run `docker compose down -v`.
//!
//! ## Booting Up
//!
//! Payphone has a CLI interface that allows you to pass the bot token and database URL as CLI
//! arguments. To get all possible arguments, run the following command:
//!
//! ```shell
//! $ cargo run -- --help
//! ```
//!
//! Payphone also supports configuration via environment variables and `.env` files by default.
//!
//! ```env
//! PAYPHONE_TOKEN="your_discord_bot_token"
//! ```
//!
//! All environment variables are prefixed with `PAYPHONE_` and can be used to override the CLI
//! arguments.
//!
//! # Framework
//!
//! Payphone has a custom framework that allows you to create commands, components, and event
//! handlers easily. The framework is designed to be modular and extensible, allowing you to add
//! new features and commands without modifying the core codebase. The documentation for the
//! framework is all in the [`framework`] module.
//!
//! # Database
//!
//! The database Payphone runs on is ScyllaDB, a high-performance NoSQL database compatible with
//! Apache Cassandra. For documentation on how to interact with the database, check the
//! [`database`] module's documentation.
//!
//! # Application Code
//!
//! Payphone's application code is under the [`groups`] module. It contains all commands, events,
//! embeds, and other configurations for the bot grouped in submodules (e.g. [`groups::calls`],
//! [`groups::about`], etc.), all which can be found at the [`groups`] module's documentation.
//!
//! Each submodule contains files like `commands.rs`, `events.rs`, `embeds.rs`, and `components.rs`
//! that define the commands, events, embeds, and components for that group. Some common file names
//! are:
//!
//! - `commands.rs`: Contains the commands for the group.
//! - `events.rs`: Contains the event handlers for the group.
//! - `embeds.rs`: Contains the embeds for the group.
//! - `components.rs`: Contains the components for the group (action rows, buttons, soon also
//! select menus).
//!
//! Always try to follow the same structure and patterns as the existing code to maintain
//! consistency and readability.
//!
//! 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, Commands},
framework::bot::Bot,
groups::{
about::{commands::HelpCommand, components::HelpGoBackButton},
calls::{
commands::{CallCommand, HangupCommand},
events::MessageRedirector,
},
gateway::events::{ReadyHandler, ReconnectHandler},
servers::events::{ServerCreateHandler, ServerDeleteHandler, ServerUpdateHandler},
},
state::State,
};
mod cli;
mod constants;
mod database;
mod discord;
mod framework;
mod groups;
mod logging;
mod state;
mod utils;
#[tokio::main]
pub async fn main() {
dotenvy::dotenv().ok();
let args = Args::parse();
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(database_addr)
.await
.expect("Could not connect to the database!");
log::info!("Database connection established successfully!");
let state = State { db };
let bot = Bot::new(state, ["p."])
.intent(Intents::GUILDS)
.intent(Intents::GUILD_MESSAGES)
.intent(Intents::MESSAGE_CONTENT)
.command(HelpCommand)
.command(CallCommand::new())
.command(HangupCommand)
.component(HelpGoBackButton)
.on_ready(ReadyHandler)
.on_reconnect(ReconnectHandler)
.on_message(MessageRedirector)
.on_server_create(ServerCreateHandler)
.on_server_update(ServerUpdateHandler)
.on_server_delete(ServerDeleteHandler);
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!");
}