From 5833224ba9c78cb18e14e0a8d44e9dc5cfb3a7e9 Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Sat, 20 Dec 2025 20:59:44 +0100 Subject: [PATCH] feat: Implement welcome messages for new enrollments using a new webhook utility and refactor the admin webhook command to utilize it. --- src/commands/admin/webhook.ts | 33 +++---------- src/lib/config.ts | 8 +++- src/lib/webhookUtils.ts | 56 ++++++++++++++++++++++ src/modules/user/enrollment.interaction.ts | 26 ++++++++++ 4 files changed, 96 insertions(+), 27 deletions(-) create mode 100644 src/lib/webhookUtils.ts diff --git a/src/commands/admin/webhook.ts b/src/commands/admin/webhook.ts index 3c89e0e..eb8eefe 100644 --- a/src/commands/admin/webhook.ts +++ b/src/commands/admin/webhook.ts @@ -1,6 +1,7 @@ import { createCommand } from "@/lib/utils"; import { SlashCommandBuilder, PermissionFlagsBits, TextChannel, NewsChannel, VoiceChannel, MessageFlags } from "discord.js"; import { createErrorEmbed } from "@/lib/embeds"; +import { sendWebhookMessage } from "@/lib/webhookUtils"; export const webhook = createCommand({ data: new SlashCommandBuilder() @@ -36,37 +37,17 @@ export const webhook = createCommand({ return; } - let webhook; try { - webhook = await channel.createWebhook({ - name: `${interaction.client.user.username} - Proxy`, - avatar: interaction.client.user.displayAvatarURL(), - reason: `Proxy message requested by ${interaction.user.tag}` - }); - - - // Support snake_case keys for raw API compatibility - if (payload.avatar_url && !payload.avatarURL) { - payload.avatarURL = payload.avatar_url; - delete payload.avatar_url; - } - - await webhook.send(payload); - - await webhook.delete("Proxy message sent"); + await sendWebhookMessage( + channel, + payload, + interaction.client.user, + `Proxy message requested by ${interaction.user.tag}` + ); await interaction.editReply({ content: "Message sent successfully!" }); } catch (error) { console.error("Webhook error:", error); - // Attempt cleanup if webhook was created but sending failed - if (webhook) { - try { - await webhook.delete("Cleanup after failure"); - } catch (cleanupError) { - console.error("Failed to delete webhook during cleanup:", cleanupError); - } - } - await interaction.editReply({ embeds: [createErrorEmbed("Failed to send message via webhook. Ensure the bot has 'Manage Webhooks' permission and the payload is valid.", "Delivery Failed")] }); diff --git a/src/lib/config.ts b/src/lib/config.ts index 2a5dd86..0f2cee4 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -47,6 +47,8 @@ export interface GameConfigType { }; studentRole: string; visitorRole: string; + welcomeChannelId?: string; + welcomeMessage?: string; } // Initial default config state @@ -106,7 +108,9 @@ const configSchema = z.object({ }), studentRole: z.string(), - visitorRole: z.string() + visitorRole: z.string(), + welcomeChannelId: z.string().optional(), + welcomeMessage: z.string().optional() }); export function reloadConfig() { @@ -139,6 +143,8 @@ export function reloadConfig() { config.lootdrop = rawConfig.lootdrop; config.studentRole = rawConfig.studentRole; config.visitorRole = rawConfig.visitorRole; + config.welcomeChannelId = rawConfig.welcomeChannelId; + config.welcomeMessage = rawConfig.welcomeMessage; console.log("🔄 Config reloaded from disk."); } diff --git a/src/lib/webhookUtils.ts b/src/lib/webhookUtils.ts new file mode 100644 index 0000000..97bee43 --- /dev/null +++ b/src/lib/webhookUtils.ts @@ -0,0 +1,56 @@ +import { type TextBasedChannel, User, Client } from 'discord.js'; + +/** + * Sends a message to a channel using a temporary webhook (imitating the bot or custom persona). + * + * @param channel The channel to send the message to (must support webhooks). + * @param payload The message payload (string content or JSON object for embeds/options). + * @param clientUser The client user (bot) to fallback for avatar/name if not specified in payload. + * @param reason The reason for creating the webhook (for audit logs). + */ +export async function sendWebhookMessage( + channel: TextBasedChannel, + payload: any, + clientUser: User, + reason: string +): Promise { + + if (!('createWebhook' in channel)) { + throw new Error("Channel does not support webhooks."); + } + + // Normalize payload if it's just a string, wrap it in content + if (typeof payload === 'string') { + payload = { content: payload }; + } + + let webhook; + try { + webhook = await channel.createWebhook({ + name: payload.username || `${clientUser.username}`, // Use payload name or bot name + avatar: payload.avatar_url || payload.avatarURL || clientUser.displayAvatarURL(), + reason: reason + }); + + // Support snake_case keys for raw API compatibility if passed from config + if (payload.avatar_url && !payload.avatarURL) { + payload.avatarURL = payload.avatar_url; + delete payload.avatar_url; + } + + await webhook.send(payload); + + await webhook.delete(reason); + + } catch (error) { + // Attempt cleanup if webhook was created but sending failed + if (webhook) { + try { + await webhook.delete("Cleanup after failure"); + } catch (cleanupError) { + console.error("Failed to delete webhook during cleanup:", cleanupError); + } + } + throw error; + } +} diff --git a/src/modules/user/enrollment.interaction.ts b/src/modules/user/enrollment.interaction.ts index 52a8a53..a444030 100644 --- a/src/modules/user/enrollment.interaction.ts +++ b/src/modules/user/enrollment.interaction.ts @@ -3,6 +3,7 @@ import { config } from "@/lib/config"; import { createErrorEmbed } from "@/lib/embeds"; import { classService } from "@modules/class/class.service"; import { userService } from "@modules/user/user.service"; +import { sendWebhookMessage } from "@/lib/webhookUtils"; export async function handleEnrollmentInteraction(interaction: ButtonInteraction) { if (!interaction.inCachedGuild()) { @@ -84,6 +85,31 @@ export async function handleEnrollmentInteraction(interaction: ButtonInteraction flags: MessageFlags.Ephemeral }); + // 5. Send Welcome Message (if configured) + if (config.welcomeChannelId) { + const welcomeChannel = interaction.guild.channels.cache.get(config.welcomeChannelId); + if (welcomeChannel && welcomeChannel.isTextBased()) { + const rawMessage = config.welcomeMessage || "Welcome to Aurora, {user}! You have been enrolled as a **{class}**."; + + const processedMessage = rawMessage + .replace(/{user}/g, member.toString()) + .replace(/{username}/g, member.user.username) + .replace(/{class}/g, selectedClass.name) + .replace(/{guild}/g, interaction.guild.name); + + let payload; + try { + payload = JSON.parse(processedMessage); + } catch { + payload = processedMessage; + } + + // Fire and forget webhook + sendWebhookMessage(welcomeChannel, payload, interaction.client.user, "New Student Enrollment") + .catch((err: any) => console.error("Failed to send welcome message:", err)); + } + } + } catch (error) { console.error("Enrollment error:", error); await interaction.reply({