summaryrefslogtreecommitdiff
path: root/packages/gateway/src/commands/webhook.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/gateway/src/commands/webhook.ts')
-rw-r--r--packages/gateway/src/commands/webhook.ts157
1 files changed, 157 insertions, 0 deletions
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;
+ }
+};