@helyx/bot 0.4.37 → 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.
Files changed (64) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +17 -2
  3. package/dist/cloud-management-module.js +275 -206
  4. package/dist/command-registration-policy.d.ts +8 -0
  5. package/dist/command-registration-policy.js +39 -0
  6. package/dist/command-sync.d.ts +5 -1
  7. package/dist/command-sync.js +67 -3
  8. package/dist/control-server.d.ts +15 -3
  9. package/dist/control-server.js +34 -48
  10. package/dist/dashboard-commands.js +5 -1
  11. package/dist/developer-operations-module.d.ts +2 -0
  12. package/dist/developer-operations-module.js +65 -4
  13. package/dist/discord-rest.d.ts +5 -0
  14. package/dist/discord-rest.js +9 -0
  15. package/dist/events.js +4 -2
  16. package/dist/first-time-command-setup.d.ts +25 -0
  17. package/dist/first-time-command-setup.js +22 -0
  18. package/dist/guild-command-control-routes.d.ts +5 -0
  19. package/dist/guild-command-control-routes.js +191 -0
  20. package/dist/guild-command-discord-client.d.ts +21 -0
  21. package/dist/guild-command-discord-client.js +50 -0
  22. package/dist/guild-command-projection.d.ts +46 -0
  23. package/dist/guild-command-projection.js +254 -0
  24. package/dist/guild-command-read-service.d.ts +201 -0
  25. package/dist/guild-command-read-service.js +272 -0
  26. package/dist/guild-command-release.d.ts +19 -0
  27. package/dist/guild-command-release.js +47 -0
  28. package/dist/guild-command-worker.d.ts +49 -0
  29. package/dist/guild-command-worker.js +443 -0
  30. package/dist/guilds.d.ts +1 -1
  31. package/dist/guilds.js +41 -26
  32. package/dist/index.js +75 -4
  33. package/dist/interactions.d.ts +15 -4
  34. package/dist/interactions.js +285 -70
  35. package/dist/management-module.js +117 -41
  36. package/dist/module-logging.d.ts +1 -0
  37. package/dist/module-logging.js +5 -5
  38. package/dist/module-status-control.d.ts +19 -0
  39. package/dist/module-status-control.js +69 -0
  40. package/dist/modules.d.ts +2 -0
  41. package/dist/modules.js +21 -2
  42. package/dist/nexus-commands.d.ts +1 -12
  43. package/dist/nexus-commands.js +1 -99
  44. package/dist/oauth-module.js +134 -88
  45. package/dist/permissions-module.js +112 -39
  46. package/dist/privacy-module.d.ts +2 -0
  47. package/dist/privacy-module.js +60 -0
  48. package/dist/scheduled-task-runner.js +36 -1
  49. package/dist/shard-contracts.d.ts +24 -0
  50. package/dist/shard-contracts.js +20 -0
  51. package/dist/shard-router.d.ts +1 -0
  52. package/dist/shard-router.js +7 -0
  53. package/dist/shard-supervisor.d.ts +2 -1
  54. package/dist/shard-supervisor.js +94 -16
  55. package/dist/shard-worker.d.ts +5 -0
  56. package/dist/shard-worker.js +9 -1
  57. package/dist/shared-commands.d.ts +13 -0
  58. package/dist/shared-commands.js +163 -0
  59. package/dist/support-module.js +51 -51
  60. package/dist/trivia-data-provider.d.ts +26 -0
  61. package/dist/trivia-data-provider.js +51 -0
  62. package/package.json +5 -4
  63. package/dist/oauth-module.test.d.ts +0 -2
  64. package/dist/oauth-module.test.js +0 -172
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { parseBotEnvironment } from "@helyx/config";
2
2
  import { StatusHeartbeatReporter } from "@helyx/status-client";
3
3
  import { safeError, ServiceRegistry } from "@helyx/core";
4
- import { checkDatabaseConnection, createDatabasePool, AuditRepository, InteractionSessionRepository, RateLimitRepository, runCoreMigrations, runConfiguredModuleMigrations, } from "@helyx/database";
4
+ import { checkDatabaseConnection, createDatabasePool, AuditRepository, GuildCommandRepository, InteractionSessionRepository, RateLimitRepository, runCoreMigrations, runConfiguredModuleMigrations, } from "@helyx/database";
5
5
  import { createRuntime } from "@helyx/framework";
