@helyx/bot 0.4.44 → 0.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - bbc4eee: Add the shared exact-progress, zoned-time, ordered-retention, multi-schedule, and durable cross-module role-ownership contracts required by the Events and Community Goals Testing modules.
8
+
9
+ Register the PostgreSQL progress and ownership services in core, deliver owned roles through bot roots and shard workers, and package the corresponding database migration and rollback.
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies [bbc4eee]
14
+ - Updated dependencies
15
+ - @helyx/sdk@0.10.0
16
+ - @helyx/platform@0.7.0
17
+ - @helyx/database@0.10.0
18
+
19
+ ## 0.4.48
20
+
21
+ ### Patch Changes
22
+
23
+ - Add declared command-operation permission targets, project them through both permission-management surfaces, and harden module lifecycle, health, visibility, bounded-work, optional-side-effect, command-permission, and Forms durable-workflow behavior. Preserve existing FAQ, Access Control, Achievements, Giveaways and Levels overrides through additive migrations while assigning every slash-command path its own permission identity.
24
+
25
+ ## 0.4.47
26
+
27
+ ### Patch Changes
28
+
29
+ - Add the bounded Discord inspection, durable record scheduling and recurrence, trusted dashboard workspaces, rich-message composition, optional invite-attribution contracts, module deactivation/configuration hooks, and versioned schedulable-action registry required by the Analytics, Invite Tracking, Reminders, Birthdays, Role Expiry, Scheduler, Message Embedder, Auto-message, and Sticky Messages testing modules.
30
+
31
+ ## 0.4.46
32
+
33
+ ### Patch Changes
34
+
35
+ - Add explicit role-mention allowlists and member-leave identity metadata to the
36
+ module runtime contract. Upgrade Welcome with multiline user, role and channel
37
+ placeholders, an inline dashboard test send, and independent member-left
38
+ logging.
39
+
40
+ ## 0.4.45
41
+
42
+ ### Patch Changes
43
+
44
+ - Expose bounded structural user mentions to guild message event contributions without exposing message content, and include AFK in the reviewed self-host catalogue.
45
+
3
46
  ## 0.4.44
4
47
 
5
48
  ### Patch Changes
