@helyx/bot 0.5.0 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,56 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Allow Premium slots to be released, reassigned and moved immediately without a
8
+ transfer cooldown, while preserving atomic entitlement projection and
9
+ final-source Premium module disablement.
10
+
11
+ Add fail-closed Premium quantity incident controls, recoverable allocation
12
+ management for removed servers, overlapping Stripe webhook-secret rotation and
13
+ restricted exception diagnostics. Complete the production environment
14
+ contract for every API and bot schema field.
15
+
16
+ - Updated dependencies
17
+ - @helyx/config@1.0.0
18
+ - @helyx/database@1.0.0
19
+
20
+ ## 0.6.0
21
+
22
+ ### Minor Changes
23
+
24
+ - 1dfbf5b: Add the reviewed Premium module catalogue and enforce its entitlement across
25
+ hosted command, interaction, event, scheduled-task, configuration and dashboard
26
+ mutation paths. Add account-owned Premium slot allocation, the `/activate`
27
+ Discord flow, final-source-loss repair and optional safe billing lifecycle
28
+ notifications. Add audited, privacy-aware Admin Premium overrides and
29
+ deterministic entitlement convergence for inactive subscriptions and quantity
30
+ reductions, while keeping billing and Premium-only commands out of generated
31
+ self-host projects.
32
+
33
+ ### Patch Changes
34
+
35
+ - Updated dependencies [1dfbf5b]
36
+ - @helyx/config@0.7.0
37
+ - @helyx/database@0.12.0
38
+ - @helyx/module-manifest@0.9.0
39
+ - @helyx/platform@0.8.0
40
+
41
+ ## 0.5.1
42
+
43
+ ### Patch Changes
44
+
45
+ - 579f2c4: Keep transport-public components behind module availability and enablement,
46
+ and classify dashboard permission operations separately from Discord commands.
47
+ - Updated dependencies [579f2c4]
48
+ - Updated dependencies [579f2c4]
49
+ - Updated dependencies [579f2c4]
50
+ - @helyx/sdk@0.11.0
51
+ - @helyx/config@0.6.0
52
+ - @helyx/database@0.11.0
53
+
3
54
  ## 0.5.0
4
55
 
5
56
  ### Minor Changes
package/README.md CHANGED
@@ -45,10 +45,10 @@ bot-specific `HELYX_STATUS_HEARTBEAT_SECRET`,
45
45
  `CF_ACCESS_CLIENT_ID`/`CF_ACCESS_CLIENT_SECRET` pair. Never reuse another
46
46
  producer's Access identity or heartbeat secret.
47
47
 
48
- Giveaways also requires the existing `HELYX_DATA_ENCRYPTION_KEY` so core can
49
- resume secure weighted-selection checkpoints without exposing the selection
50
- key to a module. Keep it stable across deployments; do not rotate it while a
51
- giveaway is drawing.
48
+ Giveaways and Invite Tracking also require the existing
49
+ `HELYX_DATA_ENCRYPTION_KEY`. Core uses it for secure weighted-selection
50
+ checkpoints and stable invite-member fingerprints without exposing the key to
51
+ a module. Keep it stable across deployments.
52
52
 
53
53
  Trivia uses questions already imported into PostgreSQL. Multiple Choice and
54
54
  Guess both run entirely from the saved question catalogue, and the bot runtime
@@ -1,4 +1,5 @@
1
1
  import { defineModule, } from "@helyx/sdk";
2
+ import { safeError } from "@helyx/core";
2
3
  import { Events } from "discord.js";
3
4
  import { ClientCloudDiscordDirectory, CloudConnectorUserError, } from "./cloud-connector.js";
4
5
  import { INTERNAL_MODULE_MANIFEST_FIELDS } from "./internal-module-manifest.js";
@@ -622,9 +623,4 @@ function restrictedPolicy() {
622
623
  deniedUserIds: [],
623
624
  };
624
625
  }
625
- function safeError(error) {
626
- return error instanceof Error
627
- ? { name: error.name, message: error.message }
628
- : { name: "Error", message: String(error) };
629
- }
630
626
  //# sourceMappingURL=cloud-management-module.js.map