6
6
  import { createPlatformServices, gamingStatisticsRecordOwner, achievementContributionRecordOwner, PostgresPrivacyProviderExecutor, } from "@helyx/platform";
7
7
  import { HELYX_SERVICE_NAMES, } from "@helyx/sdk";
@@ -29,12 +29,19 @@ import { createDeveloperOperationsModule } from "./developer-operations-module.j
29
29
  import { shardHealthSnapshotSchema } from "./shard-contracts.js";
30
30
  import { createSupportModule } from "./support-module.js";
31
31
  import { createOAuthModule } from "./oauth-module.js";
32
+ import { createPrivacyModule } from "./privacy-module.js";
32
33
  import { HttpOAuthService } from "./oauth-service.js";
33
34
  import { DiscordOwnedMessageDeletionService } from "./discord-owned-message-service.js";
34
35
  import { DiscordGuildHealthInspection } from "./discord-guild-health.js";
36
+ import { GuildCommandWorker } from "./guild-command-worker.js";
37
+ import { GuildCommandDiscordClient } from "./guild-command-discord-client.js";
38
+ import { GuildCommandReadService } from "./guild-command-read-service.js";
39
+ import { scheduleGuildCommandRelease } from "./guild-command-release.js";
35
40
  import { CoreResourceAuthorizationService } from "./resource-authorization.js";
41
+ import { PostgresTriviaDataProvider } from "./trivia-data-provider.js";
36
42
  import { createLocalScheduledTaskRoute, createScheduledTaskRunner, schedulerOperationalHealth, } from "./scheduled-task-runner.js";
37
43
  import { createLocalNumericWriteBehind } from "./numeric-write-behind.js";
44
+ import { discordRestOptions } from "./discord-rest.js";
38
45
  export { loadConfiguredModulePackages } from "./modules.js";
39
46
  export { resolveModulePackageSpecifiers } from "./module-configuration.js";
40
47
  export * from "./control-server.js";
@@ -128,17 +135,26 @@ async function startUnshardedBot(processEnvironment) {
128
135
  verifiedMigrationCount: migrationResult.verified.length,
129
136
  }, "Core database ready");
