mirror of
https://github.com/dsec-hub/dsec-discord-bot.git
synced 2026-09-22 07:44:26 +00:00
updated verify command
This commit is contained in:
parent
163be0c990
commit
27e84e98a7
3 changed files with 225 additions and 230 deletions
|
|
@ -1,44 +1,32 @@
|
|||
use crate::{ApplicationContext, Error};
|
||||
use dotenv::dotenv;
|
||||
use poise::{CreateReply, Modal};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serenity::all::{
|
||||
CreateActionRow, CreateButton, CreateEmbed, CreateEmbedFooter, GuildId, RoleId,
|
||||
};
|
||||
use serenity::all::{CreateActionRow, CreateButton, CreateEmbed};
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
struct StudentRow {
|
||||
full_name: String,
|
||||
student_id: String,
|
||||
pub struct StudentRow {
|
||||
pub full_name: String,
|
||||
pub student_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Modal)]
|
||||
#[name = "Club Verification"] // Struct name by default
|
||||
struct VerificationModal {
|
||||
#[name = "Club Verification"]
|
||||
pub struct VerificationModal {
|
||||
#[name = "Full Name"]
|
||||
#[placeholder = "John Doe"]
|
||||
#[max_length = 50]
|
||||
name: String,
|
||||
pub name: String,
|
||||
#[name = "Student ID"]
|
||||
#[placeholder = "s123456789"]
|
||||
student_id: String,
|
||||
pub student_id: String,
|
||||
}
|
||||
|
||||
#[poise::command(slash_command, subcommands("embed", "myself"))]
|
||||
pub async fn verify(_: ApplicationContext<'_>) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verify your DSEC club membership to obtain role
|
||||
#[poise::command(slash_command)]
|
||||
pub async fn myself(ctx: ApplicationContext<'_>) -> Result<(), Error> {
|
||||
verify_member(ctx).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[poise::command(slash_command)]
|
||||
pub async fn embed(ctx: ApplicationContext<'_>) -> Result<(), Error> {
|
||||
/// Embed message with verify button to verify membership
|
||||
#[poise::command(
|
||||
slash_command,
|
||||
required_permissions = "MANAGE_MESSAGES | MANAGE_THREADS"
|
||||
)]
|
||||
pub async fn verify(ctx: ApplicationContext<'_>) -> Result<(), Error> {
|
||||
let reply: CreateReply = {
|
||||
let embed: CreateEmbed = CreateEmbed::new()
|
||||
.title("Verify your DSEC membership")
|
||||
|
|
@ -56,193 +44,5 @@ pub async fn embed(ctx: ApplicationContext<'_>) -> Result<(), Error> {
|
|||
|
||||
ctx.send(reply).await?;
|
||||
|
||||
// let mut stream = ComponentInteractionCollector::new(ctx.serenity_context())
|
||||
// .filter(move |mci| mci.data.custom_id == "verify")
|
||||
// .stream();
|
||||
|
||||
// while let Some(_) = stream.next().await {
|
||||
// verify_member(ctx).await?;
|
||||
// }
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// flow:
|
||||
// 1. check correct discord server -> layer 1
|
||||
// 2. check if user has role -> layer 1
|
||||
// 3. get user input
|
||||
// 4. check cache for membership -> layer 2
|
||||
// 5. check db for membership + update cache -> layer 3
|
||||
|
||||
// Student ID not found: - add student ID to negative cache (cooldown) if ID not present (pending)
|
||||
// Student ID found:
|
||||
// - check if name matches full name
|
||||
// - if match, assign role
|
||||
async fn verify_member(ctx: ApplicationContext<'_>) -> Result<(), Error> {
|
||||
dotenv().ok();
|
||||
|
||||
// check if user is in a Discord server, then check if user is in THE Discord server
|
||||
let guild_id = ctx.guild_id();
|
||||
|
||||
if guild_id.is_none() {
|
||||
ctx.say("Error, not in server").await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let guild_id_string = std::env::var("GUILD_ID").expect("missing GUILD_ID");
|
||||
let role_id_string = std::env::var("VERIFIED_ROLE_ID").expect("missing VERIFIED_ROLE_ID");
|
||||
|
||||
let guild_id_u64: u64 = guild_id_string
|
||||
.parse()
|
||||
.expect("Unable to parse GUILD_ID into number");
|
||||
|
||||
let role_id_u64: u64 = role_id_string
|
||||
.parse()
|
||||
.expect("Unable to parse VERIFIED_ROLE_ID into number");
|
||||
|
||||
let server = GuildId::new(guild_id_u64);
|
||||
|
||||
let in_correct_server = &guild_id.unwrap() == &server; // CHANGE
|
||||
|
||||
if !in_correct_server {
|
||||
ctx.say("Wrong server error. If you're in the DSEC server, let DSEC admins know")
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// role ID 1441965955822649344
|
||||
let user_id = ctx.author().id;
|
||||
|
||||
let verified_role_id = RoleId::new(role_id_u64); // hardcoded for now
|
||||
let discord_member = GuildId::member(ctx.guild_id().unwrap(), ctx, user_id).await?;
|
||||
let has_role = discord_member.roles.contains(&verified_role_id);
|
||||
|
||||
if has_role {
|
||||
let already_verified_embed =
|
||||
CreateEmbed::new()
|
||||
.title("Already Verified ✅")
|
||||
.description(format!(
|
||||
"You already have the <@&{}> role!",
|
||||
verified_role_id
|
||||
));
|
||||
|
||||
ctx.send(
|
||||
CreateReply::default()
|
||||
.embed(already_verified_embed)
|
||||
.ephemeral(true),
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// get user input
|
||||
let modal_data = VerificationModal::execute(ctx)
|
||||
.await?
|
||||
.expect("Modal failed");
|
||||
|
||||
// remove s if input student ID starts with it
|
||||
let input_student_id: &str = &modal_data.student_id.to_lowercase();
|
||||
let student_id = input_student_id
|
||||
.strip_prefix("s")
|
||||
.unwrap_or(input_student_id);
|
||||
|
||||
let state = &ctx.data().state;
|
||||
|
||||
let student_in_cache: bool = {
|
||||
let cache = state.student_cache.lock().expect("Failed to get cache");
|
||||
|
||||
println!("{:?}", cache.get(student_id));
|
||||
|
||||
match cache.get(student_id) {
|
||||
Some(cached_name) => cached_name == &modal_data.name.to_lowercase(),
|
||||
None => false,
|
||||
}
|
||||
};
|
||||
|
||||
if student_in_cache {
|
||||
discord_member.add_role(ctx, verified_role_id).await?;
|
||||
|
||||
let verified_cache_embed = CreateEmbed::new()
|
||||
.title("Verified ✅")
|
||||
.description(format!(
|
||||
"You have been assigned the <@&{}> role!",
|
||||
verified_role_id
|
||||
))
|
||||
.footer(CreateEmbedFooter::new("⚡ via cache"));
|
||||
|
||||
ctx.send(
|
||||
CreateReply::default()
|
||||
.embed(verified_cache_embed)
|
||||
.ephemeral(true),
|
||||
)
|
||||
.await?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// fetch from DB
|
||||
let student_data: Vec<StudentRow> = state
|
||||
.supabase
|
||||
.database()
|
||||
.from("active_members")
|
||||
.select("full_name, student_id")
|
||||
.eq("student_id", &student_id)
|
||||
.execute()
|
||||
.await?;
|
||||
|
||||
let result = student_data.iter().next();
|
||||
|
||||
// Student ID not found
|
||||
if result.is_none() {
|
||||
// TODO: add user to "don't use this command for 5 minutes"
|
||||
|
||||
let id_not_found_embed = CreateEmbed::new()
|
||||
.title("Student ID not found!")
|
||||
.description("Your student ID is not found. It takes up to **a week** for your membership to be updated in the database since sign up. Try again later.");
|
||||
|
||||
ctx.send(
|
||||
CreateReply::default()
|
||||
.embed(id_not_found_embed)
|
||||
.ephemeral(true),
|
||||
)
|
||||
.await?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// get name from result
|
||||
let result_name = &result.unwrap().full_name;
|
||||
|
||||
{
|
||||
let mut cache = state.student_cache.lock().unwrap();
|
||||
cache.insert(
|
||||
student_id.to_string(),
|
||||
result_name.to_string().to_lowercase(),
|
||||
);
|
||||
}
|
||||
|
||||
if &result_name.to_lowercase() == &modal_data.name.to_lowercase() {
|
||||
discord_member.add_role(ctx, verified_role_id).await?;
|
||||
|
||||
let verified_embed = CreateEmbed::new().title("Verified ✅").description(format!(
|
||||
"You have been assigned the <@&{}> role!",
|
||||
verified_role_id
|
||||
));
|
||||
|
||||
ctx.send(CreateReply::default().embed(verified_embed))
|
||||
.await?;
|
||||
} else {
|
||||
let name_mismatched_embed = CreateEmbed::new()
|
||||
.title("Name mismatch ❌")
|
||||
.description("Your student ID is present, however the name does not match. Try again.");
|
||||
|
||||
ctx.send(
|
||||
CreateReply::default()
|
||||
.embed(name_mismatched_embed)
|
||||
.ephemeral(true),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,218 @@
|
|||
use crate::Error;
|
||||
use poise::serenity_prelude as serenity;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Debug, poise::Modal)]
|
||||
#[allow(dead_code)] // fields only used for Debug print
|
||||
struct MyModal {
|
||||
first_input: String,
|
||||
second_input: Option<String>,
|
||||
use crate::{
|
||||
Data, Error,
|
||||
commands::verification::{StudentRow, VerificationModal},
|
||||
};
|
||||
use ::serenity::all::{
|
||||
ComponentInteraction, Context, CreateEmbed, CreateEmbedFooter, CreateInteractionResponse,
|
||||
CreateInteractionResponseFollowup, CreateInteractionResponseMessage, GuildId, RoleId,
|
||||
};
|
||||
use dotenv::dotenv;
|
||||
use poise::{modal, serenity_prelude as serenity};
|
||||
|
||||
struct ContextRef<'a>(&'a Context);
|
||||
impl AsRef<Context> for ContextRef<'_> {
|
||||
fn as_ref(&self) -> &Context {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
async fn embed_response(
|
||||
ctx: &serenity::Context,
|
||||
interaction: &ComponentInteraction,
|
||||
title: impl Into<String>,
|
||||
description: impl Into<String>,
|
||||
) -> Result<(), Error> {
|
||||
let embed = CreateEmbed::new().title(title).description(description);
|
||||
|
||||
let response = CreateInteractionResponse::Message(
|
||||
CreateInteractionResponseMessage::new()
|
||||
.add_embed(embed)
|
||||
.ephemeral(true),
|
||||
);
|
||||
|
||||
interaction.create_response(ctx, response).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn embed_followup(
|
||||
ctx: &serenity::Context,
|
||||
interaction: &ComponentInteraction,
|
||||
title: impl Into<String>,
|
||||
description: impl Into<String>,
|
||||
) -> Result<(), Error> {
|
||||
let embed = CreateEmbed::new().title(title).description(description);
|
||||
|
||||
let response = CreateInteractionResponseFollowup::new()
|
||||
.add_embed(embed)
|
||||
.ephemeral(true);
|
||||
|
||||
interaction.create_followup(ctx, response).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn on_interaction_create(
|
||||
_ctx: &serenity::Context,
|
||||
ctx: &serenity::Context,
|
||||
interaction: &serenity::Interaction,
|
||||
data: &Data,
|
||||
) -> Result<(), Error> {
|
||||
let some_message_component = interaction.as_message_component();
|
||||
let Some(interaction) = interaction.as_message_component() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if !some_message_component.is_none() {
|
||||
let component = some_message_component.unwrap();
|
||||
if interaction.data.custom_id == "verify" {
|
||||
dotenv().ok();
|
||||
let guild_id = match interaction.guild_id {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
// user not in a server at all
|
||||
embed_response(
|
||||
ctx,
|
||||
interaction,
|
||||
"Unable to perform action",
|
||||
"Action can only be performed in the DSEC server",
|
||||
)
|
||||
.await?;
|
||||
|
||||
if component.data.custom_id == "verify" {
|
||||
println!("Component data: {:?}", component);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let role_id_string = std::env::var("VERIFIED_ROLE_ID").expect("missing VERIFIED_ROLE_ID");
|
||||
|
||||
let role_id_u64: u64 = role_id_string
|
||||
.parse()
|
||||
.expect("Unable to parse VERIFIED_ROLE_ID into number");
|
||||
|
||||
let user_id = interaction.user.id;
|
||||
let verified_role_id = RoleId::new(role_id_u64);
|
||||
|
||||
let discord_member = GuildId::member(guild_id, ctx, user_id).await?;
|
||||
let has_role = discord_member.roles.contains(&verified_role_id);
|
||||
|
||||
// Has role
|
||||
if has_role {
|
||||
embed_response(
|
||||
ctx,
|
||||
interaction,
|
||||
"Already Verified ✅",
|
||||
format!("You already have the <@&{}> role!", verified_role_id),
|
||||
)
|
||||
.await?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// modal
|
||||
let timeout = Duration::from_secs(120);
|
||||
|
||||
let modal_data = modal::execute_modal_on_component_interaction::<VerificationModal>(
|
||||
ContextRef(ctx),
|
||||
interaction.clone(),
|
||||
None,
|
||||
Some(timeout),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let modal_data = match modal_data {
|
||||
Some(data) => data,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let input_student_id: &str = &modal_data.student_id.to_lowercase();
|
||||
let student_id = input_student_id
|
||||
.strip_prefix("s")
|
||||
.unwrap_or(input_student_id);
|
||||
|
||||
let state = &data.state;
|
||||
|
||||
let student_in_cache: bool = {
|
||||
let cache: std::sync::MutexGuard<'_, std::collections::HashMap<String, String>> =
|
||||
state.student_cache.lock().expect("Failed to get cache");
|
||||
|
||||
match cache.get(student_id) {
|
||||
Some(cached_name) => cached_name == &modal_data.name.to_lowercase(),
|
||||
None => false,
|
||||
}
|
||||
};
|
||||
|
||||
if student_in_cache {
|
||||
discord_member.add_role(ctx, verified_role_id).await?;
|
||||
|
||||
let verified_cache_embed = CreateEmbed::new()
|
||||
.title("Verified ✅")
|
||||
.description(format!(
|
||||
"You have been assigned the <@&{}> role!",
|
||||
verified_role_id
|
||||
))
|
||||
.footer(CreateEmbedFooter::new("⚡ via cache"));
|
||||
|
||||
let verified_msg = CreateInteractionResponseFollowup::new()
|
||||
.add_embed(verified_cache_embed)
|
||||
.ephemeral(true);
|
||||
|
||||
interaction.create_followup(ctx, verified_msg).await?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// fetch from DB
|
||||
let student_data: Vec<StudentRow> = state
|
||||
.supabase
|
||||
.database()
|
||||
.from("active_members")
|
||||
.select("full_name, student_id")
|
||||
.eq("student_id", &student_id)
|
||||
.execute()
|
||||
.await?;
|
||||
|
||||
let result = student_data.iter().next();
|
||||
|
||||
if result.is_none() {
|
||||
// TODO: add user to "don't use this command for 5 minutes"
|
||||
|
||||
embed_followup(ctx,
|
||||
interaction,
|
||||
"Student ID not found!",
|
||||
"Your student ID is not found.
|
||||
It takes up to **a week** for your membership to be updated in the database since sign up.
|
||||
Try again later.").await?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// get name from result
|
||||
let result_name = &result.unwrap().full_name;
|
||||
|
||||
{
|
||||
let mut cache = state.student_cache.lock().unwrap();
|
||||
cache.insert(
|
||||
student_id.to_string(),
|
||||
result_name.to_string().to_lowercase(),
|
||||
);
|
||||
}
|
||||
|
||||
if &result_name.to_lowercase() == &modal_data.name.to_lowercase() {
|
||||
discord_member.add_role(ctx, verified_role_id).await?;
|
||||
|
||||
embed_followup(
|
||||
ctx,
|
||||
interaction,
|
||||
"Verified ✅",
|
||||
format!("You have been assigned the <@&{}> role!", verified_role_id),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
embed_followup(
|
||||
ctx,
|
||||
interaction,
|
||||
"Name mismatch ❌",
|
||||
"Your student ID is present, however the name does not match. Try again.",
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use supabase::Client;
|
|||
mod commands;
|
||||
mod events;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Data {
|
||||
pub state: AppState,
|
||||
}
|
||||
|
|
@ -17,7 +18,7 @@ type Error = Box<dyn std::error::Error + Send + Sync>;
|
|||
type Context<'a> = poise::Context<'a, Data, Error>;
|
||||
type ApplicationContext<'a> = poise::ApplicationContext<'a, Data, Error>;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AppState {
|
||||
pub supabase: Arc<Client>,
|
||||
pub student_cache: Arc<Mutex<HashMap<String, String>>>,
|
||||
|
|
@ -41,14 +42,14 @@ async fn event_handler(
|
|||
ctx: &serenity::Context,
|
||||
event: &serenity::FullEvent,
|
||||
_framework: poise::FrameworkContext<'_, Data, Error>,
|
||||
_data: &Data,
|
||||
data: &Data,
|
||||
) -> Result<(), Error> {
|
||||
match event {
|
||||
serenity::FullEvent::Ready { data_about_bot, .. } => {
|
||||
events::ready::on_ready(ctx, data_about_bot).await?;
|
||||
}
|
||||
serenity::FullEvent::InteractionCreate { interaction } => {
|
||||
events::interaction_create::on_interaction_create(ctx, interaction).await?;
|
||||
events::interaction_create::on_interaction_create(ctx, interaction, &data).await?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue