1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
|
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 application = await message.client.application?.fetch();
const ownerId = application?.owner?.id;
if (message.author.id !== ownerId) 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;
}
};
|