@helyx/bot 0.4.31 → 0.4.32

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.
@@ -0,0 +1,370 @@
1
+ import { HELYX_SERVICE_NAMES } from "@helyx/sdk";
2
+ import { AuditLogEvent, GatewayIntentBits, } from "discord.js";
3
+ import { isDeepStrictEqual } from "node:util";
4
+ const unavailable = (code) => ({ available: false, code });
5
+ const INTERNAL_HELYX_MODULE_IDS = new Set([
6
+ "helyx.cloud-connect",
7
+ "helyx.management",
8
+ "helyx.permissions",
9
+ "helyx.support",
10
+ ]);
11
+ export function moduleCatalogueMatches(packagedManifest, executableManifest) {
12
+ return (packagedManifest !== null &&
13
+ isDeepStrictEqual(packagedManifest, executableManifest));
14
+ }
15
+ export function missingRequiredPermissions(grantedPermissions, requiredPermissions) {
16
+ const granted = new Set(grantedPermissions.map(normalizePermission));
17
+ if (granted.has(normalizePermission("Administrator")))
18
+ return [];
19
+ return requiredPermissions.filter((permission) => !granted.has(normalizePermission(permission)));
20
+ }
21
+ export function hasRequiredModuleConfiguration(exposesConfiguration, savedConfiguration) {
22
+ return !exposesConfiguration || savedConfiguration !== null;
23
+ }
24
+ export function isHealthInspectableModule(moduleId) {
25
+ return !INTERNAL_HELYX_MODULE_IDS.has(moduleId);
26
+ }
27
+ export class DiscordGuildHealthInspection {
28
+ client;
29
+ modules;
30
+ services;
31
+ constructor(client, modules = [], services) {
32
+ this.client = client;
33
+ this.modules = modules;
34
+ this.services = services;
35
+ }
36
+ inspectOverview(input) {
37
+ const guild = this.#guild(input.guildId);
38
+ const bot = guild.members.me;
39
+ if (!bot)
40
+ return Promise.resolve(unavailable("discord_unavailable"));
41
+ return Promise.resolve({
42
+ available: true,
43
+ guildId: guild.id,
44
+ name: guild.name,
45
+ ownerId: guild.ownerId,
46
+ createdAt: guild.createdAt,
47
+ memberCount: guild.memberCount,
48
+ verificationLevel: ["None", "Low", "Medium", "High", "VeryHigh"][Number(guild.verificationLevel)] ?? "Unknown",
49
+ explicitContentFilter: ["Disabled", "MembersWithoutRoles", "AllMembers"][Number(guild.explicitContentFilter)] ?? "Unknown",
50
+ defaultMessageNotifications: ["AllMessages", "OnlyMentions"][Number(guild.defaultMessageNotifications)] ?? "Unknown",
51
+ mfaLevel: Number(guild.mfaLevel) === 1 ? "Elevated" : "None",
52
+ premiumTier: Number(guild.premiumTier),
53
+ features: [...guild.features],
54
+ botPermissions: bot.permissions.toArray(),
55
+ channelCount: guild.channels.cache.size,
56
+ roleCount: guild.roles.cache.size,
57
+ emojiCount: guild.emojis.cache.size,
58
+ stickerCount: guild.stickers.cache.size,
59
+ rulesChannelId: guild.rulesChannelId,
60
+ publicUpdatesChannelId: guild.publicUpdatesChannelId,
61
+ safetyAlertsChannelId: guild.safetyAlertsChannelId,
62
+ invitesDisabledUntil: guild.incidentsData?.invitesDisabledUntil ?? null,
63
+ dmsDisabledUntil: guild.incidentsData?.dmsDisabledUntil ?? null,
64
+ botHighestRolePosition: bot.roles.highest.position,
65
+ });
66
+ }
67
+ listChannels(input) {
68
+ const guild = this.#guild(input.guildId);
69
+ const bot = guild.members.me;
70
+ if (!bot)
71
+ return Promise.resolve(unavailable("discord_unavailable"));
72
+ return Promise.resolve({
73
+ available: true,
74
+ items: guild.channels.cache.map((channel) => ({
75
+ channelId: channel.id,
76
+ name: channel.name,
77
+ type: String(channel.type),
78
+ parentId: "parentId" in channel ? (channel.parentId ?? null) : null,
79
+ ageRestricted: "nsfw" in channel && channel.nsfw === true,
80
+ everyonePermissions: channel.permissionsFor(guild.roles.everyone)?.toArray() ?? [],
81
+ botPermissions: channel.permissionsFor(bot)?.toArray() ?? [],
82
+ permissionsLocked: "permissionsLocked" in channel
83
+ ? (channel.permissionsLocked ?? null)
84
+ : null,
85
+ })),
86
+ });
87
+ }
88
+ listRoles(input) {
89
+ const guild = this.#guild(input.guildId);
90
+ return Promise.resolve({
91
+ available: true,
92
+ items: guild.roles.cache.map((role) => ({
93
+ roleId: role.id,
94
+ name: role.name,
95
+ position: role.position,
96
+ managed: role.managed,
97
+ memberCount: role.members.size,
98
+ permissions: role.permissions.toArray(),
99
+ })),
100
+ });
101
+ }
102
+ async listMembers(input) {
103
+ const guild = this.#guild(input.guildId);
104
+ try {
105
+ const limit = Math.min(1_000, Math.max(1, input.limit));
106
+ const members = await guild.members.list({
107
+ limit,
108
+ ...(input.after ? { after: input.after } : {}),
109
+ });
110
+ const items = members.map((member) => ({
111
+ userId: member.id,
112
+ bot: member.user.bot,
113
+ system: member.user.system === true,
114
+ createdAt: member.user.createdAt,
115
+ joinedAt: member.joinedAt,
116
+ timedOutUntil: member.communicationDisabledUntil,
117
+ publicFlags: member.user.flags?.toArray() ?? [],
118
+ roleIds: [...member.roles.cache.keys()],
119
+ }));
120
+ return {
121
+ available: true,
122
+ items,
123
+ ...(members.size === limit && members.last()
124
+ ? { nextAfter: members.last().id }
125
+ : {}),
126
+ };
127
+ }
128
+ catch (error) {
129
+ return this.#unavailable(error, "missing_intent");
130
+ }
131
+ }
132
+ async listInvites(input) {
133
+ try {
134
+ const guild = this.#guild(input.guildId);
135
+ const invites = await guild.invites.fetch();
136
+ return {
137
+ available: true,
138
+ items: invites.map((invite) => ({
139
+ code: invite.code,
140
+ channelId: invite.channelId,
141
+ channelName: invite.channelId
142
+ ? (guild.channels.cache.get(invite.channelId)?.name ?? null)
143
+ : null,
144
+ channelExists: invite.channelId !== null &&
145
+ guild.channels.cache.has(invite.channelId),
146
+ inviterUserId: invite.inviterId,
147
+ createdAt: invite.createdAt,
148
+ expiresAt: invite.expiresAt,
149
+ maxUses: invite.maxUses ?? 0,
150
+ uses: invite.uses ?? 0,
151
+ temporary: invite.temporary ?? false,
152
+ })),
153
+ };
154
+ }
155
+ catch (error) {
156
+ return this.#unavailable(error, "missing_permission");
157
+ }
158
+ }
159
+ async listIntegrations(input) {
160
+ try {
161
+ const guild = this.#guild(input.guildId);
162
+ const integrations = await guild.fetchIntegrations();
163
+ return {
164
+ available: true,
165
+ items: integrations.map((integration) => ({
166
+ integrationId: integration.id,
167
+ name: integration.name,
168
+ type: integration.type,
169
+ enabled: integration.enabled ?? false,
170
+ syncing: "syncing" in integration ? (integration.syncing ?? null) : null,
171
+ roleId: "role" in integration ? (integration.role?.id ?? null) : null,
172
+ roleExists: !("role" in integration) ||
173
+ !integration.role?.id ||
174
+ guild.roles.cache.has(integration.role.id),
175
+ })),
176
+ };
177
+ }
178
+ catch (error) {
179
+ return this.#unavailable(error, "missing_permission");
180
+ }
181
+ }
182
+ async listWebhooks(input) {
183
+ try {
184
+ const webhooks = await this.#guild(input.guildId).fetchWebhooks();
185
+ return {
186
+ available: true,
187
+ items: webhooks.map((webhook) => ({
188
+ webhookId: webhook.id,
189
+ name: webhook.name,
190
+ channelId: webhook.channelId,
191
+ ownerUserId: webhook.owner?.id ?? null,
192
+ applicationId: webhook.applicationId,
193
+ channelName: webhook.channelId
194
+ ? (this.#guild(input.guildId).channels.cache.get(webhook.channelId)
195
+ ?.name ?? null)
196
+ : null,
197
+ })),
198
+ };
199
+ }
200
+ catch (error) {
201
+ return this.#unavailable(error, "missing_permission");
202
+ }
203
+ }
204
+ async listAutoModerationRules(input) {
205
+ try {
206
+ const rules = await this.#guild(input.guildId).autoModerationRules.fetch();
207
+ return {
208
+ available: true,
209
+ items: rules.map((rule) => ({
210
+ ruleId: rule.id,
211
+ name: rule.name,
212
+ enabled: rule.enabled,
213
+ triggerType: String(rule.triggerType),
214
+ eventType: String(rule.eventType),
215
+ actionCount: rule.actions.length,
216
+ })),
217
+ };
218
+ }
219
+ catch (error) {
220
+ return this.#unavailable(error, "missing_permission");
221
+ }
222
+ }
223
+ async inspectAuditSummary(input) {
224
+ try {
225
+ const audit = await this.#guild(input.guildId).fetchAuditLogs({
226
+ limit: 100,
227
+ });
228
+ const cutoff = Date.now() - 60 * 60 * 1_000;
229
+ const highImpact = new Set([
230
+ AuditLogEvent.MemberBanAdd,
231
+ AuditLogEvent.MemberKick,
232
+ AuditLogEvent.RoleCreate,
233
+ AuditLogEvent.RoleDelete,
234
+ AuditLogEvent.RoleUpdate,
235
+ AuditLogEvent.ChannelDelete,
236
+ AuditLogEvent.WebhookCreate,
237
+ AuditLogEvent.WebhookDelete,
238
+ AuditLogEvent.WebhookUpdate,
239
+ ]);
240
+ return {
241
+ available: true,
242
+ inspected: audit.entries.size,
243
+ highImpactActionsLastHour: audit.entries.filter((entry) => entry.createdTimestamp >= cutoff && highImpact.has(entry.action)).size,
244
+ };
245
+ }
246
+ catch (error) {
247
+ return this.#unavailable(error, "missing_permission");
248
+ }
249
+ }
250
+ async inspectHelyxModules(input) {
251
+ if (!this.services)
252
+ return unavailable("not_supported");
253
+ try {
254
+ const installations = this.services.get(HELYX_SERVICE_NAMES.installations);
255
+ const configurations = this.services.get(HELYX_SERVICE_NAMES.configuration);
256
+ const catalogue = this.services.has(HELYX_SERVICE_NAMES.moduleCatalogue)
257
+ ? this.services.get(HELYX_SERVICE_NAMES.moduleCatalogue)
258
+ : null;
259
+ const resources = this.services.has(HELYX_SERVICE_NAMES.discordResources)
260
+ ? this.services.get(HELYX_SERVICE_NAMES.discordResources)
261
+ : null;
262
+ const capabilities = resources
263
+ ? await resources.inspectRuntimeCapabilities()
264
+ : {
265
+ guildMembers: this.client.options.intents.has(GatewayIntentBits.GuildMembers),
266
+ messageContent: this.client.options.intents.has(GatewayIntentBits.MessageContent),
267
+ };
268
+ const overview = await this.inspectOverview(input);
269
+ if (!overview.available)
270
+ return overview;
271
+ const items = [];
272
+ const batchSize = Math.min(20, Math.max(1, input.batchSize ?? 20));
273
+ const inspectableModules = this.modules.filter((module) => isHealthInspectableModule(module.manifest.id));
274
+ for (let index = 0; index < inspectableModules.length; index += 1) {
275
+ const module = inspectableModules[index];
276
+ const state = await installations.getModuleState(input.guildId, module.manifest.id);
277
+ const configuration = await configurations.get(input.guildId, module.manifest.id);
278
+ let configurationValid = true;
279
+ let resourceValidationValid = null;
280
+ if (configuration && module.configuration)
281
+ try {
282
+ module.configuration.validate(configuration.value);
283
+ }
284
+ catch {
285
+ configurationValid = false;
286
+ }
287
+ if (state?.enabled &&
288
+ configuration &&
289
+ module.configuration?.validateUpdate)
290
+ try {
291
+ await module.configuration.validateUpdate({
292
+ guildId: input.guildId,
293
+ value: configuration.value,
294
+ services: this.services,
295
+ });
296
+ resourceValidationValid = true;
297
+ }
298
+ catch {
299
+ resourceValidationValid = false;
300
+ }
301
+ let activationValid = null;
302
+ if (state?.enabled && module.validateActivation)
303
+ try {
304
+ await module.validateActivation({
305
+ guildId: input.guildId,
306
+ services: this.services,
307
+ });
308
+ activationValid = true;
309
+ }
310
+ catch {
311
+ activationValid = false;
312
+ }
313
+ const health = module.health
314
+ ? await Promise.resolve(module.health()).catch(() => ({
315
+ status: "unhealthy",
316
+ message: "Module health check failed",
317
+ }))
318
+ : { status: "healthy", message: null };
319
+ const packaged = catalogue?.get(module.manifest.id) ?? null;
320
+ const requiredCapabilities = module.requiredDiscordCapabilities ?? [];
321
+ const missingCapabilities = requiredCapabilities.filter((capability) => (capability === "guild-members" && !capabilities.guildMembers) ||
322
+ (capability === "message-content" && !capabilities.messageContent));
323
+ const missingPermissions = missingRequiredPermissions(overview.botPermissions, module.manifest.permissions);
324
+ items.push({
325
+ moduleId: module.manifest.id,
326
+ name: module.manifest.name,
327
+ version: module.manifest.version,
328
+ enabled: state?.enabled ?? false,
329
+ configured: hasRequiredModuleConfiguration(module.configuration !== undefined, configuration),
330
+ configurationValid,
331
+ resourceValidationValid,
332
+ activationValid,
333
+ catalogueMatches: moduleCatalogueMatches(packaged, module.manifest),
334
+ requiredCapabilities,
335
+ missingCapabilities,
336
+ missingPermissions,
337
+ healthStatus: health.status,
338
+ healthMessage: health.message ?? null,
339
+ diagnosticsAvailable: module.health !== undefined,
340
+ });
341
+ if ((index + 1) % batchSize === 0)
342
+ await Promise.resolve();
343
+ }
344
+ return { available: true, items };
345
+ }
346
+ catch {
347
+ return unavailable("not_supported");
348
+ }
349
+ }
350
+ #guild(guildId) {
351
+ const guild = this.client.guilds.cache.get(guildId);
352
+ if (!guild)
353
+ throw new Error("Discord server is not owned by this worker");
354
+ return guild;
355
+ }
356
+ #unavailable(error, fallback) {
357
+ if (typeof error === "object" && error !== null && "code" in error) {
358
+ const code = Number(error.code);
359
+ if (code === 50_001 || code === 50_013)
360
+ return unavailable(fallback);
361
+ }
362
+ throw error instanceof Error
363
+ ? error
364
+ : new Error("Discord guild inspection failed");
365
+ }
366
+ }
367
+ function normalizePermission(value) {
368
+ return value.replaceAll(/[^a-z0-9]/giu, "").toLowerCase();
369
+ }
370
+ //# sourceMappingURL=discord-guild-health.js.map
package/dist/events.d.ts CHANGED
@@ -7,6 +7,7 @@ export interface ModuleEventHandle {
7
7
  queueDepth(): number;
8
8
  }
