voting.rs 15 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

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
16
        if !motion.is_empty() {
tec's avatar
tec committed
17
            create_motion(&ctx, &msg, motion);
18
            return;
tec's avatar
tec committed
19
        }
tec's avatar
tec committed
20
21
22
23
24
        send_message!(
            msg.channel_id,
            &ctx.http,
            "If there's something you want to motion, put it after the !move keyword"
        );
tec's avatar
tec committed
25
26
    }
    pub fn motion(ctx: Context, msg: Message, _content: &str) {
tec's avatar
tec committed
27
28
29
30
31
        send_message!(
            msg.channel_id,
            &ctx.http,
            "I hope you're not having a motion. You may have wanted to !move something instead."
        );
tec's avatar
tec committed
32
33
34
    }
    pub fn poll(ctx: Context, msg: Message, content: &str) {
        let topic = content;
Timothy du Heaume's avatar
Timothy du Heaume committed
35
        if !topic.is_empty() {
tec's avatar
tec committed
36
            create_poll(&ctx, &msg, topic);
37
            return;
tec's avatar
tec committed
38
        }
tec's avatar
tec committed
39
40
41
42
43
        send_message!(
            msg.channel_id,
            &ctx.http,
            "If there's something you want to motion, put it after the !move keyword"
        );
tec's avatar
tec committed
44
45
    }
    pub fn cowsay(ctx: Context, msg: Message, content: &str) {
tec's avatar
tec committed
46
        let output = if !content.trim().is_empty() {
47
48
49
50
51
            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(), '\'');
tec's avatar
tec committed
52
            std::process::Command::new("cowsay")
53
54
                .arg(text)
                .output()
tec's avatar
tec committed
55
                .expect("failed to execute cowsay")
56
        } else {
tec's avatar
tec committed
57
            std::process::Command::new("sh")
58
                .arg("-c")
tec's avatar
tec committed
59
                .arg("fortune | cowsay -f \"/usr/share/cowsay/cows/$(echo 'www\nhellokitty\nbud-frogs\nkoala\nsuse\nthree-eyes\npony-smaller\nsheep\nvader\ncower\nmoofasa\nelephant\nflaming-sheep\nskeleton\nsnowman\ntux\napt\nmoose' | shuf -n 1).cow\"")
60
                .output()
tec's avatar
tec committed
61
62
                .expect("failed to execute fortune/cowsay")
        };
tec's avatar
tec committed
63
        let mut message = MessageBuilder::new();
tec's avatar
tec committed
64
        message.push_codeblock_safe(
tec's avatar
tec committed
65
66
67
            String::from_utf8(output.stdout).expect("unable to parse stdout to String"),
            None,
        );
68
        send_message!(msg.channel_id, &ctx.http, message.build());
tec's avatar
tec committed
69
70
71
72
    }
}

fn create_motion(ctx: &Context, msg: &Message, topic: &str) {
tec's avatar
tec committed
73
    info!("{} created a motion {}", msg.author.name, topic);
tec's avatar
tec committed
74
    if let Err(why) = msg.delete(ctx) {
tec's avatar
tec committed
75
        error!("Error deleting motion prompt: {:?}", why);
tec's avatar
tec committed
76
    }
Timothy du Heaume's avatar
Timothy du Heaume committed
77
    let result = msg.channel_id.send_message(&ctx.http, |m| {
tec's avatar
tec committed
78
79
80
81
82
83
84
85
86
87
88
89
90
        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();
91
            desc.role(CONFIG.vote_role);
tec's avatar
tec committed
92
93
94
95
96
97
98
99
100
            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![
101
102
103
104
105
            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
106
107
        ]);
        m
Timothy du Heaume's avatar
Timothy du Heaume committed
108
109
110
    });
    if let Err(why) = result {
        error!("Error creating motion: {:?}", why);
tec's avatar
tec committed
111
112
113
114
    }
}

fn create_poll(ctx: &Context, msg: &Message, topic: &str) {
tec's avatar
tec committed
115
    info!("{} created a poll {}", msg.author.name, topic);
tec's avatar
tec committed
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
    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![
137
138
139
            CONFIG.approve_react.to_string(),
            CONFIG.disapprove_react.to_string(),
            CONFIG.unsure_react.to_string(),
tec's avatar
tec committed
140
141
142
143
        ]);
        m
    }) {
        Err(why) => {
tec's avatar
tec committed
144
            error!("Error sending message: {:?}", why);
tec's avatar
tec committed
145
146
147
        }
        Ok(_) => {
            if let Err(why) = msg.delete(ctx) {
tec's avatar
tec committed
148
                error!("Error deleting motion prompt: {:?}", why);
tec's avatar
tec committed
149
150
151
152
153
            }
        }
    }
}

tec's avatar
tec committed
154
155
#[derive(Debug, Clone)]
struct MotionInfo {
156
    votes: HashMap<String, Vec<serenity::model::user::User>>,
tec's avatar
tec committed
157
158
159
160
161
162
163
164
165
166
}

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
167
        info!("Initialising representation of motion {:?}", msg.id);
