@helyx/bot 1.1.0 → 1.2.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 +71 -0
- package/README.md +16 -1
- package/dist/auto-moderation-scanner-registry.d.ts +27 -0
- package/dist/auto-moderation-scanner-registry.js +172 -0
- package/dist/cloud-connector.js +3 -0
- package/dist/cloud-control-dispatcher.js +32 -0
- package/dist/command-path-contract.js +1 -0
- package/dist/control-permissions.d.ts +2 -2
- package/dist/control-permissions.js +4 -2
- package/dist/control-server.d.ts +72 -16
- package/dist/control-server.js +150 -250
- package/dist/dashboard-action-control.d.ts +25 -0
- package/dist/dashboard-action-control.js +73 -0
- package/dist/dashboard-action-routes.d.ts +9 -0
- package/dist/dashboard-action-routes.js +36 -0
- package/dist/dashboard-feature-control.d.ts +37 -0
- package/dist/dashboard-feature-control.js +206 -0
- package/dist/dashboard-module-projection.d.ts +9 -0
- package/dist/dashboard-module-projection.js +15 -0
- package/dist/dashboard-preview-import-routes.d.ts +6 -0
- package/dist/dashboard-preview-import-routes.js +121 -0
- package/dist/discord-analytics.d.ts +3 -0
- package/dist/discord-analytics.js +49 -5
- package/dist/discord-auto-moderation-service.d.ts +21 -0
- package/dist/discord-auto-moderation-service.js +425 -0
- package/dist/discord-guild-health.d.ts +2 -2
- package/dist/discord-member-enforcement-service.d.ts +26 -0
- package/dist/discord-member-enforcement-service.js +590 -0
- package/dist/discord-message-enforcement-service.d.ts +9 -0
- package/dist/discord-message-enforcement-service.js +102 -0
- package/dist/discord-violation-thread-service.d.ts +33 -0
- package/dist/discord-violation-thread-service.js +789 -0
- package/dist/ecko-evidence-receiver.d.ts +32 -0
- package/dist/ecko-evidence-receiver.js +173 -0
- package/dist/ecko-transport-auth.d.ts +19 -0
- package/dist/ecko-transport-auth.js +80 -0
- package/dist/ecko-transport-routes.d.ts +19 -0
- package/dist/ecko-transport-routes.js +130 -0
- package/dist/embedded-auto-moderation-pool.d.ts +46 -0
- package/dist/embedded-auto-moderation-pool.js +324 -0
- package/dist/embedded-auto-moderation-scanner.d.ts +34 -0
- package/dist/embedded-auto-moderation-scanner.js +263 -0
- package/dist/embedded-auto-moderation-worker-runtime.d.ts +37 -0
- package/dist/embedded-auto-moderation-worker-runtime.js +37 -0
- package/dist/embedded-auto-moderation-worker.d.ts +2 -0
- package/dist/embedded-auto-moderation-worker.js +13 -0
- package/dist/events.d.ts +5 -1
- package/dist/events.js +243 -4
- package/dist/guilds.d.ts +2 -2
- package/dist/guilds.js +14 -8
- package/dist/hosted-server-moderation.d.ts +16 -5
- package/dist/hosted-server-moderation.js +32 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +84 -14
- package/dist/moderation-action-registry.d.ts +11 -0
- package/dist/moderation-action-registry.js +375 -0
- package/dist/moderation-case-disassociation.d.ts +13 -0
- package/dist/moderation-case-disassociation.js +83 -0
- package/dist/moderation-platform-services.d.ts +27 -0
- package/dist/moderation-platform-services.js +50 -0
- package/dist/modules.d.ts +6 -0
- package/dist/modules.js +65 -3
- package/dist/privileged-message-metadata-cache.d.ts +35 -0
- package/dist/privileged-message-metadata-cache.js +70 -0
- package/dist/scheduled-task-runner.js +24 -1
- package/dist/shard-contracts.d.ts +151 -1
- package/dist/shard-contracts.js +142 -1
- package/dist/shard-coordinator.d.ts +1 -1
- package/dist/shard-ecko-scanner-registry.d.ts +38 -0
- package/dist/shard-ecko-scanner-registry.js +96 -0
- package/dist/shard-router.d.ts +23 -0
- package/dist/shard-router.js +69 -5
- package/dist/shard-supervisor.d.ts +11 -0
- package/dist/shard-supervisor.js +67 -4
- package/dist/shard-worker.d.ts +15 -1
- package/dist/shard-worker.js +95 -17
- package/dist/transient-violation-evidence.d.ts +39 -0
- package/dist/transient-violation-evidence.js +132 -0
- package/documentation/README.md +2 -0
- package/documentation/examples/.env.example +12 -0
- package/documentation/guides/self-hosting.md +17 -1
- package/documentation/modules/auto-moderation.md +51 -0
- package/documentation/modules/moderation.md +42 -0
- package/documentation/reference/environment.md +11 -0
- package/package.json +17 -5
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { parentPort } from "node:worker_threads";
|
|
2
|
+
import { EmbeddedWorkerRuntime, } from "./embedded-auto-moderation-worker-runtime.js";
|
|
3
|
+
const workerPort = parentPort;
|
|
4
|
+
if (!workerPort)
|
|
5
|
+
throw new Error("Embedded Auto Moderation worker requires a parent port");
|
|
6
|
+
const runtime = new EmbeddedWorkerRuntime();
|
|
7
|
+
workerPort.on("message", (request) => {
|
|
8
|
+
const response = runtime.handle(request);
|
|
9
|
+
if (response)
|
|
10
|
+
workerPort.postMessage(response);
|
|
11
|
+
});
|
|
12
|
+
workerPort.postMessage({ type: "ready" });
|
|
13
|
+
//# sourceMappingURL=embedded-auto-moderation-worker.js.map
|
package/dist/events.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
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
|
+
import type { TransientViolationEvidenceStore } from "./transient-violation-evidence.js";
|
|
4
5
|
export declare function availabilityAwareInstallations(services: ServiceAccess, modulesById?: ReadonlyMap<string, ModuleDefinition>): Pick<InstallationService, "isModuleEnabled">;
|
|
5
|
-
export declare function installModuleEvents(client: Client, modules: readonly ModuleDefinition[], logger: InteractionLogger, services: ServiceAccess
|
|
6
|
+
export declare function installModuleEvents(client: Client, modules: readonly ModuleDefinition[], logger: InteractionLogger, services: ServiceAccess, options?: {
|
|
7
|
+
runtimeProfile?: "hosted" | "self-hosted";
|
|
8
|
+
evidenceStore?: TransientViolationEvidenceStore;
|
|
9
|
+
}): ModuleEventHandle;
|
|
6
10
|
export interface ModuleEventHandle {
|
|
7
11
|
stop(): Promise<void>;
|
|
8
12
|
queueDepth(): number;
|
package/dist/events.js
CHANGED
|
@@ -5,6 +5,8 @@ import { toReplyOptions } from "./interactions.js";
|
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { classifyGuildMessage, unavailableGuildMessageClassification, } from "./message-classification.js";
|
|
7
7
|
import { canAccessModule } from "./premium-access.js";
|
|
8
|
+
import { fromDiscordAction, toRuleSnapshot, } from "./discord-auto-moderation-service.js";
|
|
9
|
+
import { PrivilegedMessageMetadataCache } from "./privileged-message-metadata-cache.js";
|
|
8
10
|
export function availabilityAwareInstallations(services, modulesById) {
|
|
9
11
|
const installations = services.get(HELYX_SERVICE_NAMES.installations);
|
|
10
12
|
const candidate = typeof services.has === "function" &&
|
|
@@ -21,12 +23,65 @@ export function availabilityAwareInstallations(services, modulesById) {
|
|
|
21
23
|
},
|
|
22
24
|
};
|
|
23
25
|
}
|
|
24
|
-
|
|
26
|
+
async function featureContributionAvailable(services, guildId, moduleId, featureId) {
|
|
27
|
+
if (!featureId)
|
|
28
|
+
return true;
|
|
29
|
+
if (!services.has(HELYX_SERVICE_NAMES.featureAccess))
|
|
30
|
+
return false;
|
|
31
|
+
try {
|
|
32
|
+
const result = await services
|
|
33
|
+
.get(HELYX_SERVICE_NAMES.featureAccess)
|
|
34
|
+
.check({ guildId, moduleId, featureId });
|
|
35
|
+
return result.decision === "granted";
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function privilegedSource(message) {
|
|
42
|
+
const author = message.author;
|
|
43
|
+
if (!author?.id)
|
|
44
|
+
return { resolved: false };
|
|
45
|
+
const channel = "channel" in message ? message.channel : undefined;
|
|
46
|
+
const channelIsThread = Boolean(channel && typeof channel.isThread === "function" && channel.isThread());
|
|
47
|
+
return {
|
|
48
|
+
resolved: true,
|
|
49
|
+
userId: author.id,
|
|
50
|
+
userIsBot: author.bot,
|
|
51
|
+
userIsSystem: Boolean(author.system || message.system),
|
|
52
|
+
webhookId: message.webhookId,
|
|
53
|
+
member: message.member
|
|
54
|
+
? {
|
|
55
|
+
present: true,
|
|
56
|
+
roleIds: [...message.member.roles.cache.keys()].sort(),
|
|
57
|
+
}
|
|
58
|
+
: { present: false },
|
|
59
|
+
channelId: message.channelId,
|
|
60
|
+
parentChannelId: channelIsThread && channel && "parentId" in channel
|
|
61
|
+
? channel.parentId
|
|
62
|
+
: null,
|
|
63
|
+
channelIsThread,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export function installModuleEvents(client, modules, logger, services, options) {
|
|
67
|
+
const runtimeProfile = options?.runtimeProfile ?? "hosted";
|
|
25
68
|
const modulesById = new Map(modules.map((module) => [module.manifest.id, module]));
|
|
26
|
-
const privilegedMessageContributions = modules.flatMap((module) => (module.events?.privilegedMessageCreate ?? [])
|
|
69
|
+
const privilegedMessageContributions = modules.flatMap((module) => (module.events?.privilegedMessageCreate ?? [])
|
|
70
|
+
.filter((contribution) => !contribution.featureId || runtimeProfile === "self-hosted")
|
|
71
|
+
.map((contribution) => ({
|
|
27
72
|
moduleId: module.manifest.id,
|
|
28
73
|
contribution,
|
|
29
74
|
})));
|
|
75
|
+
const privilegedMessageUpdateContributions = modules.flatMap((module) => (module.events?.privilegedMessageUpdate ?? [])
|
|
76
|
+
.filter((contribution) => !contribution.featureId || runtimeProfile === "self-hosted")
|
|
77
|
+
.map((contribution) => ({
|
|
78
|
+
moduleId: module.manifest.id,
|
|
79
|
+
contribution,
|
|
80
|
+
})));
|
|
81
|
+
const autoModerationRuleCreateContributions = modules.flatMap((module) => (module.events?.discordAutoModerationRuleCreate ?? []).map((contribution) => ({ moduleId: module.manifest.id, contribution })));
|
|
82
|
+
const autoModerationRuleUpdateContributions = modules.flatMap((module) => (module.events?.discordAutoModerationRuleUpdate ?? []).map((contribution) => ({ moduleId: module.manifest.id, contribution })));
|
|
83
|
+
const autoModerationRuleDeleteContributions = modules.flatMap((module) => (module.events?.discordAutoModerationRuleDelete ?? []).map((contribution) => ({ moduleId: module.manifest.id, contribution })));
|
|
84
|
+
const autoModerationActionContributions = modules.flatMap((module) => (module.events?.discordAutoModerationActionExecution ?? []).map((contribution) => ({ moduleId: module.manifest.id, contribution })));
|
|
30
85
|
const guildMessageContributions = modules.flatMap((module) => (module.events?.guildMessageCreate ?? []).map((contribution) => ({
|
|
31
86
|
moduleId: module.manifest.id,
|
|
32
87
|
classificationRequested: module.requiredDiscordCapabilities?.includes("message-content") ??
|
|
@@ -101,6 +156,11 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
101
156
|
contribution,
|
|
102
157
|
})));
|
|
103
158
|
if (privilegedMessageContributions.length === 0 &&
|
|
159
|
+
privilegedMessageUpdateContributions.length === 0 &&
|
|
160
|
+
autoModerationRuleCreateContributions.length === 0 &&
|
|
161
|
+
autoModerationRuleUpdateContributions.length === 0 &&
|
|
162
|
+
autoModerationRuleDeleteContributions.length === 0 &&
|
|
163
|
+
autoModerationActionContributions.length === 0 &&
|
|
104
164
|
guildMessageContributions.length === 0 &&
|
|
105
165
|
guildVoiceSessionContributions.length === 0 &&
|
|
106
166
|
reactionContributions.length === 0 &&
|
|
@@ -120,6 +180,7 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
120
180
|
guildEmojiUpdateContributions.length === 0)
|
|
121
181
|
return { stop: () => Promise.resolve(), queueDepth: () => 0 };
|
|
122
182
|
const queue = new BoundedGuildEventQueue(logger);
|
|
183
|
+
const privilegedMetadata = new PrivilegedMessageMetadataCache();
|
|
123
184
|
const voiceSessions = new Map();
|
|
124
185
|
const privilegedEligibilityQueue = privilegedMessageContributions.length > 0
|
|
125
186
|
? new BoundedGuildEventQueue(logger)
|
|
@@ -149,6 +210,21 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
149
210
|
});
|
|
150
211
|
});
|
|
151
212
|
}
|
|
213
|
+
if (privilegedMessageUpdateContributions.length > 0) {
|
|
214
|
+
client.on(Events.MessageCreate, (message) => {
|
|
215
|
+
if (!message.guildId)
|
|
216
|
+
return;
|
|
217
|
+
const source = privilegedSource(message);
|
|
218
|
+
if (!source.resolved)
|
|
219
|
+
return;
|
|
220
|
+
privilegedMetadata.remember({
|
|
221
|
+
guildId: message.guildId,
|
|
222
|
+
channelId: message.channelId,
|
|
223
|
+
messageId: message.id,
|
|
224
|
+
source,
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
}
|
|
152
228
|
if (privilegedMessageContributions.length > 0) {
|
|
153
229
|
client.on(Events.MessageCreate, (message) => {
|
|
154
230
|
if (!message.guildId || !privilegedEligibilityQueue)
|
|
@@ -159,6 +235,7 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
159
235
|
for (const item of privilegedMessageContributions) {
|
|
160
236
|
try {
|
|
161
237
|
if ((await installations.isModuleEnabled(message.guildId, item.moduleId)) &&
|
|
238
|
+
(await featureContributionAvailable(services, message.guildId, item.moduleId, item.contribution.featureId)) &&
|
|
162
239
|
(await item.contribution.isEligible({
|
|
163
240
|
serverId: message.guildId,
|
|
164
241
|
channelId: message.channelId,
|
|
@@ -167,6 +244,7 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
167
244
|
userIsSystem: message.author.system || message.system,
|
|
168
245
|
webhookId: message.webhookId,
|
|
169
246
|
content: message.content,
|
|
247
|
+
source: privilegedSource(message),
|
|
170
248
|
services,
|
|
171
249
|
}))) {
|
|
172
250
|
eligible.push(item);
|
|
@@ -241,6 +319,32 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
241
319
|
queue.enqueue(message.guildId, () => handleMessageUpdate(message));
|
|
242
320
|
});
|
|
243
321
|
}
|
|
322
|
+
if (privilegedMessageUpdateContributions.length > 0) {
|
|
323
|
+
client.on(Events.MessageUpdate, (_before, message) => {
|
|
324
|
+
if (message.guildId)
|
|
325
|
+
queue.enqueue(message.guildId, () => handlePrivilegedMessageUpdate(message));
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
if (autoModerationRuleCreateContributions.length > 0) {
|
|
329
|
+
client.on(Events.AutoModerationRuleCreate, (rule) => {
|
|
330
|
+
queue.enqueue(rule.guild.id, () => handleAutoModerationRule(autoModerationRuleCreateContributions, rule, "create"));
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
if (autoModerationRuleUpdateContributions.length > 0) {
|
|
334
|
+
client.on(Events.AutoModerationRuleUpdate, (_before, rule) => {
|
|
335
|
+
queue.enqueue(rule.guild.id, () => handleAutoModerationRule(autoModerationRuleUpdateContributions, rule, "update"));
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
if (autoModerationRuleDeleteContributions.length > 0) {
|
|
339
|
+
client.on(Events.AutoModerationRuleDelete, (rule) => {
|
|
340
|
+
queue.enqueue(rule.guild.id, () => handleAutoModerationRule(autoModerationRuleDeleteContributions, rule, "delete"));
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
if (autoModerationActionContributions.length > 0) {
|
|
344
|
+
client.on(Events.AutoModerationActionExecution, (execution) => {
|
|
345
|
+
queue.enqueue(execution.guild.id, () => handleAutoModerationAction(execution));
|
|
346
|
+
});
|
|
347
|
+
}
|
|
244
348
|
if (messageDeleteContributions.length > 0) {
|
|
245
349
|
client.on(Events.MessageDelete, (message) => {
|
|
246
350
|
if (message.guildId)
|
|
@@ -335,7 +439,8 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
335
439
|
const installations = availabilityAwareInstallations(services, modulesById);
|
|
336
440
|
for (const { moduleId, contribution } of contributions) {
|
|
337
441
|
try {
|
|
338
|
-
if (!(await installations.isModuleEnabled(message.guildId, moduleId))
|
|
442
|
+
if (!(await installations.isModuleEnabled(message.guildId, moduleId)) ||
|
|
443
|
+
!(await featureContributionAvailable(services, message.guildId, moduleId, contribution.featureId)))
|
|
339
444
|
continue;
|
|
340
445
|
await contribution.execute({
|
|
341
446
|
serverId: message.guildId,
|
|
@@ -345,6 +450,7 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
345
450
|
userIsBot: message.author.bot,
|
|
346
451
|
webhookId: message.webhookId,
|
|
347
452
|
content: message.content,
|
|
453
|
+
source: privilegedSource(message),
|
|
348
454
|
services,
|
|
349
455
|
deleteSource: async () => {
|
|
350
456
|
await message.delete();
|
|
@@ -362,6 +468,138 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
362
468
|
}
|
|
363
469
|
}
|
|
364
470
|
}
|
|
471
|
+
async function handlePrivilegedMessageUpdate(message) {
|
|
472
|
+
if (!message.guildId)
|
|
473
|
+
return;
|
|
474
|
+
const payloadSource = privilegedSource(message);
|
|
475
|
+
const cachedSource = privilegedMetadata.resolve({
|
|
476
|
+
guildId: message.guildId,
|
|
477
|
+
channelId: message.channelId,
|
|
478
|
+
messageId: message.id,
|
|
479
|
+
});
|
|
480
|
+
const source = payloadSource.resolved
|
|
481
|
+
? payloadSource
|
|
482
|
+
: (cachedSource ?? { resolved: false });
|
|
483
|
+
if (source.resolved)
|
|
484
|
+
privilegedMetadata.remember({
|
|
485
|
+
guildId: message.guildId,
|
|
486
|
+
channelId: message.channelId,
|
|
487
|
+
messageId: message.id,
|
|
488
|
+
source,
|
|
489
|
+
});
|
|
490
|
+
const observedAt = new Date();
|
|
491
|
+
const content = typeof message.content === "string"
|
|
492
|
+
? { present: true, value: message.content }
|
|
493
|
+
: { present: false };
|
|
494
|
+
const installations = availabilityAwareInstallations(services, modulesById);
|
|
495
|
+
for (const { moduleId, contribution, } of privilegedMessageUpdateContributions) {
|
|
496
|
+
try {
|
|
497
|
+
if (!(await installations.isModuleEnabled(message.guildId, moduleId)) ||
|
|
498
|
+
!(await featureContributionAvailable(services, message.guildId, moduleId, contribution.featureId)))
|
|
499
|
+
continue;
|
|
500
|
+
await contribution.execute({
|
|
501
|
+
serverId: message.guildId,
|
|
502
|
+
channelId: message.channelId,
|
|
503
|
+
messageId: message.id,
|
|
504
|
+
observationVersion: message.editedTimestamp?.toString() ?? observedAt.toISOString(),
|
|
505
|
+
observedAt,
|
|
506
|
+
content,
|
|
507
|
+
source,
|
|
508
|
+
services,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
catch (error) {
|
|
512
|
+
logger.warn({
|
|
513
|
+
event: "module.privileged_message_update_failed",
|
|
514
|
+
moduleId,
|
|
515
|
+
error: safeError(error),
|
|
516
|
+
messageId: message.id,
|
|
517
|
+
guildId: message.guildId,
|
|
518
|
+
}, "Module privileged message-update event failed");
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
async function handleAutoModerationRule(contributions, rule, eventName) {
|
|
523
|
+
const installations = availabilityAwareInstallations(services, modulesById);
|
|
524
|
+
for (const { moduleId, contribution } of contributions) {
|
|
525
|
+
try {
|
|
526
|
+
if (!(await installations.isModuleEnabled(rule.guild.id, moduleId)))
|
|
527
|
+
continue;
|
|
528
|
+
await contribution.execute({
|
|
529
|
+
serverId: rule.guild.id,
|
|
530
|
+
rule: toRuleSnapshot(rule),
|
|
531
|
+
services,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
catch (error) {
|
|
535
|
+
logger.warn({
|
|
536
|
+
event: `module.auto_moderation_rule_${eventName}_failed`,
|
|
537
|
+
moduleId,
|
|
538
|
+
guildId: rule.guild.id,
|
|
539
|
+
ruleId: rule.id,
|
|
540
|
+
error: safeError(error),
|
|
541
|
+
}, "Module Auto Moderation rule event failed");
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
async function handleAutoModerationAction(execution) {
|
|
546
|
+
const installations = availabilityAwareInstallations(services, modulesById);
|
|
547
|
+
const action = fromDiscordAction(execution.action);
|
|
548
|
+
const executionId = createHash("sha256")
|
|
549
|
+
.update(JSON.stringify([
|
|
550
|
+
execution.guild.id,
|
|
551
|
+
execution.ruleId,
|
|
552
|
+
execution.userId,
|
|
553
|
+
execution.channelId,
|
|
554
|
+
execution.messageId,
|
|
555
|
+
]))
|
|
556
|
+
.digest("hex");
|
|
557
|
+
const evidenceReference = options?.evidenceStore && execution.channelId && execution.content
|
|
558
|
+
? options.evidenceStore.capture({
|
|
559
|
+
source: "discord_auto_moderation",
|
|
560
|
+
sourceEventId: executionId,
|
|
561
|
+
guildId: execution.guild.id,
|
|
562
|
+
channelId: execution.channelId,
|
|
563
|
+
messageId: execution.messageId,
|
|
564
|
+
authorUserId: execution.userId,
|
|
565
|
+
content: execution.content,
|
|
566
|
+
})
|
|
567
|
+
: null;
|
|
568
|
+
try {
|
|
569
|
+
for (const { moduleId, contribution, } of autoModerationActionContributions) {
|
|
570
|
+
try {
|
|
571
|
+
if (!(await installations.isModuleEnabled(execution.guild.id, moduleId)))
|
|
572
|
+
continue;
|
|
573
|
+
await contribution.execute({
|
|
574
|
+
serverId: execution.guild.id,
|
|
575
|
+
ruleId: execution.ruleId,
|
|
576
|
+
userId: execution.userId,
|
|
577
|
+
channelId: execution.channelId,
|
|
578
|
+
messageId: execution.messageId,
|
|
579
|
+
alertSystemMessageId: execution.alertSystemMessageId,
|
|
580
|
+
action,
|
|
581
|
+
executionId,
|
|
582
|
+
evidenceReference,
|
|
583
|
+
occurredAt: new Date(),
|
|
584
|
+
services,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
logger.warn({
|
|
589
|
+
event: "module.auto_moderation_action_execution_failed",
|
|
590
|
+
moduleId,
|
|
591
|
+
guildId: execution.guild.id,
|
|
592
|
+
ruleId: execution.ruleId,
|
|
593
|
+
error: safeError(error),
|
|
594
|
+
}, "Module Auto Moderation action event failed");
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
finally {
|
|
599
|
+
if (evidenceReference)
|
|
600
|
+
options?.evidenceStore?.discard(evidenceReference);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
365
603
|
async function handleGuildMessageCreate(message) {
|
|
366
604
|
if (!message.guildId || !message.channelId)
|
|
367
605
|
return;
|
|
@@ -862,6 +1100,7 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
862
1100
|
await guildMessageQueue?.stop();
|
|
863
1101
|
await queue.stop();
|
|
864
1102
|
voiceSessions.clear();
|
|
1103
|
+
privilegedMetadata.clear();
|
|
865
1104
|
},
|
|
866
1105
|
queueDepth: () => queue.queueDepth() +
|
|
867
1106
|
(privilegedEligibilityQueue?.queueDepth() ?? 0) +
|
|
@@ -2088,7 +2327,7 @@ function validateSnowflake(value, label) {
|
|
|
2088
2327
|
throw new Error(`Invalid Discord ${label} ID`);
|
|
2089
2328
|
}
|
|
2090
2329
|
function discordAuditReason(input) {
|
|
2091
|
-
return `Helyx ${input.moduleId}: ${input.reason.trim()}
|
|
2330
|
+
return `Helyx ${input.moduleId}${input.operationKey ? ` ${input.operationKey}` : ""}: ${input.reason.trim()}`.slice(0, 512);
|
|
2092
2331
|
}
|
|
2093
2332
|
function normalizePermissionName(permission) {
|
|
2094
2333
|
return permission.replaceAll(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase();
|
package/dist/guilds.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type DatabasePool, type GuildShardOwnership } from "@helyx/database";
|
|
|
2
2
|
import type { ModuleDefinition } from "@helyx/sdk";
|
|
3
3
|
import { type Client } from "discord.js";
|
|
4
4
|
import type { InteractionLogger } from "./interactions.js";
|
|
5
|
-
import type { HostedBotInviteGate, HostedGuildSafety } from "./hosted-server-moderation.js";
|
|
5
|
+
import type { HostedBotInviteGate, HostedGuildOwnerObserver, HostedGuildSafety } from "./hosted-server-moderation.js";
|
|
6
6
|
export interface GuildReconciliationHealth {
|
|
7
7
|
status: "pending" | "running" | "healthy" | "failed" | "stopped";
|
|
8
8
|
lastStartedAt?: string;
|
|
@@ -22,5 +22,5 @@ export interface GuildTrackingPreparation {
|
|
|
22
22
|
health(): GuildReconciliationHealth;
|
|
23
23
|
stop(): void;
|
|
24
24
|
}
|
|
25
|
-
export declare function installGuildTracking(client: Client, database: DatabasePool, modules: readonly ModuleDefinition[], logger: InteractionLogger, enabledByDefaultModuleIds?: ReadonlySet<string>, ownership?: GuildShardOwnership, reconciliationIntervalMs?: number, shardCount?: number, applicationId?: string, hostedSafety?: HostedGuildSafety, hostedInviteGate?: HostedBotInviteGate): GuildTrackingPreparation;
|
|
25
|
+
export declare function installGuildTracking(client: Client, database: DatabasePool, modules: readonly ModuleDefinition[], logger: InteractionLogger, enabledByDefaultModuleIds?: ReadonlySet<string>, ownership?: GuildShardOwnership, reconciliationIntervalMs?: number, shardCount?: number, applicationId?: string, hostedSafety?: HostedGuildSafety, hostedInviteGate?: HostedBotInviteGate, hostedOwnerObserver?: HostedGuildOwnerObserver | undefined): GuildTrackingPreparation;
|
|
26
26
|
//# sourceMappingURL=guilds.d.ts.map
|
package/dist/guilds.js
CHANGED
|
@@ -3,7 +3,7 @@ import { safeError } from "@helyx/core";
|
|
|
3
3
|
import { Events } from "discord.js";
|
|
4
4
|
const RECONCILIATION_PAGE_SIZE = 250;
|
|
5
5
|
const DEFAULT_RECONCILIATION_INTERVAL_MS = 15 * 60 * 1_000;
|
|
6
|
-
export function installGuildTracking(client, database, modules, logger, enabledByDefaultModuleIds = new Set(), ownership, reconciliationIntervalMs = DEFAULT_RECONCILIATION_INTERVAL_MS, shardCount, applicationId, hostedSafety, hostedInviteGate) {
|
|
6
|
+
export function installGuildTracking(client, database, modules, logger, enabledByDefaultModuleIds = new Set(), ownership, reconciliationIntervalMs = DEFAULT_RECONCILIATION_INTERVAL_MS, shardCount, applicationId, hostedSafety, hostedInviteGate, hostedOwnerObserver = hostedSafety) {
|
|
7
7
|
if (!Number.isSafeInteger(reconciliationIntervalMs) ||
|
|
8
8
|
reconciliationIntervalMs < 60_000)
|
|
9
9
|
throw new Error("Guild reconciliation interval must be at least one minute");
|
|
@@ -14,8 +14,8 @@ export function installGuildTracking(client, database, modules, logger, enabledB
|
|
|
14
14
|
let timer;
|
|
15
15
|
const admissionRetries = new Map();
|
|
16
16
|
const inviteGateRejections = new Map();
|
|
17
|
-
const ownerObservations =
|
|
18
|
-
? new OwnerObservationQueue(
|
|
17
|
+
const ownerObservations = hostedOwnerObserver
|
|
18
|
+
? new OwnerObservationQueue(hostedOwnerObserver, logger)
|
|
19
19
|
: undefined;
|
|
20
20
|
let health = {
|
|
21
21
|
status: "pending",
|
|
@@ -26,11 +26,14 @@ export function installGuildTracking(client, database, modules, logger, enabledB
|
|
|
26
26
|
unavailableGuilds: 0,
|
|
27
27
|
};
|
|
28
28
|
async function syncGuild(guild, firstObservation = false) {
|
|
29
|
+
const cachedOwner = guild.members?.cache.get(guild.ownerId)?.user ??
|
|
30
|
+
guild.client?.users.cache.get(guild.ownerId);
|
|
29
31
|
const persist = async (connection) => {
|
|
30
32
|
const installations = new InstallationRepository(connection);
|
|
31
33
|
const record = await installations.syncGuild({
|
|
32
34
|
guildId: guild.id,
|
|
33
35
|
ownerUserId: guild.ownerId,
|
|
36
|
+
...(cachedOwner ? { ownerUsername: cachedOwner.username } : {}),
|
|
34
37
|
name: guild.name,
|
|
35
38
|
...(ownership ? { ownership } : {}),
|
|
36
39
|
});
|
|
@@ -62,7 +65,10 @@ export function installGuildTracking(client, database, modules, logger, enabledB
|
|
|
62
65
|
await withTransaction(database, persist);
|
|
63
66
|
else
|
|
64
67
|
await persist(database);
|
|
65
|
-
|
|
68
|
+
if (cachedOwner && hostedOwnerObserver)
|
|
69
|
+
await hostedOwnerObserver.observeOwner(guild);
|
|
70
|
+
else
|
|
71
|
+
ownerObservations?.enqueue(guild);
|
|
66
72
|
}
|
|
67
73
|
async function leaveInviteRejectedGuild(guild) {
|
|
68
74
|
const reasonCode = inviteGateRejections.get(guild.id);
|
|
@@ -325,13 +331,13 @@ export function installGuildTracking(client, database, modules, logger, enabledB
|
|
|
325
331
|
return prepare;
|
|
326
332
|
}
|
|
327
333
|
class OwnerObservationQueue {
|
|
328
|
-
|
|
334
|
+
observer;
|
|
329
335
|
logger;
|
|
330
336
|
#pending = new Map();
|
|
331
337
|
#active = 0;
|
|
332
338
|
#stopped = false;
|
|
333
|
-
constructor(
|
|
334
|
-
this.
|
|
339
|
+
constructor(observer, logger) {
|
|
340
|
+
this.observer = observer;
|
|
335
341
|
this.logger = logger;
|
|
336
342
|
}
|
|
337
343
|
enqueue(guild) {
|
|
@@ -352,7 +358,7 @@ class OwnerObservationQueue {
|
|
|
352
358
|
const [guildId, guild] = next;
|
|
353
359
|
this.#pending.delete(guildId);
|
|
354
360
|
this.#active += 1;
|
|
355
|
-
void this.
|
|
361
|
+
void this.observer
|
|
356
362
|
.observeOwner(guild)
|
|
357
363
|
.catch(() => {
|
|
358
364
|
this.logger.warn({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type DatabasePool } from "@helyx/database";
|
|
1
|
+
import { type DatabasePool, type HostedServerLeaveDeliveryOutcome } from "@helyx/database";
|
|
2
2
|
import type { Client, Guild } from "discord.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import type { InteractionLogger } from "./interactions.js";
|
|
@@ -14,9 +14,9 @@ declare const resultSchema: z.ZodObject<{
|
|
|
14
14
|
requires_review: "requires_review";
|
|
15
15
|
failed: "failed";
|
|
16
16
|
queued: "queued";
|
|
17
|
-
already_blacklisted: "already_blacklisted";
|
|
18
17
|
not_currently_installed: "not_currently_installed";
|
|
19
18
|
accepted_for_retry: "accepted_for_retry";
|
|
19
|
+
already_blacklisted: "already_blacklisted";
|
|
20
20
|
}>;
|
|
21
21
|
paidSlotsReleased: z.ZodNumber;
|
|
22
22
|
}, z.core.$loose>;
|
|
@@ -45,7 +45,7 @@ export declare class HostedServerModerationClient implements DeveloperServerMode
|
|
|
45
45
|
idempotencyKey: string;
|
|
46
46
|
}): Promise<{
|
|
47
47
|
operationId: string;
|
|
48
|
-
outcome: "completed" | "requires_review" | "failed" | "queued" | "
|
|
48
|
+
outcome: "completed" | "requires_review" | "failed" | "queued" | "not_currently_installed" | "accepted_for_retry" | "already_blacklisted";
|
|
49
49
|
paidSlotsReleased: number;
|
|
50
50
|
}>;
|
|
51
51
|
}
|
|
@@ -58,11 +58,17 @@ export declare class HostedBotInviteGateClient implements HostedBotInviteGate {
|
|
|
58
58
|
constructor(apiUrl: string, secret: string);
|
|
59
59
|
isEnabled(): Promise<boolean>;
|
|
60
60
|
}
|
|
61
|
-
export interface
|
|
61
|
+
export interface HostedGuildOwnerObserver {
|
|
62
|
+
observeOwner(guild: Guild): Promise<void>;
|
|
63
|
+
}
|
|
64
|
+
export interface HostedGuildSafety extends HostedGuildOwnerObserver {
|
|
62
65
|
isBlacklisted(guildId: string): Promise<boolean>;
|
|
63
66
|
blacklistedGuildIds(guildIds: readonly string[]): Promise<Set<string>>;
|
|
64
67
|
recordAutomaticLeave(guildId: string): Promise<string>;
|
|
65
|
-
|
|
68
|
+
}
|
|
69
|
+
export interface HostedServerOwnerObservation {
|
|
70
|
+
username: string;
|
|
71
|
+
observedAt: string;
|
|
66
72
|
}
|
|
67
73
|
export declare class PostgresHostedGuildSafety implements HostedGuildSafety {
|
|
68
74
|
#private;
|
|
@@ -71,6 +77,7 @@ export declare class PostgresHostedGuildSafety implements HostedGuildSafety {
|
|
|
71
77
|
blacklistedGuildIds(guildIds: readonly string[]): Promise<Set<string>>;
|
|
72
78
|
recordAutomaticLeave(guildId: string): Promise<string>;
|
|
73
79
|
observeOwner(guild: Guild): Promise<void>;
|
|
80
|
+
refreshOwner(guild: Guild): Promise<HostedServerOwnerObservation>;
|
|
74
81
|
}
|
|
75
82
|
export declare class HostedServerLeaveWorker {
|
|
76
83
|
#private;
|
|
@@ -83,6 +90,10 @@ export declare class HostedServerLeaveWorker {
|
|
|
83
90
|
} | undefined);
|
|
84
91
|
start(): void;
|
|
85
92
|
runOnce(): Promise<void>;
|
|
93
|
+
deliverOperation(input: {
|
|
94
|
+
operationId: string;
|
|
95
|
+
guildId: string;
|
|
96
|
+
}): Promise<HostedServerLeaveDeliveryOutcome>;
|
|
86
97
|
stop(): Promise<void>;
|
|
87
98
|
}
|
|
88
99
|
export {};
|
|
@@ -84,13 +84,22 @@ export class PostgresHostedGuildSafety {
|
|
|
84
84
|
return this.#repository.enqueueAutomaticLeave(guildId);
|
|
85
85
|
}
|
|
86
86
|
async observeOwner(guild) {
|
|
87
|
-
|
|
87
|
+
await this.refreshOwner(guild);
|
|
88
|
+
}
|
|
89
|
+
async refreshOwner(guild) {
|
|
90
|
+
const cached = guild.members.cache.get(guild.ownerId)?.user ??
|
|
91
|
+
guild.client.users.cache?.get(guild.ownerId);
|
|
88
92
|
const user = cached ?? (await guild.client.users.fetch(guild.ownerId));
|
|
89
|
-
|
|
93
|
+
const observedAt = new Date();
|
|
94
|
+
const persisted = await this.#repository.observeOwner({
|
|
90
95
|
guildId: guild.id,
|
|
91
96
|
ownerUserId: guild.ownerId,
|
|
92
97
|
username: user.username,
|
|
98
|
+
observedAt,
|
|
93
99
|
});
|
|
100
|
+
if (!persisted)
|
|
101
|
+
throw new Error("Guild owner observation no longer matched the installation");
|
|
102
|
+
return { username: user.username, observedAt: observedAt.toISOString() };
|
|
94
103
|
}
|
|
95
104
|
}
|
|
96
105
|
export class HostedServerLeaveWorker {
|
|
@@ -122,6 +131,19 @@ export class HostedServerLeaveWorker {
|
|
|
122
131
|
});
|
|
123
132
|
return this.#active;
|
|
124
133
|
}
|
|
134
|
+
async deliverOperation(input) {
|
|
135
|
+
if (this.#stopped)
|
|
136
|
+
return "queued";
|
|
137
|
+
await this.#active;
|
|
138
|
+
const claim = await this.#repository.claimLeaveDelivery({
|
|
139
|
+
...input,
|
|
140
|
+
leaseSeconds: 30,
|
|
141
|
+
...(this.shardScope ?? {}),
|
|
142
|
+
});
|
|
143
|
+
if (claim)
|
|
144
|
+
return this.#deliver(claim);
|
|
145
|
+
return ((await this.#repository.getLeaveDeliveryOutcome(input.operationId, input.guildId)) ?? "queued");
|
|
146
|
+
}
|
|
125
147
|
async stop() {
|
|
126
148
|
this.#stopped = true;
|
|
127
149
|
if (this.#timer)
|
|
@@ -149,7 +171,7 @@ export class HostedServerLeaveWorker {
|
|
|
149
171
|
const guild = this.client.guilds.cache.get(claim.guildId);
|
|
150
172
|
if (!guild) {
|
|
151
173
|
await this.#repository.completeLeaveDelivery(claim, "not_currently_installed");
|
|
152
|
-
return;
|
|
174
|
+
return "not_currently_installed";
|
|
153
175
|
}
|
|
154
176
|
try {
|
|
155
177
|
await guild.leave();
|
|
@@ -159,12 +181,13 @@ export class HostedServerLeaveWorker {
|
|
|
159
181
|
guildId: claim.guildId,
|
|
160
182
|
operationId: claim.operationId,
|
|
161
183
|
}, "Hosted server leave completed");
|
|
184
|
+
return "completed";
|
|
162
185
|
}
|
|
163
186
|
catch (error) {
|
|
164
187
|
const failureCode = classifyLeaveFailure(error);
|
|
165
188
|
if (failureCode === "discord_not_installed") {
|
|
166
189
|
await this.#repository.completeLeaveDelivery(claim, "not_currently_installed");
|
|
167
|
-
return;
|
|
190
|
+
return "not_currently_installed";
|
|
168
191
|
}
|
|
169
192
|
const outcome = await this.#repository.failLeaveDelivery({
|
|
170
193
|
claim,
|
|
@@ -178,6 +201,11 @@ export class HostedServerLeaveWorker {
|
|
|
178
201
|
failureCode,
|
|
179
202
|
outcome,
|
|
180
203
|
}, "Hosted server leave will be retried or reviewed");
|
|
204
|
+
return outcome === "requires_review"
|
|
205
|
+
? outcome
|
|
206
|
+
: outcome === "retry"
|
|
207
|
+
? "accepted_for_retry"
|
|
208
|
+
: "queued";
|
|
181
209
|
}
|
|
182
210
|
}
|
|
183
211
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,5 +11,5 @@ export interface BotHandle {
|
|
|
11
11
|
export declare function startBot(processEnvironment?: NodeJS.ProcessEnv): Promise<BotHandle>;
|
|
12
12
|
export declare function resolveBotWorkerEntryPoint(entryUrl?: string): string;
|
|
13
13
|
export declare function readBotVersion(): string;
|
|
14
|
-
export declare function gatewayIntentsForModules(modules: readonly ModuleDefinition[], messageContentEnabled?: boolean, guildMembersEnabled?: boolean): GatewayIntentBits[];
|
|
14
|
+
export declare function gatewayIntentsForModules(modules: readonly ModuleDefinition[], messageContentEnabled?: boolean, guildMembersEnabled?: boolean, runtimeProfile?: "hosted" | "self-hosted"): GatewayIntentBits[];
|
|
15
15
|
//# sourceMappingURL=index.d.ts.map
|