voting.rs 15.2 KB
Newer Older
tec's avatar
tec committed
1
2
3
4
5
use serenity::{
    model::{channel, channel::Message},
    prelude::*,
    utils::MessageBuilder,
};
tec's avatar
tec committed
6
7
use std::collections::HashMap;
use std::sync::Mutex;
tec's avatar
tec committed
8

9
use crate::config::CONFIG;
10
use crate::util::get_string_from_react;
tec's avatar
tec committed
11
12
13
14
15

macro_rules! e {
    ($error: literal, $x:expr) => {
        match $x {
            Ok(_) => (),
tec's avatar
tec committed
16
            Err(why) => error!($error, why),
tec's avatar
tec committed
17
18
19
20
21
22
23
24
        }
    };
}

pub struct Commands;
impl Commands {
    pub fn move_something(ctx: Context, msg: Message, content: &str) {
        let motion = content;
Timothy du Heaume's avatar
Timothy du Heaume committed
25
        if !motion.is_empty() {
tec's avatar
tec committed
26
            create_motion(&ctx, &msg, motion);
tec's avatar
tec committed
27
            return;
tec's avatar
tec committed
28
        }
tec's avatar
tec committed
29
30
31
32
33
34
35
        e!(
            "Error sending message: {:?}",
            msg.channel_id.say(
                &ctx.http,
                "If there's something you want to motion, put it after the !move keyword",
            )
        );
tec's avatar
tec committed
36
37
38
39
40
41
42
43
44
45
    }
    pub fn motion(ctx: Context, msg: Message, _content: &str) {
        e!("Error sending message: {:?}",
                msg.channel_id.say(
                &ctx.http,
                "I hope you're not having a motion. You may have wanted to !move something instead."
            ));
    }
    pub fn poll(ctx: Context, msg: Message, content: &str) {
        let topic = content;
Timothy du Heaume's avatar
Timothy du Heaume committed
46
        if !topic.is_empty() {
tec's avatar
tec committed
47
            create_poll(&ctx, &msg, topic);
tec's avatar
tec committed
48
            return;
tec's avatar
tec committed
49
        }
tec's avatar
tec committed
50
51
52
53
54
55
56
        e!(
            "Error sending message: {:?}",
            msg.channel_id.say(
                &ctx.http,
                "If there's something you want to motion, put it after the !move keyword",
            )
        );
tec's avatar
tec committed
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
    }
    pub fn cowsay(ctx: Context, msg: Message, content: &str) {
        let mut text = content.to_owned();
        text.escape_default();
        // Guess what buddy! You definitely are passing a string to cowsay
        text.insert(0, '\'');
        text.insert(text.len(), '\'');
        let output = std::process::Command::new("cowsay")
            .arg(text)
            .output()
            // btw, if we can't execute cowsay we crash
            .expect("failed to execute cowsay");
        let mut message = MessageBuilder::new();
        message.push_codeblock(
            String::from_utf8(output.stdout).expect("unable to parse stdout to String"),
            None,
        );
        e!(
            "Error sending message: {:?}",
            msg.channel_id.say(&ctx.http, message.build())
        );
    }
}

