use serenity::framework::standard::CommandError as Error; use serenity::{ collector::ReactionAction, framework::standard::{macros::command, Args, CommandResult}, model::{ channel::{Message, ReactionType}, id::UserId, }, utils::MessageBuilder, }; use std::time::Duration; use std::{ collections::{HashMap as Map, HashSet as Set}, convert::TryFrom, }; use youmubot_prelude::{Duration as ParseDuration, *}; #[command] #[description = "๐ŸŽŒ Cast a poll upon everyone and ask them for opinions!"] #[usage = "[duration] / [question] / [answer #1 = Yes!] / [answer #2 = No!] ..."] #[example = "2m/How early do you get up?/Before 6/Before 7/Before 8/Fuck time"] #[bucket = "voting"] #[only_in(guilds)] #[min_args(2)] #[owner_privilege] pub async fn vote(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { // Parse stuff first let args = args.quoted(); let _duration = args.single::()?; let duration = &_duration.0; if *duration < Duration::from_secs(2) || *duration > Duration::from_secs(60 * 60 * 24) { msg.reply(ctx, format!("๐Ÿ˜’ Invalid duration ({}). The voting time should be between **2 minutes** and **1 day**.", _duration)).await?; return Ok(()); } let question = args.single::()?; let choices = if args.is_empty() { vec![ ("๐Ÿ˜".to_owned(), "Yes! ๐Ÿ˜".to_owned()), ("๐Ÿคข".to_owned(), "No! ๐Ÿคข".to_owned()), ] } else { let choices: Vec<_> = args.iter().map(|v| v.unwrap()).collect(); if choices.len() < 2 { // Where are the choices? msg.reply( ctx, "๐Ÿ˜’ Can't have a nice voting session if you only have one choice.", ) .await?; return Ok(()); } if choices.len() > MAX_CHOICES { // Too many choices! msg.reply( ctx, format!( "๐Ÿ˜ต Too many choices... We only support {} choices at the moment!", MAX_CHOICES ), ) .await?; return Ok(()); } pick_n_reactions(choices.len())? .into_iter() .zip(choices.into_iter()) .collect() }; let fields: Vec<_> = { choices .iter() .map(|(reaction, choice)| { ( MessageBuilder::new().push_bold_safe(choice).build(), format!("React with {}", reaction), true, ) }) .collect() }; // Ok... now we post up a nice voting panel. let channel = msg.channel_id; let author = msg.author.clone(); let panel = channel.send_message(&ctx, |c| { c.content("@here").embed(|e| { e.author(|au| { au.icon_url(author.avatar_url().unwrap_or("".to_owned())) .name(&author.name) }) .title(format!("You have {} to vote!", _duration)) .thumbnail("https://images-ext-2.discordapp.net/external/BK7injOyt4XT8yNfbCDV4mAkwoRy49YPfq-3IwCc_9M/http/cdn.i.ntere.st/p/9197498/image") .description(MessageBuilder::new().push_bold_line_safe(&question).push("\nThis question was asked by ").push(author.mention())) .fields(fields.into_iter()) }) }).await?; msg.delete(&ctx).await?; drop(msg); // React on all the choices choices .iter() .map(|(emote, _)| { panel .react(&ctx, ReactionType::try_from(&emote[..]).unwrap()) .map_ok(|_| ()) }) .collect::>() .try_collect::<()>() .await?; // A handler for votes. let user_reactions: Map> = choices .iter() .map(|(emote, _)| (emote.clone(), Set::new())) .collect(); // Collect reactions... let user_reactions = panel .await_reactions(&ctx) .removed(true) .timeout(*duration) .await .fold(user_reactions, |mut set, reaction| async move { let (reaction, is_add) = match &*reaction { ReactionAction::Added(r) => (r, true), ReactionAction::Removed(r) => (r, false), }; let users = if let ReactionType::Unicode(ref s) = reaction.emoji { if let Some(users) = set.get_mut(s.as_str()) { users } else { return set; } } else { return set; }; let user_id = match reaction.user_id { Some(v) => v, None => return set, }; if is_add { users.insert(user_id); } else { users.remove(&user_id); } set }) .await; // Handle choices let choice_map = choices.into_iter().collect::>(); let mut result: Vec<(String, Vec)> = user_reactions .into_iter() .filter(|(_, users)| !users.is_empty()) .map(|(emote, users)| (emote, users.into_iter().collect())) .collect(); result.sort_unstable_by(|(_, v), (_, w)| w.len().cmp(&v.len())); if result.len() == 0 { msg.reply( &ctx, MessageBuilder::new() .push("no one answer your question ") .push_bold_safe(&question) .push(", sorry ๐Ÿ˜ญ") .build(), ) .await?; return Ok(()); } channel .send_message(&ctx, |c| { c.content({ let mut content = MessageBuilder::new(); content .push("@here, ") .push(author.mention()) .push(" previously asked ") .push_bold_safe(&question) .push(", and here are the results!"); result.into_iter().for_each(|(emote, votes)| { content .push("\n - ") .push_bold(format!("{}", votes.len())) .push(" voted for ") .push(&emote) .push(" ") .push_bold_safe(choice_map.get(&emote).unwrap()) .push(": ") .push( votes .into_iter() .map(|v| v.mention()) .collect::>() .join(", "), ); }); content.build() }) }) .await?; panel.delete(&ctx).await?; Ok(()) // unimplemented!(); } // Pick a set of random n reactions! fn pick_n_reactions(n: usize) -> Result, Error> { use rand::seq::SliceRandom; if n > MAX_CHOICES { Err(Error::from("Too many options")) } else { let mut rand = rand::thread_rng(); Ok(REACTIONS .choose_multiple(&mut rand, n) .map(|v| (*v).to_owned()) .collect()) } } const MAX_CHOICES: usize = 15; // All the defined reactions. const REACTIONS: [&'static str; 90] = [ "๐Ÿ˜€", "๐Ÿ˜", "๐Ÿ˜‚", "๐Ÿคฃ", "๐Ÿ˜ƒ", "๐Ÿ˜„", "๐Ÿ˜…", "๐Ÿ˜†", "๐Ÿ˜‰", "๐Ÿ˜Š", "๐Ÿ˜‹", "๐Ÿ˜Ž", "๐Ÿ˜", "๐Ÿ˜˜", "๐Ÿฅฐ", "๐Ÿ˜—", "๐Ÿ˜™", "๐Ÿ˜š", "โ˜บ๏ธ", "๐Ÿ™‚", "๐Ÿค—", "๐Ÿคฉ", "๐Ÿค”", "๐Ÿคจ", "๐Ÿ˜", "๐Ÿ˜‘", "๐Ÿ˜ถ", "๐Ÿ™„", "๐Ÿ˜", "๐Ÿ˜ฃ", "๐Ÿ˜ฅ", "๐Ÿ˜ฎ", "๐Ÿค", "๐Ÿ˜ฏ", "๐Ÿ˜ช", "๐Ÿ˜ซ", "๐Ÿ˜ด", "๐Ÿ˜Œ", "๐Ÿ˜›", "๐Ÿ˜œ", "๐Ÿ˜", "๐Ÿคค", "๐Ÿ˜’", "๐Ÿ˜“", "๐Ÿ˜”", "๐Ÿ˜•", "๐Ÿ™ƒ", "๐Ÿค‘", "๐Ÿ˜ฒ", "โ˜น๏ธ", "๐Ÿ™", "๐Ÿ˜–", "๐Ÿ˜ž", "๐Ÿ˜Ÿ", "๐Ÿ˜ค", "๐Ÿ˜ข", "๐Ÿ˜ญ", "๐Ÿ˜ฆ", "๐Ÿ˜ง", "๐Ÿ˜จ", "๐Ÿ˜ฉ", "๐Ÿคฏ", "๐Ÿ˜ฌ", "๐Ÿ˜ฐ", "๐Ÿ˜ฑ", "๐Ÿฅต", "๐Ÿฅถ", "๐Ÿ˜ณ", "๐Ÿคช", "๐Ÿ˜ต", "๐Ÿ˜ก", "๐Ÿ˜ ", "๐Ÿคฌ", "๐Ÿ˜ท", "๐Ÿค’", "๐Ÿค•", "๐Ÿคข", "๐Ÿคฎ", "๐Ÿคง", "๐Ÿ˜‡", "๐Ÿค ", "๐Ÿคก", "๐Ÿฅณ", "๐Ÿฅด", "๐Ÿฅบ", "๐Ÿคฅ", "๐Ÿคซ", "๐Ÿคญ", "๐Ÿง", "๐Ÿค“", ]; // Assertions static_assertions::const_assert!(MAX_CHOICES <= REACTIONS.len()); static_assertions::const_assert!(MAX_CHOICES <= REACTIONS.len());