use super::{Context, DISABLED, SUCCESS}; use ::serenity::builder::CreateActionRow; use ::serenity::builder::CreateEmbed; use anyhow::anyhow; use anyhow::Result; use itertools::Itertools; use poise::serenity_prelude::*; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs::{read_dir, File}; use std::io::BufReader; use std::path::Path; use std::str::FromStr; use std::sync::Mutex; use std::time::SystemTime; use tokio::time::*; pub type Vote = usize; #[derive(Default, Clone, Serialize, Deserialize, Debug)] struct VoteOptions { options: Vec, styles: Vec, title: String, fields: HashMap, image: Option, } #[derive(Serialize, Deserialize, Debug, Clone, Default)] pub struct BeforePushVoteData { votes: HashMap, // you can parse this from the message but its easier to do this deadline: Duration, #[serde(skip)] deadline_changed: Duration, options: VoteOptions, #[serde(skip)] reply: Option>, id: u64, mid: MessageId, cid: ChannelId, endat: u64, } #[derive(Clone, Debug)] pub struct AfterPushVoteData { index: usize, deadline_changed: Duration, options: VoteOptions, id: u64, } macro_rules! read { ($self:expr,$ctx:expr,$v:expr) => {{ $v = $ctx.data().vote_data.lock().unwrap(); match $v.get_mut($self.index).unwrap() { VoteData::Before(x) => x, VoteData::After(_) => unreachable!(), } }}; } #[derive(Debug)] pub enum VoteData { Before(BeforePushVoteData), After(AfterPushVoteData), } pub type Votes = Mutex>; trait EmbedUtil { fn imageor(self, img: Option) -> Self where String: From; fn set_fields, S: ToString>(self, fields: M) -> Self where String: From; } impl EmbedUtil for CreateEmbed { fn imageor>(self, img: Option) -> Self where String: From, { if let Some(iuri) = img { self.image(iuri) } else { self } } fn set_fields, S: ToString>(mut self, fields: M) -> Self where String: From, { for (k, v) in fields { self = self.field(k, v, false); } self } } macro_rules! votes { ($self:expr, $ctx:expr, $v:expr) => { match &mut $self { VoteData::After(a) => &mut read!(a, $ctx, $v).votes, VoteData::Before(b) => &mut b.votes, } }; } impl VoteData { pub fn summarize(&mut self, ctx: &Context<'_>, optcount: usize) -> Vec { let mut ret = vec![0; optcount]; let mut v; for v in votes!(*self, ctx, v).values() { ret[*v] += 1; } ret } fn deadline(&self) -> Duration { match self { Self::After(a) => a.deadline_changed, Self::Before(b) => b.deadline_changed, } } fn id(&self) -> u64 { match self { Self::After(a) => a.id, Self::Before(b) => b.id, } } fn options(&self) -> &VoteOptions { match self { Self::After(a) => &a.options, Self::Before(b) => &b.options, } } fn set_reply(&mut self, ctx: &Context<'_>, reply: Message) { let mut v; let mid = reply.id; let cid = reply.channel_id; let reply = Some(Box::new(reply)); match self { Self::After(a) => { let read = read!(a, ctx, v); read.reply = reply; read.mid = mid; read.cid = cid; } Self::Before(b) => { b.reply = reply; b.mid = mid; b.cid = cid; } } } fn get_reply(&mut self, ctx: &Context<'_>) -> Box { let mut v; match self { Self::After(a) => read!(a, ctx, v).reply.take().unwrap(), Self::Before(b) => b.reply.take().unwrap(), } } fn set_end(&mut self) { let end = self.dead_secs(); match self { Self::Before(x) => x.endat = end, _ => unreachable!(), } } fn dead_secs(&self) -> u64 { (SystemTime::now() + self.deadline()) .duration_since(SystemTime::UNIX_EPOCH) .unwrap() .as_secs() } fn end_stamp(&self) -> String { format!("", self.dead_secs()) } pub async fn begin(mut self, ctx: Context<'_>) -> Result { self.set_end(); let o = self.options(); let handle = poise::send_reply( ctx, poise::CreateReply::default() .embed( CreateEmbed::new() .imageor(o.image.as_ref()) .color(SUCCESS) .title(&o.title) .description(format!("vote ends {}", self.end_stamp())) .set_fields(&o.fields), ) .components(vec![CreateActionRow::Buttons( o.options .iter() .enumerate() .map(|(n, option)| { CreateButton::new(format!("{}{n}", self.id())) .label(option) .style(o.styles[n]) }) .collect(), )]), ) .await?; let msg = handle.into_message().await?; self.set_reply(&ctx, msg); self.push(&ctx).save(&ctx) } fn save_ref(&self, ctx: &Context<'_>) -> Result<()> { let t = self.options().title.clone() + ".vd"; let mut re; let thing = match &self { Self::Before(x) => x, Self::After(y) => { re = ctx.data().vote_data.lock().unwrap(); match re.get_mut(y.index).unwrap() { Self::Before(x) => x, Self::After(_) => unreachable!(), } } }; std::fs::write(t, serde_json::to_string(thing)?)?; Ok(()) } pub fn save(self, ctx: &Context<'_>) -> Result { self.save_ref(ctx)?; Ok(self) } pub fn push(self, ctx: &Context<'_>) -> Self { let mut data = ctx.data().vote_data.lock().unwrap(); let n = data.len(); let v = Self::After(AfterPushVoteData { index: n, id: self.id(), deadline_changed: self.deadline(), options: self.options().clone(), }); data.push(self); v } fn remove(self, ctx: &Context<'_>) -> Self { match self { Self::After(x) => ctx.data().vote_data.lock().unwrap().remove(x.index), Self::Before(_) => unreachable!(), } } pub async fn input(mut self, ctx: &Context<'_>) -> Result { let dead = self.deadline(); let ctx_id = self.id(); let ctx_id_len = ctx_id.to_string().len(); let o = self.options().clone(); let timestamp = self.end_stamp(); while let Some(press) = ComponentInteractionCollector::new(ctx) .filter(move |press| press.data.custom_id.starts_with(&ctx_id.to_string())) .timeout(dead) .await { let s = { let mut v; if votes!(self, ctx, v) .insert( press.user.id, Vote::from_str(&press.data.custom_id[ctx_id_len..]).unwrap(), ) .is_some() { "updated" } else { "voted" } }; self.save_ref(ctx)?; let (_m, _) = tokio::join!( press.create_followup( ctx, CreateInteractionResponseFollowup::default() .ephemeral(true) .content(s) ), press.create_response( ctx, CreateInteractionResponse::UpdateMessage( CreateInteractionResponseMessage::new().embed({ let mut e = CreateEmbed::new(); for (option, votes) in self.summarize(ctx, o.options.len()).iter().enumerate() { e = e.field(&o.options[option], votes.to_string(), true); } e.imageor(o.image.as_ref()) .color(SUCCESS) .title(&o.title) .set_fields(&o.fields) .description(format!("vote ends {timestamp}")) }) ) ) ); // let m = m?; // let http = ctx.serenity_context().http.clone(); // tokio::spawn(async move { // sleep(Duration::from_secs(10)).await; // m.delete(http).await.unwrap(); // }); } Ok(self) } pub async fn finish(mut self, ctx: &Context<'_>) -> Result<()> { let o = self.options().clone(); let filename = format!("{}.vd", o.title); let p = std::path::Path::new(&filename); if p.exists() { let _ = std::fs::remove_file(p); } self.get_reply(ctx) .edit( ctx, EditMessage::default() .embed({ let mut e = CreateEmbed::new(); for (option, votes) in self .remove(ctx) .summarize(ctx, o.options.len()) .iter() .enumerate() { e = e.field(&o.options[option], votes.to_string(), true); } e.color(DISABLED) .title(&o.title) .imageor(o.image.as_ref()) .set_fields(o.fields) .description("vote ended!".to_string()) }) .components(vec![CreateActionRow::Buttons( o.options .iter() .enumerate() .map(|(n, option)| { CreateButton::new("_") .label(option) .disabled(true) .style(o.styles[n]) }) .collect(), )]), ) .await?; Ok(()) } } trait Parsing { fn parse_pscsp(self) -> HashMap; fn parse_psv(self) -> Vec; fn parse_psv_to_styles(self) -> Vec; } impl Parsing for S where S: AsRef, { fn parse_pscsp(self) -> HashMap { let pair = self.as_ref().split('|'); let pairs = pair.map(|s| { s.split(':') .map(|s| s.trim().to_owned()) .collect_tuple() .unwrap() }); let mut map = HashMap::new(); for (k, v) in pairs { map.insert(k, v); } map } fn parse_psv(self) -> Vec { self.as_ref() .split('|') .map(|s| s.trim().to_owned()) .collect() } fn parse_psv_to_styles(self) -> Vec { self.as_ref() .split('|') .map(|s| { use ButtonStyle::{Danger, Primary, Secondary, Success}; match s.trim().to_lowercase().as_str() { // "blue" => Primary, "gray" => Secondary, "green" => Success, "red" => Danger, _ => Primary, } }) .collect() } } #[poise::command( slash_command, category = "Discord", rename = "create_vote", default_member_permissions = "ADMINISTRATOR", required_permissions = "ADMINISTRATOR" )] /// make a vote pub async fn create( ctx: Context<'_>, #[description = "picture url"] image: Option, #[description = "pressables (psv)"] options: String, #[description = "option styles (psv) {blue|gray|green|red}"] styles: Option, #[description = "how long the vote will be up"] length: String, #[description = "fields (pipe separated colon seperated pairs) (a:b|c:d)"] fields: Option< String, >, title: String, ) -> Result<()> { let options = options.parse_psv(); let styles = styles.map_or( vec![ButtonStyle::Primary; options.len()], Parsing::parse_psv_to_styles, ); let fields = fields.as_ref().map_or(HashMap::new(), Parsing::parse_pscsp); let Ok(dur) = parse_duration::parse(&length) else { ctx.say(format!("`{length}` is not time")).await?; return Ok(()); }; VoteData::Before({ BeforePushVoteData { votes: HashMap::new(), deadline: dur, deadline_changed: dur, options: VoteOptions { options, styles, title, fields, image, }, reply: None, id: ctx.id(), ..Default::default() } }) .begin(ctx) .await? .input(&ctx) .await? .finish(&ctx) .await } async fn fix(ctx: &Context<'_>, data: BufReader) -> Result<()> { let mut v: BeforePushVoteData = serde_json::from_reader(data)?; let m = ctx.http().get_message(v.cid, v.mid).await?; let end = dbg!(m.timestamp.unix_timestamp()) as u64; v.reply = Some(Box::new(m)); let now = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap() .as_secs(); let end = end + dbg!(v.deadline.as_secs()); println!("@{now} | :{end}"); // cant use abs() because unsigned v.deadline_changed = if now < end { Duration::from_secs(end - now) } else { Duration::from_secs(now - end) }; let v = VoteData::Before(v); if end < now { v.push(ctx).finish(ctx).await } else { v.push(ctx).input(ctx).await?.finish(ctx).await } } #[poise::command( slash_command, category = "Discord", default_member_permissions = "ADMINISTRATOR", required_permissions = "ADMINISTRATOR" )] pub async fn fixall(ctx: Context<'_>) -> Result<()> { use futures::future; let mut futs = vec![]; for e in read_dir(".")? { let e = e?; let fname = e.file_name(); let p = Path::new(&fname); if let Some(ext) = p.extension() { if ext == "vd" { futs.push(fix(&ctx, BufReader::new(File::open(p).unwrap()))); } } } let msg = format!("fixed {}", futs.len()); poise::send_reply( ctx, poise::CreateReply::default().content(msg).ephemeral(true), ) .await?; future::join_all(futs).await; Ok(()) } // #[poise::command( // context_menu_command = "fix vote", // slash_command, // category = "Discord", // rename = "fix_vote", // required_permissions = "ADMINISTRATOR" // )] // /// restart vote (use ctx menu) // pub async fn reinstate( // ctx: Context<'_>, // #[description = "previous vote, id or link"] m: Message, // ) -> Result<()> { // static RE: LazyLock = LazyLock::new(|| Regex::new(r#""#).unwrap()); // let e = m.embeds.get(0).ok_or(anyhow!("no embed?"))?; // let end = u64::from_str( // RE.captures( // m.embeds // .get(0) // .ok_or(anyhow!("no embed?"))? // .description // .as_ref() // .ok_or(anyhow!("no desc?"))?, // ) // .ok_or(anyhow!("no timestamp?"))? // .get(1) // .unwrap() // .as_str(), // ) // .unwrap(); // let now = SystemTime::now() // .duration_since(SystemTime::UNIX_EPOCH) // .unwrap() // .as_secs(); // let f = BufReader::new(std::fs::File::open( // e.title.to_owned().ok_or(anyhow!("no title?"))? + ".vd", // )?); // poise::send_reply(ctx, |m| m.content("yes").ephemeral(true)).await?; // let mut v: BeforePushVoteData = serde_json::from_reader(f)?; // v.reply = Some(Box::new(m)); // // cant use abs() because unsigned // v.deadline = if now < end { // Duration::from_secs(end - now) // } else { // Duration::from_secs(now - end) // }; // let v = VoteData::Before(v); // if end < now { // v.push(&ctx).finish(&ctx).await // } else { // v.push(&ctx).input(&ctx).await?.finish(&ctx).await // } // } // voters #[poise::command(slash_command, category = "Discord", rename = "votes")] pub async fn list(ctx: Context<'_>, #[description = "the vote title"] vote: String) -> Result<()> { let vd = { let buf = ctx.data().vote_data.lock().unwrap(); match &buf[buf .iter() .position(|x| x.options().title == vote) .ok_or(anyhow!("vote doesnt exist"))?] { VoteData::Before(x) => x.clone(), VoteData::After(_) => unreachable!(), } }; poise::send_reply( ctx, poise::CreateReply::default().embed({ let mut e = CreateEmbed::default(); let mut votes: HashMap> = HashMap::new(); for (user, vote) in vd.votes { votes.entry(vote).or_default().push(user.get()); } for (vote, voters) in votes { let mut s = vec![]; s.reserve(voters.len()); for person in voters { s.push(format!("<@{person}>")); } e = e.field(&vd.options.options[vote], s.join("\n"), false); } e.color(SUCCESS) .title(format!("voter list for {vote}")) .footer(CreateEmbedFooter::new("privacy is a illusion")) }), ) .await?; Ok(()) }