@helyx/bot 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +725 -0
- package/README.md +19 -0
- package/dist/control-server.d.ts +198 -0
- package/dist/control-server.js +663 -0
- package/dist/events.d.ts +7 -0
- package/dist/events.js +142 -0
- package/dist/guilds.d.ts +6 -0
- package/dist/guilds.js +53 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +185 -0
- package/dist/interactions.d.ts +26 -0
- package/dist/interactions.js +666 -0
- package/dist/management-module.d.ts +15 -0
- package/dist/management-module.js +865 -0
- package/dist/module-configuration.d.ts +10 -0
- package/dist/module-configuration.js +58 -0
- package/dist/module-logging.d.ts +21 -0
- package/dist/module-logging.js +97 -0
- package/dist/modules.d.ts +16 -0
- package/dist/modules.js +81 -0
- package/dist/permissions-module.d.ts +3 -0
- package/dist/permissions-module.js +497 -0
- package/package.json +62 -0
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
import { HELYX_SERVICE_NAMES } from "@helyx/sdk";
|
|
2
|
+
import { canUseCommand } from "@helyx/core";
|
|
3
|
+
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelSelectMenuBuilder, EmbedBuilder, Events, GuildMember, ApplicationIntegrationType, ChannelType, InteractionContextType, LabelBuilder, MessageFlags, MentionableSelectMenuBuilder, ModalBuilder, RoleSelectMenuBuilder, StringSelectMenuBuilder, SlashCommandBuilder, TextInputBuilder, TextInputStyle, UserSelectMenuBuilder, TextDisplayBuilder, PermissionFlagsBits, escapeMarkdown, } from "discord.js";
|
|
4
|
+
const buttonStyles = {
|
|
5
|
+
primary: ButtonStyle.Primary,
|
|
6
|
+
secondary: ButtonStyle.Secondary,
|
|
7
|
+
success: ButtonStyle.Success,
|
|
8
|
+
danger: ButtonStyle.Danger,
|
|
9
|
+
};
|
|
10
|
+
export function installInteractions(client, modules, registration, logger, services) {
|
|
11
|
+
const commands = uniqueMap(modules.flatMap((module) => (module.interactions?.commands ?? []).map((contribution) => ({
|
|
12
|
+
moduleId: module.manifest.id,
|
|
13
|
+
contribution,
|
|
14
|
+
}))), ({ contribution }) => contribution.name, "command name");
|
|
15
|
+
const globalCommands = [...commands.values()].filter(({ contribution }) => contribution.registration === "global");
|
|
16
|
+
const guildCommands = [...commands.values()].filter(({ contribution }) => contribution.registration === "guild");
|
|
17
|
+
if (guildCommands.length > 0 && !registration.internalGuildId) {
|
|
18
|
+
throw new Error("DISCORD_INTERNAL_GUILD_ID is required when guild commands are loaded");
|
|
19
|
+
}
|
|
20
|
+
const buttons = uniqueMap(modules.flatMap((module) => (module.interactions?.buttons ?? []).map((contribution) => ({
|
|
21
|
+
moduleId: module.manifest.id,
|
|
22
|
+
contribution,
|
|
23
|
+
}))), ({ contribution }) => contribution.customId, "button custom ID");
|
|
24
|
+
const modals = uniqueMap(modules.flatMap((module) => (module.interactions?.modals ?? []).map((contribution) => ({
|
|
25
|
+
moduleId: module.manifest.id,
|
|
26
|
+
contribution,
|
|
27
|
+
}))), ({ contribution }) => contribution.customId, "modal custom ID");
|
|
28
|
+
const selects = uniqueMap(modules.flatMap((module) => (module.interactions?.selects ?? []).map((contribution) => ({
|
|
29
|
+
moduleId: module.manifest.id,
|
|
30
|
+
contribution,
|
|
31
|
+
}))), ({ contribution }) => contribution.customId, "select custom ID");
|
|
32
|
+
let resolveRegistration;
|
|
33
|
+
let rejectRegistration;
|
|
34
|
+
const registrationReady = new Promise((resolve, reject) => {
|
|
35
|
+
resolveRegistration = resolve;
|
|
36
|
+
rejectRegistration = reject;
|
|
37
|
+
});
|
|
38
|
+
client.once(Events.ClientReady, (readyClient) => {
|
|
39
|
+
void registerCommands().then(resolveRegistration, (error) => {
|
|
40
|
+
logger.error({ event: "commands.registration_failed", error: safeError(error) }, "Application command registration failed");
|
|
41
|
+
rejectRegistration(error);
|
|
42
|
+
});
|
|
43
|
+
async function registerCommands() {
|
|
44
|
+
await registration.prepare?.(readyClient);
|
|
45
|
+
const globalDefinitions = globalCommands.map(toCommandDefinition);
|
|
46
|
+
await readyClient.application.commands.set(globalDefinitions);
|
|
47
|
+
if (registration.internalGuildId) {
|
|
48
|
+
const guildDefinitions = guildCommands.map(toCommandDefinition);
|
|
49
|
+
await readyClient.application.commands.set(guildDefinitions, registration.internalGuildId);
|
|
50
|
+
}
|
|
51
|
+
logger.info({
|
|
52
|
+
event: "commands.registered",
|
|
53
|
+
globalCount: globalCommands.length,
|
|
54
|
+
guildCount: guildCommands.length,
|
|
55
|
+
...(registration.internalGuildId
|
|
56
|
+
? { internalGuildId: registration.internalGuildId }
|
|
57
|
+
: {}),
|
|
58
|
+
}, "Application commands registered");
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
client.on(Events.InteractionCreate, (interaction) => {
|
|
62
|
+
void handleInteraction(interaction);
|
|
63
|
+
});
|
|
64
|
+
async function handleInteraction(interaction) {
|
|
65
|
+
try {
|
|
66
|
+
if (interaction.isChatInputCommand()) {
|
|
67
|
+
const registered = commands.get(interaction.commandName);
|
|
68
|
+
if (registered) {
|
|
69
|
+
const { contribution: command, moduleId } = registered;
|
|
70
|
+
const subcommandName = command.subcommands?.length
|
|
71
|
+
? interaction.options.getSubcommand(false)
|
|
72
|
+
: null;
|
|
73
|
+
const subcommand = subcommandName
|
|
74
|
+
? command.subcommands?.find(({ name }) => name === subcommandName)
|
|
75
|
+
: undefined;
|
|
76
|
+
const execute = resolveCommandExecutor(command, subcommandName);
|
|
77
|
+
if (!execute) {
|
|
78
|
+
logger.warn({
|
|
79
|
+
event: "interaction.stale_command",
|
|
80
|
+
commandName: command.name,
|
|
81
|
+
...(subcommandName ? { subcommandName } : {}),
|
|
82
|
+
}, "Interaction used an outdated command definition");
|
|
83
|
+
const choices = (command.subcommands ?? [])
|
|
84
|
+
.map(({ name }) => `\`/${command.name} ${name}\``)
|
|
85
|
+
.join(" or ");
|
|
86
|
+
await interaction.reply({
|
|
87
|
+
content: choices
|
|
88
|
+
? `This command was recently updated. Refresh Discord, then use ${choices}.`
|
|
89
|
+
: "This command was recently updated. Refresh Discord and try again.",
|
|
90
|
+
flags: MessageFlags.Ephemeral,
|
|
91
|
+
});
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const commandId = subcommandName
|
|
95
|
+
? `${command.name}.${subcommandName}`
|
|
96
|
+
: command.name;
|
|
97
|
+
if (await authorizeCommand(interaction, moduleId, commandId, subcommand?.rateLimit ?? command.rateLimit, services)) {
|
|
98
|
+
const context = createCommandContext(interaction, services);
|
|
99
|
+
await execute(context);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else if (interaction.isButton()) {
|
|
104
|
+
const button = buttons.get(interaction.customId);
|
|
105
|
+
if (button &&
|
|
106
|
+
(await authorizeComponent(interaction, button.moduleId, services))) {
|
|
107
|
+
await button.contribution.execute(createContext(interaction, services));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else if (interaction.isModalSubmit()) {
|
|
111
|
+
const modal = modals.get(interaction.customId);
|
|
112
|
+
if (modal &&
|
|
113
|
+
(await authorizeComponent(interaction, modal.moduleId, services))) {
|
|
114
|
+
await modal.contribution.execute(createModalContext(interaction, services));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else if (interaction.isStringSelectMenu() ||
|
|
118
|
+
interaction.isChannelSelectMenu() ||
|
|
119
|
+
interaction.isRoleSelectMenu()) {
|
|
120
|
+
const select = selects.get(interaction.customId);
|
|
121
|
+
if (select &&
|
|
122
|
+
(await authorizeComponent(interaction, select.moduleId, services))) {
|
|
123
|
+
await select.contribution.execute(createSelectContext(interaction, services));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
logger.error({ event: "interaction.failed", error: safeError(error) }, "Module interaction failed");
|
|
129
|
+
await sendFailure(interaction);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return registrationReady;
|
|
133
|
+
}
|
|
134
|
+
export function resolveCommandExecutor(command, subcommandName) {
|
|
135
|
+
if (command.subcommands?.length) {
|
|
136
|
+
if (!subcommandName)
|
|
137
|
+
return undefined;
|
|
138
|
+
const subcommand = command.subcommands.find(({ name }) => name === subcommandName);
|
|
139
|
+
return subcommand ? (context) => subcommand.execute(context) : undefined;
|
|
140
|
+
}
|
|
141
|
+
return command.execute
|
|
142
|
+
? (context) => command.execute?.(context) ?? Promise.resolve()
|
|
143
|
+
: undefined;
|
|
144
|
+
}
|
|
145
|
+
export function toCommandDefinition(registered) {
|
|
146
|
+
const command = registered.contribution;
|
|
147
|
+
if (!!command.execute === !!command.subcommands?.length) {
|
|
148
|
+
throw new Error(`Command ${command.name} must define either execute or subcommands`);
|
|
149
|
+
}
|
|
150
|
+
const contextTypes = {
|
|
151
|
+
guild: InteractionContextType.Guild,
|
|
152
|
+
bot_dm: InteractionContextType.BotDM,
|
|
153
|
+
private_channel: InteractionContextType.PrivateChannel,
|
|
154
|
+
};
|
|
155
|
+
const integrationTypes = {
|
|
156
|
+
guild_install: ApplicationIntegrationType.GuildInstall,
|
|
157
|
+
user_install: ApplicationIntegrationType.UserInstall,
|
|
158
|
+
};
|
|
159
|
+
const builder = new SlashCommandBuilder()
|
|
160
|
+
.setName(command.name)
|
|
161
|
+
.setDescription(command.description)
|
|
162
|
+
.setContexts(command.contexts.map((context) => contextTypes[context]))
|
|
163
|
+
.setIntegrationTypes(command.integrationTypes.map((integrationType) => integrationTypes[integrationType]));
|
|
164
|
+
for (const subcommand of command.subcommands ?? []) {
|
|
165
|
+
builder.addSubcommand((subcommandBuilder) => {
|
|
166
|
+
subcommandBuilder
|
|
167
|
+
.setName(subcommand.name)
|
|
168
|
+
.setDescription(subcommand.description);
|
|
169
|
+
for (const option of subcommand.options ?? []) {
|
|
170
|
+
addCommandOption(subcommandBuilder, option);
|
|
171
|
+
}
|
|
172
|
+
return subcommandBuilder;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return builder.toJSON();
|
|
176
|
+
}
|
|
177
|
+
function addCommandOption(builder, option) {
|
|
178
|
+
if (option.type === "string") {
|
|
179
|
+
builder.addStringOption((optionBuilder) => {
|
|
180
|
+
optionBuilder
|
|
181
|
+
.setName(option.name)
|
|
182
|
+
.setDescription(option.description)
|
|
183
|
+
.setRequired(option.required ?? false);
|
|
184
|
+
if (option.minLength !== undefined)
|
|
185
|
+
optionBuilder.setMinLength(option.minLength);
|
|
186
|
+
if (option.maxLength !== undefined)
|
|
187
|
+
optionBuilder.setMaxLength(option.maxLength);
|
|
188
|
+
if (option.choices)
|
|
189
|
+
optionBuilder.addChoices(...option.choices);
|
|
190
|
+
return optionBuilder;
|
|
191
|
+
});
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (option.type === "boolean") {
|
|
195
|
+
builder.addBooleanOption((optionBuilder) => optionBuilder
|
|
196
|
+
.setName(option.name)
|
|
197
|
+
.setDescription(option.description)
|
|
198
|
+
.setRequired(option.required ?? false));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (option.type === "role") {
|
|
202
|
+
builder.addRoleOption((optionBuilder) => optionBuilder
|
|
203
|
+
.setName(option.name)
|
|
204
|
+
.setDescription(option.description)
|
|
205
|
+
.setRequired(option.required ?? false));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const channelTypes = {
|
|
209
|
+
guild_text: ChannelType.GuildText,
|
|
210
|
+
guild_announcement: ChannelType.GuildAnnouncement,
|
|
211
|
+
};
|
|
212
|
+
builder.addChannelOption((optionBuilder) => {
|
|
213
|
+
optionBuilder
|
|
214
|
+
.setName(option.name)
|
|
215
|
+
.setDescription(option.description)
|
|
216
|
+
.setRequired(option.required ?? false);
|
|
217
|
+
if (option.channelTypes)
|
|
218
|
+
optionBuilder.addChannelTypes(...option.channelTypes.map((type) => channelTypes[type]));
|
|
219
|
+
return optionBuilder;
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
export function uniqueMap(items, keyOf, label) {
|
|
223
|
+
const result = new Map();
|
|
224
|
+
for (const item of items) {
|
|
225
|
+
const key = keyOf(item);
|
|
226
|
+
if (result.has(key))
|
|
227
|
+
throw new Error(`Duplicate ${label}: ${key}`);
|
|
228
|
+
result.set(key, item);
|
|
229
|
+
}
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
function createContext(interaction, services) {
|
|
233
|
+
return {
|
|
234
|
+
interactionId: interaction.id,
|
|
235
|
+
userId: interaction.user.id,
|
|
236
|
+
userDisplayName: interaction.user.globalName ?? interaction.user.username,
|
|
237
|
+
userRoleIds: interaction.member instanceof GuildMember
|
|
238
|
+
? [...interaction.member.roles.cache.keys()]
|
|
239
|
+
: (interaction.member?.roles ?? []),
|
|
240
|
+
...(interaction.guildId ? { serverId: interaction.guildId } : {}),
|
|
241
|
+
...(interaction.channelId ? { channelId: interaction.channelId } : {}),
|
|
242
|
+
...(interaction.isButton() ? { messageId: interaction.message.id } : {}),
|
|
243
|
+
services,
|
|
244
|
+
reply: async (response) => interaction.reply(toReplyOptions(response)).then(() => undefined),
|
|
245
|
+
...(interaction.isMessageComponent()
|
|
246
|
+
? {
|
|
247
|
+
updateResponse: async (response) => interaction.update(toUpdateOptions(response)).then(() => undefined),
|
|
248
|
+
}
|
|
249
|
+
: {}),
|
|
250
|
+
showModal: async (modal) => interaction.showModal(toModal(modal)).then(() => undefined),
|
|
251
|
+
sendToChannel: (channelId, response) => sendToChannel(interaction.client, channelId, response),
|
|
252
|
+
deleteChannelMessage: (channelId, messageId) => deleteChannelMessage(interaction.client, channelId, messageId),
|
|
253
|
+
addChannelMessageReaction: (channelId, messageId, emojiIdentifier) => addChannelMessageReaction(interaction.client, channelId, messageId, emojiIdentifier),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function createCommandContext(interaction, services) {
|
|
257
|
+
return {
|
|
258
|
+
...createContext(interaction, services),
|
|
259
|
+
getStringOption: (name) => interaction.options.getString(name),
|
|
260
|
+
getBooleanOption: (name) => interaction.options.getBoolean(name),
|
|
261
|
+
getRoleOption: (name) => {
|
|
262
|
+
const role = interaction.options.getRole(name);
|
|
263
|
+
return role ? { id: role.id, name: role.name } : null;
|
|
264
|
+
},
|
|
265
|
+
getChannelOption: (name) => {
|
|
266
|
+
const channel = interaction.options.getChannel(name);
|
|
267
|
+
if (!channel)
|
|
268
|
+
return null;
|
|
269
|
+
return {
|
|
270
|
+
id: channel.id,
|
|
271
|
+
name: channel.name ?? channel.id,
|
|
272
|
+
type: channel.type === ChannelType.GuildText
|
|
273
|
+
? "guild_text"
|
|
274
|
+
: channel.type === ChannelType.GuildAnnouncement
|
|
275
|
+
? "guild_announcement"
|
|
276
|
+
: "other",
|
|
277
|
+
};
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function createModalContext(interaction, services) {
|
|
282
|
+
const updateResponse = interaction.isFromMessage()
|
|
283
|
+
? async (response) => interaction.update(toUpdateOptions(response)).then(() => undefined)
|
|
284
|
+
: undefined;
|
|
285
|
+
return {
|
|
286
|
+
interactionId: interaction.id,
|
|
287
|
+
userId: interaction.user.id,
|
|
288
|
+
userDisplayName: interaction.user.globalName ?? interaction.user.username,
|
|
289
|
+
userRoleIds: interaction.member instanceof GuildMember
|
|
290
|
+
? [...interaction.member.roles.cache.keys()]
|
|
291
|
+
: (interaction.member?.roles ?? []),
|
|
292
|
+
...(interaction.guildId ? { serverId: interaction.guildId } : {}),
|
|
293
|
+
...(interaction.channelId ? { channelId: interaction.channelId } : {}),
|
|
294
|
+
services,
|
|
295
|
+
reply: async (response) => interaction.reply(toReplyOptions(response)).then(() => undefined),
|
|
296
|
+
...(updateResponse ? { updateResponse } : {}),
|
|
297
|
+
getField: (customId) => interaction.fields.getTextInputValue(customId),
|
|
298
|
+
getStringSelectValues: (customId) => interaction.fields.getStringSelectValues(customId),
|
|
299
|
+
getSelectedUserIds: (customId) => [
|
|
300
|
+
...(interaction.fields.getSelectedUsers(customId, false)?.keys() ?? []),
|
|
301
|
+
],
|
|
302
|
+
getSelectedRoleIds: (customId) => [
|
|
303
|
+
...(interaction.fields.getSelectedRoles(customId, false)?.keys() ?? []),
|
|
304
|
+
],
|
|
305
|
+
getSelectedMentionables: (customId) => {
|
|
306
|
+
const selected = interaction.fields.getSelectedMentionables(customId, false);
|
|
307
|
+
return {
|
|
308
|
+
userIds: [...(selected?.users.keys() ?? [])],
|
|
309
|
+
roleIds: [...(selected?.roles.keys() ?? [])],
|
|
310
|
+
};
|
|
311
|
+
},
|
|
312
|
+
getSelectedChannelIds: (customId) => [
|
|
313
|
+
...(interaction.fields.getSelectedChannels(customId, false)?.keys() ??
|
|
314
|
+
[]),
|
|
315
|
+
],
|
|
316
|
+
sendToChannel: (channelId, response) => sendToChannel(interaction.client, channelId, response),
|
|
317
|
+
deleteChannelMessage: (channelId, messageId) => deleteChannelMessage(interaction.client, channelId, messageId),
|
|
318
|
+
addChannelMessageReaction: (channelId, messageId, emojiIdentifier) => addChannelMessageReaction(interaction.client, channelId, messageId, emojiIdentifier),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
function createSelectContext(interaction, services) {
|
|
322
|
+
return {
|
|
323
|
+
...createContext(interaction, services),
|
|
324
|
+
selectedValues: interaction.values,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
export async function sendToChannel(client, channelId, response) {
|
|
328
|
+
const channel = await client.channels.fetch(channelId);
|
|
329
|
+
if (!channel?.isSendable())
|
|
330
|
+
throw new Error("Configured channel is not sendable");
|
|
331
|
+
const options = toReplyOptions({ ...response, ephemeral: false });
|
|
332
|
+
const message = await channel.send(options);
|
|
333
|
+
return { messageId: message.id };
|
|
334
|
+
}
|
|
335
|
+
export async function deleteChannelMessage(client, channelId, messageId) {
|
|
336
|
+
const channel = await client.channels.fetch(channelId);
|
|
337
|
+
if (!channel?.isTextBased()) {
|
|
338
|
+
throw new Error("Configured channel cannot contain messages");
|
|
339
|
+
}
|
|
340
|
+
await channel.messages.delete(messageId);
|
|
341
|
+
}
|
|
342
|
+
async function addChannelMessageReaction(client, channelId, messageId, emojiIdentifier) {
|
|
343
|
+
const channel = await client.channels.fetch(channelId);
|
|
344
|
+
if (!channel?.isTextBased()) {
|
|
345
|
+
throw new Error("Configured channel cannot contain messages");
|
|
346
|
+
}
|
|
347
|
+
const message = await channel.messages.fetch(messageId);
|
|
348
|
+
await message.react(emojiIdentifier);
|
|
349
|
+
}
|
|
350
|
+
async function authorizeComponent(interaction, moduleId, services) {
|
|
351
|
+
if (!interaction.guildId)
|
|
352
|
+
return true;
|
|
353
|
+
const installations = services.get(HELYX_SERVICE_NAMES.installations);
|
|
354
|
+
if (await installations.isModuleEnabled(interaction.guildId, moduleId)) {
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
await interaction.reply({
|
|
358
|
+
content: "This module is disabled in this server. Ask a server administrator to enable it with `/helyx` or in the Helyx dashboard.",
|
|
359
|
+
flags: MessageFlags.Ephemeral,
|
|
360
|
+
});
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
async function authorizeCommand(interaction, moduleId, commandId, rateLimit, services) {
|
|
364
|
+
if (!interaction.guildId)
|
|
365
|
+
return true;
|
|
366
|
+
if (!(await authorizeComponent(interaction, moduleId, services)))
|
|
367
|
+
return false;
|
|
368
|
+
const permissions = services.get(HELYX_SERVICE_NAMES.permissions);
|
|
369
|
+
const policy = await permissions.getEffectivePolicy(interaction.guildId, moduleId, commandId);
|
|
370
|
+
const actor = {
|
|
371
|
+
userId: interaction.user.id,
|
|
372
|
+
roleIds: interaction.member instanceof GuildMember
|
|
373
|
+
? [...interaction.member.roles.cache.keys()]
|
|
374
|
+
: (interaction.member?.roles ?? []),
|
|
375
|
+
permissions: interaction.memberPermissions
|
|
376
|
+
?.toArray()
|
|
377
|
+
.map(normalizeDiscordPermission) ?? [],
|
|
378
|
+
isServerOwner: interaction.guild?.ownerId === interaction.user.id,
|
|
379
|
+
isAdministrator: interaction.memberPermissions?.has(PermissionFlagsBits.Administrator) ??
|
|
380
|
+
false,
|
|
381
|
+
};
|
|
382
|
+
if (!canUseCommand(actor, policy)) {
|
|
383
|
+
await interaction.reply({
|
|
384
|
+
content: "You do not have permission to use this command.",
|
|
385
|
+
flags: MessageFlags.Ephemeral,
|
|
386
|
+
});
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
if (rateLimit) {
|
|
390
|
+
const limits = services.get(HELYX_SERVICE_NAMES.rateLimits);
|
|
391
|
+
const result = await limits.consume({
|
|
392
|
+
scope: rateLimit.scope,
|
|
393
|
+
subjectId: rateLimit.scope === "guild" ? interaction.guildId : interaction.user.id,
|
|
394
|
+
action: `${moduleId}:${commandId}`,
|
|
395
|
+
limit: rateLimit.limit,
|
|
396
|
+
windowSeconds: rateLimit.windowSeconds,
|
|
397
|
+
});
|
|
398
|
+
if (!result.allowed) {
|
|
399
|
+
await interaction.reply({
|
|
400
|
+
content: `This command is temporarily rate limited. Try again <t:${Math.ceil(result.resetsAt.getTime() / 1_000)}:R>.`,
|
|
401
|
+
flags: MessageFlags.Ephemeral,
|
|
402
|
+
});
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
export function normalizeDiscordPermission(permission) {
|
|
409
|
+
return permission.replaceAll(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase();
|
|
410
|
+
}
|
|
411
|
+
export function toReplyOptions(response) {
|
|
412
|
+
const allowedMentions = toAllowedUserMentions(response.allowedUserMentionIds);
|
|
413
|
+
const selectRows = toSelectRows(response);
|
|
414
|
+
if (response.componentsV2) {
|
|
415
|
+
const flags = response.ephemeral
|
|
416
|
+
? MessageFlags.IsComponentsV2 | MessageFlags.Ephemeral
|
|
417
|
+
: MessageFlags.IsComponentsV2;
|
|
418
|
+
return {
|
|
419
|
+
flags,
|
|
420
|
+
...(allowedMentions ? { allowedMentions } : {}),
|
|
421
|
+
components: [
|
|
422
|
+
{
|
|
423
|
+
type: 17,
|
|
424
|
+
...(response.componentsV2.accentColor === undefined
|
|
425
|
+
? {}
|
|
426
|
+
: { accent_color: response.componentsV2.accentColor }),
|
|
427
|
+
components: response.componentsV2.text.map((text) => ({
|
|
428
|
+
type: 10,
|
|
429
|
+
content: text.markdown === false
|
|
430
|
+
? escapeMarkdown(text.content)
|
|
431
|
+
: text.content,
|
|
432
|
+
})),
|
|
433
|
+
},
|
|
434
|
+
...selectRows,
|
|
435
|
+
...(response.buttons?.length
|
|
436
|
+
? [
|
|
437
|
+
{
|
|
438
|
+
type: 1,
|
|
439
|
+
components: response.buttons.map((button) => ({
|
|
440
|
+
type: 2,
|
|
441
|
+
custom_id: button.customId,
|
|
442
|
+
label: button.label,
|
|
443
|
+
style: buttonStyles[button.style],
|
|
444
|
+
...(button.emojiIdentifier
|
|
445
|
+
? { emoji: toButtonEmoji(button.emojiIdentifier) }
|
|
446
|
+
: {}),
|
|
447
|
+
})),
|
|
448
|
+
},
|
|
449
|
+
]
|
|
450
|
+
: []),
|
|
451
|
+
],
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
const embed = response.embed
|
|
455
|
+
? new EmbedBuilder()
|
|
456
|
+
.setTitle(response.embed.title)
|
|
457
|
+
.setDescription(response.embed.description)
|
|
458
|
+
.setColor(response.embed.color ?? 0x7c3aed)
|
|
459
|
+
: undefined;
|
|
460
|
+
if (embed && response.embed?.footer)
|
|
461
|
+
embed.setFooter({ text: response.embed.footer });
|
|
462
|
+
const components = response.buttons?.length
|
|
463
|
+
? [
|
|
464
|
+
new ActionRowBuilder().addComponents(response.buttons.map((button) => {
|
|
465
|
+
const builder = new ButtonBuilder()
|
|
466
|
+
.setCustomId(button.customId)
|
|
467
|
+
.setLabel(button.label)
|
|
468
|
+
.setStyle(buttonStyles[button.style]);
|
|
469
|
+
if (button.emojiIdentifier) {
|
|
470
|
+
builder.setEmoji(toButtonEmoji(button.emojiIdentifier));
|
|
471
|
+
}
|
|
472
|
+
return builder;
|
|
473
|
+
})),
|
|
474
|
+
]
|
|
475
|
+
: [];
|
|
476
|
+
components.push(...selectRows);
|
|
477
|
+
return {
|
|
478
|
+
...(response.ephemeral ? { flags: MessageFlags.Ephemeral } : {}),
|
|
479
|
+
...(allowedMentions ? { allowedMentions } : {}),
|
|
480
|
+
...(embed ? { embeds: [embed] } : {}),
|
|
481
|
+
components,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
function toSelectRows(response) {
|
|
485
|
+
const rows = [];
|
|
486
|
+
for (const select of response.selects ?? []) {
|
|
487
|
+
if (select.type === "discord-channel") {
|
|
488
|
+
rows.push(new ActionRowBuilder().addComponents(new ChannelSelectMenuBuilder()
|
|
489
|
+
.setCustomId(select.customId)
|
|
490
|
+
.setPlaceholder(select.placeholder)
|
|
491
|
+
.setMinValues(select.minValues ?? 1)
|
|
492
|
+
.setMaxValues(select.maxValues ?? 1)
|
|
493
|
+
.setChannelTypes(...select.channelTypes.map((type) => type === "guild_announcement"
|
|
494
|
+
? ChannelType.GuildAnnouncement
|
|
495
|
+
: ChannelType.GuildText))));
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
if (select.type === "discord-role") {
|
|
499
|
+
rows.push(new ActionRowBuilder().addComponents(new RoleSelectMenuBuilder()
|
|
500
|
+
.setCustomId(select.customId)
|
|
501
|
+
.setPlaceholder(select.placeholder)
|
|
502
|
+
.setMinValues(select.minValues ?? 1)
|
|
503
|
+
.setMaxValues(select.maxValues ?? 1)));
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
rows.push(new ActionRowBuilder().addComponents(new StringSelectMenuBuilder()
|
|
507
|
+
.setCustomId(select.customId)
|
|
508
|
+
.setPlaceholder(select.placeholder)
|
|
509
|
+
.setMinValues(select.minValues ?? 1)
|
|
510
|
+
.setMaxValues(select.maxValues ?? 1)
|
|
511
|
+
.addOptions(select.options.map((option) => ({
|
|
512
|
+
label: option.label,
|
|
513
|
+
value: option.value,
|
|
514
|
+
...(option.description
|
|
515
|
+
? { description: option.description }
|
|
516
|
+
: {}),
|
|
517
|
+
})))));
|
|
518
|
+
}
|
|
519
|
+
return rows;
|
|
520
|
+
}
|
|
521
|
+
export function toUpdateOptions(response) {
|
|
522
|
+
const options = toReplyOptions({
|
|
523
|
+
...response,
|
|
524
|
+
ephemeral: false,
|
|
525
|
+
});
|
|
526
|
+
return response.componentsV2
|
|
527
|
+
? {
|
|
528
|
+
...options,
|
|
529
|
+
// Discord requires legacy fields to be cleared when an existing
|
|
530
|
+
// message is first converted to Components V2.
|
|
531
|
+
content: null,
|
|
532
|
+
embeds: [],
|
|
533
|
+
}
|
|
534
|
+
: options;
|
|
535
|
+
}
|
|
536
|
+
function toAllowedUserMentions(userIds) {
|
|
537
|
+
if (userIds === undefined)
|
|
538
|
+
return undefined;
|
|
539
|
+
if (userIds.length > 100 ||
|
|
540
|
+
new Set(userIds).size !== userIds.length ||
|
|
541
|
+
userIds.some((userId) => !/^\d{17,20}$/u.test(userId))) {
|
|
542
|
+
throw new Error("Allowed user mentions must be unique Discord snowflakes");
|
|
543
|
+
}
|
|
544
|
+
return { parse: [], users: [...userIds] };
|
|
545
|
+
}
|
|
546
|
+
function toButtonEmoji(emojiIdentifier) {
|
|
547
|
+
const custom = /^([A-Za-z0-9_]{2,32}):(\d{17,20})$/u.exec(emojiIdentifier);
|
|
548
|
+
return custom
|
|
549
|
+
? { name: custom[1], id: custom[2] }
|
|
550
|
+
: { name: emojiIdentifier };
|
|
551
|
+
}
|
|
552
|
+
export function toModal(definition) {
|
|
553
|
+
const modal = new ModalBuilder()
|
|
554
|
+
.setCustomId(definition.customId)
|
|
555
|
+
.setTitle(definition.title);
|
|
556
|
+
for (const text of definition.text ?? []) {
|
|
557
|
+
modal.addTextDisplayComponents(new TextDisplayBuilder().setContent(text.markdown === false ? escapeMarkdown(text.content) : text.content));
|
|
558
|
+
}
|
|
559
|
+
for (const field of definition.fields) {
|
|
560
|
+
const label = new LabelBuilder().setLabel(field.label);
|
|
561
|
+
if (field.description !== undefined)
|
|
562
|
+
label.setDescription(field.description);
|
|
563
|
+
if (field.type === undefined || field.type === "text") {
|
|
564
|
+
const input = new TextInputBuilder()
|
|
565
|
+
.setCustomId(field.customId)
|
|
566
|
+
.setStyle(field.style === "paragraph"
|
|
567
|
+
? TextInputStyle.Paragraph
|
|
568
|
+
: TextInputStyle.Short)
|
|
569
|
+
.setRequired(field.required ?? false);
|
|
570
|
+
if (field.minLength !== undefined)
|
|
571
|
+
input.setMinLength(field.minLength);
|
|
572
|
+
if (field.maxLength !== undefined)
|
|
573
|
+
input.setMaxLength(field.maxLength);
|
|
574
|
+
if (field.placeholder !== undefined)
|
|
575
|
+
input.setPlaceholder(field.placeholder);
|
|
576
|
+
if (field.value !== undefined)
|
|
577
|
+
input.setValue(field.value);
|
|
578
|
+
label.setTextInputComponent(input);
|
|
579
|
+
}
|
|
580
|
+
else if (field.type === "string-select") {
|
|
581
|
+
const input = new StringSelectMenuBuilder()
|
|
582
|
+
.setCustomId(field.customId)
|
|
583
|
+
.setRequired(field.required ?? false)
|
|
584
|
+
.setMinValues(field.minValues ?? (field.required ? 1 : 0))
|
|
585
|
+
.setMaxValues(field.maxValues ?? 1)
|
|
586
|
+
.addOptions(field.options.map((option) => ({ ...option })));
|
|
587
|
+
if (field.placeholder !== undefined)
|
|
588
|
+
input.setPlaceholder(field.placeholder);
|
|
589
|
+
label.setStringSelectMenuComponent(input);
|
|
590
|
+
}
|
|
591
|
+
else if (field.type === "discord-user") {
|
|
592
|
+
const input = new UserSelectMenuBuilder()
|
|
593
|
+
.setCustomId(field.customId)
|
|
594
|
+
.setRequired(field.required ?? false)
|
|
595
|
+
.setMinValues(field.minValues ?? (field.required ? 1 : 0))
|
|
596
|
+
.setMaxValues(field.maxValues ?? 1);
|
|
597
|
+
if (field.placeholder !== undefined)
|
|
598
|
+
input.setPlaceholder(field.placeholder);
|
|
599
|
+
if (field.defaultValues?.length)
|
|
600
|
+
input.setDefaultUsers(...field.defaultValues);
|
|
601
|
+
label.setUserSelectMenuComponent(input);
|
|
602
|
+
}
|
|
603
|
+
else if (field.type === "discord-role") {
|
|
604
|
+
const input = new RoleSelectMenuBuilder()
|
|
605
|
+
.setCustomId(field.customId)
|
|
606
|
+
.setRequired(field.required ?? false)
|
|
607
|
+
.setMinValues(field.minValues ?? (field.required ? 1 : 0))
|
|
608
|
+
.setMaxValues(field.maxValues ?? 1);
|
|
609
|
+
if (field.placeholder !== undefined)
|
|
610
|
+
input.setPlaceholder(field.placeholder);
|
|
611
|
+
if (field.defaultValues?.length)
|
|
612
|
+
input.setDefaultRoles(...field.defaultValues);
|
|
613
|
+
label.setRoleSelectMenuComponent(input);
|
|
614
|
+
}
|
|
615
|
+
else if (field.type === "discord-mentionable") {
|
|
616
|
+
const input = new MentionableSelectMenuBuilder()
|
|
617
|
+
.setCustomId(field.customId)
|
|
618
|
+
.setRequired(field.required ?? false)
|
|
619
|
+
.setMinValues(field.minValues ?? (field.required ? 1 : 0))
|
|
620
|
+
.setMaxValues(field.maxValues ?? 1);
|
|
621
|
+
if (field.placeholder !== undefined)
|
|
622
|
+
input.setPlaceholder(field.placeholder);
|
|
623
|
+
if (field.defaultValues?.length) {
|
|
624
|
+
input.addDefaultUsers(...field.defaultValues
|
|
625
|
+
.filter((value) => value.type === "user")
|
|
626
|
+
.map((value) => value.id));
|
|
627
|
+
input.addDefaultRoles(...field.defaultValues
|
|
628
|
+
.filter((value) => value.type === "role")
|
|
629
|
+
.map((value) => value.id));
|
|
630
|
+
}
|
|
631
|
+
label.setMentionableSelectMenuComponent(input);
|
|
632
|
+
}
|
|
633
|
+
else if (field.type === "discord-channel") {
|
|
634
|
+
const input = new ChannelSelectMenuBuilder()
|
|
635
|
+
.setCustomId(field.customId)
|
|
636
|
+
.setRequired(field.required ?? false)
|
|
637
|
+
.setMinValues(field.minValues ?? (field.required ? 1 : 0))
|
|
638
|
+
.setMaxValues(field.maxValues ?? 1)
|
|
639
|
+
.setChannelTypes(...field.channelTypes.map((type) => type === "guild_announcement"
|
|
640
|
+
? ChannelType.GuildAnnouncement
|
|
641
|
+
: ChannelType.GuildText));
|
|
642
|
+
if (field.placeholder !== undefined)
|
|
643
|
+
input.setPlaceholder(field.placeholder);
|
|
644
|
+
if (field.defaultValues?.length)
|
|
645
|
+
input.setDefaultChannels(...field.defaultValues);
|
|
646
|
+
label.setChannelSelectMenuComponent(input);
|
|
647
|
+
}
|
|
648
|
+
modal.addLabelComponents(label);
|
|
649
|
+
}
|
|
650
|
+
return modal;
|
|
651
|
+
}
|
|
652
|
+
async function sendFailure(interaction) {
|
|
653
|
+
if (!interaction.isRepliable() || interaction.replied || interaction.deferred)
|
|
654
|
+
return;
|
|
655
|
+
await interaction
|
|
656
|
+
.reply({
|
|
657
|
+
content: "Something went wrong while handling that interaction.",
|
|
658
|
+
flags: MessageFlags.Ephemeral,
|
|
659
|
+
})
|
|
660
|
+
.catch(() => undefined);
|
|
661
|
+
}
|
|
662
|
+
function safeError(error) {
|
|
663
|
+
const value = error instanceof Error ? error : new Error(String(error));
|
|
664
|
+
return { name: value.name, message: value.message };
|
|
665
|
+
}
|
|
666
|
+
//# sourceMappingURL=interactions.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ModuleDefinition } from "@helyx/sdk";
|
|
2
|
+
export interface ManagementGuildResources {
|
|
3
|
+
channels: readonly {
|
|
4
|
+
id: string;
|
|
5
|
+
type: "guild_text" | "guild_announcement";
|
|
6
|
+
}[];
|
|
7
|
+
roles: readonly {
|
|
8
|
+
id: string;
|
|
9
|
+
}[];
|
|
10
|
+
}
|
|
11
|
+
export interface ManagementResourceDirectory {
|
|
12
|
+
getGuildResources(guildId: string): Promise<ManagementGuildResources>;
|
|
13
|
+
}
|
|
14
|
+
export declare function createManagementModule(configurableModules: readonly ModuleDefinition[], resources: ManagementResourceDirectory): ModuleDefinition;
|
|
15
|
+
//# sourceMappingURL=management-module.d.ts.map
|