@helyx/bot 0.4.46 → 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,33 @@
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
+
3
31
  ## 0.4.46
4
32
 
5
33
  ### 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
@@ -0,0 +1,337 @@
1
+ import { ChannelType, GatewayIntentBits, PermissionFlagsBits, } from "discord.js";
2
+ const MAX_MESSAGE_PAGE = 100;
3
+ const MAX_MEMBER_PAGE = 1_000;
4
+ const MAX_THREAD_PAGE = 100;
5
+ export class DiscordAnalyticsInspection {
6
+ client;
7
+ constructor(client) {
8
+ this.client = client;
9
+ }
10
+ async listChannels(input) {
11
+ const guild = this.client.guilds.cache.get(input.guildId);
12
+ if (!guild)
13
+ return unavailable("discord_unavailable");
14
+ try {
15
+ const [channels, selectedRole] = await Promise.all([
16
+ guild.channels.fetch(),
17
+ input.roleId
18
+ ? guild.roles.fetch(input.roleId).catch(() => null)
19
+ : Promise.resolve(null),
20
+ ]);
21
+ if (input.roleId && !selectedRole)
22
+ return unavailable("missing_permission");
23
+ const items = [];
24
+ for (const channel of channels.values()) {
25
+ if (!channel)
26
+ continue;
27
+ const type = analyticsChannelType(channel);
28
+ if (!type)
29
+ continue;
30
+ const botPermissions = guild.members.me
31
+ ? channel.permissionsFor(guild.members.me)
32
+ : null;
33
+ const rolePermissions = selectedRole
34
+ ? channel.permissionsFor(selectedRole)
35
+ : null;
36
+ const threadLike = type === "forum" || type === "media";
37
+ items.push({
38
+ channelId: channel.id,
39
+ type,
40
+ parentId: channel.parentId,
41
+ botCanView: botPermissions?.has(PermissionFlagsBits.ViewChannel) ?? false,
42
+ botCanReadHistory: botPermissions?.has(PermissionFlagsBits.ReadMessageHistory) ??
43
+ false,
44
+ selectedRoleCanView: selectedRole
45
+ ? (rolePermissions?.has(PermissionFlagsBits.ViewChannel) ?? false)
46
+ : null,
47
+ selectedRoleCanWrite: selectedRole
48
+ ? (rolePermissions?.has(threadLike
49
+ ? PermissionFlagsBits.SendMessagesInThreads
50
+ : PermissionFlagsBits.SendMessages) ?? false)
51
+ : null,
52
+ });
53
+ }
54
+ return {
55
+ available: true,
56
+ items: items.sort((left, right) => left.channelId.localeCompare(right.channelId)),
57
+ };
58
+ }
59
+ catch {
60
+ return unavailable("discord_unavailable");
61
+ }
62
+ }
63
+ async listMessages(input) {
64
+ const limit = boundedLimit(input.limit, MAX_MESSAGE_PAGE);
65
+ if (!limit)
66
+ return unavailable("not_supported");
67
+ const guild = this.client.guilds.cache.get(input.guildId);
68
+ if (!guild)
69
+ return unavailable("discord_unavailable");
70
+ try {
71
+ const channel = await guild.channels.fetch(input.channelId);
72
+ if (!channel?.isTextBased() || !("messages" in channel))
73
+ return unavailable("not_supported");
74
+ const permissions = guild.members.me
75
+ ? channel.permissionsFor(guild.members.me)
76
+ : null;
77
+ if (!permissions?.has(PermissionFlagsBits.ViewChannel))
78
+ return unavailable("missing_permission");
79
+ if (!permissions.has(PermissionFlagsBits.ReadMessageHistory))
80
+ return unavailable("missing_permission");
81
+ const messages = await channel.messages.fetch({
82
+ limit,
83
+ ...(input.before ? { before: input.before } : {}),
84
+ });
85
+ const items = [...messages.values()].map((message) => ({
86
+ messageId: message.id,
87
+ createdAt: message.createdAt,
88
+ authorUserId: message.author?.id ?? null,
89
+ authorIsBot: message.author?.bot ?? false,
90
+ webhook: message.webhookId !== null,
91
+ system: message.system,
92
+ currentRoleIds: message.member
93
+ ? [...message.member.roles.cache.keys()].sort()
94
+ : null,
95
+ }));
96
+ return {
97
+ available: true,
98
+ items,
99
+ ...(items.length === limit
100
+ ? { nextBefore: items.at(-1).messageId }
101
+ : {}),
102
+ };
103
+ }
104
+ catch {
105
+ return unavailable("discord_unavailable");
106
+ }
107
+ }
108
+ async listMembers(input) {
109
+ const limit = boundedLimit(input.limit, MAX_MEMBER_PAGE);
110
+ if (!limit)
111
+ return unavailable("not_supported");
112
+ if (!this.client.options.intents.has(GatewayIntentBits.GuildMembers))
113
+ return unavailable("missing_intent");
114
+ const guild = this.client.guilds.cache.get(input.guildId);
115
+ if (!guild)
116
+ return unavailable("discord_unavailable");
117
+ try {
118
+ const members = await guild.members.list({
119
+ limit,
120
+ ...(input.after ? { after: input.after } : {}),
121
+ });
122
+ const items = [...members.values()].map((member) => ({
123
+ userId: member.id,
124
+ joinedAt: member.joinedAt,
125
+ accountCreatedAt: member.user.createdAt,
126
+ isBot: member.user.bot,
127
+ roleIds: [...member.roles.cache.keys()].sort(),
128
+ }));
129
+ return {
130
+ available: true,
131
+ items,
132
+ ...(items.length === limit ? { nextAfter: items.at(-1).userId } : {}),
133
+ };
134
+ }
135
+ catch {
136
+ return unavailable("discord_unavailable");
137
+ }
138
+ }
139
+ async listThreads(input) {
140
+ const limit = boundedLimit(input.limit, MAX_THREAD_PAGE);
141
+ if (!limit)
142
+ return unavailable("not_supported");
143
+ const guild = this.client.guilds.cache.get(input.guildId);
144
+ if (!guild)
145
+ return unavailable("discord_unavailable");
146
+ const cursor = decodeThreadCursor(input.cursor);
147
+ if (input.cursor && !cursor)
148
+ return unavailable("not_supported");
149
+ try {
150
+ const parents = await threadParents(guild, input.parentChannelId);
151
+ const privateCoverage = privateThreadCoverage(guild);
152
+ const current = cursor ?? { phase: "active", offset: 0 };
153
+ if (current.phase === "active") {
154
+ const active = await guild.channels.fetchActiveThreads(false);
155
+ const facts = [...active.threads.values()]
156
+ .filter((thread) => !input.parentChannelId ||
157
+ thread.parentId === input.parentChannelId)
158
+ .sort((left, right) => left.id.localeCompare(right.id))
159
+ .map((thread) => threadFact(guild, thread));
160
+ const items = facts.slice(current.offset, current.offset + limit);
161
+ const nextOffset = current.offset + items.length;
162
+ const nextCursor = nextOffset < facts.length
163
+ ? encodeThreadCursor({ phase: "active", offset: nextOffset })
164
+ : input.includeArchived
165
+ ? encodeThreadCursor({ phase: "public", parentIndex: 0 })
166
+ : undefined;
167
+ return {
168
+ available: true,
169
+ items,
170
+ ...(nextCursor ? { nextCursor } : {}),
171
+ privateCoverage,
172
+ };
173
+ }
174
+ const phases = input.includePrivate
175
+ ? ["public", "private"]
176
+ : ["public"];
177
+ let phaseIndex = phases.indexOf(current.phase);
178
+ if (phaseIndex < 0)
179
+ return unavailable("not_supported");
180
+ let parentIndex = current.parentIndex;
181
+ let before = current.before;
182
+ while (phaseIndex < phases.length) {
183
+ const phase = phases[phaseIndex];
184
+ while (parentIndex < parents.length) {
185
+ const parent = parents[parentIndex];
186
+ const page = await parent.threads.fetchArchived({
187
+ type: phase,
188
+ fetchAll: phase === "private" && privateCoverage === "all",
189
+ limit,
190
+ ...(before ? { before } : {}),
191
+ }, false);
192
+ const threads = [...page.threads.values()].sort((left, right) => left.id.localeCompare(right.id));
193
+ const items = threads.map((thread) => threadFact(guild, thread));
194
+ if (page.hasMore && threads.length > 0) {
195
+ const oldestArchive = threads.reduce((oldest, thread) => thread.archivedAt &&
196
+ (!oldest || thread.archivedAt.getTime() < oldest.getTime())
197
+ ? thread.archivedAt
198
+ : oldest, null);
199
+ return {
200
+ available: true,
201
+ items,
202
+ nextCursor: encodeThreadCursor({
203
+ phase,
204
+ parentIndex,
205
+ before: oldestArchive?.toISOString() ?? threads.at(0).id,
206
+ }),
207
+ privateCoverage,
208
+ };
209
+ }
210
+ parentIndex += 1;
211
+ before = undefined;
212
+ if (items.length > 0) {
213
+ const next = nextArchiveCursor(phases, phaseIndex, parentIndex, parents.length);
214
+ return {
215
+ available: true,
216
+ items,
217
+ ...(next ? { nextCursor: encodeThreadCursor(next) } : {}),
218
+ privateCoverage,
219
+ };
220
+ }
221
+ }
222
+ phaseIndex += 1;
223
+ parentIndex = 0;
224
+ }
225
+ return { available: true, items: [], privateCoverage };
226
+ }
227
+ catch {
228
+ return unavailable("discord_unavailable");
229
+ }
230
+ }
231
+ }
232
+ function analyticsChannelType(channel) {
233
+ switch (channel.type) {
234
+ case ChannelType.GuildCategory:
235
+ return "category";
236
+ case ChannelType.GuildText:
237
+ return "text";
238
+ case ChannelType.GuildAnnouncement:
239
+ return "announcement";
240
+ case ChannelType.GuildForum:
241
+ return "forum";
242
+ case ChannelType.GuildMedia:
243
+ return "media";
244
+ default:
245
+ return null;
246
+ }
247
+ }
248
+ async function threadParents(guild, parentChannelId) {
249
+ const channels = await guild.channels.fetch();
250
+ return [...channels.values()]
251
+ .filter((channel) => channel !== null &&
252
+ "threads" in channel &&
253
+ (!parentChannelId || channel.id === parentChannelId))
254
+ .sort((left, right) => left.id.localeCompare(right.id));
255
+ }
256
+ function threadFact(guild, thread) {
257
+ const permissions = guild.members.me
258
+ ? thread.permissionsFor(guild.members.me)
259
+ : null;
260
+ return {
261
+ threadId: thread.id,
262
+ parentChannelId: thread.parentId ?? "",
263
+ type: thread.type === ChannelType.PrivateThread
264
+ ? "private"
265
+ : thread.type === ChannelType.AnnouncementThread
266
+ ? "announcement"
267
+ : "public",
268
+ ownerUserId: thread.ownerId,
269
+ createdAt: thread.createdAt ?? new Date(snowflakeTimestamp(thread.id)),
270
+ lastActivityAt: thread.lastMessageId
271
+ ? new Date(snowflakeTimestamp(thread.lastMessageId))
272
+ : null,
273
+ archived: thread.archived ?? false,
274
+ locked: thread.locked ?? false,
275
+ messageCount: thread.messageCount ?? 0,
276
+ totalMessageSent: thread.totalMessageSent ?? 0,
277
+ appliedTagIds: "appliedTags" in thread ? [...thread.appliedTags] : [],
278
+ botCanView: permissions?.has(PermissionFlagsBits.ViewChannel) ?? false,
279
+ botCanReadHistory: permissions?.has(PermissionFlagsBits.ReadMessageHistory) ?? false,
280
+ };
281
+ }
282
+ function privateThreadCoverage(guild) {
283
+ const member = guild.members.me;
284
+ if (!member)
285
+ return "none";
286
+ return member.permissions.has(PermissionFlagsBits.ManageThreads)
287
+ ? "all"
288
+ : "joined_only";
289
+ }
290
+ function nextArchiveCursor(phases, phaseIndex, parentIndex, parentCount) {
291
+ if (parentIndex < parentCount)
292
+ return { phase: phases[phaseIndex], parentIndex };
293
+ const nextPhase = phases[phaseIndex + 1];
294
+ return nextPhase ? { phase: nextPhase, parentIndex: 0 } : undefined;
295
+ }
296
+ function encodeThreadCursor(cursor) {
297
+ return Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url");
298
+ }
299
+ function decodeThreadCursor(value) {
300
+ if (!value)
301
+ return null;
302
+ try {
303
+ const candidate = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
304
+ if (candidate.phase === "active")
305
+ return Number.isSafeInteger(candidate.offset) && candidate.offset >= 0
306
+ ? { phase: "active", offset: candidate.offset }
307
+ : null;
308
+ if (candidate.phase === "public" || candidate.phase === "private")
309
+ return Number.isSafeInteger(candidate.parentIndex) &&
310
+ candidate.parentIndex >= 0 &&
311
+ (candidate.before === undefined ||
312
+ (typeof candidate.before === "string" &&
313
+ candidate.before.length <= 100))
314
+ ? {
315
+ phase: candidate.phase,
316
+ parentIndex: candidate.parentIndex,
317
+ ...(candidate.before ? { before: candidate.before } : {}),
318
+ }
319
+ : null;
320
+ return null;
321
+ }
322
+ catch {
323
+ return null;
324
+ }
325
+ }
326
+ function boundedLimit(value, maximum) {
327
+ return Number.isSafeInteger(value) && value >= 1 && value <= maximum
328
+ ? value
329
+ : null;
330
+ }
331
+ function snowflakeTimestamp(id) {
332
+ return Number((BigInt(id) >> 22n) + 1420070400000n);
333
+ }
334
+ function unavailable(code) {
335
+ return { available: false, code };
336
+ }
337
+ //# sourceMappingURL=discord-analytics.js.map