aboutsummaryrefslogtreecommitdiff
path: root/src/main.rs
blob: 3f29d8e937ceeaafb85476571324aa1bc66d9c67 (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
#[macro_use] extern crate serenity;
#[macro_use] extern crate log;
#[macro_use] extern crate error_chain;
#[macro_use] extern crate lazy_static;

extern crate dotenv;
extern crate fern;
extern crate typemap;
extern crate url;
extern crate chrono;

mod commands;
mod util;

use std::env;
use std::thread;
use std::time::{Duration, Instant};

use serenity::prelude::*;
use serenity::framework::StandardFramework;
use serenity::framework::standard::help_commands;
use serenity::model::gateway::Ready;
use serenity::model::id::{UserId, GuildId};

use dotenv::dotenv;

use commands::register_commands;

mod errors {
    error_chain! {
        foreign_links {
            Serenity(::serenity::Error);
            MissingVar(::std::env::VarError);
        }
    }
}

use errors::*;
pub use util::*;

lazy_static! {
    static ref TARGET_GUILD: u64 = must_env_lookup::<u64>("TARGET_GUILD");
    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");
                },
                Err(e) => {
                    error!("encountered error: {:?}", e);
                }
            }
        })
        .bucket("Standard", 1, 10, 3)
        .customised_help(help_commands::with_embeds, |c| {
            c
        });

    framework = register_commands(framework);

    client.with_framework(framework);
    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);

    dotenv().ok();

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

    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_rs", log::LevelFilter::Trace)
        .level_for("serenity::voice::connection", log::LevelFilter::Error)
        .chain(std::io::stdout())
        .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);
                e.iter().skip(1).for_each(|e| {
                    error!("caused by: {}", e);
                });

                if let Some(bt) = e.backtrace() {
                    error!("backtrace: {:?}", bt);
                }

                ::std::process::exit(1);
            },
            _ => {
                warn!("somehow `run` completed without an error. should probably take a look at this.");
            }
        }

        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));
    }
}