@helyx/bot 1.3.1 → 1.5.0

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 (97) hide show
  1. package/CHANGELOG.md +100 -28
  2. package/README.md +65 -11
  3. package/dist/channel-audit-configuration.d.ts +7 -0
  4. package/dist/channel-audit-configuration.js +40 -0
  5. package/dist/channel-audit-control.d.ts +14 -0
  6. package/dist/channel-audit-control.js +61 -0
  7. package/dist/channel-audit-permissions.d.ts +56 -0
  8. package/dist/channel-audit-permissions.js +417 -0
  9. package/dist/channel-audit-routes.d.ts +4 -0
  10. package/dist/channel-audit-routes.js +42 -0
  11. package/dist/cloud-connector.d.ts +11 -4
  12. package/dist/cloud-connector.js +54 -15
  13. package/dist/cloud-control-dispatcher.d.ts +8 -3
  14. package/dist/cloud-control-dispatcher.js +84 -4
  15. package/dist/control-errors.d.ts +12 -0
  16. package/dist/control-errors.js +32 -0
  17. package/dist/control-server-schemas.d.ts +84 -0
  18. package/dist/control-server-schemas.js +101 -0
  19. package/dist/control-server.d.ts +25 -8
  20. package/dist/control-server.js +101 -152
  21. package/dist/core-retention.d.ts +26 -0
  22. package/dist/core-retention.js +49 -0
  23. package/dist/creator-guild-join-service.d.ts +25 -0
  24. package/dist/creator-guild-join-service.js +218 -0
  25. package/dist/creator-invite-service.d.ts +37 -0
  26. package/dist/creator-invite-service.js +686 -0
  27. package/dist/creator-onboarding-control.d.ts +12 -0
  28. package/dist/creator-onboarding-control.js +65 -0
  29. package/dist/creator-onboarding-platform.d.ts +20 -0
  30. package/dist/creator-onboarding-platform.js +58 -0
  31. package/dist/dashboard-action-control.d.ts +2 -2
  32. package/dist/dashboard-action-control.js +2 -3
  33. package/dist/database-bootstrap.d.ts +36 -0
  34. package/dist/database-bootstrap.js +101 -0
  35. package/dist/developer-operations-module.d.ts +1 -0
  36. package/dist/developer-operations-module.js +34 -16
  37. package/dist/discord-channel-audit.d.ts +9 -0
  38. package/dist/discord-channel-audit.js +195 -0
  39. package/dist/discord-guild-health.d.ts +2 -2
  40. package/dist/discord-owned-message-service.d.ts +1 -0
  41. package/dist/discord-owned-message-service.js +3 -0
  42. package/dist/discord-role-safety.d.ts +4 -0
  43. package/dist/discord-role-safety.js +30 -0
  44. package/dist/events.d.ts +2 -0
  45. package/dist/events.js +5 -20
  46. package/dist/guild-command-read-service.d.ts +8 -3
  47. package/dist/guild-command-read-service.js +14 -7
  48. package/dist/guild-command-release.d.ts +15 -8
  49. package/dist/guild-command-release.js +10 -3
  50. package/dist/guild-command-worker.d.ts +17 -4
  51. package/dist/guild-command-worker.js +17 -6
  52. package/dist/guilds.d.ts +24 -3
  53. package/dist/guilds.js +185 -64
  54. package/dist/hosted-server-moderation.js +10 -4
  55. package/dist/index.d.ts +1 -0
  56. package/dist/index.js +200 -82
  57. package/dist/interactions.js +16 -3
  58. package/dist/module-configuration.d.ts +1 -1
  59. package/dist/module-configuration.js +5 -2
  60. package/dist/module-logging.d.ts +8 -3
  61. package/dist/module-logging.js +13 -7
  62. package/dist/modules.d.ts +1 -0
  63. package/dist/modules.js +8 -1
  64. package/dist/mysql-runtime-persistence.d.ts +46 -0
  65. package/dist/mysql-runtime-persistence.js +91 -0
  66. package/dist/oauth-module.js +2 -2
  67. package/dist/oauth-service.d.ts +7 -0
  68. package/dist/oauth-service.js +3 -0
  69. package/dist/scheduled-task-runner.d.ts +9 -85
  70. package/dist/scheduled-task-runner.js +1 -1
  71. package/dist/shard-contracts.d.ts +222 -0
  72. package/dist/shard-contracts.js +52 -1
  73. package/dist/shard-router.d.ts +4 -1
  74. package/dist/shard-router.js +19 -0
  75. package/dist/shard-supervisor.js +43 -9
  76. package/dist/shard-worker.d.ts +5 -2
  77. package/dist/shard-worker.js +41 -5
  78. package/dist/supervisor-cloud-directory.d.ts +9 -3
  79. package/dist/supervisor-cloud-directory.js +8 -12
  80. package/dist/trivia-data-provider.d.ts +11 -0
  81. package/dist/trivia-data-provider.js +46 -26
  82. package/dist/verification-service.d.ts +2 -2
  83. package/dist/verification-service.js +6 -3
  84. package/documentation/README.md +5 -3
  85. package/documentation/examples/.env.example +15 -11
  86. package/documentation/guides/getting-started.md +29 -12
  87. package/documentation/guides/privacy-and-support.md +6 -5
  88. package/documentation/guides/self-hosting.md +16 -11
  89. package/documentation/modules/auto-moderation.md +7 -5
  90. package/documentation/modules/building-a-module.md +48 -2
  91. package/documentation/modules/moderation.md +5 -5
  92. package/documentation/modules/publishing-a-module.md +6 -4
  93. package/documentation/modules/testing-a-module.md +39 -4
  94. package/documentation/reference/environment.md +43 -9
  95. package/documentation/runbooks/backup-and-recovery.md +26 -7
  96. package/documentation/runbooks/installation-and-upgrades.md +97 -27
  97. package/package.json +20 -16
@@ -4,6 +4,7 @@ export declare class DiscordOwnedMessageDeletionService implements DiscordOwnedM
4
4
  #private;
5
5
  private readonly client;
6
6
  constructor(client: Client);
7
+ isReady(): boolean;
7
8
  addOwnedMessageReaction(input: {
8
9
  guildId: string;
9
10
  channelId: string;
@@ -6,6 +6,9 @@ export class DiscordOwnedMessageDeletionService {
6
6
  constructor(client) {
7
7
  this.client = client;
8
8
  }
9
+ isReady() {
10
+ return this.client.isReady();
11
+ }
9
12
  async addOwnedMessageReaction(input) {
10
13
  const emoji = prepareEmojiIdentifier(input.emojiIdentifier);
11
14
  const message = await this.#fetchOwnedMessage(input);
@@ -0,0 +1,4 @@
1
+ /** The existing Helyx role-grant policy, shared by delivery and inspection. */
2
+ export declare const dangerousRolePermissionNames: Set<string>;
3
+ export declare function dangerousRolePermissions(permissions: bigint): string[];
4
+ //# sourceMappingURL=discord-role-safety.d.ts.map
@@ -0,0 +1,30 @@
1
+ import { PermissionsBitField } from "discord.js";
2
+ /** The existing Helyx role-grant policy, shared by delivery and inspection. */
3
+ export const dangerousRolePermissionNames = new Set([
4
+ "ADMINISTRATOR",
5
+ "BAN_MEMBERS",
6
+ "DEAFEN_MEMBERS",
7
+ "KICK_MEMBERS",
8
+ "MANAGE_CHANNELS",
9
+ "MANAGE_EVENTS",
10
+ "MANAGE_GUILD",
11
+ "MANAGE_GUILD_EXPRESSIONS",
12
+ "MANAGE_MESSAGES",
13
+ "MANAGE_NICKNAMES",
14
+ "MANAGE_ROLES",
15
+ "MANAGE_THREADS",
16
+ "MANAGE_WEBHOOKS",
17
+ "MENTION_EVERYONE",
18
+ "MODERATE_MEMBERS",
19
+ "MOVE_MEMBERS",
20
+ "MUTE_MEMBERS",
21
+ "VIEW_AUDIT_LOG",
22
+ ]);
23
+ export function dangerousRolePermissions(permissions) {
24
+ return new PermissionsBitField(permissions)
25
+ .toArray()
26
+ .map((permission) => permission.replaceAll(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase())
27
+ .filter((permission) => dangerousRolePermissionNames.has(permission))
28
+ .sort();
29
+ }
30
+ //# sourceMappingURL=discord-role-safety.js.map
package/dist/events.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { DiscordChannelAudienceInspection, DiscordChannelInspection, DiscordEmojiInspection, DiscordMessageInspection, DiscordGuildCapabilityInspection, DiscordGuildChannelAudienceInspection, DiscordResourceInspectionService, DiscordRoleInspection, DiscordMessageService, InstallationService, MemberEnforcementService, MemberRoleService, ModuleDefinition, ServiceAccess, VerificationMemberService } from "@helyx/sdk";
2
+ import { inspectDiscordChannelAudit } from "./discord-channel-audit.js";
2
3
  import { type Client, type MessageReaction, type PartialMessageReaction } from "discord.js";
3
4
  import { type InteractionLogger } from "./interactions.js";
4
5
  import type { TransientViolationEvidenceStore } from "./transient-violation-evidence.js";
@@ -80,6 +81,7 @@ export declare class DiscordResourceInspection implements DiscordResourceInspect
80
81
  #private;
81
82
  private readonly client;
82
83
  constructor(client: Client);
84
+ inspectChannelAudit(input: Parameters<typeof inspectDiscordChannelAudit>[1]): Promise<import("@helyx/sdk").ChannelAuditReport>;
83
85
  inspectGuildCapabilities(input: {
84
86
  guildId: string;
85
87
  }): Promise<DiscordGuildCapabilityInspection>;
package/dist/events.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { HELYX_SERVICE_NAMES } from "@helyx/sdk";
2
+ import { inspectDiscordChannelAudit } from "./discord-channel-audit.js";
3
+ import { dangerousRolePermissionNames } from "./discord-role-safety.js";
2
4
  import { safeError } from "@helyx/core";
3
5
  import { ChannelType, Events, GatewayIntentBits, GuildMemberFlags, PermissionFlagsBits, RESTJSONErrorCodes, UserFlags, } from "discord.js";
4
6
  import { toReplyOptions } from "./interactions.js";
@@ -1272,26 +1274,6 @@ export function reactionCacheKey(emojiIdentifier) {
1272
1274
  const discordSnowflake = /^\d{17,20}$/u;
1273
1275
  const moduleIdentifier = /^helyx\.[a-z0-9][a-z0-9-]*$/u;
1274
1276
  const customEmojiIdentifier = /^(?:<a?:([A-Za-z0-9_]{2,32}):(\d{17,20})>|([A-Za-z0-9_]{2,32}):(\d{17,20}))$/u;
1275
- const dangerousRolePermissionNames = new Set([
1276
- "ADMINISTRATOR",
1277
- "BAN_MEMBERS",
1278
- "DEAFEN_MEMBERS",
1279
- "KICK_MEMBERS",
1280
- "MANAGE_CHANNELS",
1281
- "MANAGE_EVENTS",
1282
- "MANAGE_GUILD",
1283
- "MANAGE_GUILD_EXPRESSIONS",
1284
- "MANAGE_MESSAGES",
1285
- "MANAGE_NICKNAMES",
1286
- "MANAGE_ROLES",
1287
- "MANAGE_THREADS",
1288
- "MANAGE_WEBHOOKS",
1289
- "MENTION_EVERYONE",
1290
- "MODERATE_MEMBERS",
1291
- "MOVE_MEMBERS",
1292
- "MUTE_MEMBERS",
1293
- "VIEW_AUDIT_LOG",
1294
- ]);
1295
1277
  const directMessageAttemptTimeoutMs = 5_000;
1296
1278
  const guildChannelAudienceInspectionLimit = 500;
1297
1279
  export class DiscordMessageDeliveryService {
@@ -1696,6 +1678,9 @@ export class DiscordResourceInspection {
1696
1678
  constructor(client) {
1697
1679
  this.client = client;
1698
1680
  }
1681
+ inspectChannelAudit(input) {
1682
+ return inspectDiscordChannelAudit(this.client, input);
1683
+ }
1699
1684
  async inspectGuildCapabilities(input) {
1700
1685
  validateSnowflake(input.guildId, "guild");
1701
1686
  const guild = await this.client.guilds.fetch(input.guildId);
@@ -1,4 +1,5 @@
1
- import { type GuildCommandBudgetClass, type GuildCommandHealth, type GuildCommandHistoryRecord, type GuildCommandRequestRecord, type GuildCommandRequestSource, type GuildCommandStateRecord, type GuildCommandVisibilityProfile } from "@helyx/database";
1
+ import { GuildCommandRepository, InstallationRepository, type GuildCommandBudgetClass, type GuildCommandHealth, type GuildCommandHistoryRecord, type GuildCommandRequestRecord, type GuildCommandRequestSource, type GuildCommandStateRecord, type GuildCommandVisibilityProfile } from "@helyx/database";
2
+ import { type GuildCommandRepository as MysqlGuildCommandRepository, type InstallationRepository as MysqlInstallationRepository } from "@helyx/database-mysql";
2
3
  import type { ModuleDefinition } from "@helyx/sdk";
3
4
  import type { Pool } from "pg";
4
5
  import type { GuildCommandWorkerHealth } from "./guild-command-worker.js";
@@ -78,9 +79,13 @@ export interface GuildCommandReadHandler {
78
79
  scheduledGuildCount: number;
79
80
  }>;
80
81
  }
82
+ type ReadCommandMethod = "getState" | "getActiveRequest" | "getCapacity" | "ensureState" | "schedule" | "recordStatusRefresh" | "changeVisibilityProfile" | "setMigrationHold" | "listHistory" | "getHealth";
83
+ export interface GuildCommandReadRepositories {
84
+ commands: Pick<GuildCommandRepository, ReadCommandMethod> & Pick<MysqlGuildCommandRepository, ReadCommandMethod>;
85
+ installations: Pick<InstallationRepository, "listActiveGuildIdsWithEnabledModule"> & Pick<MysqlInstallationRepository, "listActiveGuildIdsWithEnabledModule">;
86
+ }
81
87
  export declare class GuildCommandReadService implements GuildCommandReadHandler {
82
88
  #private;
83
- private readonly database;
84
89
  private readonly applicationId;
85
90
  private readonly workerHealth?;
86
91
  private readonly discordToken?;
@@ -88,7 +93,7 @@ export declare class GuildCommandReadService implements GuildCommandReadHandler
88
93
  private readonly writeGuildIds;
89
94
  private readonly writeAllGuilds;
90
95
  private readonly modules;
91
- constructor(database: Pool, applicationId: string, workerHealth?: (() => Promise<GuildCommandWorkerHealth> | undefined) | undefined, discordToken?: string | undefined, guildCommandWritesEnabled?: boolean, writeGuildIds?: readonly string[], writeAllGuilds?: boolean, modules?: readonly ModuleDefinition[]);
96
+ constructor(databaseOrRepositories: Pool | GuildCommandReadRepositories, applicationId: string, workerHealth?: (() => Promise<GuildCommandWorkerHealth> | undefined) | undefined, discordToken?: string | undefined, guildCommandWritesEnabled?: boolean, writeGuildIds?: readonly string[], writeAllGuilds?: boolean, modules?: readonly ModuleDefinition[]);
92
97
  previewFirstTimeSetup(moduleIds: string[]): Promise<{
93
98
  selectedModuleIds: string[];
94
99
  rootCount: number;
@@ -1,10 +1,10 @@
1
1
  import { GuildCommandRepository, GuildCommandRevisionConflictError, InstallationRepository, } from "@helyx/database";
2
+ import { GuildCommandRevisionConflictError as MysqlGuildCommandRevisionConflictError, } from "@helyx/database-mysql";
2
3
  import { isCustomerSelectableModule } from "@helyx/module-manifest";
3
4
  import { createHash } from "node:crypto";
4
5
  import { REST, Routes } from "discord.js";
5
6
  import { guildCommandDefinitionHash, projectGuildCommands, } from "./guild-command-projection.js";
6
7
  export class GuildCommandReadService {
7
- database;
8
8
  applicationId;
9
9
  workerHealth;
10
10
  discordToken;
@@ -13,8 +13,8 @@ export class GuildCommandReadService {
13
13
  writeAllGuilds;
14
14
  modules;
15
15
  #repository;
16
- constructor(database, applicationId, workerHealth, discordToken, guildCommandWritesEnabled = false, writeGuildIds = [], writeAllGuilds = false, modules = []) {
17
- this.database = database;
16
+ #installations;
17
+ constructor(databaseOrRepositories, applicationId, workerHealth, discordToken, guildCommandWritesEnabled = false, writeGuildIds = [], writeAllGuilds = false, modules = []) {
18
18
  this.applicationId = applicationId;
19
19
  this.workerHealth = workerHealth;
20
20
  this.discordToken = discordToken;
@@ -22,7 +22,14 @@ export class GuildCommandReadService {
22
22
  this.writeGuildIds = writeGuildIds;
23
23
  this.writeAllGuilds = writeAllGuilds;
24
24
  this.modules = modules;
25
- this.#repository = new GuildCommandRepository(this.database);
25
+ const repositories = "commands" in databaseOrRepositories
26
+ ? databaseOrRepositories
27
+ : {
28
+ commands: new GuildCommandRepository(databaseOrRepositories),
29
+ installations: new InstallationRepository(databaseOrRepositories),
30
+ };
31
+ this.#repository = repositories.commands;
32
+ this.#installations = repositories.installations;
26
33
  }
27
34
  previewFirstTimeSetup(moduleIds) {
28
35
  const requested = new Set(moduleIds);
@@ -97,11 +104,10 @@ export class GuildCommandReadService {
97
104
  return this.getStatus(input.guildId);
98
105
  }
99
106
  async reconcileModuleAvailability(input) {
100
- const installations = new InstallationRepository(this.database);
101
107
  let cursor;
102
108
  let scheduledGuildCount = 0;
103
109
  do {
104
- const page = await installations.listActiveGuildIdsWithEnabledModule({
110
+ const page = await this.#installations.listActiveGuildIdsWithEnabledModule({
105
111
  moduleId: input.moduleId,
106
112
  ...(cursor ? { cursor } : {}),
107
113
  limit: 500,
@@ -152,7 +158,8 @@ export class GuildCommandReadService {
152
158
  });
153
159
  }
154
160
  catch (error) {
155
- if (!(error instanceof GuildCommandRevisionConflictError))
161
+ if (!(error instanceof GuildCommandRevisionConflictError) &&
162
+ !(error instanceof MysqlGuildCommandRevisionConflictError))
156
163
  throw error;
157
164
  }
158
165
  return this.getStatus(input.guildId);
@@ -1,16 +1,23 @@
1
1
  import { GuildCommandRepository, InstallationRepository, RuntimeStateRepository, type DatabasePool } from "@helyx/database";
2
- type ReleaseInstallationRepository = Pick<InstallationRepository, "listActiveGuilds">;
3
- type ReleaseCommandRepository = Pick<GuildCommandRepository, "getState" | "schedule">;
4
- type ReleaseRuntimeRepository = Pick<RuntimeStateRepository, "get" | "compareAndSet">;
5
- export declare function scheduleGuildCommandRelease(input: {
2
+ import type { GuildCommandRepository as MysqlGuildCommandRepository, InstallationRepository as MysqlInstallationRepository, RuntimeStateRepository as MysqlRuntimeStateRepository } from "@helyx/database-mysql";
3
+ type ReleaseInstallationRepository = Pick<InstallationRepository, "listActiveGuilds"> & Pick<MysqlInstallationRepository, "listActiveGuilds">;
4
+ type ReleaseCommandRepository = Pick<GuildCommandRepository, "getState" | "schedule"> & Pick<MysqlGuildCommandRepository, "getState" | "schedule">;
5
+ type ReleaseRuntimeRepository = Pick<RuntimeStateRepository, "get" | "compareAndSet"> & Pick<MysqlRuntimeStateRepository, "get" | "compareAndSet">;
6
+ type ReleaseRepositories = {
7
+ installations: ReleaseInstallationRepository;
8
+ commands: ReleaseCommandRepository;
9
+ runtime: ReleaseRuntimeRepository;
10
+ };
11
+ type ReleasePersistence = ({
6
12
  database: DatabasePool;
13
+ } & Partial<ReleaseRepositories>) | ({
14
+ database?: never;
15
+ } & ReleaseRepositories);
16
+ export declare function scheduleGuildCommandRelease(input: {
7
17
  applicationId: string;
8
18
  releaseId: string;
9
19
  staggerMs?: number;
10
- installations?: ReleaseInstallationRepository;
11
- commands?: ReleaseCommandRepository;
12
- runtime?: ReleaseRuntimeRepository;
13
- }): Promise<{
20
+ } & ReleasePersistence): Promise<{
14
21
  scheduled: number;
15
22
  skippedIncomplete: number;
16
23
  replay: boolean;
@@ -6,9 +6,16 @@ export async function scheduleGuildCommandRelease(input) {
6
6
  if (!RELEASE_ID.test(input.releaseId))
7
7
  throw new Error("Guild command release ID is invalid");
8
8
  const markerKey = `guild-commands:release:${input.applicationId}:${input.releaseId}`;
9
- const installations = input.installations ?? new InstallationRepository(input.database);
10
- const commands = input.commands ?? new GuildCommandRepository(input.database);
11
- const runtime = input.runtime ?? new RuntimeStateRepository(input.database);
9
+ const database = input.database;
10
+ const installations = input.installations ??
11
+ (database ? new InstallationRepository(database) : undefined);
12
+ const commands = input.commands ??
13
+ (database ? new GuildCommandRepository(database) : undefined);
14
+ const runtime = input.runtime ??
15
+ (database ? new RuntimeStateRepository(database) : undefined);
16
+ if (!installations || !commands || !runtime) {
17
+ throw new Error("Guild command release requires a database or all three repositories");
18
+ }
12
19
  if (await runtime.get(markerKey))
13
20
  return { scheduled: 0, skippedIncomplete: 0, replay: true };
14
21
  const guilds = await installations.listActiveGuilds();
@@ -1,4 +1,5 @@
1
- import { GuildCommandRepository, type GuildCommandHealth, type GuildCommandRequestRecord } from "@helyx/database";
1
+ import { GuildCommandRepository, InstallationRepository, ModuleAvailabilityRepository, type GuildCommandHealth, type GuildCommandRequestRecord } from "@helyx/database";
2
+ import { type GuildCommandRepository as MysqlGuildCommandRepository, type InstallationRepository as MysqlInstallationRepository, type ModuleAvailabilityRepository as MysqlModuleAvailabilityRepository } from "@helyx/database-mysql";
2
3
  import type { Logger, ModuleDefinition } from "@helyx/sdk";
3
4
  import type { Pool } from "pg";
4
5
  import { type GuildCommandDiff, type GuildCommandProjection } from "./guild-command-projection.js";
@@ -9,10 +10,22 @@ export interface GuildCommandWorkerHealth extends GuildCommandHealth {
9
10
  workerRunning: boolean;
10
11
  activeCount: number;
11
12
  }
13
+ type WorkerCommandRepository = Pick<GuildCommandRepository, "claimDue" | "getHealth" | "heartbeat" | "getState" | "completeDryRun" | "defer" | "markBlocked" | "markFailed" | "completeLive" | "isLeaseCurrent" | "reserveCreations" | "settleCreation"> & Pick<MysqlGuildCommandRepository, "claimDue" | "getHealth" | "heartbeat" | "getState" | "completeDryRun" | "defer" | "markBlocked" | "markFailed" | "completeLive" | "isLeaseCurrent" | "reserveCreations" | "settleCreation">;
14
+ export interface GuildCommandWorkerRepositories {
15
+ commands: WorkerCommandRepository;
16
+ installations: Pick<InstallationRepository, "listEnabledModules"> & Pick<MysqlInstallationRepository, "listEnabledModules">;
17
+ availability: Pick<ModuleAvailabilityRepository, "list"> & Pick<MysqlModuleAvailabilityRepository, "list">;
18
+ }
19
+ type WorkerPersistence = {
20
+ database: Pool;
21
+ repositories?: never;
22
+ } | {
23
+ database?: never;
24
+ repositories: GuildCommandWorkerRepositories;
25
+ };
12
26
  export declare class GuildCommandWorker {
13
27
  #private;
14
28
  constructor(input: {
15
- database: Pool;
16
29
  modules: readonly ModuleDefinition[];
17
30
  logger: Logger;
18
31
  workerId?: string;
@@ -24,13 +37,13 @@ export declare class GuildCommandWorker {
24
37
  claimLimit?: number;
25
38
  concurrency?: number;
26
39
  stopTimeoutMs?: number;
27
- });
40
+ } & WorkerPersistence);
28
41
  start(): void;
29
42
  runOnce(): Promise<void>;
30
43
  stop(): Promise<void>;
31
44
  getHealth(): Promise<GuildCommandWorkerHealth>;
32
45
  }
33
- type LiveRepository = Pick<GuildCommandRepository, "completeLive" | "isLeaseCurrent" | "reserveCreations" | "settleCreation">;
46
+ type LiveRepository = Pick<GuildCommandRepository, "completeLive" | "isLeaseCurrent" | "reserveCreations" | "settleCreation"> & Pick<MysqlGuildCommandRepository, "completeLive" | "isLeaseCurrent" | "reserveCreations" | "settleCreation">;
34
47
  export declare function reconcileLiveGuildCommands(input: {
35
48
  repository: LiveRepository;
36
49
  discord: Pick<GuildCommandDiscordClient, "observe" | "replace">;
@@ -1,4 +1,5 @@
1
1
  import { GuildCommandBudgetExceededError, GuildCommandRepository, InstallationRepository, ModuleAvailabilityRepository, } from "@helyx/database";
2
+ import { GuildCommandBudgetExceededError as MysqlGuildCommandBudgetExceededError, } from "@helyx/database-mysql";
2
3
  import { randomUUID } from "node:crypto";
3
4
  import { diffDryRunGuildCommands, diffGuildCommands, projectGuildCommands, } from "./guild-command-projection.js";
4
5
  const DEFAULT_POLL_INTERVAL_MS = 1_000;
@@ -8,7 +9,8 @@ const DEFAULT_CONCURRENCY = 2;
8
9
  const DEFAULT_STOP_TIMEOUT_MS = 5_000;
9
10
  export class GuildCommandWorker {
10
11
  #repository;
11
- #database;
12
+ #installations;
13
+ #availability;
12
14
  #modules;
13
15
  #logger;
14
16
  #workerId;
@@ -28,8 +30,16 @@ export class GuildCommandWorker {
28
30
  constructor(input) {
29
31
  if (input.fencingToken < 1n)
30
32
  throw new Error("Guild command worker fencing token must be positive");
31
- this.#database = input.database;
32
- this.#repository = new GuildCommandRepository(input.database);
33
+ const repositories = input.database
34
+ ? {
35
+ commands: new GuildCommandRepository(input.database),
36
+ installations: new InstallationRepository(input.database),
37
+ availability: new ModuleAvailabilityRepository(input.database),
38
+ }
39
+ : input.repositories;
40
+ this.#repository = repositories.commands;
41
+ this.#installations = repositories.installations;
42
+ this.#availability = repositories.availability;
33
43
  this.#modules = input.modules;
34
44
  this.#logger = input.logger;
35
45
  this.#workerId = input.workerId ?? `guild-command:${randomUUID()}`;
@@ -145,8 +155,8 @@ export class GuildCommandWorker {
145
155
  heartbeat.unref();
146
156
  try {
147
157
  const [enabled, availability, state] = await Promise.all([
148
- new InstallationRepository(this.#database).listEnabledModules(request.guildId),
149
- new ModuleAvailabilityRepository(this.#database).list(),
158
+ this.#installations.listEnabledModules(request.guildId),
159
+ this.#availability.list(),
150
160
  this.#repository.getState(request.applicationId, request.guildId),
151
161
  ]);
152
162
  if (!state || state.desiredRevision !== request.requestedRevision) {
@@ -310,7 +320,8 @@ export async function reconcileLiveGuildCommands(input) {
310
320
  })).ledgerId;
311
321
  }
312
322
  catch (error) {
313
- if (error instanceof GuildCommandBudgetExceededError)
323
+ if (error instanceof GuildCommandBudgetExceededError ||
324
+ error instanceof MysqlGuildCommandBudgetExceededError)
314
325
  throw new GuildCommandLiveError("creation_budget_exhausted");
315
326
  throw error;
316
327
  }
package/dist/guilds.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { type DatabasePool, type GuildShardOwnership } from "@helyx/database";
1
+ import { InstallationRepository, type DatabasePool, type GuildShardOwnership } from "@helyx/database";
2
+ import type { MysqlPool } from "@helyx/database-mysql";
2
3
  import type { ModuleDefinition } from "@helyx/sdk";
3
4
  import { type Client } from "discord.js";
4
5
  import type { InteractionLogger } from "./interactions.js";
@@ -20,7 +21,27 @@ export interface GuildTrackingPreparation {
20
21
  (readyClient: Client<true>): Promise<void>;
21
22
  reconcile(reason: "ready" | "resume" | "periodic"): Promise<void>;
22
23
  health(): GuildReconciliationHealth;
23
- stop(): void;
24
+ stop(): Promise<void>;
24
25
  }
25
- export declare function installGuildTracking(client: Client, database: DatabasePool, modules: readonly ModuleDefinition[], logger: InteractionLogger, enabledByDefaultModuleIds?: ReadonlySet<string>, ownership?: GuildShardOwnership, reconciliationIntervalMs?: number, shardCount?: number, applicationId?: string, hostedSafety?: HostedGuildSafety, hostedInviteGate?: HostedBotInviteGate, hostedOwnerObserver?: HostedGuildOwnerObserver | undefined): GuildTrackingPreparation;
26
+ interface GuildObservation {
27
+ guildId: string;
28
+ ownerUserId: string;
29
+ ownerUsername?: string;
30
+ name: string;
31
+ modules: readonly ModuleDefinition[];
32
+ enabledByDefaultModuleIds: ReadonlySet<string>;
33
+ firstObservation: boolean;
34
+ applicationId?: string;
35
+ ownership?: GuildShardOwnership;
36
+ continueObservation?: () => boolean;
37
+ }
38
+ type ReconciliationRepository = Pick<InstallationRepository, "listActiveGuildIdsForOwnership" | "adoptGuildsForReconciliation" | "markGuildsRemovedForReconciliation">;
39
+ export interface GuildTrackingPersistence extends ReconciliationRepository {
40
+ readonly engine: "postgresql" | "mysql";
41
+ syncGuild(input: GuildObservation): Promise<void>;
42
+ markGuildRemoved(guildId: string, ownership: GuildShardOwnership | undefined, applicationId: string | undefined): Promise<boolean>;
43
+ }
44
+ export declare function createMysqlGuildTrackingPersistence(database: MysqlPool): Promise<GuildTrackingPersistence>;
45
+ export declare function installGuildTracking(client: Client, database: DatabasePool | GuildTrackingPersistence, modules: readonly ModuleDefinition[], logger: InteractionLogger, enabledByDefaultModuleIds?: ReadonlySet<string>, ownership?: GuildShardOwnership, reconciliationIntervalMs?: number, shardCount?: number, applicationId?: string, hostedSafety?: HostedGuildSafety, hostedInviteGate?: HostedBotInviteGate, hostedOwnerObserver?: HostedGuildOwnerObserver | undefined): GuildTrackingPreparation;
46
+ export {};
26
47
  //# sourceMappingURL=guilds.d.ts.map