diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000000000000000000000000000000000..570135d8ec9ed067d9931f5d6a741fcabf664ed5 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "(gdb) Launch", + "type": "gdb", + "request": "launch", + "target": "${workspaceFolder}/target/debug/ucc-discord-bot", + "cwd": "${workspaceFolder}", + "gdbpath": "/home/tec/.cargo/bin/rust-gdb", + "arguments": "read latest -e pdf -o" + } + ] +} diff --git a/src/main.rs b/src/main.rs index bf933e89af9115b75b3c08b75edc2dc2c7f491bb..15a17e3ad4396aff78a44e02ee7d6044544a9975 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,12 +4,11 @@ use serenity::{ utils::MessageBuilder, }; -extern crate rand; - use rand::Rng; struct Handler; +const SERVER_ID: u64 = 606351521117896704; // #general const MAIN_CHANNEL: serenity::model::id::ChannelId = serenity::model::id::ChannelId(606351521117896706); @@ -19,9 +18,14 @@ const WELCOME_CHANNEL: serenity::model::id::ChannelId = const BOT_ID: u64 = 607078903969742848; +const VOTE_POOL_SIZE: i8 = 2; const VOTE_ROLE: u64 = 607478818038480937; +const TIEBREAKER_ROLE: u64 = 607509283483025409; -const SERVER_ID: u64 = 606351521117896704; +const FOR_VOTE: &'static str = "ðŸ‘"; +const AGAINST_VOTE: &'static str = "👎"; +const ABSTAIN_VOTE: &'static str = "🙊"; +const ALLOWED_REACTS: &'static [&'static str] = &[FOR_VOTE, AGAINST_VOTE, ABSTAIN_VOTE]; impl EventHandler for Handler { // Set a handler for the `message` event - so that whenever a new message @@ -52,10 +56,9 @@ impl EventHandler for Handler { let topic = iter.as_str(); create_motion(&ctx, &msg, topic); } else if msg.content.starts_with("!motion") { - let mut iter = msg.content.chars(); - iter.by_ref().nth(7); - let topic = iter.as_str(); - create_motion(&ctx, &msg, topic); + if let Err(why) = msg.channel_id.say(&ctx.http, "I hope you're not having a motion. You may have wanted to !move something instead.") { + println!("Error sending message: {:?}", why); + } } else if msg.content == "!help" { let mut message = MessageBuilder::new(); message.push("Use !move <action> to make a circular motion"); @@ -68,20 +71,33 @@ impl EventHandler for Handler { fn reaction_add(&self, ctx: Context, add_reaction: channel::Reaction) { match add_reaction.message(&ctx.http) { Ok(mut message) => { - println!("{:#?}", message.embeds[0]); if message.author.id.0 == BOT_ID { if let Ok(user) = add_reaction.user(&ctx) { match user.has_role(&ctx, SERVER_ID, VOTE_ROLE) { Ok(true) => { - // for reaction in message.reactions { - // // FIXME: this isn't right - // if reaction.me { - // if let Err(why) = add_reaction.delete(&ctx) { - // println!("Error deleting react: {:?}", why); - // }; - // } - // } - updateMotion(&ctx, &mut message, &user); + for react in [FOR_VOTE, AGAINST_VOTE, ABSTAIN_VOTE] + .iter() + .filter(|r| r != &&add_reaction.emoji.as_data().as_str()) + { + for a_user in + message.reaction_users(&ctx, *react, None, None).unwrap() + { + if a_user.id.0 == user.id.0 { + if let Err(why) = add_reaction.delete(&ctx) { + println!("Error deleting react: {:?}", why); + }; + } + } + } + if !ALLOWED_REACTS.contains(&add_reaction.emoji.as_data().as_str()) + { + if let Err(why) = add_reaction.delete(&ctx) { + println!("Error deleting react: {:?}", why); + }; + } + if user.id.0 != BOT_ID { + update_motion(&ctx, &mut message, &user, "add", add_reaction); + } } Ok(false) => { if user.id.0 != BOT_ID { @@ -103,6 +119,21 @@ impl EventHandler for Handler { } } + fn reaction_remove(&self, ctx: Context, removed_reaction: channel::Reaction) { + match removed_reaction.message(&ctx.http) { + Ok(mut message) => { + if message.author.id.0 == BOT_ID { + if let Ok(user) = removed_reaction.user(&ctx) { + update_motion(&ctx, &mut message, &user, "remove", removed_reaction); + } + } + } + Err(why) => { + println!("Error getting user role: {:?}", why); + } + } + } + fn guild_member_addition( &self, ctx: Context, @@ -155,26 +186,21 @@ fn main() { } fn create_motion(ctx: &Context, msg: &Message, topic: &str) { + println!("{} created a motion {}", msg.author.name, topic); match msg.channel_id.send_message(&ctx.http, |m| { m.embed(|embed| { embed.colour(serenity::utils::Colour::GOLD); embed.title(format!("Motion to {}", topic)); let mut desc = MessageBuilder::new(); - desc.push("Motion by "); + desc.role(VOTE_ROLE); + 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: ?\n👎 Against: ?\n🙊 Abstain: ?", - true, - ); - embed.footer(|f| { - f.text("Motion power: 0"); - f - }); + embed.field("Votes", "For: 0\nAgainst: 0\nAbstain: 0", true); embed }); + // m.reactions(&[FOR_VOTE.to_string(),AGAINST_VOTE.to_string(), ABSTAIN_VOTE.to_string()]); m }) { Err(why) => { @@ -184,26 +210,117 @@ fn create_motion(ctx: &Context, msg: &Message, topic: &str) { if let Err(why) = msg.delete(ctx) { println!("Error deleting motion prompt: {:?}", why); } - if let Err(why) = message.react(ctx, "ðŸ‘") { + if let Err(why) = message.react(ctx, FOR_VOTE) { println!("Error sending 👠react: {:?}", why); } - if let Err(why) = message.react(ctx, "👎") { + if let Err(why) = message.react(ctx, AGAINST_VOTE) { println!("Error sending 👎 react: {:?}", why); } - if let Err(why) = message.react(ctx, "🙊") { - println!("Error sending 🤷 react: {:?}", why); + if let Err(why) = message.react(ctx, ABSTAIN_VOTE) { + println!("Error sending 🙊 react: {:?}", why); } } } } -fn updateMotion(ctx: &Context, msg: &mut Message, user: &serenity::model::user::User) { +fn update_motion( + ctx: &Context, + msg: &mut Message, + user: &serenity::model::user::User, + change: &str, + reaction: channel::Reaction, +) { + let for_votes = msg.reaction_users(ctx, FOR_VOTE, None, None).unwrap().len() as isize - 1; + let against_votes = msg + .reaction_users(ctx, AGAINST_VOTE, None, None) + .unwrap() + .len() as isize + - 1; + let abstain_votes = msg + .reaction_users(ctx, ABSTAIN_VOTE, None, None) + .unwrap() + .len() as isize + - 1; + + let strength_buff = |react: &str| { + msg.reaction_users(ctx, react, None, None) + .unwrap() + .iter() + .filter(|u| match u.has_role(ctx, SERVER_ID, TIEBREAKER_ROLE) { + Ok(true) => true, + _ => false, + }) + .count() + > 0 + }; + + let for_strength = for_votes as f32 + (if strength_buff(FOR_VOTE) { 0.5 } else { 0.0 }); + let against_strength = against_votes as f32 + + (if strength_buff(AGAINST_VOTE) { + 0.5 + } else { + 0.0 + }); + let abstain_strength = abstain_votes as f32 + + (if strength_buff(ABSTAIN_VOTE) { + 0.5 + } else { + 0.0 + }); + let old_embed = msg.embeds[0].clone(); + let topic = old_embed.clone().title.unwrap(); + println!( + " {:10} {:6} {} on {}", + user.name, + change, + reaction.emoji.as_data().as_str(), + topic + ); if let Err(why) = msg.edit(ctx, |m| { m.embed(|e| { - e.title(old_embed.title.unwrap()); - e.colour(serenity::utils::Colour::RED); + e.title(&topic); e.description(old_embed.description.unwrap()); + let last_status = old_embed + .fields + .iter() + .filter(|f| f.name == "Status") + .next() + .expect("No previous status") + .clone() + .value; + if for_strength > (VOTE_POOL_SIZE / 2) as f32 { + e.colour(serenity::utils::Colour::TEAL); + e.field("Status", format!("Passed\n_was_ {}", last_status), true); + println!("Motion to {} PASSED", &topic) + } else if against_strength + abstain_strength > (VOTE_POOL_SIZE / 2) as f32 { + e.colour(serenity::utils::Colour::RED); + e.field("Status", format!("Failed\n_was_ {}", last_status), true); + println!("Motion to {} FAILED", &topic) + } else { + e.colour(serenity::utils::Colour::GOLD); + e.field( + "Status", + if last_status != "Under Consideration" { + format!("Under Consideration\n_was_ {}", last_status) + } else { + "Under Consideration".to_string() + }, + true, + ); + } + e.field( + format!( + "Votes ({}/{})", + for_votes + against_votes + abstain_votes, + VOTE_POOL_SIZE + ), + format!( + "For: {}\nAgainst: {}\nAbstain: {}", + for_votes, against_votes, abstain_votes + ), + true, + ); e }) }) {