@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.
- package/CHANGELOG.md +12 -0
- package/dist/control-permissions.d.ts +38 -0
- package/dist/control-permissions.js +139 -0
- package/dist/control-server.d.ts +1 -15
- package/dist/control-server.js +112 -166
- package/dist/discord-guild-health.d.ts +174 -0
- package/dist/discord-guild-health.js +370 -0
- package/dist/events.d.ts +15 -0
- package/dist/events.js +338 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +26 -7
- package/dist/interactions.js +36 -6
- package/dist/modules.d.ts +2 -0
- package/dist/modules.js +10 -1
- package/dist/oauth-module.d.ts +2 -0
- package/dist/oauth-module.js +192 -0
- package/dist/oauth-module.test.d.ts +2 -0
- package/dist/oauth-module.test.js +172 -0
- package/dist/oauth-service.d.ts +16 -0
- package/dist/oauth-service.js +40 -0
- package/dist/shard-supervisor.js +6 -0
- package/dist/shard-worker.d.ts +5 -0
- package/dist/shard-worker.js +16 -1
- package/package.json +1 -1
|
@@ -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;
|