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 super::*; pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> 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(); } db_pending_clear_expired(&db).await; if get_verify_from_db(&db, &command.user.id).await.is_some() { return "Linking already in process, please check email.".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 user".to_string(); }; // check if email exists let details = match get_server_member_email(&db, email).await { None => { return "Please check it is your preferred contact on https://ulwolves.ie/memberships/profile and that you are fully paid up.".to_string() } Some(x) => x, }; if details.discord.is_some() { return "Email already verified".to_string(); } // generate a auth key let auth = random_string(20); match send_mail(&config, &details, &auth, &command.user.name) { Ok(_) => match save_to_db(&db, &details, &auth, &command.user.id).await { Ok(_) => {} Err(e) => { return format!("Unable to save to db {} {e:?}", &details.email); } }, Err(e) => { return format!("Unable to send mail to {} {e:?}", &details.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_wolves") .description("Set Wolves Email") .create_option(|option| option.name("email").description("UL Wolves 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 wolves WHERE discord = ? "#, ) .bind(*user.as_u64() as i64) .fetch_one(db) .await .ok() } async fn get_server_member_email(db: &Pool, email: &str) -> Option { sqlx::query_as::<_, Wolves>( r#" SELECT * FROM wolves WHERE email = ? "#, ) .bind(email) .fetch_one(db) .await .ok() } fn send_mail(config: &Config, email: &Wolves, auth: &str, user: &str) -> Result { let mail = &email.email; let discord = "https://discord.skynet.ie"; 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 code: " (auth)} " to verify your discord account." } p { "If you have issues please refer to our Discord server:" br; a href=(discord) { (discord) } } p { "Skynet Team" br; "UL Computer Society" } } }; let body_text = format!( r#" Hi {user} Please use "/verify code: {auth}" to verify your discord account. If you have issues please refer to our Discord server: {discord} Skynet Team UL Computer Society "# ); // Build the message. let email = Message::builder() .from(sender.parse().unwrap()) .to(mail.parse().unwrap()) .subject("Skynet-Discord: Link Wolves.") .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 db_pending_clear_expired(pool: &Pool) -> Option { sqlx::query_as::<_, WolvesVerify>( r#" DELETE FROM wolves_verify WHERE date_expiry < ? "#, ) .bind(get_now_iso(true)) .fetch_one(pool) .await .ok() } pub async fn get_verify_from_db(db: &Pool, user: &UserId) -> Option { sqlx::query_as::<_, WolvesVerify>( r#" SELECT * FROM wolves_verify WHERE discord = ? "#, ) .bind(*user.as_u64() as i64) .fetch_one(db) .await .ok() } async fn save_to_db(db: &Pool, record: &Wolves, auth: &str, user: &UserId) -> Result, sqlx::Error> { sqlx::query_as::<_, WolvesVerify>( " INSERT INTO wolves_verify (email, discord, auth_code, date_expiry) VALUES (?1, ?2, ?3, ?4) ", ) .bind(record.email.to_owned()) .bind(*user.as_u64() as i64) .bind(auth.to_owned()) .bind(get_now_iso(false)) .fetch_optional(db) .await } } pub mod verify { use super::*; use crate::commands::link_email::link::{db_pending_clear_expired, 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 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_wolves let details = if let Some(x) = get_verify_from_db(&db, &command.user.id).await { x } else { return "Please use /link_wolves 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(); }; db_pending_clear_expired(&db).await; if &details.auth_code != code { return "Invalid verification code".to_string(); } match db_pending_clear_successful(&db, &command.user.id).await { Ok(_) => { return match set_discord(&db, &command.user.id, &details.email).await { Ok(_) => { // get teh right roles for the user set_server_roles(&db, &command.user, ctx).await; "Discord username linked to Wolves".to_string() } Err(e) => { println!("{:?}", e); "Failed to save, please try /link_wolves again".to_string() } }; } Err(e) => println!("{:?}", e), } "Failed to verify".to_string() } pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { command.name("verify").description("Verify Wolves Email").create_option(|option| { option .name("code") .description("Code from verification email") .kind(CommandOptionType::String) .required(true) }) } async fn db_pending_clear_successful(pool: &Pool, user: &UserId) -> Result, Error> { sqlx::query_as::<_, WolvesVerify>( r#" DELETE FROM wolves_verify WHERE discord = ? "#, ) .bind(*user.as_u64() as i64) .fetch_optional(pool) .await } async fn set_discord(db: &Pool, discord: &UserId, email: &str) -> Result, Error> { sqlx::query_as::<_, Wolves>( " UPDATE wolves SET discord = ? WHERE email = ? ", ) .bind(*discord.as_u64() as i64) .bind(email) .fetch_optional(db) .await } async fn set_server_roles(db: &Pool, discord: &User, ctx: &Context) { if let Ok(servers) = get_servers(db, &discord.id).await { for server in servers { if let Ok(mut member) = server.server.member(&ctx.http, &discord.id).await { if let Some(config) = get_server_config(db, &server.server).await { let Servers { role_past, role_current, .. } = config; let mut roles = vec![]; if let Some(role) = &role_past { if !member.roles.contains(role) { roles.push(role.to_owned()); } } if !member.roles.contains(&role_current) { roles.push(role_current.to_owned()); } if let Err(e) = member.add_roles(&ctx, &roles).await { println!("{:?}", e); } } } } } } async fn get_servers(db: &Pool, discord: &UserId) -> Result, Error> { sqlx::query_as::<_, ServerMembersWolves>( " SELECT * FROM server_members JOIN wolves USING (id_wolves) WHERE discord = ? ", ) .bind(*discord.as_u64() as i64) .fetch_all(db) .await } }