@helyx/bot 0.4.9 → 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 +33 -0
- package/dist/cloud-control-dispatcher.js +2 -1
- package/dist/control-server.d.ts +5 -0
- package/dist/control-server.js +104 -13
- package/dist/events.d.ts +64 -1
- package/dist/events.js +699 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +69 -19
- package/dist/interactions.js +79 -16
- 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/shard-contracts.d.ts +486 -0
- package/dist/shard-contracts.js +245 -1
- package/dist/shard-supervisor.d.ts +2 -1
- package/dist/shard-supervisor.js +208 -1
- package/dist/shard-worker.d.ts +1 -0
- package/dist/shard-worker.js +42 -5
- 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,
|
|
@@ -124,7 +128,8 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
124
128
|
}
|
|
125
129
|
}
|
|
126
130
|
else if (interaction.isButton()) {
|
|
127
|
-
const button = buttons.get(interaction.customId)
|
|
131
|
+
const button = buttons.get(interaction.customId) ??
|
|
132
|
+
resolveComponentPrefix(buttonPrefixes, interaction.customId);
|
|
128
133
|
const context = button
|
|
129
134
|
? createContext(interaction, services)
|
|
130
135
|
: undefined;
|
|
@@ -132,6 +137,11 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
132
137
|
button?.contribution.acknowledgement === "deferred-ephemeral") {
|
|
133
138
|
await context.deferReply?.(true);
|
|
134
139
|
}
|
|
140
|
+
if (button &&
|
|
141
|
+
interaction.isButton() &&
|
|
142
|
+
button.contribution.acknowledgement === "deferred-update") {
|
|
143
|
+
await interaction.deferUpdate();
|
|
144
|
+
}
|
|
135
145
|
if (button &&
|
|
136
146
|
context &&
|
|
137
147
|
(await authorizeComponent(interaction, button.moduleId, services))) {
|
|
@@ -139,7 +149,8 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
139
149
|
}
|
|
140
150
|
}
|
|
141
151
|
else if (interaction.isModalSubmit()) {
|
|
142
|
-
const modal = modals.get(interaction.customId)
|
|
152
|
+
const modal = modals.get(interaction.customId) ??
|
|
153
|
+
resolveComponentPrefix(modalPrefixes, interaction.customId);
|
|
143
154
|
if (modal &&
|
|
144
155
|
(await authorizeComponent(interaction, modal.moduleId, services))) {
|
|
145
156
|
await modal.contribution.execute(createModalContext(interaction, services));
|
|
@@ -205,6 +216,9 @@ export function toCommandDefinition(registered) {
|
|
|
205
216
|
if (!!command.execute === !!command.subcommands?.length) {
|
|
206
217
|
throw new Error(`Command ${command.name} must define either execute or subcommands`);
|
|
207
218
|
}
|
|
219
|
+
if (command.options?.length && command.subcommands?.length) {
|
|
220
|
+
throw new Error(`Command ${command.name} cannot define both direct options and subcommands`);
|
|
221
|
+
}
|
|
208
222
|
const contextTypes = {
|
|
209
223
|
guild: InteractionContextType.Guild,
|
|
210
224
|
bot_dm: InteractionContextType.BotDM,
|
|
@@ -219,6 +233,9 @@ export function toCommandDefinition(registered) {
|
|
|
219
233
|
.setDescription(command.description)
|
|
220
234
|
.setContexts(command.contexts.map((context) => contextTypes[context]))
|
|
221
235
|
.setIntegrationTypes(command.integrationTypes.map((integrationType) => integrationTypes[integrationType]));
|
|
236
|
+
for (const option of command.options ?? []) {
|
|
237
|
+
addCommandOption(builder, option);
|
|
238
|
+
}
|
|
222
239
|
for (const subcommand of command.subcommands ?? []) {
|
|
223
240
|
builder.addSubcommand((subcommandBuilder) => {
|
|
224
241
|
subcommandBuilder
|
|
@@ -260,6 +277,13 @@ function addCommandOption(builder, option) {
|
|
|
260
277
|
.setRequired(option.required ?? false));
|
|
261
278
|
return;
|
|
262
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
|
+
}
|
|
263
287
|
if (option.type === "role") {
|
|
264
288
|
builder.addRoleOption((optionBuilder) => optionBuilder
|
|
265
289
|
.setName(option.name)
|
|
@@ -291,9 +315,43 @@ export function uniqueMap(items, keyOf, label) {
|
|
|
291
315
|
}
|
|
292
316
|
return result;
|
|
293
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
|
+
}
|
|
294
351
|
function createContext(interaction, services) {
|
|
295
352
|
return {
|
|
296
353
|
interactionId: interaction.id,
|
|
354
|
+
...("customId" in interaction ? { customId: interaction.customId } : {}),
|
|
297
355
|
userId: interaction.user.id,
|
|
298
356
|
userDisplayName: interaction.user.globalName ?? interaction.user.username,
|
|
299
357
|
userRoleIds: interaction.member instanceof GuildMember
|
|
@@ -322,6 +380,9 @@ function createContext(interaction, services) {
|
|
|
322
380
|
...(interaction.isMessageComponent()
|
|
323
381
|
? {
|
|
324
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),
|
|
325
386
|
}
|
|
326
387
|
: {}),
|
|
327
388
|
showModal: async (modal) => interaction.showModal(toModal(modal)).then(() => undefined),
|
|
@@ -336,6 +397,16 @@ function createCommandContext(interaction, services) {
|
|
|
336
397
|
...createContext(interaction, services),
|
|
337
398
|
getStringOption: (name) => interaction.options.getString(name),
|
|
338
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
|
+
},
|
|
339
410
|
getRoleOption: (name) => {
|
|
340
411
|
const role = interaction.options.getRole(name);
|
|
341
412
|
return role ? { id: role.id, name: role.name } : null;
|
|
@@ -397,6 +468,7 @@ function createModalContext(interaction, services) {
|
|
|
397
468
|
: undefined;
|
|
398
469
|
return {
|
|
399
470
|
interactionId: interaction.id,
|
|
471
|
+
customId: interaction.customId,
|
|
400
472
|
userId: interaction.user.id,
|
|
401
473
|
userDisplayName: interaction.user.globalName ?? interaction.user.username,
|
|
402
474
|
userRoleIds: interaction.member instanceof GuildMember
|
|
@@ -701,19 +773,10 @@ function toSelectRows(response) {
|
|
|
701
773
|
return rows;
|
|
702
774
|
}
|
|
703
775
|
export function toUpdateOptions(response) {
|
|
704
|
-
|
|
776
|
+
return toReplyOptions({
|
|
705
777
|
...response,
|
|
706
778
|
ephemeral: false,
|
|
707
779
|
});
|
|
708
|
-
return response.componentsV2
|
|
709
|
-
? {
|
|
710
|
-
...options,
|
|
711
|
-
// Discord requires legacy fields to be cleared when an existing
|
|
712
|
-
// message is first converted to Components V2.
|
|
713
|
-
content: null,
|
|
714
|
-
embeds: [],
|
|
715
|
-
}
|
|
716
|
-
: options;
|
|
717
780
|
}
|
|
718
781
|
function toAllowedUserMentions(userIds) {
|
|
719
782
|
if (userIds === undefined)
|
|
@@ -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
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { MessageFlags } from "discord.js";
|
|
2
|
+
const LINK_PATTERN = /https?:\/\/[^\s<>]+/giu;
|
|
3
|
+
const GIF_LINK_PATTERN = /(?:https?:\/\/[^\s<>]*(?:\.gif(?:[?#][^\s<>]*)?|giphy\.com|tenor\.com|media\.discordapp\.net[^\s<>]*\.gif))/giu;
|
|
4
|
+
const CUSTOM_EMOJI_PATTERN = /<a?:[a-zA-Z0-9_]{2,32}:\d{17,20}>/gu;
|
|
5
|
+
const UNICODE_EMOJI_PATTERN = /(?:\p{Regional_Indicator}{2}|[0-9#*]\uFE0F?\u20E3|\p{Extended_Pictographic}(?:\uFE0F|\p{Emoji_Modifier})?(?:\u200D\p{Extended_Pictographic}(?:\uFE0F|\p{Emoji_Modifier})?)*)/gu;
|
|
6
|
+
export function classifyGuildMessage(message) {
|
|
7
|
+
const kinds = new Set();
|
|
8
|
+
const content = message.content.trim();
|
|
9
|
+
const withoutLinks = content.replace(LINK_PATTERN, " ").trim();
|
|
10
|
+
const withoutEmoji = withoutLinks
|
|
11
|
+
.replace(CUSTOM_EMOJI_PATTERN, " ")
|
|
12
|
+
.replace(UNICODE_EMOJI_PATTERN, " ")
|
|
13
|
+
.replace(/\u200D|\uFE0F|\p{Emoji_Modifier}|\s/gu, "");
|
|
14
|
+
if (matches(LINK_PATTERN, content))
|
|
15
|
+
kinds.add("link");
|
|
16
|
+
if (withoutLinks.length > 0 &&
|
|
17
|
+
withoutEmoji.length === 0 &&
|
|
18
|
+
(matches(CUSTOM_EMOJI_PATTERN, withoutLinks) ||
|
|
19
|
+
matches(UNICODE_EMOJI_PATTERN, withoutLinks)))
|
|
20
|
+
kinds.add("emoji");
|
|
21
|
+
else if (withoutEmoji.length > 0)
|
|
22
|
+
kinds.add("text");
|
|
23
|
+
const voiceMessage = message.flags.has(MessageFlags.IsVoiceMessage);
|
|
24
|
+
if (voiceMessage)
|
|
25
|
+
kinds.add("voice-message");
|
|
26
|
+
for (const attachment of message.attachments.values()) {
|
|
27
|
+
const contentType = attachment.contentType?.toLowerCase() ?? "";
|
|
28
|
+
const filename = attachment.name.toLowerCase();
|
|
29
|
+
if (voiceMessage)
|
|
30
|
+
continue;
|
|
31
|
+
if (contentType === "image/gif" || filename.endsWith(".gif"))
|
|
32
|
+
kinds.add("gif");
|
|
33
|
+
else if (contentType.startsWith("image/"))
|
|
34
|
+
kinds.add("image");
|
|
35
|
+
else
|
|
36
|
+
kinds.add("file");
|
|
37
|
+
}
|
|
38
|
+
for (const embed of message.embeds) {
|
|
39
|
+
const urls = [embed.url, embed.image?.url, embed.thumbnail?.url]
|
|
40
|
+
.filter((value) => Boolean(value))
|
|
41
|
+
.join(" ");
|
|
42
|
+
if (matches(GIF_LINK_PATTERN, urls))
|
|
43
|
+
kinds.add("gif");
|
|
44
|
+
else if (embed.image || embed.thumbnail)
|
|
45
|
+
kinds.add("image");
|
|
46
|
+
}
|
|
47
|
+
if (matches(GIF_LINK_PATTERN, content))
|
|
48
|
+
kinds.add("gif");
|
|
49
|
+
if (message.stickers.size > 0)
|
|
50
|
+
kinds.add("sticker");
|
|
51
|
+
return {
|
|
52
|
+
available: true,
|
|
53
|
+
kinds: [
|
|
54
|
+
"text",
|
|
55
|
+
"emoji",
|
|
56
|
+
"link",
|
|
57
|
+
"image",
|
|
58
|
+
"gif",
|
|
59
|
+
"file",
|
|
60
|
+
"sticker",
|
|
61
|
+
"voice-message",
|
|
62
|
+
].filter((kind) => kinds.has(kind)),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export const unavailableGuildMessageClassification = {
|
|
66
|
+
available: false,
|
|
67
|
+
kinds: [],
|
|
68
|
+
};
|
|
69
|
+
function matches(pattern, value) {
|
|
70
|
+
pattern.lastIndex = 0;
|
|
71
|
+
const result = pattern.test(value);
|
|
72
|
+
pattern.lastIndex = 0;
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=message-classification.js.map
|
package/dist/modules.js
CHANGED
|
@@ -90,12 +90,21 @@ async function loadDefinitions(specifiers, importer) {
|
|
|
90
90
|
return orderedManifests.map((manifest) => byId.get(manifest.id));
|
|
91
91
|
}
|
|
92
92
|
export function validateContributionParity(definition) {
|
|
93
|
+
const requiredDiscordCapabilities = definition.requiredDiscordCapabilities ?? [];
|
|
94
|
+
if (new Set(requiredDiscordCapabilities).size !==
|
|
95
|
+
requiredDiscordCapabilities.length ||
|
|
96
|
+
requiredDiscordCapabilities.some((capability) => capability !== "guild-members" && capability !== "message-content")) {
|
|
97
|
+
throw new Error("Module requests an unsupported or duplicate Discord capability");
|
|
98
|
+
}
|
|
93
99
|
const executableCommands = (definition.interactions?.commands ?? []).map(({ name }) => name);
|
|
94
100
|
const executableEvents = [
|
|
101
|
+
...(definition.events?.privilegedMessageCreate ?? []),
|
|
102
|
+
...(definition.events?.guildMessageCreate ?? []),
|
|
95
103
|
...(definition.events?.messageReactionAdd ?? []),
|
|
96
104
|
...(definition.events?.messageReactionRemove ?? []),
|
|
97
105
|
...(definition.events?.messageDelete ?? []),
|
|
98
106
|
...(definition.events?.guildMemberAdd ?? []),
|
|
107
|
+
...(definition.events?.guildMemberRemove ?? []),
|
|
99
108
|
...(definition.events?.guildRoleDelete ?? []),
|
|
100
109
|
...(definition.events?.guildRoleUpdate ?? []),
|
|
101
110
|
...(definition.events?.channelDelete ?? []),
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type NumericWriteBehindCommittedBatch, type NumericWriteBehindEnqueueInput, type NumericWriteBehindMemberScope, type NumericWriteBehindScope, type NumericWriteBehindService } from "@helyx/sdk";
|
|
2
|
+
import { type ShardIdentity } from "./shard-contracts.js";
|
|
3
|
+
export declare class WorkerNumericWriteBehindProxy implements NumericWriteBehindService {
|
|
4
|
+
#private;
|
|
5
|
+
constructor(identity: ShardIdentity);
|
|
6
|
+
enqueue(input: NumericWriteBehindEnqueueInput): Promise<{
|
|
7
|
+
outcome: "accepted" | "cooldown" | "suppressed" | "paused" | "capacity" | "unavailable";
|
|
8
|
+
}>;
|
|
9
|
+
pendingMemberSnapshot(input: NumericWriteBehindMemberScope): Promise<{
|
|
10
|
+
increments: Readonly<Record<string, number>>;
|
|
11
|
+
maxima: Readonly<Record<string, Date>>;
|
|
12
|
+
oldestAcceptedAt: Date | null;
|
|
13
|
+
}>;
|
|
14
|
+
flushMember(input: NumericWriteBehindMemberScope): Promise<void>;
|
|
15
|
+
flushGuild(input: NumericWriteBehindScope & {
|
|
16
|
+
guildId: string;
|
|
17
|
+
}): Promise<void>;
|
|
18
|
+
flushSubject(input: NumericWriteBehindScope & {
|
|
19
|
+
subjectId: string;
|
|
20
|
+
}): Promise<void>;
|
|
21
|
+
flushModule(input: NumericWriteBehindScope): Promise<void>;
|
|
22
|
+
runMemberBarrier<Result>(input: NumericWriteBehindMemberScope, operation: () => Promise<Result>): Promise<Result>;
|
|
23
|
+
runGuildBarrier<Result>(input: NumericWriteBehindScope & {
|
|
24
|
+
guildId: string;
|
|
25
|
+
}, operation: () => Promise<Result>): Promise<Result>;
|
|
26
|
+
runSubjectBarrier<Result>(input: NumericWriteBehindScope & {
|
|
27
|
+
subjectId: string;
|
|
28
|
+
}, operation: () => Promise<Result>): Promise<Result>;
|
|
29
|
+
suppressSubject(input: NumericWriteBehindScope & {
|
|
30
|
+
subjectId: string;
|
|
31
|
+
cutoff: Date;
|
|
32
|
+
}): Promise<{
|
|
33
|
+
suppressionId: string;
|
|
34
|
+
discarded: number;
|
|
35
|
+
}>;
|
|
36
|
+
suppressGuild(input: NumericWriteBehindScope & {
|
|
37
|
+
guildId: string;
|
|
38
|
+
}): Promise<{
|
|
39
|
+
suppressionId: string;
|
|
40
|
+
discarded: number;
|
|
41
|
+
}>;
|
|
42
|
+
releaseGuild(input: NumericWriteBehindScope & {
|
|
43
|
+
guildId: string;
|
|
44
|
+
suppressionId: string;
|
|
45
|
+
}): Promise<{
|
|
46
|
+
released: boolean;
|
|
47
|
+
}>;
|
|
48
|
+
releaseSubject(input: NumericWriteBehindScope & {
|
|
49
|
+
subjectId: string;
|
|
50
|
+
suppressionId: string;
|
|
51
|
+
}): Promise<{
|
|
52
|
+
released: boolean;
|
|
53
|
+
}>;
|
|
54
|
+
clearMemberCooldown(input: NumericWriteBehindMemberScope): Promise<void>;
|
|
55
|
+
clearSubjectCooldown(input: NumericWriteBehindScope & {
|
|
56
|
+
subjectId: string;
|
|
57
|
+
}): Promise<void>;
|
|
58
|
+
subscribeCommitted(input: NumericWriteBehindScope, handler: (batch: NumericWriteBehindCommittedBatch) => void | Promise<void>): () => void;
|
|
59
|
+
stop(): void;
|
|
60
|
+
}
|
|
61
|
+
export declare function encodeNumericIpcValue(input: unknown): unknown;
|
|
62
|
+
//# sourceMappingURL=numeric-write-behind-proxy.d.ts.map
|