@helyx/bot 0.4.9 → 0.4.11

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,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.11
4
+
5
+ ### Patch Changes
6
+
7
+ - Add the reusable Stage 8 Levels foundations: privacy-bounded guild message
8
+ events with optional transient type classification, optional Discord-user
9
+ command options, multiple channel settings, bounded ranked numeric records,
10
+ idempotent numeric mutation receipts, and core-owned write-behind accumulation
11
+ across local and coordinated shard compositions.
12
+ - Updated dependencies
13
+ - @helyx/database@0.5.0
14
+ - @helyx/module-manifest@0.5.0
15
+ - @helyx/control-protocol@0.2.0
16
+ - @helyx/sdk@0.4.0
17
+ - @helyx/platform@0.3.0
18
+ - @helyx/core@0.1.11
19
+ - @helyx/framework@0.1.11
20
+
21
+ ## 0.4.10
22
+
23
+ ### Patch Changes
24
+
25
+ - Add the shared session, Discord event, resource inspection, enforcement,
26
+ privacy and hosted/self-hosted composition support required by the Account Gate
27
+ Testing module.
28
+ - Updated dependencies
29
+ - @helyx/config@0.4.3
30
+ - @helyx/database@0.4.4
31
+ - @helyx/sdk@0.3.5
32
+ - @helyx/platform@0.2.8
33
+ - @helyx/core@0.1.10
34
+ - @helyx/framework@0.1.10
35
+
3
36
  ## 0.4.9
4
37
 
5
38
  ### Patch Changes
@@ -146,11 +146,12 @@ export class CloudControlDispatcher {
146
146
  break;
147
147
  }
148
148
  case "guild.module.resource.action.execute.v1": {
149
- const input = parseOperationInput(request.operation, request.input);
149
+ const { actorAuthenticatedAt, ...input } = parseOperationInput(request.operation, request.input);
150
150
  result = await this.control.executeManagedResourceAction({
151
151
  ...input,
152
152
  actorUserId: requiredActor(actorUserId),
153
153
  correlationId,
154
+ ...(actorAuthenticatedAt ? { actorAuthenticatedAt } : {}),
154
155
  });
155
156
  break;
156
157
  }
@@ -104,6 +104,9 @@ export interface SupportNotificationInput {
104
104
  source: "dashboard_bug" | "dashboard_support" | "dashboard_security" | "discord_command";
105
105
  }
106
106
  export interface BotControlState {
107
+ discordCapabilities?: {
108
+ messageContent: boolean;
109
+ };
107
110
  channels: BotControlGuildResources["channels"];
108
111
  roles: BotControlGuildResources["roles"];
109
112
  modules: DashboardModuleDefinition[];
@@ -237,6 +240,7 @@ export interface BotControlHandler {
237
240
  value: Readonly<Record<string, ManagedResourceValue>>;
238
241
  actorUserId: string;
239
242
  correlationId: string;
243
+ actorAuthenticatedAt?: string;
240
244
  }): Promise<ManagedResourceDetail | null>;
241
245
  listPrivacyProviders(): Promise<readonly {
242
246
  id: string;
@@ -415,6 +419,7 @@ export declare class BotControlService implements BotControlHandler {
415
419
  value: Readonly<Record<string, ManagedResourceValue>>;
416
420
  actorUserId: string;
417
421
  correlationId: string;
422
+ actorAuthenticatedAt?: string;
418
423
  }): Promise<ManagedResourceDetail | null>;
419
424
  }
