diff --git a/Cargo.toml b/Cargo.toml index bcabb5a009f44a781df6a9f1f1d52a347b3112cc..6b2dc220297bfb262421692db5b1c6679e9cc4dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,4 +22,3 @@ diesel = { version = "1.4.3", features = ["sqlite"] } ldap3 = "0.6" url = "^2.1" regex = "^1.3" -toml = "0.5" diff --git a/src/config.rs b/src/config.rs index b14edafc5f9dfc80364c4acea24cad3400b16566..86287a5e16e6bc1403d372bdb82b5fade0c4e802 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,6 @@ use indexmap::IndexMap; use serde::Deserialize; use serde_yaml; -use toml; use serenity::model::id; use std::fs; @@ -10,11 +9,6 @@ lazy_static! { pub static ref CONFIG: UccbotConfig = serde_yaml::from_str(&CONFIG_FILE).unwrap(); } -lazy_static! { - static ref SECRETS_FILE: String = fs::read_to_string("secrets.toml").unwrap(); - pub static ref SECRETS: UccbotSecrets = toml::from_str(&SECRETS_FILE).unwrap(); -} - #[derive(Debug, Deserialize)] pub struct UccbotConfig { pub server_id: u64, @@ -38,6 +32,7 @@ pub struct UccbotConfig { pub react_role_messages: Vec<ReactionMapping>, #[serde(default = "ldap_bind_address")] pub bind_address: String, + pub ldap_pass: String, } pub fn ldap_bind_address() -> String { @@ -57,12 +52,6 @@ impl UccbotConfig { } } -#[derive(Debug, Deserialize)] -pub struct UccbotSecrets { - pub ldap_pass: String, - pub discord_token: String, -} - pub type ReactRoleMap = IndexMap<String, id::RoleId>; #[derive(Debug, Deserialize, Clone)] diff --git a/src/ldap.rs b/src/ldap.rs index 44a6ebd211656b25aef50470fc34d9e33ff619f1..bf9aa123d44d788ba7b5883d43ed9cbac72fa296 100644 --- a/src/ldap.rs +++ b/src/ldap.rs @@ -1,5 +1,5 @@ use ldap3::{LdapConn, LdapConnSettings, Scope, SearchEntry}; -use crate::config::{CONFIG, SECRETS}; +use crate::config::CONFIG; #[derive(Debug)] pub struct LDAPUser { @@ -14,7 +14,7 @@ pub fn ldap_search(username: &str) -> Option<LDAPUser> { .expect("Unable to connect to LDAP"); ldap.simple_bind( "cn=ucc-discord-bot,cn=Users,dc=ad,dc=ucc,dc=gu,dc=uwa,dc=edu,dc=au", - &SECRETS.ldap_pass, + &CONFIG.ldap_pass, ) .expect("Unable to attempt to bind to LDAP") .success() @@ -29,8 +29,8 @@ pub fn ldap_search(username: &str) -> Option<LDAPUser> { .expect("LDAP error") .success() .expect("LDAP search error"); - if rs.is_empty() { - return None + if rs.len() != 1 { + return None; } let result = SearchEntry::construct(rs[0].clone()).attrs; Some(LDAPUser { @@ -50,7 +50,10 @@ pub fn ldap_search(username: &str) -> Option<LDAPUser> { } pub fn ldap_exists(username: &str) -> bool { - ldap_search(username).is_some() + match ldap_search(username) { + Some(_) => true, + None => false, + } } #[derive(Debug)] diff --git a/src/main.rs b/src/main.rs index e4ded92b0fe817fcbbc0f24822150738d21cdbfc..439e38e279472f7771f7a6637c45dd734207ebc0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,7 @@ extern crate diesel; extern crate ldap3; use simplelog::*; -use std::fs::File; +use std::fs::{read_to_string, File}; use chrono::prelude::Utc; use serenity::{ @@ -32,7 +32,7 @@ mod token_management; mod user_management; mod voting; -use config::{CONFIG, SECRETS}; +use config::CONFIG; use reaction_roles::{add_role_by_reaction, remove_role_by_reaction}; use util::get_string_from_react; @@ -130,12 +130,12 @@ impl EventHandler for Handler { Ok(message) => match get_message_type(&message) { MessageType::RoleReactMessage if add_reaction.user_id.0 != CONFIG.bot_id => { add_role_by_reaction(&ctx, message, add_reaction); - return + return; } _ if message.author.id.0 != CONFIG.bot_id || add_reaction.user_id == CONFIG.bot_id => { - return + return; } MessageType::Motion => voting::reaction_add(ctx, add_reaction), MessageType::LogReact => { @@ -146,7 +146,7 @@ impl EventHandler for Handler { "The logreact message {} just tried to use is too old", react_user.name ); - return + return; } info!( "The react {} just added is {:?}. In full: {:?}", @@ -172,12 +172,12 @@ impl EventHandler for Handler { Ok(message) => match get_message_type(&message) { MessageType::RoleReactMessage if removed_reaction.user_id != CONFIG.bot_id => { remove_role_by_reaction(&ctx, message, removed_reaction); - return + return; } _ if message.author.id.0 != CONFIG.bot_id || removed_reaction.user_id == CONFIG.bot_id => { - return + return; } MessageType::Motion => voting::reaction_remove(ctx, removed_reaction), _ => {} @@ -222,10 +222,13 @@ fn main() { ]) .unwrap(); + // Configure the client with your Discord bot token in the environment. + let token = read_to_string("discord_token").unwrap(); + // 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(&SECRETS.discord_token, Handler).expect("Err creating client"); + let mut client = Client::new(&token, Handler).expect("Err creating client"); // Finally, start a single shard, and start listening to events. // diff --git a/src/reaction_roles.rs b/src/reaction_roles.rs index fbc188d215de9e6b5ecaac0f972189923df17911..f0f1b38b009b00eedde06b9fd25098e57fd7215a 100644 --- a/src/reaction_roles.rs +++ b/src/reaction_roles.rs @@ -97,7 +97,7 @@ pub fn sync_all_role_reactions(ctx: &Context) { for react in &message.reactions { let react_as_string = get_string_from_react(&react.reaction_type); if mapping.contains_key(&react_as_string) { - continue + continue; } info!( " message #{}: Removing non-role react '{}'", diff --git a/src/token_management.rs b/src/token_management.rs index 49a5039a92de3a9763fd7631f54cdb482f78d3ed..d32a067c08e90d47161acc3c24947a9005467e7f 100644 --- a/src/token_management.rs +++ b/src/token_management.rs @@ -58,8 +58,6 @@ impl std::fmt::Display for TokenError { } } -pub static TOKEN_LIFETIME: i64 = 300; // 5 minutes - pub fn parse_token(discord_user: &User, encrypted_token: &str) -> Result<String, TokenError> { guard!(let Some(token) = text_decrypt(encrypted_token) else { return Err(TokenError::TokenInvalid) @@ -75,15 +73,15 @@ pub fn parse_token(discord_user: &User, encrypted_token: &str) -> Result<String, let token_username = token_components[2]; if token_discord_user != discord_user.id.0.to_string() { warn!("... attempt failed : DiscordID mismatch"); - return Err(TokenError::DiscordIdMismatch) + return Err(TokenError::DiscordIdMismatch); } let time_delta_seconds = Utc::now().timestamp() - token_timestamp.timestamp(); - if time_delta_seconds > TOKEN_LIFETIME { + if time_delta_seconds > 5 * 60 { warn!( "... attempt failed : token expired ({} seconds old)", time_delta_seconds ); - return Err(TokenError::TokenExpired) + return Err(TokenError::TokenExpired); } info!( "... verification successful (token {} seconds old)", diff --git a/src/user_management.rs b/src/user_management.rs index 4dca8198f59bad32e72c6a0dc5a847c496560156..2c31a17891056e7828ad68a34b8942bb4337131f 100644 --- a/src/user_management.rs +++ b/src/user_management.rs @@ -34,7 +34,7 @@ pub fn new_member(ctx: &Context, mut new_member: Member) { if let Err(why) = new_member.add_role(&ctx.http, CONFIG.unregistered_member_role) { error!("Error adding user role: {:?}", why); - } + }; } fn member_nickname(member: &database::Member) -> String { @@ -78,7 +78,7 @@ impl Commands { &ctx.http, format!("Usage: {}register <username>", CONFIG.command_prefix) ); - return + return; } if RESERVED_NAMES.contains(&account_name) || database::username_exists(account_name) { send_message!( @@ -88,7 +88,7 @@ impl Commands { .choose(&mut rand::thread_rng()) .expect("We couldn't get any sass") ); - return + return; } if !ldap_exists(account_name) { send_message!( @@ -99,7 +99,7 @@ impl Commands { account_name ) ); - return + return; } send_message!( msg.channel_id, @@ -200,7 +200,7 @@ impl Commands { &ctx.http, "Sorry, I couldn't find that profile (you need to !register for a profile)" ); - return + return; } let member = possible_member.unwrap(); let result = msg.channel_id.send_message(&ctx.http, |m| { @@ -250,15 +250,29 @@ impl Commands { } pub fn set_info(ctx: Context, msg: Message, info: &str) { if info.trim().is_empty() { - send_message!( - msg.channel_id, - &ctx.http, - format!( - "Usage: {}set <bio|git|web|photo> <value>", - CONFIG.command_prefix - ) - ); - return + msg.channel_id + .send_message(&ctx.http, |m| { + m.embed(|embed| { + embed.colour(serenity::utils::Colour::LIGHT_GREY); + embed.title("Usage"); + embed.description( + format!( + "`{}set <field> <info>` or `{}clear <field>`", + CONFIG.command_prefix, + CONFIG.command_prefix, + ) + ); + embed.field("Biography", format!("`{}set bio <info>`\nBe friendly! Provide a little introduction to yourself.", CONFIG.command_prefix), false); + embed.field("Git", format!("`{}set git <url>`\nA link to your git forge profile. Also takes a github username for convinience", CONFIG.command_prefix), false); + embed.field("Photo", format!("`{}set photo <url>`\nPut a face to a name! Provide a profile photo.", CONFIG.command_prefix), false); + embed.field("Website", format!("`{}set web <info>`\nGot a personal website? Share it here :)", CONFIG.command_prefix), false); + embed.field("Studying", format!("`{}set study <info>`\nYou're (probably) a Uni student, what's your major?", CONFIG.command_prefix), false); + embed + }); + m + }) + .expect("Failed to send usage help embed"); + return; } let info_content: Vec<_> = info.splitn(2, ' ').collect(); let mut property = String::from(info_content[0]); @@ -266,23 +280,43 @@ impl Commands { if info_content.len() == 1 || !vec!["bio", "git", "web", "photo", "study"].contains(&property.as_str()) { - send_message!( - msg.channel_id, - &ctx.http, - format!( - "Usage: {}set {} {}", - CONFIG.command_prefix, - property, - match property.as_str() { - "bio" => "some information about yourself :)", - "git" => "a url to your git{hub,lab} account", - "photo" => "a url to a profile photo online", - "web" => "a url to your website/webpage", - _ => "whatever you want, because this does absolutely nothing. Try !set to see what you can do" - } - ) - ); - return + msg.channel_id + .send_message(&ctx.http, |m| { + m.embed(|embed| { + embed.colour(serenity::utils::Colour::LIGHT_GREY); + embed.title("Usage"); + embed.field( + match property.as_str() { + "bio" => "Biography", + "git" => "Git Forge Profile", + "photo" => "Profile Photo", + "web" => "Personal Website", + "study" => "Area of study", + _ => "???", + }, + format!( + "`{}set {} <info>` or `{}clear {}`\n{}", + CONFIG.command_prefix, + property, + CONFIG.command_prefix, + property, + match property.as_str() { + "bio" => "Some information about yourself :)", + "git" => "A url to your git{hub,lab} account", + "photo" => "A url to a profile photo online", + "web" => "A url to your website/webpage", + "study" => "Your degree title", + _ => "Whatever you want, because this does absolutely nothing.", + } + ), + false, + ); + embed + }); + m + }) + .expect("Failed to send usage embed"); + return; } let mut value = info_content[1].to_string(); @@ -299,7 +333,7 @@ impl Commands { &ctx.http, "That ain't a URL where I come from..." ); - return + return; } } } @@ -366,7 +400,7 @@ impl Commands { Commands::set_info(ctx, msg, ""); return; } - match field { + let clear_property = match field { "bio" => database::set_member_bio(&msg.author.id.0, None), "git" => database::set_member_git(&msg.author.id.0, None), "photo" => database::set_member_photo(&msg.author.id.0, None), diff --git a/src/voting.rs b/src/voting.rs index 5f2e05f5a3ba52add0eeabf62cfc738f85b88872..bbb5c7daa53975674a364b9b1787eef9a78aac3c 100644 --- a/src/voting.rs +++ b/src/voting.rs @@ -15,7 +15,7 @@ impl Commands { let motion = content; if !motion.is_empty() { create_motion(&ctx, &msg, motion); - return + return; } send_message!( msg.channel_id, @@ -34,7 +34,7 @@ impl Commands { let topic = content; if !topic.is_empty() { create_poll(&ctx, &msg, topic); - return + return; } send_message!( msg.channel_id, @@ -193,24 +193,11 @@ fn get_cached_motion(ctx: &Context, msg: &Message) -> MotionInfo { fn set_cached_motion(id: serenity::model::id::MessageId, motion_info: MotionInfo) { if let Some(motion) = MOTIONS_CACHE.lock().unwrap().get_mut(&id) { *motion = motion_info; - return + return; } warn!("{}", "Couldn't find motion in cache to set"); } -macro_rules! tiebreaker { - ($ctx: expr, $vote: expr, $motion_info: expr) => { - if $motion_info.votes.get($vote).unwrap().iter().any(|u| { - u.has_role($ctx, CONFIG.server_id, CONFIG.tiebreaker_role) - .unwrap() - }) { - 0.25 - } else { - 0.0 - } - } -} - fn update_motion( ctx: &Context, msg: &mut Message, @@ -224,9 +211,31 @@ fn update_motion( let against_votes = motion_info.votes.get(&CONFIG.against_vote).unwrap().len() as isize - 1; let abstain_votes = motion_info.votes.get(&CONFIG.abstain_vote).unwrap().len() as isize - 1; - let for_strength = for_votes as f32 + tiebreaker!(ctx, &CONFIG.for_vote, motion_info); - let against_strength = against_votes as f32 + tiebreaker!(ctx, &CONFIG.against_vote, motion_info); - let abstain_strength = abstain_votes as f32 + tiebreaker!(ctx, &CONFIG.abstain_vote, motion_info); + let has_tiebreaker = |users: &Vec<serenity::model::user::User>| { + users.iter().any(|u| { + u.has_role(ctx, CONFIG.server_id, CONFIG.tiebreaker_role) + .unwrap() + }) + }; + + let for_strength = for_votes as f32 + + (if has_tiebreaker(motion_info.votes.get(&CONFIG.for_vote).unwrap()) { + 0.25 + } else { + 0.0 + }); + let against_strength = against_votes as f32 + + (if has_tiebreaker(motion_info.votes.get(&CONFIG.against_vote).unwrap()) { + 0.25 + } else { + 0.0 + }); + let abstain_strength = abstain_votes as f32 + + (if has_tiebreaker(motion_info.votes.get(&CONFIG.abstain_vote).unwrap()) { + 0.25 + } else { + 0.0 + }); let old_embed = msg.embeds[0].clone(); let topic = old_embed.clone().title.unwrap();