#![deny(missing_docs)] /*! * Full rewrite of the [Glitch](https://glitchbot.net) bot in Poise with slash commands * * This iteration will focus on code correctness and durability. The major goal is to avoid what * happened to the Campmaster by avoiding code rot and forcing documentation on _everything_ */ #[macro_use] extern crate tracing; use std::{sync::Mutex, time::Duration}; use dotenv::dotenv; use sqlx::{postgres::PgPoolOptions, PgPool}; use tracing::{info, instrument}; use poise::serenity_prelude::{self as serenity, Activity}; use ::serenity::model::gateway::GatewayIntents; type Error = Box; type Context<'a> = poise::Context<'a, Data, Error>; mod commands; mod handler; mod models; mod errors; /// Contains data shared between all commands pub struct Data { pg: Mutex, } /// Show help menu #[poise::command(prefix_command, slash_command)] async fn help( ctx: Context<'_>, #[description = "Command to get help for"] command: Option, ) -> Result<(), Error> { poise::builtins::help( ctx, command.as_deref(), poise::builtins::HelpConfiguration::default(), ) .await?; Ok(()) } async fn on_error(error: poise::FrameworkError<'_, Data, Error>) { match error { poise::FrameworkError::Setup { error } => panic!("Failed to start bot: {:?}", error), poise::FrameworkError::Command { error, ctx } => { error!("Error in command {}: {:?}", ctx.command().name, error); ctx.say(format!("Whoops! Something went wrong: `{:?}`", error)) .await .unwrap(); } error => { if let Err(e) = poise::builtins::on_error(error).await { error!("Error handling error: {}", e); } } } } /// Register application commands in this guild or globally /// /// Run with no arguments to register in guild, run with argument "global" to register globally. #[poise::command(prefix_command, hide_in_help)] async fn register(ctx: Context<'_>, #[flag] global: bool) -> Result<(), Error> { poise::builtins::register_application_commands(ctx, global).await?; Ok(()) } #[tokio::main] #[instrument] async fn main() { // Initialize environment and logging dotenv().unwrap(); color_eyre::install().unwrap(); tracing_subscriber::fmt::init(); info!("Initialized logging"); let options = poise::FrameworkOptions { commands: vec![ help(), register(), commands::meta::ping(), commands::meta::about(), commands::meta::userinfo(), commands::meta::set_status(), commands::actions::boop(), commands::actions::hug(), commands::pony::randpony(), commands::pony::tpony(), commands::pony::ponybyid(), commands::osu::osup(), commands::osu::osubm(), poise::Command { subcommands: vec![ commands::reactionroles::init(), commands::reactionroles::add(), commands::reactionroles::del(), ], ..commands::reactionroles::rroles() }, poise::Command { subcommands: vec![ commands::filters::list(), commands::filters::add(), commands::filters::del(), commands::filters::channel(), ], ..commands::filters::filter() }, ], // This requires a closure, for some reason on_error: |error| Box::pin(on_error(error)), // Honestly could probably be removed, but it's kept in for ~reasons~ pre_command: |ctx| { Box::pin(async move { debug!("Executing command {}...", ctx.command().name); }) }, post_command: |ctx| { Box::pin(async move { debug!("Done executing command {}!", ctx.command().name); }) }, prefix_options: poise::PrefixFrameworkOptions { prefix: Some("~".into()), edit_tracker: Some(poise::EditTracker::for_timespan(Duration::from_secs(3600))), // These don't work, I thought they might but -\_()_/- additional_prefixes: vec![ poise::Prefix::Literal("hey glitch"), poise::Prefix::Literal("hey glitch,"), ], ..Default::default() }, // For once, we abstracted the handler *out* of main.rs so we can actually read the damn // file listener: |ctx, event, _, data| Box::pin(handler::event_handler(ctx, event, data)), ..Default::default() }; poise::Framework::build() .token(std::env::var("DISCORD_TOKEN").unwrap_or("BAD-TOKEN".into())) .client_settings(|c| { c.intents(serenity::GatewayIntents::all()) }) .intents(GatewayIntents::all()) .user_data_setup(move |_ctx, _ready, _framework| { Box::pin(async move { /* * Hoo boy okay * * This sets up the postgres pool and adds it to the Data struct we defined * earlier. Once that's done, it runs the migrations that have been embeded within * the completed binary * * A sane default was chosen if DATABASE_URL doesn't exist * * If migrations fail, we panic and exit because then we're in an incorrect DB * state and something needs to be fixed before any further work can be done. */ let pool = PgPoolOptions::new() .max_connections(5) .connect( &std::env::var("DATABASE_URL") .unwrap_or("postgres://postgres@localhost/glitch".to_string()), ) .await .expect("Couldn't connect to postgresql"); sqlx::migrate!("./migrations") .run(&pool) .await.unwrap(); // Since this has context access, we can change the game activity here _ctx.set_activity(Activity::playing(String::from("~help | https://glitchbot.net"))).await; Ok(Data { pg: Mutex::new(pool), }) }) }) .options(options) .run() .await .unwrap(); }