diff --git a/src/verify/login.rs b/src/verify/login.rs new file mode 100644 index 0000000..fdc82a6 --- /dev/null +++ b/src/verify/login.rs @@ -0,0 +1,258 @@ +use crate::{Data, Error}; +use poise::serenity_prelude as serenity; +use poise::Modal; + +const LOGIN_INTRO: &str = indoc::indoc! {" + To use automatic verification via Imperial Login: + - Open the link provided and login using your shortcode + - Your account will be checked and then the login details immediately discarded + - Your shortcode will then be connected to your Discord Account by Nano + + You can then complete the remaining details in the next step! +"}; + +pub(crate) async fn login_1( + ctx: &serenity::Context, + m: &serenity::MessageComponentInteraction, +) -> Result<(), Error> { + m.create_interaction_response(&ctx.http, |i| { + i.kind(serenity::InteractionResponseType::UpdateMessage) + .interaction_response_data(|d| { + d.content(LOGIN_INTRO).components(|c| { + c.create_action_row(|a| { + a.create_button(|b| { + b.style(serenity::ButtonStyle::Danger) + .emoji('🔙') + .custom_id("restart") + }) + .create_button(|b| { + b.style(serenity::ButtonStyle::Link) + .emoji('🚀') + .label("Login Here") + .url(format!("https://icas.8bitsqu.id/verify?id={}", m.user.id.0)) + }) + .create_button(|b| { + b.style(serenity::ButtonStyle::Secondary) + .emoji('👉') + .label("Then continue") + .custom_id("login_2") + }) + }) + }) + }) + }) + .await?; + Ok(()) +} + +const LOGIN_FORM: &str = indoc::indoc! {" + Congratulations, your Imperial shortcode has been connected to your Discord Account by Nano! + + The last step is a short form with some extra details +"}; + +pub(crate) async fn login_2( + ctx: &serenity::Context, + m: &serenity::MessageComponentInteraction, + data: &Data, +) -> Result<(), Error> { + match crate::db::get_pending_by_id(&data.db, m.user.id.0 as i64).await { + Err(e) => { + eprintln!("Error in login_2: {e}"); + m.create_interaction_response(&ctx.http, |i| { + i.kind(serenity::InteractionResponseType::ChannelMessageWithSource) + .interaction_response_data(|d| { + d.content("Sorry, something went wrong. Please try again") + .ephemeral(true) + }) + }) + .await? + } + Ok(None) => { + m.create_interaction_response(&ctx.http, |i| { + i.kind(serenity::InteractionResponseType::ChannelMessageWithSource) + .interaction_response_data(|d| { + d.content("Error, have you completed login verification via the link?") + .ephemeral(true) + }) + }) + .await? + } + Ok(Some(_)) => { + m.create_interaction_response(&ctx.http, |i| { + i.kind(serenity::InteractionResponseType::UpdateMessage) + .interaction_response_data(|d| { + d.content(LOGIN_FORM).components(|c| { + c.create_action_row(|a| { + a.create_button(|b| { + b.style(serenity::ButtonStyle::Danger) + .emoji('🔙') + .custom_id("login_1") + }) + .create_button(|b| { + b.style(serenity::ButtonStyle::Primary) + .emoji('📑') + .label("Form") + .custom_id("login_3") + }) + }) + }) + }) + }) + .await? + } + }; + Ok(()) +} + +pub(crate) async fn login_3( + ctx: &serenity::Context, + m: &serenity::MessageComponentInteraction, +) -> Result<(), Error> { + m.create_interaction_response(&ctx.http, |i| { + i.kind(serenity::InteractionResponseType::UpdateMessage) + .interaction_response_data(|d| { + d.content("Are you a fresher?").components(|c| { + c.create_action_row(|a| { + a.create_button(|b| { + b.style(serenity::ButtonStyle::Danger) + .emoji('🔙') + .custom_id("login_2") + }) + .create_button(|b| { + b.style(serenity::ButtonStyle::Success) + .emoji('✅') + .label("Fresher") + .custom_id("login_4f") + }) + .create_button(|b| { + b.style(serenity::ButtonStyle::Primary) + .emoji('❌') + .label("Non-fresher") + .custom_id("login_4n") + }) + }) + }) + }) + }) + .await?; + Ok(()) +} + +pub(crate) async fn login_4( + ctx: &serenity::Context, + m: &serenity::MessageComponentInteraction, + fresher: bool, +) -> Result<(), Error> { + m.create_interaction_response(&ctx.http, |i| { + i.kind(serenity::InteractionResponseType::UpdateMessage) + .interaction_response_data(|d| { + d.content("And a preferred name for Nano whois commands") + .components(|c| { + c.create_action_row(|a| { + a.create_button(|b| { + b.style(serenity::ButtonStyle::Danger) + .emoji('🔙') + .custom_id("login_3") + }) + .create_button(|b| { + b.style(serenity::ButtonStyle::Primary) + .emoji('💬') + .label("Name") + .custom_id(if fresher { "login_5f" } else { "login_5n" }) + }) + }) + }) + }) + }) + .await?; + Ok(()) +} + +#[derive(Modal)] +#[name = "Preferred Name"] +struct Nickname { + #[name = "Preferred name for Nano whois commands"] + #[placeholder = "Firstname Lastname"] + nickname: String, +} + +pub(crate) async fn login_5( + ctx: &serenity::Context, + m: &serenity::MessageComponentInteraction, + fresher: bool, +) -> Result<(), Error> { + m.create_interaction_response(&ctx.http, |i| { + *i = Nickname::create( + None, + if fresher { + "login_6f".to_string() + } else { + "login_6n".to_string() + }, + ); + i + }) + .await?; + Ok(()) +} + +pub(crate) async fn login_6( + ctx: &serenity::Context, + m: &serenity::ModalSubmitInteraction, + data: &Data, + fresher: bool, +) -> Result<(), Error> { + match Nickname::parse(m.data.clone()) { + Ok(Nickname { nickname }) => { + // Delete from manual if exists + let _ = crate::db::delete_manual_by_id(&data.db, m.user.id.0 as i64).await; + + match crate::db::insert_member_from_pending( + &data.db, + m.user.id.0 as i64, + &nickname, + fresher, + ) + .await + { + Ok(()) => { + let mut mm = m.member.clone().unwrap(); + crate::verify::apply_role(ctx, &mut mm, data.member).await?; + if fresher { + crate::verify::apply_role(ctx, &mut mm, data.fresher).await?; + } + m.create_interaction_response(&ctx.http, |i| { + i.kind(serenity::InteractionResponseType::UpdateMessage) + .interaction_response_data(|d| { + d.content(if fresher { + "Congratulations, you have completed verification and now \ + have access to the ICAS Discord and freshers thread" + } else { + "Congratulations, you have completed verification and now \ + have access to the ICAS Discord" + }) + .components(|c| c) + }) + }) + .await? + } + Err(e) => { + eprintln!("Error: {e}"); + m.create_interaction_response(&ctx.http, |i| { + i.kind(serenity::InteractionResponseType::ChannelMessageWithSource) + .interaction_response_data(|d| { + d.content("Sorry, something went wrong. Please try again") + .ephemeral(true) + }) + }) + .await? + } + } + } + Err(e) => { + eprintln!("Error: {e}") + } + }; + Ok(()) +} diff --git a/src/verify/manual.rs b/src/verify/manual.rs new file mode 100644 index 0000000..1595802 --- /dev/null +++ b/src/verify/manual.rs @@ -0,0 +1,266 @@ +use crate::{Data, Error}; +use poise::serenity_prelude as serenity; +use poise::Modal; + +const MANUAL_INTRO: &str = indoc::indoc! {" + Submit details to be manually checked by a committee member: + - Your Imperial Shortcode + - Your First and Last Names as on your Imperial record + - Preferred First and Last Names for the Nano whois command + - URL to proof of being an Imperial student, e.g. photo of College ID Card \ + or screenshot of College Acceptance Letter, if you need to upload this, \ + you can send it in a DM and then copy the image URL + + We try to respond quickly but this may take a day or two during busy term times :) + + First, are you a fresher? +"}; + +pub(crate) async fn manual_1( + ctx: &serenity::Context, + m: &serenity::MessageComponentInteraction, +) -> Result<(), Error> { + m.create_interaction_response(&ctx.http, |i| { + i.kind(serenity::InteractionResponseType::UpdateMessage) + .interaction_response_data(|d| { + d.content(MANUAL_INTRO).components(|c| { + c.create_action_row(|a| { + a.create_button(|b| { + b.style(serenity::ButtonStyle::Danger) + .emoji('🔙') + .custom_id("restart") + }) + .create_button(|b| { + b.style(serenity::ButtonStyle::Success) + .emoji('✅') + .label("Fresher") + .custom_id("manual_2f") + }) + .create_button(|b| { + b.style(serenity::ButtonStyle::Primary) + .emoji('❌') + .label("Non-fresher") + .custom_id("manual_2n") + }) + }) + }) + }) + }) + .await?; + Ok(()) +} + +#[derive(Modal)] +#[name = "Manual Verification"] +struct Manual { + #[name = "Imperial Shortcode"] + #[placeholder = "ab1234"] + shortcode: String, + #[name = "Name as on Imperial record"] + #[placeholder = "Firstname Lastname"] + realname: String, + #[name = "URL to proof image"] + #[placeholder = "E.g. photo of College ID Card \ + or screenshot of College Acceptance Letter"] + url: String, + #[name = "Preferred name for Nano whois commands"] + #[placeholder = "Firstname Lastname"] + nickname: String, +} + +pub(crate) async fn manual_2( + ctx: &serenity::Context, + m: &serenity::MessageComponentInteraction, + data: &Data, + fresher: bool, +) -> Result<(), Error> { + // Delete from manual if exists + let _ = crate::db::delete_manual_by_id(&data.db, m.user.id.0 as i64).await; + + m.create_interaction_response(&ctx.http, |i| { + *i = Manual::create( + None, + if fresher { + "manual_3f".to_string() + } else { + "manual_3n".to_string() + }, + ); + i + }) + .await?; + Ok(()) +} + +pub(crate) async fn manual_3( + ctx: &serenity::Context, + m: &serenity::ModalSubmitInteraction, + data: &Data, + fresher: bool, +) -> Result<(), Error> { + match Manual::parse(m.data.clone()) { + Ok(Manual { + shortcode, + realname, + url, + nickname, + }) => { + if ::url::Url::parse(&url).is_err() { + m.create_interaction_response(&ctx.http, |i| { + i.kind(serenity::InteractionResponseType::ChannelMessageWithSource) + .interaction_response_data(|d| { + d.content("The url provided is invalid, please try again") + .ephemeral(true) + }) + }) + .await?; + return Ok(()); + } + + // Delete from pending if exists + let _ = crate::db::delete_pending_by_id(&data.db, m.user.id.0 as i64).await?; + + let prompt_sent = data + .au_ch_id + .send_message(&ctx.http, |cm| { + cm.add_embed(|e| { + e.title("New verification request from") + .thumbnail(m.user.avatar_url().unwrap_or( + "https://cdn.discordapp.com/embed/avatars/0.png".to_string(), + )) + .description(&m.user) + .field("Real Name (To be checked)", &realname, true) + .field("Imperial Shortcode (To be checked", &shortcode, true) + .field("Fresher (To be checked)", fresher, true) + .field("Nickname (Nano whois commands)", &nickname, true) + .field("Verification URL (Also displayed below)", &url, true) + .image(&url) + .timestamp(serenity::Timestamp::now()) + }) + .components(|c| { + c.create_action_row(|a| { + a.create_button(|b| { + b.style(serenity::ButtonStyle::Success) + .emoji('✅') + .label("Accept") + .custom_id(format!("verify-y-{}", m.user.id)) + }) + .create_button(|b| { + b.style(serenity::ButtonStyle::Danger) + .emoji('❎') + .label("Deny") + .custom_id(format!("verify-n-{}", m.user.id)) + }) + }) + }) + }) + .await + .is_ok(); + + let inserted = crate::db::insert_manual( + &data.db, + crate::ManualMember { + discord_id: m.user.id.0 as i64, + shortcode, + nickname, + realname, + fresher, + }, + ) + .await + .is_ok(); + + let msg = if prompt_sent { + if inserted { + "Thanks, your verification request has been sent, we'll try to get back to you quickly!" + } else { + "Thanks, your verification request has been sent, but there was an issue, please ask a Committee member to take a look!" + } + } else { + "Sending your verification request failed, please try again." + }; + + m.create_interaction_response(&ctx.http, |i| { + i.kind(serenity::InteractionResponseType::UpdateMessage) + .interaction_response_data(|d| d.content(msg).components(|c| c)) + }) + .await?; + return Ok(()); + } + Err(e) => eprintln!("Error: {e}"), + }; + m.create_interaction_response(&ctx.http, |i| { + i.kind(serenity::InteractionResponseType::ChannelMessageWithSource) + .interaction_response_data(|d| { + d.content("Sorry, something went wrong. Please try again") + .ephemeral(true) + }) + }) + .await?; + Ok(()) +} + +pub(crate) async fn manual_4( + ctx: &serenity::Context, + m: &serenity::MessageComponentInteraction, + data: &Data, + id: &str, +) -> Result<(), Error> { + let verify = match id.chars().nth(7) { + Some('y') => true, + Some('n') => false, + _ => false, + }; + + let user = id + .chars() + .skip(9) + .collect::() + .parse::() + .map(serenity::UserId) + .unwrap_or_default() + .to_user(ctx) + .await + .unwrap_or_default(); + + match crate::db::insert_member_from_manual(&data.db, user.id.0 as i64).await { + Ok(()) => { + let fresher = crate::db::get_member_by_id(&data.db, user.id.0 as i64) + .await? + .unwrap() + .fresher; + let mut mm = m.member.clone().unwrap(); + crate::verify::apply_role(ctx, &mut mm, data.member).await?; + if fresher { + crate::verify::apply_role(ctx, &mut mm, data.fresher).await?; + } + m.create_interaction_response(&ctx.http, |i| { + i.kind(serenity::InteractionResponseType::UpdateMessage) + .interaction_response_data(|d| { + d.components(|c| c).embed(|e| { + e.title(format!( + "Verification {} for", + if verify { "accepted" } else { "denied" }, + )) + .description(&user) + .thumbnail(user.avatar_url().unwrap_or_default()) + .timestamp(serenity::Timestamp::now()) + }) + }) + }) + .await? + } + Err(e) => { + eprintln!("Error: {e}"); + m.create_interaction_response(&ctx.http, |i| { + i.kind(serenity::InteractionResponseType::ChannelMessageWithSource) + .interaction_response_data(|d| { + d.content(format!("Failed to add user {user} to member database")) + }) + }) + .await? + } + } + + Ok(()) +} diff --git a/src/verify/membership.rs b/src/verify/membership.rs new file mode 100644 index 0000000..6d6f839 --- /dev/null +++ b/src/verify/membership.rs @@ -0,0 +1,179 @@ +use crate::{Data, Error}; +use poise::serenity_prelude as serenity; +use poise::Modal; + +const MEMBERSHIP_INTRO: &str = indoc::indoc! {" + To use automatic verification via Membership: + - Enter your Union order number (from this academic year) + - Enter your Imperial shortcode + - Enter your preferred name for Nano whois commands + - Your shortcode will then be connected to your Discord Account by Nano + + First, are you a fresher? +"}; + +pub(crate) async fn membership_1( + ctx: &serenity::Context, + m: &serenity::MessageComponentInteraction, +) -> Result<(), Error> { + m.create_interaction_response(&ctx.http, |i| { + i.kind(serenity::InteractionResponseType::UpdateMessage) + .interaction_response_data(|d| { + d.content(MEMBERSHIP_INTRO).components(|c| { + c.create_action_row(|a| { + a.create_button(|b| { + b.style(serenity::ButtonStyle::Danger) + .emoji('🔙') + .custom_id("restart") + }) + .create_button(|b| { + b.style(serenity::ButtonStyle::Success) + .emoji('✅') + .label("Fresher") + .custom_id("membership_2f") + }) + .create_button(|b| { + b.style(serenity::ButtonStyle::Primary) + .emoji('❌') + .label("Non-fresher") + .custom_id("membership_2n") + }) + }) + }) + }) + }) + .await?; + Ok(()) +} + +#[derive(Modal)] +#[name = "ICAS Membership Verification"] +struct Membership { + #[name = "ICAS Membership Union Order Number"] + #[placeholder = "1234567"] + order: String, + #[name = "Imperial Shortcode"] + #[placeholder = "ab1234"] + shortcode: String, + #[name = "Preferred name for Nano whois commands"] + #[placeholder = "Firstname Lastname"] + nickname: String, +} + +pub(crate) async fn membership_2( + ctx: &serenity::Context, + m: &serenity::MessageComponentInteraction, + data: &Data, + fresher: bool, +) -> Result<(), Error> { + // Delete from pending if exists + let _ = crate::db::delete_pending_by_id(&data.db, m.user.id.0 as i64).await; + + // Delete from manual if exists + let _ = crate::db::delete_manual_by_id(&data.db, m.user.id.0 as i64).await; + + m.create_interaction_response(&ctx.http, |i| { + *i = Membership::create( + None, + if fresher { + "membership_3f".to_string() + } else { + "membership_3n".to_string() + }, + ); + i + }) + .await?; + Ok(()) +} + +pub(crate) async fn membership_3( + ctx: &serenity::Context, + m: &serenity::ModalSubmitInteraction, + data: &Data, + fresher: bool, +) -> Result<(), Error> { + match Membership::parse(m.data.clone()) { + Ok(Membership { + order, + shortcode, + nickname, + }) => { + let members = match crate::ea::get_members_list(&data.ea_key, &data.ea_url).await { + Ok(v) => v, + Err(e) => { + eprintln!("Error: {e}"); + m.create_interaction_response(&ctx.http, |i| { + i.kind(serenity::InteractionResponseType::ChannelMessageWithSource) + .interaction_response_data(|d| { + d.content("Sorry, getting membership data failed. Please try again") + .ephemeral(true) + }) + }) + .await?; + return Ok(()); + } + }; + let member = match members + .iter() + .find(|&member| member.order_no.to_string() == order && member.login == shortcode) + { + Some(m) => m, + None => { + m.create_interaction_response(&ctx.http, |i| { + let msg = "Sorry, your order was not found, please check the \ + order number and that it is for your current year's membership"; + i.kind(serenity::InteractionResponseType::ChannelMessageWithSource) + .interaction_response_data(|d| d.content(msg).ephemeral(true)) + }) + .await?; + return Ok(()); + } + }; + if crate::db::insert_member( + &data.db, + crate::Member { + discord_id: m.user.id.0 as i64, + shortcode, + nickname, + realname: format!("{} {}", member.first_name, member.surname), + fresher, + }, + ) + .await + .is_ok() + { + let mut mm = m.member.clone().unwrap(); + crate::verify::apply_role(ctx, &mut mm, data.member).await?; + if fresher { + crate::verify::apply_role(ctx, &mut mm, data.fresher).await?; + } + m.create_interaction_response(&ctx.http, |i| { + i.kind(serenity::InteractionResponseType::UpdateMessage) + .interaction_response_data(|d| { + d.content(if fresher { + "Congratulations, you have completed verification and now \ + have access to the ICAS Discord and freshers thread" + } else { + "Congratulations, you have completed verification and now \ + have access to the ICAS Discord" + }) + .components(|c| c) + }) + }) + .await?; + return Ok(()); + } + } + Err(e) => eprintln!("Error: {e}"), + }; + m.create_interaction_response(&ctx.http, |i| { + i.kind(serenity::InteractionResponseType::ChannelMessageWithSource) + .interaction_response_data(|d| { + d.content("Sorry, something went wrong. Please try again") + .ephemeral(true) + }) + }) + .await?; + Ok(()) +} diff --git a/src/verify/mod.rs b/src/verify/mod.rs new file mode 100644 index 0000000..9e9f307 --- /dev/null +++ b/src/verify/mod.rs @@ -0,0 +1,107 @@ +use crate::{Data, Error}; +use poise::serenity_prelude as serenity; + +pub(crate) mod login; +pub(crate) use login::*; + +pub(crate) mod membership; +pub(crate) use membership::*; + +pub(crate) mod manual; +pub(crate) use manual::*; + +pub(crate) async fn unknown( + ctx: &serenity::Context, + m: &serenity::MessageComponentInteraction, +) -> Result<(), Error> { + m.create_interaction_response(&ctx.http, |i| { + i.kind(serenity::InteractionResponseType::ChannelMessageWithSource) + .interaction_response_data(|d| { + d.content("Sorry, something went wrong. Please try again or message <@99217900254035968> for help") + .ephemeral(true) + }) + }) + .await?; + Ok(()) +} + +const START_MSG: &str = indoc::indoc! {" + There are 3 available methods for verification. + - 🚀 Automatic verification via Imperial Login (Quickest) + - ✈️ Automatic verification via ICAS Membership (Easiest) + - 🚗 Manual verification, eg. using College ID Card or Acceptance Letter +"}; + +pub(crate) async fn start( + ctx: &serenity::Context, + m: &serenity::MessageComponentInteraction, + data: &Data, + init: bool, +) -> Result<(), Error> { + // Check if user is already verified + if let Some(member) = crate::db::get_member_by_id(&data.db, m.user.id.0 as i64).await? { + let mut mm = m.member.clone().unwrap(); + apply_role(ctx, &mut mm, data.member).await?; + if member.fresher { + apply_role(ctx, &mut mm, data.fresher).await?; + } + m.create_interaction_response(&ctx.http, |i| { + i.kind(serenity::InteractionResponseType::ChannelMessageWithSource) + .interaction_response_data(|d| { + d.content("Welcome, you're already verified, re-applied your roles!") + .ephemeral(true) + }) + }) + .await? + } else { + m.create_interaction_response(&ctx.http, |i| { + i.kind(if init { + serenity::InteractionResponseType::ChannelMessageWithSource + } else { + serenity::InteractionResponseType::UpdateMessage + }) + .interaction_response_data(|d| { + d.content(START_MSG).ephemeral(true).components(|c| { + c.create_action_row(|a| { + a.create_button(|b| { + b.style(serenity::ButtonStyle::Primary) + .emoji('🚀') + .label("Login") + .custom_id("login_1") + }) + .create_button(|b| { + b.style(serenity::ButtonStyle::Secondary) + .emoji(serenity::ReactionType::Unicode("✈️".to_string())) + .label("Membership") + .custom_id("membership_1") + }) + .create_button(|b| { + b.style(serenity::ButtonStyle::Secondary) + .emoji('🚗') + .label("Manual") + .custom_id("manual_1") + }) + }) + }) + }) + }) + .await? + }; + Ok(()) +} + +pub(crate) async fn apply_role( + ctx: &serenity::Context, + member: &mut serenity::Member, + role: serenity::RoleId, +) -> Result<(), Error> { + Ok(member.add_role(&ctx.http, role).await?) +} + +pub(crate) async fn remove_role( + ctx: &serenity::Context, + member: &mut serenity::Member, + role: serenity::RoleId, +) -> Result<(), Error> { + Ok(member.remove_role(&ctx.http, role).await?) +}