aboutsummaryrefslogtreecommitdiff
path: root/src/main.rs
blob: 2a3a4fd2a033eeefdc7afe70b8634e8af61f3709 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
#![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 as serenity;
use ::serenity::model::gateway::GatewayIntents;

type Error = Box<dyn std::error::Error + Send + Sync>;
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<PgPool>,
}

/// Show help menu
#[poise::command(prefix_command, slash_command)]
async fn help(
    ctx: Context<'_>,
    #[description = "Command to get help for"] command: Option<String>,
) -> 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::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();
                Ok(Data {
                    pg: Mutex::new(pool),
                })
            })
        })
        .options(options)
        .run()
        .await
        .unwrap();
}