main.rs 10.3 KB
Newer Older
tec's avatar
tec committed
1 2 3
#[macro_use]
extern crate lazy_static;

tec's avatar
tec committed
4 5
#[macro_use]
extern crate log;
6
extern crate indexmap;
tec's avatar
tec committed
7 8 9
extern crate simplelog;
#[macro_use]
extern crate guard;
tec's avatar
tec committed
10 11 12 13 14

#[macro_use]
extern crate diesel;
extern crate ldap3;

tec's avatar
tec committed
15
use simplelog::*;
16
use std::fs::File;
tec's avatar
tec committed
17

tec's avatar
tec committed
18
use chrono::prelude::Utc;
tec's avatar
tec committed
19 20 21 22 23 24
use serenity::{
    model::{channel, channel::Message, gateway::Ready, guild::Member},
    prelude::*,
    utils::MessageBuilder,
};

25 26
#[macro_use]
mod util;
tec's avatar
tec committed
27
mod config;
tec's avatar
tec committed
28 29
mod database;
mod ldap;
tec's avatar
tec committed
30 31
mod reaction_roles;
mod token_management;
tec's avatar
tec committed
32
mod user_management;
tec's avatar
tec committed
33
mod voting;
tec's avatar
tec committed
34

35
use config::{CONFIG, SECRETS};
36
use reaction_roles::{add_role_by_reaction, remove_role_by_reaction};
tec's avatar
tec committed
37
use util::get_string_from_react;
38

Tom Almeida's avatar
Tom Almeida committed
39 40
struct Handler;

