use serenity::{ model::{channel, channel::Message, gateway::Ready, guild::Member}, prelude::*, utils::MessageBuilder, }; use rand::Rng; struct Handler; const SERVER_ID: u64 = 606351521117896704; // #general const MAIN_CHANNEL: serenity::model::id::ChannelId = serenity::model::id::ChannelId(606351521117896706); // #the-corner const WELCOME_CHANNEL: serenity::model::id::ChannelId = serenity::model::id::ChannelId(606351613816209418); const BOT_ID: u64 = 607078903969742848; const VOTE_POOL_SIZE: i8 = 2; const VOTE_ROLE: u64 = 607478818038480937; const TIEBREAKER_ROLE: u64 = 607509283483025409; 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 // is received - the closure (or function) passed will be called. // // 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( [ "I see you have seen fit to send another message ", "Why do you continue to bother us ", "Oh. It's you again ", "What are you doing ", ][rng.gen_range(0, 3)], ); message.mention(&msg.author); if let Err(why) = msg.channel_id.say(&ctx.http, message.build()) { println!("Error sending message: {:?}", why); } } if msg.content.starts_with("!move") { let mut iter = msg.content.chars(); iter.by_ref().nth(5); let topic = iter.as_str(); create_motion(&ctx, &msg, topic); } else if msg.content.starts_with("!motion") { 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 to make a circular motion"); if let Err(why) = msg.channel_id.say(&ctx.http, message.build()) { println!("Error sending message: {:?}", why); } } } 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 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); } } } 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, _guild_id: serenity::model::id::GuildId, new_member: Member, ) { let mut message = MessageBuilder::new(); message.push("Nice to see you here "); message.mention(&new_member); message.push("! Would you care to introduce yourself?"); 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); } } // Set a handler to be called on the `ready` event. This is called when a // shard is booted, and a READY payload is sent by Discord. This payload // contains data like the current user's guild Ids, current user data, // private channels, and more. // // In this case, just print what the current user's username is. fn ready(&self, _: Context, ready: Ready) { println!("{} is connected!", ready.user.name); } } fn main() { // Configure the client with your Discord bot token in the environment. let token = include_str!("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 // by Discord for bot users. let mut client = Client::new(&token, Handler).expect("Err creating client"); // Finally, start a single shard, and start listening to events. // // Shards will automatically attempt to reconnect, and will perform // exponential backoff until it reconnects. if let Err(why) = client.start() { 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.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 }); // m.reactions(&[FOR_VOTE.to_string(),AGAINST_VOTE.to_string(), ABSTAIN_VOTE.to_string()]); m }) { Err(why) => { println!("Error sending message: {:?}", why); } Ok(message) => { if let Err(why) = msg.delete(ctx) { println!("Error deleting motion prompt: {:?}", why); } if let Err(why) = message.react(ctx, FOR_VOTE) { println!("Error sending 👍 react: {:?}", why); } if let Err(why) = message.react(ctx, AGAINST_VOTE) { println!("Error sending 👎 react: {:?}", why); } if let Err(why) = message.react(ctx, ABSTAIN_VOTE) { println!("Error sending 🙊 react: {:?}", 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 ); if let Err(why) = msg.edit(ctx, |m| { m.embed(|e| { 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 }) }) { println!("Error updating motion: {:?}", why); } }