420
425
  export declare function createBotControlServer(input: {
@@ -9,6 +9,7 @@ import { addChannelMessageReaction as addDiscordChannelMessageReaction, deleteCh
9
9
  import { SHARD_HEALTH_STALE_MS, } from "./shard-contracts.js";
10
10
  const snowflake = z.string().regex(/^\d{17,20}$/u);
11
11
  const moduleId = z.string().regex(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/u);
12
+ const MAX_DISCORD_SETTING_SELECTIONS = 25;
12
13
  const configurationValue = z.record(z.string(), z.union([
13
14
  z.null(),
14
15
  z.string(),
@@ -223,7 +224,7 @@ export class BotControlService {
223
224
  ? modules
224
225
  .filter((module) => module.privacy !== undefined)
225
226
  .map((module) => {
226
- const provider = module.privacy.create(privacyExecutor);
227
+ const provider = module.privacy.create(privacyExecutor, this.#services);
227
228
  return [provider.id, provider];
228
229
  })
229
230
  : []);
@@ -290,10 +291,11 @@ export class BotControlService {
290
291
  });
291
292
  }
292
293
  async getGuildState(guildId) {
293
- const [resources, installedModules, storedLoggingPolicies] = await Promise.all([
294
+ const [resources, installedModules, storedLoggingPolicies, runtime] = await Promise.all([
294
295
  this.resources.getGuildResources(guildId),
295
296
  new InstallationRepository(this.database).listModules(guildId),
296
297
  new ModuleLoggingPolicyRepository(this.database).listForGuild(guildId),
298
+ this.#inspectRuntimeCapabilities(),
297
299
  ]);
298
300
  const installed = new Map(installedModules.map((module) => [module.moduleId, module]));
299
301
  const moduleStatuses = {};
@@ -332,6 +334,9 @@ export class BotControlService {
332
334
  });
333
335
  }));
334
336
  return {
337
+ discordCapabilities: {
338
+ messageContent: runtime.messageContent,
339
+ },
335
340
  ...resources,
336
341
  modules: structuredClone(this.#dashboardModules),
337
342
  moduleStatuses,
@@ -340,9 +345,29 @@ export class BotControlService {
340
345
  logging,
341
346
  };
342
347
  }
348
+ async #inspectRuntimeCapabilities() {
349
+ if (!this.#services.has(HELYX_SERVICE_NAMES.discordResources))
350
+ return { messageContent: false };
351
+ const inspector = this.#services.get(HELYX_SERVICE_NAMES.discordResources);
352
+ if (typeof inspector.inspectRuntimeCapabilities !== "function")
353
+ return { messageContent: false };
354
+ const runtime = await inspector.inspectRuntimeCapabilities();
355
+ return { messageContent: runtime.messageContent };
356
+ }
343
357
  async setModuleStatus(input) {
344
- this.#requireDashboardModule(input.moduleId);
345
- await this.#installations.setModuleEnabled({
358
+ const module = this.#requireDashboardModule(input.moduleId);
359
+ const scopes = Object.entries(module.records ?? {})
360
+ .filter(([, collection]) => (collection.incrementableFields?.length ?? 0) > 0)
361
+ .map(([collection]) => ({
362
+ moduleId: input.moduleId,
363
+ collection,
364
+ guildId: input.guildId,
365
+ }));
366
+ const writeBehind = scopes.length > 0 &&
367
+ this.#services.has(HELYX_SERVICE_NAMES.numericWriteBehind)
368
+ ? this.#services.get(HELYX_SERVICE_NAMES.numericWriteBehind)
369
+ : null;
370
+ const setEnabled = () => this.#installations.setModuleEnabled({
346
371
  guildId: input.guildId,
347
372
  moduleId: input.moduleId,
348
373
  enabled: input.enabled,
@@ -352,11 +377,56 @@ export class BotControlService {
352
377
  source: "dashboard",
353
378
  },
354
379
  });
380
+ if (!writeBehind) {
381
+ await setEnabled();
382
+ }
383
+ else if (input.enabled) {
384
+ for (const scope of scopes) {
385
+ const suppression = await writeBehind.suppressGuild(scope);
386
+ await writeBehind.releaseGuild({
387
+ ...scope,
388
+ suppressionId: suppression.suppressionId,
389
+ });
390
+ }
391
+ await setEnabled();
392
+ }
393
+ else {
394
+ const runBoundary = async (index) => {
395
+ const scope = scopes[index];
396
+ if (!scope) {
397
+ await setEnabled();
398
+ return;
399
+ }
400
+ await writeBehind.runGuildBarrier(scope, () => runBoundary(index + 1));
401
+ };
402
+ await runBoundary(0);
403
+ for (const scope of scopes)
404
+ await writeBehind.flushGuild(scope);
405
+ for (const scope of scopes)
406
+ await writeBehind.suppressGuild(scope);
407
+ }
355
408
  return this.getGuildState(input.guildId);
356
409
  }
357
410
  async updateConfiguration(input) {
358
411
  const module = this.#requireDashboardModule(input.moduleId);
359
412
  await this.#validateDiscordSettings(input.guildId, module, input.value);
413
+ if (module.configuration?.validateUpdate) {
414
+ try {
415
+ const value = module.configuration.validate(input.value);
416
+ await module.configuration.validateUpdate({
417
+ guildId: input.guildId,
418
+ value,
419
+ services: this.#services,
420
+ });
421
+ }
422
+ catch (error) {
423
+ if (error instanceof ModuleStateValidationError)
424
+ throw error;
425
+ throw new DashboardActionValidationError(error instanceof Error
426
+ ? error.message
427
+ : "The module rejected these settings.");
428
+ }
429
+ }
360
430
  const saved = await this.#configurations.update({
361
431
  guildId: input.guildId,
362
432
  moduleId: input.moduleId,
@@ -603,15 +673,13 @@ export class BotControlService {
603
673
  }));
604
674
  }
605
675
  async executeManagedResourceAction(input) {
606
- const module = this.#requireDashboardModule(input.moduleId);
676
+ const { module, resource } = await this.#prepareManagedResource(input);
607
677
  const declared = module.manifest.dashboard?.resources
608
678
  ?.find(({ id }) => id === input.resourceType)
609
679
  ?.actions?.find(({ id }) => id === input.actionId);
610
- const resource = this.#requireManagedResource(module, input.resourceType);
611
680
  this.#requireManagedResourceOperation(module, input.resourceType, "action");
612
681
  if (!declared)
613
682
  throw new BotControlNotFoundError("Managed resource action is not declared by this module");
614
- await this.#requireEnabledManagedResource(input.guildId, input.moduleId);
615
683
  const permissionId = managedResourcePermissionId(module, input.resourceType, "action", input.actionId, declared.permissionId);
616
684
  const context = await this.#managedResourceContext(input, module, "action", permissionId);
617
685
  validateManagedResourceActionValue(declared.inputs ?? [], input.value);
@@ -628,7 +696,8 @@ export class BotControlService {
628
696
  async #prepareManagedResource(input) {
629
697
  const module = this.#requireDashboardModule(input.moduleId);
630
698
  const resource = this.#requireManagedResource(module, input.resourceType);
631
- await this.#requireEnabledManagedResource(input.guildId, input.moduleId);
699
+ if (!resource.availableWhileDisabled)
700
+ await this.#requireEnabledManagedResource(input.guildId, input.moduleId);
632
701
  return { module, resource };
633
702
  }
634
703
  #requireManagedResource(module, resourceType) {
@@ -667,6 +736,10 @@ export class BotControlService {
667
736
  return {
668
737
  guildId: input.guildId,
669
738
  actor,
739
+ ...("actorAuthenticatedAt" in input &&
740
+ typeof input.actorAuthenticatedAt === "string"
741
+ ? { actorAuthenticatedAt: input.actorAuthenticatedAt }
742
+ : {}),
670
743
  correlationId: input.correlationId,
671
744
  services: this.#services,
672
745
  sendToChannel: (channelId, response) => this.resources.sendToChannel(channelId, response),
@@ -734,14 +807,25 @@ export class BotControlService {
734
807
  if (setting.type === "discord-channel") {
735
808
  if (configured === "" || configured === undefined)
736
809
  continue;
737
- const channel = typeof configured === "string" ? channels.get(configured) : null;
738
- if (!channel || !setting.channelTypes.includes(channel.type))
739
- throw new BotControlValidationError(`${setting.label} must be an available supported Discord channel`);
810
+ if (Boolean(setting.multiple) !== Array.isArray(configured))
811
+ throw new BotControlValidationError(`${setting.label} has an invalid Discord channel selection`);
812
+ const configuredChannels = Array.isArray(configured)
813
+ ? configured
814
+ : [configured];
815
+ if (configuredChannels.length > MAX_DISCORD_SETTING_SELECTIONS ||
816
+ new Set(configuredChannels).size !== configuredChannels.length ||
817
+ configuredChannels.some((channelId) => {
818
+ if (typeof channelId !== "string")
819
+ return true;
820
+ const channel = channels.get(channelId);
821
+ return !channel || !setting.channelTypes.includes(channel.type);
822
+ }))
823
+ throw new BotControlValidationError(`${setting.label} must contain available supported Discord channels`);
740
824
  }
741
825
  if (setting.type === "discord-role") {
742
826
  if (configured === "" || configured === undefined)
743
827
  continue;
744
- if (setting.multiple !== Array.isArray(configured))
828
+ if (Boolean(setting.multiple) !== Array.isArray(configured))
745
829
  throw new BotControlValidationError(`${setting.label} has an invalid Discord role selection`);
746
830
  const configuredRoles = Array.isArray(configured)
747
831
  ? configured
@@ -1101,12 +1185,19 @@ export async function createBotControlServer(input) {
1101
1185
  .extend({
1102
1186
  expectedRevision: z.number().int().min(0),
1103
1187
  value: managedResourceObject.optional().default({}),
1188
+ actorAuthenticatedAt: z.iso.datetime({ offset: true }).optional(),
1104
1189
  })
1105
1190
  .strict()
1106
1191
  .parse(request.body);
1107
1192
  return input.service.executeManagedResourceAction({
1108
1193
  ...params,
1109
- ...body,
1194
+ actorUserId: body.actorUserId,
1195
+ correlationId: body.correlationId,
1196
+ expectedRevision: body.expectedRevision,
1197
+ value: body.value,
1198
+ ...(body.actorAuthenticatedAt === undefined
1199
+ ? {}
1200
+ : { actorAuthenticatedAt: body.actorAuthenticatedAt }),
1110
1201
  });
1111
1202
  });
1112
1203
  server.put("/v1/guilds/:guildId/modules/:moduleId/configuration", async (request) => {
package/dist/events.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { DiscordChannelInspection, DiscordEmojiInspection, DiscordResourceInspectionService, DiscordRoleInspection, MemberRoleService, ModuleDefinition, ServiceAccess } from "@helyx/sdk";
1
+ import type { DiscordChannelAudienceInspection, DiscordChannelInspection, DiscordEmojiInspection, DiscordMessageInspection, DiscordGuildCapabilityInspection, DiscordGuildChannelAudienceInspection, DiscordResourceInspectionService, DiscordRoleInspection, DiscordMessageService, MemberEnforcementService, MemberRoleService, ModuleDefinition, ServiceAccess } from "@helyx/sdk";
2
2
  import { type Client, type MessageReaction, type PartialMessageReaction } from "discord.js";
3
3
  import { type InteractionLogger } from "./interactions.js";
4
4
  export declare function installModuleEvents(client: Client, modules: readonly ModuleDefinition[], logger: InteractionLogger, services: ServiceAccess): ModuleEventHandle;
@@ -15,9 +15,30 @@ interface ApprovedRoleMutation {
15
15
  moduleId: string;
16
16
  reason: string;
17
17
  }
18
+ export declare class DiscordMessageDeliveryService implements DiscordMessageService {
19
+ private readonly client;
20
+ constructor(client: Client);
21
+ send(input: Parameters<DiscordMessageService["send"]>[0]): ReturnType<DiscordMessageService["send"]>;
22
+ sendDirect(input: Parameters<DiscordMessageService["sendDirect"]>[0]): ReturnType<DiscordMessageService["sendDirect"]>;
23
+ }
24
+ export declare class DiscordMemberEnforcementService implements MemberEnforcementService {
25
+ #private;
26
+ private readonly client;
27
+ constructor(client: Client);
28
+ kick(input: Parameters<MemberEnforcementService["kick"]>[0]): ReturnType<MemberEnforcementService["kick"]>;
29
+ }
18
30
  export declare class DiscordMemberRoleService implements MemberRoleService {
19
31
  private readonly client;
20
32
  constructor(client: Client);
33
+ createApprovedRole(input: {
34
+ guildId: string;
35
+ name: string;
36
+ moduleId: string;
37
+ reason: string;
38
+ }): Promise<{
39
+ roleId: string;
40
+ name: string;
41
+ }>;
21
42
  getCurrentRoleIds(input: {
22
43
  guildId: string;
23
44
  memberUserId: string;
@@ -45,6 +66,15 @@ export declare class DiscordResourceInspection implements DiscordResourceInspect
45
66
  #private;
46
67
  private readonly client;
47
68
  constructor(client: Client);
69
+ inspectGuildCapabilities(input: {
70
+ guildId: string;
71
+ }): Promise<DiscordGuildCapabilityInspection>;
72
+ inspectRuntimeCapabilities(): Promise<{
73
+ guildMembers: boolean;
74
+ guildMessages: boolean;
75
+ guildMessageReactions: boolean;
76
+ messageContent: boolean;
77
+ }>;
48
78
  resolveMemberSnapshots(input: {
49
79
  guildId: string;
50
80
  userIds: readonly string[];
@@ -61,6 +91,17 @@ export declare class DiscordResourceInspection implements DiscordResourceInspect
61
91
  }): Promise<readonly {
62
92
  userId: string;
63
93
  displayName: string | null;
94
+ avatarUrl?: string;
95
+ }[]>;
96
+ searchMembers(input: {
97
+ guildId: string;
98
+ query: string;
99
+ limit: number;
100
+ }): Promise<readonly {
101
+ userId: string;
102
+ displayName: string;
103
+ avatarUrl?: string;
104
+ isBot: boolean;
64
105
  }[]>;
65
106
  inspectRole(input: {
66
107
  guildId: string;
@@ -75,6 +116,28 @@ export declare class DiscordResourceInspection implements DiscordResourceInspect
75
116
  guildId: string;
76
117
  channelId: string;
77
118
  }): Promise<DiscordChannelInspection>;
119
+ inspectMessage(input: {
120
+ guildId: string;
121
+ channelId: string;
122
+ messageId: string;
123
+ }): Promise<DiscordMessageInspection>;
124
+ inspectChannelAudience(input: {
125
+ guildId: string;
126
+ channelId: string;
127
+ roleIds: readonly string[];
128
+ }): Promise<DiscordChannelAudienceInspection>;
129
+ inspectGuildChannelAudience(input: {
130
+ guildId: string;
131
+ roleIds: readonly string[];
132
+ }): Promise<DiscordGuildChannelAudienceInspection>;
133
+ seedMessageReaction(input: {
134
+ guildId: string;
135
+ channelId: string;
136
+ messageId: string;
137
+ emojiIdentifier: string;
138
+ }): Promise<{
139
+ added: boolean;
140
+ }>;
78
141
  }
79
142
  export {};
80
143
  //# sourceMappingURL=events.d.ts.map