tec's avatar
tec committed
41 42 43 44 45 46 47
impl EventHandler for Handler {
    // Set a handler for the `message` event - so that whenever a new message
    // is received - the closure (or function) passed will be called.
    //
    // Event handlers are dispatched through a threadpool, and so multiple
    // events can be dispatched simultaneously.
    fn message(&self, ctx: Context, msg: Message) {
48
        if !(msg.content.starts_with(&CONFIG.command_prefix)) {
tec's avatar
tec committed
49 50 51
            return;
        }
        let message_content: Vec<_> = msg.content[1..].splitn(2, ' ').collect();
tec's avatar
tec committed
52 53 54 55 56
        let content = if message_content.len() > 1 {
            message_content[1]
        } else {
            ""
        };
tec's avatar
tec committed
57
        match message_content[0] {
58
            "say" => println!("{:#?}", msg.content),
tec's avatar
tec committed
59 60 61 62
            "register" => user_management::Commands::register(ctx, msg.clone(), content),
            "verify" => user_management::Commands::verify(ctx, msg.clone(), content),
            "profile" => user_management::Commands::profile(ctx, msg.clone(), content),
            "set" => user_management::Commands::set_info(ctx, msg.clone(), content),
tec's avatar
tec committed
63
            "clear" => user_management::Commands::clear_info(ctx, msg.clone(), content),
tec's avatar
tec committed
64 65 66 67
            "move" => voting::Commands::move_something(ctx, msg.clone(), content),
            "motion" => voting::Commands::motion(ctx, msg.clone(), content),
            "poll" => voting::Commands::poll(ctx, msg.clone(), content),
            "cowsay" => voting::Commands::cowsay(ctx, msg.clone(), content),
tec's avatar
tec committed
68 69
            "logreact" => {
                e!("Error deleting logreact prompt: {:?}", msg.delete(&ctx));
tec's avatar
tec committed
70 71 72 73 74
                send_message!(
                    msg.channel_id,
                    &ctx.http,
                    "React to this to log the ID (for the next 5min)"
                );
tec's avatar
tec committed
75
            }
tec's avatar
tec committed
76
            "help" => {
tec's avatar
tec committed
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
                // Plaintext version, keep in case IRC users kick up a fuss
                // let mut message = MessageBuilder::new();
                // message.push_line(format!(
                //     "Use {}move <action> to make a circular motion",
                //     &CONFIG.command_prefix
                // ));
                // message.push_line(format!(
                //     "Use {}poll <proposal> to see what people think about something",
                //     &CONFIG.command_prefix
                // ));
                // send_message!(msg.channel_id, &ctx.http, message.build());

                let result = msg.channel_id.send_message(&ctx.http, |m| {
                    m.embed(|embed| {
                        embed.colour(serenity::utils::Colour::DARK_GREY);
                        embed.title("Commands for the UCC Bot");
                        embed.field("About", "This is UCC's own little in-house bot, please treat it nicely :)", false);
                        embed.field("Commitee", "`!move <text>` to make a circular motion\n\
                                                 `!poll <text>` to get people's opinions on something", false);
                        embed.field("Account", "`!register <ucc username>` to link your Discord and UCC account\n\
                                                `!profile <user>` to get the profile of a user\n\
                                                `!set <bio|git|web|photo>` to set that property of _your_ profile", false);
                        embed.field("Fun", "`!cowsay <text>` to have a cow say your words\n\
                                            with no `<text>` it'll give you a fortune 😉", false);
                        embed
                    });
                    m
                });
                if let Err(why) = result {
                    error!("Error sending help embed: {:?}", why);
                }
            }
            // undocumented (in !help) functins
            "ldap" => send_message!(
                msg.channel_id,
                &ctx.http,
                format!("{:?}", ldap::ldap_search(message_content[1]))
            ),
            "tla" => send_message!(
                msg.channel_id,
                &ctx.http,
                format!("{:?}", ldap::tla_search(message_content[1]))
            ),
            _ => send_message!(
                msg.channel_id,
                &ctx.http,
                format!("Unrecognised command. Try {}help", &CONFIG.command_prefix)
            ),
tec's avatar
tec committed
125 126 127
        }
    }

tec's avatar
tec committed
128
    fn reaction_add(&self, ctx: Context, add_reaction: channel::Reaction) {
tec's avatar
tec committed
129
        match add_reaction.message(&ctx.http) {
tec's avatar
tec committed
130 131 132
            Ok(message) => match get_message_type(&message) {
                MessageType::RoleReactMessage if add_reaction.user_id.0 != CONFIG.bot_id => {
                    add_role_by_reaction(&ctx, message, add_reaction);
Ash's avatar
cleanup  
Ash committed
133
                    return
tec's avatar
tec committed
134 135 136 137
                }
                _ if message.author.id.0 != CONFIG.bot_id
                    || add_reaction.user_id == CONFIG.bot_id =>
                {
138
                    return
tec's avatar
tec committed
139 140 141 142 143 144 145 146 147
                }
                MessageType::Motion => voting::reaction_add(ctx, add_reaction),
                MessageType::LogReact => {
                    let react_user = add_reaction.user(&ctx).unwrap();
                    let react_as_string = get_string_from_react(&add_reaction.emoji);
                    if Utc::now().timestamp() - message.timestamp.timestamp() > 300 {
                        warn!(
                            "The logreact message {} just tried to use is too old",
                            react_user.name
tec's avatar
tec committed
148
                        );
Ash's avatar
cleanup  
Ash committed
149
                        return
tec's avatar
tec committed
150 151 152 153 154 155 156 157 158 159 160 161 162
                    }
                    info!(
                        "The react {} just added is {:?}. In full: {:?}",
                        react_user.name, react_as_string, add_reaction.emoji
                    );
                    let mut msg = MessageBuilder::new();
                    msg.push_italic(react_user.name);
                    msg.push(format!(
                        " wanted to know that {} is represented by ",
                        add_reaction.emoji,
                    ));
                    msg.push_mono(react_as_string);
                    send_message!(message.channel_id, &ctx.http, msg.build());
tec's avatar
tec committed
163
                }
tec's avatar
tec committed
164
                _ => {}
165
            },
tec's avatar
tec committed
166 167
            Err(why) => error!("Failed to get react message {:?}", why),
        }
tec's avatar
tec committed
168 169
    }

tec's avatar
tec committed
170
    fn reaction_remove(&self, ctx: Context, removed_reaction: channel::Reaction) {
tec's avatar
tec committed
171
        match removed_reaction.message(&ctx.http) {
tec's avatar
tec committed
172 173 174
            Ok(message) => match get_message_type(&message) {
                MessageType::RoleReactMessage if removed_reaction.user_id != CONFIG.bot_id => {
                    remove_role_by_reaction(&ctx, message, removed_reaction);
Ash's avatar
cleanup  
Ash committed
175
                    return
tec's avatar
tec committed
176 177 178 179
                }
                _ if message.author.id.0 != CONFIG.bot_id
                    || removed_reaction.user_id == CONFIG.bot_id =>
                {
180
                    return
tec's avatar
tec committed
181
                }
tec's avatar
tec committed
182 183
                MessageType::Motion => voting::reaction_remove(ctx, removed_reaction),
                _ => {}
184
            },
tec's avatar
tec committed
185 186
            Err(why) => error!("Failed to get react message {:?}", why),
        }
tec's avatar
tec committed
187 188
    }

tec's avatar
tec committed
189 190 191 192
    fn guild_member_addition(
        &self,
        ctx: Context,
        _guild_id: serenity::model::id::GuildId,
tec's avatar
tec committed
193
        the_new_member: Member,
tec's avatar
tec committed
194
    ) {
tec's avatar
tec committed
195
        user_management::new_member(&ctx, the_new_member);
tec's avatar
tec committed
196 197 198 199 200 201 202 203
    }

    // Set a handler to be called on the `ready` event. This is called when a
    // shard is booted, and a READY payload is sent by Discord. This payload
    // contains data like the current user's guild Ids, current user data,
    // private channels, and more.
    //
    // In this case, just print what the current user's username is.
204
    fn ready(&self, ctx: Context, ready: Ready) {
tec's avatar
tec committed
205
        info!("{} is connected!", ready.user.name);
tec's avatar
tec committed
206
        reaction_roles::sync_all_role_reactions(&ctx);
207 208 209
    }

    fn resume(&self, ctx: Context, _: serenity::model::event::ResumedEvent) {
tec's avatar
tec committed
210
        reaction_roles::sync_all_role_reactions(&ctx);
tec's avatar
tec committed
211 212 213 214
    }
}

fn main() {
tec's avatar
tec committed
215 216 217 218 219 220 221 222 223
    CombinedLogger::init(vec![
        TermLogger::new(LevelFilter::Info, Config::default(), TerminalMode::Mixed).unwrap(),
        WriteLogger::new(
            LevelFilter::Info,
            Config::default(),
            File::create("ucc-bot.log").unwrap(),
        ),
    ])
    .unwrap();
224

tec's avatar
tec committed
225 226 227
    // Create a new instance of the Client, logging in as a bot. This will
    // automatically prepend your bot token with "Bot ", which is a requirement
    // by Discord for bot users.
228
    let mut client = Client::new(&SECRETS.discord_token, Handler).expect("Err creating client");
tec's avatar
tec committed
229 230 231 232 233 234

    // Finally, start a single shard, and start listening to events.
    //
    // Shards will automatically attempt to reconnect, and will perform
    // exponential backoff until it reconnects.
    if let Err(why) = client.start() {
tec's avatar
tec committed
235
        error!("Client error: {:?}", why);
tec's avatar
tec committed
236 237
    }
}
tec's avatar
tec committed
238

239 240 241 242
#[derive(Debug, PartialEq)]
enum MessageType {
    Motion,
    Role,
243
    RoleReactMessage,
244 245
    LogReact,
    Poll,
tec's avatar
tec committed
246
    Misc,
247 248 249
}

fn get_message_type(message: &Message) -> MessageType {
tec's avatar
tec committed
250 251 252 253 254
    if CONFIG
        .react_role_messages
        .iter()
        .any(|rrm| rrm.message == message.id)
    {
255 256
        return MessageType::RoleReactMessage;
    }
Timothy du Heaume's avatar
Timothy du Heaume committed
257
    if message.embeds.is_empty() {
tec's avatar
tec committed
258
        // Get first word of message
tec's avatar
tec committed
259
        return match message.content.splitn(2, ' ').next().unwrap() {
260 261 262
            "Role" => MessageType::Role,
            "React" => MessageType::LogReact,
            _ => MessageType::Misc,
tec's avatar
tec committed
263
        };
tec's avatar
tec committed
264
    }
tec's avatar
tec committed
265 266 267
    let title: String = message.embeds[0].title.clone().unwrap();
    let words_of_title: Vec<_> = title.splitn(2, ' ').collect();
    let first_word_of_title = words_of_title[0];
Timothy du Heaume's avatar
Timothy du Heaume committed
268
    match first_word_of_title {
269 270 271
        "Motion" => MessageType::Motion,
        "Poll" => MessageType::Poll,
        _ => MessageType::Misc,
Timothy du Heaume's avatar
Timothy du Heaume committed
272
    }
tec's avatar
tec committed
273
}