@helyx/bot 0.4.36 → 0.4.38
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/CHANGELOG.md +20 -0
- package/README.md +17 -2
- package/dist/cloud-management-module.js +275 -206
- package/dist/command-registration-policy.d.ts +8 -0
- package/dist/command-registration-policy.js +39 -0
- package/dist/command-sync.d.ts +5 -1
- package/dist/command-sync.js +67 -3
- package/dist/control-server.d.ts +15 -3
- package/dist/control-server.js +34 -48
- package/dist/dashboard-commands.js +5 -1
- package/dist/developer-operations-module.d.ts +2 -0
- package/dist/developer-operations-module.js +65 -4
- package/dist/discord-guild-health.d.ts +2 -2
- package/dist/discord-rest.d.ts +5 -0
- package/dist/discord-rest.js +9 -0
- package/dist/events.js +4 -2
- package/dist/first-time-command-setup.d.ts +25 -0
- package/dist/first-time-command-setup.js +22 -0
- package/dist/guild-command-control-routes.d.ts +5 -0
- package/dist/guild-command-control-routes.js +191 -0
- package/dist/guild-command-discord-client.d.ts +21 -0
- package/dist/guild-command-discord-client.js +50 -0
- package/dist/guild-command-projection.d.ts +46 -0
- package/dist/guild-command-projection.js +254 -0
- package/dist/guild-command-read-service.d.ts +201 -0
- package/dist/guild-command-read-service.js +272 -0
- package/dist/guild-command-release.d.ts +19 -0
- package/dist/guild-command-release.js +47 -0
- package/dist/guild-command-worker.d.ts +49 -0
- package/dist/guild-command-worker.js +443 -0
- package/dist/guilds.d.ts +1 -1
- package/dist/guilds.js +41 -26
- package/dist/index.js +75 -4
- package/dist/interactions.d.ts +15 -4
- package/dist/interactions.js +285 -70
- package/dist/management-module.js +117 -41
- package/dist/module-logging.d.ts +1 -0
- package/dist/module-logging.js +5 -5
- package/dist/module-status-control.d.ts +19 -0
- package/dist/module-status-control.js +69 -0
- package/dist/modules.d.ts +2 -0
- package/dist/modules.js +21 -2
- package/dist/nexus-commands.d.ts +1 -12
- package/dist/nexus-commands.js +1 -99
- package/dist/oauth-module.js +134 -88
- package/dist/permissions-module.js +112 -39
- package/dist/privacy-module.d.ts +2 -0
- package/dist/privacy-module.js +60 -0
- package/dist/scheduled-task-runner.js +36 -1
- package/dist/shard-contracts.d.ts +24 -0
- package/dist/shard-contracts.js +20 -0
- package/dist/shard-router.d.ts +1 -0
- package/dist/shard-router.js +7 -0
- package/dist/shard-supervisor.d.ts +2 -1
- package/dist/shard-supervisor.js +94 -16
- package/dist/shard-worker.d.ts +6 -1
- package/dist/shard-worker.js +9 -1
- package/dist/shared-commands.d.ts +13 -0
- package/dist/shared-commands.js +163 -0
- package/dist/support-module.js +51 -51
- package/dist/trivia-data-provider.d.ts +26 -0
- package/dist/trivia-data-provider.js +51 -0
- package/package.json +5 -4
- package/dist/oauth-module.test.d.ts +0 -2
- package/dist/oauth-module.test.js +0 -172
package/dist/interactions.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { lookup as lookupDns } from "node:dns/promises";
|
|
1
2
|
import { HELYX_SERVICE_NAMES } from "@helyx/sdk";
|
|
2
3
|
import { canUseCommand } from "@helyx/core";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
4
|
+
import { commandRegistrationPolicy, contributionRegistrationPolicy, } from "./command-registration-policy.js";
|
|
5
|
+
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelSelectMenuBuilder, EmbedBuilder, Events, GuildMember, ApplicationIntegrationType, ChannelType, InteractionContextType, LabelBuilder, MessageFlags, MentionableSelectMenuBuilder, ModalBuilder, RoleSelectMenuBuilder, Routes, StringSelectMenuBuilder, SlashCommandBuilder, TextInputBuilder, TextInputStyle, UserSelectMenuBuilder, TextDisplayBuilder, PermissionFlagsBits, escapeMarkdown, } from "discord.js";
|
|
6
|
+
import { collectCommandRegistrations, } from "./shared-commands.js";
|
|
5
7
|
const buttonStyles = {
|
|
6
8
|
primary: ButtonStyle.Primary,
|
|
7
9
|
secondary: ButtonStyle.Secondary,
|
|
@@ -10,6 +12,8 @@ const buttonStyles = {
|
|
|
10
12
|
link: ButtonStyle.Link,
|
|
11
13
|
};
|
|
12
14
|
export const GLOBAL_MODULE_UNAVAILABLE_MESSAGE = "This module is temporarily unavailable for all hosted servers.";
|
|
15
|
+
export const AUTOCOMPLETE_RESPONSE_DEADLINE_MS = 2_000;
|
|
16
|
+
export const DISCORD_DNS_REFRESH_INTERVAL_MS = 30_000;
|
|
13
17
|
async function moduleIsGloballyAvailable(services, moduleId) {
|
|
14
18
|
if (typeof services.has !== "function" ||
|
|
15
19
|
!services.has(HELYX_SERVICE_NAMES.moduleAvailability))
|
|
@@ -21,8 +25,10 @@ async function moduleIsGloballyAvailable(services, moduleId) {
|
|
|
21
25
|
}
|
|
22
26
|
export function installInteractions(client, modules, registration, logger, services) {
|
|
23
27
|
const commands = collectCommandRegistrations(modules);
|
|
24
|
-
const
|
|
25
|
-
const
|
|
28
|
+
const modulesById = new Map(modules.map((module) => [module.manifest.id, module]));
|
|
29
|
+
const allowLegacyCustomerGlobals = (registration.globalCommandMode ?? "legacy") === "legacy";
|
|
30
|
+
const globalCommands = [...commands.values()].filter(({ moduleId, contribution }) => isApplicationGlobalPolicy(contributionRegistrationPolicy(requiredCommandModule(modulesById, moduleId), contribution, { allowLegacyCustomerGlobals })));
|
|
31
|
+
const guildCommands = [...commands.values()].filter(({ moduleId, contribution }) => contributionRegistrationPolicy(requiredCommandModule(modulesById, moduleId), contribution, { allowLegacyCustomerGlobals }) === "internal-guild");
|
|
26
32
|
if (guildCommands.length > 0 && !registration.internalGuildId) {
|
|
27
33
|
throw new Error("DISCORD_INTERNAL_GUILD_ID is required when guild commands are loaded");
|
|
28
34
|
}
|
|
@@ -54,6 +60,8 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
54
60
|
rejectRegistration(error);
|
|
55
61
|
});
|
|
56
62
|
async function registerCommands() {
|
|
63
|
+
await warmDiscordRestConnection(readyClient, logger);
|
|
64
|
+
startDiscordDnsRefresh(logger);
|
|
57
65
|
await registration.prepare?.(readyClient);
|
|
58
66
|
if (registration.synchronise === false) {
|
|
59
67
|
logger.info({ event: "commands.registration_delegated" }, "Application command registration is owned by the supervisor");
|
|
@@ -61,7 +69,8 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
61
69
|
}
|
|
62
70
|
const globalDefinitions = globalCommands.map(toCommandDefinition);
|
|
63
71
|
await readyClient.application.commands.set(globalDefinitions);
|
|
64
|
-
if (registration.internalGuildId
|
|
72
|
+
if (registration.internalGuildId &&
|
|
73
|
+
registration.synchroniseInternalGuild !== false) {
|
|
65
74
|
const guildDefinitions = guildCommands.map(toCommandDefinition);
|
|
66
75
|
await readyClient.application.commands.set(guildDefinitions, registration.internalGuildId);
|
|
67
76
|
}
|
|
@@ -69,6 +78,9 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
69
78
|
event: "commands.registered",
|
|
70
79
|
globalCount: globalCommands.length,
|
|
71
80
|
guildCount: guildCommands.length,
|
|
81
|
+
internalGuildSynchronisation: registration.synchroniseInternalGuild === false
|
|
82
|
+
? "delegated"
|
|
83
|
+
: "startup",
|
|
72
84
|
...(registration.internalGuildId
|
|
73
85
|
? { internalGuildId: registration.internalGuildId }
|
|
74
86
|
: {}),
|
|
@@ -81,25 +93,48 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
81
93
|
async function handleInteraction(interaction) {
|
|
82
94
|
try {
|
|
83
95
|
if (interaction.isAutocomplete()) {
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
const response = createAutocompleteResponseController(interaction, logger);
|
|
97
|
+
try {
|
|
98
|
+
const registered = commands.get(interaction.commandName);
|
|
99
|
+
const groupName = registered?.contribution.subcommandGroups?.length
|
|
100
|
+
? interaction.options.getSubcommandGroup(false)
|
|
101
|
+
: null;
|
|
102
|
+
const subcommandName = hasNestedCommands(registered?.contribution)
|
|
103
|
+
? interaction.options.getSubcommand(false)
|
|
104
|
+
: null;
|
|
105
|
+
const subcommand = resolveCommandSubcommand(registered?.contribution, subcommandName, groupName);
|
|
106
|
+
const owner = resolveCommandOwner(registered, subcommandName, groupName);
|
|
107
|
+
const ownerModuleId = owner?.moduleId ?? registered?.moduleId;
|
|
108
|
+
if (!registered ||
|
|
109
|
+
!subcommand?.autocomplete ||
|
|
110
|
+
!ownerModuleId ||
|
|
111
|
+
!(await authorizeAutocomplete(interaction, ownerModuleId, subcommand.permissionId ??
|
|
112
|
+
[registered.contribution.name, groupName, subcommand.name]
|
|
113
|
+
.filter(Boolean)
|
|
114
|
+
.join("."), services, registration.selfAuthorizingGuildModuleIds?.has(ownerModuleId) ??
|
|
115
|
+
false, subcommand.authorization === "resource-scoped", isInstallationIndependentModule(modulesById, ownerModuleId), subcommand.authorization))) {
|
|
116
|
+
await response.respond([]);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
await subcommand.autocomplete(createAutocompleteContext(interaction, services, response.respond));
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
logger.error({
|
|
123
|
+
event: "interaction.failed",
|
|
124
|
+
...interactionLogContext(interaction),
|
|
125
|
+
error: safeError(error),
|
|
126
|
+
}, "Module autocomplete interaction failed");
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
await response.ensureResponded().catch((error) => {
|
|
130
|
+
logger.warn({
|
|
131
|
+
event: "interaction.autocomplete_response_failed",
|
|
132
|
+
...interactionLogContext(interaction),
|
|
133
|
+
error: safeError(error),
|
|
134
|
+
}, "Autocomplete response failed");
|
|
135
|
+
});
|
|
101
136
|
}
|
|
102
|
-
|
|
137
|
+
return;
|
|
103
138
|
}
|
|
104
139
|
else if (interaction.isChatInputCommand()) {
|
|
105
140
|
const registered = commands.get(interaction.commandName);
|
|
@@ -148,12 +183,21 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
148
183
|
const permissionId = subcommand?.permissionId ?? command.permissionId ?? commandId;
|
|
149
184
|
const owner = resolveCommandOwner(registered, subcommandName, groupName);
|
|
150
185
|
const ownerModuleId = owner?.moduleId ?? moduleId;
|
|
186
|
+
const installationIndependentCommand = isInstallationIndependentModule(modulesById, ownerModuleId);
|
|
151
187
|
const acknowledgement = subcommand?.acknowledgement ?? command.acknowledgement;
|
|
188
|
+
const authorization = subcommand?.authorization ?? command.authorization;
|
|
189
|
+
const rateLimit = subcommand?.rateLimit ?? command.rateLimit;
|
|
190
|
+
if (installationIndependentCommand &&
|
|
191
|
+
authorization === "public" &&
|
|
192
|
+
!rateLimit) {
|
|
193
|
+
await execute(createCommandContext(interaction, services));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
152
196
|
if (acknowledgement === "deferred-ephemeral") {
|
|
153
197
|
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
|
154
198
|
}
|
|
155
|
-
if (await authorizeCommand(interaction, ownerModuleId, permissionId,
|
|
156
|
-
false, subcommand?.authorization === "resource-scoped", owner?.moduleName)) {
|
|
199
|
+
if (await authorizeCommand(interaction, ownerModuleId, permissionId, rateLimit, services, registration.selfAuthorizingGuildModuleIds?.has(ownerModuleId) ??
|
|
200
|
+
false, subcommand?.authorization === "resource-scoped", owner?.moduleName, installationIndependentCommand, authorization)) {
|
|
157
201
|
const context = createCommandContext(interaction, services);
|
|
158
202
|
await execute(context);
|
|
159
203
|
}
|
|
@@ -162,6 +206,10 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
162
206
|
else if (interaction.isButton()) {
|
|
163
207
|
const button = buttons.get(interaction.customId) ??
|
|
164
208
|
resolveComponentPrefix(buttonPrefixes, interaction.customId);
|
|
209
|
+
if (!button) {
|
|
210
|
+
await interaction.reply(staleComponentResponse());
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
165
213
|
const context = button
|
|
166
214
|
? createContext(interaction, services)
|
|
167
215
|
: undefined;
|
|
@@ -176,15 +224,29 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
176
224
|
}
|
|
177
225
|
if (button &&
|
|
178
226
|
context &&
|
|
179
|
-
|
|
227
|
+
button.contribution.authorization === "public") {
|
|
228
|
+
await button.contribution.execute(context);
|
|
229
|
+
}
|
|
230
|
+
else if (button &&
|
|
231
|
+
context &&
|
|
232
|
+
(await authorizeComponent(interaction, button.moduleId, services, undefined, isInstallationIndependentModule(modulesById, button.moduleId)))) {
|
|
180
233
|
await button.contribution.execute(context);
|
|
181
234
|
}
|
|
182
235
|
}
|
|
183
236
|
else if (interaction.isModalSubmit()) {
|
|
184
237
|
const modal = modals.get(interaction.customId) ??
|
|
185
238
|
resolveComponentPrefix(modalPrefixes, interaction.customId);
|
|
239
|
+
if (!modal) {
|
|
240
|
+
await interaction.reply(staleComponentResponse());
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (modal?.contribution.acknowledgement === "deferred-ephemeral" &&
|
|
244
|
+
!interaction.deferred &&
|
|
245
|
+
!interaction.replied) {
|
|
246
|
+
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
|
247
|
+
}
|
|
186
248
|
if (modal &&
|
|
187
|
-
(await authorizeComponent(interaction, modal.moduleId, services))) {
|
|
249
|
+
(await authorizeComponent(interaction, modal.moduleId, services, undefined, isInstallationIndependentModule(modulesById, modal.moduleId)))) {
|
|
188
250
|
await modal.contribution.execute(createModalContext(interaction, services));
|
|
189
251
|
}
|
|
190
252
|
}
|
|
@@ -192,6 +254,10 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
192
254
|
interaction.isChannelSelectMenu() ||
|
|
193
255
|
interaction.isRoleSelectMenu()) {
|
|
194
256
|
const select = selects.get(interaction.customId);
|
|
257
|
+
if (!select) {
|
|
258
|
+
await interaction.reply(staleComponentResponse());
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
195
261
|
const context = select
|
|
196
262
|
? createSelectContext(interaction, services)
|
|
197
263
|
: undefined;
|
|
@@ -199,9 +265,13 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
199
265
|
select?.contribution.acknowledgement === "deferred-ephemeral") {
|
|
200
266
|
await context.deferReply?.(true);
|
|
201
267
|
}
|
|
268
|
+
if (select &&
|
|
269
|
+
select.contribution.acknowledgement === "deferred-update") {
|
|
270
|
+
await interaction.deferUpdate();
|
|
271
|
+
}
|
|
202
272
|
if (select &&
|
|
203
273
|
context &&
|
|
204
|
-
(await authorizeComponent(interaction, select.moduleId, services))) {
|
|
274
|
+
(await authorizeComponent(interaction, select.moduleId, services, undefined, isInstallationIndependentModule(modulesById, select.moduleId)))) {
|
|
205
275
|
await select.contribution.execute(context);
|
|
206
276
|
}
|
|
207
277
|
}
|
|
@@ -217,18 +287,69 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
217
287
|
}
|
|
218
288
|
return registrationReady;
|
|
219
289
|
}
|
|
220
|
-
export function
|
|
290
|
+
export async function warmDiscordRestConnection(client, logger) {
|
|
291
|
+
const rest = client.rest;
|
|
292
|
+
if (!rest)
|
|
293
|
+
return;
|
|
294
|
+
try {
|
|
295
|
+
await rest.get(Routes.gateway());
|
|
296
|
+
logger.info({ event: "discord.rest_connection_warmed" }, "Discord REST connection warmed");
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
logger.warn({
|
|
300
|
+
event: "discord.rest_connection_warm_failed",
|
|
301
|
+
error: safeError(error),
|
|
302
|
+
}, "Discord REST connection warm-up failed");
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
export function startDiscordDnsRefresh(logger, lookup = lookupDns, intervalMilliseconds = DISCORD_DNS_REFRESH_INTERVAL_MS) {
|
|
306
|
+
let unavailable = false;
|
|
307
|
+
const refresh = () => {
|
|
308
|
+
void lookup("discord.com").then(() => {
|
|
309
|
+
if (unavailable) {
|
|
310
|
+
logger.info({ event: "discord.dns_refresh_recovered" }, "Discord DNS refresh recovered");
|
|
311
|
+
}
|
|
312
|
+
unavailable = false;
|
|
313
|
+
}, (error) => {
|
|
314
|
+
if (!unavailable) {
|
|
315
|
+
logger.warn({
|
|
316
|
+
event: "discord.dns_refresh_failed",
|
|
317
|
+
error: safeError(error),
|
|
318
|
+
}, "Discord DNS refresh failed");
|
|
319
|
+
}
|
|
320
|
+
unavailable = true;
|
|
321
|
+
});
|
|
322
|
+
};
|
|
323
|
+
const timer = setInterval(refresh, intervalMilliseconds);
|
|
324
|
+
timer.unref();
|
|
325
|
+
return timer;
|
|
326
|
+
}
|
|
327
|
+
export function buildApplicationCommandDefinitions(modules, internalGuildId, globalCommandMode = "legacy") {
|
|
221
328
|
const commands = collectCommandRegistrations(modules);
|
|
329
|
+
const modulesById = new Map(modules.map((module) => [module.manifest.id, module]));
|
|
330
|
+
const allowLegacyCustomerGlobals = globalCommandMode === "legacy";
|
|
222
331
|
const global = [...commands.values()]
|
|
223
|
-
.filter(({ contribution }) => contribution
|
|
332
|
+
.filter(({ moduleId, contribution }) => isApplicationGlobalPolicy(contributionRegistrationPolicy(requiredCommandModule(modulesById, moduleId), contribution, { allowLegacyCustomerGlobals })))
|
|
224
333
|
.map(toCommandDefinition);
|
|
225
334
|
const guild = [...commands.values()]
|
|
226
|
-
.filter(({ contribution }) => contribution
|
|
335
|
+
.filter(({ moduleId, contribution }) => contributionRegistrationPolicy(requiredCommandModule(modulesById, moduleId), contribution, { allowLegacyCustomerGlobals }) === "internal-guild")
|
|
227
336
|
.map(toCommandDefinition);
|
|
228
337
|
if (guild.length > 0 && !internalGuildId)
|
|
229
338
|
throw new Error("DISCORD_INTERNAL_GUILD_ID is required when guild commands are loaded");
|
|
230
339
|
return { global, guild };
|
|
231
340
|
}
|
|
341
|
+
function isApplicationGlobalPolicy(policy) {
|
|
342
|
+
return policy === "platform-global" || policy === "deployment";
|
|
343
|
+
}
|
|
344
|
+
function requiredCommandModule(modulesById, moduleId) {
|
|
345
|
+
const module = modulesById.get(moduleId);
|
|
346
|
+
if (!module)
|
|
347
|
+
throw new Error(`Command owner is not loaded: ${moduleId}`);
|
|
348
|
+
return module;
|
|
349
|
+
}
|
|
350
|
+
function isInstallationIndependentModule(modulesById, moduleId) {
|
|
351
|
+
return isApplicationGlobalPolicy(commandRegistrationPolicy(requiredCommandModule(modulesById, moduleId)));
|
|
352
|
+
}
|
|
232
353
|
export function resolveCommandExecutor(command, subcommandName, groupName = null) {
|
|
233
354
|
if (hasNestedCommands(command)) {
|
|
234
355
|
if (!subcommandName)
|
|
@@ -448,6 +569,12 @@ function resolveComponentPrefix(items, customId) {
|
|
|
448
569
|
}
|
|
449
570
|
return items.find(({ contribution }) => customId.startsWith(contribution.customIdPrefix ?? ""));
|
|
450
571
|
}
|
|
572
|
+
function staleComponentResponse() {
|
|
573
|
+
return {
|
|
574
|
+
content: "This control is no longer available. Open the latest Helyx message and try again.",
|
|
575
|
+
flags: MessageFlags.Ephemeral,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
451
578
|
function createContext(interaction, services) {
|
|
452
579
|
return {
|
|
453
580
|
interactionId: interaction.id,
|
|
@@ -483,7 +610,13 @@ function createContext(interaction, services) {
|
|
|
483
610
|
reply: (response) => replyToInteraction(interaction, response),
|
|
484
611
|
...(interaction.isMessageComponent()
|
|
485
612
|
? {
|
|
486
|
-
updateResponse: async (response) =>
|
|
613
|
+
updateResponse: async (response) => {
|
|
614
|
+
if (interaction.deferred || interaction.replied) {
|
|
615
|
+
await interaction.editReply(toUpdateOptions(response));
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
await interaction.update(toUpdateOptions(response));
|
|
619
|
+
},
|
|
487
620
|
editSourceMessage: async (response) => interaction.webhook
|
|
488
621
|
.editMessage(interaction.message.id, toUpdateOptions(response))
|
|
489
622
|
.then(() => undefined),
|
|
@@ -532,7 +665,7 @@ function createCommandContext(interaction, services) {
|
|
|
532
665
|
},
|
|
533
666
|
};
|
|
534
667
|
}
|
|
535
|
-
function createAutocompleteContext(interaction, services) {
|
|
668
|
+
function createAutocompleteContext(interaction, services, respond) {
|
|
536
669
|
const focused = interaction.options.getFocused(true);
|
|
537
670
|
const actor = permissionActor(interaction);
|
|
538
671
|
return {
|
|
@@ -546,27 +679,60 @@ function createAutocompleteContext(interaction, services) {
|
|
|
546
679
|
focusedOptionName: focused.name,
|
|
547
680
|
focusedValue: String(focused.value),
|
|
548
681
|
services,
|
|
549
|
-
respond
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
682
|
+
respond,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
export function createAutocompleteResponseController(interaction, logger) {
|
|
686
|
+
let attempted = false;
|
|
687
|
+
const respond = async (choices) => {
|
|
688
|
+
const normalized = normalizeAutocompleteChoices(choices);
|
|
689
|
+
if (attempted)
|
|
690
|
+
return;
|
|
691
|
+
attempted = true;
|
|
692
|
+
clearTimeout(deadline);
|
|
693
|
+
await interaction.respond(normalized);
|
|
694
|
+
};
|
|
695
|
+
const deadline = setTimeout(() => {
|
|
696
|
+
logger.warn({
|
|
697
|
+
event: "interaction.autocomplete_deadline",
|
|
698
|
+
...interactionLogContext(interaction),
|
|
699
|
+
}, "Autocomplete reached its response deadline");
|
|
700
|
+
void respond([]).catch((error) => {
|
|
701
|
+
logger.warn({
|
|
702
|
+
event: "interaction.autocomplete_response_failed",
|
|
703
|
+
...interactionLogContext(interaction),
|
|
704
|
+
error: safeError(error),
|
|
705
|
+
}, "Autocomplete fallback response failed");
|
|
706
|
+
});
|
|
707
|
+
}, AUTOCOMPLETE_RESPONSE_DEADLINE_MS);
|
|
708
|
+
deadline.unref();
|
|
709
|
+
return {
|
|
710
|
+
respond,
|
|
711
|
+
async ensureResponded() {
|
|
712
|
+
clearTimeout(deadline);
|
|
713
|
+
if (!attempted)
|
|
714
|
+
await respond([]);
|
|
567
715
|
},
|
|
568
716
|
};
|
|
569
717
|
}
|
|
718
|
+
function normalizeAutocompleteChoices(choices) {
|
|
719
|
+
if (choices.length > 25)
|
|
720
|
+
throw new Error("Autocomplete supports at most 25 choices");
|
|
721
|
+
const values = new Set();
|
|
722
|
+
return choices.map(({ name, value }) => {
|
|
723
|
+
const choiceName = name.trim().normalize("NFC");
|
|
724
|
+
const choiceValue = value.trim().normalize("NFC");
|
|
725
|
+
if (choiceName.length < 1 ||
|
|
726
|
+
choiceName.length > 100 ||
|
|
727
|
+
choiceValue.length < 1 ||
|
|
728
|
+
choiceValue.length > 100)
|
|
729
|
+
throw new Error("Autocomplete names and values must be between 1 and 100 characters");
|
|
730
|
+
if (values.has(choiceValue))
|
|
731
|
+
throw new Error("Autocomplete choice values must be unique");
|
|
732
|
+
values.add(choiceValue);
|
|
733
|
+
return { name: choiceName, value: choiceValue };
|
|
734
|
+
});
|
|
735
|
+
}
|
|
570
736
|
function createModalContext(interaction, services) {
|
|
571
737
|
const updateResponse = interaction.isFromMessage()
|
|
572
738
|
? async (response) => interaction.update(toUpdateOptions(response)).then(() => undefined)
|
|
@@ -694,7 +860,9 @@ export async function addChannelMessageReaction(client, channelId, messageId, em
|
|
|
694
860
|
const message = await channel.messages.fetch(messageId);
|
|
695
861
|
await message.react(emojiIdentifier);
|
|
696
862
|
}
|
|
697
|
-
async function authorizeComponent(interaction, moduleId, services, requiredModuleName) {
|
|
863
|
+
async function authorizeComponent(interaction, moduleId, services, requiredModuleName, installationIndependent = false) {
|
|
864
|
+
if (installationIndependent)
|
|
865
|
+
return true;
|
|
698
866
|
if (!(await moduleIsGloballyAvailable(services, moduleId))) {
|
|
699
867
|
await replyEphemeralText(interaction, GLOBAL_MODULE_UNAVAILABLE_MESSAGE);
|
|
700
868
|
return false;
|
|
@@ -717,17 +885,39 @@ async function replyEphemeralText(interaction, content) {
|
|
|
717
885
|
}
|
|
718
886
|
await interaction.reply({ content, flags: MessageFlags.Ephemeral });
|
|
719
887
|
}
|
|
720
|
-
export async function authorizeCommand(interaction, moduleId, commandId, rateLimit, services, selfAuthorizingGuildCommand, resourceScopedCommand = false, requiredModuleName) {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
888
|
+
export async function authorizeCommand(interaction, moduleId, commandId, rateLimit, services, selfAuthorizingGuildCommand, resourceScopedCommand = false, requiredModuleName, installationIndependentCommand = false, authorization) {
|
|
889
|
+
const bypassPlatformAuthorization = authorization === "public" || authorization === "self-authorized";
|
|
890
|
+
if (!installationIndependentCommand && !selfAuthorizingGuildCommand) {
|
|
891
|
+
const installations = interaction.guildId
|
|
892
|
+
? services.get(HELYX_SERVICE_NAMES.installations)
|
|
893
|
+
: undefined;
|
|
894
|
+
const [globallyAvailable, moduleEnabled] = await Promise.all([
|
|
895
|
+
moduleIsGloballyAvailable(services, moduleId),
|
|
896
|
+
installations && !selfAuthorizingGuildCommand
|
|
897
|
+
? installations.isModuleEnabled(interaction.guildId, moduleId)
|
|
898
|
+
: Promise.resolve(true),
|
|
899
|
+
]);
|
|
900
|
+
if (!globallyAvailable) {
|
|
901
|
+
await replyEphemeralText(interaction, GLOBAL_MODULE_UNAVAILABLE_MESSAGE);
|
|
902
|
+
return false;
|
|
903
|
+
}
|
|
904
|
+
if (!moduleEnabled) {
|
|
905
|
+
await replyEphemeralText(interaction, requiredModuleName
|
|
906
|
+
? `This command needs ${requiredModuleName}. Ask a server administrator to enable it in the Helyx dashboard.`
|
|
907
|
+
: "This module is disabled in this server. Ask a server administrator to enable it with `/helyx` or in the Helyx dashboard.");
|
|
725
908
|
return false;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
if (!bypassPlatformAuthorization &&
|
|
912
|
+
interaction.guildId &&
|
|
913
|
+
!selfAuthorizingGuildCommand) {
|
|
726
914
|
if (!resourceScopedCommand) {
|
|
727
915
|
const permissions = services.get(HELYX_SERVICE_NAMES.permissions);
|
|
728
|
-
const policy = await
|
|
916
|
+
const [policy, guildAccess] = await Promise.all([
|
|
917
|
+
permissions.getEffectivePolicy(interaction.guildId, moduleId, commandId),
|
|
918
|
+
permissions.getGuildAccessPolicy?.(interaction.guildId),
|
|
919
|
+
]);
|
|
729
920
|
const actor = permissionActor(interaction);
|
|
730
|
-
const guildAccess = await permissions.getGuildAccessPolicy?.(interaction.guildId);
|
|
731
921
|
if (!canUseCommand(actor, policy, guildAccess)) {
|
|
732
922
|
await replyEphemeralText(interaction, "You do not have permission to use this command.");
|
|
733
923
|
return false;
|
|
@@ -738,7 +928,9 @@ export async function authorizeCommand(interaction, moduleId, commandId, rateLim
|
|
|
738
928
|
const limits = services.get(HELYX_SERVICE_NAMES.rateLimits);
|
|
739
929
|
const result = await limits.consume({
|
|
740
930
|
scope: rateLimit.scope,
|
|
741
|
-
subjectId: rateLimit.scope === "guild"
|
|
931
|
+
subjectId: rateLimit.scope === "guild"
|
|
932
|
+
? (interaction.guildId ?? interaction.user.id)
|
|
933
|
+
: interaction.user.id,
|
|
742
934
|
action: `${moduleId}:${commandId}`,
|
|
743
935
|
limit: rateLimit.limit,
|
|
744
936
|
windowSeconds: rateLimit.windowSeconds,
|
|
@@ -750,19 +942,42 @@ export async function authorizeCommand(interaction, moduleId, commandId, rateLim
|
|
|
750
942
|
}
|
|
751
943
|
return true;
|
|
752
944
|
}
|
|
753
|
-
async function authorizeAutocomplete(interaction, moduleId, commandId, services, selfAuthorizingGuildCommand, resourceScopedCommand) {
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
945
|
+
async function authorizeAutocomplete(interaction, moduleId, commandId, services, selfAuthorizingGuildCommand, resourceScopedCommand, installationIndependentCommand = false, authorization) {
|
|
946
|
+
const bypassPlatformAuthorization = authorization === "public" || authorization === "self-authorized";
|
|
947
|
+
if (!installationIndependentCommand && !selfAuthorizingGuildCommand) {
|
|
948
|
+
const installations = interaction.guildId
|
|
949
|
+
? services.get(HELYX_SERVICE_NAMES.installations)
|
|
950
|
+
: undefined;
|
|
951
|
+
const [globallyAvailable, moduleEnabled] = await Promise.all([
|
|
952
|
+
moduleIsGloballyAvailable(services, moduleId),
|
|
953
|
+
installations && !selfAuthorizingGuildCommand
|
|
954
|
+
? installations.isModuleEnabled(interaction.guildId, moduleId)
|
|
955
|
+
: Promise.resolve(true),
|
|
956
|
+
]);
|
|
957
|
+
if (!globallyAvailable || !moduleEnabled)
|
|
958
|
+
return false;
|
|
959
|
+
}
|
|
960
|
+
if (bypassPlatformAuthorization ||
|
|
961
|
+
!interaction.guildId ||
|
|
962
|
+
selfAuthorizingGuildCommand)
|
|
757
963
|
return true;
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
964
|
+
if (installationIndependentCommand) {
|
|
965
|
+
if (resourceScopedCommand)
|
|
966
|
+
return true;
|
|
967
|
+
const permissions = services.get(HELYX_SERVICE_NAMES.permissions);
|
|
968
|
+
const [policy, guildAccess] = await Promise.all([
|
|
969
|
+
permissions.getEffectivePolicy(interaction.guildId, moduleId, commandId),
|
|
970
|
+
permissions.getGuildAccessPolicy?.(interaction.guildId),
|
|
971
|
+
]);
|
|
972
|
+
return canUseCommand(permissionActor(interaction), policy, guildAccess);
|
|
973
|
+
}
|
|
761
974
|
if (resourceScopedCommand)
|
|
762
975
|
return true;
|
|
763
976
|
const permissions = services.get(HELYX_SERVICE_NAMES.permissions);
|
|
764
|
-
const policy = await
|
|
765
|
-
|
|
977
|
+
const [policy, guildAccess] = await Promise.all([
|
|
978
|
+
permissions.getEffectivePolicy(interaction.guildId, moduleId, commandId),
|
|
979
|
+
permissions.getGuildAccessPolicy?.(interaction.guildId),
|
|
980
|
+
]);
|
|
766
981
|
return canUseCommand(permissionActor(interaction), policy, guildAccess);
|
|
767
982
|
}
|
|
768
983
|
function permissionActor(interaction) {
|
|
@@ -995,7 +1210,7 @@ function toModalLabel(field) {
|
|
|
995
1210
|
input.setMaxLength(field.maxLength);
|
|
996
1211
|
if (field.placeholder !== undefined)
|
|
997
1212
|
input.setPlaceholder(field.placeholder);
|
|
998
|
-
if (field.value !== undefined)
|
|
1213
|
+
if (field.value !== undefined && field.value.length > 0)
|
|
999
1214
|
input.setValue(field.value);
|
|
1000
1215
|
label.setTextInputComponent(input);
|
|
1001
1216
|
}
|