diff options
| author | Fuwn <[email protected]> | 2025-10-24 19:25:17 -0700 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2025-10-24 19:25:20 -0700 |
| commit | d717f55600e7e3485a94f977488fa140c492ec5a (patch) | |
| tree | 5530417c01b758e45683d7d50f6364824203ca14 | |
| parent | feat(gateway:listeners): Add bot message logger (diff) | |
| download | umabotdiscord-d717f55600e7e3485a94f977488fa140c492ec5a.tar.xz umabotdiscord-d717f55600e7e3485a94f977488fa140c492ec5a.zip | |
feat(gateway:commands): Add webhook utility commands
| -rw-r--r-- | packages/gateway/src/commands/commandHandler.ts | 4 | ||||
| -rw-r--r-- | packages/gateway/src/commands/deleteWebhook.ts | 76 | ||||
| -rw-r--r-- | packages/gateway/src/commands/webhook.ts | 157 |
3 files changed, 237 insertions, 0 deletions
diff --git a/packages/gateway/src/commands/commandHandler.ts b/packages/gateway/src/commands/commandHandler.ts index 5b99c16..63ff6ed 100644 --- a/packages/gateway/src/commands/commandHandler.ts +++ b/packages/gateway/src/commands/commandHandler.ts @@ -8,6 +8,8 @@ import { handleDeleteCommand } from "./delete"; import { handlePinCommand } from "./pin"; import { handleRoleCommand } from "./role"; import { handleVerbalGatesCommand } from "./verbalGates"; +import { handleWebhookCommand } from "./webhook"; +import { handleDeleteWebhookCommand } from "./deleteWebhook"; export const handleCommandHandler = (client: Client) => { client.on(Events.MessageCreate, async (message: Message) => { @@ -27,6 +29,8 @@ export const handleCommandHandler = (client: Client) => { handleDeleteCommand(message), handlePinCommand(message), handleRoleCommand(message), + handleWebhookCommand(message), + handleDeleteWebhookCommand(message), ]); }); }; diff --git a/packages/gateway/src/commands/deleteWebhook.ts b/packages/gateway/src/commands/deleteWebhook.ts new file mode 100644 index 0000000..7408c17 --- /dev/null +++ b/packages/gateway/src/commands/deleteWebhook.ts @@ -0,0 +1,76 @@ +import { Message } from "discord.js"; +import { logUnexpectedDiscordAPIError, replyWithCleanup } from "../utilities"; + +export const handleDeleteWebhookCommand = async ( + message: Message, +): Promise<boolean> => { + if (message.author.bot) return false; + + const content = message.content.trim(); + const commandMatch = content.match(/^uma!delwh\s+(\d+)\s*,\s*(.+)$/s); + + if (!commandMatch) return false; + + const [, channelId, webhookName] = commandMatch; + + if (!webhookName.trim()) { + await replyWithCleanup( + message, + "❌ You need to provide a webhook name to delete.", + ); + + return true; + } + + try { + const channel = await message.client.channels.fetch(channelId); + + if (!channel || !channel.isTextBased() || channel.isDMBased()) { + await replyWithCleanup( + message, + "❌ Channel not found or is not a text channel.", + ); + + return true; + } + + if (!("fetchWebhooks" in channel)) { + await replyWithCleanup( + message, + "❌ This channel does not support webhooks.", + ); + + return true; + } + + const webhooks = await channel.fetchWebhooks(); + const targetWebhook = webhooks.find( + (webhook) => webhook.name === webhookName.trim(), + ); + + if (!targetWebhook) { + await replyWithCleanup( + message, + `❌ No webhook found with name "${webhookName.trim()}" in <#${channelId}>.`, + ); + + return true; + } + + await targetWebhook.delete(); + await replyWithCleanup( + message, + `✅ Successfully deleted webhook "${webhookName.trim()}" from <#${channelId}>.`, + ); + + return true; + } catch (error) { + logUnexpectedDiscordAPIError(error); + await replyWithCleanup( + message, + "❌ Failed to delete webhook. Make sure I have permission to manage webhooks.", + ); + + return true; + } +}; diff --git a/packages/gateway/src/commands/webhook.ts b/packages/gateway/src/commands/webhook.ts new file mode 100644 index 0000000..a3b6b72 --- /dev/null +++ b/packages/gateway/src/commands/webhook.ts @@ -0,0 +1,157 @@ +import { Message, WebhookClient } from "discord.js"; +import { logUnexpectedDiscordAPIError, replyWithCleanup } from "../utilities"; + +const webhookCache = new Map<string, WebhookClient>(); + +const getOrCreateWebhook = async ( + message: Message, + channelId: string, + webhookName: string, + avatarUrl?: string, +): Promise<WebhookClient | null> => { + const cacheKey = `${channelId}-${webhookName}`; + + if (webhookCache.has(cacheKey)) return webhookCache.get(cacheKey)!; + + try { + const channel = await message.client.channels.fetch(channelId); + + if (!channel) { + logUnexpectedDiscordAPIError(new Error(`Channel ${channelId} not found`)); + + return null; + } + + if (!channel.isTextBased()) { + logUnexpectedDiscordAPIError( + new Error(`Channel ${channelId} is not text-based`), + ); + + return null; + } + + if (channel.isDMBased()) { + logUnexpectedDiscordAPIError( + new Error(`Channel ${channelId} is DM-based`), + ); + + return null; + } + + if (!("fetchWebhooks" in channel)) { + logUnexpectedDiscordAPIError( + new Error(`Channel ${channelId} does not support webhooks`), + ); + + return null; + } + + const webhooks = await channel.fetchWebhooks(); + let existingWebhook = webhooks.find( + (webhook) => webhook.name === webhookName, + ); + + if (existingWebhook) { + const webhookClient = new WebhookClient({ url: existingWebhook.url }); + + webhookCache.set(cacheKey, webhookClient); + + return webhookClient; + } + + if ("createWebhook" in channel) { + const webhook = await channel.createWebhook({ + name: webhookName, + avatar: avatarUrl || "https://cdn.discordapp.com/embed/avatars/0.png", + }); + const webhookClient = new WebhookClient({ url: webhook.url }); + + webhookCache.set(cacheKey, webhookClient); + + return webhookClient; + } + + logUnexpectedDiscordAPIError( + new Error(`Channel ${channelId} does not support createWebhook`), + ); + + return null; + } catch (error) { + logUnexpectedDiscordAPIError(error); + + return null; + } +}; + +export const handleWebhookCommand = async ( + message: Message, +): Promise<boolean> => { + if (message.author.bot) return false; + + const content = message.content.trim(); + const commandMatch = content.match( + /^uma!wh\s+(\d+)\s*,\s*([^\s,]+)\s*,\s*([^,]+)\s*,\s*(.+)$/s, + ); + + if (!commandMatch) return false; + + const [, channelId, avatarUrl, webhookName, messageContent] = commandMatch; + + if (!messageContent.trim()) { + await replyWithCleanup( + message, + "❌ You need to provide a message to send.", + ); + + return true; + } + + try { + const webhookClient = await getOrCreateWebhook( + message, + channelId, + webhookName.trim(), + avatarUrl.trim(), + ); + + if (!webhookClient) { + await replyWithCleanup( + message, + "❌ Failed to create or access webhook. Make sure the channel exists and I have permission to manage webhooks.", + ); + + return true; + } + + await webhookClient.send({ + content: messageContent.trim(), + username: webhookName.trim(), + avatarURL: avatarUrl.trim(), + }); + await replyWithCleanup( + message, + `✅ Message sent via webhook to <#${channelId}>.`, + ); + + return true; + } catch (error) { + logUnexpectedDiscordAPIError(error); + + let errorMessage = "❌ Failed to send message via webhook."; + + if (error instanceof Error) + if (error.message.includes("Missing Permissions")) { + errorMessage = + "❌ Missing permissions to manage webhooks in this channel."; + } else if (error.message.includes("Invalid Form Body")) { + errorMessage = + "❌ Invalid webhook data. Check your avatar URL and webhook name."; + } else if (error.message.includes("Unknown Channel")) { + errorMessage = "❌ Channel not found or inaccessible."; + } + + await replyWithCleanup(message, errorMessage); + + return true; + } +}; |