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); } } } }