130
137
  const cleanupExpiredCoreRecords = async () => {
131
- const [sessions, rateLimits, verificationAudit] = await Promise.all([
138
+ const [sessions, rateLimits, verificationAudit, commandRecords] = await Promise.all([
132
139
  new InteractionSessionRepository(database).deleteExpired(),
133
140
  new RateLimitRepository(database).deleteExpired(),
134
141
  new AuditRepository(database).deleteModuleEventsOlderThanMonths("helyx.verification", 12),
142
+ new GuildCommandRepository(database).deleteExpired(12),
135
143
  ]);
136
- if (sessions > 0 || rateLimits > 0 || verificationAudit > 0) {
144
+ const commandRecordCount = commandRecords.history +
145
+ commandRecords.idempotencyKeys +
146
+ commandRecords.creationLedger +
147
+ commandRecords.requests;
148
+ if (sessions > 0 ||
149
+ rateLimits > 0 ||
150
+ verificationAudit > 0 ||
151
+ commandRecordCount > 0) {
137
152
  bootstrapLogger.info({
138
153
  event: "database.retention_cleaned",
139
154
  interactionSessions: sessions,
140
155
  rateLimitWindows: rateLimits,
141
156
  verificationAuditEvents: verificationAudit,
157
+ commandRecords: commandRecordCount,
142
158
  }, "Expired core records removed");
143
159
  }
144
160
  };
@@ -169,6 +185,7 @@ async function startUnshardedBot(processEnvironment) {
169
185
  }, "Module databases ready");
170
186
  const featureModules = modulePackages.map(({ definition }) => definition);
171
187
  const client = new Client({
188
+ rest: discordRestOptions,
172
189
  intents: gatewayIntentsForModules(featureModules, environment.HELYX_MESSAGE_CONTENT_ENABLED, environment.HELYX_GUILD_MEMBERS_ENABLED),
173
190
  partials: [Partials.Message, Partials.Channel, Partials.Reaction],
174
191
  allowedMentions: { parse: [] },
@@ -192,6 +209,7 @@ async function startUnshardedBot(processEnvironment) {
192
209
  })
193
210
  : undefined;
194
211
  const supportModule = createSupportModule(cloudConnector);
212
+ const privacyModule = createPrivacyModule();
195
213
  const oauthModule = environment.HELYX_OAUTH_ENABLED
196
214
  ? createOAuthModule()
197
215
  : undefined;
@@ -199,11 +217,13 @@ async function startUnshardedBot(processEnvironment) {
199
217
  managementModule,
200
218
  permissionsModule,
201
219
  supportModule,
220
+ privacyModule,
202
221
  ...(oauthModule ? [oauthModule] : []),
203
222
  ...(cloudManagementModule ? [cloudManagementModule] : []),
204
223
  ...featureModules,
205
224
  ];
206
225
  let scheduledTasks;
226
+ let guildCommandWorker;
207
227
  const unshardedSupervisorId = randomUUID();
208
228
  const deployedCommit = deploymentReference(processEnvironment);
209
229
  const operationsModule = environment.DISCORD_INTERNAL_GUILD_ID
@@ -216,11 +236,13 @@ async function startUnshardedBot(processEnvironment) {
216
236
  managementModule,
217
237
  permissionsModule,
218
238
  supportModule,
239
+ privacyModule,
219
240
  ...(oauthModule ? [oauthModule] : []),
220
241
  ...(cloudManagementModule ? [cloudManagementModule] : []),
221
242
  ].map(({ manifest }) => manifest),
222
243
  modules: featureModules.map(({ manifest }) => manifest),
223
244
  processLabel: "unsharded bot process",
245
+ triviaData: new PostgresTriviaDataProvider(database),
224
246
  health: {
225
247
  getHealth: async () => {
226
248
  let databaseHealthy = true;
@@ -333,11 +355,13 @@ async function startUnshardedBot(processEnvironment) {
333
355
  ...(cloudManagementModule ? [cloudManagementModule.manifest.id] : []),
334
356
  ...(operationsModule ? [operationsModule.manifest.id] : []),
335
357
  ]);
336
- const prepareGuilds = installGuildTracking(client, database, publicModules, bootstrapLogger, enabledCoreModuleIds, undefined, environment.HELYX_GUILD_RECONCILIATION_INTERVAL_SECONDS * 1_000);
358
+ const prepareGuilds = installGuildTracking(client, database, publicModules, bootstrapLogger, enabledCoreModuleIds, undefined, environment.HELYX_GUILD_RECONCILIATION_INTERVAL_SECONDS * 1_000, undefined, environment.DISCORD_CLIENT_ID);
337
359
  const commandsReady = installInteractions(client, modules, {
360
+ globalCommandMode: environment.HELYX_GLOBAL_COMMAND_MODE,
338
361
  ...(environment.DISCORD_INTERNAL_GUILD_ID
339
362
  ? { internalGuildId: environment.DISCORD_INTERNAL_GUILD_ID }
340
363
  : {}),
364
+ synchroniseInternalGuild: !environment.HELYX_GUILD_COMMAND_WRITES_ENABLED,
341
365
  prepare: prepareGuilds,
342
366
  selfAuthorizingGuildModuleIds: new Set([
343
367
  supportModule.manifest.id,
@@ -383,6 +407,13 @@ async function startUnshardedBot(processEnvironment) {
383
407
  process.exitCode = 1;
384
408
  bootstrapLogger.error({ event: "scheduler.stop_failed", error: safeError(error) }, "Scheduled task runner shutdown failed");
385
409
  });
410
+ await guildCommandWorker?.stop().catch((error) => {
411
+ process.exitCode = 1;
412
+ bootstrapLogger.error({
413
+ event: "guild_commands.worker_stop_failed",
414
+ error: safeError(error),
415
+ }, "Guild command worker shutdown failed");
416
+ });
386
417
  bootstrapLogger.info({ event: "bot.shutdown", reason }, "Bot shutdown requested");
387
418
  await cloudConnector.stop().catch((error) => {
388
419
  process.exitCode = 1;
@@ -442,12 +473,51 @@ async function startUnshardedBot(processEnvironment) {
442
473
  workerId: `unsharded:${unshardedSupervisorId}`,
443
474
  });
444
475
  scheduledTasks.start();
476
+ guildCommandWorker = new GuildCommandWorker({
477
+ database,
478
+ modules,
479
+ logger: bootstrapLogger,
480
+ workerId: `unsharded-command:${unshardedSupervisorId}`,
481
+ fencingToken: 1n,
482
+ ...(environment.DISCORD_INTERNAL_GUILD_ID
483
+ ? { internalGuildId: environment.DISCORD_INTERNAL_GUILD_ID }
484
+ : {}),
485
+ ...(environment.HELYX_GUILD_COMMAND_WRITES_ENABLED
486
+ ? {
487
+ discord: new GuildCommandDiscordClient({
488
+ applicationId: environment.DISCORD_CLIENT_ID,
489
+ token: environment.DISCORD_TOKEN,
490
+ writeGuildIds: environment.HELYX_GUILD_COMMAND_WRITE_GUILD_IDS,
491
+ writeAllGuilds: environment.HELYX_GUILD_COMMAND_WRITE_SCOPE === "all",
492
+ }),
493
+ }
494
+ : {}),
495
+ });
496
+ guildCommandWorker.start();
497
+ if (environment.HELYX_GUILD_COMMAND_WRITES_ENABLED &&
498
+ environment.HELYX_GUILD_COMMAND_WRITE_SCOPE === "all") {
499
+ void scheduleGuildCommandRelease({
500
+ database,
501
+ applicationId: environment.DISCORD_CLIENT_ID,
502
+ releaseId: environment.HELYX_GUILD_COMMAND_RELEASE_ID,
503
+ })
504
+ .then((release) => {
505
+ bootstrapLogger.info({ event: "guild_commands.release_scheduled", ...release }, "Guild command release rollout prepared");
506
+ })
507
+ .catch((error) => {
508
+ bootstrapLogger.error({
509
+ event: "guild_commands.release_schedule_failed",
510
+ error: safeError(error),
511
+ }, "Guild command release rollout could not be prepared");
512
+ });
513
+ }
445
514
  await cloudConnector.start();
446
515
  if (environment.HELYX_BOT_CONTROL_SECRET) {
447
516
  controlServer = await createBotControlServer({
448
517
  service: controlService,
449
518
  secret: environment.HELYX_BOT_CONTROL_SECRET,
450
519
  logger: { level: environment.LOG_LEVEL },
520
+ 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", modules),
451
521
  });
452
522
  await controlServer.listen({
453
523
  host: "::",
@@ -464,6 +534,7 @@ async function startUnshardedBot(processEnvironment) {
464
534
  }
465
535
  catch (error) {
466
536
  await scheduledTasks?.stop().catch(() => undefined);
537
+ await guildCommandWorker?.stop().catch(() => undefined);
467
538
  await cloudConnector.stop().catch(() => undefined);
468
539
  if (controlServer)
469
540
  await controlServer.close().catch(() => undefined);
@@ -1,6 +1,9 @@
1
- import type { CommandContribution, MessageResponse, ModalDefinition, ModuleDefinition, ServiceAccess } from "@helyx/sdk";
2
- import { ModalBuilder, type ChatInputCommandInteraction, type Client, type InteractionReplyOptions, type InteractionUpdateOptions } from "discord.js";
1
+ import type { CommandAutocompleteContext, CommandAuthorization, CommandContribution, MessageResponse, ModalDefinition, ModuleDefinition, ServiceAccess } from "@helyx/sdk";
2
+ import { type GlobalCommandRegistrationMode } from "./command-registration-policy.js";
3
+ import { ModalBuilder, type AutocompleteInteraction, type ChatInputCommandInteraction, type Client, type InteractionReplyOptions, type InteractionUpdateOptions } from "discord.js";
3
4
  export declare const GLOBAL_MODULE_UNAVAILABLE_MESSAGE = "This module is temporarily unavailable for all hosted servers.";
5
+ export declare const AUTOCOMPLETE_RESPONSE_DEADLINE_MS = 2000;
6
+ export declare const DISCORD_DNS_REFRESH_INTERVAL_MS = 30000;
4
7
  export interface InteractionLogger {
5
8
  info(fields: Record<string, unknown>, message: string): void;
6
9
  warn(fields: Record<string, unknown>, message: string): void;
@@ -10,10 +13,14 @@ export interface CommandRegistration {
10
13
  internalGuildId?: string;
11
14
  prepare?(client: Client<true>): Promise<void>;
12
15
  synchronise?: boolean;
16
+ synchroniseInternalGuild?: boolean;
17
+ globalCommandMode?: GlobalCommandRegistrationMode;
13
18
  selfAuthorizingGuildModuleIds?: ReadonlySet<string>;
14
19
  }
15
20
  export declare function installInteractions(client: Client, modules: readonly ModuleDefinition[], registration: CommandRegistration, logger: InteractionLogger, services: ServiceAccess): Promise<void>;
16
- export declare function buildApplicationCommandDefinitions(modules: readonly ModuleDefinition[], internalGuildId?: string): {
21
+ export declare function warmDiscordRestConnection(client: Client<true>, logger: InteractionLogger): Promise<void>;
22
+ export declare function startDiscordDnsRefresh(logger: InteractionLogger, lookup?: (hostname: string) => Promise<unknown>, intervalMilliseconds?: number): ReturnType<typeof setInterval>;
23
+ export declare function buildApplicationCommandDefinitions(modules: readonly ModuleDefinition[], internalGuildId?: string, globalCommandMode?: GlobalCommandRegistrationMode): {
17
24
  global: ReturnType<typeof toCommandDefinition>[];
18
25
  guild: ReturnType<typeof toCommandDefinition>[];
19
26
  };
@@ -22,13 +29,17 @@ export declare function toCommandDefinition(registered: {
22
29
  contribution: CommandContribution;
23
30
  }): import("discord.js").RESTPostAPIChatInputApplicationCommandsJSONBody;
24
31
  export declare function uniqueMap<T>(items: readonly T[], keyOf: (item: T) => string, label: string): Map<string, T>;
32
+ export declare function createAutocompleteResponseController(interaction: AutocompleteInteraction, logger: InteractionLogger): {
33
+ respond: CommandAutocompleteContext["respond"];
34
+ ensureResponded(): Promise<void>;
35
+ };
25
36
  export declare function sendToChannel(client: Client, channelId: string, response: MessageResponse, deliveryId?: string): Promise<{
26
37
  messageId: string;
27
38
  }>;
28
39
  export declare function deleteChannelMessage(client: Client, channelId: string, messageId: string): Promise<void>;
29
40
  export declare function editChannelMessage(client: Client, channelId: string, messageId: string, response: MessageResponse): Promise<void>;
30
41
  export declare function addChannelMessageReaction(client: Client, channelId: string, messageId: string, emojiIdentifier: string): Promise<void>;
31
- export declare function authorizeCommand(interaction: ChatInputCommandInteraction, moduleId: string, commandId: string, rateLimit: CommandContribution["rateLimit"], services: ServiceAccess, selfAuthorizingGuildCommand: boolean, resourceScopedCommand?: boolean, requiredModuleName?: string): Promise<boolean>;
42
+ export declare function authorizeCommand(interaction: ChatInputCommandInteraction, moduleId: string, commandId: string, rateLimit: CommandContribution["rateLimit"], services: ServiceAccess, selfAuthorizingGuildCommand: boolean, resourceScopedCommand?: boolean, requiredModuleName?: string, installationIndependentCommand?: boolean, authorization?: CommandAuthorization): Promise<boolean>;
32
43
  export declare function normalizeDiscordPermission(permission: string): string;
33
44
  export declare function toReplyOptions(response: MessageResponse): InteractionReplyOptions;
34
45
  export declare function toUpdateOptions(response: MessageResponse): InteractionUpdateOptions;