@@ -5,6 +5,7 @@ const platformGlobalModuleIds = new Set([
5
5
  "helyx.oauth",
6
6
  "helyx.permissions",
7
7
  "helyx.privacy",
8
+ "helyx.premium-activation",
8
9
  ]);
9
10
  const deploymentModuleIds = new Set(["helyx.cloud-connect"]);
10
11
  const internalGuildModuleIds = new Set(["helyx.developer-operations"]);
@@ -7,41 +7,9 @@ import { type BotModuleStatusMutationResult } from "./module-status-control.js";
7
7
  import { type FirstTimeCommandSetupHandler } from "./first-time-command-setup.js";
8
8
  import { type ShardHealthSnapshot } from "./shard-contracts.js";
9
9
  import { type DashboardPermissionState } from "./control-permissions.js";
10
- import { type DashboardCommandDefinition } from "./dashboard-commands.js";
10
+ import { type DashboardModuleDefinition } from "./dashboard-module-projection.js";
11
11
  import { type AvailabilityModuleCatalogue } from "./module-availability-control.js";
12
12
  import { registerGuildCommandControlRoutes } from "./guild-command-control-routes.js";
13
- interface DashboardLogEventDefinition {
14
- id: string;
15
- name: string;
16
- description: string;
17
- defaultEnabled: boolean;
18
- }
19
- interface DashboardActionDefinition {
20
- id: string;
21
- label: string;
22
- description: string;
23
- buttonLabel: string;
24
- style: "primary" | "secondary" | "danger";
25
- }
26
- type DashboardResourceDefinition = NonNullable<NonNullable<ModuleDefinition["manifest"]["dashboard"]>["resources"]>[number];
27
- interface DashboardModuleDefinition {
28
- id: string;
29
- name: string;
30
- version: string;
31
- summary: string;
32
- category: string;
33
- icon: string;
34
- availability: "available";
35
- releaseStatus: "planned" | "testing" | "beta" | "live";
36
- requiredPermissions: string[];
37
- settings: readonly unknown[];
38
- commands: DashboardCommandDefinition[];
39
- logEvents: DashboardLogEventDefinition[];
40
- actions: DashboardActionDefinition[];
41
- achievementTriggers: NonNullable<ModuleDefinition["manifest"]["contributions"]["achievementTriggers"]>;
42
- resources: readonly DashboardResourceDefinition[];
43
- workspace?: NonNullable<ModuleDefinition["manifest"]["dashboard"]>["workspace"];
44
- }
45
13
  interface DashboardLoggingPolicy {
46
14
  eventId: string;
47
15
  enabled: boolean;
@@ -1,4 +1,5 @@
1
1
  import { AuditRepository, GuildAccessPolicyRepository, GuildCommandActiveLeaseError, GuildCommandRevisionConflictError, GuildCommandSetupConflictError, InstallationRepository, ModuleLoggingPolicyRepository, OptimisticConcurrencyError, PermissionPolicyRepository, withTransaction, } from "@helyx/database";
2
+ import { safeError } from "@helyx/core";
2
3
  import { DashboardActionValidationError, DEFAULT_GUILD_ACCESS_POLICY, HELYX_SERVICE_NAMES, ModuleStateValidationError, } from "@helyx/sdk";
3
4
  import { actionReceiptSchema, signedAssessmentEnvelopeSchema, } from "@helyx/verification-contracts";
4
5
  import { canAccessGuildDashboard, canMutatePermissionPolicy, canUseCommand, resolveHelyxAuthority, } from "@helyx/core";
@@ -8,12 +9,13 @@ import { createHash, timingSafeEqual } from "node:crypto";
8
9
  import { z, ZodError } from "zod";
9
10
  import { addChannelMessageReaction as addDiscordChannelMessageReaction, deleteChannelMessage as deleteDiscordChannelMessage, editChannelMessage as editDiscordChannelMessage, normalizeDiscordPermission, sendToChannel as sendDiscordMessageToChannel, } from "./interactions.js";
10
11
  import { mutateDashboardModuleStatus, } from "./module-status-control.js";
12
+ import { canAccessModule, PREMIUM_MODULE_UNAVAILABLE_MESSAGE, } from "./premium-access.js";
11
13
  import { completeFirstTimeCommandSetup, } from "./first-time-command-setup.js";
12
14
  import { SHARD_HEALTH_STALE_MS, } from "./shard-contracts.js";
13
- import { dashboardCategoryLabel } from "./dashboard-category.js";
14
15
  import { assertVersion, dashboardActionPermissionId, declaredManagedResourcePermission, fromDashboardPolicy, managedResourcePermissionId, permissionPoliciesEqual, permissionPolicyKey, safeOptionalVersion, safeVersion, toDashboardPolicy, } from "./control-permissions.js";
15
16
  import { assertPermissionMutationAllowed, BotControlForbiddenError, } from "./control-errors.js";
16
- import { dashboardCommands, } from "./dashboard-commands.js";
17
+ import { dashboardCommands } from "./dashboard-commands.js";
18
+ import { mapDashboardModule, } from "./dashboard-module-projection.js";
17
19
  import { availabilityCatalogueDefinitions, availabilityCatalogue, dashboardModuleStatus, disabledModuleSummary, moduleAvailabilityService, moduleAvailabilityStates, statusCatalogueDefinitions, } from "./module-availability-control.js";
18
20
  import { registerGuildCommandControlRoutes } from "./guild-command-control-routes.js";
19
21
  import { createPrivacyProviderRegistry, describePrivacyProviders, } from "./privacy-provider-registry.js";
@@ -633,13 +635,13 @@ export class BotControlService {
633
635
  dashboardModuleIds: this.#dashboardModules.map(({ id }) => id),
634
636
  installations: this.#installations,
635
637
  services: this.#services,
636
- requireModuleAvailable: () => this.#requireModuleAvailable(input.moduleId),
638
+ requireModuleAvailable: () => this.#requireModuleAccessible(input.guildId, input.moduleId),
637
639
  });