fn create_motion(ctx: &Context, msg: &Message, topic: &str) {
tec's avatar
tec committed
82
    info!("{} created a motion {}", msg.author.name, topic);
tec's avatar
tec committed
83
    if let Err(why) = msg.delete(ctx) {
tec's avatar
tec committed
84
        error!("Error deleting motion prompt: {:?}", why);
tec's avatar
tec committed
85
    }
Timothy du Heaume's avatar
Timothy du Heaume committed
86
    let result = msg.channel_id.send_message(&ctx.http, |m| {
tec's avatar
tec committed
87
88
89
90
91
92
93
94
95
96
97
98
99
        m.embed(|embed| {
            embed.author(|a| {
                a.name(&msg.author.name);
                a.icon_url(
                    msg.author
                        .static_avatar_url()
                        .expect("Expected author to have avatar"),
                );
                a
            });
            embed.colour(serenity::utils::Colour::GOLD);
            embed.title(format!("Motion to {}", topic));
            let mut desc = MessageBuilder::new();
100
            desc.role(CONFIG.vote_role);
tec's avatar
tec committed
101
102
103
104
105
106
107
108
109
            desc.push(" take a look at this motion from ");
            desc.mention(&msg.author);
            embed.description(desc.build());
            embed.field("Status", "Under Consideration", true);
            embed.field("Votes", "For: 0\nAgainst: 0\nAbstain: 0", true);
            embed.timestamp(msg.timestamp.to_rfc3339());
            embed
        });
        m.reactions(vec![
110
111
112
113
114
            CONFIG.for_vote.to_string(),
            CONFIG.against_vote.to_string(),
            CONFIG.abstain_vote.to_string(),
            CONFIG.approve_react.to_string(),
            CONFIG.disapprove_react.to_string(),
tec's avatar
tec committed
115
116
        ]);
        m
Timothy du Heaume's avatar
Timothy du Heaume committed
117
118
119
    });
    if let Err(why) = result {
        error!("Error creating motion: {:?}", why);
tec's avatar
tec committed
120
121
122
123
    }
}

fn create_poll(ctx: &Context, msg: &Message, topic: &str) {
tec's avatar
tec committed
124
    info!("{} created a poll {}", msg.author.name, topic);
tec's avatar
tec committed
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
    match msg.channel_id.send_message(&ctx.http, |m| {
        m.embed(|embed| {
            embed.author(|a| {
                a.name(&msg.author.name);
                a.icon_url(
                    msg.author
                        .static_avatar_url()
                        .expect("Expected author to have avatar"),
                );
                a
            });
            embed.colour(serenity::utils::Colour::BLUE);
            embed.title(format!("Poll {}", topic));
            let mut desc = MessageBuilder::new();
            desc.mention(&msg.author);
            desc.push(" wants to know what you think.");
            embed.description(desc.build());
            embed.timestamp(msg.timestamp.to_rfc3339());
            embed
        });
        m.reactions(vec![
146
147
148
            CONFIG.approve_react.to_string(),
            CONFIG.disapprove_react.to_string(),
            CONFIG.unsure_react.to_string(),
tec's avatar
tec committed
149
150
151
152
        ]);
        m
    }) {
        Err(why) => {
tec's avatar
tec committed
153
            error!("Error sending message: {:?}", why);
tec's avatar
tec committed
154
155
156
        }
        Ok(_) => {
            if let Err(why) = msg.delete(ctx) {
tec's avatar
tec committed
157
                error!("Error deleting motion prompt: {:?}", why);
tec's avatar
tec committed
158
159
160
161
162
            }
        }
    }
}

tec's avatar
tec committed
163
164
#[derive(Debug, Clone)]
struct MotionInfo {
165
    votes: HashMap<String, Vec<serenity::model::user::User>>,
tec's avatar
tec committed
166
167
168
169
170
171
172
173
174
175
}

lazy_static! {
    static ref MOTIONS_CACHE: Mutex<HashMap<serenity::model::id::MessageId, MotionInfo>> =
        Mutex::new(HashMap::new());
}

