voting.rs 14.8 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
}

201
202
203
204
205
206
207
208
209
210
211
212
213
macro_rules! tiebreaker {
    ($ctx: expr, $vote: expr, $motion_info: expr) => {
        if $motion_info.votes.get($vote).unwrap().iter().any(|u| {
            u.has_role($ctx, CONFIG.server_id, CONFIG.tiebreaker_role)
                .unwrap()
        }) {
            0.25
        } else {
            0.0
        }
    };
}

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

223
224
225
    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
226

227
228
229
230
231
    let for_strength = for_votes as f32 + tiebreaker!(ctx, &CONFIG.for_vote, motion_info);
    let against_strength =
        against_votes as f32 + tiebreaker!(ctx, &CONFIG.against_vote, motion_info);
    let abstain_strength =
        abstain_votes as f32 + tiebreaker!(ctx, &CONFIG.abstain_vote, motion_info);
tec's avatar
tec committed
232
233
234
235

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

tec's avatar
tec committed
236
    info!(
tec's avatar
tec committed
237
238
239
        "  {:10} {:6} {} on {}",
        user.name,
        change,
240
        get_string_from_react(&reaction.emoji),
tec's avatar
tec committed
241
242
243
244
        topic
    );

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

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

pub fn reaction_add(ctx: Context, add_reaction: channel::Reaction) {
324
    let react_as_string = get_string_from_react(&add_reaction.emoji);
tec's avatar
tec committed
325
326
    match add_reaction.message(&ctx.http) {
        Ok(mut message) => {
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
            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
344
                        {
345
346
347
348
349
                            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
350
                            }
tec's avatar
tec committed
351
352
                        }
                    }
353
354
355
356
357
358
359
360
361
362
363
364
                    // 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
365
                    }
366
367
368
369
370
371
372
373
374
375
376
377
378
379
                    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
380
                    }
tec's avatar
tec committed
381
                }
382
383
384
                Err(why) => {
                    error!("Error getting user role: {:?}", why);
                }
tec's avatar
tec committed
385
386
387
            }
        }
        Err(why) => {
tec's avatar
tec committed
388
            error!("Error processing react: {:?}", why);
tec's avatar
tec committed
389
390
391
392
393
394
395
        }
    }
}

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