From ac44176acbdaf58813113fd75709d3b3ec5cefb3 Mon Sep 17 00:00:00 2001
From: tec <tec@ucc.gu.uwa.edu.au>
Date: Thu, 26 Mar 2020 23:08:51 +0800
Subject: [PATCH] Add profiles

---
 Cargo.toml             |   4 +
 src/config.rs          |   1 +
 src/config.ucc.yml     |   1 +
 src/database.rs        | 121 ++++++++++++++++
 src/ldap.rs            |  87 +++++++++++
 src/main.rs            | 182 ++++++++++++++---------
 src/user_management.rs | 318 +++++++++++++++++++++++++++++++++++++----
 src/util.rs            |  16 ++-
 state.db               |   3 +
 9 files changed, 638 insertions(+), 95 deletions(-)
 create mode 100644 src/database.rs
 create mode 100644 src/ldap.rs
 create mode 100644 state.db

diff --git a/Cargo.toml b/Cargo.toml
index d31667a..6b2dc22 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 c6f922a..659da0a 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 675a201..04f7cbc 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 0000000..1498c38
--- /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 0000000..da9cc3e
--- /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 7058333..295fcb6 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 8f18fe9..390db41 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 777c641..bdf9a13 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 0000000..d8718d3
--- /dev/null
+++ b/state.db
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e6642eb54f8c63bfd31fa28b105e359ae5d023f04d0e990aa452afc6d163e316
+size 32768
-- 
GitLab