@helyx/bot 0.4.46 → 0.6.1

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 (44) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +4 -4
  3. package/dist/cloud-management-module.js +1 -5
  4. package/dist/command-path-contract.d.ts +26 -0
  5. package/dist/command-path-contract.js +157 -0
  6. package/dist/command-registration-policy.js +1 -0
  7. package/dist/control-permissions.js +1 -0
  8. package/dist/control-server.d.ts +2 -34
  9. package/dist/control-server.js +31 -43
  10. package/dist/dashboard-commands.d.ts +1 -0
  11. package/dist/dashboard-commands.js +6 -35
  12. package/dist/dashboard-module-projection.d.ts +39 -0
  13. package/dist/dashboard-module-projection.js +28 -0
  14. package/dist/discord-analytics.d.ts +30 -0
  15. package/dist/discord-analytics.js +337 -0
  16. package/dist/discord-invites.d.ts +24 -0
  17. package/dist/discord-invites.js +91 -0
  18. package/dist/events.d.ts +1 -1
  19. package/dist/events.js +78 -22
  20. package/dist/guilds.js +1 -4
  21. package/dist/index.js +49 -8
  22. package/dist/interactions.d.ts +1 -1
  23. package/dist/interactions.js +89 -68
  24. package/dist/management-module.d.ts +1 -1
  25. package/dist/module-logging.js +1 -4
  26. package/dist/module-status-control.js +1 -1
  27. package/dist/modules.d.ts +2 -0
  28. package/dist/modules.js +27 -4
  29. package/dist/permissions-module.js +13 -16
  30. package/dist/premium-access.d.ts +7 -0
  31. package/dist/premium-access.js +38 -0
  32. package/dist/premium-activation-module.d.ts +20 -0
  33. package/dist/premium-activation-module.js +542 -0
  34. package/dist/premium-catalogue-enforcement.d.ts +5 -0
  35. package/dist/premium-catalogue-enforcement.js +29 -0
  36. package/dist/premium-guild-authority.d.ts +12 -0
  37. package/dist/premium-guild-authority.js +25 -0
  38. package/dist/scheduled-task-runner.js +14 -1
  39. package/dist/shard-contracts.js +1 -1
  40. package/dist/shard-supervisor.js +30 -1
  41. package/dist/shard-worker.d.ts +2 -0
  42. package/dist/shard-worker.js +36 -4
  43. package/documentation/examples/.env.example +2 -2
  44. package/package.json +6 -6
package/dist/events.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { HELYX_SERVICE_NAMES } from "@helyx/sdk";
2
+ import { safeError } from "@helyx/core";
2
3
  import { ChannelType, Events, GatewayIntentBits, GuildMemberFlags, PermissionFlagsBits, RESTJSONErrorCodes, UserFlags, } from "discord.js";
3
4
  import { toReplyOptions } from "./interactions.js";
4
5
  import { createHash } from "node:crypto";
5
6
  import { classifyGuildMessage, unavailableGuildMessageClassification, } from "./message-classification.js";
6
- export function availabilityAwareInstallations(services) {
7
+ import { canAccessModule } from "./premium-access.js";
8
+ export function availabilityAwareInstallations(services, modulesById) {
7
9
  const installations = services.get(HELYX_SERVICE_NAMES.installations);
8
10
  const candidate = typeof services.has === "function" &&
9
11
  services.has(HELYX_SERVICE_NAMES.moduleAvailability)
@@ -12,12 +14,15 @@ export function availabilityAwareInstallations(services) {
12
14
  const availability = typeof candidate?.isAvailable === "function" ? candidate : null;
13
15
  return {
14
16
  async isModuleEnabled(guildId, moduleId) {
17
+ const module = modulesById?.get(moduleId);
15
18
  return ((!availability || (await availability.isAvailable(moduleId))) &&
16
- installations.isModuleEnabled(guildId, moduleId));
19
+ (await installations.isModuleEnabled(guildId, moduleId)) &&
20
+ (!module || (await canAccessModule(services, guildId, module))));
17
21
  },
18
22
  };
19
23
  }
20
24
  export function installModuleEvents(client, modules, logger, services) {
25
+ const modulesById = new Map(modules.map((module) => [module.manifest.id, module]));
21
26
  const privilegedMessageContributions = modules.flatMap((module) => (module.events?.privilegedMessageCreate ?? []).map((contribution) => ({
22
27
  moduleId: module.manifest.id,
23
28
  contribution,
@@ -122,12 +127,34 @@ export function installModuleEvents(client, modules, logger, services) {
122
127
  const guildMessageQueue = guildMessageContributions.length > 0
123
128
  ? new BoundedGuildEventQueue(logger)
124
129
  : null;
130
+ const inviteInspection = memberAddContributions.some(({ moduleId }) => moduleId === "helyx.invite-tracking") &&
131
+ typeof services.has === "function" &&
132
+ services.has(HELYX_SERVICE_NAMES.discordInvites)
133
+ ? services.get(HELYX_SERVICE_NAMES.discordInvites)
134
+ : null;
135
+ if (inviteInspection) {
136
+ client.on(Events.ShardDisconnect, (_event, shardId) => {
137
+ for (const guild of client.guilds.cache.values())
138
+ if (guild.shardId === shardId)
139
+ inviteInspection.invalidateContinuity({
140
+ guildId: guild.id,
141
+ reason: "gateway_session_gap",
142
+ });
143
+ });
144
+ client.on(Events.Invalidated, () => {
145
+ for (const guildId of client.guilds.cache.keys())
146
+ inviteInspection.invalidateContinuity({
147
+ guildId,
148
+ reason: "gateway_session_gap",
149
+ });
150
+ });
151
+ }
125
152
  if (privilegedMessageContributions.length > 0) {
126
153
  client.on(Events.MessageCreate, (message) => {
127
154
  if (!message.guildId || !privilegedEligibilityQueue)
128
155
  return;
129
156
  privilegedEligibilityQueue.enqueue(message.guildId, async () => {
130
- const installations = availabilityAwareInstallations(services);
157
+ const installations = availabilityAwareInstallations(services, modulesById);
131
158
  const eligible = [];
132
159
  for (const item of privilegedMessageContributions) {
133
160
  try {
@@ -231,7 +258,12 @@ export function installModuleEvents(client, modules, logger, services) {
231
258
  }
232
259
  if (memberAddContributions.length > 0) {
233
260
  client.on(Events.GuildMemberAdd, (member) => {
234
- queue.enqueue(member.guild.id, () => handleMemberAdd(member));
261
+ const accepted = queue.enqueue(member.guild.id, () => handleMemberAdd(member));
262
+ if (!accepted && inviteInspection)
263
+ inviteInspection.invalidateContinuity({
264
+ guildId: member.guild.id,
265
+ reason: "event_queue_gap",
266
+ });
235
267
  });
236
268
  }
237
269
  if (memberRemoveContributions.length > 0) {
@@ -274,7 +306,7 @@ export function installModuleEvents(client, modules, logger, services) {
274
306
  });
275
307
  }
276
308
  async function handleResourceLifecycle(contributions, serverId, identifier, resourceId, eventName) {
277
- const installations = availabilityAwareInstallations(services);
309
+ const installations = availabilityAwareInstallations(services, modulesById);
278
310
  for (const { moduleId, contribution } of contributions) {
279
311
  try {
280
312
  if (!(await installations.isModuleEnabled(serverId, moduleId))) {
@@ -300,7 +332,7 @@ export function installModuleEvents(client, modules, logger, services) {
300
332
  async function handlePrivilegedMessageCreate(message, contributions) {
301
333
  if (!message.guildId || !message.channelId)
302
334
  return;
303
- const installations = availabilityAwareInstallations(services);
335
+ const installations = availabilityAwareInstallations(services, modulesById);
304
336
  for (const { moduleId, contribution } of contributions) {
305
337
  try {
306
338
  if (!(await installations.isModuleEnabled(message.guildId, moduleId)))
@@ -333,7 +365,7 @@ export function installModuleEvents(client, modules, logger, services) {
333
365
  async function handleGuildMessageCreate(message) {
334
366
  if (!message.guildId || !message.channelId)
335
367
  return;
336
- const installations = availabilityAwareInstallations(services);
368
+ const installations = availabilityAwareInstallations(services, modulesById);
337
369
  const parentChannelId = message.channel.isThread()
338
370
  ? (message.channel.parentId ?? undefined)
339
371
  : undefined;
@@ -429,7 +461,7 @@ export function installModuleEvents(client, modules, logger, services) {
429
461
  : reaction.message;
430
462
  if (!message.guildId || !message.channelId)
431
463
  return;
432
- const installations = availabilityAwareInstallations(services);
464
+ const installations = availabilityAwareInstallations(services, modulesById);
433
465
  const member = await message.guild?.members.fetch(user.id);
434
466
  const userRoleIds = member ? [...member.roles.cache.keys()] : [];
435
467
  const parentChannelId = messageParentChannelId(message);
@@ -445,6 +477,14 @@ export function installModuleEvents(client, modules, logger, services) {
445
477
  messageId: message.id,
446
478
  userId: receivedUser.id,
447
479
  userRoleIds,
480
+ userPermissions: member?.permissions
481
+ .toArray()
482
+ .map((permission) => permission
483
+ .replaceAll(/([a-z0-9])([A-Z])/g, "$1_$2")
484
+ .toUpperCase()) ?? [],
485
+ isServerOwner: message.guild?.ownerId === user.id,
486
+ isAdministrator: member?.permissions.has(PermissionFlagsBits.Administrator) ??
487
+ false,
448
488
  emojiKey: reactionEmojiKey(reaction),
449
489
  emojiIdentifier: reactionEmojiIdentifier(reaction),
450
490
  reactionCount: Math.max(0, reaction.count ?? 0),
@@ -486,10 +526,27 @@ export function installModuleEvents(client, modules, logger, services) {
486
526
  async function handleMemberAdd(member) {
487
527
  if (!member.joinedAt)
488
528
  throw new Error("Discord member join timestamp is unavailable");
489
- const installations = availabilityAwareInstallations(services);
529
+ const installations = availabilityAwareInstallations(services, modulesById);
530
+ const continuityGeneration = inviteInspection
531
+ ? inviteInspection.continuityGeneration({ guildId: member.guild.id })
532
+ : `runtime:${client.readyTimestamp ?? 0}`;
533
+ const occurrenceId = createHash("sha256")
534
+ .update("helyx.guild-member-add.v1\0", "utf8")
535
+ .update(member.guild.id, "utf8")
536
+ .update("\0", "utf8")
537
+ .update(member.id, "utf8")
538
+ .update("\0", "utf8")
539
+ .update(member.joinedAt.toISOString(), "utf8")
540
+ .digest("base64url");
490
541
  for (const { moduleId, contribution } of memberAddContributions) {
491
542
  try {
492
543
  if (!(await installations.isModuleEnabled(member.guild.id, moduleId))) {
544
+ if (moduleId === "helyx.invite-tracking" && inviteInspection) {
545
+ inviteInspection.invalidateContinuity({
546
+ guildId: member.guild.id,
547
+ reason: "ownership_gap",
548
+ });
549
+ }
493
550
  continue;
494
551
  }
495
552
  await contribution.execute({
@@ -500,6 +557,8 @@ export function installModuleEvents(client, modules, logger, services) {
500
557
  memberIsBot: member.user.bot,
501
558
  accountCreatedAt: member.user.createdAt,
502
559
  joinedAt: member.joinedAt,
560
+ occurrenceId,
561
+ continuityGeneration,
503
562
  services,
504
563
  assignRole: (roleId) => assignMemberRole(member, roleId),
505
564
  sendToChannel: (channelId, response) => sendToGuildChannel(member, channelId, response),
@@ -517,7 +576,7 @@ export function installModuleEvents(client, modules, logger, services) {
517
576
  }
518
577
  }
519
578
  async function handleMemberRemove(member) {
520
- const installations = availabilityAwareInstallations(services);
579
+ const installations = availabilityAwareInstallations(services, modulesById);
521
580
  for (const { moduleId, contribution } of memberRemoveContributions) {
522
581
  try {
523
582
  if (!(await installations.isModuleEnabled(member.guild.id, moduleId))) {
@@ -557,7 +616,7 @@ export function installModuleEvents(client, modules, logger, services) {
557
616
  : reaction.message;
558
617
  if (!message.guildId || !message.channelId)
559
618
  return;
560
- const installations = availabilityAwareInstallations(services);
619
+ const installations = availabilityAwareInstallations(services, modulesById);
561
620
  const parentChannelId = messageParentChannelId(message);
562
621
  for (const { moduleId, contribution } of reactionRemoveContributions) {
563
622
  if (!(await installations.isModuleEnabled(message.guildId, moduleId))) {
@@ -601,7 +660,7 @@ export function installModuleEvents(client, modules, logger, services) {
601
660
  : receivedMessage;
602
661
  if (!message.guildId || !message.channelId)
603
662
  return;
604
- const installations = availabilityAwareInstallations(services);
663
+ const installations = availabilityAwareInstallations(services, modulesById);
605
664
  for (const { moduleId, contribution } of reactionRemoveAllContributions) {
606
665
  try {
607
666
  if (!(await installations.isModuleEnabled(message.guildId, moduleId)))
@@ -631,7 +690,7 @@ export function installModuleEvents(client, modules, logger, services) {
631
690
  const message = reaction.message;
632
691
  if (!message.guildId || !message.channelId)
633
692
  return;
634
- const installations = availabilityAwareInstallations(services);
693
+ const installations = availabilityAwareInstallations(services, modulesById);
635
694
  for (const { moduleId, contribution } of reactionRemoveEmojiContributions) {
636
695
  try {
637
696
  if (!(await installations.isModuleEnabled(message.guildId, moduleId)))
@@ -659,7 +718,7 @@ export function installModuleEvents(client, modules, logger, services) {
659
718
  async function handleMessageUpdate(receivedMessage) {
660
719
  if (!receivedMessage.guildId || !receivedMessage.channelId)
661
720
  return;
662
- const installations = availabilityAwareInstallations(services);
721
+ const installations = availabilityAwareInstallations(services, modulesById);
663
722
  for (const { moduleId, contribution } of messageUpdateContributions) {
664
723
  try {
665
724
  if (!(await installations.isModuleEnabled(receivedMessage.guildId, moduleId)))
@@ -693,7 +752,7 @@ export function installModuleEvents(client, modules, logger, services) {
693
752
  async function handleMessageDelete(message) {
694
753
  if (!message.guildId || !message.channelId)
695
754
  return;
696
- const installations = availabilityAwareInstallations(services);
755
+ const installations = availabilityAwareInstallations(services, modulesById);
697
756
  for (const { moduleId, contribution } of messageDeleteContributions) {
698
757
  try {
699
758
  if (!(await installations.isModuleEnabled(message.guildId, moduleId))) {
@@ -720,7 +779,7 @@ export function installModuleEvents(client, modules, logger, services) {
720
779
  async function handleMessageDeleteBulk(guildId, channelId, messageIds) {
721
780
  if (messageIds.length === 0)
722
781
  return;
723
- const installations = availabilityAwareInstallations(services);
782
+ const installations = availabilityAwareInstallations(services, modulesById);
724
783
  const boundedIds = messageIds.slice(0, 100);
725
784
  for (const { moduleId, contribution } of messageDeleteBulkContributions) {
726
785
  try {
@@ -754,7 +813,7 @@ export function installModuleEvents(client, modules, logger, services) {
754
813
  voiceSessions.delete(key);
755
814
  const endedAt = new Date();
756
815
  const durationSeconds = Math.max(1, Math.floor((endedAt.getTime() - previous.startedAt.getTime()) / 1_000));
757
- const installations = availabilityAwareInstallations(services);
816
+ const installations = availabilityAwareInstallations(services, modulesById);
758
817
  for (const { moduleId, contribution } of guildVoiceSessionContributions) {
759
818
  try {
760
819
  if (!(await installations.isModuleEnabled(previous.guildId, moduleId)))
@@ -822,10 +881,11 @@ class BoundedGuildEventQueue {
822
881
  enqueue(guildId, run) {
823
882
  if (!this.#accepting || this.#pending.length >= 1_000) {
824
883
  this.logger.warn({ event: "module.event_dropped", reason: "queue_capacity" }, "Module gateway event queue rejected work");
825
- return;
884
+ return false;
826
885
  }
827
886
  this.#pending.push({ guildId, run });
828
887
  this.#pump();
888
+ return true;
829
889
  }
830
890
  queueDepth() {
831
891
  return this.#pending.length;
@@ -2014,8 +2074,4 @@ function isSingleUnicodeEmoji(value) {
2014
2074
  ];
2015
2075
  return graphemes.length === 1;
2016
2076
  }
2017
- function safeError(error) {
2018
- const value = error instanceof Error ? error : new Error(String(error));
2019
- return { name: value.name, message: value.message };
2020
- }
2021
2077
  //# sourceMappingURL=events.js.map
package/dist/guilds.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { InstallationRepository, GuildCommandRepository, withTransaction, } from "@helyx/database";
2
+ import { safeError } from "@helyx/core";
2
3
  import { Events } from "discord.js";
3
4
  const RECONCILIATION_PAGE_SIZE = 250;
4
5
  const DEFAULT_RECONCILIATION_INTERVAL_MS = 15 * 60 * 1_000;
@@ -214,8 +215,4 @@ export function installGuildTracking(client, database, modules, logger, enabledB
214
215
  };
215
216
  return prepare;
216
217
  }
217
- function safeError(error) {
218
- const value = error instanceof Error ? error : new Error(String(error));
219
- return { name: value.name, message: value.message };
220
- }
221
218
  //# sourceMappingURL=guilds.js.map
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
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, GuildCommandRepository, InteractionSessionRepository, RateLimitRepository, runCoreMigrations, runConfiguredModuleMigrations, } from "@helyx/database";
4
+ import { checkDatabaseConnection, createDatabasePool, AuditRepository, GuildCommandRepository, InteractionSessionRepository, PremiumAllocationRepository, RateLimitRepository, runCoreMigrations, runConfiguredModuleMigrations, } from "@helyx/database";
5
5
  import { createRuntime } from "@helyx/framework";
6
- import { createPlatformServices, gamingStatisticsRecordOwner, achievementContributionRecordOwner, PostgresPrivacyProviderExecutor, } from "@helyx/platform";
6
+ import { createPlatformServices, gamingStatisticsRecordOwner, achievementContributionRecordOwner, PostgresPrivacyProviderExecutor, PlatformMemberRoleGrantOwnershipService, PostgresMemberRoleGrantOwnershipStore, } from "@helyx/platform";
7
7
  import { HELYX_SERVICE_NAMES, } from "@helyx/sdk";
8
8
  import { Client, Events, GatewayIntentBits, Partials } from "discord.js";
9
9
  import pino from "pino";
@@ -30,9 +30,14 @@ import { shardHealthSnapshotSchema, } from "./shard-contracts.js";
30
30
  import { createSupportModule } from "./support-module.js";
31
31
  import { createOAuthModule } from "./oauth-module.js";
32
32
  import { createPrivacyModule } from "./privacy-module.js";
33
+ import { REVIEWED_PREMIUM_MODULE_IDS } from "@helyx/module-manifest";
34
+ import { createPremiumActivationModule } from "./premium-activation-module.js";
35
+ import { DiscordPremiumGuildAuthority } from "./premium-guild-authority.js";
33
36
  import { HttpOAuthService } from "./oauth-service.js";
34
37
  import { DiscordOwnedMessageDeletionService } from "./discord-owned-message-service.js";
35
38
  import { DiscordGuildHealthInspection } from "./discord-guild-health.js";
39
+ import { DiscordAnalyticsInspection } from "./discord-analytics.js";
40
+ import { DiscordInviteInspection } from "./discord-invites.js";
36
41
  import { GuildCommandWorker } from "./guild-command-worker.js";
37
42
  import { GuildCommandDiscordClient } from "./guild-command-discord-client.js";
38
43
  import { GuildCommandReadService } from "./guild-command-read-service.js";
@@ -43,6 +48,8 @@ import { createLocalScheduledTaskRoute, createScheduledTaskRunner, schedulerOper
43
48
  import { createLocalNumericWriteBehind } from "./numeric-write-behind.js";
44
49
  import { discordRestOptions } from "./discord-rest.js";
45
50
  import { discordBotStatusService } from "./status-producer.js";
51
+ import { assertReviewedPremiumCatalogue, requirePremiumProviderEnvironment, } from "./premium-access.js";
52
+ import { enforcePremiumCatalogue, PREMIUM_CATALOGUE_ENFORCEMENT_INTERVAL_MS, } from "./premium-catalogue-enforcement.js";
46
53
  export { loadConfiguredModulePackages } from "./modules.js";
47
54
  export { resolveModulePackageSpecifiers } from "./module-configuration.js";
48
55
  export * from "./control-server.js";
@@ -81,9 +88,7 @@ export async function startBot(processEnvironment = process.env) {
81
88
  },
82
89
  {
83
90
  key: "creator-connections",
84
- state: environment.HELYX_OAUTH_ENABLED
85
- ? "operational"
86
- : "major_outage",
91
+ state: "operational",
87
92
  reasonCode: environment.HELYX_OAUTH_ENABLED
88
93
  ? null
89
94
  : "feature_disabled",
@@ -93,7 +98,7 @@ export async function startBot(processEnvironment = process.env) {
93
98
  ];
94
99
  },
95
100
  onError(error) {
96
- bootstrapLogger.warn({ event: "status.heartbeat_failed", reason: error.message }, "Status heartbeat could not be delivered");
101
+ bootstrapLogger.warn({ event: "status.heartbeat_failed", error: safeError(error) }, "Status heartbeat could not be delivered");
97
102
  },
98
103
  });
99
104
  reporter.start();
@@ -126,6 +131,7 @@ async function startUnshardedBot(processEnvironment) {
126
131
  : undefined;
127
132
  let databaseClosed = false;
128
133
  let retentionCleanupTimer;
134
+ let premiumCatalogueTimer;
129
135
  async function closeDatabase() {
130
136
  if (databaseClosed)
131
137
  return;
@@ -196,6 +202,15 @@ async function startUnshardedBot(processEnvironment) {
196
202
  })),
197
203
  }, "Module databases ready");
198
204
  const featureModules = modulePackages.map(({ definition }) => definition);
205
+ assertReviewedPremiumCatalogue(featureModules);
206
+ await enforcePremiumCatalogue(database, bootstrapLogger);
207
+ premiumCatalogueTimer = setInterval(() => void enforcePremiumCatalogue(database, bootstrapLogger).catch((error) => {
208
+ bootstrapLogger.warn({
209
+ event: "premium.catalogue_enforcement_failed",
210
+ error: safeError(error),
211
+ }, "Premium module catalogue enforcement failed");
212
+ }), PREMIUM_CATALOGUE_ENFORCEMENT_INTERVAL_MS);
213
+ premiumCatalogueTimer.unref();
199
214
  const client = new Client({
200
215
  rest: discordRestOptions,
201
216
  intents: gatewayIntentsForModules(featureModules, environment.HELYX_MESSAGE_CONTENT_ENABLED, environment.HELYX_GUILD_MEMBERS_ENABLED),
@@ -222,6 +237,16 @@ async function startUnshardedBot(processEnvironment) {
222
237
  : undefined;
223
238
  const supportModule = createSupportModule(cloudConnector);
224
239
  const privacyModule = createPrivacyModule();
240
+ const premiumEnvironment = requirePremiumProviderEnvironment(featureModules, environment.STRIPE_ENVIRONMENT);
241
+ const premiumActivationModule = !premiumEnvironment
242
+ ? undefined
243
+ : createPremiumActivationModule({
244
+ allocations: new PremiumAllocationRepository(database),
245
+ authority: new DiscordPremiumGuildAuthority(client),
246
+ provider: "stripe",
247
+ providerEnvironment: premiumEnvironment,
248
+ reviewedPremiumModuleIds: REVIEWED_PREMIUM_MODULE_IDS,
249
+ });
225
250
  const oauthModule = environment.HELYX_OAUTH_ENABLED
226
251
  ? createOAuthModule()
227
252
  : undefined;
@@ -230,6 +255,7 @@ async function startUnshardedBot(processEnvironment) {
230
255
  permissionsModule,
231
256
  supportModule,
232
257
  privacyModule,
258
+ ...(premiumActivationModule ? [premiumActivationModule] : []),
233
259
  ...(oauthModule ? [oauthModule] : []),
234
260
  ...(cloudManagementModule ? [cloudManagementModule] : []),
235
261
  ...featureModules,
@@ -303,6 +329,7 @@ async function startUnshardedBot(processEnvironment) {
303
329
  permissionsModule,
304
330
  supportModule,
305
331
  privacyModule,
332
+ ...(premiumActivationModule ? [premiumActivationModule] : []),
306
333
  ...(oauthModule ? [oauthModule] : []),
307
334
  ...(cloudManagementModule ? [cloudManagementModule] : []),
308
335
  ].map(({ manifest }) => manifest),
@@ -320,8 +347,9 @@ async function startUnshardedBot(processEnvironment) {
320
347
  ];
321
348
  const platformServices = new Map();
322
349
  const serviceAccess = new ServiceRegistry(platformServices);
350
+ const serviceOptions = platformServiceOptions(environment, publicModules);
323
351
  for (const [name, service] of createPlatformServices(database, publicModules, {
324
- ...platformServiceOptions(environment, publicModules),
352
+ ...serviceOptions,
325
353
  activationServices: serviceAccess,
326
354
  })) {
327
355
  platformServices.set(name, service);
@@ -349,9 +377,14 @@ async function startUnshardedBot(processEnvironment) {
349
377
  platformServices.set(HELYX_SERVICE_NAMES.discordMessages, new DiscordMessageDeliveryService(client));
350
378
  platformServices.set(HELYX_SERVICE_NAMES.discordOwnedMessages, new DiscordOwnedMessageDeletionService(client));
351
379
  platformServices.set(HELYX_SERVICE_NAMES.memberEnforcement, new DiscordMemberEnforcementService(client));
352
- platformServices.set(HELYX_SERVICE_NAMES.memberRoles, new DiscordMemberRoleService(client));
380
+ const memberRoles = new DiscordMemberRoleService(client);
381
+ platformServices.set(HELYX_SERVICE_NAMES.memberRoles, memberRoles);
382
+ platformServices.set(HELYX_SERVICE_NAMES.memberRoleGrants, new PlatformMemberRoleGrantOwnershipService(new PostgresMemberRoleGrantOwnershipStore(database), memberRoles));
353
383
  platformServices.set(HELYX_SERVICE_NAMES.discordResources, new DiscordResourceInspection(client));
354
384
  platformServices.set(HELYX_SERVICE_NAMES.discordGuildHealth, new DiscordGuildHealthInspection(client, publicModules, serviceAccess));
385
+ platformServices.set(HELYX_SERVICE_NAMES.discordAnalytics, new DiscordAnalyticsInspection(client));
386
+ if (serviceOptions.inviteFingerprintKey)
387
+ platformServices.set(HELYX_SERVICE_NAMES.discordInvites, new DiscordInviteInspection(client, serviceOptions.inviteFingerprintKey));
355
388
  if (featureModules.some(({ manifest }) => manifest.id === "helyx.logging")) {
356
389
  platformServices.set(HELYX_SERVICE_NAMES.moduleLogging, new DiscordModuleLoggingService(client, database, publicModules, bootstrapLogger));
357
390
  }
@@ -364,6 +397,7 @@ async function startUnshardedBot(processEnvironment) {
364
397
  managementModule.manifest.id,
365
398
  permissionsModule.manifest.id,
366
399
  supportModule.manifest.id,
400
+ ...(premiumActivationModule ? [premiumActivationModule.manifest.id] : []),
367
401
  ...(oauthModule ? [oauthModule.manifest.id] : []),
368
402
  ...(cloudManagementModule ? [cloudManagementModule.manifest.id] : []),
369
403
  ...(operationsModule ? [operationsModule.manifest.id] : []),
@@ -378,6 +412,9 @@ async function startUnshardedBot(processEnvironment) {
378
412
  prepare: prepareGuilds,
379
413
  selfAuthorizingGuildModuleIds: new Set([
380
414
  supportModule.manifest.id,
415
+ ...(premiumActivationModule
416
+ ? [premiumActivationModule.manifest.id]
417
+ : []),
381
418
  ...(oauthModule ? [oauthModule.manifest.id] : []),
382
419
  ...(operationsModule ? [operationsModule.manifest.id] : []),
383
420
  ]),
@@ -416,6 +453,8 @@ async function startUnshardedBot(processEnvironment) {
416
453
  process.off("SIGTERM", onSigterm);
417
454
  if (retentionCleanupTimer)
418
455
  clearInterval(retentionCleanupTimer);
456
+ if (premiumCatalogueTimer)
457
+ clearInterval(premiumCatalogueTimer);
419
458
  await scheduledTasks?.stop().catch((error) => {
420
459
  process.exitCode = 1;
421
460
  bootstrapLogger.error({ event: "scheduler.stop_failed", error: safeError(error) }, "Scheduled task runner shutdown failed");
@@ -579,6 +618,8 @@ async function startUnshardedBot(processEnvironment) {
579
618
  catch (error) {
580
619
  if (retentionCleanupTimer)
581
620
  clearInterval(retentionCleanupTimer);
621
+ if (premiumCatalogueTimer)
622
+ clearInterval(premiumCatalogueTimer);
582
623
  await closeDatabase().catch((closeError) => {
583
624
  bootstrapLogger.error({ event: "database.close_failed", error: safeError(closeError) }, "Database pool cleanup failed");
584
625
  });
@@ -39,7 +39,7 @@ export declare function sendToChannel(client: Client, channelId: string, respons
39
39
  export declare function deleteChannelMessage(client: Client, channelId: string, messageId: string): Promise<void>;
40
40
  export declare function editChannelMessage(client: Client, channelId: string, messageId: string, response: MessageResponse): Promise<void>;
41
41
  export declare function addChannelMessageReaction(client: Client, channelId: string, messageId: string, emojiIdentifier: string): Promise<void>;
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>;
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, module?: ModuleDefinition): Promise<boolean>;
43
43
  export declare function normalizeDiscordPermission(permission: string): string;
44
44
  export declare function toReplyOptions(response: MessageResponse): InteractionReplyOptions;
45
45
  export declare function toUpdateOptions(response: MessageResponse): InteractionUpdateOptions;