638
640
  }
639
641
  async completeFirstTimeCommandSetup(input) {
640
642
  for (const moduleIdValue of new Set(input.moduleIds)) {
641
643
  this.#requireDashboardModule(moduleIdValue);
642
- await this.#requireModuleAvailable(moduleIdValue);
644
+ await this.#requireModuleAccessible(input.guildId, moduleIdValue);
643
645
  }
644
646
  return completeFirstTimeCommandSetup({
645
647
  installations: this.#installations,
@@ -649,7 +651,7 @@ export class BotControlService {
649
651
  }
650
652
  async updateConfiguration(input) {
651
653
  const module = this.#requireDashboardModule(input.moduleId);
652
- await this.#requireModuleAvailable(input.moduleId);
654
+ await this.#requireModuleAccessible(input.guildId, input.moduleId);
653
655
  await this.#validateDiscordSettings(input.guildId, module, input.value);
654
656
  if (module.configuration?.validateUpdate) {
655
657
  try {
@@ -683,7 +685,7 @@ export class BotControlService {
683
685
  }
684
686
  async updatePermissions(input) {
685
687
  const module = this.#requireDashboardModule(input.moduleId);
686
- await this.#requireModuleAvailable(input.moduleId);
688
+ await this.#requireModuleAccessible(input.guildId, input.moduleId);
687
689
  const commands = dashboardCommands(module);
688
690
  const supplied = new Map(input.state.commands.map((command) => [command.commandId, command]));
689
691
  if (supplied.size !== input.state.commands.length ||
@@ -808,8 +810,8 @@ export class BotControlService {
808
810
  }
809
811
  async updateLoggingPolicy(input) {
810
812
  await Promise.all([
811
- this.#requireModuleAvailable(input.moduleId),
812
- this.#requireModuleAvailable("helyx.logging"),
813
+ this.#requireModuleAccessible(input.guildId, input.moduleId),
814
+ this.#requireModuleAccessible(input.guildId, "helyx.logging"),
813
815
  ]);
814
816
  if (!this.#modules.has("helyx.logging")) {
815
817
  throw new BotControlNotFoundError("Logging is not installed");
@@ -855,7 +857,7 @@ export class BotControlService {
855
857
  }
856
858
  async executeDashboardAction(input) {
857
859
  const module = this.#requireDashboardModule(input.moduleId);
858
- await this.#requireModuleAvailable(input.moduleId);
860
+ await this.#requireModuleAccessible(input.guildId, input.moduleId);
859
861
  const declared = module.manifest.dashboard?.actions?.find(({ id }) => id === input.actionId);
860
862
  const action = module.dashboardActions?.find(({ id }) => id === input.actionId);
861
863
  if (!declared || !action) {
@@ -914,13 +916,13 @@ export class BotControlService {
914
916
  })), relation.columns.map(({ key }) => key));
915
917
  }
916
918
  async previewManagedResource(input) {
917
- await this.#requireModuleAvailable(input.moduleId);
919
+ await this.#requireModuleAccessible(input.guildId, input.moduleId);
918
920
  const { module, resource } = this.#prepareManagedResource(input);
919
921
  this.#requireManagedResourceOperation(module, input.resourceType, "preview");
920
922
  return resource.preview(await this.#managedResourceContext(input, module, "preview"), input.value);
921
923
  }
922
924
  async createManagedResource(input) {
923
- await this.#requireModuleAvailable(input.moduleId);
925
+ await this.#requireModuleAccessible(input.guildId, input.moduleId);
924
926
  const { module, resource } = this.#prepareManagedResource(input);
925
927
  this.#requireManagedResourceOperation(module, input.resourceType, "create");
926
928
  const context = await this.#managedResourceContext(input, module, "create");
@@ -930,7 +932,7 @@ export class BotControlService {
930
932
  return normalizeManagedResourceDetail(managedResourceDetailSchema.parse(await resource.create(context, input.value, input.intent)), managedResourceColumnKeys(module, input.resourceType));
931
933
  }
932
934
  async updateManagedResource(input) {
933
- await this.#requireModuleAvailable(input.moduleId);
935
+ await this.#requireModuleAccessible(input.guildId, input.moduleId);
934
936
  const { module, resource } = this.#prepareManagedResource(input);
935
937
  this.#requireManagedResourceOperation(module, input.resourceType, "update");
936
938
  return normalizeManagedResourceDetail(managedResourceDetailSchema.parse(await resource.update(await this.#managedResourceContext(input, module, "update"), {
@@ -940,7 +942,7 @@ export class BotControlService {
940
942
  })), managedResourceColumnKeys(module, input.resourceType));
941
943
  }
942
944
  async deleteManagedResource(input) {
943
- await this.#requireModuleAvailable(input.moduleId);
945
+ await this.#requireModuleAccessible(input.guildId, input.moduleId);
944
946
  const { module, resource } = this.#prepareManagedResource(input);
945
947
  this.#requireManagedResourceOperation(module, input.resourceType, "delete");
946
948
  return managedResourceDeleteResultSchema.parse(await resource.delete(await this.#managedResourceContext(input, module, "delete"), {
@@ -949,7 +951,7 @@ export class BotControlService {
949
951
  }));
950
952
  }
951
953
  async executeManagedResourceAction(input) {
952
- await this.#requireModuleAvailable(input.moduleId);
954
+ await this.#requireModuleAccessible(input.guildId, input.moduleId);
953
955
  const { module, resource } = this.#prepareManagedResource(input);
954
956
  const declared = module.manifest.dashboard?.resources
955
957
  ?.find(({ id }) => id === input.resourceType)
@@ -1053,6 +1055,12 @@ export class BotControlService {
1053
1055
  if (availability && !(await availability.isAvailable(moduleIdValue)))
1054
1056
  throw new BotControlValidationError("This module is temporarily unavailable for all hosted servers.");
1055
1057
  }
1058
+ async #requireModuleAccessible(guildId, moduleIdValue) {
1059
+ await this.#requireModuleAvailable(moduleIdValue);
1060
+ const module = this.#requireModule(moduleIdValue);
1061
+ if (!(await canAccessModule(this.#services, guildId, module)))
1062
+ throw new BotControlValidationError(PREMIUM_MODULE_UNAVAILABLE_MESSAGE);
1063
+ }
1056
1064
  #requireDashboardModule(moduleIdValue) {
1057
1065
  const module = this.#requireModule(moduleIdValue);
1058
1066
  if (!module.manifest.dashboard)
@@ -1703,10 +1711,7 @@ export async function createBotControlServer(input) {
1703
1711
  return reply.code(mapped.status).send({ message: mapped.message });
1704
1712
  if (error instanceof BotControlTransportError)
1705
1713
  return reply.code(error.status).send({ message: error.message });
1706
- const safeError = error instanceof Error
1707
- ? { name: error.name, message: error.message }
1708
- : { name: "Error", message: "Unknown control failure" };
1709
- request.log.error({ error: safeError }, "Private bot control request failed");
1714
+ request.log.error({ error: safeError(error) }, "Private bot control request failed");
1710
1715
  return reply.code(500).send({ message: "The control request failed" });
1711
1716
  });
1712
1717
  return server;
@@ -1885,27 +1890,4 @@ function validateManagedResourceActionValue(inputs, value) {
1885
1890
  throw new DashboardActionValidationError(`${input.label} is not a valid action value.`);
1886
1891
  }
1887
1892
  }
1888
- function mapDashboardModule(module) {
1889
- const dashboard = module.manifest.dashboard;
1890
- return {
1891
- id: module.manifest.id,
1892
- name: module.manifest.name,
1893
- version: module.manifest.version,
1894
- summary: module.manifest.description,
1895
- category: dashboardCategoryLabel(dashboard.category),
1896
- icon: dashboard.icon,
1897
- availability: "available",
1898
- releaseStatus: module.manifest.releaseStatus,
1899
- requiredPermissions: [...module.manifest.permissions],
1900
- settings: dashboard.settings,
1901
- commands: dashboardCommands(module),
1902
- logEvents: (module.logging?.events ?? []).map((event) => ({ ...event })),
1903
- actions: (dashboard.actions ?? []).map((action) => ({ ...action })),
1904
- achievementTriggers: (module.manifest.contributions?.achievementTriggers ?? []).map((trigger) => structuredClone(trigger)),
1905
- resources: (dashboard.resources ?? []).map((resource) => structuredClone(resource)),
1906
- workspace: dashboard.workspace
1907
- ? structuredClone(dashboard.workspace)
1908
- : undefined,
1909
- };
1910
- }
1911
1893
  //# sourceMappingURL=control-server.js.map
@@ -4,6 +4,7 @@ export interface DashboardCommandDefinition {
4
4
  id: string;
5
5
  name: string;
6
6
  description: string;
7
+ kind: "command" | "operation";
7
8
  defaultPreset: DashboardPermissionPolicy["preset"];
8
9
  }
9
10
  export declare function dashboardCommands(module: ModuleDefinition): DashboardCommandDefinition[];
@@ -5,6 +5,7 @@ export function dashboardCommands(module) {
5
5
  id: command.id,
6
6
  name: command.name,
7
7
  description: command.description,
8
+ kind: command.kind,
8
9
  }));
9
10
  return [
10
11
  ...new Map(commands.map((command) => [command.id, command])).values(),
@@ -0,0 +1,39 @@
1
+ import { type ModuleTier } from "@helyx/module-manifest";
2
+ import type { ModuleDefinition } from "@helyx/sdk";
3
+ import { type DashboardCommandDefinition } from "./dashboard-commands.js";
4
+ interface DashboardLogEventDefinition {
5
+ id: string;
6
+ name: string;
7
+ description: string;
8
+ defaultEnabled: boolean;
9
+ }
10
+ interface DashboardActionDefinition {
11
+ id: string;
12
+ label: string;
13
+ description: string;
14
+ buttonLabel: string;
15
+ style: "primary" | "secondary" | "danger";
16
+ }
17
+ type DashboardResourceDefinition = NonNullable<NonNullable<ModuleDefinition["manifest"]["dashboard"]>["resources"]>[number];
18
+ export interface DashboardModuleDefinition {
19
+ id: string;
20
+ name: string;
21
+ version: string;
22
+ summary: string;
23
+ category: string;
24
+ icon: string;
25
+ availability: "available";
26
+ releaseStatus: "planned" | "testing" | "beta" | "live";
27
+ tier: ModuleTier;
28
+ requiredPermissions: string[];
29
+ settings: readonly unknown[];
30
+ commands: DashboardCommandDefinition[];
31
+ logEvents: DashboardLogEventDefinition[];
32
+ actions: DashboardActionDefinition[];
33
+ achievementTriggers: NonNullable<ModuleDefinition["manifest"]["contributions"]["achievementTriggers"]>;
34
+ resources: readonly DashboardResourceDefinition[];
35
+ workspace?: NonNullable<ModuleDefinition["manifest"]["dashboard"]>["workspace"];
36
+ }
37
+ export declare function mapDashboardModule(module: ModuleDefinition): DashboardModuleDefinition;
38
+ export {};
39
+ //# sourceMappingURL=dashboard-module-projection.d.ts.map
@@ -0,0 +1,28 @@
1
+ import { getModuleTier } from "@helyx/module-manifest";
2
+ import { dashboardCategoryLabel } from "./dashboard-category.js";
3
+ import { dashboardCommands, } from "./dashboard-commands.js";
4
+ export function mapDashboardModule(module) {
5
+ const dashboard = module.manifest.dashboard;
6
+ return {
7
+ id: module.manifest.id,
8
+ name: module.manifest.name,
9
+ version: module.manifest.version,
10
+ summary: module.manifest.description,
11
+ category: dashboardCategoryLabel(dashboard.category),
12
+ icon: dashboard.icon,
13
+ availability: "available",
14
+ releaseStatus: module.manifest.releaseStatus,
15
+ tier: getModuleTier(module.manifest),
16
+ requiredPermissions: [...module.manifest.permissions],
17
+ settings: dashboard.settings,
18
+ commands: dashboardCommands(module),
19
+ logEvents: (module.logging?.events ?? []).map((event) => ({ ...event })),
20
+ actions: (dashboard.actions ?? []).map((action) => ({ ...action })),
21
+ achievementTriggers: (module.manifest.contributions?.achievementTriggers ?? []).map((trigger) => structuredClone(trigger)),
22
+ resources: (dashboard.resources ?? []).map((resource) => structuredClone(resource)),
23
+ workspace: dashboard.workspace
24
+ ? structuredClone(dashboard.workspace)
25
+ : undefined,
26
+ };
27
+ }
28
+ //# sourceMappingURL=dashboard-module-projection.js.map
package/dist/events.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { DiscordChannelAudienceInspection, DiscordChannelInspection, DiscordEmojiInspection, DiscordMessageInspection, DiscordGuildCapabilityInspection, DiscordGuildChannelAudienceInspection, DiscordResourceInspectionService, DiscordRoleInspection, DiscordMessageService, InstallationService, MemberEnforcementService, MemberRoleService, ModuleDefinition, ServiceAccess, VerificationMemberService } from "@helyx/sdk";
2
2
  import { type Client, type MessageReaction, type PartialMessageReaction } from "discord.js";
3
3
  import { type InteractionLogger } from "./interactions.js";
4
- export declare function availabilityAwareInstallations(services: ServiceAccess): Pick<InstallationService, "isModuleEnabled">;
4
+ export declare function availabilityAwareInstallations(services: ServiceAccess, modulesById?: ReadonlyMap<string, ModuleDefinition>): Pick<InstallationService, "isModuleEnabled">;
5
5
  export declare function installModuleEvents(client: Client, modules: readonly ModuleDefinition[], logger: InteractionLogger, services: ServiceAccess): ModuleEventHandle;
6
6
  export interface ModuleEventHandle {
7
7
  stop(): Promise<void>;
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,
@@ -149,7 +154,7 @@ export function installModuleEvents(client, modules, logger, services) {
149
154
  if (!message.guildId || !privilegedEligibilityQueue)
150
155
  return;
151
156
  privilegedEligibilityQueue.enqueue(message.guildId, async () => {
152
- const installations = availabilityAwareInstallations(services);
157
+ const installations = availabilityAwareInstallations(services, modulesById);
153
158
  const eligible = [];
154
159
  for (const item of privilegedMessageContributions) {
155
160
  try {
@@ -301,7 +306,7 @@ export function installModuleEvents(client, modules, logger, services) {
301
306
  });
302
307
  }
303
308
  async function handleResourceLifecycle(contributions, serverId, identifier, resourceId, eventName) {
304
- const installations = availabilityAwareInstallations(services);
309
+ const installations = availabilityAwareInstallations(services, modulesById);
305
310
  for (const { moduleId, contribution } of contributions) {
306
311
  try {
307
312
  if (!(await installations.isModuleEnabled(serverId, moduleId))) {
@@ -327,7 +332,7 @@ export function installModuleEvents(client, modules, logger, services) {
327
332
  async function handlePrivilegedMessageCreate(message, contributions) {
328
333
  if (!message.guildId || !message.channelId)
329
334
  return;
330
- const installations = availabilityAwareInstallations(services);
335
+ const installations = availabilityAwareInstallations(services, modulesById);
331
336
  for (const { moduleId, contribution } of contributions) {
332
337
  try {
333
338
  if (!(await installations.isModuleEnabled(message.guildId, moduleId)))
@@ -360,7 +365,7 @@ export function installModuleEvents(client, modules, logger, services) {
360
365
  async function handleGuildMessageCreate(message) {
361
366
  if (!message.guildId || !message.channelId)
362
367
  return;
363
- const installations = availabilityAwareInstallations(services);
368
+ const installations = availabilityAwareInstallations(services, modulesById);
364
369
  const parentChannelId = message.channel.isThread()
365
370
  ? (message.channel.parentId ?? undefined)
366
371
  : undefined;
@@ -456,7 +461,7 @@ export function installModuleEvents(client, modules, logger, services) {
456
461
  : reaction.message;
457
462
  if (!message.guildId || !message.channelId)
458
463
  return;
459
- const installations = availabilityAwareInstallations(services);
464
+ const installations = availabilityAwareInstallations(services, modulesById);
460
465
  const member = await message.guild?.members.fetch(user.id);
461
466
  const userRoleIds = member ? [...member.roles.cache.keys()] : [];
462
467
  const parentChannelId = messageParentChannelId(message);
@@ -472,6 +477,14 @@ export function installModuleEvents(client, modules, logger, services) {
472
477
  messageId: message.id,
473
478
  userId: receivedUser.id,
474
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,
475
488
  emojiKey: reactionEmojiKey(reaction),
476
489
  emojiIdentifier: reactionEmojiIdentifier(reaction),
477
490
  reactionCount: Math.max(0, reaction.count ?? 0),
@@ -513,7 +526,7 @@ export function installModuleEvents(client, modules, logger, services) {
513
526
  async function handleMemberAdd(member) {
514
527
  if (!member.joinedAt)
515
528
  throw new Error("Discord member join timestamp is unavailable");
516
- const installations = availabilityAwareInstallations(services);
529
+ const installations = availabilityAwareInstallations(services, modulesById);
517
530
  const continuityGeneration = inviteInspection
518
531
  ? inviteInspection.continuityGeneration({ guildId: member.guild.id })
519
532
  : `runtime:${client.readyTimestamp ?? 0}`;
@@ -563,7 +576,7 @@ export function installModuleEvents(client, modules, logger, services) {
563
576
  }
564
577
  }
565
578
  async function handleMemberRemove(member) {
566
- const installations = availabilityAwareInstallations(services);
579
+ const installations = availabilityAwareInstallations(services, modulesById);
567
580
  for (const { moduleId, contribution } of memberRemoveContributions) {
568
581
  try {
569
582
  if (!(await installations.isModuleEnabled(member.guild.id, moduleId))) {
@@ -603,7 +616,7 @@ export function installModuleEvents(client, modules, logger, services) {
603
616
  : reaction.message;
604
617
  if (!message.guildId || !message.channelId)
605
618
  return;
606
- const installations = availabilityAwareInstallations(services);
619
+ const installations = availabilityAwareInstallations(services, modulesById);
607
620
  const parentChannelId = messageParentChannelId(message);
608
621
  for (const { moduleId, contribution } of reactionRemoveContributions) {
609
622
  if (!(await installations.isModuleEnabled(message.guildId, moduleId))) {
@@ -647,7 +660,7 @@ export function installModuleEvents(client, modules, logger, services) {
647
660
  : receivedMessage;
648
661
  if (!message.guildId || !message.channelId)
649
662
  return;
650
- const installations = availabilityAwareInstallations(services);
663
+ const installations = availabilityAwareInstallations(services, modulesById);
651
664
  for (const { moduleId, contribution } of reactionRemoveAllContributions) {
652
665
  try {
653
666
  if (!(await installations.isModuleEnabled(message.guildId, moduleId)))
@@ -677,7 +690,7 @@ export function installModuleEvents(client, modules, logger, services) {
677
690
  const message = reaction.message;
678
691
  if (!message.guildId || !message.channelId)
679
692
  return;
680
- const installations = availabilityAwareInstallations(services);
693
+ const installations = availabilityAwareInstallations(services, modulesById);
681
694
  for (const { moduleId, contribution } of reactionRemoveEmojiContributions) {
682
695
  try {
683
696
  if (!(await installations.isModuleEnabled(message.guildId, moduleId)))
@@ -705,7 +718,7 @@ export function installModuleEvents(client, modules, logger, services) {
705
718
  async function handleMessageUpdate(receivedMessage) {
706
719
  if (!receivedMessage.guildId || !receivedMessage.channelId)
707
720
  return;
708
- const installations = availabilityAwareInstallations(services);
721
+ const installations = availabilityAwareInstallations(services, modulesById);
709
722
  for (const { moduleId, contribution } of messageUpdateContributions) {
710
723
  try {
711
724
  if (!(await installations.isModuleEnabled(receivedMessage.guildId, moduleId)))
@@ -739,7 +752,7 @@ export function installModuleEvents(client, modules, logger, services) {
739
752
  async function handleMessageDelete(message) {
740
753
  if (!message.guildId || !message.channelId)
741
754
  return;
742
- const installations = availabilityAwareInstallations(services);
755
+ const installations = availabilityAwareInstallations(services, modulesById);
743
756
  for (const { moduleId, contribution } of messageDeleteContributions) {
744
757
  try {
745
758
  if (!(await installations.isModuleEnabled(message.guildId, moduleId))) {
@@ -766,7 +779,7 @@ export function installModuleEvents(client, modules, logger, services) {
766
779
  async function handleMessageDeleteBulk(guildId, channelId, messageIds) {
767
780
  if (messageIds.length === 0)
768
781
  return;
769
- const installations = availabilityAwareInstallations(services);
782
+ const installations = availabilityAwareInstallations(services, modulesById);
770
783
  const boundedIds = messageIds.slice(0, 100);
771
784
  for (const { moduleId, contribution } of messageDeleteBulkContributions) {
772
785
  try {
@@ -800,7 +813,7 @@ export function installModuleEvents(client, modules, logger, services) {
800
813
  voiceSessions.delete(key);
801
814
  const endedAt = new Date();
802
815
  const durationSeconds = Math.max(1, Math.floor((endedAt.getTime() - previous.startedAt.getTime()) / 1_000));
803
- const installations = availabilityAwareInstallations(services);
816
+ const installations = availabilityAwareInstallations(services, modulesById);
804
817
  for (const { moduleId, contribution } of guildVoiceSessionContributions) {
805
818
  try {
806
819
  if (!(await installations.isModuleEnabled(previous.guildId, moduleId)))
@@ -2061,8 +2074,4 @@ function isSingleUnicodeEmoji(value) {
2061
2074
  ];
2062
2075
  return graphemes.length === 1;
2063
2076
  }
2064
- function safeError(error) {
2065
- const value = error instanceof Error ? error : new Error(String(error));
2066
- return { name: value.name, message: value.message };
2067
- }
2068
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