fn get_cached_motion(ctx: &Context, msg: &Message) -> MotionInfo {
    let mut cached_motions = MOTIONS_CACHE.lock().unwrap();
    if !cached_motions.contains_key(&msg.id) {
tec's avatar
tec committed
176
        info!("Initialising representation of motion {:?}", msg.id);
tec's avatar
tec committed
177
178
179
180
        let this_motion = MotionInfo {
            votes: {
                let mut m = HashMap::new();
                m.insert(
181
182
                    CONFIG.for_vote.to_string(),
                    msg.reaction_users(ctx, CONFIG.for_vote.to_string(), None, None)
tec's avatar
tec committed
183
184
185
                        .unwrap(),
                );
                m.insert(
186
187
                    CONFIG.against_vote.to_string(),
                    msg.reaction_users(ctx, CONFIG.against_vote.to_string(), None, None)
tec's avatar
tec committed
188
189
190
                        .unwrap(),
                );
                m.insert(
191
192
                    CONFIG.abstain_vote.to_string(),
                    msg.reaction_users(ctx, CONFIG.abstain_vote.to_string(), None, None)
tec's avatar
tec committed
193
194
195
196
197
198
199
                        .unwrap(),
                );
                m
            },
        };
        cached_motions.insert(msg.id, this_motion);
    }
Timothy du Heaume's avatar
Timothy du Heaume committed
200
    (*cached_motions.get(&msg.id).unwrap()).clone()
tec's avatar
tec committed
201
}
Timothy du Heaume's avatar
Timothy du Heaume committed
202
203
fn set_cached_motion(id: serenity::model::id::MessageId, motion_info: MotionInfo) {
    if let Some(motion) = MOTIONS_CACHE.lock().unwrap().get_mut(&id) {
tec's avatar
tec committed
204
        *motion = motion_info;
tec's avatar
tec committed
205
        return;
tec's avatar
tec committed
206
    }
tec's avatar
tec committed
207
    warn!("{}", "Couldn't find motion in cache to set");
tec's avatar
tec committed
208
209
}

