@helyx/bot 0.4.36 → 0.4.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +17 -2
- package/dist/cloud-management-module.js +275 -206
- package/dist/command-registration-policy.d.ts +8 -0
- package/dist/command-registration-policy.js +39 -0
- package/dist/command-sync.d.ts +5 -1
- package/dist/command-sync.js +67 -3
- package/dist/control-server.d.ts +15 -3
- package/dist/control-server.js +34 -48
- package/dist/dashboard-commands.js +5 -1
- package/dist/developer-operations-module.d.ts +2 -0
- package/dist/developer-operations-module.js +65 -4
- package/dist/discord-guild-health.d.ts +2 -2
- package/dist/discord-rest.d.ts +5 -0
- package/dist/discord-rest.js +9 -0
- package/dist/events.js +4 -2
- package/dist/first-time-command-setup.d.ts +25 -0
- package/dist/first-time-command-setup.js +22 -0
- package/dist/guild-command-control-routes.d.ts +5 -0
- package/dist/guild-command-control-routes.js +191 -0
- package/dist/guild-command-discord-client.d.ts +21 -0
- package/dist/guild-command-discord-client.js +50 -0
- package/dist/guild-command-projection.d.ts +46 -0
- package/dist/guild-command-projection.js +254 -0
- package/dist/guild-command-read-service.d.ts +201 -0
- package/dist/guild-command-read-service.js +272 -0
- package/dist/guild-command-release.d.ts +19 -0
- package/dist/guild-command-release.js +47 -0
- package/dist/guild-command-worker.d.ts +49 -0
- package/dist/guild-command-worker.js +443 -0
- package/dist/guilds.d.ts +1 -1
- package/dist/guilds.js +41 -26
- package/dist/index.js +75 -4
- package/dist/interactions.d.ts +15 -4
- package/dist/interactions.js +285 -70
- package/dist/management-module.js +117 -41
- package/dist/module-logging.d.ts +1 -0
- package/dist/module-logging.js +5 -5
- package/dist/module-status-control.d.ts +19 -0
- package/dist/module-status-control.js +69 -0
- package/dist/modules.d.ts +2 -0
- package/dist/modules.js +21 -2
- package/dist/nexus-commands.d.ts +1 -12
- package/dist/nexus-commands.js +1 -99
- package/dist/oauth-module.js +134 -88
- package/dist/permissions-module.js +112 -39
- package/dist/privacy-module.d.ts +2 -0
- package/dist/privacy-module.js +60 -0
- package/dist/scheduled-task-runner.js +36 -1
- package/dist/shard-contracts.d.ts +24 -0
- package/dist/shard-contracts.js +20 -0
- package/dist/shard-router.d.ts +1 -0
- package/dist/shard-router.js +7 -0
- package/dist/shard-supervisor.d.ts +2 -1
- package/dist/shard-supervisor.js +94 -16
- package/dist/shard-worker.d.ts +6 -1
- package/dist/shard-worker.js +9 -1
- package/dist/shared-commands.d.ts +13 -0
- package/dist/shared-commands.js +163 -0
- package/dist/support-module.js +51 -51
- package/dist/trivia-data-provider.d.ts +26 -0
- package/dist/trivia-data-provider.js +51 -0
- package/package.json +5 -4
- package/dist/oauth-module.test.d.ts +0 -2
- package/dist/oauth-module.test.js +0 -172
|
@@ -208,6 +208,14 @@ export declare const shardControlOperationSchema: z.ZodDiscriminatedUnion<[z.Zod
|
|
|
208
208
|
guildId: z.ZodString;
|
|
209
209
|
moduleId: z.ZodString;
|
|
210
210
|
enabled: z.ZodBoolean;
|
|
211
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
212
|
+
actorUserId: z.ZodString;
|
|
213
|
+
correlationId: z.ZodString;
|
|
214
|
+
kind: z.ZodLiteral<"guild.commands.first-time-setup">;
|
|
215
|
+
guildId: z.ZodString;
|
|
216
|
+
moduleIds: z.ZodArray<z.ZodString>;
|
|
217
|
+
expectedRevision: z.ZodString;
|
|
218
|
+
idempotencyKey: z.ZodString;
|
|
211
219
|
}, z.core.$strict>, z.ZodObject<{
|
|
212
220
|
actorUserId: z.ZodString;
|
|
213
221
|
correlationId: z.ZodString;
|
|
@@ -561,6 +569,14 @@ export declare const shardControlRequestSchema: z.ZodObject<{
|
|
|
561
569
|
guildId: z.ZodString;
|
|
562
570
|
moduleId: z.ZodString;
|
|
563
571
|
enabled: z.ZodBoolean;
|
|
572
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
573
|
+
actorUserId: z.ZodString;
|
|
574
|
+
correlationId: z.ZodString;
|
|
575
|
+
kind: z.ZodLiteral<"guild.commands.first-time-setup">;
|
|
576
|
+
guildId: z.ZodString;
|
|
577
|
+
moduleIds: z.ZodArray<z.ZodString>;
|
|
578
|
+
expectedRevision: z.ZodString;
|
|
579
|
+
idempotencyKey: z.ZodString;
|
|
564
580
|
}, z.core.$strict>, z.ZodObject<{
|
|
565
581
|
actorUserId: z.ZodString;
|
|
566
582
|
correlationId: z.ZodString;
|
|
@@ -1419,6 +1435,14 @@ export declare const supervisorMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObje
|
|
|
1419
1435
|
guildId: z.ZodString;
|
|
1420
1436
|
moduleId: z.ZodString;
|
|
1421
1437
|
enabled: z.ZodBoolean;
|
|
1438
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
1439
|
+
actorUserId: z.ZodString;
|
|
1440
|
+
correlationId: z.ZodString;
|
|
1441
|
+
kind: z.ZodLiteral<"guild.commands.first-time-setup">;
|
|
1442
|
+
guildId: z.ZodString;
|
|
1443
|
+
moduleIds: z.ZodArray<z.ZodString>;
|
|
1444
|
+
expectedRevision: z.ZodString;
|
|
1445
|
+
idempotencyKey: z.ZodString;
|
|
1422
1446
|
}, z.core.$strict>, z.ZodObject<{
|
|
1423
1447
|
actorUserId: z.ZodString;
|
|
1424
1448
|
correlationId: z.ZodString;
|
package/dist/shard-contracts.js
CHANGED
|
@@ -280,6 +280,16 @@ export const shardControlOperationSchema = z.discriminatedUnion("kind", [
|
|
|
280
280
|
...mutationContext.shape,
|
|
281
281
|
})
|
|
282
282
|
.strict(),
|
|
283
|
+
z
|
|
284
|
+
.object({
|
|
285
|
+
kind: z.literal("guild.commands.first-time-setup"),
|
|
286
|
+
guildId: snowflake,
|
|
287
|
+
moduleIds: z.array(moduleId).min(1).max(100),
|
|
288
|
+
expectedRevision: z.string().regex(/^\d+$/u),
|
|
289
|
+
idempotencyKey: z.string().min(1).max(160),
|
|
290
|
+
...mutationContext.shape,
|
|
291
|
+
})
|
|
292
|
+
.strict(),
|
|
283
293
|
z
|
|
284
294
|
.object({
|
|
285
295
|
kind: z.literal("guild.module.configuration.update"),
|
|
@@ -992,6 +1002,16 @@ export async function executeShardControlOperation(handler, operation) {
|
|
|
992
1002
|
correlationId: operation.correlationId,
|
|
993
1003
|
});
|
|
994
1004
|
}
|
|
1005
|
+
case "guild.commands.first-time-setup": {
|
|
1006
|
+
return handler.completeFirstTimeCommandSetup({
|
|
1007
|
+
guildId: operation.guildId,
|
|
1008
|
+
moduleIds: operation.moduleIds,
|
|
1009
|
+
expectedRevision: BigInt(operation.expectedRevision),
|
|
1010
|
+
idempotencyKey: operation.idempotencyKey,
|
|
1011
|
+
actorUserId: operation.actorUserId,
|
|
1012
|
+
correlationId: operation.correlationId,
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
995
1015
|
case "guild.module.configuration.update": {
|
|
996
1016
|
return handler.updateConfiguration({
|
|
997
1017
|
guildId: operation.guildId,
|
package/dist/shard-router.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export declare class ShardControlRouter implements BotControlHandler {
|
|
|
26
26
|
canAccessManagedResource(input: Parameters<BotControlHandler["canAccessManagedResource"]>[0]): ReturnType<BotControlHandler["canAccessManagedResource"]>;
|
|
27
27
|
listGuildPresence(input: Parameters<BotControlHandler["listGuildPresence"]>[0]): ReturnType<BotControlHandler["listGuildPresence"]>;
|
|
28
28
|
setModuleStatus(input: Parameters<BotControlHandler["setModuleStatus"]>[0]): ReturnType<BotControlHandler["setModuleStatus"]>;
|
|
29
|
+
completeFirstTimeCommandSetup(input: Parameters<BotControlHandler["completeFirstTimeCommandSetup"]>[0]): ReturnType<BotControlHandler["completeFirstTimeCommandSetup"]>;
|
|
29
30
|
updateConfiguration(input: Parameters<BotControlHandler["updateConfiguration"]>[0]): ReturnType<BotControlHandler["updateConfiguration"]>;
|
|
30
31
|
updatePermissions(input: Parameters<BotControlHandler["updatePermissions"]>[0]): ReturnType<BotControlHandler["updatePermissions"]>;
|
|
31
32
|
updateLoggingPolicy(input: Parameters<BotControlHandler["updateLoggingPolicy"]>[0]): ReturnType<BotControlHandler["updateLoggingPolicy"]>;
|
package/dist/shard-router.js
CHANGED
|
@@ -84,6 +84,13 @@ export class ShardControlRouter {
|
|
|
84
84
|
setModuleStatus(input) {
|
|
85
85
|
return this.#route({ kind: "guild.module.status.set", ...input });
|
|
86
86
|
}
|
|
87
|
+
completeFirstTimeCommandSetup(input) {
|
|
88
|
+
return this.#route({
|
|
89
|
+
kind: "guild.commands.first-time-setup",
|
|
90
|
+
...input,
|
|
91
|
+
expectedRevision: input.expectedRevision.toString(),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
87
94
|
updateConfiguration(input) {
|
|
88
95
|
return this.#route(shardControlOperationSchema.parse({
|
|
89
96
|
kind: "guild.module.configuration.update",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BotEnvironment } from "@helyx/config";
|
|
2
2
|
import { type DatabasePool } from "@helyx/database";
|
|
3
3
|
import type { ModuleDefinition, NumericWriteBehindCommittedBatch, ScheduledTaskExecutionResult } from "@helyx/sdk";
|
|
4
|
+
import { buildApplicationCommandDefinitions } from "./interactions.js";
|
|
4
5
|
import { type ShardCloudRequest, type ShardIdentity, type ShardProcessHealth } from "./shard-contracts.js";
|
|
5
6
|
import { type ShardCoordinator, type ShardGenerationDescriptor, type ShardLeaderLease, type ShardLease } from "./shard-coordinator.js";
|
|
6
7
|
import type { BotHandle } from "./index.js";
|
|
@@ -18,7 +19,7 @@ export declare function deferScheduledRoutingFailures(route: () => Promise<Sched
|
|
|
18
19
|
export declare function startShardSupervisor(environment: BotEnvironment, processEnvironment: NodeJS.ProcessEnv, workerEntry: string): Promise<BotHandle>;
|
|
19
20
|
export declare function splitNumericCommittedBatch(batch: NumericWriteBehindCommittedBatch, index: number): NumericWriteBehindCommittedBatch;
|
|
20
21
|
export declare function buildShardWorkerEnvironment(source: NodeJS.ProcessEnv, identity: ShardIdentity, restartCount: number): NodeJS.ProcessEnv;
|
|
21
|
-
export declare function shardRuntimeCompositionHash(modules: readonly ModuleDefinition[], internalGuildId?: string): string;
|
|
22
|
+
export declare function shardRuntimeCompositionHash(modules: readonly ModuleDefinition[], internalGuildId?: string, globalCommandMode?: Parameters<typeof buildApplicationCommandDefinitions>[2]): string;
|
|
22
23
|
export declare function boundedRespawnDelay(attempt: number): number;
|
|
23
24
|
export interface SteadyStateOwnershipRenewal {
|
|
24
25
|
generationRenewed: boolean;
|
package/dist/shard-supervisor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { safeError, ServiceRegistry } from "@helyx/core";
|
|
2
|
-
import { checkDatabaseConnection, createDatabasePool, AuditRepository, InteractionSessionRepository, RateLimitRepository, runConfiguredModuleMigrations, runCoreMigrations, RuntimeStateRepository, } from "@helyx/database";
|
|
2
|
+
import { checkDatabaseConnection, createDatabasePool, AuditRepository, GuildCommandRepository, InteractionSessionRepository, RateLimitRepository, runConfiguredModuleMigrations, runCoreMigrations, RuntimeStateRepository, } from "@helyx/database";
|
|
3
3
|
import { createPlatformServices, gamingStatisticsRecordOwner, achievementContributionRecordOwner, PostgresPrivacyProviderExecutor, } from "@helyx/platform";
|
|
4
4
|
import { HELYX_SERVICE_NAMES } from "@helyx/sdk";
|
|
5
5
|
import { Client, ShardingManager } from "discord.js";
|
|
@@ -23,11 +23,18 @@ import { createDeveloperOperationsModule } from "./developer-operations-module.j
|
|
|
23
23
|
import { SupervisorCloudDiscordDirectory } from "./supervisor-cloud-directory.js";
|
|
24
24
|
import { createSupportModule } from "./support-module.js";
|
|
25
25
|
import { createOAuthModule } from "./oauth-module.js";
|
|
26
|
+
import { createPrivacyModule } from "./privacy-module.js";
|
|
27
|
+
import { discordRestOptions } from "./discord-rest.js";
|
|
26
28
|
import { availabilityCatalogue, availabilityCatalogueDefinitions, } from "./module-availability-control.js";
|
|
27
29
|
import { createScheduledTaskRunner, schedulerOperationalHealth, } from "./scheduled-task-runner.js";
|
|
28
30
|
import { numericWriteBehindRegistrations } from "./numeric-write-behind.js";
|
|
31
|
+
import { PostgresTriviaDataProvider } from "./trivia-data-provider.js";
|
|
29
32
|
import { executeRedisNumericOperation, RedisNumericWriteBehindService, } from "./numeric-write-behind-redis.js";
|
|
30
33
|
import { encodeNumericIpcValue } from "./numeric-write-behind-proxy.js";
|
|
34
|
+
import { GuildCommandWorker } from "./guild-command-worker.js";
|
|
35
|
+
import { GuildCommandDiscordClient } from "./guild-command-discord-client.js";
|
|
36
|
+
import { GuildCommandReadService } from "./guild-command-read-service.js";
|
|
37
|
+
import { scheduleGuildCommandRelease } from "./guild-command-release.js";
|
|
31
38
|
const LEASE_RENEW_INTERVAL_MS = 10_000;
|
|
32
39
|
const STARTUP_LEASE_RENEW_INTERVAL_MS = 5_000;
|
|
33
40
|
const LEASE_VALIDATION_TIMEOUT_MS = 1_000;
|
|
@@ -137,7 +144,10 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
137
144
|
let privacyService;
|
|
138
145
|
let manager;
|
|
139
146
|
let scheduledTasks;
|
|
147
|
+
let guildCommandWorker;
|
|
148
|
+
let guildCommandReleasePromise;
|
|
140
149
|
let scheduledModules = [];
|
|
150
|
+
let commandModules = [];
|
|
141
151
|
let workersRoutable = false;
|
|
142
152
|
let promoteToLeader = () => Promise.resolve(false);
|
|
143
153
|
try {
|
|
@@ -171,7 +181,10 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
171
181
|
coreVersion: botVersion(),
|
|
172
182
|
logger,
|
|
173
183
|
});
|
|
174
|
-
const commandClient = new Client({
|
|
184
|
+
const commandClient = new Client({
|
|
185
|
+
intents: [],
|
|
186
|
+
rest: discordRestOptions,
|
|
187
|
+
});
|
|
175
188
|
const unavailableResources = {
|
|
176
189
|
listGuildPresence: (guildIds) => Promise.resolve(guildIds.map((guildId) => ({
|
|
177
190
|
guildId,
|
|
@@ -197,6 +210,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
197
210
|
})
|
|
198
211
|
: undefined;
|
|
199
212
|
const supportModule = createSupportModule(cloudConnector);
|
|
213
|
+
const privacyModule = createPrivacyModule();
|
|
200
214
|
const oauthModule = environment.HELYX_OAUTH_ENABLED
|
|
201
215
|
? createOAuthModule()
|
|
202
216
|
: undefined;
|
|
@@ -204,6 +218,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
204
218
|
managementModule,
|
|
205
219
|
permissionsModule,
|
|
206
220
|
supportModule,
|
|
221
|
+
privacyModule,
|
|
207
222
|
...(oauthModule ? [oauthModule] : []),
|
|
208
223
|
...(cloudManagementModule ? [cloudManagementModule] : []),
|
|
209
224
|
...featureModules,
|
|
@@ -224,11 +239,13 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
224
239
|
managementModule,
|
|
225
240
|
permissionsModule,
|
|
226
241
|
supportModule,
|
|
242
|
+
privacyModule,
|
|
227
243
|
...(oauthModule ? [oauthModule] : []),
|
|
228
244
|
...(cloudManagementModule ? [cloudManagementModule] : []),
|
|
229
245
|
].map(({ manifest }) => manifest),
|
|
230
246
|
modules: featureModules.map(({ manifest }) => manifest),
|
|
231
247
|
processLabel: "shard supervisor registration",
|
|
248
|
+
triviaData: new PostgresTriviaDataProvider(database),
|
|
232
249
|
health: {
|
|
233
250
|
getHealth: async () => boundedOperationalHealth(await buildHealth()),
|
|
234
251
|
},
|
|
@@ -238,6 +255,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
238
255
|
...publicModules,
|
|
239
256
|
...(operationsModule ? [operationsModule] : []),
|
|
240
257
|
];
|
|
258
|
+
commandModules = modules;
|
|
241
259
|
const availabilityModules = availabilityCatalogueDefinitions(modules);
|
|
242
260
|
const availabilityService = {
|
|
243
261
|
listAvailabilityModules: () => availabilityCatalogue(availabilityModules),
|
|
@@ -289,7 +307,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
289
307
|
environment: environment.NODE_ENV,
|
|
290
308
|
generation,
|
|
291
309
|
shardCount,
|
|
292
|
-
catalogueHash: shardRuntimeCompositionHash(modules, environment.DISCORD_INTERNAL_GUILD_ID),
|
|
310
|
+
catalogueHash: shardRuntimeCompositionHash(modules, environment.DISCORD_INTERNAL_GUILD_ID, environment.HELYX_GLOBAL_COMMAND_MODE),
|
|
293
311
|
};
|
|
294
312
|
deploymentOrder = await nextShardDeploymentOrder(database);
|
|
295
313
|
if (environment.HELYX_SHARD_SCOPE === "coordinated") {
|
|
@@ -402,6 +420,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
402
420
|
secret: environment.HELYX_BOT_CONTROL_SECRET,
|
|
403
421
|
logger: { level: environment.LOG_LEVEL },
|
|
404
422
|
shardHealth: buildHealth,
|
|
423
|
+
guildCommands: new GuildCommandReadService(database, environment.DISCORD_CLIENT_ID, () => guildCommandWorker?.getHealth(), environment.DISCORD_TOKEN, environment.HELYX_GUILD_COMMAND_WRITES_ENABLED, environment.HELYX_GUILD_COMMAND_WRITE_GUILD_IDS, environment.HELYX_GUILD_COMMAND_WRITE_SCOPE === "all", commandModules),
|
|
405
424
|
});
|
|
406
425
|
await controlServer.listen({
|
|
407
426
|
host: "::",
|
|
@@ -1087,16 +1106,62 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
1087
1106
|
startScheduledTasks();
|
|
1088
1107
|
}
|
|
1089
1108
|
function startScheduledTasks() {
|
|
1090
|
-
if (
|
|
1109
|
+
if (!leaderLease || !workersRoutable)
|
|
1091
1110
|
return;
|
|
1092
|
-
scheduledTasks
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1111
|
+
if (!scheduledTasks) {
|
|
1112
|
+
scheduledTasks = createScheduledTaskRunner({
|
|
1113
|
+
database,
|
|
1114
|
+
modules: scheduledModules,
|
|
1115
|
+
route: routeScheduledTask,
|
|
1116
|
+
logger,
|
|
1117
|
+
workerId: `shard-leader:${leaderLease.generation}:${String(leaderLease.fencingToken)}`,
|
|
1118
|
+
});
|
|
1119
|
+
scheduledTasks.start();
|
|
1120
|
+
}
|
|
1121
|
+
if (!guildCommandWorker) {
|
|
1122
|
+
guildCommandWorker = new GuildCommandWorker({
|
|
1123
|
+
database,
|
|
1124
|
+
modules: commandModules,
|
|
1125
|
+
logger,
|
|
1126
|
+
workerId: `shard-leader-command:${leaderLease.generation}:${String(leaderLease.fencingToken)}`,
|
|
1127
|
+
fencingToken: BigInt(leaderLease.fencingToken),
|
|
1128
|
+
...(environment.DISCORD_INTERNAL_GUILD_ID
|
|
1129
|
+
? { internalGuildId: environment.DISCORD_INTERNAL_GUILD_ID }
|
|
1130
|
+
: {}),
|
|
1131
|
+
...(environment.HELYX_GUILD_COMMAND_WRITES_ENABLED
|
|
1132
|
+
? {
|
|
1133
|
+
discord: new GuildCommandDiscordClient({
|
|
1134
|
+
applicationId: environment.DISCORD_CLIENT_ID,
|
|
1135
|
+
token: environment.DISCORD_TOKEN,
|
|
1136
|
+
writeGuildIds: environment.HELYX_GUILD_COMMAND_WRITE_GUILD_IDS,
|
|
1137
|
+
writeAllGuilds: environment.HELYX_GUILD_COMMAND_WRITE_SCOPE === "all",
|
|
1138
|
+
}),
|
|
1139
|
+
}
|
|
1140
|
+
: {}),
|
|
1141
|
+
});
|
|
1142
|
+
guildCommandWorker.start();
|
|
1143
|
+
}
|
|
1144
|
+
if (environment.HELYX_GUILD_COMMAND_WRITES_ENABLED &&
|
|
1145
|
+
environment.HELYX_GUILD_COMMAND_WRITE_SCOPE === "all" &&
|
|
1146
|
+
!guildCommandReleasePromise) {
|
|
1147
|
+
const releasePromise = scheduleGuildCommandRelease({
|
|
1148
|
+
database,
|
|
1149
|
+
applicationId: environment.DISCORD_CLIENT_ID,
|
|
1150
|
+
releaseId: environment.HELYX_GUILD_COMMAND_RELEASE_ID,
|
|
1151
|
+
})
|
|
1152
|
+
.then((release) => {
|
|
1153
|
+
logger.info({ event: "guild_commands.release_scheduled", ...release }, "Guild command release rollout prepared");
|
|
1154
|
+
})
|
|
1155
|
+
.catch((error) => {
|
|
1156
|
+
if (guildCommandReleasePromise === releasePromise)
|
|
1157
|
+
guildCommandReleasePromise = undefined;
|
|
1158
|
+
logger.error({
|
|
1159
|
+
event: "guild_commands.release_schedule_failed",
|
|
1160
|
+
error: safeError(error),
|
|
1161
|
+
}, "Guild command release rollout could not be prepared");
|
|
1162
|
+
});
|
|
1163
|
+
guildCommandReleasePromise = releasePromise;
|
|
1164
|
+
}
|
|
1100
1165
|
}
|
|
1101
1166
|
async function routeScheduledTask(task, signal) {
|
|
1102
1167
|
if (!task.guildId)
|
|
@@ -1296,6 +1361,8 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
1296
1361
|
workersRoutable = false;
|
|
1297
1362
|
await scheduledTasks?.stop().catch(() => undefined);
|
|
1298
1363
|
scheduledTasks = undefined;
|
|
1364
|
+
await guildCommandWorker?.stop().catch(() => undefined);
|
|
1365
|
+
guildCommandWorker = undefined;
|
|
1299
1366
|
for (const [requestId, pending] of pendingScheduled) {
|
|
1300
1367
|
clearTimeout(pending.timer);
|
|
1301
1368
|
pending.resolve(unavailableScheduled(requestId, "supervisor_stopping"));
|
|
@@ -1366,9 +1433,11 @@ async function runLeaderStartup(database, environment, modulePackages, modules,
|
|
|
1366
1433
|
applicationId: environment.DISCORD_CLIENT_ID,
|
|
1367
1434
|
token: environment.DISCORD_TOKEN,
|
|
1368
1435
|
modules,
|
|
1436
|
+
globalCommandMode: environment.HELYX_GLOBAL_COMMAND_MODE,
|
|
1369
1437
|
...(environment.DISCORD_INTERNAL_GUILD_ID
|
|
1370
1438
|
? { internalGuildId: environment.DISCORD_INTERNAL_GUILD_ID }
|
|
1371
1439
|
: {}),
|
|
1440
|
+
synchroniseInternalGuild: !environment.HELYX_GUILD_COMMAND_WRITES_ENABLED,
|
|
1372
1441
|
state: new RuntimeCommandHashStore(repository),
|
|
1373
1442
|
});
|
|
1374
1443
|
await setRuntimeState(repository, `shards:${environment.HELYX_SHARD_GENERATION ?? "local"}:startup`, {
|
|
@@ -1514,8 +1583,8 @@ export function buildShardWorkerEnvironment(source, identity, restartCount) {
|
|
|
1514
1583
|
HELYX_SHARD_RESTART_COUNT: String(restartCount),
|
|
1515
1584
|
};
|
|
1516
1585
|
}
|
|
1517
|
-
export function shardRuntimeCompositionHash(modules, internalGuildId) {
|
|
1518
|
-
const commands = buildApplicationCommandDefinitions(modules, internalGuildId);
|
|
1586
|
+
export function shardRuntimeCompositionHash(modules, internalGuildId, globalCommandMode = "legacy") {
|
|
1587
|
+
const commands = buildApplicationCommandDefinitions(modules, internalGuildId, globalCommandMode);
|
|
1519
1588
|
return createHash("sha256")
|
|
1520
1589
|
.update(JSON.stringify({
|
|
1521
1590
|
botVersion: botVersion(),
|
|
@@ -1581,17 +1650,26 @@ function failedCloud(requestId, message) {
|
|
|
1581
1650
|
}
|
|
1582
1651
|
async function cleanupExpiredCoreRecords(database, logger) {
|
|
1583
1652
|
try {
|
|
1584
|
-
const [sessions, rateLimits, verificationAudit] = await Promise.all([
|
|
1653
|
+
const [sessions, rateLimits, verificationAudit, commandRecords] = await Promise.all([
|
|
1585
1654
|
new InteractionSessionRepository(database).deleteExpired(),
|
|
1586
1655
|
new RateLimitRepository(database).deleteExpired(),
|
|
1587
1656
|
new AuditRepository(database).deleteModuleEventsOlderThanMonths("helyx.verification", 12),
|
|
1657
|
+
new GuildCommandRepository(database).deleteExpired(12),
|
|
1588
1658
|
]);
|
|
1589
|
-
|
|
1659
|
+
const commandRecordCount = commandRecords.history +
|
|
1660
|
+
commandRecords.idempotencyKeys +
|
|
1661
|
+
commandRecords.creationLedger +
|
|
1662
|
+
commandRecords.requests;
|
|
1663
|
+
if (sessions > 0 ||
|
|
1664
|
+
rateLimits > 0 ||
|
|
1665
|
+
verificationAudit > 0 ||
|
|
1666
|
+
commandRecordCount > 0)
|
|
1590
1667
|
logger.info({
|
|
1591
1668
|
event: "database.retention_cleaned",
|
|
1592
1669
|
interactionSessions: sessions,
|
|
1593
1670
|
rateLimitWindows: rateLimits,
|
|
1594
1671
|
verificationAuditEvents: verificationAudit,
|
|
1672
|
+
commandRecords: commandRecordCount,
|
|
1595
1673
|
}, "Expired core records removed");
|
|
1596
1674
|
}
|
|
1597
1675
|
catch (error) {
|
package/dist/shard-worker.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export declare function createLeaseHeartbeatWatchdog(onExpired: () => void, time
|
|
|
9
9
|
};
|
|
10
10
|
export declare function startShardWorker(processEnvironment?: NodeJS.ProcessEnv, identity?: {
|
|
11
11
|
version: 1;
|
|
12
|
-
mode: "
|
|
12
|
+
mode: "fixed" | "auto";
|
|
13
13
|
scope: "local" | "coordinated";
|
|
14
14
|
generation: string;
|
|
15
15
|
supervisorId: string;
|
|
@@ -40,6 +40,10 @@ export declare function parseShardWorkerEnvironment(processEnvironment: NodeJS.P
|
|
|
40
40
|
HELYX_VERIFICATION_SIGNING_PUBLIC_KEYS: Record<string, string>;
|
|
41
41
|
HELYX_BOT_CONTROL_PORT: number;
|
|
42
42
|
HELYX_GUILD_RECONCILIATION_INTERVAL_SECONDS: number;
|
|
43
|
+
HELYX_GUILD_COMMAND_WRITES_ENABLED: boolean;
|
|
44
|
+
HELYX_GUILD_COMMAND_WRITE_SCOPE: "allowlist" | "all";
|
|
45
|
+
HELYX_GLOBAL_COMMAND_MODE: "legacy" | "platform-only";
|
|
46
|
+
HELYX_GUILD_COMMAND_WRITE_GUILD_IDS: string[];
|
|
43
47
|
HELYX_PRIVACY_ENABLED: boolean;
|
|
44
48
|
HELYX_MESSAGE_CONTENT_ENABLED: boolean;
|
|
45
49
|
HELYX_GUILD_MEMBERS_ENABLED: boolean;
|
|
@@ -64,6 +68,7 @@ export declare function parseShardWorkerEnvironment(processEnvironment: NodeJS.P
|
|
|
64
68
|
HELYX_VERIFICATION_INSTALLATION_ID?: string | undefined;
|
|
65
69
|
HELYX_VERIFICATION_URL?: string | undefined;
|
|
66
70
|
HELYX_VERIFICATION_INTERNAL_SECRET?: string | undefined;
|
|
71
|
+
HELYX_GUILD_COMMAND_RELEASE_ID?: string | undefined;
|
|
67
72
|
HELYX_PRIVACY_EXECUTOR_DATABASE_URL?: string | undefined;
|
|
68
73
|
HELYX_PRIVACY_EXECUTOR_DATABASE_SSL?: "disable" | "require" | "verify-full" | undefined;
|
|
69
74
|
HELYX_OAUTH_INTERNAL_SECRET?: string | undefined;
|
package/dist/shard-worker.js
CHANGED
|
@@ -22,15 +22,18 @@ import { WorkerCloudConnectorProxy } from "./shard-cloud-proxy.js";
|
|
|
22
22
|
import { DiscordOwnedMessageDeletionService } from "./discord-owned-message-service.js";
|
|
23
23
|
import { DiscordGuildHealthInspection } from "./discord-guild-health.js";
|
|
24
24
|
import { CoreResourceAuthorizationService } from "./resource-authorization.js";
|
|
25
|
+
import { PostgresTriviaDataProvider } from "./trivia-data-provider.js";
|
|
25
26
|
import { createDeveloperOperationsModule, WorkerShardHealthProvider, } from "./developer-operations-module.js";
|
|
26
27
|
import { assertShardMessageSize, controlOperationGuildId, executeShardControlOperation, guildIdToShardId, SHARD_LEASE_HEARTBEAT_TIMEOUT_MS, shardIdentitySchema, supervisorMessageSchema, } from "./shard-contracts.js";
|
|
27
28
|
import { gatewayIntentsForModules, readBotVersion, } from "./index.js";
|
|
28
29
|
import { createSupportModule } from "./support-module.js";
|
|
29
30
|
import { createOAuthModule } from "./oauth-module.js";
|
|
31
|
+
import { createPrivacyModule } from "./privacy-module.js";
|
|
30
32
|
import { HttpOAuthService } from "./oauth-service.js";
|
|
31
33
|
import { createLocalScheduledTaskRoute, } from "./scheduled-task-runner.js";
|
|
32
34
|
import { createLocalNumericWriteBehind } from "./numeric-write-behind.js";
|
|
33
35
|
import { WorkerNumericWriteBehindProxy } from "./numeric-write-behind-proxy.js";
|
|
36
|
+
import { discordRestOptions } from "./discord-rest.js";
|
|
34
37
|
const HEALTH_INTERVAL_MS = 5_000;
|
|
35
38
|
export const WORKER_ORPHAN_TERMINATION_BOUND_MS = SHARD_LEASE_HEARTBEAT_TIMEOUT_MS + 5_000;
|
|
36
39
|
export function createLeaseHeartbeatWatchdog(onExpired, timeoutMs = SHARD_LEASE_HEARTBEAT_TIMEOUT_MS) {
|
|
@@ -114,6 +117,7 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
114
117
|
const modulePackages = await loadConfiguredModulePackages(moduleSpecifiers);
|
|
115
118
|
const featureModules = modulePackages.map(({ definition }) => definition);
|
|
116
119
|
client = new Client({
|
|
120
|
+
rest: discordRestOptions,
|
|
117
121
|
intents: gatewayIntentsForModules(featureModules, environment.HELYX_MESSAGE_CONTENT_ENABLED, environment.HELYX_GUILD_MEMBERS_ENABLED),
|
|
118
122
|
partials: [Partials.Message, Partials.Channel, Partials.Reaction],
|
|
119
123
|
allowedMentions: { parse: [] },
|
|
@@ -133,6 +137,7 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
133
137
|
})
|
|
134
138
|
: undefined;
|
|
135
139
|
const supportModule = createSupportModule(cloudProxy);
|
|
140
|
+
const privacyModule = createPrivacyModule();
|
|
136
141
|
const oauthModule = environment.HELYX_OAUTH_ENABLED
|
|
137
142
|
? createOAuthModule()
|
|
138
143
|
: undefined;
|
|
@@ -140,6 +145,7 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
140
145
|
managementModule,
|
|
141
146
|
permissionsModule,
|
|
142
147
|
supportModule,
|
|
148
|
+
privacyModule,
|
|
143
149
|
...(oauthModule ? [oauthModule] : []),
|
|
144
150
|
...(cloudManagementModule ? [cloudManagementModule] : []),
|
|
145
151
|
...featureModules,
|
|
@@ -159,11 +165,13 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
159
165
|
managementModule,
|
|
160
166
|
permissionsModule,
|
|
161
167
|
supportModule,
|
|
168
|
+
privacyModule,
|
|
162
169
|
...(oauthModule ? [oauthModule] : []),
|
|
163
170
|
...(cloudManagementModule ? [cloudManagementModule] : []),
|
|
164
171
|
].map(({ manifest }) => manifest),
|
|
165
172
|
modules: featureModules.map(({ manifest }) => manifest),
|
|
166
173
|
processLabel: `shard worker ${identity.shardId}`,
|
|
174
|
+
triviaData: new PostgresTriviaDataProvider(database),
|
|
167
175
|
health: shardHealthProvider,
|
|
168
176
|
})
|
|
169
177
|
: undefined;
|
|
@@ -231,7 +239,7 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
231
239
|
shardId: identity.shardId,
|
|
232
240
|
fencingToken: BigInt(identity.fencingToken),
|
|
233
241
|
};
|
|
234
|
-
const prepareGuilds = installGuildTracking(client, database, publicModules, logger, enabledCoreModuleIds, shardOwnership, environment.HELYX_GUILD_RECONCILIATION_INTERVAL_SECONDS * 1_000, identity.shardCount);
|
|
242
|
+
const prepareGuilds = installGuildTracking(client, database, publicModules, logger, enabledCoreModuleIds, shardOwnership, environment.HELYX_GUILD_RECONCILIATION_INTERVAL_SECONDS * 1_000, identity.shardCount, environment.DISCORD_CLIENT_ID);
|
|
235
243
|
guildTracking = prepareGuilds;
|
|
236
244
|
const commandsReady = installInteractions(client, modules, shardWorkerCommandRegistration({
|
|
237
245
|
...(environment.DISCORD_INTERNAL_GUILD_ID
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type CommandContribution, type ModuleDefinition } from "@helyx/sdk";
|
|
2
|
+
export interface CommandRouteOwner {
|
|
3
|
+
moduleId: string;
|
|
4
|
+
moduleName: string;
|
|
5
|
+
}
|
|
6
|
+
export interface RegisteredCommand {
|
|
7
|
+
moduleId: string;
|
|
8
|
+
moduleName: string;
|
|
9
|
+
contribution: CommandContribution;
|
|
10
|
+
routeOwners?: ReadonlyMap<string, CommandRouteOwner>;
|
|
11
|
+
}
|
|
12
|
+
export declare function collectCommandRegistrations(modules: readonly ModuleDefinition[]): Map<string, RegisteredCommand>;
|
|
13
|
+
//# sourceMappingURL=shared-commands.d.ts.map
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { NEXUS_COMMAND_SPECS, NEXUS_ROOT_MODULE_ID, SHARED_COMMAND_FAMILY_SPECS, } from "@helyx/sdk";
|
|
2
|
+
const nexusFamilySpec = {
|
|
3
|
+
name: "nexus",
|
|
4
|
+
description: "View and manage Nexus",
|
|
5
|
+
registrationPolicy: "enabled-guild",
|
|
6
|
+
contexts: ["guild"],
|
|
7
|
+
integrationTypes: ["guild_install"],
|
|
8
|
+
paths: NEXUS_COMMAND_SPECS,
|
|
9
|
+
};
|
|
10
|
+
const familySpecs = [
|
|
11
|
+
...SHARED_COMMAND_FAMILY_SPECS,
|
|
12
|
+
nexusFamilySpec,
|
|
13
|
+
];
|
|
14
|
+
export function collectCommandRegistrations(modules) {
|
|
15
|
+
const registrations = modules.flatMap((module) => (module.interactions?.commands ?? []).map((contribution) => ({
|
|
16
|
+
moduleId: module.manifest.id,
|
|
17
|
+
moduleName: module.manifest.name,
|
|
18
|
+
contribution,
|
|
19
|
+
})));
|
|
20
|
+
const familyContributions = modules.flatMap((module) => [
|
|
21
|
+
...(module.sharedCommands ?? []).map((contribution) => ({
|
|
22
|
+
moduleId: module.manifest.id,
|
|
23
|
+
moduleName: module.manifest.name,
|
|
24
|
+
contribution,
|
|
25
|
+
})),
|
|
26
|
+
...(module.nexusCommands ?? []).map((contribution) => ({
|
|
27
|
+
moduleId: module.manifest.id,
|
|
28
|
+
moduleName: module.manifest.name,
|
|
29
|
+
contribution: fromNexusContribution(contribution),
|
|
30
|
+
})),
|
|
31
|
+
]);
|
|
32
|
+
for (const family of familySpecs) {
|
|
33
|
+
const contributions = familyContributions.filter(({ contribution }) => contribution.family === family.name);
|
|
34
|
+
if (contributions.length === 0)
|
|
35
|
+
continue;
|
|
36
|
+
const roots = registrations.filter(({ contribution }) => contribution.name === family.name);
|
|
37
|
+
if (roots.length > 1)
|
|
38
|
+
throw new Error(`Shared command /${family.name} has multiple root owners`);
|
|
39
|
+
const explicitRoot = roots[0];
|
|
40
|
+
if (explicitRoot) {
|
|
41
|
+
validateExplicitRoot(family, explicitRoot);
|
|
42
|
+
registrations.splice(registrations.indexOf(explicitRoot), 1);
|
|
43
|
+
}
|
|
44
|
+
registrations.push(composeSharedCommand(family, explicitRoot ?? syntheticRoot(family, contributions), contributions));
|
|
45
|
+
}
|
|
46
|
+
const unknownFamily = familyContributions.find(({ contribution }) => !familySpecs.some(({ name }) => name === contribution.family));
|
|
47
|
+
if (unknownFamily)
|
|
48
|
+
throw new Error(`Unknown shared command family: ${unknownFamily.contribution.family}`);
|
|
49
|
+
return uniqueRegistrations(registrations);
|
|
50
|
+
}
|
|
51
|
+
function syntheticRoot(family, contributions) {
|
|
52
|
+
const owner = contributions[0];
|
|
53
|
+
return {
|
|
54
|
+
moduleId: owner.moduleId,
|
|
55
|
+
moduleName: owner.moduleName,
|
|
56
|
+
contribution: {
|
|
57
|
+
name: family.name,
|
|
58
|
+
description: family.description,
|
|
59
|
+
registrationPolicy: family.registrationPolicy,
|
|
60
|
+
contexts: family.contexts,
|
|
61
|
+
integrationTypes: family.integrationTypes,
|
|
62
|
+
subcommands: [],
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function validateExplicitRoot(family, root) {
|
|
67
|
+
if (family.name !== "nexus" || root.moduleId !== NEXUS_ROOT_MODULE_ID)
|
|
68
|
+
throw new Error(`Shared command /${family.name} must use its composed root`);
|
|
69
|
+
if (root.contribution.execute ||
|
|
70
|
+
root.contribution.options?.length ||
|
|
71
|
+
root.contribution.subcommands?.length ||
|
|
72
|
+
root.contribution.subcommandGroups?.length) {
|
|
73
|
+
throw new Error(`Shared command /${family.name} root must be empty`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function composeSharedCommand(family, root, contributions) {
|
|
77
|
+
const byPath = new Map();
|
|
78
|
+
for (const item of contributions) {
|
|
79
|
+
const path = family.paths.find(({ path: candidate }) => candidate === item.contribution.path);
|
|
80
|
+
if (!path || path.ownerModuleId !== item.moduleId) {
|
|
81
|
+
throw new Error(`Module ${item.moduleId} cannot contribute ${family.name} command ${item.contribution.path}`);
|
|
82
|
+
}
|
|
83
|
+
if (byPath.has(item.contribution.path))
|
|
84
|
+
throw new Error(`Duplicate shared command path: ${family.name}.${item.contribution.path}`);
|
|
85
|
+
byPath.set(item.contribution.path, item);
|
|
86
|
+
}
|
|
87
|
+
const subcommands = [];
|
|
88
|
+
const groups = new Map();
|
|
89
|
+
const routeOwners = new Map();
|
|
90
|
+
const contexts = new Set(family.contexts);
|
|
91
|
+
for (const pathSpec of family.paths) {
|
|
92
|
+
const item = byPath.get(pathSpec.path);
|
|
93
|
+
if (!item)
|
|
94
|
+
continue;
|
|
95
|
+
for (const context of item.contribution.contexts ?? [])
|
|
96
|
+
contexts.add(context);
|
|
97
|
+
routeOwners.set(pathSpec.path, {
|
|
98
|
+
moduleId: item.moduleId,
|
|
99
|
+
moduleName: item.moduleName,
|
|
100
|
+
});
|
|
101
|
+
const [first, second, ...rest] = pathSpec.path.split(".");
|
|
102
|
+
if (rest.length > 0)
|
|
103
|
+
throw new Error(`Shared command path is deeper than one group: ${family.name}.${pathSpec.path}`);
|
|
104
|
+
const command = toSubcommand(item.contribution, second ?? first, `${family.name}.${pathSpec.path}`);
|
|
105
|
+
if (!second) {
|
|
106
|
+
subcommands.push(command);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const current = groups.get(first);
|
|
110
|
+
const group = current ?? {
|
|
111
|
+
name: first,
|
|
112
|
+
description: pathSpec.groupDescription ?? `Manage ${family.name} ${first}`,
|
|
113
|
+
subcommands: [],
|
|
114
|
+
};
|
|
115
|
+
if (current &&
|
|
116
|
+
pathSpec.groupDescription &&
|
|
117
|
+
current.description !== pathSpec.groupDescription)
|
|
118
|
+
throw new Error(`Shared command group description mismatch: ${family.name}.${first}`);
|
|
119
|
+
groups.set(first, {
|
|
120
|
+
...group,
|
|
121
|
+
subcommands: [...group.subcommands, command],
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
...root,
|
|
126
|
+
contribution: {
|
|
127
|
+
...root.contribution,
|
|
128
|
+
contexts: [...contexts],
|
|
129
|
+
subcommands,
|
|
130
|
+
subcommandGroups: [...groups.values()],
|
|
131
|
+
},
|
|
132
|
+
routeOwners,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function toSubcommand(contribution, name, defaultPermissionId) {
|
|
136
|
+
const subcommand = {
|
|
137
|
+
...contribution,
|
|
138
|
+
name,
|
|
139
|
+
permissionId: contribution.permissionId ?? defaultPermissionId,
|
|
140
|
+
};
|
|
141
|
+
delete subcommand.family;
|
|
142
|
+
delete subcommand.path;
|
|
143
|
+
delete subcommand.contexts;
|
|
144
|
+
return subcommand;
|
|
145
|
+
}
|
|
146
|
+
function fromNexusContribution(contribution) {
|
|
147
|
+
return {
|
|
148
|
+
...contribution,
|
|
149
|
+
family: "nexus",
|
|
150
|
+
permissionId: `nexus.${contribution.path}`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function uniqueRegistrations(items) {
|
|
154
|
+
const result = new Map();
|
|
155
|
+
for (const item of items) {
|
|
156
|
+
const key = item.contribution.name;
|
|
157
|
+
if (result.has(key))
|
|
158
|
+
throw new Error(`Duplicate command name: ${key}`);
|
|
159
|
+
result.set(key, item);
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=shared-commands.js.map
|