@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
package/dist/shard-router.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { AUTOMOD_ENHANCED_CONTRACT_VERSION, AUTOMOD_ENHANCED_PROVIDER_ID, AUTOMOD_SCANNER_LIMITS, } from "@helyx/sdk";
|
|
2
3
|
import { BotControlTransportError, } from "./control-server.js";
|
|
3
4
|
import { SHARD_CONTROL_TIMEOUT_MS, controlOperationGuildId, guildIdToShardId, shardControlOperationSchema, shardControlResponseSchema, } from "./shard-contracts.js";
|
|
4
5
|
// Cross-region/public Redis connections used by local staging routinely take
|
|
@@ -7,6 +8,7 @@ import { SHARD_CONTROL_TIMEOUT_MS, controlOperationGuildId, guildIdToShardId, sh
|
|
|
7
8
|
// deadline turned healthy leases into deterministic 503s. Keep the check
|
|
8
9
|
// bounded while allowing normal network variance.
|
|
9
10
|
const LEASE_VALIDATION_TIMEOUT_MS = 3_000;
|
|
11
|
+
const ECKO_RESULT_TIMEOUT_MS = AUTOMOD_SCANNER_LIMITS.actionDeadlineSeconds * 1_000;
|
|
10
12
|
export class ShardControlRouter {
|
|
11
13
|
input;
|
|
12
14
|
constructor(input) {
|
|
@@ -15,11 +17,28 @@ export class ShardControlRouter {
|
|
|
15
17
|
cleanupOAuthConsumer(input) {
|
|
16
18
|
return this.#route({ kind: "oauth.consumer.cleanup", ...input });
|
|
17
19
|
}
|
|
20
|
+
deliverHostedServerLeave(input) {
|
|
21
|
+
return this.#route({ kind: "hosted-server.leave.deliver", ...input });
|
|
22
|
+
}
|
|
23
|
+
refreshHostedServerOwner(guildId) {
|
|
24
|
+
return this.#route({ kind: "hosted-server.owner.refresh", guildId });
|
|
25
|
+
}
|
|
18
26
|
listAvailabilityModules() {
|
|
19
27
|
if (!this.input.availability)
|
|
20
28
|
return Promise.reject(new BotControlTransportError(503, "Module availability catalogue is unavailable"));
|
|
21
29
|
return this.input.availability.listAvailabilityModules();
|
|
22
30
|
}
|
|
31
|
+
/** Routes an authenticated Ecko result to the current guild owner. */
|
|
32
|
+
acceptHostedEckoResult(input) {
|
|
33
|
+
return this.#route({ kind: "automod.enhanced.result.accept", ...input }, remainingEnhancedDeadline(input.result));
|
|
34
|
+
}
|
|
35
|
+
/** Reads the fenced leader's current Enhanced configuration snapshot. */
|
|
36
|
+
getHostedEckoConfigurationPage(input) {
|
|
37
|
+
return this.#routeLeader({
|
|
38
|
+
kind: "automod.enhanced.configuration.page",
|
|
39
|
+
...input,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
23
42
|
getGuildState(guildId) {
|
|
24
43
|
return this.#route({ kind: "guild.state.get", guildId });
|
|
25
44
|
}
|
|
@@ -97,6 +116,12 @@ export class ShardControlRouter {
|
|
|
97
116
|
...input,
|
|
98
117
|
}));
|
|
99
118
|
}
|
|
119
|
+
previewConfiguration(input) {
|
|
120
|
+
return this.#route(shardControlOperationSchema.parse({
|
|
121
|
+
kind: "guild.module.configuration.preview",
|
|
122
|
+
...input,
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
100
125
|
updatePermissions(input) {
|
|
101
126
|
return this.#route({ kind: "guild.module.permissions.update", ...input });
|
|
102
127
|
}
|
|
@@ -106,6 +131,9 @@ export class ShardControlRouter {
|
|
|
106
131
|
executeDashboardAction(input) {
|
|
107
132
|
return this.#route({ kind: "guild.module.action.execute", ...input });
|
|
108
133
|
}
|
|
134
|
+
previewDashboardAction(input) {
|
|
135
|
+
return this.#route({ kind: "guild.module.action.preview", ...input });
|
|
136
|
+
}
|
|
109
137
|
listManagedResources(input) {
|
|
110
138
|
return this.#route(shardControlOperationSchema.parse({
|
|
111
139
|
kind: "guild.module.resource.list",
|
|
@@ -130,6 +158,12 @@ export class ShardControlRouter {
|
|
|
130
158
|
...input,
|
|
131
159
|
}));
|
|
132
160
|
}
|
|
161
|
+
importManagedResource(input) {
|
|
162
|
+
return this.#route(shardControlOperationSchema.parse({
|
|
163
|
+
kind: "guild.module.resource.import",
|
|
164
|
+
...input,
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
133
167
|
createManagedResource(input) {
|
|
134
168
|
return this.#route(shardControlOperationSchema.parse({
|
|
135
169
|
kind: "guild.module.resource.create",
|
|
@@ -188,7 +222,7 @@ export class ShardControlRouter {
|
|
|
188
222
|
executeThrough: executeThrough.toISOString(),
|
|
189
223
|
});
|
|
190
224
|
}
|
|
191
|
-
async #route(operation) {
|
|
225
|
+
async #route(operation, timeoutMs = SHARD_CONTROL_TIMEOUT_MS) {
|
|
192
226
|
const guildId = controlOperationGuildId(operation);
|
|
193
227
|
if (!guildId)
|
|
194
228
|
throw new BotControlTransportError(502, "The control operation has no shard route");
|
|
@@ -212,13 +246,13 @@ export class ShardControlRouter {
|
|
|
212
246
|
requestId: randomUUID(),
|
|
213
247
|
generation: this.input.generation,
|
|
214
248
|
fencingToken: lease.fencingToken,
|
|
215
|
-
deadlineAt: new Date(Date.now() +
|
|
249
|
+
deadlineAt: new Date(Date.now() + timeoutMs).toISOString(),
|
|
216
250
|
operation,
|
|
217
251
|
};
|
|
218
252
|
let response;
|
|
219
253
|
try {
|
|
220
254
|
response = localLease
|
|
221
|
-
? await withDeadline(this.input.dispatchLocal(shardId, request),
|
|
255
|
+
? await withDeadline(this.input.dispatchLocal(shardId, request), timeoutMs)
|
|
222
256
|
: await this.input.coordinator.routeRemote(lease, request);
|
|
223
257
|
}
|
|
224
258
|
catch (error) {
|
|
@@ -231,13 +265,13 @@ export class ShardControlRouter {
|
|
|
231
265
|
return unwrapResponse(response);
|
|
232
266
|
}
|
|
233
267
|
async #routeLeader(operation) {
|
|
234
|
-
if (this.input.localLeaderLease
|
|
268
|
+
if (this.input.localLeaderLease) {
|
|
235
269
|
if (this.input.coordinator) {
|
|
236
270
|
const current = await this.#currentLeaderLease();
|
|
237
271
|
if (!current || !sameLeaderLease(this.input.localLeaderLease, current))
|
|
238
272
|
throw new BotControlTransportError(503, "The shard leader is temporarily unavailable");
|
|
239
273
|
}
|
|
240
|
-
return
|
|
274
|
+
return executeLeaderOperation(this.input, operation);
|
|
241
275
|
}
|
|
242
276
|
const coordinator = this.input.coordinator;
|
|
243
277
|
const lease = await this.#currentLeaderLease();
|
|
@@ -283,6 +317,36 @@ export class ShardControlRouter {
|
|
|
283
317
|
}
|
|
284
318
|
}
|
|
285
319
|
}
|
|
320
|
+
async function executeLeaderOperation(input, operation) {
|
|
321
|
+
if (operation.kind === "automod.enhanced.configuration.page") {
|
|
322
|
+
if (!input.leaderScannerRegistry)
|
|
323
|
+
throw new BotControlTransportError(503, "The Enhanced configuration leader is unavailable");
|
|
324
|
+
return input.leaderScannerRegistry.getConfigurationPage({
|
|
325
|
+
adapter: "hosted_ecko",
|
|
326
|
+
providerId: AUTOMOD_ENHANCED_PROVIDER_ID,
|
|
327
|
+
contractVersion: AUTOMOD_ENHANCED_CONTRACT_VERSION,
|
|
328
|
+
limit: operation.limit,
|
|
329
|
+
...(operation.cursor ? { cursor: operation.cursor } : {}),
|
|
330
|
+
...(operation.knownFingerprint
|
|
331
|
+
? { knownFingerprint: operation.knownFingerprint }
|
|
332
|
+
: {}),
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
if (!input.privacy)
|
|
336
|
+
throw new BotControlTransportError(503, "The shard leader is temporarily unavailable");
|
|
337
|
+
return executePrivacy(input.privacy, operation);
|
|
338
|
+
}
|
|
339
|
+
function remainingEnhancedDeadline(result) {
|
|
340
|
+
if (!result || typeof result !== "object")
|
|
341
|
+
return ECKO_RESULT_TIMEOUT_MS;
|
|
342
|
+
const deadlineAt = result["deadlineAt"];
|
|
343
|
+
if (typeof deadlineAt !== "string")
|
|
344
|
+
return ECKO_RESULT_TIMEOUT_MS;
|
|
345
|
+
const parsed = Date.parse(deadlineAt);
|
|
346
|
+
if (!Number.isFinite(parsed))
|
|
347
|
+
return ECKO_RESULT_TIMEOUT_MS;
|
|
348
|
+
return Math.max(1, Math.min(ECKO_RESULT_TIMEOUT_MS, parsed - Date.now()));
|
|
349
|
+
}
|
|
286
350
|
async function executePrivacy(privacy, operation) {
|
|
287
351
|
if (operation.kind === "privacy.providers.list")
|
|
288
352
|
return (await privacy.listPrivacyProviders());
|
|
@@ -4,6 +4,7 @@ import type { ModuleDefinition, NumericWriteBehindCommittedBatch, ScheduledTaskE
|
|
|
4
4
|
import { buildApplicationCommandDefinitions } from "./interactions.js";
|
|
5
5
|
import { type ShardCloudRequest, type ShardHealthSnapshot, type ShardIdentity, type ShardProcessHealth } from "./shard-contracts.js";
|
|
6
6
|
import { type ShardCoordinator, type ShardGenerationDescriptor, type ShardLeaderLease, type ShardLease } from "./shard-coordinator.js";
|
|
7
|
+
import { ShardControlRouter } from "./shard-router.js";
|
|
7
8
|
import type { BotHandle } from "./index.js";
|
|
8
9
|
export declare const STEADY_STATE_LEASE_LOSS_BOUND_MS: number;
|
|
9
10
|
export declare function supervisorShutdownExitCode(reason: string): number | undefined;
|
|
@@ -17,6 +18,16 @@ export declare function relinquishRestartCircuitOwnership(input: {
|
|
|
17
18
|
}): Promise<boolean>;
|
|
18
19
|
export declare function deferScheduledRoutingFailures(route: () => Promise<ScheduledTaskExecutionResult>): Promise<ScheduledTaskExecutionResult>;
|
|
19
20
|
export declare function startShardSupervisor(environment: BotEnvironment, processEnvironment: NodeJS.ProcessEnv, workerEntry: string): Promise<BotHandle>;
|
|
21
|
+
export declare function shardHostedControlCallbacks(router: Pick<ShardControlRouter, "deliverHostedServerLeave" | "refreshHostedServerOwner">): {
|
|
22
|
+
deliverHostedServerLeave: (input: {
|
|
23
|
+
operationId: string;
|
|
24
|
+
guildId: string;
|
|
25
|
+
}) => Promise<"completed" | "requires_review" | "queued" | "not_currently_installed" | "accepted_for_retry">;
|
|
26
|
+
refreshHostedServerOwner: (guildId: string) => Promise<{
|
|
27
|
+
username: string;
|
|
28
|
+
observedAt: string;
|
|
29
|
+
}>;
|
|
30
|
+
};
|
|
20
31
|
export declare function splitNumericCommittedBatch(batch: NumericWriteBehindCommittedBatch, index: number): NumericWriteBehindCommittedBatch;
|
|
21
32
|
export declare function buildShardWorkerEnvironment(source: NodeJS.ProcessEnv, identity: ShardIdentity, restartCount: number): NodeJS.ProcessEnv;
|
|
22
33
|
export declare function shardRuntimeCompositionHash(modules: readonly ModuleDefinition[], internalGuildId?: string, globalCommandMode?: Parameters<typeof buildApplicationCommandDefinitions>[2]): string;
|
package/dist/shard-supervisor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { safeError, ServiceRegistry } from "@helyx/core";
|
|
2
|
-
import { checkDatabaseConnection, createDatabasePool, AuditRepository, GuildCommandRepository, HostedServerSafetyRepository, InteractionSessionRepository, PremiumAllocationRepository, RateLimitRepository, runConfiguredModuleMigrations, runCoreMigrations, RuntimeStateRepository, } from "@helyx/database";
|
|
2
|
+
import { checkDatabaseConnection, createDatabasePool, AuditRepository, GuildCommandRepository, HostedServerSafetyRepository, InteractionSessionRepository, EckoOperationsRepository, PremiumAllocationRepository, RateLimitRepository, runConfiguredModuleMigrations, runCoreMigrations, RuntimeStateRepository, } from "@helyx/database";
|
|
3
3
|
import { createPlatformServices, gamingStatisticsRecordOwner, achievementContributionRecordOwner, PostgresPrivacyProviderExecutor, } from "@helyx/platform";
|
|
4
|
-
import { HELYX_SERVICE_NAMES } from "@helyx/sdk";
|
|
4
|
+
import { AUTOMOD_ENHANCED_CONTRACT_VERSION, AUTOMOD_ENHANCED_PROVIDER_ID, HELYX_SERVICE_NAMES, } from "@helyx/sdk";
|
|
5
5
|
import { Client, ShardingManager } from "discord.js";
|
|
6
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
7
7
|
import { createRequire } from "node:module";
|
|
@@ -19,6 +19,11 @@ import { createPermissionsModule } from "./permissions-module.js";
|
|
|
19
19
|
import { SHARD_CONTROL_MAX_BYTES, SHARD_CONTROL_TIMEOUT_MS, SHARD_HEALTH_STALE_MS, assertShardMessageSize, controlOperationGuildId, executeShardControlOperation, gatewayBotInformationSchema, guildIdToShardId, shardCloudRequestSchema, shardHealthSnapshotSchema, workerMessageSchema, } from "./shard-contracts.js";
|
|
20
20
|
import { RedisShardCoordinator, } from "./shard-coordinator.js";
|
|
21
21
|
import { ShardControlRouter } from "./shard-router.js";
|
|
22
|
+
import { BotAutoModerationScannerRegistry } from "./auto-moderation-scanner-registry.js";
|
|
23
|
+
import { ShardEckoScannerRegistry } from "./shard-ecko-scanner-registry.js";
|
|
24
|
+
import { EckoEvidenceReceiver } from "./ecko-evidence-receiver.js";
|
|
25
|
+
import { EckoTransportAuthenticator } from "./ecko-transport-auth.js";
|
|
26
|
+
import { TransientViolationEvidenceStore } from "./transient-violation-evidence.js";
|
|
22
27
|
import { createDeveloperOperationsModule } from "./developer-operations-module.js";
|
|
23
28
|
import { SupervisorCloudDiscordDirectory } from "./supervisor-cloud-directory.js";
|
|
24
29
|
import { createSupportModule } from "./support-module.js";
|
|
@@ -148,6 +153,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
148
153
|
let deploymentOrder = 1;
|
|
149
154
|
let shardCount = 1;
|
|
150
155
|
let privacyService;
|
|
156
|
+
let leaderScannerRegistry;
|
|
151
157
|
let manager;
|
|
152
158
|
let scheduledTasks;
|
|
153
159
|
let guildCommandWorker;
|
|
@@ -180,6 +186,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
180
186
|
observedAt: new Date().toISOString(),
|
|
181
187
|
};
|
|
182
188
|
const supervisorDirectory = new SupervisorCloudDiscordDirectory(environment.DISCORD_TOKEN, database, environment.HELYX_OWNER_USER_IDS);
|
|
189
|
+
const eckoOperations = new EckoOperationsRepository(database);
|
|
183
190
|
cloudConnector = new CloudConnectorController({
|
|
184
191
|
environment,
|
|
185
192
|
database,
|
|
@@ -288,6 +295,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
288
295
|
assertStartupLeaseHealthy();
|
|
289
296
|
const services = new Map(createPlatformServices(database, publicModules, platformServiceOptions(environment, publicModules)));
|
|
290
297
|
runtimeAvailability = services.get(HELYX_SERVICE_NAMES.moduleAvailability);
|
|
298
|
+
leaderScannerRegistry = new BotAutoModerationScannerRegistry(publicModules, new ServiceRegistry(services), "hosted", undefined, environment.HELYX_DEPLOYMENT_ENVIRONMENT);
|
|
291
299
|
if (numericWriteBehind)
|
|
292
300
|
services.set(HELYX_SERVICE_NAMES.numericWriteBehind, numericWriteBehind);
|
|
293
301
|
privacyService = new BotControlService(database, [
|
|
@@ -311,6 +319,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
311
319
|
localLeases,
|
|
312
320
|
dispatchLocal,
|
|
313
321
|
privacy: privacyService,
|
|
322
|
+
leaderScannerRegistry,
|
|
314
323
|
availability: availabilityService,
|
|
315
324
|
localLeaderLease: leaderLease,
|
|
316
325
|
...(coordinator ? { coordinator } : {}),
|
|
@@ -405,6 +414,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
405
414
|
localLeases,
|
|
406
415
|
dispatchLocal,
|
|
407
416
|
...(privacyService ? { privacy: privacyService } : {}),
|
|
417
|
+
...(leaderScannerRegistry ? { leaderScannerRegistry } : {}),
|
|
408
418
|
availability: availabilityService,
|
|
409
419
|
...(leaderLease ? { localLeaderLease: leaderLease } : {}),
|
|
410
420
|
...(coordinator ? { coordinator } : {}),
|
|
@@ -437,13 +447,41 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
437
447
|
startScheduledTasks();
|
|
438
448
|
assertStartupLeaseHealthy();
|
|
439
449
|
if (environment.HELYX_BOT_CONTROL_SECRET) {
|
|
450
|
+
const supervisorEvidenceStore = new TransientViolationEvidenceStore();
|
|
451
|
+
const shardEckoRegistry = new ShardEckoScannerRegistry({
|
|
452
|
+
configurationRegistry: () => leaderScannerRegistry,
|
|
453
|
+
routeConfiguration: (input) => router.getHostedEckoConfigurationPage(input),
|
|
454
|
+
evidenceStore: supervisorEvidenceStore,
|
|
455
|
+
routeResult: (input) => router.acceptHostedEckoResult(input),
|
|
456
|
+
});
|
|
457
|
+
const ecko = environment.HELYX_ECKO_TRANSPORT_SECRET
|
|
458
|
+
? {
|
|
459
|
+
authenticator: new EckoTransportAuthenticator(environment.HELYX_DEPLOYMENT_ENVIRONMENT, [
|
|
460
|
+
environment.HELYX_ECKO_TRANSPORT_SECRET,
|
|
461
|
+
...(environment.HELYX_ECKO_TRANSPORT_PREVIOUS_SECRET
|
|
462
|
+
? [environment.HELYX_ECKO_TRANSPORT_PREVIOUS_SECRET]
|
|
463
|
+
: []),
|
|
464
|
+
]),
|
|
465
|
+
registry: shardEckoRegistry,
|
|
466
|
+
evidence: new EckoEvidenceReceiver(supervisorEvidenceStore, environment.HELYX_DEPLOYMENT_ENVIRONMENT, Buffer.from(environment.HELYX_ECKO_TRANSPORT_SECRET, "base64")),
|
|
467
|
+
gateState: () => eckoOperations.readGate(),
|
|
468
|
+
telemetry: {
|
|
469
|
+
accept: (batch) => eckoOperations.acceptTelemetry(batch),
|
|
470
|
+
},
|
|
471
|
+
}
|
|
472
|
+
: undefined;
|
|
440
473
|
controlServer = await createBotControlServer({
|
|
441
474
|
service: router,
|
|
442
475
|
secret: environment.HELYX_BOT_CONTROL_SECRET,
|
|
476
|
+
...shardHostedControlCallbacks(router),
|
|
443
477
|
logger: { level: environment.LOG_LEVEL },
|
|
478
|
+
runtimeConfiguration: {
|
|
479
|
+
hostedModerationEnabled: environment.HELYX_HOSTED_MODERATION_ENABLED,
|
|
480
|
+
},
|
|
444
481
|
shardHealth: buildHealth,
|
|
445
482
|
readiness: buildDeploymentReadiness,
|
|
446
483
|
guildCommands: new GuildCommandReadService(database, environment.DISCORD_CLIENT_ID, () => guildCommandWorker?.getHealth(), environment.DISCORD_TOKEN, environment.HELYX_GUILD_COMMAND_WRITES_ENABLED, environment.HELYX_GUILD_COMMAND_WRITE_GUILD_IDS, environment.HELYX_GUILD_COMMAND_WRITE_SCOPE === "all", commandModules),
|
|
484
|
+
...(ecko ? { ecko } : {}),
|
|
447
485
|
});
|
|
448
486
|
await controlServer.listen({
|
|
449
487
|
host: "::",
|
|
@@ -855,7 +893,10 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
855
893
|
});
|
|
856
894
|
}
|
|
857
895
|
async function executeLeaderControl(request) {
|
|
858
|
-
if (!leaderLease ||
|
|
896
|
+
if (!leaderLease ||
|
|
897
|
+
!privacyService ||
|
|
898
|
+
(request.operation.kind === "automod.enhanced.configuration.page" &&
|
|
899
|
+
!leaderScannerRegistry))
|
|
859
900
|
return unavailableControl(request.requestId);
|
|
860
901
|
if (request.generation !== leaderLease.generation ||
|
|
861
902
|
request.fencingToken !== leaderLease.fencingToken ||
|
|
@@ -867,7 +908,22 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
867
908
|
type: "helyx.control.response",
|
|
868
909
|
requestId: request.requestId,
|
|
869
910
|
ok: true,
|
|
870
|
-
result:
|
|
911
|
+
result: request.operation.kind === "automod.enhanced.configuration.page"
|
|
912
|
+
? await leaderScannerRegistry?.getConfigurationPage({
|
|
913
|
+
adapter: "hosted_ecko",
|
|
914
|
+
providerId: AUTOMOD_ENHANCED_PROVIDER_ID,
|
|
915
|
+
contractVersion: AUTOMOD_ENHANCED_CONTRACT_VERSION,
|
|
916
|
+
limit: request.operation.limit,
|
|
917
|
+
...(request.operation.cursor
|
|
918
|
+
? { cursor: request.operation.cursor }
|
|
919
|
+
: {}),
|
|
920
|
+
...(request.operation.knownFingerprint
|
|
921
|
+
? {
|
|
922
|
+
knownFingerprint: request.operation.knownFingerprint,
|
|
923
|
+
}
|
|
924
|
+
: {}),
|
|
925
|
+
})
|
|
926
|
+
: await executeShardControlOperation(privacyService, request.operation),
|
|
871
927
|
};
|
|
872
928
|
}
|
|
873
929
|
catch (error) {
|
|
@@ -1454,6 +1510,12 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
1454
1510
|
logger.info({ event: "shard.supervisor_stopped", reason }, "Shard supervisor stopped");
|
|
1455
1511
|
}
|
|
1456
1512
|
}
|
|
1513
|
+
export function shardHostedControlCallbacks(router) {
|
|
1514
|
+
return {
|
|
1515
|
+
deliverHostedServerLeave: (input) => router.deliverHostedServerLeave(input),
|
|
1516
|
+
refreshHostedServerOwner: (guildId) => router.refreshHostedServerOwner(guildId),
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1457
1519
|
export function splitNumericCommittedBatch(batch, index) {
|
|
1458
1520
|
const mutation = batch.mutations[index];
|
|
1459
1521
|
const result = batch.results[index];
|
|
@@ -1583,6 +1645,7 @@ export function buildShardWorkerEnvironment(source, identity, restartCount) {
|
|
|
1583
1645
|
"HELYX_MESSAGE_CONTENT_ENABLED",
|
|
1584
1646
|
"HELYX_GUILD_MEMBERS_ENABLED",
|
|
1585
1647
|
"HELYX_GUILD_RECONCILIATION_INTERVAL_SECONDS",
|
|
1648
|
+
"HELYX_HOSTED_MODERATION_ENABLED",
|
|
1586
1649
|
"HELYX_MODULE_CATALOGUE_PATH",
|
|
1587
1650
|
"STRIPE_ENVIRONMENT",
|
|
1588
1651
|
"HELYX_CLOUD_CONNECT_ENABLED",
|
package/dist/shard-worker.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type CommandRegistration } from "./interactions.js";
|
|
2
|
-
import
|
|
2
|
+
import type { BotAutoModerationScannerRegistry } from "./auto-moderation-scanner-registry.js";
|
|
3
|
+
import type { TransientViolationEvidenceStore } from "./transient-violation-evidence.js";
|
|
4
|
+
import { type ShardControlOperation, type ShardIdentity, type ShardScheduledTaskRequest, type ShardScheduledTaskResponse } from "./shard-contracts.js";
|
|
3
5
|
import { type BotHandle } from "./index.js";
|
|
4
6
|
import { type ScheduledTaskRoute } from "./scheduled-task-runner.js";
|
|
5
7
|
export declare const WORKER_ORPHAN_TERMINATION_BOUND_MS: number;
|
|
@@ -28,6 +30,7 @@ export declare function parseShardWorkerEnvironment(processEnvironment: NodeJS.P
|
|
|
28
30
|
HELYX_SHARD_WORKER_DATABASE_POOL_MAX: number | undefined;
|
|
29
31
|
DATABASE_URL: string;
|
|
30
32
|
DATABASE_SSL: "disable" | "require" | "verify-full";
|
|
33
|
+
HELYX_RUNTIME_PROFILE: "hosted" | "self-hosted";
|
|
31
34
|
DISCORD_TOKEN: string;
|
|
32
35
|
DISCORD_CLIENT_ID: string;
|
|
33
36
|
DISCORD_INTERNAL_GUILD_ID: string | undefined;
|
|
@@ -35,6 +38,7 @@ export declare function parseShardWorkerEnvironment(processEnvironment: NodeJS.P
|
|
|
35
38
|
HELYX_MODULE_PACKAGES: string[];
|
|
36
39
|
HELYX_MODULE_CATALOGUE_PATH: string | undefined;
|
|
37
40
|
HELYX_DASHBOARD_PUBLIC_URL: string;
|
|
41
|
+
HELYX_DEPLOYMENT_ENVIRONMENT: string;
|
|
38
42
|
HELYX_HOSTED_MODERATION_ENABLED: boolean;
|
|
39
43
|
HELYX_SUPPORT_STAFF_CHANNEL_ID: string | undefined;
|
|
40
44
|
HELYX_VERIFICATION_ENABLED: boolean;
|
|
@@ -47,6 +51,11 @@ export declare function parseShardWorkerEnvironment(processEnvironment: NodeJS.P
|
|
|
47
51
|
HELYX_GUILD_COMMAND_WRITE_GUILD_IDS: string[];
|
|
48
52
|
HELYX_PRIVACY_ENABLED: boolean;
|
|
49
53
|
HELYX_MESSAGE_CONTENT_ENABLED: boolean;
|
|
54
|
+
HELYX_AUTOMOD_EMBEDDED_WORKERS: number;
|
|
55
|
+
HELYX_AUTOMOD_EMBEDDED_QUEUE_ITEMS: number;
|
|
56
|
+
HELYX_AUTOMOD_EMBEDDED_QUEUE_BYTES: number;
|
|
57
|
+
HELYX_AUTOMOD_EMBEDDED_QUEUE_ITEMS_PER_GUILD: number;
|
|
58
|
+
HELYX_AUTOMOD_EMBEDDED_REFRESH_SECONDS: number;
|
|
50
59
|
HELYX_GUILD_MEMBERS_ENABLED: boolean;
|
|
51
60
|
HELYX_CLOUD_CONNECT_ENABLED: boolean;
|
|
52
61
|
HELYX_CLOUD_ENROLMENT_TOKEN: string | undefined;
|
|
@@ -66,6 +75,8 @@ export declare function parseShardWorkerEnvironment(processEnvironment: NodeJS.P
|
|
|
66
75
|
HELYX_STATUS_HEARTBEAT_INTERVAL_SECONDS: number;
|
|
67
76
|
STRIPE_ENVIRONMENT?: "sandbox" | "live" | undefined;
|
|
68
77
|
HELYX_BOT_CONTROL_SECRET?: string | undefined;
|
|
78
|
+
HELYX_ECKO_TRANSPORT_SECRET?: string | undefined;
|
|
79
|
+
HELYX_ECKO_TRANSPORT_PREVIOUS_SECRET?: string | undefined;
|
|
69
80
|
HELYX_SUPPORT_SERVICE_SECRET?: string | undefined;
|
|
70
81
|
HELYX_VERIFICATION_INSTALLATION_ID?: string | undefined;
|
|
71
82
|
HELYX_VERIFICATION_URL?: string | undefined;
|
|
@@ -91,4 +102,7 @@ export declare function shardWorkerCommandRegistration(input: {
|
|
|
91
102
|
premiumActivationModuleId?: string;
|
|
92
103
|
oauthModuleId?: string;
|
|
93
104
|
}): CommandRegistration;
|
|
105
|
+
export declare function acceptEnhancedAutoModerationResult(registry: Pick<BotAutoModerationScannerRegistry, "acceptResult"> | undefined, evidenceStore: TransientViolationEvidenceStore | undefined, operation: Extract<ShardControlOperation, {
|
|
106
|
+
kind: "automod.enhanced.result.accept";
|
|
107
|
+
}>): Promise<import("@helyx/sdk").AutoModerationEnhancedResultOutcome>;
|
|
94
108
|
//# sourceMappingURL=shard-worker.d.ts.map
|
package/dist/shard-worker.js
CHANGED
|
@@ -4,14 +4,15 @@ import { checkDatabaseConnection, createDatabasePool, PremiumAllocationRepositor
|
|
|
4
4
|
import { REVIEWED_PREMIUM_MODULE_IDS } from "@helyx/module-manifest";
|
|
5
5
|
import { createRuntime } from "@helyx/framework";
|
|
6
6
|
import { createPlatformServices, PlatformMemberRoleGrantOwnershipService, PostgresMemberRoleGrantOwnershipStore, } from "@helyx/platform";
|
|
7
|
-
import { HELYX_SERVICE_NAMES, } from "@helyx/sdk";
|
|
7
|
+
import { AUTOMOD_ENHANCED_CONTRACT_VERSION, AUTOMOD_ENHANCED_PROVIDER_ID, HELYX_SERVICE_NAMES, } from "@helyx/sdk";
|
|
8
8
|
import { Client, Events, Partials } from "discord.js";
|
|
9
9
|
import { monitorEventLoopDelay } from "node:perf_hooks";
|
|
10
10
|
import pino from "pino";
|
|
11
11
|
import { createCloudManagementModule } from "./cloud-management-module.js";
|
|
12
|
-
import { BotControlService, DiscordGuildResourceDirectory, mapBotControlError, } from "./control-server.js";
|
|
12
|
+
import { BotControlService, BotControlTransportError, DiscordGuildResourceDirectory, mapBotControlError, } from "./control-server.js";
|
|
13
13
|
import { HttpVerificationService } from "./verification-service.js";
|
|
14
|
-
import { DiscordMessageDeliveryService,
|
|
14
|
+
import { DiscordMessageDeliveryService, DiscordMemberRoleService, DiscordVerificationMemberService, DiscordResourceInspection, installModuleEvents, } from "./events.js";
|
|
15
|
+
import { DiscordModerationMemberEnforcementAdapter } from "./discord-member-enforcement-service.js";
|
|
15
16
|
import { installGuildTracking, } from "./guilds.js";
|
|
16
17
|
import { installInteractions, } from "./interactions.js";
|
|
17
18
|
import { createManagementModule } from "./management-module.js";
|
|
@@ -24,6 +25,7 @@ import { DiscordPremiumGuildAuthority } from "./premium-guild-authority.js";
|
|
|
24
25
|
import { assertReviewedPremiumCatalogue, requirePremiumProviderEnvironment, } from "./premium-access.js";
|
|
25
26
|
import { WorkerCloudConnectorProxy } from "./shard-cloud-proxy.js";
|
|
26
27
|
import { DiscordOwnedMessageDeletionService } from "./discord-owned-message-service.js";
|
|
28
|
+
import { installModerationPlatformServices } from "./moderation-platform-services.js";
|
|
27
29
|
import { DiscordGuildHealthInspection } from "./discord-guild-health.js";
|
|
28
30
|
import { DiscordAnalyticsInspection } from "./discord-analytics.js";
|
|
29
31
|
import { DiscordInviteInspection } from "./discord-invites.js";
|
|
@@ -80,8 +82,9 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
80
82
|
? {}
|
|
81
83
|
: { maxConnections: environment.HELYX_SHARD_WORKER_DATABASE_POOL_MAX }),
|
|
82
84
|
});
|
|
85
|
+
const hostedOwnerObserver = new PostgresHostedGuildSafety(database);
|
|
83
86
|
const hostedSafety = environment.HELYX_HOSTED_MODERATION_ENABLED
|
|
84
|
-
?
|
|
87
|
+
? hostedOwnerObserver
|
|
85
88
|
: undefined;
|
|
86
89
|
const developerModeration = environment.HELYX_HOSTED_MODERATION_ENABLED
|
|
87
90
|
? new HostedServerModerationClient(environment.HELYX_CLOUD_API_URL, environment.HELYX_BOT_CONTROL_SECRET)
|
|
@@ -100,6 +103,9 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
100
103
|
let client;
|
|
101
104
|
let cloudProxy;
|
|
102
105
|
let shardHealthProvider;
|
|
106
|
+
let embeddedScanner;
|
|
107
|
+
let autoModerationScannerRegistry;
|
|
108
|
+
let violationEvidenceStore;
|
|
103
109
|
let hostedLeaveWorker;
|
|
104
110
|
let leaseHeartbeatWatchdog;
|
|
105
111
|
let forcedLeaseExit;
|
|
@@ -120,6 +126,7 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
120
126
|
await cloudProxy?.stop().catch(() => undefined);
|
|
121
127
|
shardHealthProvider?.stop();
|
|
122
128
|
await moduleEvents?.stop().catch(() => undefined);
|
|
129
|
+
await embeddedScanner?.stop().catch(() => undefined);
|
|
123
130
|
if (runtimeStarted)
|
|
124
131
|
await runtime?.stop().catch(() => undefined);
|
|
125
132
|
await numericWriteBehind?.stop();
|
|
@@ -137,7 +144,7 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
137
144
|
assertReviewedPremiumCatalogue(featureModules);
|
|
138
145
|
client = new Client({
|
|
139
146
|
rest: discordRestOptions,
|
|
140
|
-
intents: gatewayIntentsForModules(featureModules, environment.HELYX_MESSAGE_CONTENT_ENABLED, environment.HELYX_GUILD_MEMBERS_ENABLED),
|
|
147
|
+
intents: gatewayIntentsForModules(featureModules, environment.HELYX_MESSAGE_CONTENT_ENABLED, environment.HELYX_GUILD_MEMBERS_ENABLED, environment.HELYX_RUNTIME_PROFILE),
|
|
141
148
|
partials: [Partials.Message, Partials.Channel, Partials.Reaction],
|
|
142
149
|
allowedMentions: { parse: [] },
|
|
143
150
|
shards: [identity.shardId],
|
|
@@ -245,11 +252,29 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
245
252
|
platformServices.set(HELYX_SERVICE_NAMES.resourceAuthorization, new CoreResourceAuthorizationService(platformServices.get(HELYX_SERVICE_NAMES.permissions)));
|
|
246
253
|
platformServices.set(HELYX_SERVICE_NAMES.discordMessages, new DiscordMessageDeliveryService(client));
|
|
247
254
|
platformServices.set(HELYX_SERVICE_NAMES.discordOwnedMessages, new DiscordOwnedMessageDeletionService(client));
|
|
248
|
-
platformServices.set(HELYX_SERVICE_NAMES.memberEnforcement, new
|
|
255
|
+
platformServices.set(HELYX_SERVICE_NAMES.memberEnforcement, new DiscordModerationMemberEnforcementAdapter(client));
|
|
249
256
|
const memberRoles = new DiscordMemberRoleService(client);
|
|
250
257
|
platformServices.set(HELYX_SERVICE_NAMES.memberRoles, memberRoles);
|
|
251
258
|
platformServices.set(HELYX_SERVICE_NAMES.memberRoleGrants, new PlatformMemberRoleGrantOwnershipService(new PostgresMemberRoleGrantOwnershipStore(database), memberRoles));
|
|
252
259
|
platformServices.set(HELYX_SERVICE_NAMES.discordResources, new DiscordResourceInspection(client));
|
|
260
|
+
const moderationPlatformServices = await installModerationPlatformServices({
|
|
261
|
+
services: platformServices,
|
|
262
|
+
serviceAccess,
|
|
263
|
+
client,
|
|
264
|
+
modules: publicModules,
|
|
265
|
+
runtimeProfile: environment.HELYX_RUNTIME_PROFILE,
|
|
266
|
+
deploymentEnvironment: environment.HELYX_DEPLOYMENT_ENVIRONMENT,
|
|
267
|
+
embedded: {
|
|
268
|
+
workers: environment.HELYX_AUTOMOD_EMBEDDED_WORKERS,
|
|
269
|
+
maximumItems: environment.HELYX_AUTOMOD_EMBEDDED_QUEUE_ITEMS,
|
|
270
|
+
maximumBytes: environment.HELYX_AUTOMOD_EMBEDDED_QUEUE_BYTES,
|
|
271
|
+
maximumItemsPerGuild: environment.HELYX_AUTOMOD_EMBEDDED_QUEUE_ITEMS_PER_GUILD,
|
|
272
|
+
refreshIntervalMs: environment.HELYX_AUTOMOD_EMBEDDED_REFRESH_SECONDS * 1_000,
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
embeddedScanner = moderationPlatformServices.embeddedScanner;
|
|
276
|
+
autoModerationScannerRegistry = moderationPlatformServices.scannerRegistry;
|
|
277
|
+
violationEvidenceStore = moderationPlatformServices.evidenceStore;
|
|
253
278
|
platformServices.set(HELYX_SERVICE_NAMES.discordGuildHealth, new DiscordGuildHealthInspection(client, publicModules, serviceAccess));
|
|
254
279
|
platformServices.set(HELYX_SERVICE_NAMES.discordAnalytics, new DiscordAnalyticsInspection(client));
|
|
255
280
|
if (serviceOptions.inviteFingerprintKey)
|
|
@@ -278,14 +303,12 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
278
303
|
shardId: identity.shardId,
|
|
279
304
|
fencingToken: BigInt(identity.fencingToken),
|
|
280
305
|
};
|
|
281
|
-
const prepareGuilds = installGuildTracking(client, database, publicModules, logger, enabledCoreModuleIds, shardOwnership, environment.HELYX_GUILD_RECONCILIATION_INTERVAL_SECONDS * 1_000, identity.shardCount, environment.DISCORD_CLIENT_ID, hostedSafety, hostedInviteGate);
|
|
306
|
+
const prepareGuilds = installGuildTracking(client, database, publicModules, logger, enabledCoreModuleIds, shardOwnership, environment.HELYX_GUILD_RECONCILIATION_INTERVAL_SECONDS * 1_000, identity.shardCount, environment.DISCORD_CLIENT_ID, hostedSafety, hostedInviteGate, hostedOwnerObserver);
|
|
282
307
|
guildTracking = prepareGuilds;
|
|
283
|
-
hostedLeaveWorker =
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
})
|
|
288
|
-
: undefined;
|
|
308
|
+
hostedLeaveWorker = new HostedServerLeaveWorker(database, client, logger, {
|
|
309
|
+
shardId: identity.shardId,
|
|
310
|
+
shardCount: identity.shardCount,
|
|
311
|
+
});
|
|
289
312
|
const commandsReady = installInteractions(client, modules, shardWorkerCommandRegistration({
|
|
290
313
|
...(environment.DISCORD_INTERNAL_GUILD_ID
|
|
291
314
|
? { internalGuildId: environment.DISCORD_INTERNAL_GUILD_ID }
|
|
@@ -300,7 +323,10 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
300
323
|
: {}),
|
|
301
324
|
...(oauthModule ? { oauthModuleId: oauthModule.manifest.id } : {}),
|
|
302
325
|
}), logger, serviceAccess);
|
|
303
|
-
moduleEvents = installModuleEvents(client, publicModules, logger, serviceAccess
|
|
326
|
+
moduleEvents = installModuleEvents(client, publicModules, logger, serviceAccess, {
|
|
327
|
+
runtimeProfile: environment.HELYX_RUNTIME_PROFILE,
|
|
328
|
+
evidenceStore: moderationPlatformServices.evidenceStore,
|
|
329
|
+
});
|
|
304
330
|
const controlService = new BotControlService(database, publicModules, guildResources, serviceAccess, undefined, environment.DISCORD_INTERNAL_GUILD_ID &&
|
|
305
331
|
environment.HELYX_SUPPORT_STAFF_CHANNEL_ID
|
|
306
332
|
? {
|
|
@@ -313,7 +339,7 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
313
339
|
if (!parsed.success)
|
|
314
340
|
return;
|
|
315
341
|
if (parsed.data.type === "helyx.control.request") {
|
|
316
|
-
void handleControl(identity, controlService, parsed.data, logger);
|
|
342
|
+
void handleControl(identity, controlService, hostedOwnerObserver, client, hostedLeaveWorker, autoModerationScannerRegistry, violationEvidenceStore, parsed.data, logger);
|
|
317
343
|
}
|
|
318
344
|
else if (parsed.data.type === "helyx.lease.heartbeat") {
|
|
319
345
|
if (sameShardIdentity(parsed.data.identity, identity))
|
|
@@ -375,6 +401,9 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
375
401
|
await cloudProxy.start();
|
|
376
402
|
await runtime.start();
|
|
377
403
|
runtimeStarted = true;
|
|
404
|
+
await embeddedScanner?.synchronize().catch((error) => {
|
|
405
|
+
logger.warn({ event: "automod.embedded_sync_failed", error: safeError(error) }, "Embedded Auto Moderation scanner started in Standard fallback");
|
|
406
|
+
});
|
|
378
407
|
await client.login(environment.DISCORD_TOKEN);
|
|
379
408
|
await commandsReady;
|
|
380
409
|
hostedLeaveWorker?.start();
|
|
@@ -501,7 +530,7 @@ export function shardWorkerCommandRegistration(input) {
|
|
|
501
530
|
]),
|
|
502
531
|
};
|
|
503
532
|
}
|
|
504
|
-
async function handleControl(identity, service, request, logger) {
|
|
533
|
+
async function handleControl(identity, service, hostedOwnerObserver, client, hostedLeaveWorker, scannerRegistry, evidenceStore, request, logger) {
|
|
505
534
|
let response;
|
|
506
535
|
const guildId = controlOperationGuildId(request.operation);
|
|
507
536
|
if (request.generation !== identity.generation ||
|
|
@@ -525,7 +554,13 @@ async function handleControl(identity, service, request, logger) {
|
|
|
525
554
|
type: "helyx.control.response",
|
|
526
555
|
requestId: request.requestId,
|
|
527
556
|
ok: true,
|
|
528
|
-
result:
|
|
557
|
+
result: request.operation.kind === "automod.enhanced.result.accept"
|
|
558
|
+
? await acceptEnhancedAutoModerationResult(scannerRegistry, evidenceStore, request.operation)
|
|
559
|
+
: request.operation.kind === "hosted-server.leave.deliver"
|
|
560
|
+
? await deliverHostedLeave(hostedLeaveWorker, request.operation)
|
|
561
|
+
: request.operation.kind === "hosted-server.owner.refresh"
|
|
562
|
+
? await refreshHostedOwner(hostedOwnerObserver, client, request.operation.guildId)
|
|
563
|
+
: await executeShardControlOperation(service, request.operation),
|
|
529
564
|
};
|
|
530
565
|
}
|
|
531
566
|
catch (error) {
|
|
@@ -570,6 +605,49 @@ async function handleControl(identity, service, request, logger) {
|
|
|
570
605
|
}
|
|
571
606
|
process.send?.(response);
|
|
572
607
|
}
|
|
608
|
+
export async function acceptEnhancedAutoModerationResult(registry, evidenceStore, operation) {
|
|
609
|
+
if (!registry || !evidenceStore)
|
|
610
|
+
return {
|
|
611
|
+
outcome: "rejected",
|
|
612
|
+
code: "module_unavailable",
|
|
613
|
+
};
|
|
614
|
+
const resultGuildId = operation.result && typeof operation.result === "object"
|
|
615
|
+
? operation.result["guildId"]
|
|
616
|
+
: undefined;
|
|
617
|
+
if (resultGuildId !== operation.guildId)
|
|
618
|
+
return { outcome: "rejected", code: "wrong_owner" };
|
|
619
|
+
const evidenceReference = operation.evidence
|
|
620
|
+
? evidenceStore.capture({
|
|
621
|
+
source: "enhanced_scanner",
|
|
622
|
+
...operation.evidence,
|
|
623
|
+
})
|
|
624
|
+
: null;
|
|
625
|
+
if (operation.evidence && !evidenceReference)
|
|
626
|
+
return {
|
|
627
|
+
outcome: "rejected",
|
|
628
|
+
code: "module_unavailable",
|
|
629
|
+
};
|
|
630
|
+
return registry.acceptResult({
|
|
631
|
+
adapter: "hosted_ecko",
|
|
632
|
+
providerId: AUTOMOD_ENHANCED_PROVIDER_ID,
|
|
633
|
+
contractVersion: AUTOMOD_ENHANCED_CONTRACT_VERSION,
|
|
634
|
+
result: operation.result,
|
|
635
|
+
...(evidenceReference ? { evidenceReference } : {}),
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
function deliverHostedLeave(worker, input) {
|
|
639
|
+
if (!worker)
|
|
640
|
+
throw new BotControlTransportError(503, "Hosted server moderation delivery is unavailable");
|
|
641
|
+
return worker.deliverOperation(input);
|
|
642
|
+
}
|
|
643
|
+
function refreshHostedOwner(ownerObserver, client, guildId) {
|
|
644
|
+
if (!client)
|
|
645
|
+
throw new BotControlTransportError(503, "Hosted server owner refresh is unavailable");
|
|
646
|
+
const guild = client.guilds.cache.get(guildId);
|
|
647
|
+
if (!guild)
|
|
648
|
+
throw new BotControlTransportError(404, "The hosted server is not installed");
|
|
649
|
+
return ownerObserver.refreshOwner(guild);
|
|
650
|
+
}
|
|
573
651
|
function health(identity, client, status, eventLoopDelayNanoseconds, queueDepth) {
|
|
574
652
|
return {
|
|
575
653
|
shardId: identity.shardId,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type ViolationEvidenceReference } from "@helyx/sdk";
|
|
2
|
+
export interface TransientViolationEvidenceStoreLimits {
|
|
3
|
+
entries?: number;
|
|
4
|
+
totalBytes?: number;
|
|
5
|
+
}
|
|
6
|
+
export type TransientEvidenceReadResult = {
|
|
7
|
+
outcome: "available";
|
|
8
|
+
content: string;
|
|
9
|
+
} | {
|
|
10
|
+
outcome: "unavailable";
|
|
11
|
+
} | {
|
|
12
|
+
outcome: "mismatch";
|
|
13
|
+
};
|
|
14
|
+
export type TransientViolationEvidenceCaptureInput = (Omit<Extract<ViolationEvidenceReference, {
|
|
15
|
+
source: "discord_auto_moderation";
|
|
16
|
+
}>, "key"> & {
|
|
17
|
+
content: string;
|
|
18
|
+
}) | (Omit<Extract<ViolationEvidenceReference, {
|
|
19
|
+
source: "enhanced_scanner";
|
|
20
|
+
}>, "key"> & {
|
|
21
|
+
content: string;
|
|
22
|
+
});
|
|
23
|
+
/**
|
|
24
|
+
* Bot-private, memory-only hand-off for message content. The opaque reference is
|
|
25
|
+
* safe to cross module boundaries; the content and this vault are not.
|
|
26
|
+
*/
|
|
27
|
+
export declare class TransientViolationEvidenceStore {
|
|
28
|
+
#private;
|
|
29
|
+
private readonly now;
|
|
30
|
+
private readonly createKey;
|
|
31
|
+
constructor(now?: () => number, createKey?: () => string, limits?: TransientViolationEvidenceStoreLimits);
|
|
32
|
+
capture(input: TransientViolationEvidenceCaptureInput): ViolationEvidenceReference | null;
|
|
33
|
+
consume(reference: ViolationEvidenceReference): TransientEvidenceReadResult;
|
|
34
|
+
/** Read without discarding so a failed Discord send can be retried. */
|
|
35
|
+
read(reference: ViolationEvidenceReference): TransientEvidenceReadResult;
|
|
36
|
+
discard(reference: ViolationEvidenceReference): void;
|
|
37
|
+
get size(): number;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=transient-violation-evidence.d.ts.map
|