tec's avatar
tec committed
210
211
212
213
214
215
216
fn update_motion(
    ctx: &Context,
    msg: &mut Message,
    user: &serenity::model::user::User,
    change: &str,
    reaction: channel::Reaction,
) {
tec's avatar
tec committed
217
218
    let motion_info: MotionInfo = get_cached_motion(ctx, msg);

219
220
221
    let for_votes = motion_info.votes.get(&CONFIG.for_vote).unwrap().len() as isize - 1;
    let against_votes = motion_info.votes.get(&CONFIG.against_vote).unwrap().len() as isize - 1;
    let abstain_votes = motion_info.votes.get(&CONFIG.abstain_vote).unwrap().len() as isize - 1;
tec's avatar
tec committed
222

tec's avatar
tec committed
223
224
    let has_tiebreaker = |users: &Vec<serenity::model::user::User>| {
        users.iter().any(|u| {
225
            u.has_role(ctx, CONFIG.server_id, CONFIG.tiebreaker_role)
tec's avatar
tec committed
226
227
                .unwrap()
        })
tec's avatar
tec committed
228
229
230
    };

    let for_strength = for_votes as f32
231
        + (if has_tiebreaker(motion_info.votes.get(&CONFIG.for_vote).unwrap()) {
tec's avatar
tec committed
232
            0.25
tec's avatar
tec committed
233
234
235
236
        } else {
            0.0
        });
    let against_strength = against_votes as f32
237
        + (if has_tiebreaker(motion_info.votes.get(&CONFIG.against_vote).unwrap()) {
tec's avatar
tec committed
238
            0.25
tec's avatar
tec committed
239
240
241
242
        } else {
            0.0
        });
    let abstain_strength = abstain_votes as f32
243
        + (if has_tiebreaker(motion_info.votes.get(&CONFIG.abstain_vote).unwrap()) {
tec's avatar
tec committed
244
            0.25
tec's avatar
tec committed
245
246
247
248
249
250
251
        } else {
            0.0
        });

    let old_embed = msg.embeds[0].clone();
    let topic = old_embed.clone().title.unwrap();

tec's avatar
tec committed
252
    info!(
tec's avatar
tec committed
253
254
255
        "  {:10} {:6} {} on {}",
        user.name,
        change,
256
        get_string_from_react(reaction.emoji),
tec's avatar
tec committed
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
        topic
    );

    let update_status = |e: &mut serenity::builder::CreateEmbed,
                         status: &str,
                         last_status_full: String,
                         topic: &str| {
        let last_status = last_status_full.lines().next().expect("No previous status");
        if last_status == status {
            e.field("Status", last_status_full, true);
        } else {
            e.field(
                "Status",
                format!("{}\n_was_ {}", status, last_status_full),
                true,
            );
tec's avatar
tec committed
273
            info!("Motion to {} now {}", topic, status);
tec's avatar
tec committed
274
275
276
277
278
279
            //
            let mut message = MessageBuilder::new();
            message.push_bold(topic);
            message.push(" is now ");
            message.push_bold(status);
            message.push_italic(format!(" (was {})", last_status));
280
            if let Err(why) = CONFIG.announcement_channel.say(&ctx.http, message.build()) {
tec's avatar
tec committed
281
                error!("Error sending message: {:?}", why);
tec's avatar
tec committed
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
            };
        }
    };

    if let Err(why) = msg.edit(ctx, |m| {
        m.embed(|e| {
            e.author(|a| {
                let old_author = old_embed.clone().author.expect("Expected author in embed");
                a.name(old_author.name);
                a.icon_url(
                    old_author
                        .icon_url
                        .expect("Expected embed author to have icon"),
                );
                a
            });
            e.title(&topic);
            e.description(old_embed.description.unwrap());
            let last_status_full = old_embed
                .fields
                .iter()
Timothy du Heaume's avatar
Timothy du Heaume committed
303
                .find(|f| f.name == "Status")
tec's avatar
tec committed
304
305
306
                .expect("No previous status")
                .clone()
                .value;
307
            if for_strength > (CONFIG.vote_pool_size as f32 / 2.0) {
tec's avatar
tec committed
308
309
                e.colour(serenity::utils::Colour::TEAL);
                update_status(e, "Passed", last_status_full, &topic);
310
            } else if against_strength + abstain_strength > (CONFIG.vote_pool_size as f32 / 2.0) {
tec's avatar
tec committed
311
312
313
314
315
316
317
318
319
320
                e.colour(serenity::utils::Colour::RED);
                update_status(e, "Failed", last_status_full, &topic);
            } else {
                e.colour(serenity::utils::Colour::GOLD);
                update_status(e, "Under Consideration", last_status_full, &topic);
            }
            e.field(
                format!(
                    "Votes ({}/{})",
                    for_votes + against_votes + abstain_votes,
321
                    CONFIG.vote_pool_size
tec's avatar
tec committed
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
                ),
                format!(
                    "For: {}\nAgainst: {}\nAbstain: {}",
                    for_votes, against_votes, abstain_votes
                ),
                true,
            );
            e.timestamp(
                old_embed
                    .timestamp
                    .expect("Expected embed to have timestamp"),
            );
            e
        })
    }) {
tec's avatar
tec committed
337
        error!("Error updating motion: {:?}", why);
tec's avatar
tec committed
338
339
340
341
    }
}

