@helyx/bot 1.0.1 → 1.2.0
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 +80 -0
- package/README.md +27 -2
- 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/developer-operations-module.d.ts +2 -0
- package/dist/developer-operations-module.js +136 -2
- 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 +275 -9
- package/dist/guilds.d.ts +2 -1
- package/dist/guilds.js +164 -5
- package/dist/hosted-server-moderation.d.ts +100 -0
- package/dist/hosted-server-moderation.js +226 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +101 -12
- 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 +47 -0
- package/dist/modules.d.ts +6 -0
- package/dist/modules.js +65 -3
- package/dist/premium-activation-module.js +2 -0
- 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 +72 -6
- package/dist/shard-worker.d.ts +16 -1
- package/dist/shard-worker.js +108 -10
- 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 +13 -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
|
@@ -4,7 +4,7 @@ import { SHARD_CONTROL_TIMEOUT_MS, assertShardMessageSize, shardOperationsHealth
|
|
|
4
4
|
import { INTERNAL_MODULE_MANIFEST_FIELDS } from "./internal-module-manifest.js";
|
|
5
5
|
const DEVELOPER_MODULE_ID = "helyx.developer-operations";
|
|
6
6
|
const DEVELOPER_MODULE_NAME = "Helyx Developer Operations";
|
|
7
|
-
const DEVELOPER_MODULE_VERSION = "0.
|
|
7
|
+
const DEVELOPER_MODULE_VERSION = "0.3.0";
|
|
8
8
|
const MAX_MODULE_LINES = 8;
|
|
9
9
|
export function createDeveloperOperationsModule(input) {
|
|
10
10
|
const approvedRoles = new Set(input.approvedRoleIds);
|
|
@@ -15,13 +15,24 @@ export function createDeveloperOperationsModule(input) {
|
|
|
15
15
|
id: DEVELOPER_MODULE_ID,
|
|
16
16
|
name: DEVELOPER_MODULE_NAME,
|
|
17
17
|
version: DEVELOPER_MODULE_VERSION,
|
|
18
|
-
description: "Private
|
|
18
|
+
description: "Private development and hosted-server operations for Helyx staff.",
|
|
19
19
|
entry: "./dist/developer-operations-module.js",
|
|
20
20
|
framework: "^0.1.0",
|
|
21
21
|
dependencies: [],
|
|
22
22
|
permissions: [],
|
|
23
23
|
contributions: { commands: ["dev"], events: [] },
|
|
24
24
|
},
|
|
25
|
+
defaultPermissions: {
|
|
26
|
+
module: restrictedPolicy(),
|
|
27
|
+
commands: Object.fromEntries([
|
|
28
|
+
"dev.shards",
|
|
29
|
+
"dev.bot",
|
|
30
|
+
"dev.proc",
|
|
31
|
+
"dev.trivia-data",
|
|
32
|
+
"dev.leave",
|
|
33
|
+
"dev.blacklist",
|
|
34
|
+
].map((id) => [id, restrictedPolicy()])),
|
|
35
|
+
},
|
|
25
36
|
interactions: {
|
|
26
37
|
commands: [
|
|
27
38
|
{
|
|
@@ -33,6 +44,7 @@ export function createDeveloperOperationsModule(input) {
|
|
|
33
44
|
subcommands: [
|
|
34
45
|
{
|
|
35
46
|
name: "shards",
|
|
47
|
+
permissionId: "dev.shards",
|
|
36
48
|
description: "Show bounded read-only shard health",
|
|
37
49
|
acknowledgement: "deferred-ephemeral",
|
|
38
50
|
authorization: "self-authorized",
|
|
@@ -83,6 +95,7 @@ export function createDeveloperOperationsModule(input) {
|
|
|
83
95
|
},
|
|
84
96
|
{
|
|
85
97
|
name: "bot",
|
|
98
|
+
permissionId: "dev.bot",
|
|
86
99
|
description: "Show the bot build and installed module versions",
|
|
87
100
|
acknowledgement: "immediate-response",
|
|
88
101
|
authorization: "self-authorized",
|
|
@@ -137,6 +150,7 @@ export function createDeveloperOperationsModule(input) {
|
|
|
137
150
|
},
|
|
138
151
|
{
|
|
139
152
|
name: "proc",
|
|
153
|
+
permissionId: "dev.proc",
|
|
140
154
|
description: "Show safe process resource and uptime information",
|
|
141
155
|
acknowledgement: "immediate-response",
|
|
142
156
|
authorization: "self-authorized",
|
|
@@ -171,6 +185,7 @@ export function createDeveloperOperationsModule(input) {
|
|
|
171
185
|
},
|
|
172
186
|
{
|
|
173
187
|
name: "trivia-data",
|
|
188
|
+
permissionId: "dev.trivia-data",
|
|
174
189
|
description: "Show question counts from the Trivia database",
|
|
175
190
|
acknowledgement: "deferred-ephemeral",
|
|
176
191
|
authorization: "self-authorized",
|
|
@@ -220,6 +235,100 @@ export function createDeveloperOperationsModule(input) {
|
|
|
220
235
|
});
|
|
221
236
|
},
|
|
222
237
|
},
|
|
238
|
+
...(input.moderation
|
|
239
|
+
? ["leave", "blacklist"].map((action) => ({
|
|
240
|
+
name: action,
|
|
241
|
+
permissionId: `dev.${action}`,
|
|
242
|
+
description: action === "leave"
|
|
243
|
+
? "Make hosted Helyx leave a server"
|
|
244
|
+
: "Blacklist a server and make hosted Helyx leave",
|
|
245
|
+
acknowledgement: "deferred-ephemeral",
|
|
246
|
+
authorization: "self-authorized",
|
|
247
|
+
rateLimit: {
|
|
248
|
+
scope: "user",
|
|
249
|
+
limit: 3,
|
|
250
|
+
windowSeconds: 60,
|
|
251
|
+
},
|
|
252
|
+
options: [
|
|
253
|
+
{
|
|
254
|
+
type: "string",
|
|
255
|
+
name: "server_id",
|
|
256
|
+
description: "Discord server ID",
|
|
257
|
+
required: true,
|
|
258
|
+
minLength: 17,
|
|
259
|
+
maxLength: 20,
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
type: "string",
|
|
263
|
+
name: "reason",
|
|
264
|
+
description: "Private reason for this action",
|
|
265
|
+
required: true,
|
|
266
|
+
minLength: 3,
|
|
267
|
+
maxLength: 500,
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
async execute(context) {
|
|
271
|
+
if (!(await authorizeDeveloperCommand(context)))
|
|
272
|
+
return;
|
|
273
|
+
const guildId = context
|
|
274
|
+
.getStringOption("server_id")
|
|
275
|
+
?.trim();
|
|
276
|
+
const reason = context.getStringOption("reason")?.trim();
|
|
277
|
+
if (!guildId ||
|
|
278
|
+
!/^\d{17,20}$/u.test(guildId) ||
|
|
279
|
+
!reason ||
|
|
280
|
+
reason.length < 3 ||
|
|
281
|
+
reason.length > 500 ||
|
|
282
|
+
/[\r\n\0]/u.test(reason)) {
|
|
283
|
+
await context.reply({
|
|
284
|
+
ephemeral: true,
|
|
285
|
+
componentsV2: {
|
|
286
|
+
accentColor: 0xdc2626,
|
|
287
|
+
text: [
|
|
288
|
+
{
|
|
289
|
+
content: "The server ID or private reason is invalid.",
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
const result = await input.moderation.moderate({
|
|
298
|
+
action,
|
|
299
|
+
guildId,
|
|
300
|
+
actorUserId: context.userId,
|
|
301
|
+
reason,
|
|
302
|
+
idempotencyKey: `discord:${context.interactionId}`,
|
|
303
|
+
});
|
|
304
|
+
await context.reply({
|
|
305
|
+
ephemeral: true,
|
|
306
|
+
componentsV2: {
|
|
307
|
+
accentColor: 0x2563eb,
|
|
308
|
+
text: [
|
|
309
|
+
{
|
|
310
|
+
content: `## Hosted server ${action}\n${moderationOutcome(result.outcome)}\nOperation: \`${result.operationId}\`${result.paidSlotsReleased ? `\nPremium slots released: ${result.paidSlotsReleased}` : ""}`,
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
await context.reply({
|
|
318
|
+
ephemeral: true,
|
|
319
|
+
componentsV2: {
|
|
320
|
+
accentColor: 0xdc2626,
|
|
321
|
+
text: [
|
|
322
|
+
{
|
|
323
|
+
content: "The hosted moderation service is unavailable. The request was not reported as complete; retry with the same command when service is restored.",
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
}))
|
|
331
|
+
: []),
|
|
223
332
|
],
|
|
224
333
|
},
|
|
225
334
|
],
|
|
@@ -247,6 +356,31 @@ export function createDeveloperOperationsModule(input) {
|
|
|
247
356
|
return false;
|
|
248
357
|
}
|
|
249
358
|
}
|
|
359
|
+
function restrictedPolicy() {
|
|
360
|
+
return {
|
|
361
|
+
everyone: false,
|
|
362
|
+
requiredPermissions: ["ADMINISTRATOR"],
|
|
363
|
+
allowedRoleIds: [],
|
|
364
|
+
deniedRoleIds: [],
|
|
365
|
+
allowedUserIds: [],
|
|
366
|
+
deniedUserIds: [],
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function moderationOutcome(outcome) {
|
|
370
|
+
if (outcome === "queued")
|
|
371
|
+
return "Accepted. Helyx will leave through the durable delivery queue.";
|
|
372
|
+
if (outcome === "already_blacklisted")
|
|
373
|
+
return "No new blacklist was created; this server is already blacklisted.";
|
|
374
|
+
if (outcome === "not_currently_installed")
|
|
375
|
+
return "Recorded. Helyx is not currently installed in that server.";
|
|
376
|
+
if (outcome === "accepted_for_retry")
|
|
377
|
+
return "Accepted for a safe retry.";
|
|
378
|
+
if (outcome === "requires_review")
|
|
379
|
+
return "Recorded and flagged for staff review.";
|
|
380
|
+
if (outcome === "completed")
|
|
381
|
+
return "Completed.";
|
|
382
|
+
return "The request was recorded but did not complete.";
|
|
383
|
+
}
|
|
250
384
|
function safeInlineCode(value) {
|
|
251
385
|
return `\`${value.replaceAll("`", "'").slice(0, 120)}\``;
|
|
252
386
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DiscordAnalyticsInspectionService } from "@helyx/sdk";
|
|
2
2
|
import { type Client } from "discord.js";
|
|
3
3
|
export declare class DiscordAnalyticsInspection implements DiscordAnalyticsInspectionService {
|
|
4
|
+
#private;
|
|
4
5
|
private readonly client;
|
|
5
6
|
constructor(client: Client);
|
|
6
7
|
listChannels(input: {
|
|
@@ -25,6 +26,8 @@ export declare class DiscordAnalyticsInspection implements DiscordAnalyticsInspe
|
|
|
25
26
|
limit: number;
|
|
26
27
|
includeArchived: boolean;
|
|
27
28
|
includePrivate: boolean;
|
|
29
|
+
signal?: AbortSignal;
|
|
28
30
|
}): ReturnType<DiscordAnalyticsInspectionService["listThreads"]>;
|
|
31
|
+
private activeThreads;
|
|
29
32
|
}
|
|
30
33
|
//# sourceMappingURL=discord-analytics.d.ts.map
|
|
@@ -2,8 +2,10 @@ import { ChannelType, GatewayIntentBits, PermissionFlagsBits, } from "discord.js
|
|
|
2
2
|
const MAX_MESSAGE_PAGE = 100;
|
|
3
3
|
const MAX_MEMBER_PAGE = 1_000;
|
|
4
4
|
const MAX_THREAD_PAGE = 100;
|
|
5
|
+
const ACTIVE_THREAD_CACHE_MS = 15_000;
|
|
5
6
|
export class DiscordAnalyticsInspection {
|
|
6
7
|
client;
|
|
8
|
+
#activeThreads = new WeakMap();
|
|
7
9
|
constructor(client) {
|
|
8
10
|
this.client = client;
|
|
9
11
|
}
|
|
@@ -147,12 +149,12 @@ export class DiscordAnalyticsInspection {
|
|
|
147
149
|
if (input.cursor && !cursor)
|
|
148
150
|
return unavailable("not_supported");
|
|
149
151
|
try {
|
|
150
|
-
const parents = await threadParents(guild, input.parentChannelId);
|
|
152
|
+
const parents = await abortable(threadParents(guild, input.parentChannelId), input.signal);
|
|
151
153
|
const privateCoverage = privateThreadCoverage(guild);
|
|
152
154
|
const current = cursor ?? { phase: "active", offset: 0 };
|
|
153
155
|
if (current.phase === "active") {
|
|
154
|
-
const active = await
|
|
155
|
-
const facts =
|
|
156
|
+
const active = await abortable(this.activeThreads(guild), input.signal);
|
|
157
|
+
const facts = active
|
|
156
158
|
.filter((thread) => !input.parentChannelId ||
|
|
157
159
|
thread.parentId === input.parentChannelId)
|
|
158
160
|
.sort((left, right) => left.id.localeCompare(right.id))
|
|
@@ -183,12 +185,12 @@ export class DiscordAnalyticsInspection {
|
|
|
183
185
|
const phase = phases[phaseIndex];
|
|
184
186
|
while (parentIndex < parents.length) {
|
|
185
187
|
const parent = parents[parentIndex];
|
|
186
|
-
const page = await parent.threads.fetchArchived({
|
|
188
|
+
const page = await abortable(parent.threads.fetchArchived({
|
|
187
189
|
type: phase,
|
|
188
190
|
fetchAll: phase === "private" && privateCoverage === "all",
|
|
189
191
|
limit,
|
|
190
192
|
...(before ? { before } : {}),
|
|
191
|
-
}, false);
|
|
193
|
+
}, false), input.signal);
|
|
192
194
|
const threads = [...page.threads.values()].sort((left, right) => left.id.localeCompare(right.id));
|
|
193
195
|
const items = threads.map((thread) => threadFact(guild, thread));
|
|
194
196
|
if (page.hasMore && threads.length > 0) {
|
|
@@ -228,6 +230,23 @@ export class DiscordAnalyticsInspection {
|
|
|
228
230
|
return unavailable("discord_unavailable");
|
|
229
231
|
}
|
|
230
232
|
}
|
|
233
|
+
activeThreads(guild) {
|
|
234
|
+
const cached = this.#activeThreads.get(guild);
|
|
235
|
+
if (cached && cached.expiresAt > Date.now())
|
|
236
|
+
return cached.promise;
|
|
237
|
+
const promise = guild.channels
|
|
238
|
+
.fetchActiveThreads(false)
|
|
239
|
+
.then((response) => [...response.threads.values()]);
|
|
240
|
+
this.#activeThreads.set(guild, {
|
|
241
|
+
expiresAt: Date.now() + ACTIVE_THREAD_CACHE_MS,
|
|
242
|
+
promise,
|
|
243
|
+
});
|
|
244
|
+
void promise.catch(() => {
|
|
245
|
+
if (this.#activeThreads.get(guild)?.promise === promise)
|
|
246
|
+
this.#activeThreads.delete(guild);
|
|
247
|
+
});
|
|
248
|
+
return promise;
|
|
249
|
+
}
|
|
231
250
|
}
|
|
232
251
|
function analyticsChannelType(channel) {
|
|
233
252
|
switch (channel.type) {
|
|
@@ -246,6 +265,10 @@ function analyticsChannelType(channel) {
|
|
|
246
265
|
}
|
|
247
266
|
}
|
|
248
267
|
async function threadParents(guild, parentChannelId) {
|
|
268
|
+
if (parentChannelId) {
|
|
269
|
+
const channel = await guild.channels.fetch(parentChannelId);
|
|
270
|
+
return channel && "threads" in channel ? [channel] : [];
|
|
271
|
+
}
|
|
249
272
|
const channels = await guild.channels.fetch();
|
|
250
273
|
return [...channels.values()]
|
|
251
274
|
.filter((channel) => channel !== null &&
|
|
@@ -334,4 +357,25 @@ function snowflakeTimestamp(id) {
|
|
|
334
357
|
function unavailable(code) {
|
|
335
358
|
return { available: false, code };
|
|
336
359
|
}
|
|
360
|
+
function abortable(promise, signal) {
|
|
361
|
+
if (!signal)
|
|
362
|
+
return promise;
|
|
363
|
+
return new Promise((resolve, reject) => {
|
|
364
|
+
const abort = () => reject(signal.reason instanceof Error
|
|
365
|
+
? signal.reason
|
|
366
|
+
: new Error("Discord Analytics inspection was aborted"));
|
|
367
|
+
if (signal.aborted) {
|
|
368
|
+
abort();
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
372
|
+
void promise.then((value) => {
|
|
373
|
+
signal.removeEventListener("abort", abort);
|
|
374
|
+
resolve(value);
|
|
375
|
+
}, (error) => {
|
|
376
|
+
signal.removeEventListener("abort", abort);
|
|
377
|
+
reject(error instanceof Error ? error : new Error("Discord read failed"));
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
}
|
|
337
381
|
//# sourceMappingURL=discord-analytics.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { DiscordAutoModerationAction, DiscordAutoModerationMutationResult, DiscordAutoModerationRuleSnapshot, DiscordAutoModerationService } from "@helyx/sdk";
|
|
2
|
+
import { type AutoModerationAction, type AutoModerationRule, type Client } from "discord.js";
|
|
3
|
+
/** Discord REST adapter scoped to guilds owned by this client/shard. */
|
|
4
|
+
export declare class DiscordAutoModerationAdapter implements DiscordAutoModerationService {
|
|
5
|
+
#private;
|
|
6
|
+
private readonly client;
|
|
7
|
+
constructor(client: Client);
|
|
8
|
+
list(input: {
|
|
9
|
+
guildId: string;
|
|
10
|
+
}): Promise<readonly DiscordAutoModerationRuleSnapshot[]>;
|
|
11
|
+
read(input: {
|
|
12
|
+
guildId: string;
|
|
13
|
+
ruleId: string;
|
|
14
|
+
}): Promise<DiscordAutoModerationRuleSnapshot | null>;
|
|
15
|
+
create(input: Parameters<DiscordAutoModerationService["create"]>[0]): Promise<DiscordAutoModerationMutationResult>;
|
|
16
|
+
update(input: Parameters<DiscordAutoModerationService["update"]>[0]): Promise<DiscordAutoModerationMutationResult>;
|
|
17
|
+
delete(input: Parameters<DiscordAutoModerationService["delete"]>[0]): Promise<DiscordAutoModerationMutationResult>;
|
|
18
|
+
}
|
|
19
|
+
export declare function toRuleSnapshot(rule: AutoModerationRule): DiscordAutoModerationRuleSnapshot;
|
|
20
|
+
export declare function fromDiscordAction(action: AutoModerationAction): DiscordAutoModerationAction;
|
|
21
|
+
//# sourceMappingURL=discord-auto-moderation-service.d.ts.map
|