From 7e40b862d30b18f18a3fd23a89f3a119d35296a4 Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Sat, 31 Aug 2024 16:05:38 +0100 Subject: [PATCH] feat: add functionality for the committee server. Related to #16 --- db/migrations/4_committee-mk-i.sql | 7 + src/commands/committee.rs | 306 +++++++++++++++++++++++++++++ src/commands/mod.rs | 1 + 3 files changed, 314 insertions(+) create mode 100644 db/migrations/4_committee-mk-i.sql create mode 100644 src/commands/committee.rs diff --git a/db/migrations/4_committee-mk-i.sql b/db/migrations/4_committee-mk-i.sql new file mode 100644 index 0000000..c4a78e4 --- /dev/null +++ b/db/migrations/4_committee-mk-i.sql @@ -0,0 +1,7 @@ +-- temp table to allow folks to verify by committee email. +CREATE TABLE IF NOT EXISTS committee ( + discord integer PRIMARY KEY, + email text not null, + auth_code text not null, + committee integer DEFAULT 0, +); \ No newline at end of file diff --git a/src/commands/committee.rs b/src/commands/committee.rs new file mode 100644 index 0000000..d135570 --- /dev/null +++ b/src/commands/committee.rs @@ -0,0 +1,306 @@ +use lettre::{ + message::{header, MultiPart, SinglePart}, + transport::smtp::{self, authentication::Credentials}, + Message, SmtpTransport, Transport, +}; +use maud::html; +use serenity::{ + builder::CreateApplicationCommand, + client::Context, + model::{ + application::interaction::application_command::ApplicationCommandInteraction, + id::UserId, + prelude::{command::CommandOptionType, interaction::application_command::CommandDataOptionValue}, + }, +}; +use skynet_discord_bot::{get_now_iso, random_string, Config, DataBase, Wolves, WolvesVerify}; +use sqlx::{Pool, Sqlite}; + + +pub mod link { + use serenity::model::id::GuildId; + use super::*; + + pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String { + let committee_server = GuildId(1220150752656363520); + match command.guild_id { + None => { + return "Not in correct discord server.".to_string(); + } + Some(x) => { + if x != committee_server { + return "Not in correct discord server.".to_string(); + } + } + } + + + let option = command + .data + .options + .first() + .expect("Expected email option") + .resolved + .as_ref() + .expect("Expected email object"); + + let email = if let CommandDataOptionValue::String(email) = option { + email.trim() + } else { + return "Please provide a valid committee email.".to_string(); + }; + + // fail early + if !email.ends_with("@ulwolves.ie") { + return "Please use a @ulwolves.ie address you have access to.".to_string(); + } + + + let db_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Databse in TypeMap.").clone() + }; + let db = db_lock.read().await; + + let config_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Config in TypeMap.").clone() + }; + let config = config_lock.read().await; + + if get_server_member_discord(&db, &command.user.id).await.is_some() { + return "Already linked".to_string(); + } + + if get_verify_from_db(&db, &command.user.id).await.is_some() { + return "Linking already in process, please check email.".to_string(); + } + + + // generate a auth key + let auth = random_string(20); + match send_mail(&config, email, &auth, &command.user.name) { + Ok(_) => match save_to_db(&db, email, &auth, &command.user.id).await { + Ok(_) => {} + Err(e) => { + return format!("Unable to save to db {} {e:?}", email); + } + }, + Err(e) => { + return format!("Unable to send mail to {} {e:?}", email); + } + } + + format!("Verification email sent to {}, it may take up to 15 min for it to arrive. If it takes longer check the Junk folder.", email) + } + + pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { + command + .name("link_committee") + .description("Verify you are a committee member") + .create_option(|option| option.name("email").description("UL Wolves Committee Email").kind(CommandOptionType::String).required(true)) + } + + pub async fn get_server_member_discord(db: &Pool, user: &UserId) -> Option { + sqlx::query_as::<_, Wolves>( + r#" + SELECT * + FROM committee + WHERE discord = ? + "#, + ) + .bind(*user.as_u64() as i64) + .fetch_one(db) + .await + .ok() + } + + fn send_mail(config: &Config, mail: &str, auth: &str, user: &str) -> Result { + let sender = format!("UL Computer Society <{}>", &config.mail_user); + + // Create the html we want to send. + let html = html! { + head { + title { "Hello from Skynet!" } + style type="text/css" { + "h2, h4 { font-family: Arial, Helvetica, sans-serif; }" + } + } + div style="display: flex; flex-direction: column; align-items: center;" { + h2 { "Hello from Skynet!" } + // Substitute in the name of our recipient. + p { "Hi " (user) "," } + p { + "Please use " pre { "/verify_committee code: " (auth)} " to verify your discord account." + } + p { + "Skynet Team" + br; + "UL Computer Society" + } + } + }; + + let body_text = format!( + r#" + Hi {user} + + Please use "/verify_committee code: {auth}" to verify your discord account. + + Skynet Team + UL Computer Society + "# + ); + + // Build the message. + let email = Message::builder() + .from(sender.parse().unwrap()) + .to(mail.parse().unwrap()) + .subject("Skynet-Discord: Link Committee.") + .multipart( + // This is composed of two parts. + // also helps not trip spam settings (uneven number of url's + MultiPart::alternative() + .singlepart(SinglePart::builder().header(header::ContentType::TEXT_PLAIN).body(body_text)) + .singlepart(SinglePart::builder().header(header::ContentType::TEXT_HTML).body(html.into_string())), + ) + .expect("failed to build email"); + + let creds = Credentials::new(config.mail_user.clone(), config.mail_pass.clone()); + + // Open a remote connection to gmail using STARTTLS + let mailer = SmtpTransport::starttls_relay(&config.mail_smtp).unwrap().credentials(creds).build(); + + // Send the email + mailer.send(&email) + } + + pub async fn get_verify_from_db(db: &Pool, user: &UserId) -> Option { + sqlx::query_as::<_, WolvesVerify>( + r#" + SELECT * + FROM committee + WHERE discord = ? + "#, + ) + .bind(*user.as_u64() as i64) + .fetch_one(db) + .await + .ok() + } + + async fn save_to_db(db: &Pool, email: &str, auth: &str, user: &UserId) -> Result, sqlx::Error> { + sqlx::query_as::<_, WolvesVerify>( + " + INSERT INTO committee (email, discord, auth_code) + VALUES (?1, ?2, ?3) + ", + ) + .bind(email.to_owned()) + .bind(*user.as_u64() as i64) + .bind(auth.to_owned()) + .fetch_optional(db) + .await + } +} + +pub mod verify { + use serenity::model::id::{GuildId, RoleId}; + use super::*; + use crate::commands::committee::link::{get_verify_from_db}; + use serenity::model::user::User; + use skynet_discord_bot::{get_server_config, ServerMembersWolves, Servers}; + use sqlx::Error; + + pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String { + let committee_server = GuildId(1220150752656363520); + match command.guild_id { + None => { + return "Not in correct discord server.".to_string(); + } + Some(x) => { + if x != committee_server { + return "Not in correct discord server.".to_string(); + } + } + } + + let db_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Databse in TypeMap.").clone() + }; + let db = db_lock.read().await; + + // check if user has used /link_committee + let details = if let Some(x) = get_verify_from_db(&db, &command.user.id).await { + x + } else { + return "Please use /link_committee first".to_string(); + }; + + let option = command + .data + .options + .first() + .expect("Expected code option") + .resolved + .as_ref() + .expect("Expected code object"); + + let code = if let CommandDataOptionValue::String(code) = option { + code + } else { + return "Please provide a verification code".to_string(); + }; + + if &details.auth_code != code { + return "Invalid verification code".to_string(); + } + + return match set_discord(&db, &command.user.id).await { + Ok(_) => { + // get teh right roles for the user + set_server_roles(&command.user, ctx).await; + "Discord username linked to Wolves for committee".to_string() + } + Err(e) => { + println!("{:?}", e); + "Failed to save, please try /link_committee again".to_string() + } + }; + } + + pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { + command.name("verify_committee").description("Verify Wolves Committee Email").create_option(|option| { + option + .name("code") + .description("Code from verification email") + .kind(CommandOptionType::String) + .required(true) + }) + } + + async fn set_discord(db: &Pool, discord: &UserId) -> Result, Error> { + sqlx::query_as::<_, Wolves>( + " + UPDATE committee + SET committee = 1 + WHERE discord = ? + ", + ) + .bind(*discord.as_u64() as i64) + .fetch_optional(db) + .await + } + + async fn set_server_roles(discord: &User, ctx: &Context) { + let committee_server = GuildId(1220150752656363520); + if let Ok(mut member) = committee_server.member(&ctx.http, &discord.id).await { + let committee_member = RoleId(1226602779968274573); + if let Err(e) = member.add_role(&ctx, committee_member).await { + println!("{:?}", e); + } + } + } + +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 9d3bea5..c95bf66 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,3 +1,4 @@ pub mod add_server; pub mod link_email; pub mod minecraft; +pub mod committee;