@@ -0,0 +1,26 @@
1
+ import type { CommandContribution, ModuleDefinition, SubcommandContribution } from "@helyx/sdk";
2
+ export interface ExecutableCommandPath {
3
+ path: string;
4
+ permissionId: string;
5
+ explicitPermissionId: boolean;
6
+ name: string;
7
+ description: string;
8
+ source: "direct" | "shared" | "nexus";
9
+ }
10
+ export interface CommandPermissionTarget {
11
+ id: string;
12
+ name: string;
13
+ description: string;
14
+ kind: "command" | "operation";
15
+ }
16
+ export interface CommandPathContractIssue {
17
+ key: string;
18
+ message: string;
19
+ }
20
+ export declare const legacyCommandPathContractIssues: Readonly<Record<string, ReadonlySet<string>>>;
21
+ export declare function collectExecutableCommandPaths(module: ModuleDefinition): ExecutableCommandPath[];
22
+ export declare function collectCommandPermissionTargets(module: ModuleDefinition): CommandPermissionTarget[];
23
+ export declare function resolveDirectCommandPath(command: CommandContribution, subcommand?: SubcommandContribution, groupName?: string | null): Pick<ExecutableCommandPath, "path" | "permissionId">;
24
+ export declare function inspectCommandPathContract(module: ModuleDefinition): CommandPathContractIssue[];
25
+ export declare function assertCommandPathContract(module: ModuleDefinition): void;
26
+ //# sourceMappingURL=command-path-contract.d.ts.map
@@ -0,0 +1,157 @@
1
+ export const legacyCommandPathContractIssues = Object.freeze({});
2
+ export function collectExecutableCommandPaths(module) {
3
+ const paths = (module.interactions?.commands ?? []).flatMap((command) => collectDirectCommandPaths(command));
4
+ paths.push(...(module.sharedCommands ?? []).map((command) => {
5
+ const path = `${command.family}.${command.path}`;
6
+ return {
7
+ path,
8
+ permissionId: command.permissionId ?? path,
9
+ explicitPermissionId: command.permissionId !== undefined,
10
+ name: `/${command.family} ${command.path.replaceAll(".", " ")}`,
11
+ description: command.description,
12
+ source: "shared",
13
+ };
14
+ }), ...(module.nexusCommands ?? []).map((command) => {
15
+ const path = `nexus.${command.path}`;
16
+ return {
17
+ path,
18
+ permissionId: path,
19
+ explicitPermissionId: true,
20
+ name: `/nexus ${command.path.replaceAll(".", " ")}`,
21
+ description: command.description,
22
+ source: "nexus",
23
+ };
24
+ }));
25
+ return paths;
26
+ }
27
+ export function collectCommandPermissionTargets(module) {
28
+ return [
29
+ ...collectExecutableCommandPaths(module).map((command) => ({
30
+ id: command.permissionId,
31
+ name: command.name,
32
+ description: command.description,
33
+ kind: "command",
34
+ })),
35
+ ...(module.interactions?.commands ?? []).flatMap((command) => [
36
+ ...permissionOperations(command.permissionOperations),
37
+ ...(command.subcommands ?? []).flatMap((subcommand) => permissionOperations(subcommand.permissionOperations)),
38
+ ...(command.subcommandGroups ?? []).flatMap((group) => group.subcommands.flatMap((subcommand) => permissionOperations(subcommand.permissionOperations))),
39
+ ]),
40
+ ...(module.sharedCommands ?? []).flatMap((command) => permissionOperations(command.permissionOperations)),
41
+ ...(module.nexusCommands ?? []).flatMap((command) => permissionOperations(command.permissionOperations)),
42
+ ];
43
+ }
44
+ export function resolveDirectCommandPath(command, subcommand, groupName) {
45
+ const path = [command.name, groupName, subcommand?.name]
46
+ .filter(Boolean)
47
+ .join(".");
48
+ return {
49
+ path,
50
+ permissionId: subcommand?.permissionId ?? command.permissionId ?? path,
51
+ };
52
+ }
53
+ export function inspectCommandPathContract(module) {
54
+ const issues = [];
55
+ const paths = collectExecutableCommandPaths(module);
56
+ const operations = collectCommandPermissionTargets(module).filter(({ kind }) => kind === "operation");
57
+ const pathsByPermissionId = new Map();
58
+ const seenPaths = new Set();
59
+ for (const command of paths) {
60
+ if (seenPaths.has(command.path)) {
61
+ issues.push({
62
+ key: `duplicate-command-path:${command.path}`,
63
+ message: `duplicate executable command path ${command.path}`,
64
+ });
65
+ }
66
+ seenPaths.add(command.path);
67
+ if (!command.explicitPermissionId) {
68
+ issues.push({
69
+ key: `missing-permission-id:${command.path}`,
70
+ message: `command ${command.path} has no explicit permission ID`,
71
+ });
72
+ continue;
73
+ }
74
+ const owners = pathsByPermissionId.get(command.permissionId) ?? [];
75
+ pathsByPermissionId.set(command.permissionId, [...owners, command.path]);
76
+ if (!Object.prototype.hasOwnProperty.call(module.defaultPermissions?.commands ?? {}, command.permissionId)) {
77
+ issues.push({
78
+ key: `missing-default-policy:${command.path}`,
79
+ message: `command ${command.path} has no explicit default policy for ${command.permissionId}`,
80
+ });
81
+ }
82
+ }
83
+ for (const operation of operations) {
84
+ if (operation.id.trim().length === 0 ||
85
+ operation.id.length > 100 ||
86
+ !/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/u.test(operation.id) ||
87
+ operation.name.trim().length === 0 ||
88
+ operation.name.length > 100 ||
89
+ operation.description.trim().length === 0) {
90
+ issues.push({
91
+ key: `invalid-permission-operation:${operation.id}`,
92
+ message: "command permission operations require a namespaced ID and a non-empty Discord-safe name and description",
93
+ });
94
+ continue;
95
+ }
96
+ const owners = pathsByPermissionId.get(operation.id) ?? [];
97
+ pathsByPermissionId.set(operation.id, [
98
+ ...owners,
99
+ `operation:${operation.name}`,
100
+ ]);
101
+ if (!Object.prototype.hasOwnProperty.call(module.defaultPermissions?.commands ?? {}, operation.id)) {
102
+ issues.push({
103
+ key: `missing-default-policy:operation:${operation.id}`,
104
+ message: `command operation ${operation.name} has no explicit default policy for ${operation.id}`,
105
+ });
106
+ }
107
+ }
108
+ for (const [permissionId, owners] of pathsByPermissionId) {
109
+ if (owners.length > 1) {
110
+ issues.push({
111
+ key: `duplicate-permission-id:${permissionId}`,
112
+ message: `permission ID ${permissionId} is shared by ${owners.join(", ")}`,
113
+ });
114
+ }
115
+ }
116
+ return issues.sort((left, right) => left.key.localeCompare(right.key, "en-GB"));
117
+ }
118
+ function permissionOperations(operations) {
119
+ return (operations ?? []).map((operation) => ({
120
+ ...operation,
121
+ kind: "operation",
122
+ }));
123
+ }
124
+ export function assertCommandPathContract(module) {
125
+ const issues = inspectCommandPathContract(module);
126
+ const actual = new Set(issues.map(({ key }) => key));
127
+ const expected = legacyCommandPathContractIssues[module.manifest.id] ?? new Set();
128
+ const unexpected = issues.filter(({ key }) => !expected.has(key));
129
+ const stale = [...expected].filter((key) => !actual.has(key));
130
+ if (unexpected.length === 0 && stale.length === 0)
131
+ return;
132
+ const details = [
133
+ ...unexpected.map(({ message }) => message),
134
+ ...stale.map((key) => `legacy command-path exception is no longer needed: ${key}`),
135
+ ];
136
+ throw new Error(`Module ${module.manifest.id} command-path contract failed:\n${details.join("\n")}`);
137
+ }
138
+ function collectDirectCommandPaths(command) {
139
+ const nested = [
140
+ ...(command.subcommands ?? []).map((subcommand) => directCommandPath(command, subcommand)),
141
+ ...(command.subcommandGroups ?? []).flatMap((group) => group.subcommands.map((subcommand) => directCommandPath(command, subcommand, group.name))),
142
+ ];
143
+ if (nested.length > 0 || !command.execute)
144
+ return nested;
145
+ return [directCommandPath(command)];
146
+ }
147
+ function directCommandPath(command, subcommand, groupName) {
148
+ const resolved = resolveDirectCommandPath(command, subcommand, groupName);
149
+ return {
150
+ ...resolved,
151
+ explicitPermissionId: (subcommand?.permissionId ?? command.permissionId) !== undefined,
152
+ name: `/${resolved.path.replaceAll(".", " ")}`,
153
+ description: subcommand?.description ?? command.description,
154
+ source: "direct",
155
+ };
156
+ }
157
+ //# sourceMappingURL=command-path-contract.js.map
@@ -9,6 +9,7 @@ export function dashboardActionPermissionId(module, actionId) {
9
9
  return selectDeclaredPermission(module, [
10
10
  `${namespace}.${actionId.replaceAll("-", ".")}`,
11
11
  `${namespace}.configure`,
12
+ `${namespace}.manage`,
12
13
  ]);
13
14
  }