9
9
  export declare function reactionEmojiKey(reaction: MessageReaction | PartialMessageReaction): string;
10
+ export declare function reactionEmojiIdentifier(reaction: MessageReaction | PartialMessageReaction): string;
10
11
  export declare function reactionCacheKey(emojiIdentifier: string): string;
11
12
  interface ApprovedRoleMutation {
12
13
  guildId: string;
@@ -20,6 +21,7 @@ export declare class DiscordMessageDeliveryService implements DiscordMessageServ
20
21
  constructor(client: Client);
21
22
  send(input: Parameters<DiscordMessageService["send"]>[0]): ReturnType<DiscordMessageService["send"]>;
22
23
  sendDirect(input: Parameters<DiscordMessageService["sendDirect"]>[0]): ReturnType<DiscordMessageService["sendDirect"]>;
24
+ fetchSource(input: Parameters<DiscordMessageService["fetchSource"]>[0]): ReturnType<DiscordMessageService["fetchSource"]>;
23
25
  }
24
26
  export declare class DiscordMemberEnforcementService implements MemberEnforcementService {
25
27
  #private;
@@ -110,6 +112,19 @@ export declare class DiscordResourceInspection implements DiscordResourceInspect
110
112
  avatarUrl?: string;
111
113
  isBot: boolean;
112
114
  }[]>;
115
+ listMembers(input: {
116
+ guildId: string;
117
+ after?: string;
118
+ limit: number;
119
+ }): Promise<{
120
+ items: readonly {
121
+ userId: string;
122
+ displayName: string;
123
+ avatarUrl?: string;
124
+ isBot: boolean;
125
+ }[];
126
+ nextCursor?: string;
127
+ }>;
113
128
  inspectRole(input: {
114
129
  guildId: string;
115
130
  roleId: string;