@helyx/bot 0.4.8 → 0.4.11
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 +50 -0
- package/dist/cloud-control-dispatcher.js +2 -1
- package/dist/control-server.d.ts +5 -0
- package/dist/control-server.js +106 -14
- package/dist/discord-owned-message-service.js +9 -1
- package/dist/events.d.ts +64 -1
- package/dist/events.js +709 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.js +69 -19
- package/dist/interactions.js +167 -28
- package/dist/management-module.js +42 -10
- package/dist/message-classification.d.ts +5 -0
- package/dist/message-classification.js +75 -0
- package/dist/modules.js +9 -0
- package/dist/numeric-write-behind-proxy.d.ts +62 -0
- package/dist/numeric-write-behind-proxy.js +313 -0
- package/dist/numeric-write-behind-redis.d.ts +123 -0
- package/dist/numeric-write-behind-redis.js +1281 -0
- package/dist/numeric-write-behind.d.ts +5 -0
- package/dist/numeric-write-behind.js +37 -0
- package/dist/scheduled-task-runner.js +11 -5
- package/dist/shard-contracts.d.ts +490 -4
- package/dist/shard-contracts.js +247 -2
- package/dist/shard-supervisor.d.ts +5 -1
- package/dist/shard-supervisor.js +227 -12
- package/dist/shard-worker.d.ts +1 -0
- package/dist/shard-worker.js +51 -7
- package/package.json +9 -9
package/dist/index.d.ts
CHANGED
|
@@ -9,5 +9,5 @@ export interface BotHandle {
|
|
|
9
9
|
export declare function startBot(processEnvironment?: NodeJS.ProcessEnv): Promise<BotHandle>;
|
|
10
10
|
export declare function resolveBotWorkerEntryPoint(entryUrl?: string): string;
|
|
11
11
|
export declare function readBotVersion(): string;
|
|
12
|
-
export declare function gatewayIntentsForModules(modules: readonly ModuleDefinition[]): GatewayIntentBits[];
|
|
12
|
+
export declare function gatewayIntentsForModules(modules: readonly ModuleDefinition[], messageContentEnabled?: boolean): GatewayIntentBits[];
|
|
13
13
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import { loadConfiguredModulePackages, platformServiceOptions, } from "./modules
|
|
|
18
18
|
import { resolveModulePackageSpecifiers } from "./module-configuration.js";
|
|
19
19
|
import { createPermissionsModule } from "./permissions-module.js";
|
|
20
20
|
import { createManagementModule } from "./management-module.js";
|
|
21
|
-
import { DiscordMemberRoleService, DiscordResourceInspection, installModuleEvents, } from "./events.js";
|
|
21
|
+
import { DiscordMessageDeliveryService, DiscordMemberEnforcementService, DiscordMemberRoleService, DiscordResourceInspection, installModuleEvents, } from "./events.js";
|
|
22
22
|
import { DiscordModuleLoggingService } from "./module-logging.js";
|
|
23
23
|
import { BotControlService, createBotControlServer, DiscordGuildResourceDirectory, } from "./control-server.js";
|
|
24
24
|
import { startShardSupervisor } from "./shard-supervisor.js";
|
|
@@ -29,6 +29,7 @@ import { createSupportModule } from "./support-module.js";
|
|
|
29
29
|
import { DiscordOwnedMessageDeletionService } from "./discord-owned-message-service.js";
|
|
30
30
|
import { CoreResourceAuthorizationService } from "./resource-authorization.js";
|
|
31
31
|
import { createLocalScheduledTaskRoute, createScheduledTaskRunner, schedulerOperationalHealth, } from "./scheduled-task-runner.js";
|
|
32
|
+
import { createLocalNumericWriteBehind } from "./numeric-write-behind.js";
|
|
32
33
|
export { loadConfiguredModulePackages } from "./modules.js";
|
|
33
34
|
export { resolveModulePackageSpecifiers } from "./module-configuration.js";
|
|
34
35
|
export * from "./control-server.js";
|
|
@@ -125,7 +126,7 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
125
126
|
}, "Module databases ready");
|
|
126
127
|
const featureModules = modulePackages.map(({ definition }) => definition);
|
|
127
128
|
const client = new Client({
|
|
128
|
-
intents: gatewayIntentsForModules(featureModules),
|
|
129
|
+
intents: gatewayIntentsForModules(featureModules, environment.HELYX_MESSAGE_CONTENT_ENABLED),
|
|
129
130
|
partials: [Partials.Message, Partials.Channel, Partials.Reaction],
|
|
130
131
|
allowedMentions: { parse: [] },
|
|
131
132
|
});
|
|
@@ -234,15 +235,25 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
234
235
|
...publicModules,
|
|
235
236
|
...(operationsModule ? [operationsModule] : []),
|
|
236
237
|
];
|
|
237
|
-
const platformServices = new Map(
|
|
238
|
+
const platformServices = new Map();
|
|
239
|
+
const serviceAccess = new ServiceRegistry(platformServices);
|
|
240
|
+
for (const [name, service] of createPlatformServices(database, publicModules, {
|
|
241
|
+
...platformServiceOptions(environment, publicModules),
|
|
242
|
+
activationServices: serviceAccess,
|
|
243
|
+
})) {
|
|
244
|
+
platformServices.set(name, service);
|
|
245
|
+
}
|
|
246
|
+
const numericWriteBehind = createLocalNumericWriteBehind(platformServices, publicModules);
|
|
247
|
+
platformServices.set(HELYX_SERVICE_NAMES.numericWriteBehind, numericWriteBehind);
|
|
238
248
|
platformServices.set(HELYX_SERVICE_NAMES.resourceAuthorization, new CoreResourceAuthorizationService(platformServices.get(HELYX_SERVICE_NAMES.permissions)));
|
|
249
|
+
platformServices.set(HELYX_SERVICE_NAMES.discordMessages, new DiscordMessageDeliveryService(client));
|
|
239
250
|
platformServices.set(HELYX_SERVICE_NAMES.discordOwnedMessages, new DiscordOwnedMessageDeletionService(client));
|
|
251
|
+
platformServices.set(HELYX_SERVICE_NAMES.memberEnforcement, new DiscordMemberEnforcementService(client));
|
|
240
252
|
platformServices.set(HELYX_SERVICE_NAMES.memberRoles, new DiscordMemberRoleService(client));
|
|
241
253
|
platformServices.set(HELYX_SERVICE_NAMES.discordResources, new DiscordResourceInspection(client));
|
|
242
254
|
if (featureModules.some(({ manifest }) => manifest.id === "helyx.logging")) {
|
|
243
255
|
platformServices.set(HELYX_SERVICE_NAMES.moduleLogging, new DiscordModuleLoggingService(client, database, publicModules, bootstrapLogger));
|
|
244
256
|
}
|
|
245
|
-
const serviceAccess = new ServiceRegistry(platformServices);
|
|
246
257
|
const runtime = createRuntime({
|
|
247
258
|
modules,
|
|
248
259
|
logger: bootstrapLogger,
|
|
@@ -305,14 +316,29 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
305
316
|
process.exitCode = 1;
|
|
306
317
|
bootstrapLogger.error({ event: "control.stop_failed", error: safeError(error) }, "Private bot control shutdown failed");
|
|
307
318
|
});
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
|
|
319
|
+
await moduleEvents.stop().catch((error) => {
|
|
320
|
+
process.exitCode = 1;
|
|
321
|
+
bootstrapLogger.error({ event: "module_events.stop_failed", error: safeError(error) }, "Module event shutdown failed");
|
|
322
|
+
});
|
|
323
|
+
await runtime.stop().catch((error) => {
|
|
313
324
|
process.exitCode = 1;
|
|
314
325
|
bootstrapLogger.error({ event: "runtime.stop_failed", error: safeError(error) }, "Runtime shutdown failed");
|
|
315
|
-
}
|
|
326
|
+
});
|
|
327
|
+
await numericWriteBehind
|
|
328
|
+
.stop()
|
|
329
|
+
.then(({ drained }) => {
|
|
330
|
+
if (drained)
|
|
331
|
+
return;
|
|
332
|
+
process.exitCode = 1;
|
|
333
|
+
bootstrapLogger.error({ event: "numeric_write_behind.shutdown_not_drained" }, "Numeric write-behind exceeded its bounded shutdown drain");
|
|
334
|
+
})
|
|
335
|
+
.catch((error) => {
|
|
336
|
+
process.exitCode = 1;
|
|
337
|
+
bootstrapLogger.error({
|
|
338
|
+
event: "numeric_write_behind.shutdown_failed",
|
|
339
|
+
error: safeError(error),
|
|
340
|
+
}, "Numeric write-behind shutdown failed");
|
|
341
|
+
});
|
|
316
342
|
await client.destroy();
|
|
317
343
|
await closeDatabase();
|
|
318
344
|
}
|
|
@@ -364,13 +390,26 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
364
390
|
await cloudConnector.stop().catch(() => undefined);
|
|
365
391
|
if (controlServer)
|
|
366
392
|
await controlServer.close().catch(() => undefined);
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
393
|
+
await moduleEvents.stop().catch((stopError) => {
|
|
394
|
+
bootstrapLogger.error({
|
|
395
|
+
event: "module_events.cleanup_failed",
|
|
396
|
+
error: safeError(stopError),
|
|
397
|
+
}, "Module event cleanup failed");
|
|
398
|
+
});
|
|
399
|
+
await runtime.stop().catch((stopError) => {
|
|
372
400
|
bootstrapLogger.error({ event: "runtime.cleanup_failed", error: safeError(stopError) }, "Runtime cleanup failed");
|
|
373
|
-
}
|
|
401
|
+
});
|
|
402
|
+
const numericCleanup = await numericWriteBehind
|
|
403
|
+
.stop()
|
|
404
|
+
.catch((stopError) => {
|
|
405
|
+
bootstrapLogger.error({
|
|
406
|
+
event: "numeric_write_behind.cleanup_failed",
|
|
407
|
+
error: safeError(stopError),
|
|
408
|
+
}, "Numeric write-behind cleanup failed");
|
|
409
|
+
return { drained: false };
|
|
410
|
+
});
|
|
411
|
+
if (!numericCleanup.drained)
|
|
412
|
+
bootstrapLogger.error({ event: "numeric_write_behind.cleanup_not_drained" }, "Numeric write-behind cleanup did not drain");
|
|
374
413
|
await client.destroy();
|
|
375
414
|
throw error;
|
|
376
415
|
}
|
|
@@ -407,16 +446,27 @@ function deploymentReference(processEnvironment) {
|
|
|
407
446
|
processEnvironment.GIT_COMMIT_SHA;
|
|
408
447
|
return value?.trim().slice(0, 12) || undefined;
|
|
409
448
|
}
|
|
410
|
-
export function gatewayIntentsForModules(modules) {
|
|
449
|
+
export function gatewayIntentsForModules(modules, messageContentEnabled = false) {
|
|
411
450
|
const intents = [GatewayIntentBits.Guilds];
|
|
412
451
|
if (modules.some((module) => module.events?.messageReactionAdd?.length ||
|
|
413
452
|
module.events?.messageReactionRemove?.length)) {
|
|
414
453
|
intents.push(GatewayIntentBits.GuildMessageReactions);
|
|
415
454
|
}
|
|
416
|
-
|
|
455
|
+
const hasPrivilegedMessageCreate = modules.some((module) => module.events?.privilegedMessageCreate?.length);
|
|
456
|
+
const requiresMessageContent = modules.some((module) => module.requiredDiscordCapabilities?.includes("message-content"));
|
|
457
|
+
if (((hasPrivilegedMessageCreate || requiresMessageContent) &&
|
|
458
|
+
messageContentEnabled) ||
|
|
459
|
+
modules.some((module) => module.events?.guildMessageCreate?.length ||
|
|
460
|
+
module.events?.messageDelete?.length)) {
|
|
417
461
|
intents.push(GatewayIntentBits.GuildMessages);
|
|
418
462
|
}
|
|
419
|
-
if (
|
|
463
|
+
if ((hasPrivilegedMessageCreate || requiresMessageContent) &&
|
|
464
|
+
messageContentEnabled) {
|
|
465
|
+
intents.push(GatewayIntentBits.MessageContent);
|
|
466
|
+
}
|
|
467
|
+
if (modules.some((module) => module.requiredDiscordCapabilities?.includes("guild-members") ||
|
|
468
|
+
module.events?.guildMemberAdd?.length ||
|
|
469
|
+
module.events?.guildMemberRemove?.length)) {
|
|
420
470
|
intents.push(GatewayIntentBits.GuildMembers);
|
|
421
471
|
}
|
|
422
472
|
if (modules.some((module) => module.events?.guildEmojiDelete?.length ||
|
package/dist/interactions.js
CHANGED
|
@@ -17,14 +17,18 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
17
17
|
if (guildCommands.length > 0 && !registration.internalGuildId) {
|
|
18
18
|
throw new Error("DISCORD_INTERNAL_GUILD_ID is required when guild commands are loaded");
|
|
19
19
|
}
|
|
20
|
-
const
|
|
20
|
+
const buttonItems = modules.flatMap((module) => (module.interactions?.buttons ?? []).map((contribution) => ({
|
|
21
21
|
moduleId: module.manifest.id,
|
|
22
22
|
contribution,
|
|
23
|
-
})))
|
|
24
|
-
const
|
|
23
|
+
})));
|
|
24
|
+
const buttons = uniqueMap(buttonItems.filter(({ contribution }) => contribution.customId !== undefined), ({ contribution }) => contribution.customId, "button custom ID");
|
|
25
|
+
const buttonPrefixes = componentPrefixes(buttonItems, "button");
|
|
26
|
+
const modalItems = modules.flatMap((module) => (module.interactions?.modals ?? []).map((contribution) => ({
|
|
25
27
|
moduleId: module.manifest.id,
|
|
26
28
|
contribution,
|
|
27
|
-
})))
|
|
29
|
+
})));
|
|
30
|
+
const modals = uniqueMap(modalItems.filter(({ contribution }) => contribution.customId !== undefined), ({ contribution }) => contribution.customId, "modal custom ID");
|
|
31
|
+
const modalPrefixes = componentPrefixes(modalItems, "modal");
|
|
28
32
|
const selects = uniqueMap(modules.flatMap((module) => (module.interactions?.selects ?? []).map((contribution) => ({
|
|
29
33
|
moduleId: module.manifest.id,
|
|
30
34
|
contribution,
|
|
@@ -67,7 +71,24 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
67
71
|
});
|
|
68
72
|
async function handleInteraction(interaction) {
|
|
69
73
|
try {
|
|
70
|
-
if (interaction.
|
|
74
|
+
if (interaction.isAutocomplete()) {
|
|
75
|
+
const registered = commands.get(interaction.commandName);
|
|
76
|
+
const subcommandName = registered?.contribution.subcommands?.length
|
|
77
|
+
? interaction.options.getSubcommand(false)
|
|
78
|
+
: null;
|
|
79
|
+
const subcommand = subcommandName
|
|
80
|
+
? registered?.contribution.subcommands?.find(({ name }) => name === subcommandName)
|
|
81
|
+
: undefined;
|
|
82
|
+
if (!registered ||
|
|
83
|
+
!subcommand?.autocomplete ||
|
|
84
|
+
!(await authorizeAutocomplete(interaction, registered.moduleId, subcommand.permissionId ??
|
|
85
|
+
`${registered.contribution.name}.${subcommand.name}`, services, registration.selfAuthorizingGuildModuleIds?.has(registered.moduleId) ?? false, subcommand.authorization === "resource-scoped"))) {
|
|
86
|
+
await interaction.respond([]);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
await subcommand.autocomplete(createAutocompleteContext(interaction, services));
|
|
90
|
+
}
|
|
91
|
+
else if (interaction.isChatInputCommand()) {
|
|
71
92
|
const registered = commands.get(interaction.commandName);
|
|
72
93
|
if (registered) {
|
|
73
94
|
const { contribution: command, moduleId } = registered;
|
|
@@ -107,7 +128,8 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
107
128
|
}
|
|
108
129
|
}
|
|
109
130
|
else if (interaction.isButton()) {
|
|
110
|
-
const button = buttons.get(interaction.customId)
|
|
131
|
+
const button = buttons.get(interaction.customId) ??
|
|
132
|
+
resolveComponentPrefix(buttonPrefixes, interaction.customId);
|
|
111
133
|
const context = button
|
|
112
134
|
? createContext(interaction, services)
|
|
113
135
|
: undefined;
|
|
@@ -115,6 +137,11 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
115
137
|
button?.contribution.acknowledgement === "deferred-ephemeral") {
|
|
116
138
|
await context.deferReply?.(true);
|
|
117
139
|
}
|
|
140
|
+
if (button &&
|
|
141
|
+
interaction.isButton() &&
|
|
142
|
+
button.contribution.acknowledgement === "deferred-update") {
|
|
143
|
+
await interaction.deferUpdate();
|
|
144
|
+
}
|
|
118
145
|
if (button &&
|
|
119
146
|
context &&
|
|
120
147
|
(await authorizeComponent(interaction, button.moduleId, services))) {
|
|
@@ -122,7 +149,8 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
122
149
|
}
|
|
123
150
|
}
|
|
124
151
|
else if (interaction.isModalSubmit()) {
|
|
125
|
-
const modal = modals.get(interaction.customId)
|
|
152
|
+
const modal = modals.get(interaction.customId) ??
|
|
153
|
+
resolveComponentPrefix(modalPrefixes, interaction.customId);
|
|
126
154
|
if (modal &&
|
|
127
155
|
(await authorizeComponent(interaction, modal.moduleId, services))) {
|
|
128
156
|
await modal.contribution.execute(createModalContext(interaction, services));
|
|
@@ -188,6 +216,9 @@ export function toCommandDefinition(registered) {
|
|
|
188
216
|
if (!!command.execute === !!command.subcommands?.length) {
|
|
189
217
|
throw new Error(`Command ${command.name} must define either execute or subcommands`);
|
|
190
218
|
}
|
|
219
|
+
if (command.options?.length && command.subcommands?.length) {
|
|
220
|
+
throw new Error(`Command ${command.name} cannot define both direct options and subcommands`);
|
|
221
|
+
}
|
|
191
222
|
const contextTypes = {
|
|
192
223
|
guild: InteractionContextType.Guild,
|
|
193
224
|
bot_dm: InteractionContextType.BotDM,
|
|
@@ -202,6 +233,9 @@ export function toCommandDefinition(registered) {
|
|
|
202
233
|
.setDescription(command.description)
|
|
203
234
|
.setContexts(command.contexts.map((context) => contextTypes[context]))
|
|
204
235
|
.setIntegrationTypes(command.integrationTypes.map((integrationType) => integrationTypes[integrationType]));
|
|
236
|
+
for (const option of command.options ?? []) {
|
|
237
|
+
addCommandOption(builder, option);
|
|
238
|
+
}
|
|
205
239
|
for (const subcommand of command.subcommands ?? []) {
|
|
206
240
|
builder.addSubcommand((subcommandBuilder) => {
|
|
207
241
|
subcommandBuilder
|
|
@@ -217,6 +251,8 @@ export function toCommandDefinition(registered) {
|
|
|
217
251
|
}
|
|
218
252
|
function addCommandOption(builder, option) {
|
|
219
253
|
if (option.type === "string") {
|
|
254
|
+
if (option.autocomplete && option.choices?.length)
|
|
255
|
+
throw new Error(`Command option ${option.name} cannot use choices and autocomplete together`);
|
|
220
256
|
builder.addStringOption((optionBuilder) => {
|
|
221
257
|
optionBuilder
|
|
222
258
|
.setName(option.name)
|
|
@@ -228,6 +264,8 @@ function addCommandOption(builder, option) {
|
|
|
228
264
|
optionBuilder.setMaxLength(option.maxLength);
|
|
229
265
|
if (option.choices)
|
|
230
266
|
optionBuilder.addChoices(...option.choices);
|
|
267
|
+
if (option.autocomplete)
|
|
268
|
+
optionBuilder.setAutocomplete(option.autocomplete);
|
|
231
269
|
return optionBuilder;
|
|
232
270
|
});
|
|
233
271
|
return;
|
|
@@ -239,6 +277,13 @@ function addCommandOption(builder, option) {
|
|
|
239
277
|
.setRequired(option.required ?? false));
|
|
240
278
|
return;
|
|
241
279
|
}
|
|
280
|
+
if (option.type === "user") {
|
|
281
|
+
builder.addUserOption((optionBuilder) => optionBuilder
|
|
282
|
+
.setName(option.name)
|
|
283
|
+
.setDescription(option.description)
|
|
284
|
+
.setRequired(option.required ?? false));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
242
287
|
if (option.type === "role") {
|
|
243
288
|
builder.addRoleOption((optionBuilder) => optionBuilder
|
|
244
289
|
.setName(option.name)
|
|
@@ -270,9 +315,43 @@ export function uniqueMap(items, keyOf, label) {
|
|
|
270
315
|
}
|
|
271
316
|
return result;
|
|
272
317
|
}
|
|
318
|
+
function componentPrefixes(items, label) {
|
|
319
|
+
const prefixes = [];
|
|
320
|
+
for (const item of items) {
|
|
321
|
+
const { customId, customIdPrefix } = item.contribution;
|
|
322
|
+
if ((customId === undefined) === (customIdPrefix === undefined)) {
|
|
323
|
+
throw new Error(`${label} contribution must declare exactly one custom ID or prefix`);
|
|
324
|
+
}
|
|
325
|
+
const route = customId ?? customIdPrefix;
|
|
326
|
+
if (route.length < 1 ||
|
|
327
|
+
route.length > 100 ||
|
|
328
|
+
!/^[A-Za-z0-9._:-]+$/u.test(route)) {
|
|
329
|
+
throw new Error(`${label} custom ID routing value is invalid`);
|
|
330
|
+
}
|
|
331
|
+
if (customIdPrefix !== undefined)
|
|
332
|
+
prefixes.push(item);
|
|
333
|
+
}
|
|
334
|
+
for (let index = 0; index < prefixes.length; index += 1) {
|
|
335
|
+
const prefix = prefixes[index].contribution.customIdPrefix;
|
|
336
|
+
if (prefixes.some((candidate, candidateIndex) => candidateIndex !== index &&
|
|
337
|
+
(candidate.contribution.customIdPrefix ?? "").startsWith(prefix))) {
|
|
338
|
+
throw new Error(`Ambiguous ${label} custom ID prefix: ${prefix}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return prefixes;
|
|
342
|
+
}
|
|
343
|
+
function resolveComponentPrefix(items, customId) {
|
|
344
|
+
if (customId.length < 1 ||
|
|
345
|
+
customId.length > 100 ||
|
|
346
|
+
!/^[A-Za-z0-9._:-]+$/u.test(customId)) {
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
return items.find(({ contribution }) => customId.startsWith(contribution.customIdPrefix ?? ""));
|
|
350
|
+
}
|
|
273
351
|
function createContext(interaction, services) {
|
|
274
352
|
return {
|
|
275
353
|
interactionId: interaction.id,
|
|
354
|
+
...("customId" in interaction ? { customId: interaction.customId } : {}),
|
|
276
355
|
userId: interaction.user.id,
|
|
277
356
|
userDisplayName: interaction.user.globalName ?? interaction.user.username,
|
|
278
357
|
userRoleIds: interaction.member instanceof GuildMember
|
|
@@ -301,6 +380,9 @@ function createContext(interaction, services) {
|
|
|
301
380
|
...(interaction.isMessageComponent()
|
|
302
381
|
? {
|
|
303
382
|
updateResponse: async (response) => interaction.update(toUpdateOptions(response)).then(() => undefined),
|
|
383
|
+
editSourceMessage: async (response) => interaction.webhook
|
|
384
|
+
.editMessage(interaction.message.id, toUpdateOptions(response))
|
|
385
|
+
.then(() => undefined),
|
|
304
386
|
}
|
|
305
387
|
: {}),
|
|
306
388
|
showModal: async (modal) => interaction.showModal(toModal(modal)).then(() => undefined),
|
|
@@ -315,6 +397,16 @@ function createCommandContext(interaction, services) {
|
|
|
315
397
|
...createContext(interaction, services),
|
|
316
398
|
getStringOption: (name) => interaction.options.getString(name),
|
|
317
399
|
getBooleanOption: (name) => interaction.options.getBoolean(name),
|
|
400
|
+
getUserOption: (name) => {
|
|
401
|
+
const user = interaction.options.getUser(name);
|
|
402
|
+
return user
|
|
403
|
+
? {
|
|
404
|
+
id: user.id,
|
|
405
|
+
displayName: user.globalName ?? user.username,
|
|
406
|
+
isBot: user.bot,
|
|
407
|
+
}
|
|
408
|
+
: null;
|
|
409
|
+
},
|
|
318
410
|
getRoleOption: (name) => {
|
|
319
411
|
const role = interaction.options.getRole(name);
|
|
320
412
|
return role ? { id: role.id, name: role.name } : null;
|
|
@@ -335,12 +427,48 @@ function createCommandContext(interaction, services) {
|
|
|
335
427
|
},
|
|
336
428
|
};
|
|
337
429
|
}
|
|
430
|
+
function createAutocompleteContext(interaction, services) {
|
|
431
|
+
const focused = interaction.options.getFocused(true);
|
|
432
|
+
const actor = permissionActor(interaction);
|
|
433
|
+
return {
|
|
434
|
+
interactionId: interaction.id,
|
|
435
|
+
userId: actor.userId,
|
|
436
|
+
userRoleIds: actor.roleIds,
|
|
437
|
+
userPermissions: actor.permissions,
|
|
438
|
+
isServerOwner: actor.isServerOwner,
|
|
439
|
+
isAdministrator: actor.isAdministrator,
|
|
440
|
+
...(interaction.guildId ? { serverId: interaction.guildId } : {}),
|
|
441
|
+
focusedOptionName: focused.name,
|
|
442
|
+
focusedValue: String(focused.value),
|
|
443
|
+
services,
|
|
444
|
+
respond: async (choices) => {
|
|
445
|
+
if (choices.length > 25)
|
|
446
|
+
throw new Error("Autocomplete supports at most 25 choices");
|
|
447
|
+
const values = new Set();
|
|
448
|
+
const normalized = choices.map(({ name, value }) => {
|
|
449
|
+
const choiceName = name.trim().normalize("NFC");
|
|
450
|
+
const choiceValue = value.trim().normalize("NFC");
|
|
451
|
+
if (choiceName.length < 1 ||
|
|
452
|
+
choiceName.length > 100 ||
|
|
453
|
+
choiceValue.length < 1 ||
|
|
454
|
+
choiceValue.length > 100)
|
|
455
|
+
throw new Error("Autocomplete names and values must be between 1 and 100 characters");
|
|
456
|
+
if (values.has(choiceValue))
|
|
457
|
+
throw new Error("Autocomplete choice values must be unique");
|
|
458
|
+
values.add(choiceValue);
|
|
459
|
+
return { name: choiceName, value: choiceValue };
|
|
460
|
+
});
|
|
461
|
+
await interaction.respond(normalized);
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
}
|
|
338
465
|
function createModalContext(interaction, services) {
|
|
339
466
|
const updateResponse = interaction.isFromMessage()
|
|
340
467
|
? async (response) => interaction.update(toUpdateOptions(response)).then(() => undefined)
|
|
341
468
|
: undefined;
|
|
342
469
|
return {
|
|
343
470
|
interactionId: interaction.id,
|
|
471
|
+
customId: interaction.customId,
|
|
344
472
|
userId: interaction.user.id,
|
|
345
473
|
userDisplayName: interaction.user.globalName ?? interaction.user.username,
|
|
346
474
|
userRoleIds: interaction.member instanceof GuildMember
|
|
@@ -467,17 +595,7 @@ export async function authorizeCommand(interaction, moduleId, commandId, rateLim
|
|
|
467
595
|
if (!resourceScopedCommand) {
|
|
468
596
|
const permissions = services.get(HELYX_SERVICE_NAMES.permissions);
|
|
469
597
|
const policy = await permissions.getEffectivePolicy(interaction.guildId, moduleId, commandId);
|
|
470
|
-
const actor =
|
|
471
|
-
userId: interaction.user.id,
|
|
472
|
-
roleIds: interaction.member instanceof GuildMember
|
|
473
|
-
? [...interaction.member.roles.cache.keys()]
|
|
474
|
-
: (interaction.member?.roles ?? []),
|
|
475
|
-
permissions: interaction.memberPermissions
|
|
476
|
-
?.toArray()
|
|
477
|
-
.map(normalizeDiscordPermission) ?? [],
|
|
478
|
-
isServerOwner: interaction.guild?.ownerId === interaction.user.id,
|
|
479
|
-
isAdministrator: interaction.memberPermissions?.has(PermissionFlagsBits.Administrator) ?? false,
|
|
480
|
-
};
|
|
598
|
+
const actor = permissionActor(interaction);
|
|
481
599
|
if (!canUseCommand(actor, policy)) {
|
|
482
600
|
await interaction.reply({
|
|
483
601
|
content: "You do not have permission to use this command.",
|
|
@@ -506,6 +624,32 @@ export async function authorizeCommand(interaction, moduleId, commandId, rateLim
|
|
|
506
624
|
}
|
|
507
625
|
return true;
|
|
508
626
|
}
|
|
627
|
+
async function authorizeAutocomplete(interaction, moduleId, commandId, services, selfAuthorizingGuildCommand, resourceScopedCommand) {
|
|
628
|
+
if (!interaction.guildId || selfAuthorizingGuildCommand)
|
|
629
|
+
return true;
|
|
630
|
+
const installations = services.get(HELYX_SERVICE_NAMES.installations);
|
|
631
|
+
if (!(await installations.isModuleEnabled(interaction.guildId, moduleId)))
|
|
632
|
+
return false;
|
|
633
|
+
if (resourceScopedCommand)
|
|
634
|
+
return true;
|
|
635
|
+
const permissions = services.get(HELYX_SERVICE_NAMES.permissions);
|
|
636
|
+
const policy = await permissions.getEffectivePolicy(interaction.guildId, moduleId, commandId);
|
|
637
|
+
return canUseCommand(permissionActor(interaction), policy);
|
|
638
|
+
}
|
|
639
|
+
function permissionActor(interaction) {
|
|
640
|
+
return {
|
|
641
|
+
userId: interaction.user.id,
|
|
642
|
+
roleIds: interaction.member instanceof GuildMember
|
|
643
|
+
? [...interaction.member.roles.cache.keys()]
|
|
644
|
+
: (interaction.member?.roles ?? []),
|
|
645
|
+
permissions: interaction.memberPermissions
|
|
646
|
+
?.toArray()
|
|
647
|
+
.map(normalizeDiscordPermission) ?? [],
|
|
648
|
+
isServerOwner: interaction.guild?.ownerId === interaction.user.id,
|
|
649
|
+
isAdministrator: interaction.memberPermissions?.has(PermissionFlagsBits.Administrator) ??
|
|
650
|
+
false,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
509
653
|
export function normalizeDiscordPermission(permission) {
|
|
510
654
|
return permission.replaceAll(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase();
|
|
511
655
|
}
|
|
@@ -629,19 +773,10 @@ function toSelectRows(response) {
|
|
|
629
773
|
return rows;
|
|
630
774
|
}
|
|
631
775
|
export function toUpdateOptions(response) {
|
|
632
|
-
|
|
776
|
+
return toReplyOptions({
|
|
633
777
|
...response,
|
|
634
778
|
ephemeral: false,
|
|
635
779
|
});
|
|
636
|
-
return response.componentsV2
|
|
637
|
-
? {
|
|
638
|
-
...options,
|
|
639
|
-
// Discord requires legacy fields to be cleared when an existing
|
|
640
|
-
// message is first converted to Components V2.
|
|
641
|
-
content: null,
|
|
642
|
-
embeds: [],
|
|
643
|
-
}
|
|
644
|
-
: options;
|
|
645
780
|
}
|
|
646
781
|
function toAllowedUserMentions(userIds) {
|
|
647
782
|
if (userIds === undefined)
|
|
@@ -760,6 +895,10 @@ export function toModal(definition) {
|
|
|
760
895
|
return modal;
|
|
761
896
|
}
|
|
762
897
|
async function sendFailure(interaction) {
|
|
898
|
+
if (interaction.isAutocomplete()) {
|
|
899
|
+
await interaction.respond([]).catch(() => undefined);
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
763
902
|
if (!interaction.isRepliable())
|
|
764
903
|
return;
|
|
765
904
|
const message = "Something went wrong while handling that interaction.";
|
|
@@ -18,6 +18,7 @@ const VALUE_MODAL_ID = "helyx.management.value.modal";
|
|
|
18
18
|
const VALUE_FIELD_ID = "value";
|
|
19
19
|
const SESSION_PURPOSE = "modules.manage";
|
|
20
20
|
const PAGE_SIZE = 25;
|
|
21
|
+
const MAX_DISCORD_SETTING_SELECTIONS = 25;
|
|
21
22
|
const SESSION_LIFETIME_MS = 15 * 60 * 1_000;
|
|
22
23
|
const VIOLET = 0x7c3aed;
|
|
23
24
|
const SUCCESS = 0x16a34a;
|
|
@@ -177,7 +178,14 @@ export function createManagementModule(configurableModules, resources) {
|
|
|
177
178
|
return;
|
|
178
179
|
}
|
|
179
180
|
try {
|
|
180
|
-
module.configuration?.validate(value);
|
|
181
|
+
const validated = module.configuration?.validate(value);
|
|
182
|
+
if (validated && module.configuration?.validateUpdate) {
|
|
183
|
+
await module.configuration.validateUpdate({
|
|
184
|
+
guildId: context.serverId,
|
|
185
|
+
value: validated,
|
|
186
|
+
services: context.services,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
181
189
|
}
|
|
182
190
|
catch (error) {
|
|
183
191
|
await context.reply(validationResponse(error instanceof Error
|
|
@@ -563,8 +571,12 @@ function settingValueResponse(setting) {
|
|
|
563
571
|
{
|
|
564
572
|
type: "discord-channel",
|
|
565
573
|
customId: VALUE_SELECT_ID,
|
|
566
|
-
placeholder:
|
|
574
|
+
placeholder: setting.multiple
|
|
575
|
+
? "Choose channels"
|
|
576
|
+
: "Choose a channel",
|
|
567
577
|
channelTypes: setting.channelTypes,
|
|
578
|
+
minValues: 1,
|
|
579
|
+
maxValues: setting.multiple ? MAX_DISCORD_SETTING_SELECTIONS : 1,
|
|
568
580
|
},
|
|
569
581
|
],
|
|
570
582
|
};
|
|
@@ -631,6 +643,8 @@ function initialDraft(module, existing) {
|
|
|
631
643
|
return result;
|
|
632
644
|
}
|
|
633
645
|
function emptyValue(setting) {
|
|
646
|
+
if (setting.type === "discord-channel" && setting.multiple)
|
|
647
|
+
return [];
|
|
634
648
|
if (setting.type === "discord-role" && setting.multiple)
|
|
635
649
|
return [];
|
|
636
650
|
if (setting.type === "string" ||
|
|
@@ -655,10 +669,13 @@ function selectedValue(setting, values) {
|
|
|
655
669
|
return values[0];
|
|
656
670
|
}
|
|
657
671
|
if (setting.type === "discord-channel") {
|
|
658
|
-
if (values.length
|
|
659
|
-
|
|
672
|
+
if (values.length < 1 ||
|
|
673
|
+
values.length > (setting.multiple ? MAX_DISCORD_SETTING_SELECTIONS : 1) ||
|
|
674
|
+
new Set(values).size !== values.length ||
|
|
675
|
+
values.some((value) => !isSnowflake(value))) {
|
|
676
|
+
throw new Error("Selected channel value is invalid");
|
|
660
677
|
}
|
|
661
|
-
return values[0];
|
|
678
|
+
return setting.multiple ? [...values] : values[0];
|
|
662
679
|
}
|
|
663
680
|
if (setting.type === "discord-role") {
|
|
664
681
|
if (values.length < 1 ||
|
|
@@ -723,9 +740,21 @@ async function validateDiscordSettings(directory, guildId, module, values) {
|
|
|
723
740
|
if (configured === "" || configured === undefined)
|
|
724
741
|
continue;
|
|
725
742
|
if (setting.type === "discord-channel") {
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
743
|
+
if (setting.multiple !== Array.isArray(configured)) {
|
|
744
|
+
return `${setting.label} has an invalid Discord channel selection.`;
|
|
745
|
+
}
|
|
746
|
+
const configuredChannels = Array.isArray(configured)
|
|
747
|
+
? configured
|
|
748
|
+
: [configured];
|
|
749
|
+
if (configuredChannels.length > MAX_DISCORD_SETTING_SELECTIONS ||
|
|
750
|
+
new Set(configuredChannels).size !== configuredChannels.length ||
|
|
751
|
+
configuredChannels.some((channelId) => {
|
|
752
|
+
if (typeof channelId !== "string")
|
|
753
|
+
return true;
|
|
754
|
+
const channel = channels.get(channelId);
|
|
755
|
+
return !channel || !setting.channelTypes.includes(channel.type);
|
|
756
|
+
})) {
|
|
757
|
+
return `${setting.label} must contain available supported Discord channels.`;
|
|
729
758
|
}
|
|
730
759
|
}
|
|
731
760
|
if (setting.type === "discord-role") {
|
|
@@ -776,8 +805,11 @@ function settingStatus(setting, value) {
|
|
|
776
805
|
}
|
|
777
806
|
if (setting.type === "boolean")
|
|
778
807
|
return value === true ? "Enabled" : "Disabled";
|
|
779
|
-
if (setting.type === "discord-channel")
|
|
780
|
-
return
|
|
808
|
+
if (setting.type === "discord-channel") {
|
|
809
|
+
return Array.isArray(value)
|
|
810
|
+
? `${String(value.length)} channels selected`
|
|
811
|
+
: "Channel selected";
|
|
812
|
+
}
|
|
781
813
|
if (setting.type === "discord-role") {
|
|
782
814
|
return Array.isArray(value)
|
|
783
815
|
? `${String(value.length)} roles selected`
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type Message } from "discord.js";
|
|
2
|
+
import type { GuildMessageClassification } from "@helyx/sdk";
|
|
3
|
+
export declare function classifyGuildMessage(message: Pick<Message, "attachments" | "content" | "embeds" | "flags" | "stickers">): GuildMessageClassification;
|
|
4
|
+
export declare const unavailableGuildMessageClassification: GuildMessageClassification;
|
|
5
|
+
//# sourceMappingURL=message-classification.d.ts.map
|