@helyx/bot 0.4.46 → 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 +79 -0
- package/README.md +4 -4
- package/dist/cloud-management-module.js +1 -5
- package/dist/command-path-contract.d.ts +26 -0
- package/dist/command-path-contract.js +157 -0
- package/dist/command-registration-policy.js +1 -0
- package/dist/control-permissions.js +1 -0
- package/dist/control-server.d.ts +2 -34
- package/dist/control-server.js +31 -43
- package/dist/dashboard-commands.d.ts +1 -0
- package/dist/dashboard-commands.js +6 -35
- package/dist/dashboard-module-projection.d.ts +39 -0
- package/dist/dashboard-module-projection.js +28 -0
- 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.d.ts +1 -1
- package/dist/events.js +78 -22
- package/dist/guilds.js +1 -4
- package/dist/index.js +49 -8
- package/dist/interactions.d.ts +1 -1
- package/dist/interactions.js +89 -68
- package/dist/management-module.d.ts +1 -1
- package/dist/module-logging.js +1 -4
- package/dist/module-status-control.js +1 -1
- package/dist/modules.d.ts +2 -0
- package/dist/modules.js +27 -4
- package/dist/permissions-module.js +13 -16
- package/dist/premium-access.d.ts +7 -0
- package/dist/premium-access.js +38 -0
- package/dist/premium-activation-module.d.ts +20 -0
- package/dist/premium-activation-module.js +542 -0
- package/dist/premium-catalogue-enforcement.d.ts +5 -0
- package/dist/premium-catalogue-enforcement.js +29 -0
- package/dist/premium-guild-authority.d.ts +12 -0
- package/dist/premium-guild-authority.js +25 -0
- package/dist/scheduled-task-runner.js +14 -1
- package/dist/shard-contracts.js +1 -1
- package/dist/shard-supervisor.js +30 -1
- package/dist/shard-worker.d.ts +2 -0
- package/dist/shard-worker.js +36 -4
- package/documentation/examples/.env.example +2 -2
- package/package.json +6 -6
|
@@ -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
|
|
@@ -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
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { DiscordInviteInspectionService, DiscordInviteSnapshot, DiscordInviteUnavailable } from "@helyx/sdk";
|
|
2
|
+
import { type Client } from "discord.js";
|
|
3
|
+
export declare class DiscordInviteInspection implements DiscordInviteInspectionService {
|
|
4
|
+
#private;
|
|
5
|
+
private readonly client;
|
|
6
|
+
constructor(client: Client, fingerprintKey: Uint8Array);
|
|
7
|
+
continuityGeneration(input: {
|
|
8
|
+
guildId: string;
|
|
9
|
+
}): string;
|
|
10
|
+
invalidateContinuity(input: {
|
|
11
|
+
guildId: string;
|
|
12
|
+
reason: "event_queue_gap" | "gateway_session_gap" | "ownership_gap";
|
|
13
|
+
}): void;
|
|
14
|
+
fingerprintCode(input: {
|
|
15
|
+
guildId: string;
|
|
16
|
+
code: string;
|
|
17
|
+
}): Promise<{
|
|
18
|
+
sourceKey: string;
|
|
19
|
+
}>;
|
|
20
|
+
captureSnapshot(input: {
|
|
21
|
+
guildId: string;
|
|
22
|
+
}): Promise<DiscordInviteSnapshot | DiscordInviteUnavailable>;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=discord-invites.d.ts.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { PermissionFlagsBits } from "discord.js";
|
|
2
|
+
import { createHmac, randomUUID } from "node:crypto";
|
|
3
|
+
const VANITY_FEATURE = "VANITY_URL";
|
|
4
|
+
export class DiscordInviteInspection {
|
|
5
|
+
client;
|
|
6
|
+
#processGeneration = randomUUID();
|
|
7
|
+
#guildGenerations = new Map();
|
|
8
|
+
#fingerprintKey;
|
|
9
|
+
constructor(client, fingerprintKey) {
|
|
10
|
+
this.client = client;
|
|
11
|
+
if (fingerprintKey.byteLength !== 32)
|
|
12
|
+
throw new Error("Discord invite fingerprint key must contain 32 bytes");
|
|
13
|
+
this.#fingerprintKey = Buffer.from(fingerprintKey);
|
|
14
|
+
}
|
|
15
|
+
continuityGeneration(input) {
|
|
16
|
+
const readyGeneration = this.client.readyTimestamp ?? 0;
|
|
17
|
+
const localGeneration = this.#guildGenerations.get(input.guildId) ?? 0;
|
|
18
|
+
return `${this.#processGeneration}:${readyGeneration}:${localGeneration}`;
|
|
19
|
+
}
|
|
20
|
+
invalidateContinuity(input) {
|
|
21
|
+
void input.reason;
|
|
22
|
+
this.#guildGenerations.set(input.guildId, (this.#guildGenerations.get(input.guildId) ?? 0) + 1);
|
|
23
|
+
}
|
|
24
|
+
fingerprintCode(input) {
|
|
25
|
+
const code = input.code.trim();
|
|
26
|
+
if (!/^\d{17,20}$/u.test(input.guildId))
|
|
27
|
+
return Promise.reject(new Error("Discord server ID is invalid"));
|
|
28
|
+
if (code.length < 1 || code.length > 128)
|
|
29
|
+
return Promise.reject(new Error("Discord invite code is invalid"));
|
|
30
|
+
return Promise.resolve({
|
|
31
|
+
sourceKey: this.#fingerprint(input.guildId, code),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async captureSnapshot(input) {
|
|
35
|
+
const guild = this.client.guilds.cache.get(input.guildId);
|
|
36
|
+
if (!guild)
|
|
37
|
+
return unavailable("discord_unavailable");
|
|
38
|
+
if (!hasManageGuild(guild))
|
|
39
|
+
return unavailable("missing_permission");
|
|
40
|
+
try {
|
|
41
|
+
const [invites, vanity] = await Promise.all([
|
|
42
|
+
guild.invites.fetch(),
|
|
43
|
+
guild.features.includes(VANITY_FEATURE)
|
|
44
|
+
? guild.fetchVanityData().catch(() => null)
|
|
45
|
+
: Promise.resolve(null),
|
|
46
|
+
]);
|
|
47
|
+
const observedAt = new Date();
|
|
48
|
+
return {
|
|
49
|
+
available: true,
|
|
50
|
+
observedAt,
|
|
51
|
+
continuityGeneration: this.continuityGeneration(input),
|
|
52
|
+
invites: [...invites.values()]
|
|
53
|
+
.map((invite) => ({
|
|
54
|
+
sourceKey: this.#fingerprint(guild.id, invite.code),
|
|
55
|
+
channelId: invite.channelId,
|
|
56
|
+
inviterUserId: invite.inviterId,
|
|
57
|
+
createdAt: invite.createdAt,
|
|
58
|
+
expiresAt: invite.expiresAt,
|
|
59
|
+
maxUses: invite.maxUses ?? 0,
|
|
60
|
+
uses: invite.uses ?? 0,
|
|
61
|
+
temporary: invite.temporary ?? false,
|
|
62
|
+
}))
|
|
63
|
+
.sort((left, right) => left.sourceKey.localeCompare(right.sourceKey)),
|
|
64
|
+
vanity: vanity?.code && typeof vanity.uses === "number"
|
|
65
|
+
? {
|
|
66
|
+
sourceKey: this.#fingerprint(guild.id, vanity.code),
|
|
67
|
+
uses: vanity.uses,
|
|
68
|
+
}
|
|
69
|
+
: null,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return unavailable("discord_unavailable");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
#fingerprint(guildId, code) {
|
|
77
|
+
return createHmac("sha256", this.#fingerprintKey)
|
|
78
|
+
.update("helyx.discord-invite-source.v1\0", "utf8")
|
|
79
|
+
.update(guildId, "utf8")
|
|
80
|
+
.update("\0", "utf8")
|
|
81
|
+
.update(code, "utf8")
|
|
82
|
+
.digest("base64url");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function hasManageGuild(guild) {
|
|
86
|
+
return (guild.members.me?.permissions.has(PermissionFlagsBits.ManageGuild) ?? false);
|
|
87
|
+
}
|
|
88
|
+
function unavailable(code) {
|
|
89
|
+
return { available: false, code };
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=discord-invites.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>;
|