html terminal
Diffstat (limited to 'src/bot/voting.rs')
| -rw-r--r-- | src/bot/voting.rs | 219 |
1 files changed, 219 insertions, 0 deletions
diff --git a/src/bot/voting.rs b/src/bot/voting.rs new file mode 100644 index 0000000..090ac73 --- /dev/null +++ b/src/bot/voting.rs @@ -0,0 +1,219 @@ +use super::{Context, DISABLED, SUCCESS}; +use ::serenity::builder::CreateActionRow; +use ::serenity::builder::CreateEmbed; +use anyhow::Result; +use itertools::Itertools; +use poise::serenity_prelude::CollectComponentInteraction as Interaction; +use poise::serenity_prelude::*; +use std::collections::HashMap; +use std::str::FromStr; +use std::sync::Mutex; +use std::time::SystemTime; + +pub type Vote = usize; +pub enum VoteData { + Running(HashMap<UserId, Vote>), + Finished(Vec<usize>), +} + +impl VoteData { + pub fn get(&mut self) -> &mut HashMap<UserId, Vote> { + match self { + Self::Running(x) => x, + Self::Finished(_) => unreachable!(), + } + } + + pub fn summarize_running(&self, optcount: usize) -> Vec<usize> { + match self { + Self::Running(s) => { + let mut ret = vec![]; + ret.resize(optcount, 0); + for (_, v) in s { + ret[*v] += 1 + } + ret + } + Self::Finished(_) => unreachable!(), + } + } + + pub fn get_summarized(&self) -> &Vec<usize> { + match self { + Self::Finished(ret) => ret, + Self::Running(_) => unreachable!(), + } + } + + pub fn finish(self, optcount: usize) -> Self { + Self::Finished(self.summarize_running(optcount)) + } +} + +pub type Votes = Mutex<Vec<VoteData>>; + +trait Imgor { + fn imageor<S: ToString>(&mut self, img: Option<S>) -> &mut Self; +} + +impl Imgor for CreateEmbed { + fn imageor<S: ToString>(&mut self, img: Option<S>) -> &mut Self { + if let Some(iuri) = img { + self.image(iuri) + } else { + self + } + } +} + +#[poise::command(slash_command, category = "Discord", rename = "create_vote")] +/// make a vote +pub async fn create( + ctx: Context<'_>, + #[description = "picture url"] image: Option<String>, + #[description = "pressables (psv)"] options: String, + #[description = "option styles (psv)"] styles: String, + #[description = "how long the vote will be up"] length: String, + title: String, +) -> Result<()> { + let ctx_id = ctx.id(); + let options = options.split('|').map(|s| s.trim()).collect_vec(); + let styles = styles + .split('|') + .map(|s| s.trim().to_lowercase()) + .map(|s| { + use ButtonStyle::*; + match s.as_str() { + "blue" => Primary, + "gray" => Secondary, + "green" => Success, + "red" => Danger, + _ => Primary, + } + }) + .collect_vec(); + let dur = if let Ok(t) = parse_duration::parse(&length) { + t + } else { + ctx.say(format!("`{length}` is not time")).await?; + return Ok(()); + }; + let end = format!( + "<t:{}:R>", + (SystemTime::now() + dur) + .duration_since(SystemTime::UNIX_EPOCH)? + .as_secs() + ); + macro_rules! update_msg { + // give me unhygenic macros + ($m:expr, $ctx:expr, $image:expr, $title:expr, $options:expr, $ctx_id:expr, $n:expr) => { + $m.embed(|e| { + for (option, votes) in $ctx.data().vote_data.lock().unwrap()[$n] + .summarize_running($options.len()) + .iter() + .enumerate() + { + e.field(&$options[option], votes, true); + } + e.imageor($image.as_ref()) + .color(SUCCESS) + .title(&$title) + .description(format!("vote ends {end}")) + }) + }; + } + let n = { + let mut data = ctx.data().vote_data.lock().unwrap(); + let n = data.len(); + data.push(VoteData::Running(HashMap::new())); + n + }; + let handle = poise::send_reply(ctx, |m| { + update_msg!(m, ctx, image, title, options, ctx_id, n).components(|c| { + c.create_action_row(|r| { + for (n, option) in options.iter().enumerate() { + r.create_button(|b| { + b.custom_id(format!("{}{n}", ctx_id)) + .label(option) + .style(styles[n]) + }); + } + r + }) + }) + }) + .await?; + let ctx_id_len = ctx_id.to_string().len(); + while let Some(press) = Interaction::new(ctx) + .filter(move |press| press.data.custom_id.starts_with(&ctx_id.to_string())) + .timeout(dur) + .await + { + let s = { + if ctx.data().vote_data.lock().unwrap()[n] + .get() + .insert( + press.user.id, + Vote::from_str(&press.data.custom_id[ctx_id_len..]).unwrap(), + ) + .is_some() + { + "updated" + } else { + "voted" + } + }; + println!("got vote!"); + tokio::join!( + press.create_followup_message(ctx, |m| { + m.ephemeral(true).embed(|e| e.title(s).color(SUCCESS)) + }), + press.create_interaction_response(ctx, |c| { + c.kind(InteractionResponseType::UpdateMessage) + .interaction_response_data(|m| { + update_msg!(m, ctx, image, title, options, ctx_id, n) + }) + }) + ) + .0?; + } + println!("vote ended!"); + handle + .edit(ctx, |m| { + m.embed(|e| { + for (option, votes) in ctx + .data() + .vote_data + .lock() + .unwrap() + .remove(n) + .finish(options.len()) + .get_summarized() + .into_iter() + .enumerate() + { + e.field(&options[option], votes, true); + } + e.color(DISABLED) + .title(&title) + .imageor(image.as_ref()) + .description(format!("vote ended!")) + }) + .components(|c| { + c.set_action_row({ + let mut r = CreateActionRow::default(); + for (n, option) in options.iter().enumerate() { + r.create_button(|b| { + b.custom_id(format!("{}{n}", ctx_id)) + .label(option) + .disabled(true) + .style(styles[n]) + }); + } + r + }) + }) + }) + .await?; + Ok(()) +} |