mirror of
https://github.com/dsec-hub/dsec-discord-bot.git
synced 2026-09-22 07:44:26 +00:00
added verification command
This commit is contained in:
parent
b9d0640707
commit
caa2cfb162
5 changed files with 1683 additions and 18 deletions
1432
Cargo.lock
generated
1432
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -11,3 +11,5 @@ serde_json = "1.0.145"
|
|||
serenity = "0.12"
|
||||
tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread"] }
|
||||
tracing-subscriber = "0.3.20"
|
||||
supabase-lib-rs = "0.5.3"
|
||||
serde = "1.0.228"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// this file is to let main.rs know the existence of the "commands" folder
|
||||
pub mod info;
|
||||
pub mod verification;
|
||||
pub mod weather;
|
||||
|
|
|
|||
223
src/commands/verification.rs
Normal file
223
src/commands/verification.rs
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
use crate::{Data, Error};
|
||||
use poise::{CreateReply, Modal};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serenity::all::{
|
||||
ComponentInteractionCollector, CreateActionRow, CreateButton, CreateEmbed, CreateEmbedFooter,
|
||||
GuildId, RoleId,
|
||||
};
|
||||
|
||||
type ApplicationContext<'a> = poise::ApplicationContext<'a, Data, Error>;
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
struct StudentRow {
|
||||
full_name: String,
|
||||
student_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Modal)]
|
||||
#[name = "Club Verification"] // Struct name by default
|
||||
struct VerificationModal {
|
||||
#[name = "Full Name"]
|
||||
#[placeholder = "John Doe"]
|
||||
#[max_length = 50]
|
||||
name: String,
|
||||
#[name = "Student ID"]
|
||||
#[placeholder = "s123456789"]
|
||||
student_id: String,
|
||||
}
|
||||
|
||||
#[poise::command(slash_command)]
|
||||
pub async fn embed(ctx: ApplicationContext<'_>) -> Result<(), Error> {
|
||||
let reply: CreateReply = {
|
||||
let embed: CreateEmbed = CreateEmbed::new()
|
||||
.title("Verify your DSEC membership")
|
||||
.description("Click **Verify Here** and enter your **Full name** and **Student ID** (e.g., s123456789). Your responses are private.");
|
||||
|
||||
let button: CreateButton = CreateButton::new("verify").label("Verify Here");
|
||||
|
||||
let components = vec![CreateActionRow::Buttons(vec![button])];
|
||||
|
||||
CreateReply::default()
|
||||
.ephemeral(false)
|
||||
.embed(embed)
|
||||
.components(components)
|
||||
};
|
||||
|
||||
ctx.send(reply).await?;
|
||||
|
||||
while let Some(_) = ComponentInteractionCollector::new(ctx.serenity_context())
|
||||
.filter(move |mci| mci.data.custom_id == "verify")
|
||||
.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> {
|
||||
// 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 server = GuildId::new(735865924829315072);
|
||||
|
||||
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(1441965955822649344); // 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(())
|
||||
}
|
||||
43
src/main.rs
43
src/main.rs
|
|
@ -1,17 +1,47 @@
|
|||
#![warn(clippy::str_to_string)]
|
||||
use dotenv::dotenv;
|
||||
use poise::serenity_prelude as serenity;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use supabase::prelude::*;
|
||||
mod commands;
|
||||
|
||||
struct Data {}
|
||||
pub struct Data {
|
||||
pub state: AppState,
|
||||
}
|
||||
|
||||
// Types used by all command functions
|
||||
type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||
type Context<'a> = poise::Context<'a, Data, Error>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub supabase: Arc<Client>,
|
||||
pub student_cache: Arc<Mutex<HashMap<String, String>>>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new() -> Result<Self> {
|
||||
dotenv().ok();
|
||||
let supabase_url = std::env::var("SUPABASE_URL").expect("missing SUPABASE_URL");
|
||||
let supabase_key = std::env::var("SUPABASE_KEY").expect("missing SUPABASE_KEY");
|
||||
let client = Client::new(&supabase_url, &supabase_key)?;
|
||||
|
||||
Ok(Self {
|
||||
supabase: Arc::new(client),
|
||||
student_cache: Arc::new(Mutex::new(HashMap::new())),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
dotenv().ok();
|
||||
dotenv().ok(); // load env
|
||||
|
||||
let app_state = AppState::new().expect("Failed to initialize AppState");
|
||||
|
||||
// -- discord bot start --
|
||||
let token = std::env::var("DISCORD_TOKEN").expect("missing DISCORD_TOKEN");
|
||||
let intents = serenity::GatewayIntents::non_privileged();
|
||||
|
||||
|
|
@ -24,13 +54,16 @@ async fn main() {
|
|||
commands::info::serverinfo(),
|
||||
commands::info::botinfo(),
|
||||
commands::weather::weather(),
|
||||
commands::verification::verify(),
|
||||
],
|
||||
..Default::default()
|
||||
})
|
||||
.setup(|ctx, _ready, framework| {
|
||||
Box::pin(async move {
|
||||
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
|
||||
Ok(Data {})
|
||||
Ok(Data {
|
||||
state: app_state.clone(),
|
||||
})
|
||||
})
|
||||
})
|
||||
.build();
|
||||
|
|
@ -43,4 +76,6 @@ async fn main() {
|
|||
.start()
|
||||
.await
|
||||
.expect("Client failed to start 2");
|
||||
|
||||
// -- discord bot end --
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue