Unverified Commit ac44176a authored by tec's avatar tec

Add profiles

parent c9decd53
......@@ -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"
......@@ -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,
......
......@@ -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
......
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())
}
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: ", ""),
})
}
......@@ -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),
}
......
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: {:?}",