diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000000000000000000000000000000000000..0790fd8042775e68268ce93eb792fba29b59fcb2 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,37 @@ +use serenity; + +pub static DISCORD_TOKEN: &str = include_str!("discord_token"); + +pub static SERVER_ID: u64 = 606351521117896704; +// #general +pub static MAIN_CHANNEL: serenity::model::id::ChannelId = + serenity::model::id::ChannelId(606351521117896706); +// #the-corner +pub static WELCOME_CHANNEL: serenity::model::id::ChannelId = + serenity::model::id::ChannelId(606351613816209418); +// #general +pub static ANNOUNCEMENT_CHANNEL: serenity::model::id::ChannelId = + serenity::model::id::ChannelId(606351521117896706); + +pub static BOT_ID: u64 = 607078903969742848; + +pub static VOTE_POOL_SIZE: i8 = 2; +pub static VOTE_ROLE: u64 = 607478818038480937; +pub static TIEBREAKER_ROLE: u64 = 607509283483025409; +pub static UNREGISTERED_MEMBER_ROLE: u64 = 608282247350714408; +pub static REGISTERED_MEMBER_ROLE: u64 = 608282133118582815; + +pub static FOR_VOTE: &str = "ðŸ‘"; +pub static AGAINST_VOTE: &str = "👎"; +pub static ABSTAIN_VOTE: &str = "🙊"; +pub static APPROVE_REACT: &str = "⬆"; +pub static DISAPPROVE_REACT: &str = "⬇"; +pub static UNSURE_REACT: &str = "â”"; +pub static ALLOWED_REACTS: &[&'static str] = &[ + FOR_VOTE, + AGAINST_VOTE, + ABSTAIN_VOTE, + APPROVE_REACT, + DISAPPROVE_REACT, + UNSURE_REACT, +]; diff --git a/src/main.rs b/src/main.rs index 2729df1b811a3460a68ad081eab8a21ece92d502..c14064f4b4b20497ca1ef8902ccdb43940bc7a46 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,43 +4,9 @@ use serenity::{ utils::MessageBuilder, }; -use rand::Rng; - -static DISCORD_TOKEN: &str = include_str!("discord_token"); - -static SERVER_ID: u64 = 606351521117896704; -// #general -static MAIN_CHANNEL: serenity::model::id::ChannelId = - serenity::model::id::ChannelId(606351521117896706); -// #the-corner -static WELCOME_CHANNEL: serenity::model::id::ChannelId = - serenity::model::id::ChannelId(606351613816209418); -// #general -static ANNOUNCEMENT_CHANNEL: serenity::model::id::ChannelId = - serenity::model::id::ChannelId(606351521117896706); - -static BOT_ID: u64 = 607078903969742848; - -static VOTE_POOL_SIZE: i8 = 2; -static VOTE_ROLE: u64 = 607478818038480937; -static TIEBREAKER_ROLE: u64 = 607509283483025409; -static UNREGISTERED_MEMBER_ROLE: u64 = 608282247350714408; -static REGISTERED_MEMBER_ROLE: u64 = 608282133118582815; - -static FOR_VOTE: &str = "ðŸ‘"; -static AGAINST_VOTE: &str = "👎"; -static ABSTAIN_VOTE: &str = "🙊"; -static APPROVE_REACT: &str = "⬆"; -static DISAPPROVE_REACT: &str = "⬇"; -static UNSURE_REACT: &str = "â”"; -static ALLOWED_REACTS: &[&'static str] = &[ - FOR_VOTE, - AGAINST_VOTE, - ABSTAIN_VOTE, - APPROVE_REACT, - DISAPPROVE_REACT, - UNSURE_REACT, -]; +mod config; +mod user_management; +mod voting; macro_rules! e { ($error: literal, $x:expr) => { @@ -60,215 +26,54 @@ impl EventHandler for Handler { // Event handlers are dispatched through a threadpool, and so multiple // events can be dispatched simultaneously. fn message(&self, ctx: Context, msg: Message) { - if msg.author.id.0 == 159652921083035648 { - let mut rng = rand::thread_rng(); - let mut message = MessageBuilder::new(); - message.push( - [ - "That's quite enough from you ", - "Why do you continue to bother us ", - "Oh. It's you again ", - "What are you doing ", - ][rng.gen_range(0, 4)], - ); - message.mention(&msg.author); - if let Err(why) = msg.channel_id.say(&ctx.http, message.build()) { - println!("Error sending message: {:?}", why); - } - } - - let message_content: Vec<_> = msg.content.splitn(2, ' ').collect(); - match message_content[0] { - "!join" => { - e!( - "Unable to get user: {:?}", - serenity::model::id::GuildId(SERVER_ID) - .member(ctx.http.clone(), msg.author.id) - .map(|member| new_member(&ctx, member)) - ); - } - "!move" => { - let motion = message_content[1]; - if motion.len() > 0 { - create_motion(&ctx, &msg, motion); - } else { - e!("Error sending message: {:?}", - msg.channel_id.say( - &ctx.http, - "If there's something you want to motion, put it after the !move keyword", - )); + if msg.content.starts_with("!") { + let message_content: Vec<_> = msg.content[1..].splitn(2, ' ').collect(); + match message_content[0] { + "register" => { + user_management::Commands::register(ctx, msg.clone(), message_content[1]) } - } - "!motion" => { - e!("Error sending message: {:?}", - msg.channel_id.say( - &ctx.http, - "I hope you're not having a motion. You may have wanted to !move something instead." - )); - } - "!poll" => { - let topic = message_content[1]; - if topic.len() > 0 { - create_poll(&ctx, &msg, topic); - } else { - e!("Error sending message: {:?}", - msg.channel_id.say( - &ctx.http, - "If there's something you want to motion, put it after the !move keyword", - )); + "join" => { + user_management::Commands::join(ctx, msg.clone(), message_content[1]); } - } - "!register" => { - let name = message_content[1]; - if name.len() > 0 { + "move" => { + voting::Commands::move_something(ctx, msg.clone(), message_content[1]); + } + "motion" => { + voting::Commands::motion(ctx, msg.clone(), message_content[1]); + } + "poll" => { + voting::Commands::poll(ctx, msg.clone(), message_content[1]); + } + "cowsay" => { + voting::Commands::cowsay(ctx, msg.clone(), message_content[1]); + } + "help" => { + let mut message = MessageBuilder::new(); + message.push_line("Use !move <action> to make a circular motion"); + message + .push_line("Use !poll <proposal> to see what people think about something"); e!( - "Unable to get member: {:?}", - serenity::model::id::GuildId(SERVER_ID) - .member(ctx.http.clone(), msg.author.id) - .map(|mut member| { - e!( - "Unable to remove role: {:?}", - member.remove_role(&ctx.http, UNREGISTERED_MEMBER_ROLE) - ); - e!( - "Unable to edit nickname: {:?}", - member - .edit(&ctx.http, |m| { - let mut rng = rand::thread_rng(); - m.nickname(format!( - "{}, {}", - name, - [ - "The Big Cheese", - "The One and Only", - "The Exalted One", - "not to be trusted", - "The Scoundrel", - "A big fish in a small pond", - ][rng.gen_range(0, 5)] - )); - m - }) - .map(|()| { - e!( - "Unable to add role: {:?}", - member.add_role(&ctx.http, REGISTERED_MEMBER_ROLE) - ); - }) - ); - }) + "Error sending message: {:?}", + msg.channel_id.say(&ctx.http, message.build()) ); - e!("Error deleting register message: {:?}", msg.delete(ctx)); - } else { + } + _ => { e!( "Error sending message: {:?}", msg.channel_id - .say(&ctx.http, "Usage: !register <ucc username>") + .say(&ctx.http, "Unrecognised command. Try !help") ); } } - "!cowsay" => { - let mut text = message_content[1].to_owned(); - text.escape_default(); - // Guess what buddy! You definitely are passing a string to cowsay - text.insert(0, '\''); - text.insert(text.len(), '\''); - let output = std::process::Command::new("cowsay") - .arg(text) - .output() - // btw, if we can't execute cowsay we crash - .expect("failed to execute cowsay"); - let mut message = MessageBuilder::new(); - message.push_codeblock( - String::from_utf8(output.stdout).expect("unable to parse stdout to String"), - None, - ); - e!( - "Error sending message: {:?}", - msg.channel_id.say(&ctx.http, message.build()) - ); - } - "!help" => { - let mut message = MessageBuilder::new(); - message.push_line("Use !move <action> to make a circular motion"); - message.push_line("Use !poll <proposal> to see what people think about something"); - e!( - "Error sending message: {:?}", - msg.channel_id.say(&ctx.http, message.build()) - ); - } - _ => {} } } fn reaction_add(&self, ctx: Context, add_reaction: channel::Reaction) { - match add_reaction.message(&ctx.http) { - Ok(mut message) => { - 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 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 { - if ![APPROVE_REACT, DISAPPROVE_REACT] - .contains(&add_reaction.emoji.as_data().as_str()) - { - if let Err(why) = add_reaction.delete(&ctx) { - println!("Error deleting react: {:?}", why); - }; - } - } - } - Err(why) => { - println!("Error getting user role: {:?}", why); - } - } - } - } - } - Err(why) => { - println!("Error processing react: {:?}", why); - } - } + voting::reaction_add(ctx, add_reaction); } 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); - } - } + voting::reaction_remove(ctx, removed_reaction); } fn guild_member_addition( @@ -277,7 +82,7 @@ impl EventHandler for Handler { _guild_id: serenity::model::id::GuildId, the_new_member: Member, ) { - new_member(&ctx, the_new_member); + user_management::new_member(&ctx, the_new_member); } // Set a handler to be called on the `ready` event. This is called when a @@ -293,7 +98,7 @@ impl EventHandler for Handler { fn main() { // Configure the client with your Discord bot token in the environment. - let token = DISCORD_TOKEN; + let token = config::DISCORD_TOKEN; // Create a new instance of the Client, logging in as a bot. This will // automatically prepend your bot token with "Bot ", which is a requirement @@ -308,245 +113,3 @@ fn main() { println!("Client error: {:?}", why); } } - -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.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(); - 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: 0\nAgainst: 0\nAbstain: 0", true); - embed.timestamp(msg.timestamp.to_rfc3339()); - embed - }); - m.reactions(vec![ - FOR_VOTE, - AGAINST_VOTE, - ABSTAIN_VOTE, - APPROVE_REACT, - DISAPPROVE_REACT, - ]); - m - }) { - Err(why) => { - println!("Error sending message: {:?}", why); - } - Ok(_) => { - if let Err(why) = msg.delete(ctx) { - println!("Error deleting motion prompt: {:?}", why); - } - } - } -} - -fn create_poll(ctx: &Context, msg: &Message, topic: &str) { - println!("{} created a poll {}", msg.author.name, topic); - 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![APPROVE_REACT, DISAPPROVE_REACT, UNSURE_REACT]); - m - }) { - Err(why) => { - println!("Error sending message: {:?}", why); - } - Ok(_) => { - if let Err(why) = msg.delete(ctx) { - println!("Error deleting motion prompt: {:?}", why); - } - } - } -} - -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 - ); - - let update_status = |e: &mut serenity::builder::CreateEmbed, - status: &str, - last_status_full: String, - topic: &str| { - 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, - ); - println!("Motion to {} now {}", topic, status); - // - let mut message = MessageBuilder::new(); - message.push_bold(topic); - message.push(" is now "); - message.push_bold(status); - message.push_italic(format!(" (was {})", last_status)); - if let Err(why) = ANNOUNCEMENT_CHANNEL.say(&ctx.http, message.build()) { - println!("Error sending message: {:?}", why); - }; - } - }; - - 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() - .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); - update_status(e, "Passed", last_status_full, &topic); - } else if against_strength + abstain_strength > (VOTE_POOL_SIZE / 2) as f32 { - 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, - VOTE_POOL_SIZE - ), - format!( - "For: {}\nAgainst: {}\nAbstain: {}", - for_votes, against_votes, abstain_votes - ), - true, - ); - e.timestamp( - old_embed - .timestamp - .expect("Expected embed to have timestamp"), - ); - e - }) - }) { - println!("Error updating motion: {:?}", why); - } -} - -fn new_member(ctx: &Context, mut new_member: Member) { - let mut message = MessageBuilder::new(); - message.push("Nice to see you here "); - message.mention(&new_member); - message.push_line("! Would you care to introduce yourself?"); - message.push_line("If you're not sure where to start, perhaps you could tell us about your projects, your first computer…"); - message.push_line("You should also know that we follow the Freenode Channel Guidelines: https://freenode.net/changuide"); - if let Err(why) = WELCOME_CHANNEL.say(&ctx, message.build()) { - println!("Error sending message: {:?}", why); - } - - let mut message = MessageBuilder::new(); - message.push(format!("Say hi to {} in ", new_member.display_name())); - message.mention(&WELCOME_CHANNEL); - if let Err(why) = MAIN_CHANNEL.say(&ctx, message.build()) { - println!("Error sending message: {:?}", why); - } - - if let Err(why) = new_member.add_role(&ctx.http, UNREGISTERED_MEMBER_ROLE) { - println!("Error adding user role: {:?}", why); - }; -} diff --git a/src/user_management.rs b/src/user_management.rs new file mode 100644 index 0000000000000000000000000000000000000000..be715e3e7f4d55c694f140a8e8b725dc3f361473 --- /dev/null +++ b/src/user_management.rs @@ -0,0 +1,101 @@ +use rand::Rng; +use serenity::{ + model::{channel::Message, guild::Member}, + prelude::*, + utils::MessageBuilder, +}; + +use crate::config; + +macro_rules! e { + ($error: literal, $x:expr) => { + match $x { + Ok(_) => (), + Err(why) => eprintln!($error, why), + } + }; +} + +pub fn new_member(ctx: &Context, mut new_member: Member) { + let mut message = MessageBuilder::new(); + message.push("Nice to see you here "); + message.mention(&new_member); + message.push_line("! Would you care to introduce yourself?"); + message.push_line("If you're not sure where to start, perhaps you could tell us about your projects, your first computer…"); + message.push_line("You should also know that we follow the Freenode Channel Guidelines: https://freenode.net/changuide, and try to avoid defamatory content"); + if let Err(why) = config::WELCOME_CHANNEL.say(&ctx, message.build()) { + println!("Error sending message: {:?}", why); + } + + let mut message = MessageBuilder::new(); + message.push(format!("Say hi to {} in ", new_member.display_name())); + message.mention(&config::WELCOME_CHANNEL); + if let Err(why) = config::MAIN_CHANNEL.say(&ctx, message.build()) { + println!("Error sending message: {:?}", why); + } + + if let Err(why) = new_member.add_role(&ctx.http, config::UNREGISTERED_MEMBER_ROLE) { + println!("Error adding user role: {:?}", why); + }; +} + +pub struct Commands; +impl Commands { + pub fn join(ctx: Context, msg: Message, _content: &str) { + e!( + "Unable to get user: {:?}", + serenity::model::id::GuildId(config::SERVER_ID) + .member(ctx.http.clone(), msg.author.id) + .map(|member| new_member(&ctx, member)) + ); + } + pub fn register(ctx: Context, msg: Message, content: &str) { + let name = content; + if name.len() > 0 { + e!( + "Unable to get member: {:?}", + serenity::model::id::GuildId(config::SERVER_ID) + .member(ctx.http.clone(), msg.author.id) + .map(|mut member| { + e!( + "Unable to remove role: {:?}", + member.remove_role(&ctx.http, config::UNREGISTERED_MEMBER_ROLE) + ); + e!( + "Unable to edit nickname: {:?}", + member + .edit(&ctx.http, |m| { + let mut rng = rand::thread_rng(); + m.nickname(format!( + "{}, {}", + name, + [ + "The Big Cheese", + "The One and Only", + "The Exalted One", + "not to be trusted", + "The Scoundrel", + "A big fish in a small pond", + ][rng.gen_range(0, 5)] + )); + m + }) + .map(|()| { + e!( + "Unable to add role: {:?}", + member.add_role(&ctx.http, config::REGISTERED_MEMBER_ROLE) + ); + }) + ); + }) + ); + e!("Error deleting register message: {:?}", msg.delete(ctx)); + } else { + e!( + "Error sending message: {:?}", + msg.channel_id + .say(&ctx.http, "Usage: !register <ucc username>") + ); + } + } +} diff --git a/src/voting.rs b/src/voting.rs new file mode 100644 index 0000000000000000000000000000000000000000..97aa58ea8632cb227661bf171efb11d37560d8f7 --- /dev/null +++ b/src/voting.rs @@ -0,0 +1,382 @@ +use serenity::{ + model::{channel, channel::Message}, + prelude::*, + utils::MessageBuilder, +}; + +use crate::config; + +macro_rules! e { + ($error: literal, $x:expr) => { + match $x { + Ok(_) => (), + Err(why) => eprintln!($error, why), + } + }; +} + +pub struct Commands; +impl Commands { + pub fn move_something(ctx: Context, msg: Message, content: &str) { + let motion = content; + if motion.len() > 0 { + create_motion(&ctx, &msg, motion); + } else { + e!( + "Error sending message: {:?}", + msg.channel_id.say( + &ctx.http, + "If there's something you want to motion, put it after the !move keyword", + ) + ); + } + } + pub fn motion(ctx: Context, msg: Message, _content: &str) { + e!("Error sending message: {:?}", + msg.channel_id.say( + &ctx.http, + "I hope you're not having a motion. You may have wanted to !move something instead." + )); + } + pub fn poll(ctx: Context, msg: Message, content: &str) { + let topic = content; + if topic.len() > 0 { + create_poll(&ctx, &msg, topic); + } else { + e!( + "Error sending message: {:?}", + msg.channel_id.say( + &ctx.http, + "If there's something you want to motion, put it after the !move keyword", + ) + ); + } + } + pub fn cowsay(ctx: Context, msg: Message, content: &str) { + 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(), '\''); + let output = std::process::Command::new("cowsay") + .arg(text) + .output() + // btw, if we can't execute cowsay we crash + .expect("failed to execute cowsay"); + let mut message = MessageBuilder::new(); + message.push_codeblock( + String::from_utf8(output.stdout).expect("unable to parse stdout to String"), + None, + ); + e!( + "Error sending message: {:?}", + msg.channel_id.say(&ctx.http, message.build()) + ); + } +} + +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.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(); + desc.role(config::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: 0\nAgainst: 0\nAbstain: 0", true); + embed.timestamp(msg.timestamp.to_rfc3339()); + embed + }); + m.reactions(vec![ + config::FOR_VOTE, + config::AGAINST_VOTE, + config::ABSTAIN_VOTE, + config::APPROVE_REACT, + config::DISAPPROVE_REACT, + ]); + m + }) { + Err(why) => { + println!("Error sending message: {:?}", why); + } + Ok(_) => { + if let Err(why) = msg.delete(ctx) { + println!("Error deleting motion prompt: {:?}", why); + } + } + } +} + +fn create_poll(ctx: &Context, msg: &Message, topic: &str) { + println!("{} created a poll {}", msg.author.name, topic); + 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![ + config::APPROVE_REACT, + config::DISAPPROVE_REACT, + config::UNSURE_REACT, + ]); + m + }) { + Err(why) => { + println!("Error sending message: {:?}", why); + } + Ok(_) => { + if let Err(why) = msg.delete(ctx) { + println!("Error deleting motion prompt: {:?}", why); + } + } + } +} + +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, config::FOR_VOTE, None, None) + .unwrap() + .len() as isize + - 1; + let against_votes = msg + .reaction_users(ctx, config::AGAINST_VOTE, None, None) + .unwrap() + .len() as isize + - 1; + let abstain_votes = msg + .reaction_users(ctx, config::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, config::SERVER_ID, config::TIEBREAKER_ROLE) { + Ok(true) => true, + _ => false, + }, + ) + .count() + > 0 + }; + + let for_strength = for_votes as f32 + + (if strength_buff(config::FOR_VOTE) { + 0.5 + } else { + 0.0 + }); + let against_strength = against_votes as f32 + + (if strength_buff(config::AGAINST_VOTE) { + 0.5 + } else { + 0.0 + }); + let abstain_strength = abstain_votes as f32 + + (if strength_buff(config::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 + ); + + let update_status = |e: &mut serenity::builder::CreateEmbed, + status: &str, + last_status_full: String, + topic: &str| { + 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, + ); + println!("Motion to {} now {}", topic, status); + // + let mut message = MessageBuilder::new(); + message.push_bold(topic); + message.push(" is now "); + message.push_bold(status); + message.push_italic(format!(" (was {})", last_status)); + if let Err(why) = config::ANNOUNCEMENT_CHANNEL.say(&ctx.http, message.build()) { + println!("Error sending message: {:?}", why); + }; + } + }; + + 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() + .filter(|f| f.name == "Status") + .next() + .expect("No previous status") + .clone() + .value; + if for_strength > (config::VOTE_POOL_SIZE / 2) as f32 { + e.colour(serenity::utils::Colour::TEAL); + update_status(e, "Passed", last_status_full, &topic); + } else if against_strength + abstain_strength > (config::VOTE_POOL_SIZE / 2) as f32 { + 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, + config::VOTE_POOL_SIZE + ), + format!( + "For: {}\nAgainst: {}\nAbstain: {}", + for_votes, against_votes, abstain_votes + ), + true, + ); + e.timestamp( + old_embed + .timestamp + .expect("Expected embed to have timestamp"), + ); + e + }) + }) { + println!("Error updating motion: {:?}", why); + } +} + +pub fn reaction_add(ctx: Context, add_reaction: channel::Reaction) { + match add_reaction.message(&ctx.http) { + Ok(mut message) => { + if message.author.id.0 == config::BOT_ID { + if let Ok(user) = add_reaction.user(&ctx) { + match user.has_role(&ctx, config::SERVER_ID, config::VOTE_ROLE) { + Ok(true) => { + for react in + [config::FOR_VOTE, config::AGAINST_VOTE, config::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 !config::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 != config::BOT_ID { + update_motion(&ctx, &mut message, &user, "add", add_reaction); + } + } + Ok(false) => { + if user.id.0 != config::BOT_ID { + if ![config::APPROVE_REACT, config::DISAPPROVE_REACT] + .contains(&add_reaction.emoji.as_data().as_str()) + { + if let Err(why) = add_reaction.delete(&ctx) { + println!("Error deleting react: {:?}", why); + }; + } + } + } + Err(why) => { + println!("Error getting user role: {:?}", why); + } + } + } + } + } + Err(why) => { + println!("Error processing react: {:?}", why); + } + } +} + +pub fn reaction_remove(ctx: Context, removed_reaction: channel::Reaction) { + match removed_reaction.message(&ctx.http) { + Ok(mut message) => { + if message.author.id.0 == config::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); + } + } +}