tec's avatar
tec committed
168
169
170
171
        let this_motion = MotionInfo {
            votes: {
                let mut m = HashMap::new();
                m.insert(
172
                    CONFIG.for_vote.to_string(),
173
                    msg.reaction_users(ctx, CONFIG.for_vote.to_string(), Some(100), None)
tec's avatar
tec committed
174
175
176
                        .unwrap(),
                );
                m.insert(
177
                    CONFIG.against_vote.to_string(),
178
                    msg.reaction_users(ctx, CONFIG.against_vote.to_string(), Some(100), None)
tec's avatar
tec committed
179
180
181
                        .unwrap(),
                );
                m.insert(
182
                    CONFIG.abstain_vote.to_string(),
183
                    msg.reaction_users(ctx, CONFIG.abstain_vote.to_string(), Some(100), None)
tec's avatar
tec committed
184
185
186
187
188
189
190
                        .unwrap(),
                );
                m
            },
        };
        cached_motions.insert(msg.id, this_motion);
    }
Timothy du Heaume's avatar
Timothy du Heaume committed
191
    (*cached_motions.get(&msg.id).unwrap()).clone()
tec's avatar
tec committed
192
}
Timothy du Heaume's avatar
Timothy du Heaume committed
193
194
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
195
        *motion = motion_info;
196
        return;
tec's avatar
tec committed
197
    }
tec's avatar
tec committed
198
    warn!("{}", "Couldn't find motion in cache to set");
tec's avatar
tec committed
199
200
}

tec's avatar
tec committed
201
202
203
204
205
206
207
fn update_motion(
    ctx: &Context,
    msg: &mut Message,
    user: &serenity::model::user::User,
    change: &str,
    reaction: channel::Reaction,
) {
tec's avatar
tec committed
208
209
    let motion_info: MotionInfo = get_cached_motion(ctx, msg);

210
211
212
    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
213

214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
    let has_tiebreaker = |users: &Vec<serenity::model::user::User>| {
        users.iter().any(|u| {
            u.has_role(ctx, CONFIG.server_id, CONFIG.tiebreaker_role)
                .unwrap()
        })
    };

    let for_strength = for_votes as f32
        + (if has_tiebreaker(motion_info.votes.get(&CONFIG.for_vote).unwrap()) {
            0.25
        } else {
            0.0
        });
    let against_strength = against_votes as f32
        + (if has_tiebreaker(motion_info.votes.get(&CONFIG.against_vote).unwrap()) {
            0.25
        } else {
            0.0
        });
    let abstain_strength = abstain_votes as f32
        + (if has_tiebreaker(motion_info.votes.get(&CONFIG.abstain_vote).unwrap()) {
            0.25
        } else {
            0.0
        });
tec's avatar
tec committed
239
240
241
242

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

tec's avatar
tec committed
243
    info!(
tec's avatar
tec committed
244
245
246
        "  {:10} {:6} {} on {}",
        user.name,
        change,
247
        get_string_from_react(&reaction.emoji),
tec's avatar
tec committed
248
249
250
251
        topic
    );

    let update_status = |e: &mut serenity::builder::CreateEmbed,
tec's avatar
tec committed
252
253
254
                         status: &str,
                         last_status_full: String,
                         topic: &str| {
tec's avatar
tec committed
255
256
257
258
259
260
261
262
263
        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
264
            info!("Motion to {} now {}", topic, status);
tec's avatar
tec committed
265
266
267
268
269
270
            //
            let mut message = MessageBuilder::new();
            message.push_bold(topic);
            message.push(" is now ");
            message.push_bold(status);
            message.push_italic(format!(" (was {})", last_status));
271
            send_message!(CONFIG.announcement_channel, &ctx.http, message.build());
tec's avatar
tec committed
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
        }
    };

    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
292
                .find(|f| f.name == "Status")
tec's avatar
tec committed
293
294
295
                .expect("No previous status")
                .clone()
                .value;
296
            if for_strength > (CONFIG.vote_pool_size as f32 / 2.0) {
tec's avatar
tec committed
297
298
                e.colour(serenity::utils::Colour::TEAL);
                update_status(e, "Passed", last_status_full, &topic);
299
            } else if against_strength + abstain_strength > (CONFIG.vote_pool_size as f32 / 2.0) {
tec's avatar
tec committed
300
301
302
303
304
305
306
307
308
309
                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,
310
                    CONFIG.vote_pool_size
tec's avatar
tec committed
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
                ),
                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
326
        error!("Error updating motion: {:?}", why);
tec's avatar
tec committed
327
328
329
330
    }
}

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

pub fn reaction_remove(ctx: Context, removed_reaction: channel::Reaction) {
    match removed_reaction.message(&ctx.http) {
        Ok(mut message) => {
tec's avatar
tec committed
403
404
405
406
            if let Ok(user) = removed_reaction.user(&ctx) {
                let mut motion_info = get_cached_motion(&ctx, &message);
                if let Some(vote) = motion_info
                    .votes
407
                    .get_mut(&get_string_from_react(&removed_reaction.emoji))
tec's avatar
tec committed
408
409
                {
                    vote.retain(|u| u.id != user.id);
tec's avatar
tec committed
410
                }
Timothy du Heaume's avatar
Timothy du Heaume committed
411
                set_cached_motion(message.id, motion_info);
tec's avatar
tec committed
412
                update_motion(&ctx, &mut message, &user, "remove", removed_reaction);
tec's avatar
tec committed
413
414
415
            }
        }
        Err(why) => {
tec's avatar
tec committed
416
            error!("Error getting user role: {:?}", why);
tec's avatar
tec committed
417
418
419
        }
    }
}