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) {
let mut message = MessageBuilder::new();
message.push("Nice to see you here ");
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("Make sure to check out ");
message.mention(&CONFIG.readme_channel);
message.push(" to get yourself some roles for directed pings 😊, and ");
message.push_mono(format!("{}register username", CONFIG.command_prefix));
message.push(" to link to your UCC account.");
send_message!(CONFIG.welcome_channel, &ctx, message.build());
let mut message = MessageBuilder::new();
message.push(format!("Say hi to {} in ", new_member.display_name()));
message.mention(&CONFIG.welcome_channel);
send_message!(CONFIG.main_channel, &ctx, message.build());
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 {
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 const RESERVED_NAMES: &[&str] = &[
"committee",
"committee-only",
"ucc",
"ucc-announce",
"tech",
"wheel",
"door",
"coke",
];
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,
format!("Usage: {}register ", CONFIG.command_prefix)
);
return;
}
if RESERVED_NAMES.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!(
"Ok {}, see the email I've just sent you to complete the link",
account_name
)
);
e!("Error deleting register message: {:?}", msg.delete(ctx));
let message = Command::new("echo").arg(format!("Link your Discord account
\
Hi {}, to complete the link, go to the discord server and enter\
{}verify {}
\
The UCC discord bot",
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(_) => info!("Email sent to {}", account_name),
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) {
Ok(name) => {
e!(
"Unable to get member: {:?}",
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)
);
e!(
"Unable to add role: {:?}",
member.add_role(&ctx.http, CONFIG.registered_member_role)
);
e!(
"Unable to edit nickname: {:?}",
member.edit(&ctx.http, |m| {
m.nickname(member_nickname(&full_member));
m
})
);
let mut verification_message = MessageBuilder::new();
verification_message.push(format!("Great, {}! Verification was successful. To provide a friendly introduction to yourself, consider doing ", &full_member.username));
verification_message.push_mono(format!("{}set bio ", CONFIG.command_prefix));
send_message!(
msg.channel_id,
ctx.http.clone(),
verification_message.build()
);
})
);
}
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 = match if name.trim().is_empty() {
info!(
"{} (discord name) wants to look at their own profile",
&msg.author.name
);
database::get_member_info(&msg.author.id.0)
} else {
info!("Searching for a profile for {}", &name);
database::get_member_info_from_username(&name)
} {
Ok(member) => Some(member),
Err(why) => {
warn!("Could not find member {}, {:?}", &name, why);
if name.len() != 3 {
None
} else {
info!(
"Searching for a profile for the TLA '{}'",
&name.to_uppercase()
);
match database::get_member_info_from_tla(&name.to_uppercase()) {
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();
info!("Found matching profile, UCC username: {}", &member.username);
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()
.unwrap_or(String::from("https://www.ucc.asn.au/logos/ucc-logo.png")),
);
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(study) = member.study.clone() {
embed.field("Area of study", study, 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() {
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 ` or `{}clear `",
CONFIG.command_prefix,
CONFIG.command_prefix,
)
);
embed.field("Biography", format!("`{}set bio `\nBe friendly! Provide a little introduction to yourself.", CONFIG.command_prefix), false);
embed.field("Git", format!("`{}set git `\nA link to your git forge profile. Also takes a github username for convinience", CONFIG.command_prefix), false);
embed.field("Photo", format!("`{}set photo `\nPut a face to a name! Provide a profile photo.", CONFIG.command_prefix), false);
embed.field("Website", format!("`{}set web `\nGot a personal website? Share it here :)", CONFIG.command_prefix), false);
embed.field("Studying", format!("`{}set study `\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]);
property = property.replace("github", "git");
if info_content.len() == 1
|| !vec!["bio", "git", "web", "photo", "study"].contains(&property.as_str())
{
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 {} ` 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();
if vec!["git", "photo", "web"].contains(&property.as_str()) {
if Url::parse(&value).is_err() {
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 Url::parse(&value).is_err() {
send_message!(
msg.channel_id,
&ctx.http,
"That ain't a URL where I come from..."
);
return;
}
}
}
guard!(let Ok(member) = database::get_member_info(&msg.author.id.0) else {
send_message!(
msg.channel_id,
&ctx.http,
format!(
"You don't seem to have a profile. {}register to get one",
CONFIG.command_prefix
)
);
return
});
let set_property = match property.as_str() {
"bio" => database::set_member_bio(&msg.author.id.0, Some(&value)),
"git" => database::set_member_git(&msg.author.id.0, Some(&value)),
"photo" => database::set_member_photo(&msg.author.id.0, Some(&value)),
"web" => database::set_member_website(&msg.author.id.0, Some(&value)),
"study" => database::set_member_study(&msg.author.id.0, Some(&value)),
_ => Err(diesel::result::Error::NotFound),
};
match set_property {
Ok(_) => {
info!(
"Set {}'s {} in profile to {}",
&msg.author_nick(ctx.http.clone())
.unwrap_or(String::from("?")),
property,
value
);
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,
Some(
format!(
"https://github.com/{}.png",
path_segments.next().expect("URL doesn't have a path")
)
.as_str(),
),
)
.expect("Attempt to set member photo failed");
info!(" ... and set profile photo to github photo")
}
}
_ => {}
}
}
}
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);
}
}
pub fn clear_info(ctx: Context, msg: Message, field: &str) {
if field.trim().is_empty() {
// just show the help page from set_info
Commands::set_info(ctx, msg, "");
return;
}
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),
"web" => database::set_member_website(&msg.author.id.0, None),
"study" => database::set_member_study(&msg.author.id.0, None),
_ => Err(diesel::result::Error::NotFound),
}
.expect("Unable to clear profile field");
info!(
"Cleared {}'s {} in profile",
&msg.author_nick(ctx.http).unwrap_or(String::from("?")),
field,
);
}
}