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 crate::{ApplicationContext, Error};
|
||||||
use dotenv::dotenv;
|
|
||||||
use poise::{CreateReply, Modal};
|
use poise::{CreateReply, Modal};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serenity::all::{
|
use serenity::all::{CreateActionRow, CreateButton, CreateEmbed};
|
||||||
CreateActionRow, CreateButton, CreateEmbed, CreateEmbedFooter, GuildId, RoleId,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug)]
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
struct StudentRow {
|
pub struct StudentRow {
|
||||||
full_name: String,
|
pub full_name: String,
|
||||||
student_id: String,
|
pub student_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Modal)]
|
#[derive(Debug, Modal)]
|
||||||
#[name = "Club Verification"] // Struct name by default
|
#[name = "Club Verification"]
|
||||||
struct VerificationModal {
|
pub struct VerificationModal {
|
||||||
#[name = "Full Name"]
|
#[name = "Full Name"]
|
||||||
#[placeholder = "John Doe"]
|
#[placeholder = "John Doe"]
|
||||||
#[max_length = 50]
|
#[max_length = 50]
|
||||||
name: String,
|
pub name: String,
|
||||||
#[name = "Student ID"]
|
#[name = "Student ID"]
|
||||||
#[placeholder = "s123456789"]
|
#[placeholder = "s123456789"]
|
||||||
student_id: String,
|
pub student_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[poise::command(slash_command, subcommands("embed", "myself"))]
|
/// Embed message with verify button to verify membership
|
||||||
pub async fn verify(_: ApplicationContext<'_>) -> Result<(), Error> {
|
#[poise::command(
|
||||||
Ok(())
|
slash_command,
|
||||||
}
|
required_permissions = "MANAGE_MESSAGES | MANAGE_THREADS"
|
||||||
|
)]
|
||||||
/// Verify your DSEC club membership to obtain role
|
pub async fn verify(ctx: ApplicationContext<'_>) -> Result<(), Error> {
|
||||||
#[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> {
|
|
||||||
let reply: CreateReply = {
|
let reply: CreateReply = {
|
||||||
let embed: CreateEmbed = CreateEmbed::new()
|
let embed: CreateEmbed = CreateEmbed::new()
|
||||||
.title("Verify your DSEC membership")
|
.title("Verify your DSEC membership")
|
||||||
|
|
@ -56,193 +44,5 @@ pub async fn embed(ctx: ApplicationContext<'_>) -> Result<(), Error> {
|
||||||
|
|
||||||
ctx.send(reply).await?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,218 @@
|
||||||
use crate::Error;
|
use std::time::Duration;
|
||||||
use poise::serenity_prelude as serenity;
|
|
||||||
|
|
||||||
#[derive(Debug, poise::Modal)]
|
use crate::{
|
||||||
#[allow(dead_code)] // fields only used for Debug print
|
Data, Error,
|
||||||
struct MyModal {
|
commands::verification::{StudentRow, VerificationModal},
|
||||||
first_input: String,
|
};
|
||||||
second_input: Option<String>,
|
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(
|
pub async fn on_interaction_create(
|
||||||
_ctx: &serenity::Context,
|
ctx: &serenity::Context,
|
||||||
interaction: &serenity::Interaction,
|
interaction: &serenity::Interaction,
|
||||||
|
data: &Data,
|
||||||
) -> Result<(), Error> {
|
) -> 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() {
|
if interaction.data.custom_id == "verify" {
|
||||||
let component = some_message_component.unwrap();
|
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" {
|
return Ok(());
|
||||||
println!("Component data: {:?}", component);
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 commands;
|
||||||
mod events;
|
mod events;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Data {
|
pub struct Data {
|
||||||
pub state: AppState,
|
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 Context<'a> = poise::Context<'a, Data, Error>;
|
||||||
type ApplicationContext<'a> = poise::ApplicationContext<'a, Data, Error>;
|
type ApplicationContext<'a> = poise::ApplicationContext<'a, Data, Error>;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub supabase: Arc<Client>,
|
pub supabase: Arc<Client>,
|
||||||
pub student_cache: Arc<Mutex<HashMap<String, String>>>,
|
pub student_cache: Arc<Mutex<HashMap<String, String>>>,
|
||||||
|
|
@ -41,14 +42,14 @@ async fn event_handler(
|
||||||
ctx: &serenity::Context,
|
ctx: &serenity::Context,
|
||||||
event: &serenity::FullEvent,
|
event: &serenity::FullEvent,
|
||||||
_framework: poise::FrameworkContext<'_, Data, Error>,
|
_framework: poise::FrameworkContext<'_, Data, Error>,
|
||||||
_data: &Data,
|
data: &Data,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
match event {
|
match event {
|
||||||
serenity::FullEvent::Ready { data_about_bot, .. } => {
|
serenity::FullEvent::Ready { data_about_bot, .. } => {
|
||||||
events::ready::on_ready(ctx, data_about_bot).await?;
|
events::ready::on_ready(ctx, data_about_bot).await?;
|
||||||
}
|
}
|
||||||
serenity::FullEvent::InteractionCreate { interaction } => {
|
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