pub fn reaction_add(ctx: Context, add_reaction: channel::Reaction) {
342
    let react_as_string = get_string_from_react(add_reaction.emoji.clone());
tec's avatar
tec committed
343
344
    match add_reaction.message(&ctx.http) {
        Ok(mut message) => {
tec's avatar
tec committed
345
            if let Ok(user) = add_reaction.user(&ctx) {
346
                match user.has_role(&ctx, CONFIG.server_id, CONFIG.vote_role) {
tec's avatar
tec committed
347
                    Ok(true) => {
tec's avatar
tec committed
348
                        // remove vote if already voted
Timothy du Heaume's avatar
Timothy du Heaume committed
349
350
351
352
353
354
355
                        for react in [
                            CONFIG.for_vote.to_string(),
                            CONFIG.against_vote.to_string(),
                            CONFIG.abstain_vote.to_string(),
                        ]
                        .iter()
                        .filter(|r| r != &&react_as_string)
tec's avatar
tec committed
356
                        {
Timothy du Heaume's avatar
Timothy du Heaume committed
357
358
359
                            for a_user in message
                                .reaction_users(&ctx, react.as_str(), None, None)
                                .unwrap()
tec's avatar
tec committed
360
                            {
tec's avatar
tec committed
361
362
363
364
                                if a_user.id.0 == user.id.0 {
                                    if let Err(why) = add_reaction.delete(&ctx) {
                                        error!("Error deleting react: {:?}", why);
                                    };
tec's avatar
tec committed
365
                                    return;
tec's avatar
tec committed
366
367
                                }
                            }
tec's avatar
tec committed
368
                        }
tec's avatar
tec committed
369
                        // remove 'illegal' reacts
Timothy du Heaume's avatar
Timothy du Heaume committed
370
                        if !CONFIG.allowed_reacts().contains(&react_as_string) {
tec's avatar
tec committed
371
372
373
374
375
                            if let Err(why) = add_reaction.delete(&ctx) {
                                error!("Error deleting react: {:?}", why);
                            };
                            return;
                        }
tec's avatar
tec committed
376
377
                        // update motion
                        let mut motion_info = get_cached_motion(&ctx, &message);
Timothy du Heaume's avatar
Timothy du Heaume committed
378
                        if let Some(vote) = motion_info.votes.get_mut(&react_as_string) {
tec's avatar
tec committed
379
380
                            vote.retain(|u| u.id != user.id);
                            vote.push(user.clone());
tec's avatar
tec committed
381
                        }
Timothy du Heaume's avatar
Timothy du Heaume committed
382
                        set_cached_motion(message.id, motion_info);
tec's avatar
tec committed
383
                        update_motion(&ctx, &mut message, &user, "add", add_reaction);
tec's avatar
tec committed
384
385
                    }
                    Ok(false) => {
Timothy du Heaume's avatar
Timothy du Heaume committed
386
387
388
389
390
                        if ![
                            CONFIG.approve_react.to_string(),
                            CONFIG.disapprove_react.to_string(),
                        ]
                        .contains(&react_as_string)
tec's avatar
tec committed
391
392
393
394
395
                        {
                            if let Err(why) = add_reaction.delete(&ctx) {
                                error!("Error deleting react: {:?}", why);
                            };
                            return;
tec's avatar
tec committed
396
397
                        }
                    }
tec's avatar
tec committed
398
399
400
                    Err(why) => {
                        error!("Error getting user role: {:?}", why);
                    }
tec's avatar
tec committed
401
402
403
404
                }
            }
        }
        Err(why) => {
tec's avatar
tec committed
405
            error!("Error processing react: {:?}", why);
tec's avatar
tec committed
406
407
408
409
410
411
412
        }
    }
}

pub fn reaction_remove(ctx: Context, removed_reaction: channel::Reaction) {
    match removed_reaction.message(&ctx.http) {
        Ok(mut message) => {
tec's avatar
tec committed
413
414
415
416
            if let Ok(user) = removed_reaction.user(&ctx) {
                let mut motion_info = get_cached_motion(&ctx, &message);
                if let Some(vote) = motion_info
                    .votes
417
                    .get_mut(&get_string_from_react(removed_reaction.emoji.clone()))
tec's avatar
tec committed
418
419
                {
                    vote.retain(|u| u.id != user.id);
tec's avatar
tec committed
420
                }
Timothy du Heaume's avatar
Timothy du Heaume committed
421
                set_cached_motion(message.id, motion_info);
tec's avatar
tec committed
422
                update_motion(&ctx, &mut message, &user, "remove", removed_reaction);
tec's avatar
tec committed
423
424
425
            }
        }
        Err(why) => {
tec's avatar
tec committed
426
            error!("Error getting user role: {:?}", why);
tec's avatar
tec committed
427
428
429
        }
    }
}