@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 +28 -0
- package/dist/command-path-contract.d.ts +26 -0
- package/dist/command-path-contract.js +157 -0
- package/dist/control-permissions.js +1 -0
- package/dist/control-server.d.ts +1 -1
- package/dist/control-server.js +8 -2
- package/dist/dashboard-commands.js +5 -35
- package/dist/discord-analytics.d.ts +30 -0
- package/dist/discord-analytics.js +337 -0
- package/dist/discord-invites.d.ts +24 -0
- package/dist/discord-invites.js +91 -0
- package/dist/events.js +49 -2
- package/dist/index.js +11 -3
- package/dist/interactions.js +48 -34
- package/dist/management-module.d.ts +1 -1
- package/dist/modules.d.ts +2 -0
- package/dist/modules.js +27 -4
- package/dist/permissions-module.js +13 -16
- package/dist/shard-contracts.js +1 -1
- package/dist/shard-worker.js +11 -3
- package/package.json +4 -4
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) {
|
package/dist/control-server.d.ts
CHANGED
|
@@ -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;
|
package/dist/control-server.js
CHANGED
|
@@ -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
|
-
:
|
|
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
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
}))
|
|
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
|