From 528a66a7ef01740a7b234ed3dbb1b42b63797dab Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Thu, 18 Dec 2025 20:09:19 +0100 Subject: [PATCH] feat: Implement user enrollment interaction to assign a random class role and add new role configurations. --- src/db/schema.ts | 1 + src/events/interactionCreate.ts | 4 + src/lib/config.ts | 9 ++- src/modules/class/class.service.ts | 17 ++++ src/modules/user/enrollment.interaction.ts | 94 ++++++++++++++++++++++ 5 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 src/modules/user/enrollment.interaction.ts diff --git a/src/db/schema.ts b/src/db/schema.ts index da53f55..d147f0f 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -21,6 +21,7 @@ export const classes = pgTable('classes', { id: bigint('id', { mode: 'bigint' }).primaryKey(), name: varchar('name', { length: 255 }).unique().notNull(), balance: bigint('balance', { mode: 'bigint' }).default(0n), + roleId: varchar('role_id', { length: 255 }), }); // 2. Users diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 7750365..a361c22 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -25,6 +25,10 @@ const event: Event = { await import("@/modules/admin/item_wizard").then(m => m.handleItemWizardInteraction(interaction)); return; } + if (interaction.customId === "enrollment" && interaction.isButton()) { + await import("@/modules/user/enrollment.interaction").then(m => m.handleEnrollmentInteraction(interaction)); + return; + } } if (interaction.isAutocomplete()) { diff --git a/src/lib/config.ts b/src/lib/config.ts index 4a526dc..2a5dd86 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -45,6 +45,8 @@ export interface GameConfigType { currency: string; } }; + studentRole: string; + visitorRole: string; } // Initial default config state @@ -101,7 +103,10 @@ const configSchema = z.object({ max: z.number(), currency: z.string(), }) - }) + + }), + studentRole: z.string(), + visitorRole: z.string() }); export function reloadConfig() { @@ -132,6 +137,8 @@ export function reloadConfig() { }; config.commands = rawConfig.commands || {}; config.lootdrop = rawConfig.lootdrop; + config.studentRole = rawConfig.studentRole; + config.visitorRole = rawConfig.visitorRole; console.log("🔄 Config reloaded from disk."); } diff --git a/src/modules/class/class.service.ts b/src/modules/class/class.service.ts index 062881d..dc5cff4 100644 --- a/src/modules/class/class.service.ts +++ b/src/modules/class/class.service.ts @@ -64,5 +64,22 @@ export const classService = { return updatedClass; }; return tx ? await execute(tx) : await DrizzleClient.transaction(execute); + }, + + createClass: async (data: typeof classes.$inferInsert, tx?: any) => { + const execute = async (txFn: any) => { + const [newClass] = await txFn.insert(classes) + .values(data) + .returning(); + return newClass; + }; + return tx ? await execute(tx) : await DrizzleClient.transaction(execute); + }, + + deleteClass: async (id: bigint, tx?: any) => { + const execute = async (txFn: any) => { + await txFn.delete(classes).where(eq(classes.id, id)); + }; + return tx ? await execute(tx) : await DrizzleClient.transaction(execute); } }; diff --git a/src/modules/user/enrollment.interaction.ts b/src/modules/user/enrollment.interaction.ts new file mode 100644 index 0000000..e96692e --- /dev/null +++ b/src/modules/user/enrollment.interaction.ts @@ -0,0 +1,94 @@ +import { ButtonInteraction, MessageFlags } from "discord.js"; +import { config } from "@/lib/config"; +import { createErrorEmbed } from "@/lib/embeds"; +import { classService } from "@modules/class/class.service"; +import { userService } from "@modules/user/user.service"; + +export async function handleEnrollmentInteraction(interaction: ButtonInteraction) { + if (!interaction.inCachedGuild()) { + await interaction.reply({ content: "This action can only be performed in a server.", flags: MessageFlags.Ephemeral }); + return; + } + + const { studentRole, visitorRole } = config; + + if (!studentRole || !visitorRole) { + await interaction.reply({ + embeds: [createErrorEmbed("No student or visitor role configured for enrollment.", "Configuration Error")], + flags: MessageFlags.Ephemeral + }); + return; + } + + try { + // 1. Ensure user exists in DB and check current enrollment status + const user = await userService.getOrCreateUser(interaction.user.id, interaction.user.username); + + // Check DB enrollment + if (user.class) { + await interaction.reply({ + embeds: [createErrorEmbed("You are already enrolled in a class.", "Enrollment Failed")], + flags: MessageFlags.Ephemeral + }); + return; + } + + const member = interaction.member; + + // Check Discord role enrollment (Double safety) + if (member.roles.cache.has(studentRole)) { + await interaction.reply({ + embeds: [createErrorEmbed("You already have the student role.", "Enrollment Failed")], + flags: MessageFlags.Ephemeral + }); + return; + } + + // 2. Get available classes + const allClasses = await classService.getAllClasses(); + const validClasses = allClasses.filter(c => c.roleId); + + if (validClasses.length === 0) { + await interaction.reply({ + embeds: [createErrorEmbed("No classes with specified roles found in database.", "Configuration Error")], + flags: MessageFlags.Ephemeral + }); + return; + } + + // 3. Pick random class + const selectedClass = validClasses[Math.floor(Math.random() * validClasses.length)]!; + const classRoleId = selectedClass.roleId!; + + // Check if the role exists in the guild + const classRole = interaction.guild.roles.cache.get(classRoleId); + if (!classRole) { + await interaction.reply({ + embeds: [createErrorEmbed(`The configured role ID \`${classRoleId}\` for class **${selectedClass.name}** does not exist in this server.`, "Configuration Error")], + flags: MessageFlags.Ephemeral + }); + return; + } + + // 4. Perform Enrollment Actions + + await member.roles.remove(visitorRole); + await member.roles.add(studentRole); + await member.roles.add(classRole); + + // Persist to DB + await classService.assignClass(user.id.toString(), selectedClass.id); + + await interaction.reply({ + content: `🎉 You have been successfully enrolled! You are now a member of **${selectedClass.name}** and received the **${classRole.name}** role.`, + flags: MessageFlags.Ephemeral + }); + + } catch (error) { + console.error("Enrollment error:", error); + await interaction.reply({ + embeds: [createErrorEmbed("An unexpected error occurred during enrollment. Please contact an administrator.", "System Error")], + flags: MessageFlags.Ephemeral + }); + } +}