From 7e40b862d30b18f18a3fd23a89f3a119d35296a4 Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Sat, 31 Aug 2024 16:05:38 +0100 Subject: [PATCH 1/5] 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; -- 2.46.1 From 439d49db43a410f372ff02cf39c63c7cdc492da2 Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Sat, 31 Aug 2024 18:58:46 +0100 Subject: [PATCH 2/5] fix: use the proper toolchain --- rust-toolchain.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 6c548fa..02ad72f 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.69.0" \ No newline at end of file +channel = "1.69" \ No newline at end of file -- 2.46.1 From bda3fbe2addbe83dbab2ccf29e0a1d7c35f892f0 Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Sat, 31 Aug 2024 19:20:24 +0100 Subject: [PATCH 3/5] feat: import and use the commands --- src/main.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main.rs b/src/main.rs index d1ebf2d..cbe8d5d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -64,6 +64,9 @@ impl EventHandler for Handler { .create_application_command(|command| commands::minecraft::server::list::register(command)) .create_application_command(|command| commands::minecraft::server::delete::register(command)) .create_application_command(|command| commands::minecraft::user::add::register(command)) + // for committee server, temp + .create_application_command(|command| commands::committee::link::register(command)) + .create_application_command(|command| commands::committee::verify::register(command)) }) .await { @@ -89,6 +92,9 @@ impl EventHandler for Handler { "minecraft_add" => commands::minecraft::server::add::run(&command, &ctx).await, "minecraft_list" => commands::minecraft::server::list::run(&command, &ctx).await, "minecraft_delete" => commands::minecraft::server::delete::run(&command, &ctx).await, + // for teh committee server, temporary + "link_committee" => commands::committee::link::run(&command, &ctx).await, + "verify_committee" => commands::committee::verify::run(&command, &ctx).await, _ => "not implemented :(".to_string(), }; -- 2.46.1 From 5c2502f726466452d539a8229d8adc13955c7900 Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Sat, 31 Aug 2024 19:21:01 +0100 Subject: [PATCH 4/5] fix: small fixes to actually make it work --- db/migrations/4_committee-mk-i.sql | 2 +- src/commands/committee.rs | 27 ++++++++++++++------------- src/lib.rs | 22 ++++++++++++++++++++++ 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/db/migrations/4_committee-mk-i.sql b/db/migrations/4_committee-mk-i.sql index c4a78e4..25f925f 100644 --- a/db/migrations/4_committee-mk-i.sql +++ b/db/migrations/4_committee-mk-i.sql @@ -3,5 +3,5 @@ CREATE TABLE IF NOT EXISTS committee ( discord integer PRIMARY KEY, email text not null, auth_code text not null, - committee integer DEFAULT 0, + committee integer DEFAULT 0 ); \ No newline at end of file diff --git a/src/commands/committee.rs b/src/commands/committee.rs index d135570..0702a0e 100644 --- a/src/commands/committee.rs +++ b/src/commands/committee.rs @@ -13,12 +13,13 @@ use serenity::{ prelude::{command::CommandOptionType, interaction::application_command::CommandDataOptionValue}, }, }; -use skynet_discord_bot::{get_now_iso, random_string, Config, DataBase, Wolves, WolvesVerify}; +use skynet_discord_bot::{random_string, Config, DataBase}; use sqlx::{Pool, Sqlite}; pub mod link { use serenity::model::id::GuildId; + use skynet_discord_bot::Committee; use super::*; pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String { @@ -101,8 +102,8 @@ pub mod link { .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>( + pub async fn get_server_member_discord(db: &Pool, user: &UserId) -> Option { + sqlx::query_as::<_, Committee>( r#" SELECT * FROM committee @@ -175,8 +176,8 @@ pub mod link { mailer.send(&email) } - pub async fn get_verify_from_db(db: &Pool, user: &UserId) -> Option { - sqlx::query_as::<_, WolvesVerify>( + pub async fn get_verify_from_db(db: &Pool, user: &UserId) -> Option { + sqlx::query_as::<_, Committee>( r#" SELECT * FROM committee @@ -189,8 +190,8 @@ pub mod link { .ok() } - async fn save_to_db(db: &Pool, email: &str, auth: &str, user: &UserId) -> Result, sqlx::Error> { - sqlx::query_as::<_, WolvesVerify>( + async fn save_to_db(db: &Pool, email: &str, auth: &str, user: &UserId) -> Result, sqlx::Error> { + sqlx::query_as::<_, Committee>( " INSERT INTO committee (email, discord, auth_code) VALUES (?1, ?2, ?3) @@ -209,7 +210,7 @@ pub mod verify { 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 skynet_discord_bot::{Committee}; use sqlx::Error; pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String { @@ -227,7 +228,7 @@ pub mod verify { let db_lock = { let data_read = ctx.data.read().await; - data_read.get::().expect("Expected Databse in TypeMap.").clone() + data_read.get::().expect("Expected Database in TypeMap.").clone() }; let db = db_lock.read().await; @@ -257,7 +258,7 @@ pub mod verify { return "Invalid verification code".to_string(); } - return match set_discord(&db, &command.user.id).await { + match set_discord(&db, &command.user.id).await { Ok(_) => { // get teh right roles for the user set_server_roles(&command.user, ctx).await; @@ -267,7 +268,7 @@ pub mod verify { println!("{:?}", e); "Failed to save, please try /link_committee again".to_string() } - }; + } } pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { @@ -280,8 +281,8 @@ pub mod verify { }) } - async fn set_discord(db: &Pool, discord: &UserId) -> Result, Error> { - sqlx::query_as::<_, Wolves>( + async fn set_discord(db: &Pool, discord: &UserId) -> Result, Error> { + sqlx::query_as::<_, Committee>( " UPDATE committee SET committee = 1 diff --git a/src/lib.rs b/src/lib.rs index f05e58b..20fcb46 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -218,6 +218,28 @@ impl<'r> FromRow<'r, SqliteRow> for WolvesVerify { } } + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Committee { + pub email: String, + pub discord: UserId, + pub auth_code: String, + pub committee: i64, +} +impl<'r> FromRow<'r, SqliteRow> for Committee { + fn from_row(row: &'r SqliteRow) -> Result { + let user_tmp: i64 = row.try_get("discord")?; + let discord = UserId::from(user_tmp as u64); + + Ok(Self { + email: row.try_get("email")?, + discord, + auth_code: row.try_get("auth_code")?, + committee: row.try_get("committee")?, + }) + } +} + #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Servers { pub server: GuildId, -- 2.46.1 From 50d2923425be1cac9f29e1e75670694507e376e0 Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Sat, 31 Aug 2024 19:21:58 +0100 Subject: [PATCH 5/5] fmt: fmt and clippy --- src/commands/committee.rs | 52 +++++++++++++++++++++------------------ src/commands/mod.rs | 2 +- src/lib.rs | 1 - 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/src/commands/committee.rs b/src/commands/committee.rs index 0702a0e..3de0ec9 100644 --- a/src/commands/committee.rs +++ b/src/commands/committee.rs @@ -16,11 +16,10 @@ use serenity::{ use skynet_discord_bot::{random_string, Config, DataBase}; use sqlx::{Pool, Sqlite}; - pub mod link { + use super::*; use serenity::model::id::GuildId; use skynet_discord_bot::Committee; - use super::*; pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String { let committee_server = GuildId(1220150752656363520); @@ -35,15 +34,14 @@ pub mod link { } } - let option = command - .data - .options - .first() - .expect("Expected email option") - .resolved - .as_ref() - .expect("Expected email object"); + .data + .options + .first() + .expect("Expected email option") + .resolved + .as_ref() + .expect("Expected email object"); let email = if let CommandDataOptionValue::String(email) = option { email.trim() @@ -56,7 +54,6 @@ pub mod link { 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() @@ -77,7 +74,6 @@ pub mod link { 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) { @@ -99,7 +95,13 @@ pub mod link { 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)) + .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 { @@ -206,11 +208,11 @@ pub mod link { } pub mod verify { - use serenity::model::id::{GuildId, RoleId}; use super::*; - use crate::commands::committee::link::{get_verify_from_db}; + use crate::commands::committee::link::get_verify_from_db; + use serenity::model::id::{GuildId, RoleId}; use serenity::model::user::User; - use skynet_discord_bot::{Committee}; + use skynet_discord_bot::Committee; use sqlx::Error; pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String { @@ -272,13 +274,16 @@ pub mod verify { } 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) - }) + 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> { @@ -303,5 +308,4 @@ pub mod verify { } } } - } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index c95bf66..3acfe81 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,4 +1,4 @@ pub mod add_server; +pub mod committee; pub mod link_email; pub mod minecraft; -pub mod committee; diff --git a/src/lib.rs b/src/lib.rs index 20fcb46..cc8f7f3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -218,7 +218,6 @@ impl<'r> FromRow<'r, SqliteRow> for WolvesVerify { } } - #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Committee { pub email: String, -- 2.46.1