mirror of
https://github.com/dsec-hub/dsec-discord-bot.git
synced 2026-09-22 07:44:26 +00:00
member info command
This commit is contained in:
parent
9f88bf05b8
commit
4595ee4cf7
4 changed files with 206 additions and 15 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
// this file is to let main.rs know the existence of the "commands" folder
|
// this file is to let main.rs know the existence of the "commands" folder
|
||||||
pub mod info;
|
pub mod info;
|
||||||
|
pub mod member_info;
|
||||||
pub mod mods_only;
|
pub mod mods_only;
|
||||||
pub mod verification;
|
pub mod verification;
|
||||||
pub mod weather;
|
pub mod weather;
|
||||||
|
|
|
||||||
110
src/commands/member_info.rs
Normal file
110
src/commands/member_info.rs
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
use crate::{ApplicationContext, Error};
|
||||||
|
use poise::CreateReply;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serenity::builder::CreateEmbed;
|
||||||
|
use supabase::Database;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
pub struct MemberRow {
|
||||||
|
pub full_name: String,
|
||||||
|
pub student_id: String,
|
||||||
|
pub campus: String,
|
||||||
|
pub membership_status: String,
|
||||||
|
pub end_date: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
struct DiscordLinkRow {
|
||||||
|
student_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn member_data(database: &Database, user_id: &String) -> Result<Option<MemberRow>, Error> {
|
||||||
|
let rows: Vec<MemberRow> = database
|
||||||
|
.from("active_members")
|
||||||
|
.select("student_id, full_name, campus, membership_status, end_date")
|
||||||
|
.eq("student_id", user_id)
|
||||||
|
.execute()
|
||||||
|
.await?;
|
||||||
|
let result = rows.into_iter().next();
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Look up the student id linked to a given discord user, if any.
|
||||||
|
async fn recorded_member(database: &Database, user_id: &String) -> Result<Option<String>, Error> {
|
||||||
|
let rows: Vec<DiscordLinkRow> = database
|
||||||
|
.from("dsec_discord_members")
|
||||||
|
.select("student_id")
|
||||||
|
.eq("discord_id", user_id)
|
||||||
|
.execute()
|
||||||
|
.await?;
|
||||||
|
Ok(rows.into_iter().next().map(|row| row.student_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve DSEC member information
|
||||||
|
#[poise::command(slash_command)]
|
||||||
|
pub async fn member_info(ctx: ApplicationContext<'_>) -> Result<(), Error> {
|
||||||
|
let user = ctx.author();
|
||||||
|
let database = ctx.data.state.supabase.database().clone();
|
||||||
|
|
||||||
|
// check if discord ID is in table
|
||||||
|
let linked_student_id = recorded_member(&database, &user.id.to_string()).await?;
|
||||||
|
|
||||||
|
// if not present
|
||||||
|
let Some(student_id) = linked_student_id else {
|
||||||
|
ctx.send(
|
||||||
|
CreateReply::default().embed(
|
||||||
|
CreateEmbed::new()
|
||||||
|
.title("Re-verification required")
|
||||||
|
.description("Verify your membership in <#1433595503190474854>"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_data_option = member_data(&database, &student_id).await?;
|
||||||
|
|
||||||
|
if user_data_option.is_none() {
|
||||||
|
ctx.send(CreateReply::default().embed(
|
||||||
|
CreateEmbed::new().title("Couldn't find info").description(
|
||||||
|
"Your membership may have expired. Contact a club executive to be sure.",
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
let user_data = user_data_option.expect("Member not found");
|
||||||
|
let member_name = user_data.full_name;
|
||||||
|
let member_campus = user_data.campus;
|
||||||
|
let membership_status = user_data.membership_status;
|
||||||
|
let expiry_date = user_data.end_date;
|
||||||
|
|
||||||
|
ctx.send(
|
||||||
|
CreateReply::default()
|
||||||
|
.embed(
|
||||||
|
CreateEmbed::new()
|
||||||
|
.title("DSEC Membership Info")
|
||||||
|
.description(format!(
|
||||||
|
"
|
||||||
|
**Name:** {member_name}
|
||||||
|
**Campus:** {member_campus}
|
||||||
|
**Membership Status:** {membership_status}
|
||||||
|
**Membership Expiry Date**: {expiry_date}
|
||||||
|
"
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.ephemeral(true),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// let student_data = state
|
||||||
|
// .supabase
|
||||||
|
// .database()
|
||||||
|
// .from("active_members")
|
||||||
|
// .select("full_name, student_id")
|
||||||
|
// .eq("student_id", &student_id)
|
||||||
|
// .execute()
|
||||||
|
// .await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,13 @@ use ::serenity::{
|
||||||
use dotenv::dotenv;
|
use dotenv::dotenv;
|
||||||
use poise::Modal as _;
|
use poise::Modal as _;
|
||||||
use poise::serenity_prelude as serenity;
|
use poise::serenity_prelude as serenity;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
pub struct DiscordMemberRow {
|
||||||
|
pub student_id: String,
|
||||||
|
pub discord_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// Read the verified role id from the environment.
|
/// Read the verified role id from the environment.
|
||||||
fn verified_role_id() -> RoleId {
|
fn verified_role_id() -> RoleId {
|
||||||
|
|
@ -75,14 +82,44 @@ async fn collect_verification_modal(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add to dsec_discord_members table (skips insert if already recorded).
|
||||||
|
async fn add_dsec_discord_table(
|
||||||
|
data: &Data,
|
||||||
|
student_id: &str,
|
||||||
|
member_id: &String,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if member_recorded(data, member_id).await? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_member = serde_json::json!({
|
||||||
|
"student_id": student_id,
|
||||||
|
"discord_id": member_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
let _: Vec<DiscordMemberRow> = data
|
||||||
|
.state
|
||||||
|
.supabase
|
||||||
|
.database()
|
||||||
|
.insert("dsec_discord_members")
|
||||||
|
.values(new_member)?
|
||||||
|
.execute()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Assign the verified role and send the success response.
|
/// Assign the verified role and send the success response.
|
||||||
async fn grant_verified_role(
|
async fn grant_verified_role(
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
|
data: &Data,
|
||||||
modal_submit: &ModalInteraction,
|
modal_submit: &ModalInteraction,
|
||||||
discord_member: &Member,
|
discord_member: &Member,
|
||||||
|
student_id: &String,
|
||||||
verified_role_id: RoleId,
|
verified_role_id: RoleId,
|
||||||
via_cache: bool,
|
via_cache: bool,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
|
add_dsec_discord_table(data, student_id, &discord_member.user.id.to_string()).await?;
|
||||||
discord_member.add_role(ctx, verified_role_id).await?;
|
discord_member.add_role(ctx, verified_role_id).await?;
|
||||||
|
|
||||||
let mut embed = CreateEmbed::new().title("Verified ✅").description(format!(
|
let mut embed = CreateEmbed::new().title("Verified ✅").description(format!(
|
||||||
|
|
@ -132,6 +169,19 @@ async fn fetch_student(data: &Data, student_id: &str) -> Result<Option<StudentRo
|
||||||
Ok(student_data.into_iter().next())
|
Ok(student_data.into_iter().next())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn member_recorded(data: &Data, user_id: &String) -> Result<bool, Error> {
|
||||||
|
let rows: Vec<serde_json::Value> = data
|
||||||
|
.state
|
||||||
|
.supabase
|
||||||
|
.database()
|
||||||
|
.from("dsec_discord_members")
|
||||||
|
.select("discord_id")
|
||||||
|
.eq("discord_id", user_id)
|
||||||
|
.execute()
|
||||||
|
.await?;
|
||||||
|
Ok(!rows.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
/// Handle a click on the "verify" button: collect the modal, then verify the
|
/// Handle a click on the "verify" button: collect the modal, then verify the
|
||||||
/// submitted student id/name against the cache and database.
|
/// submitted student id/name against the cache and database.
|
||||||
async fn handle_verify(
|
async fn handle_verify(
|
||||||
|
|
@ -154,34 +204,53 @@ async fn handle_verify(
|
||||||
};
|
};
|
||||||
|
|
||||||
let verified_role_id = verified_role_id();
|
let verified_role_id = verified_role_id();
|
||||||
let user_id = component_interaction.user.id;
|
|
||||||
let discord_member = GuildId::member(guild_id, ctx, user_id).await?;
|
|
||||||
|
|
||||||
if discord_member.roles.contains(&verified_role_id) {
|
// Fast, no-network "already verified" check using the member data that is
|
||||||
component_interaction
|
// already attached to the button interaction. Anything slower than this
|
||||||
.create_response(
|
// (a DB query, a member fetch) must NOT run before the modal is shown, or
|
||||||
ctx,
|
// Discord's ~3s acknowledgement window elapses and the click fails.
|
||||||
ephemeral_embed(CreateEmbed::new().title("Already Verified ✅").description(
|
if let Some(member) = &component_interaction.member {
|
||||||
format!("You already have the <@&{}> role!", verified_role_id),
|
if member.roles.contains(&verified_role_id) {
|
||||||
)),
|
component_interaction
|
||||||
)
|
.create_response(
|
||||||
.await?;
|
ctx,
|
||||||
return Ok(());
|
ephemeral_embed(CreateEmbed::new().title("Already Verified ✅").description(
|
||||||
|
format!("You already have the <@&{}> role!", verified_role_id),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Respond to the click with the modal immediately.
|
||||||
let Some((modal_submit, modal_data)) =
|
let Some((modal_submit, modal_data)) =
|
||||||
collect_verification_modal(ctx, component_interaction).await?
|
collect_verification_modal(ctx, component_interaction).await?
|
||||||
else {
|
else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// From here on we hold the modal-submit token, so the slower member fetch
|
||||||
|
// and database work below is no longer racing the button's ack window.
|
||||||
|
let user_id = component_interaction.user.id;
|
||||||
|
let discord_member = GuildId::member(guild_id, ctx, user_id).await?;
|
||||||
|
|
||||||
let input_student_id = modal_data.student_id.to_lowercase();
|
let input_student_id = modal_data.student_id.to_lowercase();
|
||||||
let student_id = input_student_id
|
let student_id = input_student_id
|
||||||
.strip_prefix("s")
|
.strip_prefix("s")
|
||||||
.unwrap_or(&input_student_id);
|
.unwrap_or(&input_student_id);
|
||||||
|
|
||||||
if cached_name_matches(data, student_id, &modal_data.name) {
|
if cached_name_matches(data, student_id, &modal_data.name) {
|
||||||
grant_verified_role(ctx, &modal_submit, &discord_member, verified_role_id, true).await?;
|
grant_verified_role(
|
||||||
|
ctx,
|
||||||
|
data,
|
||||||
|
&modal_submit,
|
||||||
|
&discord_member,
|
||||||
|
&student_id.to_string(),
|
||||||
|
verified_role_id,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -202,7 +271,16 @@ async fn handle_verify(
|
||||||
cache_student(data, student_id, &student.full_name);
|
cache_student(data, student_id, &student.full_name);
|
||||||
|
|
||||||
if student.full_name.to_lowercase() == modal_data.name.to_lowercase() {
|
if student.full_name.to_lowercase() == modal_data.name.to_lowercase() {
|
||||||
grant_verified_role(ctx, &modal_submit, &discord_member, verified_role_id, false).await?;
|
grant_verified_role(
|
||||||
|
ctx,
|
||||||
|
data,
|
||||||
|
&modal_submit,
|
||||||
|
&discord_member,
|
||||||
|
&student_id.to_string(),
|
||||||
|
verified_role_id,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
modal_submit
|
modal_submit
|
||||||
.create_response(
|
.create_response(
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,8 @@ async fn main() {
|
||||||
|
|
||||||
// -- discord bot start --
|
// -- discord bot start --
|
||||||
let token = std::env::var("DISCORD_TOKEN").expect("missing DISCORD_TOKEN");
|
let token = std::env::var("DISCORD_TOKEN").expect("missing DISCORD_TOKEN");
|
||||||
let intents = serenity::GatewayIntents::non_privileged() | serenity::GatewayIntents::MESSAGE_CONTENT;
|
let intents =
|
||||||
|
serenity::GatewayIntents::non_privileged() | serenity::GatewayIntents::MESSAGE_CONTENT;
|
||||||
|
|
||||||
let framework = poise::Framework::builder()
|
let framework = poise::Framework::builder()
|
||||||
.options(poise::FrameworkOptions {
|
.options(poise::FrameworkOptions {
|
||||||
|
|
@ -77,6 +78,7 @@ async fn main() {
|
||||||
commands::weather::weather(),
|
commands::weather::weather(),
|
||||||
commands::verification::verify(),
|
commands::verification::verify(),
|
||||||
commands::mods_only::embed(),
|
commands::mods_only::embed(),
|
||||||
|
commands::member_info::member_info(),
|
||||||
],
|
],
|
||||||
event_handler: |ctx, event, framework, data| {
|
event_handler: |ctx, event, framework, data| {
|
||||||
Box::pin(event_handler(ctx, event, framework, data))
|
Box::pin(event_handler(ctx, event, framework, data))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue