main.rs 10.5 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::*;
tec's avatar
tec committed
16
use std::fs::{read_to_string, 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;
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
            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);
                    return;
                }
                _ if message.author.id.0 != CONFIG.bot_id
                    || add_reaction.user_id == CONFIG.bot_id =>
                {
                    return;
                }
                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
                        );
tec's avatar
tec committed
149
150
151
152
153
154
155
156
157
158
159
160
161
162
                        return;
                    }
                    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
175
176
177
178
179
180
            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);
                    return;
                }
                _ if message.author.id.0 != CONFIG.bot_id
                    || removed_reaction.user_id == CONFIG.bot_id =>
                {
                    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
    // Configure the client with your Discord bot token in the environment.
226
    let token = read_to_string("discord_token").unwrap();
tec's avatar
tec committed
227
228
229
230
231
232
233
234
235
236
237

    // 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.
    let mut client = Client::new(&token, Handler).expect("Err creating client");

    // 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
238
        error!("Client error: {:?}", why);
tec's avatar
tec committed
239
240
    }
}
tec's avatar
tec committed
241

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

fn get_message_type(message: &Message) -> MessageType {
tec's avatar
tec committed
253
254
255
256
257
    if CONFIG
        .react_role_messages
        .iter()
        .any(|rrm| rrm.message == message.id)
    {
258
259
        return MessageType::RoleReactMessage;
    }
Timothy du Heaume's avatar
Timothy du Heaume committed
260
    if message.embeds.is_empty() {
tec's avatar
tec committed
261
        // Get first word of message
tec's avatar
tec committed
262
        return match message.content.splitn(2, ' ').next().unwrap() {
263
264
265
            "Role" => MessageType::Role,
            "React" => MessageType::LogReact,
            _ => MessageType::Misc,
tec's avatar
tec committed
266
        };
tec's avatar
tec committed
267
    }
tec's avatar
tec committed
268
269
270
    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
271
    match first_word_of_title {
272
273
274
        "Motion" => MessageType::Motion,
        "Poll" => MessageType::Poll,
        _ => MessageType::Misc,
Timothy du Heaume's avatar
Timothy du Heaume committed
275
    }
tec's avatar
tec committed
276
}