summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFuwn <[email protected]>2025-10-24 19:25:17 -0700
committerFuwn <[email protected]>2025-10-24 19:25:20 -0700
commitd717f55600e7e3485a94f977488fa140c492ec5a (patch)
tree5530417c01b758e45683d7d50f6364824203ca14
parentfeat(gateway:listeners): Add bot message logger (diff)
downloadumabotdiscord-d717f55600e7e3485a94f977488fa140c492ec5a.tar.xz
umabotdiscord-d717f55600e7e3485a94f977488fa140c492ec5a.zip
feat(gateway:commands): Add webhook utility commands
-rw-r--r--packages/gateway/src/commands/commandHandler.ts4
-rw-r--r--packages/gateway/src/commands/deleteWebhook.ts76
-rw-r--r--packages/gateway/src/commands/webhook.ts157
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;
+ }
+};