aboutsummaryrefslogtreecommitdiff
path: root/src/main.rs
blob: a2a4e6d7538e181632771d5c136ee52d9ddc0e03 (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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
#![feature(transpose_result)]

extern crate chrono;
extern crate ctrlc;
extern crate dotenv;
#[macro_use] extern crate dotenv_codegen;
#[macro_use] extern crate failure;
extern crate fern;
#[macro_use] extern crate lazy_static;
#[macro_use] extern crate log;
extern crate serenity;
extern crate typemap;
extern crate url;
extern crate rand;
extern crate either;
extern crate reqwest;
extern crate sha1;
extern crate mime_guess;
extern crate regex;
extern crate clap;

use commands::register_commands;
use dotenv::dotenv;
use serenity::framework::standard::help_commands;
use serenity::framework::StandardFramework;
use serenity::model::gateway::Ready;
use serenity::model::id::{GuildId, UserId};
use serenity::prelude::*;
use std::env;
use std::thread;
use std::time::{Duration, Instant};

use failure::Error;

pub use util::*;

#[cfg(feature = "diesel")]
#[macro_use] extern crate diesel;

#[cfg(feature = "diesel")]
mod db;

mod commands;
mod util;

pub type Result<T> = ::std::result::Result<T, Error>;

lazy_static! {
    static ref TARGET_GUILD: u64 = dotenv!("TARGET_GUILD").parse().expect("unable to parse TARGET_GUILD as u64");
    static ref TARGET_GUILD_ID: GuildId = GuildId(*TARGET_GUILD);
}

struct Handler;
impl EventHandler for Handler {
    fn ready(&self, _: Context, r: Ready) {
        let guild = r.guilds.iter()
            .find(|g| g.id().0 == *TARGET_GUILD);

        if guild.is_none() {
            info!("bot isn't in configured guild. join here: {:?}", OAUTH_URL.as_str());
        }
    }
}

fn run() -> Result<()> {
    let token = &env::var("THULANI_TOKEN")?;
    let mut client = Client::new(token, Handler)?;

    commands::VoiceManager::register(&mut client);
    commands::PlayQueue::register(&mut client);

    let owner_id = must_env_lookup::<u64>("OWNER_ID");
    let mut framework = StandardFramework::new()
        .configure(|c| c
            .allow_dm(false)
            .allow_whitespace(true)
            .prefixes(vec!["!thulani ", "!thulan ", "!thulando madando ", "!thulando "])
            .ignore_bots(true)
            .on_mention(false)
            .owners(vec![UserId(owner_id)].into_iter().collect())
            .case_insensitivity(true)
        )
        .before(|_ctx, message, cmd| {
            let result = message.guild_id().map_or(false, |x| x.0 == *TARGET_GUILD);
            debug!("got command '{}' from user '{}' ({}). accept: {}", cmd, message.author.name, message.author.id, result);

            result          
        })
        .after(|_ctx, _msg, cmd, err| {
            match err {
                Ok(()) => {
                    trace!("command '{}' completed successfully", cmd);
                },
                Err(e) => {
                    error!("error encountered handling command '{}': {:?}", cmd, e);
                }
            }
        })
        .bucket("Standard", 1, 10, 3)
        .customised_help(help_commands::with_embeds, |c| {
            c
        });

    framework = register_commands(framework);
    client.with_framework(framework);

    let shard_manager = client.shard_manager.clone();
    ctrlc::set_handler(move || {
        info!("shutting down");
        shard_manager.lock().shutdown_all();
    }).expect("unable to create SIGINT/SIGTERM handlers");

    client.start()?;

    Ok(())
}

fn main() {
    const BACKOFF_FACTOR: f64 = 2.0;
    const MAX_BACKOFFS: usize = 3;
    const BACKOFF_INIT: f64 = 100.0;

    const MIN_RUN_DURATION: Duration = Duration::from_secs(120);

    info!("starting");

    dotenv().ok();

    use fern::colors::{Color, ColoredLevelConfig};
    let colors = ColoredLevelConfig::new()
        .info(Color::Green)
        .debug(Color::BrightBlue)
        .trace(Color::BrightMagenta);

    fern::Dispatch::new()
        .level_for("serenity::voice::connection", log::LevelFilter::Error)
        .chain(fern::Dispatch::new()
            .format(move |out, message, record| {
                out.finish(format_args!(
                    "{} [{}] [{}] {}",
                    chrono::Local::now().format("%_m/%_d/%y %l:%M:%S%P"),
                    colors.color(record.level()),
                    record.target(),
                    message
                ))
            })
            .level(log::LevelFilter::Warn)
            .level_for("thulani", log::LevelFilter::Debug)
            .chain(std::io::stdout())
        )
        .chain(fern::Dispatch::new()
            .format(|out, message, record| {
                out.finish(format_args!(
                    "{} [{}] [{}] {}",
                    chrono::Local::now().format("%_m/%_d/%y %l:%M:%S%P"),
                    record.level(),
                    record.target(),
                    message
                ))
            })

            .level(log::LevelFilter::Info)
            .level_for("thulani", log::LevelFilter::Trace)
            .chain(fern::log_file("thulani.log").expect("problem creating log file"))
        )
        .apply()
        .expect("error initializing logging");

    let mut backoff_count: usize = 0;

    loop {
        let start = Instant::now();

        info!("starting bot");
        match run() {
            Err(e) => {
                error!("error encountered running client: {:?}", e);
            },
            _ => {
                // NOTE: we MUST have gotten here through SIGINT/SIGTERM handlers
                ::std::process::exit(0);
            }
        }

        if Instant::now() - start >= MIN_RUN_DURATION {
            backoff_count = 0;
            continue;
        }

        backoff_count += 1;
        if backoff_count >= MAX_BACKOFFS {
            panic!("restarted bot too many times");
        }

        let backoff_millis = (BACKOFF_INIT * BACKOFF_FACTOR.powi(backoff_count as i32)) as u64;
        info!("bot died too quickly. backing off, retrying in {}ms.", backoff_millis);

        thread::sleep(Duration::from_millis(backoff_millis));
    }
}