diff --git a/Cargo.toml b/Cargo.toml index d31667ac9de90c09f3d6cc722c804cf022c5f5a1..6b2dc220297bfb262421692db5b1c6679e9cc4dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,7 @@ simplelog = "^0.7.4" guard = "0.5.0" indexmap = { version = "1.3.1", features = ["serde-1"] } rayon = "1.3.0" +diesel = { version = "1.4.3", features = ["sqlite"] } +ldap3 = "0.6" +url = "^2.1" +regex = "^1.3" diff --git a/src/config.rs b/src/config.rs index c6f922a99fda487fb1daabdec68b214324edeaa5..659da0a3c825965339fa83ac12945fdb61674161 100644 --- a/src/config.rs +++ b/src/config.rs @@ -15,6 +15,7 @@ pub struct UccbotConfig { pub main_channel: id::ChannelId, pub welcome_channel: id::ChannelId, pub announcement_channel: id::ChannelId, + pub readme_channel: id::ChannelId, pub bot_id: u64, pub vote_pool_size: i8, pub vote_role: u64, diff --git a/src/config.ucc.yml b/src/config.ucc.yml index 675a201232c271e9181495834e91546669774ad2..04f7cbc9d79bab02bb8b94411b5136769147e74a 100644 --- a/src/config.ucc.yml +++ b/src/config.ucc.yml @@ -2,6 +2,7 @@ server_id: 264401248676085760 # ucc main_channel: 264401248676085760 # ucc welcome_channel: 606750983699300372 # welcome announcement_channel: 264411219627212801 # committee +readme_channel: 674252245008908298 # readme bot_id: 635407267881156618 diff --git a/src/database.rs b/src/database.rs new file mode 100644 index 0000000000000000000000000000000000000000..1498c38c0288645095b804e017e67da88251cfee --- /dev/null +++ b/src/database.rs @@ -0,0 +1,121 @@ +use diesel::prelude::*; +use diesel::result::Error; +use diesel::sqlite::SqliteConnection; + +// TODO reuse DB connection, using r2d2 or something + +use crate::ldap::*; + +#[table_name = "members"] +#[derive(Queryable, AsChangeset, Insertable)] +pub struct Member { + pub discord_id: i64, + pub tla: Option<String>, + pub username: String, + pub member_since: Option<String>, + pub name: Option<String>, + pub biography: Option<String>, + pub github: Option<String>, + pub photo: Option<String>, + pub website: Option<String>, +} + +table! { + members (discord_id) { + discord_id -> BigInt, + tla -> Nullable<Text>, + username -> Text, + member_since -> Nullable<Text>, + name -> Nullable<Text>, + biography -> Nullable<Text>, + github -> Nullable<Text>, + photo -> Nullable<Text>, + website -> Nullable<Text>, + } +} + +pub fn db_connection() -> SqliteConnection { + SqliteConnection::establish("state.db").expect("Failed to connect to sqlite DB") +} + +pub fn add_member(discord_id: &u64, username: &str) -> Member { + let ldap_user = ldap_search(username); + let name = ldap_user.as_ref().map(|u| u.name.clone()); + let tla_user = tla_search(username); + let tla = tla_user.as_ref().map(|u| u.tla.clone()).flatten(); + let new_member = Member { + discord_id: *discord_id as i64, + username: username.to_string(), + name: name.clone(), + tla: tla, + member_since: None, + biography: None, + github: None, + photo: None, + website: None, + }; + diesel::insert_into(members::table) + .values(&new_member) + .execute(&db_connection()) + .expect("Failed to add member to DB"); + info!( + "{} added to member DB", + name.unwrap_or(discord_id.to_string()) + ); + new_member +} + +pub fn update_member(discord_id: &u64, member: Member) -> Result<usize, Error> { + diesel::update(members::table.find(*discord_id as i64)) + .set(&member) + .execute(&db_connection()) +} + +pub fn username_exists(username: &str) -> bool { + match get_member_info_from_username(username) { + Ok(_) => true, + Err(_) => false, + } +} + +pub fn get_member_info(discord_id: &u64) -> Result<Member, Error> { + members::table + .find(*discord_id as i64) + .first(&db_connection()) +} + +pub fn get_member_info_from_username(username: &str) -> Result<Member, Error> { + members::table + .filter(members::username.eq(username)) + .first(&db_connection()) +} + +pub fn get_member_info_from_tla(tla: &str) -> Result<Member, Error> { + members::table + .filter(members::tla.eq(tla)) + .first(&db_connection()) +} + +pub fn set_member_bio(discord_id: &u64, bio: &str) -> Result<usize, Error> { + diesel::update(members::table.find(*discord_id as i64)) + .set(members::biography.eq(bio)) + .execute(&db_connection()) +} + +pub fn set_member_git(discord_id: &u64, git: &str) -> Result<usize, Error> { + diesel::update(members::table.find(*discord_id as i64)) + .set(members::github.eq(git)) + .execute(&db_connection()) +} + +pub fn set_member_photo(discord_id: &u64, url: &str) -> Result<usize, Error> { + diesel::update(members::table.find(*discord_id as i64)) + .set(members::photo.eq(url)) + .execute(&db_connection()) +} + +pub fn set_member_website(discord_id: &u64, url: &str) -> Result<usize, Error> { + diesel::update(members::table.find(*discord_id as i64)) + .set(members::website.eq(url)) + .execute(&db_connection()) +} diff --git a/src/ldap.rs b/src/ldap.rs new file mode 100644 index 0000000000000000000000000000000000000000..da9cc3eba184e67ff3a4c7a50478997ef0ee2e7f --- /dev/null +++ b/src/ldap.rs @@ -0,0 +1,87 @@ +use ldap3::{LdapConn, LdapConnSettings, Scope, SearchEntry}; + +#[derive(Debug)] +pub struct LDAPUser { + pub username: String, + pub name: String, + pub when_created: String, +} + +pub fn ldap_search(username: &str) -> Option<LDAPUser> { + let settings = LdapConnSettings::new().set_no_tls_verify(true); + let ldap = LdapConn::with_settings(settings, "ldaps://samson.ucc.asn.au:636") + .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", + include_str!("../ldap_pass").trim_end(), + ) + .expect("Unable to attempt to bind to LDAP") + .success() + .expect("Unable to bind to LDAP"); + let (rs, _res) = ldap + .search( + "cn=Users,dc=ad,dc=ucc,dc=gu,dc=uwa,dc=edu,dc=au", + Scope::Subtree, + &format!("(cn={})", username), + vec!["when_created", "displayName", "name"], + ) + .expect("LDAP error") + .success() + .expect("LDAP search error"); + if rs.len() != 1 { + return None; + } + let result = SearchEntry::construct(rs[0].clone()).attrs; + Some(LDAPUser { + username: result + .get("name") + .expect("LDAP failed to get 'name' field") + .join(""), + name: result + .get("displayName") + .expect("LDAP failed to get 'displayName' field") + .join(""), + when_created: "".to_string() // result + // .get("whenCreated") + // .expect("LDAP failed to get 'whenCreated' field") + // .join(""), + }) +} + +pub fn ldap_exists(username: &str) -> bool { + match ldap_search(username) { + Some(_) => true, + None => false, + } +} + +#[derive(Debug)] +pub struct TLA { + pub tla: Option<String>, + pub name: String, + pub username: String, +} + +pub fn tla_search(term: &str) -> Option<TLA> { + let tla_search = String::from_utf8( + std::process::Command::new("tla") + .arg(term) + .output() + .expect("failed to execute tla") + .stdout, + ) + .expect("unable to parse stdout to String"); + let tla_results = tla_search.split("\n").collect::<Vec<&str>>(); + if tla_results.len() != 4 { + return None; + } + let mut the_tla = Some(tla_results[0].replace("TLA: ", "")[1..4].to_string()); + if the_tla == Some(String::from("???")) { + the_tla = None; + } + Some(TLA { + tla: the_tla, + name: tla_results[1].replace("Name: ", ""), + username: tla_results[2].replace("Login: ", ""), + }) +} diff --git a/src/main.rs b/src/main.rs index 7058333d02135e18c5b31639560ee6c7d3d45a9d..295fcb6a4fc2760dcd2af8d3819ae3bb0d86e152 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,11 @@ extern crate indexmap; extern crate simplelog; #[macro_use] extern crate guard; + +#[macro_use] +extern crate diesel; +extern crate ldap3; + use simplelog::*; use std::fs::{read_to_string, File}; @@ -20,6 +25,8 @@ use serenity::{ #[macro_use] mod util; mod config; +mod database; +mod ldap; mod reaction_roles; mod token_management; mod user_management; @@ -42,73 +49,118 @@ impl EventHandler for Handler { return; } let message_content: Vec<_> = msg.content[1..].splitn(2, ' ').collect(); + let content = if message_content.len() > 1 { + message_content[1] + } else { + "" + }; match message_content[0] { "say" => println!("{:#?}", msg.content), - "register" => user_management::Commands::register(ctx, msg.clone(), message_content[1]), - "verify" => user_management::Commands::verify(ctx, msg.clone(), message_content[1]), - "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]), + "register" => user_management::Commands::register(ctx, msg.clone(), content), + "verify" => user_management::Commands::verify(ctx, msg.clone(), content), + "profile" => user_management::Commands::profile(ctx, msg.clone(), content), + "set" => user_management::Commands::set_info(ctx, msg.clone(), content), + "move" => voting::Commands::move_something(ctx, msg.clone(), content), + "motion" => voting::Commands::motion(ctx, msg.clone(), content), + "poll" => voting::Commands::poll(ctx, msg.clone(), content), + "cowsay" => voting::Commands::cowsay(ctx, msg.clone(), content), "logreact" => { e!("Error deleting logreact prompt: {:?}", msg.delete(&ctx)); - send_message!(msg.channel_id, &ctx.http, - "React to this to log the ID (for the next 5min)"); + send_message!( + msg.channel_id, + &ctx.http, + "React to this to log the ID (for the next 5min)" + ); } "help" => { - let mut message = MessageBuilder::new(); - message.push_line(format!( - "Use {}move <action> to make a circular motion", - &CONFIG.command_prefix - )); - message.push_line(format!( - "Use {}poll <proposal> to see what people think about something", - &CONFIG.command_prefix - )); - send_message!(msg.channel_id, &ctx.http, message.build()); - }, - _ => send_message!(msg.channel_id, &ctx.http, - format!("Unrecognised command. Try {}help", &CONFIG.command_prefix)), + // Plaintext version, keep in case IRC users kick up a fuss + // let mut message = MessageBuilder::new(); + // message.push_line(format!( + // "Use {}move <action> to make a circular motion", + // &CONFIG.command_prefix + // )); + // message.push_line(format!( + // "Use {}poll <proposal> to see what people think about something", + // &CONFIG.command_prefix + // )); + // send_message!(msg.channel_id, &ctx.http, message.build()); + + let result = msg.channel_id.send_message(&ctx.http, |m| { + m.embed(|embed| { + embed.colour(serenity::utils::Colour::DARK_GREY); + embed.title("Commands for the UCC Bot"); + embed.field("About", "This is UCC's own little in-house bot, please treat it nicely :)", false); + embed.field("Commitee", "`!move <text>` to make a circular motion\n\ + `!poll <text>` to get people's opinions on something", false); + embed.field("Account", "`!register <ucc username>` to link your Discord and UCC account\n\ + `!profile <user>` to get the profile of a user\n\ + `!set <bio|git|web|photo>` to set that property of _your_ profile", false); + embed.field("Fun", "`!cowsay <text>` to have a cow say your words\n\ + with no `<text>` it'll give you a fortune 😉", false); + embed + }); + m + }); + if let Err(why) = result { + error!("Error sending help embed: {:?}", why); + } + } + // undocumented (in !help) functins + "ldap" => send_message!( + msg.channel_id, + &ctx.http, + format!("{:?}", ldap::ldap_search(message_content[1])) + ), + "tla" => send_message!( + msg.channel_id, + &ctx.http, + format!("{:?}", ldap::tla_search(message_content[1])) + ), + _ => send_message!( + msg.channel_id, + &ctx.http, + format!("Unrecognised command. Try {}help", &CONFIG.command_prefix) + ), } } fn reaction_add(&self, ctx: Context, add_reaction: channel::Reaction) { match add_reaction.message(&ctx.http) { - 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 - }, - _ if message.author.id.0 != CONFIG.bot_id || add_reaction.user_id == CONFIG.bot_id => { - return; - }, - MessageType::Motion => voting::reaction_add(ctx, add_reaction), - MessageType::LogReact => { - let react_user = add_reaction.user(&ctx).unwrap(); - let react_as_string = get_string_from_react(&add_reaction.emoji); - if Utc::now().timestamp() - message.timestamp.timestamp() > 300 { - warn!( - "The logreact message {} just tried to use is too old", - react_user.name - ); - return; - } - info!( - "The react {} just added is {:?}. In full: {:?}", - react_user.name, react_as_string, add_reaction.emoji + 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; + } + _ if message.author.id.0 != CONFIG.bot_id + || add_reaction.user_id == CONFIG.bot_id => + { + return; + } + MessageType::Motion => voting::reaction_add(ctx, add_reaction), + MessageType::LogReact => { + let react_user = add_reaction.user(&ctx).unwrap(); + let react_as_string = get_string_from_react(&add_reaction.emoji); + if Utc::now().timestamp() - message.timestamp.timestamp() > 300 { + warn!( + "The logreact message {} just tried to use is too old", + react_user.name ); - let mut msg = MessageBuilder::new(); - msg.push_italic(react_user.name); - msg.push(format!( - " wanted to know that {} is represented by ", - add_reaction.emoji, - )); - msg.push_mono(react_as_string); - send_message!(message.channel_id, &ctx.http, msg.build()); - }, - _ => {}, + return; + } + info!( + "The react {} just added is {:?}. In full: {:?}", + react_user.name, react_as_string, add_reaction.emoji + ); + let mut msg = MessageBuilder::new(); + msg.push_italic(react_user.name); + msg.push(format!( + " wanted to know that {} is represented by ", + add_reaction.emoji, + )); + msg.push_mono(react_as_string); + send_message!(message.channel_id, &ctx.http, msg.build()); } + _ => {} }, Err(why) => error!("Failed to get react message {:?}", why), } @@ -116,18 +168,18 @@ impl EventHandler for Handler { fn reaction_remove(&self, ctx: Context, removed_reaction: channel::Reaction) { match removed_reaction.message(&ctx.http) { - 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 - }, - _ if message.author.id.0 != CONFIG.bot_id || removed_reaction.user_id == CONFIG.bot_id => { - return; - }, - MessageType::Motion => voting::reaction_remove(ctx, removed_reaction), - _ => {}, + 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; + } + _ if message.author.id.0 != CONFIG.bot_id + || removed_reaction.user_id == CONFIG.bot_id => + { + return; } + MessageType::Motion => voting::reaction_remove(ctx, removed_reaction), + _ => {} }, Err(why) => error!("Failed to get react message {:?}", why), } diff --git a/src/user_management.rs b/src/user_management.rs index 8f18fe9239452c8c6090d19b8f0a478f38a1a8de..390db41c134384291599b6efaee66f662f00721f 100644 --- a/src/user_management.rs +++ b/src/user_management.rs @@ -1,11 +1,16 @@ use rand::seq::SliceRandom; +use regex::Regex; use serenity::{ model::{channel::Message, guild::Member}, prelude::*, utils::MessageBuilder, }; +use std::process::{Command, Stdio}; +use url::Url; use crate::config::CONFIG; +use crate::database; +use crate::ldap::ldap_exists; use crate::token_management::*; pub fn new_member(ctx: &Context, mut new_member: Member) { @@ -14,7 +19,10 @@ pub fn new_member(ctx: &Context, mut new_member: Member) { 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"); + message.push_line("You should also know that we follow the Freenode Channel Guidelines: https://freenode.net/changuide, and try to avoid defamatory content."); + message.push_line("Make sure to check out "); + message.mention(&CONFIG.readme_channel); + message.push_line(" to get yourself some roles for directed pings 😊"); send_message!(CONFIG.welcome_channel, &ctx, message.build()); let mut message = MessageBuilder::new(); @@ -27,29 +35,102 @@ pub fn new_member(ctx: &Context, mut new_member: Member) { }; } -pub const RANDOM_NICKNAMES: &[&str] = &[ - "The Big Cheese", - "The One and Only", - "The Exalted One", - "not to be trusted", - "The Scoundrel", - "A big fish in a small pond", +fn member_nickname(member: &database::Member) -> String { + let username = member.username.clone(); + if let Some(tla) = member.tla.clone() { + if username.to_uppercase() == tla { + return format!("{}", username); + } else { + return format!("{} [{}]", username, tla); + } + } else { + return format!("{}", username); + } +} + +pub const RANDOM_SASS: &[&str] = &[ + "Please. As if I'd fall for that.", + "Did you really think a stunt like that would work?", + "Nothing slips past me.", + "Did you even read the first line of !help?", + "I never treated you this badly.", ]; pub struct Commands; impl Commands { pub fn register(ctx: Context, msg: Message, account_name: &str) { if account_name.is_empty() { - send_message!(msg.channel_id, &ctx.http, "Usage: !register <ucc username>"); + send_message!( + msg.channel_id, + &ctx.http, + format!("Usage: {}register <username>", CONFIG.command_prefix) + ); + return; + } + if vec![ + "committee", + "committee-only", + "ucc", + "ucc-announce", + "tech", + "wheel", + "door", + "coke", + ] + .contains(&account_name) + || database::username_exists(account_name) + { + send_message!( + msg.channel_id, + &ctx.http, + RANDOM_SASS + .choose(&mut rand::thread_rng()) + .expect("We couldn't get any sass") + ); + return; + } + if !ldap_exists(account_name) { + send_message!( + msg.channel_id, + &ctx.http, + format!( + "I couldn't find an account with the username '{}'", + account_name + ) + ); return; } - send_message!( - msg.channel_id, - &ctx.http, - format!("Hey {} here's that token you ordered: {}\nIf this wasn't you just ignore this.", - account_name, - generate_token(&msg.author, account_name))); + match msg.channel_id.say( + &ctx.http, + format!("Ok {}, I've sent an email to you :)", account_name), + ) { + Ok(new_msg) => { + e!("Failed to delete message: {:?}", new_msg.delete(&ctx)); + } + Err(why) => error!("Error sending message: {:?}", why), + } + e!("Error deleting register message: {:?}", msg.delete(ctx)); + + let message = Command::new("echo").arg(format!("<h3>Link your Discord account</h3>\ + <p>Hi {}, to complete the link, go to the discord server and enter\ + <pre>{}verify {}</pre>\ + </p><sub>The UCC discord bot</sub>", + account_name, CONFIG.command_prefix, generate_token(&msg.author, account_name))).stdout(Stdio::piped()).spawn().expect("Unable to spawn echo command"); + match Command::new("mutt") + .arg("-e") + .arg("set content_type=text/html") + .arg("-e") + .arg("set realname=\"UCC Discord Bot\"") + .arg("-s") + .arg("Discord account link token") + .arg(format!("{}@ucc.asn.au", account_name)) + .stdin(message.stdout.unwrap()) + .output() + { + Ok(_) => {} + Err(why) => error!("Unable to send message with mutt {:?}", why), + }; } pub fn verify(ctx: Context, msg: Message, token: &str) { match parse_token(&msg.author, token) { @@ -59,6 +140,7 @@ impl Commands { serenity::model::id::GuildId(CONFIG.server_id) .member(ctx.http.clone(), msg.author.id) .map(|mut member| { + let full_member = database::add_member(&msg.author.id.0, &name); e!( "Unable to remove role: {:?}", member.remove_role(&ctx.http, CONFIG.unregistered_member_role) @@ -66,27 +148,207 @@ impl Commands { e!( "Unable to edit nickname: {:?}", member.edit(&ctx.http, |m| { - m.nickname(format!( - "{}, {:?}", - name, - RANDOM_NICKNAMES.choose(&mut rand::thread_rng()) - )); + m.nickname(member_nickname(&full_member)); m }) ); - let new_msg = msg - .channel_id - .say(&ctx.http, "Verification succesful") - .expect("Error sending message"); - e!( - "Error deleting register message: {:?}", - new_msg.delete(&ctx) + send_delete_message!( + msg.channel_id, + ctx.http.clone(), + "Verification sucessful" ); }) ); } - Err(reason) => send_message!(msg.channel_id, &ctx.http, format!("Verification error: {:?}", reason)), + Err(reason) => send_message!( + msg.channel_id, + &ctx.http, + format!("Verification error: {:?}", reason) + ), } e!("Error deleting register message: {:?}", msg.delete(&ctx)); } + pub fn profile(ctx: Context, msg: Message, name: &str) { + let possible_member: Option<database::Member> = match if name.trim().is_empty() { + database::get_member_info(&msg.author.id.0) + } else { + database::get_member_info_from_username(&name) + } { + Ok(member) => Some(member), + Err(why) => { + warn!("Could not find member {:?}", why); + if name.len() != 3 { + None + } else { + match database::get_member_info_from_tla(&name) { + Ok(member) => Some(member), + Err(_) => None, + } + } + } + }; + if possible_member.is_none() { + send_message!( + msg.channel_id, + &ctx.http, + "Sorry, I couldn't find that profile (you need to !register for a profile)" + ); + return; + } + let member = possible_member.unwrap(); + let result = msg.channel_id.send_message(&ctx.http, |m| { + m.embed(|embed| { + embed.colour(serenity::utils::Colour::LIGHTER_GREY); + embed.footer(|f| { + let user = &ctx + .http + .get_user(member.discord_id.clone() as u64) + .expect("We expected this user to exist... they didn't ;("); + f.text(&user.name); + f.icon_url( + user.static_avatar_url() + .expect("Expected user to have avatar"), + ); + f + }); + if let Some(name) = member.name.clone() { + embed.title(name); + } + if let Some(photo) = member.photo.clone() { + embed.thumbnail(photo); + } + embed.field("Username", &member.username, true); + if let Some(tla) = member.tla.clone() { + embed.field("TLA", tla, true); + } + if let Some(bio) = member.biography.clone() { + embed.field("Bio", bio, false); + } + if let Some(git) = member.github.clone() { + embed.field("Git", git, false); + } + if let Some(web) = member.website.clone() { + embed.field("Website", web, false); + } + embed + }); + m + }); + if let Err(why) = result { + error!("Error sending profile embed: {:?}", why); + } + } + 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; + } + let info_content: Vec<_> = info.splitn(2, ' ').collect(); + let mut property = String::from(info_content[0]); + property = property.replace("github", "git"); + if info_content.len() == 1 || !vec!["bio", "git", "photo"].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; + } + let mut value = info_content[1].to_string(); + + if vec!["git", "photo", "web"].contains(&property.as_str()) { + if match Url::parse(&value) { + Err(_) => true, + Ok(_) => false, + } { + let user_regex = Regex::new(r"^\w+$").unwrap(); + if property == "git" && user_regex.is_match(&value) { + value = format!("github.com/{}", value); + } + value = format!("https://{}", value); + if match Url::parse(&value) { + Err(_) => true, + Ok(_) => false, + } { + send_message!( + msg.channel_id, + &ctx.http, + "That ain't a URL where I come from..." + ); + return; + } + } + } + if let Ok(member) = database::get_member_info(&msg.author.id.0) { + let set_property = match property.as_str() { + "bio" => database::set_member_bio(&msg.author.id.0, &value), + "git" => database::set_member_git(&msg.author.id.0, &value), + "photo" => database::set_member_photo(&msg.author.id.0, &value), + "web" => database::set_member_website(&msg.author.id.0, &value), + _ => Err(diesel::result::Error::NotFound), + }; + match set_property { + Ok(_) => { + if property == "git" && member.photo == None { + let git_url = Url::parse(&value).unwrap(); // we parsed this earlier and it was fine + match git_url.host_str() { + Some("github.com") => { + if let Some(mut path_segments) = git_url.path_segments() { + database::set_member_photo( + &msg.author.id.0, + format!( + "https://github.com/{}.png", + path_segments.next().expect("URL doesn't have a path") + ) + .as_str(), + ) + .expect("Attempt to set member photo failed"); + } else { + info!("Git path added (2), {}", git_url.path()); + } + } + _ => info!("Git path added, {}", git_url.path()), + } + } + } + Err(why) => { + error!( + "Umable to set property {} to {} in DB {:?}", + property, value, why + ); + send_message!(msg.channel_id, &ctx.http, "Failed to set property. Ooops."); + } + } + if let Err(why) = msg.delete(&ctx) { + error!("Error deleting set profile property: {:?}", why); + } + } else { + send_message!( + msg.channel_id, + &ctx.http, + format!( + "You don't seem to have a profile. {}register to get one", + CONFIG.command_prefix + ) + ) + } + } } diff --git a/src/util.rs b/src/util.rs index 777c641f034577c8c3245124257ed750b24f5d43..bdf9a1356430890edc76f822675e64e505d6cf0c 100644 --- a/src/util.rs +++ b/src/util.rs @@ -35,10 +35,22 @@ macro_rules! e { #[macro_use] macro_rules! send_message { ($chan:expr, $context:expr, $message:expr) => { - match $chan - .say($context, $message) { + match $chan.say($context, $message) { Ok(_) => (), Err(why) => error!("Error sending message: {:?}", why), } }; } + +#[macro_use] +macro_rules! send_delete_message { + ($chan:expr, $context:expr, $message:expr) => { + match $chan.say($context, $message) { + Ok(the_new_msg) => e!( + "Error deleting register message: {:?}", + the_new_msg.delete($context) + ), + Err(why) => error!("Error sending message: {:?}", why), + } + }; +} diff --git a/state.db b/state.db new file mode 100644 index 0000000000000000000000000000000000000000..d8718d3308eb20f66a68774aa979719572eae66a --- /dev/null +++ b/state.db @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6642eb54f8c63bfd31fa28b105e359ae5d023f04d0e990aa452afc6d163e316 +size 32768