@helyx/bot 0.4.2 → 0.4.5
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 +35 -0
- package/README.md +13 -0
- package/dist/cloud-connector.d.ts +7 -0
- package/dist/cloud-connector.js +118 -1
- package/dist/cloud-control-dispatcher.js +84 -0
- package/dist/control-server.d.ts +187 -5
- package/dist/control-server.js +673 -6
- package/dist/developer-operations-module.js +8 -0
- package/dist/discord-owned-message-service.d.ts +39 -0
- package/dist/discord-owned-message-service.js +138 -0
- package/dist/events.d.ts +70 -1
- package/dist/events.js +557 -5
- package/dist/index.js +61 -13
- package/dist/interactions.d.ts +3 -2
- package/dist/interactions.js +66 -31
- package/dist/module-logging.d.ts +1 -0
- package/dist/module-logging.js +19 -1
- package/dist/modules.d.ts +9 -0
- package/dist/modules.js +45 -0
- package/dist/resource-authorization.d.ts +7 -0
- package/dist/resource-authorization.js +12 -0
- package/dist/scheduled-task-runner.d.ts +130 -0
- package/dist/scheduled-task-runner.js +417 -0
- package/dist/shard-cloud-proxy.d.ts +4 -0
- package/dist/shard-cloud-proxy.js +34 -0
- package/dist/shard-contracts.d.ts +574 -23
- package/dist/shard-contracts.js +346 -0
- package/dist/shard-coordinator.d.ts +5 -3
- package/dist/shard-coordinator.js +85 -2
- package/dist/shard-router.d.ts +10 -0
- package/dist/shard-router.js +58 -0
- package/dist/shard-supervisor.d.ts +2 -1
- package/dist/shard-supervisor.js +187 -5
- package/dist/shard-worker.d.ts +9 -3
- package/dist/shard-worker.js +97 -16
- package/dist/support-module.d.ts +9 -0
- package/dist/support-module.js +172 -0
- package/package.json +9 -9
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { safeError, ServiceRegistry } from "@helyx/core";
|
|
|
3
3
|
import { checkDatabaseConnection, createDatabasePool, InteractionSessionRepository, RateLimitRepository, runCoreMigrations, runConfiguredModuleMigrations, } from "@helyx/database";
|
|
4
4
|
import { createRuntime } from "@helyx/framework";
|
|
5
5
|
import { createPlatformServices, PostgresPrivacyProviderExecutor, } from "@helyx/platform";
|
|
6
|
-
import { HELYX_SERVICE_NAMES } from "@helyx/sdk";
|
|
6
|
+
import { HELYX_SERVICE_NAMES, } from "@helyx/sdk";
|
|
7
7
|
import { Client, Events, GatewayIntentBits, Partials } from "discord.js";
|
|
8
8
|
import pino from "pino";
|
|
9
9
|
import { randomUUID } from "node:crypto";
|
|
@@ -14,17 +14,21 @@ import { ClientCloudDiscordDirectory, CloudConnectorController, } from "./cloud-
|
|
|
14
14
|
import { createCloudManagementModule } from "./cloud-management-module.js";
|
|
15
15
|
import { installInteractions } from "./interactions.js";
|
|
16
16
|
import { installGuildTracking } from "./guilds.js";
|
|
17
|
-
import { loadConfiguredModulePackages } from "./modules.js";
|
|
17
|
+
import { loadConfiguredModulePackages, platformServiceOptions, } from "./modules.js";
|
|
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 { installModuleEvents } from "./events.js";
|
|
21
|
+
import { 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";
|
|
25
25
|
import { parseShardWorkerLaunchContract, startShardWorker, } from "./shard-worker.js";
|
|
26
26
|
import { createDeveloperOperationsModule } from "./developer-operations-module.js";
|
|
27
27
|
import { shardHealthSnapshotSchema } from "./shard-contracts.js";
|
|
28
|
+
import { createSupportModule } from "./support-module.js";
|
|
29
|
+
import { DiscordOwnedMessageDeletionService } from "./discord-owned-message-service.js";
|
|
30
|
+
import { CoreResourceAuthorizationService } from "./resource-authorization.js";
|
|
31
|
+
import { createLocalScheduledTaskRoute, createScheduledTaskRunner, schedulerOperationalHealth, } from "./scheduled-task-runner.js";
|
|
28
32
|
export { loadConfiguredModulePackages } from "./modules.js";
|
|
29
33
|
export { resolveModulePackageSpecifiers } from "./module-configuration.js";
|
|
30
34
|
export * from "./control-server.js";
|
|
@@ -143,12 +147,15 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
143
147
|
configuredOwnerUserIds: environment.HELYX_OWNER_USER_IDS,
|
|
144
148
|
})
|
|
145
149
|
: undefined;
|
|
150
|
+
const supportModule = createSupportModule(cloudConnector);
|
|
146
151
|
const publicModules = [
|
|
147
152
|
managementModule,
|
|
148
153
|
permissionsModule,
|
|
154
|
+
supportModule,
|
|
149
155
|
...(cloudManagementModule ? [cloudManagementModule] : []),
|
|
150
156
|
...featureModules,
|
|
151
157
|
];
|
|
158
|
+
let scheduledTasks;
|
|
152
159
|
const unshardedSupervisorId = randomUUID();
|
|
153
160
|
const deployedCommit = deploymentReference(processEnvironment);
|
|
154
161
|
const operationsModule = environment.DISCORD_INTERNAL_GUILD_ID
|
|
@@ -160,6 +167,7 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
160
167
|
coreFunctions: [
|
|
161
168
|
managementModule,
|
|
162
169
|
permissionsModule,
|
|
170
|
+
supportModule,
|
|
163
171
|
...(cloudManagementModule ? [cloudManagementModule] : []),
|
|
164
172
|
].map(({ manifest }) => manifest),
|
|
165
173
|
modules: featureModules.map(({ manifest }) => manifest),
|
|
@@ -174,6 +182,12 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
174
182
|
databaseHealthy = false;
|
|
175
183
|
}
|
|
176
184
|
const ready = client.isReady();
|
|
185
|
+
const scheduler = scheduledTasks
|
|
186
|
+
? await scheduledTasks
|
|
187
|
+
.getHealth()
|
|
188
|
+
.then(schedulerOperationalHealth)
|
|
189
|
+
.catch(() => ({ status: "unavailable" }))
|
|
190
|
+
: { status: "unavailable" };
|
|
177
191
|
const observedAt = new Date().toISOString();
|
|
178
192
|
const gatewayPingMs = Number.isFinite(client.ws.ping)
|
|
179
193
|
? client.ws.ping
|
|
@@ -182,7 +196,7 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
182
196
|
version: 1,
|
|
183
197
|
mode: "off",
|
|
184
198
|
scope: "local",
|
|
185
|
-
status: ready && databaseHealthy
|
|
199
|
+
status: ready && databaseHealthy && scheduler.status === "available"
|
|
186
200
|
? "healthy"
|
|
187
201
|
: ready
|
|
188
202
|
? "degraded"
|
|
@@ -196,6 +210,7 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
196
210
|
redis: "not_required",
|
|
197
211
|
database: databaseHealthy ? "healthy" : "unavailable",
|
|
198
212
|
},
|
|
213
|
+
scheduler,
|
|
199
214
|
shards: [
|
|
200
215
|
{
|
|
201
216
|
shardId: 0,
|
|
@@ -219,7 +234,11 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
219
234
|
...publicModules,
|
|
220
235
|
...(operationsModule ? [operationsModule] : []),
|
|
221
236
|
];
|
|
222
|
-
const platformServices = new Map(createPlatformServices(database, publicModules));
|
|
237
|
+
const platformServices = new Map(createPlatformServices(database, publicModules, platformServiceOptions(environment, publicModules)));
|
|
238
|
+
platformServices.set(HELYX_SERVICE_NAMES.resourceAuthorization, new CoreResourceAuthorizationService(platformServices.get(HELYX_SERVICE_NAMES.permissions)));
|
|
239
|
+
platformServices.set(HELYX_SERVICE_NAMES.discordOwnedMessages, new DiscordOwnedMessageDeletionService(client));
|
|
240
|
+
platformServices.set(HELYX_SERVICE_NAMES.memberRoles, new DiscordMemberRoleService(client));
|
|
241
|
+
platformServices.set(HELYX_SERVICE_NAMES.discordResources, new DiscordResourceInspection(client));
|
|
223
242
|
if (featureModules.some(({ manifest }) => manifest.id === "helyx.logging")) {
|
|
224
243
|
platformServices.set(HELYX_SERVICE_NAMES.moduleLogging, new DiscordModuleLoggingService(client, database, publicModules, bootstrapLogger));
|
|
225
244
|
}
|
|
@@ -232,6 +251,7 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
232
251
|
const enabledCoreModuleIds = new Set([
|
|
233
252
|
managementModule.manifest.id,
|
|
234
253
|
permissionsModule.manifest.id,
|
|
254
|
+
supportModule.manifest.id,
|
|
235
255
|
...(cloudManagementModule ? [cloudManagementModule.manifest.id] : []),
|
|
236
256
|
...(operationsModule ? [operationsModule.manifest.id] : []),
|
|
237
257
|
]);
|
|
@@ -241,13 +261,10 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
241
261
|
? { internalGuildId: environment.DISCORD_INTERNAL_GUILD_ID }
|
|
242
262
|
: {}),
|
|
243
263
|
prepare: prepareGuilds,
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
]),
|
|
249
|
-
}
|
|
250
|
-
: {}),
|
|
264
|
+
selfAuthorizingGuildModuleIds: new Set([
|
|
265
|
+
supportModule.manifest.id,
|
|
266
|
+
...(operationsModule ? [operationsModule.manifest.id] : []),
|
|
267
|
+
]),
|
|
251
268
|
}, bootstrapLogger, serviceAccess);
|
|
252
269
|
const moduleEvents = installModuleEvents(client, publicModules, bootstrapLogger, serviceAccess);
|
|
253
270
|
const controlService = new BotControlService(database, publicModules, guildResources, serviceAccess, privacyDatabase
|
|
@@ -256,6 +273,12 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
256
273
|
executorPool: privacyDatabase,
|
|
257
274
|
modules: featureModules,
|
|
258
275
|
})
|
|
276
|
+
: undefined, environment.DISCORD_INTERNAL_GUILD_ID &&
|
|
277
|
+
environment.HELYX_SUPPORT_STAFF_CHANNEL_ID
|
|
278
|
+
? {
|
|
279
|
+
guildId: environment.DISCORD_INTERNAL_GUILD_ID,
|
|
280
|
+
channelId: environment.HELYX_SUPPORT_STAFF_CHANNEL_ID,
|
|
281
|
+
}
|
|
259
282
|
: undefined);
|
|
260
283
|
cloudConnector.setControlHandler(controlService);
|
|
261
284
|
let controlServer;
|
|
@@ -268,6 +291,10 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
268
291
|
process.off("SIGTERM", onSigterm);
|
|
269
292
|
if (retentionCleanupTimer)
|
|
270
293
|
clearInterval(retentionCleanupTimer);
|
|
294
|
+
await scheduledTasks?.stop().catch((error) => {
|
|
295
|
+
process.exitCode = 1;
|
|
296
|
+
bootstrapLogger.error({ event: "scheduler.stop_failed", error: safeError(error) }, "Scheduled task runner shutdown failed");
|
|
297
|
+
});
|
|
271
298
|
bootstrapLogger.info({ event: "bot.shutdown", reason }, "Bot shutdown requested");
|
|
272
299
|
await cloudConnector.stop().catch((error) => {
|
|
273
300
|
process.exitCode = 1;
|
|
@@ -300,6 +327,18 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
300
327
|
await runtime.start();
|
|
301
328
|
await client.login(environment.DISCORD_TOKEN);
|
|
302
329
|
await commandsReady;
|
|
330
|
+
scheduledTasks = createScheduledTaskRunner({
|
|
331
|
+
database,
|
|
332
|
+
modules: publicModules,
|
|
333
|
+
route: createLocalScheduledTaskRoute({
|
|
334
|
+
modules: publicModules,
|
|
335
|
+
services: serviceAccess,
|
|
336
|
+
logger: bootstrapLogger,
|
|
337
|
+
}),
|
|
338
|
+
logger: bootstrapLogger,
|
|
339
|
+
workerId: `unsharded:${unshardedSupervisorId}`,
|
|
340
|
+
});
|
|
341
|
+
scheduledTasks.start();
|
|
303
342
|
await cloudConnector.start();
|
|
304
343
|
if (environment.HELYX_BOT_CONTROL_SECRET) {
|
|
305
344
|
controlServer = await createBotControlServer({
|
|
@@ -321,6 +360,7 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
321
360
|
}
|
|
322
361
|
}
|
|
323
362
|
catch (error) {
|
|
363
|
+
await scheduledTasks?.stop().catch(() => undefined);
|
|
324
364
|
await cloudConnector.stop().catch(() => undefined);
|
|
325
365
|
if (controlServer)
|
|
326
366
|
await controlServer.close().catch(() => undefined);
|
|
@@ -369,12 +409,20 @@ function deploymentReference(processEnvironment) {
|
|
|
369
409
|
}
|
|
370
410
|
export function gatewayIntentsForModules(modules) {
|
|
371
411
|
const intents = [GatewayIntentBits.Guilds];
|
|
372
|
-
if (modules.some((module) => module.events?.messageReactionAdd?.length
|
|
412
|
+
if (modules.some((module) => module.events?.messageReactionAdd?.length ||
|
|
413
|
+
module.events?.messageReactionRemove?.length)) {
|
|
373
414
|
intents.push(GatewayIntentBits.GuildMessageReactions);
|
|
374
415
|
}
|
|
416
|
+
if (modules.some((module) => module.events?.messageDelete?.length)) {
|
|
417
|
+
intents.push(GatewayIntentBits.GuildMessages);
|
|
418
|
+
}
|
|
375
419
|
if (modules.some((module) => module.events?.guildMemberAdd?.length)) {
|
|
376
420
|
intents.push(GatewayIntentBits.GuildMembers);
|
|
377
421
|
}
|
|
422
|
+
if (modules.some((module) => module.events?.guildEmojiDelete?.length ||
|
|
423
|
+
module.events?.guildEmojiUpdate?.length)) {
|
|
424
|
+
intents.push(GatewayIntentBits.GuildExpressions);
|
|
425
|
+
}
|
|
378
426
|
return intents;
|
|
379
427
|
}
|
|
380
428
|
if (process.argv[1] &&
|
package/dist/interactions.d.ts
CHANGED
|
@@ -21,12 +21,13 @@ export declare function toCommandDefinition(registered: {
|
|
|
21
21
|
contribution: CommandContribution;
|
|
22
22
|
}): import("discord.js").RESTPostAPIChatInputApplicationCommandsJSONBody;
|
|
23
23
|
export declare function uniqueMap<T>(items: readonly T[], keyOf: (item: T) => string, label: string): Map<string, T>;
|
|
24
|
-
export declare function sendToChannel(client: Client, channelId: string, response: MessageResponse): Promise<{
|
|
24
|
+
export declare function sendToChannel(client: Client, channelId: string, response: MessageResponse, deliveryId?: string): Promise<{
|
|
25
25
|
messageId: string;
|
|
26
26
|
}>;
|
|
27
27
|
export declare function deleteChannelMessage(client: Client, channelId: string, messageId: string): Promise<void>;
|
|
28
28
|
export declare function editChannelMessage(client: Client, channelId: string, messageId: string, response: MessageResponse): Promise<void>;
|
|
29
|
-
export declare function
|
|
29
|
+
export declare function addChannelMessageReaction(client: Client, channelId: string, messageId: string, emojiIdentifier: string): Promise<void>;
|
|
30
|
+
export declare function authorizeCommand(interaction: ChatInputCommandInteraction, moduleId: string, commandId: string, rateLimit: CommandContribution["rateLimit"], services: ServiceAccess, selfAuthorizingGuildCommand: boolean, resourceScopedCommand?: boolean): Promise<boolean>;
|
|
30
31
|
export declare function normalizeDiscordPermission(permission: string): string;
|
|
31
32
|
export declare function toReplyOptions(response: MessageResponse): InteractionReplyOptions;
|
|
32
33
|
export declare function toUpdateOptions(response: MessageResponse): InteractionUpdateOptions;
|
package/dist/interactions.js
CHANGED
|
@@ -98,8 +98,9 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
98
98
|
const commandId = subcommandName
|
|
99
99
|
? `${command.name}.${subcommandName}`
|
|
100
100
|
: command.name;
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
const permissionId = subcommand?.permissionId ?? command.permissionId ?? commandId;
|
|
102
|
+
if (await authorizeCommand(interaction, moduleId, permissionId, subcommand?.rateLimit ?? command.rateLimit, services, registration.selfAuthorizingGuildModuleIds?.has(moduleId) ??
|
|
103
|
+
false, subcommand?.authorization === "resource-scoped")) {
|
|
103
104
|
const context = createCommandContext(interaction, services);
|
|
104
105
|
await execute(context);
|
|
105
106
|
}
|
|
@@ -131,9 +132,17 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
131
132
|
interaction.isChannelSelectMenu() ||
|
|
132
133
|
interaction.isRoleSelectMenu()) {
|
|
133
134
|
const select = selects.get(interaction.customId);
|
|
135
|
+
const context = select
|
|
136
|
+
? createSelectContext(interaction, services)
|
|
137
|
+
: undefined;
|
|
138
|
+
if (context &&
|
|
139
|
+
select?.contribution.acknowledgement === "deferred-ephemeral") {
|
|
140
|
+
await context.deferReply?.(true);
|
|
141
|
+
}
|
|
134
142
|
if (select &&
|
|
143
|
+
context &&
|
|
135
144
|
(await authorizeComponent(interaction, select.moduleId, services))) {
|
|
136
|
-
await select.contribution.execute(
|
|
145
|
+
await select.contribution.execute(context);
|
|
137
146
|
}
|
|
138
147
|
}
|
|
139
148
|
}
|
|
@@ -269,6 +278,10 @@ function createContext(interaction, services) {
|
|
|
269
278
|
userRoleIds: interaction.member instanceof GuildMember
|
|
270
279
|
? [...interaction.member.roles.cache.keys()]
|
|
271
280
|
: (interaction.member?.roles ?? []),
|
|
281
|
+
userPermissions: interaction.memberPermissions
|
|
282
|
+
?.toArray()
|
|
283
|
+
.map(normalizeDiscordPermission) ?? [],
|
|
284
|
+
isServerOwner: interaction.guild?.ownerId === interaction.user.id,
|
|
272
285
|
isAdministrator: interaction.memberPermissions?.has(PermissionFlagsBits.Administrator) ??
|
|
273
286
|
false,
|
|
274
287
|
...(interaction.guildId ? { serverId: interaction.guildId } : {}),
|
|
@@ -331,6 +344,10 @@ function createModalContext(interaction, services) {
|
|
|
331
344
|
userRoleIds: interaction.member instanceof GuildMember
|
|
332
345
|
? [...interaction.member.roles.cache.keys()]
|
|
333
346
|
: (interaction.member?.roles ?? []),
|
|
347
|
+
userPermissions: interaction.memberPermissions
|
|
348
|
+
?.toArray()
|
|
349
|
+
.map(normalizeDiscordPermission) ?? [],
|
|
350
|
+
isServerOwner: interaction.guild?.ownerId === interaction.user.id,
|
|
334
351
|
isAdministrator: interaction.memberPermissions?.has(PermissionFlagsBits.Administrator) ??
|
|
335
352
|
false,
|
|
336
353
|
...(interaction.guildId ? { serverId: interaction.guildId } : {}),
|
|
@@ -376,12 +393,20 @@ function createSelectContext(interaction, services) {
|
|
|
376
393
|
selectedValues: interaction.values,
|
|
377
394
|
};
|
|
378
395
|
}
|
|
379
|
-
export async function sendToChannel(client, channelId, response) {
|
|
396
|
+
export async function sendToChannel(client, channelId, response, deliveryId) {
|
|
380
397
|
const channel = await client.channels.fetch(channelId);
|
|
381
398
|
if (!channel?.isSendable())
|
|
382
399
|
throw new Error("Configured channel is not sendable");
|
|
383
400
|
const options = toReplyOptions({ ...response, ephemeral: false });
|
|
384
|
-
const message = await channel.send(
|
|
401
|
+
const message = await channel.send({
|
|
402
|
+
...options,
|
|
403
|
+
...(deliveryId
|
|
404
|
+
? {
|
|
405
|
+
nonce: deliveryId.replaceAll("-", "").slice(0, 25),
|
|
406
|
+
enforceNonce: true,
|
|
407
|
+
}
|
|
408
|
+
: {}),
|
|
409
|
+
});
|
|
385
410
|
return { messageId: message.id };
|
|
386
411
|
}
|
|
387
412
|
export async function deleteChannelMessage(client, channelId, messageId) {
|
|
@@ -406,7 +431,7 @@ async function replyToInteraction(interaction, response) {
|
|
|
406
431
|
}
|
|
407
432
|
await interaction.reply(toReplyOptions(response));
|
|
408
433
|
}
|
|
409
|
-
async function addChannelMessageReaction(client, channelId, messageId, emojiIdentifier) {
|
|
434
|
+
export async function addChannelMessageReaction(client, channelId, messageId, emojiIdentifier) {
|
|
410
435
|
const channel = await client.channels.fetch(channelId);
|
|
411
436
|
if (!channel?.isTextBased()) {
|
|
412
437
|
throw new Error("Configured channel cannot contain messages");
|
|
@@ -431,32 +456,33 @@ async function replyEphemeralText(interaction, content) {
|
|
|
431
456
|
}
|
|
432
457
|
await interaction.reply({ content, flags: MessageFlags.Ephemeral });
|
|
433
458
|
}
|
|
434
|
-
export async function authorizeCommand(interaction, moduleId, commandId, rateLimit, services, selfAuthorizingGuildCommand) {
|
|
459
|
+
export async function authorizeCommand(interaction, moduleId, commandId, rateLimit, services, selfAuthorizingGuildCommand, resourceScopedCommand = false) {
|
|
435
460
|
if (!interaction.guildId)
|
|
436
461
|
return true;
|
|
437
462
|
if (!selfAuthorizingGuildCommand) {
|
|
438
463
|
if (!(await authorizeComponent(interaction, moduleId, services)))
|
|
439
464
|
return false;
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
false,
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
465
|
+
if (!resourceScopedCommand) {
|
|
466
|
+
const permissions = services.get(HELYX_SERVICE_NAMES.permissions);
|
|
467
|
+
const policy = await permissions.getEffectivePolicy(interaction.guildId, moduleId, commandId);
|
|
468
|
+
const actor = {
|
|
469
|
+
userId: interaction.user.id,
|
|
470
|
+
roleIds: interaction.member instanceof GuildMember
|
|
471
|
+
? [...interaction.member.roles.cache.keys()]
|
|
472
|
+
: (interaction.member?.roles ?? []),
|
|
473
|
+
permissions: interaction.memberPermissions
|
|
474
|
+
?.toArray()
|
|
475
|
+
.map(normalizeDiscordPermission) ?? [],
|
|
476
|
+
isServerOwner: interaction.guild?.ownerId === interaction.user.id,
|
|
477
|
+
isAdministrator: interaction.memberPermissions?.has(PermissionFlagsBits.Administrator) ?? false,
|
|
478
|
+
};
|
|
479
|
+
if (!canUseCommand(actor, policy)) {
|
|
480
|
+
await interaction.reply({
|
|
481
|
+
content: "You do not have permission to use this command.",
|
|
482
|
+
flags: MessageFlags.Ephemeral,
|
|
483
|
+
});
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
460
486
|
}
|
|
461
487
|
}
|
|
462
488
|
if (rateLimit) {
|
|
@@ -517,6 +543,7 @@ export function toReplyOptions(response) {
|
|
|
517
543
|
...(button.emojiIdentifier
|
|
518
544
|
? { emoji: toButtonEmoji(button.emojiIdentifier) }
|
|
519
545
|
: {}),
|
|
546
|
+
...(button.disabled ? { disabled: true } : {}),
|
|
520
547
|
})),
|
|
521
548
|
},
|
|
522
549
|
]
|
|
@@ -542,6 +569,9 @@ export function toReplyOptions(response) {
|
|
|
542
569
|
if (button.emojiIdentifier) {
|
|
543
570
|
builder.setEmoji(toButtonEmoji(button.emojiIdentifier));
|
|
544
571
|
}
|
|
572
|
+
if (button.disabled !== undefined) {
|
|
573
|
+
builder.setDisabled(button.disabled);
|
|
574
|
+
}
|
|
545
575
|
return builder;
|
|
546
576
|
})),
|
|
547
577
|
]
|
|
@@ -576,7 +606,7 @@ function toSelectRows(response) {
|
|
|
576
606
|
.setMaxValues(select.maxValues ?? 1)));
|
|
577
607
|
continue;
|
|
578
608
|
}
|
|
579
|
-
|
|
609
|
+
const builder = new StringSelectMenuBuilder()
|
|
580
610
|
.setCustomId(select.customId)
|
|
581
611
|
.setPlaceholder(select.placeholder)
|
|
582
612
|
.setMinValues(select.minValues ?? 1)
|
|
@@ -584,10 +614,15 @@ function toSelectRows(response) {
|
|
|
584
614
|
.addOptions(select.options.map((option) => ({
|
|
585
615
|
label: option.label,
|
|
586
616
|
value: option.value,
|
|
587
|
-
...(option.description
|
|
588
|
-
|
|
617
|
+
...(option.description ? { description: option.description } : {}),
|
|
618
|
+
...(option.emojiIdentifier
|
|
619
|
+
? { emoji: toButtonEmoji(option.emojiIdentifier) }
|
|
589
620
|
: {}),
|
|
590
|
-
})))
|
|
621
|
+
})));
|
|
622
|
+
if (select.disabled !== undefined) {
|
|
623
|
+
builder.setDisabled(select.disabled);
|
|
624
|
+
}
|
|
625
|
+
rows.push(new ActionRowBuilder().addComponents(builder));
|
|
591
626
|
}
|
|
592
627
|
return rows;
|
|
593
628
|
}
|
package/dist/module-logging.d.ts
CHANGED
package/dist/module-logging.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { InstallationRepository, ModuleConfigurationRepository, ModuleLoggingPolicyRepository, } from "@helyx/database";
|
|
2
2
|
import { toReplyOptions } from "./interactions.js";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
3
4
|
const LOGGING_MODULE_ID = "helyx.logging";
|
|
4
5
|
const VIOLET = 0x7c3aed;
|
|
5
6
|
export class DiscordModuleLoggingService {
|
|
@@ -68,7 +69,13 @@ export class DiscordModuleLoggingService {
|
|
|
68
69
|
],
|
|
69
70
|
},
|
|
70
71
|
});
|
|
71
|
-
|
|
72
|
+
const delivery = input.idempotencyKey
|
|
73
|
+
? loggingDeliveryNonce(input)
|
|
74
|
+
: undefined;
|
|
75
|
+
await channel.send({
|
|
76
|
+
...response,
|
|
77
|
+
...(delivery ? { nonce: delivery, enforceNonce: true } : {}),
|
|
78
|
+
});
|
|
72
79
|
return { delivered: true };
|
|
73
80
|
}
|
|
74
81
|
catch (error) {
|
|
@@ -83,6 +90,17 @@ export class DiscordModuleLoggingService {
|
|
|
83
90
|
}
|
|
84
91
|
}
|
|
85
92
|
}
|
|
93
|
+
function loggingDeliveryNonce(input) {
|
|
94
|
+
if (!input.idempotencyKey)
|
|
95
|
+
return undefined;
|
|
96
|
+
if (input.idempotencyKey.length > 200 ||
|
|
97
|
+
!/^[A-Za-z0-9][A-Za-z0-9._:-]*$/u.test(input.idempotencyKey))
|
|
98
|
+
throw new Error("Module logging idempotency key is invalid");
|
|
99
|
+
return createHash("sha256")
|
|
100
|
+
.update(`${input.guildId}\u0000${input.moduleId}\u0000${input.eventId}\u0000${input.idempotencyKey}`)
|
|
101
|
+
.digest("hex")
|
|
102
|
+
.slice(0, 25);
|
|
103
|
+
}
|
|
86
104
|
function normalizeText(value, maximum, label) {
|
|
87
105
|
const normalized = value.normalize("NFKC").trim();
|
|
88
106
|
if (normalized.length === 0 || normalized.length > maximum) {
|
package/dist/modules.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BotEnvironment } from "@helyx/config";
|
|
1
2
|
import type { ModuleDefinition } from "@helyx/sdk";
|
|
2
3
|
type ModuleImporter = (specifier: string) => Promise<unknown>;
|
|
3
4
|
type PackageJsonResolver = (specifier: string) => string;
|
|
@@ -9,6 +10,14 @@ interface LoadedDefinition {
|
|
|
9
10
|
export interface ConfiguredModulePackage extends LoadedDefinition {
|
|
10
11
|
packageRoot: string;
|
|
11
12
|
}
|
|
13
|
+
export declare const secureRandomSelectionWorkerKeyName = "HELYX_INTERNAL_SECURE_RANDOM_SELECTION_KEY";
|
|
14
|
+
interface PlatformServiceEnvironment extends Pick<BotEnvironment, "HELYX_DATA_ENCRYPTION_KEY"> {
|
|
15
|
+
HELYX_INTERNAL_SECURE_RANDOM_SELECTION_KEY?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function deriveSecureRandomSelectionKey(encodedDataEncryptionKey: string): Uint8Array;
|
|
18
|
+
export declare function platformServiceOptions(environment: PlatformServiceEnvironment, modules: readonly ModuleDefinition[]): {
|
|
19
|
+
secureRandomSelectionKey?: Uint8Array;
|
|
20
|
+
};
|
|
12
21
|
export declare function loadConfiguredModules(specifiers: readonly string[], importer?: ModuleImporter): Promise<ModuleDefinition[]>;
|
|
13
22
|
export declare function loadConfiguredModulePackages(specifiers: readonly string[], importer?: ModuleImporter, resolvePackageJson?: PackageJsonResolver, readPackageJson?: PackageJsonReader): Promise<ConfiguredModulePackage[]>;
|
|
14
23
|
export declare function validateContributionParity(definition: ModuleDefinition): void;
|
package/dist/modules.js
CHANGED
|
@@ -1,8 +1,43 @@
|
|
|
1
1
|
import { orderModuleManifests, parseModuleManifest, } from "@helyx/module-manifest";
|
|
2
|
+
import { createHmac } from "node:crypto";
|
|
2
3
|
import { readFile } from "node:fs/promises";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
4
5
|
import { dirname } from "node:path";
|
|
5
6
|
const officialModulePackage = /^@helyx\/module-[a-z0-9][a-z0-9-]*$/u;
|
|
7
|
+
const giveawaysModuleId = "helyx.giveaways";
|
|
8
|
+
export const secureRandomSelectionWorkerKeyName = "HELYX_INTERNAL_SECURE_RANDOM_SELECTION_KEY";
|
|
9
|
+
const secureRandomSelectionKeyContext = "helyx.secure-random-selection.worker.v1";
|
|
10
|
+
function decodeSecureRandomSelectionKey(encodedKey, variableName) {
|
|
11
|
+
const key = Buffer.from(encodedKey, "base64");
|
|
12
|
+
if (key.byteLength !== 32) {
|
|
13
|
+
throw new Error(`${variableName} must contain exactly 32 bytes`);
|
|
14
|
+
}
|
|
15
|
+
return key;
|
|
16
|
+
}
|
|
17
|
+
export function deriveSecureRandomSelectionKey(encodedDataEncryptionKey) {
|
|
18
|
+
const dataEncryptionKey = decodeSecureRandomSelectionKey(encodedDataEncryptionKey, "HELYX_DATA_ENCRYPTION_KEY");
|
|
19
|
+
return createHmac("sha256", dataEncryptionKey)
|
|
20
|
+
.update(secureRandomSelectionKeyContext, "utf8")
|
|
21
|
+
.digest();
|
|
22
|
+
}
|
|
23
|
+
export function platformServiceOptions(environment, modules) {
|
|
24
|
+
const internalSelectionKey = environment.HELYX_INTERNAL_SECURE_RANDOM_SELECTION_KEY;
|
|
25
|
+
if (internalSelectionKey) {
|
|
26
|
+
return {
|
|
27
|
+
secureRandomSelectionKey: decodeSecureRandomSelectionKey(internalSelectionKey, secureRandomSelectionWorkerKeyName),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const encodedKey = environment.HELYX_DATA_ENCRYPTION_KEY;
|
|
31
|
+
if (!encodedKey) {
|
|
32
|
+
if (modules.some(({ manifest }) => manifest.id === giveawaysModuleId)) {
|
|
33
|
+
throw new Error("HELYX_DATA_ENCRYPTION_KEY is required when Giveaways is loaded");
|
|
34
|
+
}
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
secureRandomSelectionKey: deriveSecureRandomSelectionKey(encodedKey),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
6
41
|
function isModuleDefinition(value) {
|
|
7
42
|
if (typeof value !== "object" || value === null)
|
|
8
43
|
return false;
|
|
@@ -58,14 +93,24 @@ export function validateContributionParity(definition) {
|
|
|
58
93
|
const executableCommands = (definition.interactions?.commands ?? []).map(({ name }) => name);
|
|
59
94
|
const executableEvents = [
|
|
60
95
|
...(definition.events?.messageReactionAdd ?? []),
|
|
96
|
+
...(definition.events?.messageReactionRemove ?? []),
|
|
97
|
+
...(definition.events?.messageDelete ?? []),
|
|
61
98
|
...(definition.events?.guildMemberAdd ?? []),
|
|
99
|
+
...(definition.events?.guildRoleDelete ?? []),
|
|
100
|
+
...(definition.events?.guildRoleUpdate ?? []),
|
|
101
|
+
...(definition.events?.channelDelete ?? []),
|
|
102
|
+
...(definition.events?.channelUpdate ?? []),
|
|
103
|
+
...(definition.events?.guildEmojiDelete ?? []),
|
|
104
|
+
...(definition.events?.guildEmojiUpdate ?? []),
|
|
62
105
|
].map(({ type }) => type);
|
|
63
106
|
const executableLogs = (definition.logging?.events ?? []).map(({ id }) => id);
|
|
64
107
|
const executableDashboardActions = (definition.dashboardActions ?? []).map(({ id }) => id);
|
|
108
|
+
const executableManagedResources = (definition.managedResources ?? []).map(({ id }) => id);
|
|
65
109
|
assertSameContributionIds(definition.manifest.contributions.commands, executableCommands, "commands");
|
|
66
110
|
assertSameContributionIds(definition.manifest.contributions.events, executableEvents, "events");
|
|
67
111
|
assertSameContributionIds(definition.manifest.contributions.logs ?? [], executableLogs, "logs");
|
|
68
112
|
assertSameContributionIds(definition.manifest.dashboard?.actions?.map(({ id }) => id) ?? [], executableDashboardActions, "dashboard actions");
|
|
113
|
+
assertSameContributionIds(definition.manifest.dashboard?.resources?.map(({ id }) => id) ?? [], executableManagedResources, "dashboard resources");
|
|
69
114
|
validatePrivacyParity(definition);
|
|
70
115
|
}
|
|
71
116
|
function validatePrivacyParity(definition) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PermissionPolicyService, ResourceAuthorizationService } from "@helyx/sdk";
|
|
2
|
+
export declare class CoreResourceAuthorizationService implements ResourceAuthorizationService {
|
|
3
|
+
#private;
|
|
4
|
+
constructor(permissions: PermissionPolicyService);
|
|
5
|
+
canUse(input: Parameters<ResourceAuthorizationService["canUse"]>[0]): Promise<boolean>;
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=resource-authorization.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { canUseCommand } from "@helyx/core";
|
|
2
|
+
export class CoreResourceAuthorizationService {
|
|
3
|
+
#permissions;
|
|
4
|
+
constructor(permissions) {
|
|
5
|
+
this.#permissions = permissions;
|
|
6
|
+
}
|
|
7
|
+
async canUse(input) {
|
|
8
|
+
const policy = await this.#permissions.getEffectivePolicy(input.guildId, input.moduleId, input.permissionId);
|
|
9
|
+
return canUseCommand(input.actor, policy);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=resource-authorization.js.map
|