@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +4 -4
  3. package/dist/cloud-management-module.js +1 -5
  4. package/dist/command-path-contract.d.ts +26 -0
  5. package/dist/command-path-contract.js +157 -0
  6. package/dist/command-registration-policy.js +1 -0
  7. package/dist/control-permissions.js +1 -0
  8. package/dist/control-server.d.ts +2 -34
  9. package/dist/control-server.js +31 -43
  10. package/dist/dashboard-commands.d.ts +1 -0
  11. package/dist/dashboard-commands.js +6 -35
  12. package/dist/dashboard-module-projection.d.ts +39 -0
  13. package/dist/dashboard-module-projection.js +28 -0
  14. package/dist/discord-analytics.d.ts +30 -0
  15. package/dist/discord-analytics.js +337 -0
  16. package/dist/discord-invites.d.ts +24 -0
  17. package/dist/discord-invites.js +91 -0
  18. package/dist/events.d.ts +1 -1
  19. package/dist/events.js +78 -22
  20. package/dist/guilds.js +1 -4
  21. package/dist/index.js +49 -8
  22. package/dist/interactions.d.ts +1 -1
  23. package/dist/interactions.js +89 -68
  24. package/dist/management-module.d.ts +1 -1
  25. package/dist/module-logging.js +1 -4
  26. package/dist/module-status-control.js +1 -1
  27. package/dist/modules.d.ts +2 -0
  28. package/dist/modules.js +27 -4
  29. package/dist/permissions-module.js +13 -16
  30. package/dist/premium-access.d.ts +7 -0
  31. package/dist/premium-access.js +38 -0
  32. package/dist/premium-activation-module.d.ts +20 -0
  33. package/dist/premium-activation-module.js +542 -0
  34. package/dist/premium-catalogue-enforcement.d.ts +5 -0
  35. package/dist/premium-catalogue-enforcement.js +29 -0
  36. package/dist/premium-guild-authority.d.ts +12 -0
  37. package/dist/premium-guild-authority.js +25 -0
  38. package/dist/scheduled-task-runner.js +14 -1
  39. package/dist/shard-contracts.js +1 -1
  40. package/dist/shard-supervisor.js +30 -1
  41. package/dist/shard-worker.d.ts +2 -0
  42. package/dist/shard-worker.js +36 -4
  43. package/documentation/examples/.env.example +2 -2
  44. 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>;