14
15
  export function managedResourcePermissionId(module, resourceType, operation, actionId, declaredPermissionId) {
@@ -51,7 +51,7 @@ export interface BotControlGuildResources {
51
51
  channels: Array<{
52
52
  id: string;
53
53
  name: string;
54
- type: "guild_text" | "guild_announcement" | "guild_category";
54
+ type: "guild_text" | "guild_announcement" | "guild_category" | "guild_forum" | "guild_media";
55
55
  }>;
56
56
  roles: Array<{
57
57
  id: string;
@@ -147,7 +147,9 @@ export class DiscordGuildResourceDirectory {
147
147
  channels: [...channels.values()]
148
148
  .filter((channel) => channel?.type === ChannelType.GuildText ||
149
149
  channel?.type === ChannelType.GuildAnnouncement ||
150
- channel?.type === ChannelType.GuildCategory)
150
+ channel?.type === ChannelType.GuildCategory ||
151
+ channel?.type === ChannelType.GuildForum ||
152
+ channel?.type === ChannelType.GuildMedia)
151
153
  .map((channel) => ({
152
154
  id: channel.id,
153
155
  name: channel.name,
@@ -155,7 +157,11 @@ export class DiscordGuildResourceDirectory {
155
157
  ? "guild_announcement"
156
158
  : channel.type === ChannelType.GuildCategory
157
159
  ? "guild_category"
158
- : "guild_text",
160
+ : channel.type === ChannelType.GuildForum
161
+ ? "guild_forum"
162
+ : channel.type === ChannelType.GuildMedia
163
+ ? "guild_media"
164
+ : "guild_text",
159
165
  }))
160
166
  .sort((left, right) => left.name.localeCompare(right.name)),
161
167
  roles: [...roles.values()]
@@ -1,41 +1,11 @@
1
1
  import { emptyPolicy, toDashboardPolicy, } from "./control-permissions.js";
2
+ import { collectCommandPermissionTargets } from "./command-path-contract.js";
2
3
  export function dashboardCommands(module) {
3
- const commands = (module.interactions?.commands ?? []).flatMap((command) => {
4
- const nested = [
5
- ...(command.subcommands ?? []).map((subcommand) => dashboardCommand(module, {
6
- id: subcommand.permissionId ??
7
- command.permissionId ??
8
- `${command.name}.${subcommand.name}`,
9
- name: `/${command.name} ${subcommand.name}`,
10
- description: subcommand.description,
11
- })),
12
- ...(command.subcommandGroups ?? []).flatMap((group) => group.subcommands.map((subcommand) => dashboardCommand(module, {
13
- id: subcommand.permissionId ??
14
- command.permissionId ??
15
- `${command.name}.${group.name}.${subcommand.name}`,
16
- name: `/${command.name} ${group.name} ${subcommand.name}`,
17
- description: subcommand.description,
18
- }))),
19
- ];
20
- if (nested.length > 0)
21
- return nested;
22
- return [
23
- dashboardCommand(module, {
24
- id: command.permissionId ?? command.name,
25
- name: `/${command.name}`,
26
- description: command.description,
27
- }),
28
- ];
29
- });
30
- commands.push(...(module.sharedCommands ?? []).map((command) => dashboardCommand(module, {
31
- id: command.permissionId ?? `${command.family}.${command.path}`,
32
- name: `/${command.family} ${command.path.replace(".", " ")}`,
4
+ const commands = collectCommandPermissionTargets(module).map((command) => dashboardCommand(module, {
5
+ id: command.id,
6
+ name: command.name,
33
7
  description: command.description,
34
- })), ...(module.nexusCommands ?? []).map((command) => dashboardCommand(module, {
35
- id: `nexus.${command.path}`,
36
- name: `/nexus ${command.path.replace(".", " ")}`,
37
- description: command.description,
38
- })));
8
+ }));
39
9
  return [
40
10
  ...new Map(commands.map((command) => [command.id, command])).values(),
41
11
  ];
@@ -0,0 +1,30 @@
1
+ import type { DiscordAnalyticsInspectionService } from "@helyx/sdk";
2
+ import { type Client } from "discord.js";
3
+ export declare class DiscordAnalyticsInspection implements DiscordAnalyticsInspectionService {
4
+ private readonly client;
5
+ constructor(client: Client);
6
+ listChannels(input: {
7
+ guildId: string;
8
+ roleId?: string;
9
+ }): ReturnType<DiscordAnalyticsInspectionService["listChannels"]>;
10
+ listMessages(input: {
11
+ guildId: string;
12
+ channelId: string;
13
+ before?: string;
14
+ limit: number;
15
+ }): ReturnType<DiscordAnalyticsInspectionService["listMessages"]>;
16
+ listMembers(input: {
17
+ guildId: string;
18
+ after?: string;
19
+ limit: number;
20
+ }): ReturnType<DiscordAnalyticsInspectionService["listMembers"]>;
21
+ listThreads(input: {
22
+ guildId: string;
23
+ parentChannelId?: string;
24
+ cursor?: string;
25
+ limit: number;
26
+ includeArchived: boolean;
27
+ includePrivate: boolean;
28
+ }): ReturnType<DiscordAnalyticsInspectionService["listThreads"]>;
29
+ }
30
+ //# sourceMappingURL=discord-analytics.d.ts.map