@helyx/bot 1.2.1 → 1.3.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 +29 -0
- package/dist/control-server.js +7 -36
- package/dist/dashboard-discord-settings.d.ts +20 -0
- package/dist/dashboard-discord-settings.js +67 -0
- package/dist/dashboard-feature-control.d.ts +2 -1
- package/dist/dashboard-feature-control.js +3 -0
- package/dist/discord-automod-execution-ids.d.ts +12 -0
- package/dist/discord-automod-execution-ids.js +62 -0
- package/dist/discord-violation-evidence-message.d.ts +12 -0
- package/dist/discord-violation-evidence-message.js +63 -0
- package/dist/discord-violation-layout-message.d.ts +13 -0
- package/dist/discord-violation-layout-message.js +156 -0
- package/dist/discord-violation-notification-users.d.ts +12 -0
- package/dist/discord-violation-notification-users.js +37 -0
- package/dist/discord-violation-thread-close.d.ts +5 -0
- package/dist/discord-violation-thread-close.js +71 -0
- package/dist/discord-violation-thread-service.d.ts +9 -0
- package/dist/discord-violation-thread-service.js +188 -50
- package/dist/events.js +13 -13
- package/dist/index.js +1 -0
- package/dist/management-module.js +36 -2
- package/dist/moderation-action-registry.js +41 -2
- package/dist/privacy-provider-registry.d.ts +1 -1
- package/dist/privacy-provider-registry.js +35 -6
- package/dist/shard-router.js +2 -13
- package/dist/shard-worker.js +5 -0
- package/package.json +7 -7
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isCustomerSelectableModule, } from "@helyx/module-manifest";
|
|
2
2
|
import { defineModule, HELYX_SERVICE_NAMES, ModuleStateValidationError, } from "@helyx/sdk";
|
|
3
3
|
import { INTERNAL_MODULE_MANIFEST_FIELDS } from "./internal-module-manifest.js";
|
|
4
|
+
import { validateDiscordUserSettingSelections } from "./dashboard-discord-settings.js";
|
|
4
5
|
const MODULE_SELECT_ID = "helyx.management.module";
|
|
5
6
|
const SETTING_SELECT_ID = "helyx.management.setting";
|
|
6
7
|
const VALUE_SELECT_ID = "helyx.management.value";
|
|
@@ -129,7 +130,9 @@ export function createManagementModule(configurableModules, resources) {
|
|
|
129
130
|
const setting = selectedSetting(module, context.selectedValues[0]);
|
|
130
131
|
const state = { ...session.state, selectedSettingKey: setting.key };
|
|
131
132
|
await updateSession(context, session, state);
|
|
132
|
-
if (setting.type === "string" ||
|
|
133
|
+
if (setting.type === "string" ||
|
|
134
|
+
setting.type === "integer" ||
|
|
135
|
+
setting.type === "discord-user") {
|
|
133
136
|
const now = Date.now();
|
|
134
137
|
for (const [key, prepared] of preparedValueModals) {
|
|
135
138
|
if (prepared.expiresAt <= now)
|
|
@@ -222,6 +225,7 @@ export function createManagementModule(configurableModules, resources) {
|
|
|
222
225
|
return;
|
|
223
226
|
}
|
|
224
227
|
try {
|
|
228
|
+
await validateDiscordUserSettingSelections(context.services, context.serverId, module.manifest.dashboard?.settings ?? [], value);
|
|
225
229
|
const validated = module.configuration?.validate(value);
|
|
226
230
|
if (validated && module.configuration?.validateUpdate) {
|
|
227
231
|
await module.configuration.validateUpdate({
|
|
@@ -353,6 +357,15 @@ export function createManagementModule(configurableModules, resources) {
|
|
|
353
357
|
return;
|
|
354
358
|
const module = selectedModule(session.state, byId);
|
|
355
359
|
const setting = selectedSetting(module, stringState(session.state, "selectedSettingKey"));
|
|
360
|
+
if (setting.type === "discord-user") {
|
|
361
|
+
const userIds = [...context.getSelectedUserIds(VALUE_FIELD_ID)];
|
|
362
|
+
const updated = await updateSession(context, session, {
|
|
363
|
+
...session.state,
|
|
364
|
+
draft: { ...draft(session.state), [setting.key]: userIds },
|
|
365
|
+
});
|
|
366
|
+
await respond(context, configurationResponse(module, updated.state));
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
356
369
|
if (setting.type !== "string" && setting.type !== "integer") {
|
|
357
370
|
throw new Error("Selected setting does not use text input");
|
|
358
371
|
}
|
|
@@ -593,7 +606,7 @@ function textSettingResponse(setting) {
|
|
|
593
606
|
text: [{ content: `## ${setting.label}\n${setting.description}` }],
|
|
594
607
|
},
|
|
595
608
|
buttons: [
|
|
596
|
-
button(OPEN_VALUE_MODAL_ID, "Enter value", "primary"),
|
|
609
|
+
button(OPEN_VALUE_MODAL_ID, setting.type === "discord-user" ? "Choose users" : "Enter value", "primary"),
|
|
597
610
|
button(CONFIGURE_ID, "Back to settings", "secondary"),
|
|
598
611
|
button(CANCEL_ID, "Cancel", "secondary"),
|
|
599
612
|
],
|
|
@@ -680,6 +693,25 @@ function preparedModalKey(serverId, userId) {
|
|
|
680
693
|
}
|
|
681
694
|
function settingModal(setting, values) {
|
|
682
695
|
const current = values[setting.key];
|
|
696
|
+
if (setting.type === "discord-user")
|
|
697
|
+
return {
|
|
698
|
+
customId: VALUE_MODAL_ID,
|
|
699
|
+
title: truncate(setting.label, 45),
|
|
700
|
+
text: [{ content: setting.description, markdown: false }],
|
|
701
|
+
fields: [
|
|
702
|
+
{
|
|
703
|
+
type: "discord-user",
|
|
704
|
+
customId: VALUE_FIELD_ID,
|
|
705
|
+
label: setting.label,
|
|
706
|
+
required: setting.required,
|
|
707
|
+
minValues: Math.max(setting.required ? 1 : 0, setting.minItems ?? 0),
|
|
708
|
+
maxValues: setting.maxItems ?? 25,
|
|
709
|
+
defaultValues: Array.isArray(current)
|
|
710
|
+
? current.filter((id) => typeof id === "string")
|
|
711
|
+
: [],
|
|
712
|
+
},
|
|
713
|
+
],
|
|
714
|
+
};
|
|
683
715
|
return {
|
|
684
716
|
customId: VALUE_MODAL_ID,
|
|
685
717
|
title: truncate(setting.label, 45),
|
|
@@ -724,6 +756,8 @@ function initialDraft(module, existing) {
|
|
|
724
756
|
return result;
|
|
725
757
|
}
|
|
726
758
|
function emptyValue(setting) {
|
|
759
|
+
if (setting.type === "discord-user")
|
|
760
|
+
return [];
|
|
727
761
|
if (setting.type === "discord-channel" && setting.multiple)
|
|
728
762
|
return [];
|
|
729
763
|
if (setting.type === "discord-role" && setting.multiple)
|
|
@@ -105,6 +105,7 @@ export class BotModerationActionRegistry {
|
|
|
105
105
|
const evidenceClaims = {
|
|
106
106
|
caseIds: new Set(),
|
|
107
107
|
threadIds: new Set(),
|
|
108
|
+
openingMessageIds: new Set(),
|
|
108
109
|
};
|
|
109
110
|
const requestServices = createRequestBoundServices(this.services, evidenceClaims);
|
|
110
111
|
return await this.#actionProvider.execute({
|
|
@@ -117,15 +118,30 @@ export class BotModerationActionRegistry {
|
|
|
117
118
|
!OPERATION_KEY_PATTERN.test(delivery.caseId) ||
|
|
118
119
|
!OPERATION_KEY_PATTERN.test(delivery.operationKey) ||
|
|
119
120
|
!evidenceClaims.caseIds.has(delivery.caseId) ||
|
|
120
|
-
!evidenceClaims.threadIds.has(delivery.threadId)
|
|
121
|
+
!evidenceClaims.threadIds.has(delivery.threadId) ||
|
|
122
|
+
(delivery.openingMessageId !== undefined &&
|
|
123
|
+
(!ID_PATTERN.test(delivery.openingMessageId) ||
|
|
124
|
+
!evidenceClaims.openingMessageIds.has(`${delivery.threadId}:${delivery.openingMessageId}`))))
|
|
121
125
|
return { outcome: "evidence_unavailable" };
|
|
122
126
|
if (!this.evidenceDelivery)
|
|
123
127
|
return { outcome: "evidence_unavailable" };
|
|
124
128
|
return await this.evidenceDelivery.deliverEvidence({
|
|
125
129
|
guildId: delivery.guildId,
|
|
126
130
|
threadId: delivery.threadId,
|
|
131
|
+
...(delivery.openingMessageId
|
|
132
|
+
? { openingMessageId: delivery.openingMessageId }
|
|
133
|
+
: {}),
|
|
127
134
|
evidenceReference: input.request.evidenceReference,
|
|
128
135
|
operationKey: delivery.operationKey,
|
|
136
|
+
context: {
|
|
137
|
+
severity: input.request.severity,
|
|
138
|
+
occurredAt: input.request.occurredAt,
|
|
139
|
+
...(policy.ruleName ? { ruleName: policy.ruleName } : {}),
|
|
140
|
+
confidenceClass: input.request.confidenceClass,
|
|
141
|
+
...(input.request.confidenceScore !== undefined
|
|
142
|
+
? { confidenceScore: input.request.confidenceScore }
|
|
143
|
+
: {}),
|
|
144
|
+
},
|
|
129
145
|
});
|
|
130
146
|
},
|
|
131
147
|
},
|
|
@@ -168,6 +184,11 @@ function isValidRequest(request) {
|
|
|
168
184
|
SAFE_CODE_PATTERN.test(request.reasonCode) &&
|
|
169
185
|
["low", "medium", "high", "critical"].includes(request.severity) &&
|
|
170
186
|
["exact", "native_equivalent", "high", "medium", "low"].includes(request.confidenceClass) &&
|
|
187
|
+
(request.confidenceScore === undefined ||
|
|
188
|
+
(request.detectionSource === "enhanced" &&
|
|
189
|
+
Number.isInteger(request.confidenceScore) &&
|
|
190
|
+
request.confidenceScore >= 0 &&
|
|
191
|
+
request.confidenceScore <= 100)) &&
|
|
171
192
|
(request.sourceMessage === null ||
|
|
172
193
|
(ID_PATTERN.test(request.sourceMessage.channelId) &&
|
|
173
194
|
(request.sourceMessage.messageId === null ||
|
|
@@ -234,9 +255,20 @@ function createRequestBoundServices(services, claims) {
|
|
|
234
255
|
},
|
|
235
256
|
addMember: (input) => service.addMember(input),
|
|
236
257
|
removeMember: (input) => service.removeMember(input),
|
|
237
|
-
send: (input) =>
|
|
258
|
+
send: async (input) => {
|
|
259
|
+
const result = await service.send(input);
|
|
260
|
+
if (claims.threadIds.has(input.threadId) &&
|
|
261
|
+
(result.outcome === "sent" || result.outcome === "already_sent"))
|
|
262
|
+
claims.openingMessageIds.add(`${input.threadId}:${result.messageId}`);
|
|
263
|
+
return result;
|
|
264
|
+
},
|
|
238
265
|
close: (input) => service.close(input),
|
|
239
266
|
reconcile: (input) => service.reconcile(input),
|
|
267
|
+
...(service.editControls
|
|
268
|
+
? {
|
|
269
|
+
editControls: (input) => service.editControls(input),
|
|
270
|
+
}
|
|
271
|
+
: {}),
|
|
240
272
|
};
|
|
241
273
|
return bound;
|
|
242
274
|
}
|
|
@@ -254,6 +286,12 @@ function isValidPolicy(policy) {
|
|
|
254
286
|
policy.windowStartsAt instanceof Date &&
|
|
255
287
|
!Number.isNaN(policy.windowStartsAt.getTime()) &&
|
|
256
288
|
typeof policy.openViolationThread === "boolean" &&
|
|
289
|
+
(policy.deleteSourceMessage === undefined ||
|
|
290
|
+
typeof policy.deleteSourceMessage === "boolean") &&
|
|
291
|
+
(policy.ruleName === undefined ||
|
|
292
|
+
(typeof policy.ruleName === "string" &&
|
|
293
|
+
policy.ruleName.trim().length > 0 &&
|
|
294
|
+
policy.ruleName.length <= 100)) &&
|
|
257
295
|
validRoleIds(policy.globalIgnoredRoleIds, MODERATION_CONTRACT_LIMITS.ignoredRolesPerScope) &&
|
|
258
296
|
validRoleIds(policy.ruleIgnoredRoleIds, MODERATION_CONTRACT_LIMITS.ignoredRolesPerScope) &&
|
|
259
297
|
Boolean(policy.actionPolicy) &&
|
|
@@ -325,6 +363,7 @@ function samePolicy(expected, current) {
|
|
|
325
363
|
expected.configurationRevision === current.configurationRevision &&
|
|
326
364
|
expected.windowStartsAt.getTime() === current.windowStartsAt.getTime() &&
|
|
327
365
|
expected.openViolationThread === current.openViolationThread &&
|
|
366
|
+
expected.deleteSourceMessage === current.deleteSourceMessage &&
|
|
328
367
|
sameStrings(expected.globalIgnoredRoleIds, current.globalIgnoredRoleIds) &&
|
|
329
368
|
sameStrings(expected.ruleIgnoredRoleIds, current.ruleIgnoredRoleIds) &&
|
|
330
369
|
sameAction(expected.actionPolicy.first, current.actionPolicy.first) &&
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ModuleDefinition, type PrivacyProvider, type PrivacyProviderExecutor, type ServiceAccess } from "@helyx/sdk";
|
|
2
2
|
export declare function createPrivacyProviderRegistry(modules: readonly ModuleDefinition[], executor: PrivacyProviderExecutor | undefined, services: ServiceAccess): ReadonlyMap<string, PrivacyProvider>;
|
|
3
3
|
export declare function describePrivacyProviders(providers: ReadonlyMap<string, PrivacyProvider>): readonly {
|
|
4
4
|
id: string;
|
|
@@ -1,12 +1,41 @@
|
|
|
1
|
+
import { createPrivacyProvider, } from "@helyx/sdk";
|
|
1
2
|
export function createPrivacyProviderRegistry(modules, executor, services) {
|
|
2
3
|
if (!executor)
|
|
3
4
|
return new Map();
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
const configurationDeclaration = {
|
|
6
|
+
containsPersonalData: true,
|
|
7
|
+
providerVersion: 1,
|
|
8
|
+
collections: [
|
|
9
|
+
{
|
|
10
|
+
name: "moderation_notification_memberships",
|
|
11
|
+
createdAtField: "updated_at",
|
|
12
|
+
subjectFields: [
|
|
13
|
+
{
|
|
14
|
+
field: "notification_user_id",
|
|
15
|
+
role: "staff-notification-recipient",
|
|
16
|
+
erasure: "set-null",
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
const configurationProvider = createPrivacyProvider({
|
|
23
|
+
id: "helyx.configuration",
|
|
24
|
+
declaration: configurationDeclaration,
|
|
25
|
+
executor,
|
|
26
|
+
});
|
|
27
|
+
if (modules.some(({ manifest }) => manifest.id === configurationProvider.id)) {
|
|
28
|
+
throw new Error("Core configuration privacy identity cannot be a module identity");
|
|
29
|
+
}
|
|
30
|
+
return new Map([
|
|
31
|
+
[configurationProvider.id, configurationProvider],
|
|
32
|
+
...modules.flatMap((module) => {
|
|
33
|
+
if (!module.privacy)
|
|
34
|
+
return [];
|
|
35
|
+
const provider = module.privacy.create(executor, services);
|
|
36
|
+
return [[provider.id, provider]];
|
|
37
|
+
}),
|
|
38
|
+
]);
|
|
10
39
|
}
|
|
11
40
|
export function describePrivacyProviders(providers) {
|
|
12
41
|
return [...providers.values()]
|
package/dist/shard-router.js
CHANGED
|
@@ -8,7 +8,7 @@ import { SHARD_CONTROL_TIMEOUT_MS, controlOperationGuildId, guildIdToShardId, sh
|
|
|
8
8
|
// deadline turned healthy leases into deterministic 503s. Keep the check
|
|
9
9
|
// bounded while allowing normal network variance.
|
|
10
10
|
const LEASE_VALIDATION_TIMEOUT_MS = 3_000;
|
|
11
|
-
const ECKO_RESULT_TIMEOUT_MS = AUTOMOD_SCANNER_LIMITS.
|
|
11
|
+
const ECKO_RESULT_TIMEOUT_MS = AUTOMOD_SCANNER_LIMITS.resultAcknowledgementSeconds * 1_000;
|
|
12
12
|
export class ShardControlRouter {
|
|
13
13
|
input;
|
|
14
14
|
constructor(input) {
|
|
@@ -30,7 +30,7 @@ export class ShardControlRouter {
|
|
|
30
30
|
}
|
|
31
31
|
/** Routes an authenticated Ecko result to the current guild owner. */
|
|
32
32
|
acceptHostedEckoResult(input) {
|
|
33
|
-
return this.#route({ kind: "automod.enhanced.result.accept", ...input },
|
|
33
|
+
return this.#route({ kind: "automod.enhanced.result.accept", ...input }, ECKO_RESULT_TIMEOUT_MS);
|
|
34
34
|
}
|
|
35
35
|
/** Reads the fenced leader's current Enhanced configuration snapshot. */
|
|
36
36
|
getHostedEckoConfigurationPage(input) {
|
|
@@ -336,17 +336,6 @@ async function executeLeaderOperation(input, operation) {
|
|
|
336
336
|
throw new BotControlTransportError(503, "The shard leader is temporarily unavailable");
|
|
337
337
|
return executePrivacy(input.privacy, operation);
|
|
338
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
|
-
}
|
|
350
339
|
async function executePrivacy(privacy, operation) {
|
|
351
340
|
if (operation.kind === "privacy.providers.list")
|
|
352
341
|
return (await privacy.listPrivacyProviders());
|
package/dist/shard-worker.js
CHANGED
|
@@ -223,6 +223,11 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
223
223
|
const serviceOptions = platformServiceOptions(environment, publicModules);
|
|
224
224
|
for (const [name, service] of createPlatformServices(database, publicModules, {
|
|
225
225
|
...serviceOptions,
|
|
226
|
+
recordRecoveryOwnership: {
|
|
227
|
+
mode: "shard",
|
|
228
|
+
shardId: identity.shardId,
|
|
229
|
+
shardCount: identity.shardCount,
|
|
230
|
+
},
|
|
226
231
|
activationServices: serviceAccess,
|
|
227
232
|
})) {
|
|
228
233
|
platformServices.set(name, service);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@helyx/bot",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Production Discord runtime for the modular Helyx platform.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"helyx",
|
|
@@ -53,11 +53,11 @@
|
|
|
53
53
|
"@helyx/config": ">=1.0.0 <2.0.0",
|
|
54
54
|
"@helyx/control-protocol": ">=0.5.0 <0.6.0",
|
|
55
55
|
"@helyx/core": ">=0.1.0 <0.2.0",
|
|
56
|
-
"@helyx/database": ">=2.
|
|
56
|
+
"@helyx/database": ">=2.3.0",
|
|
57
57
|
"@helyx/framework": ">=0.1.0 <0.2.0",
|
|
58
|
-
"@helyx/module-manifest": ">=0.
|
|
59
|
-
"@helyx/platform": ">=1.
|
|
60
|
-
"@helyx/sdk": ">=0.
|
|
58
|
+
"@helyx/module-manifest": ">=0.11.0 <0.12.0",
|
|
59
|
+
"@helyx/platform": ">=1.2.0",
|
|
60
|
+
"@helyx/sdk": ">=0.14.0",
|
|
61
61
|
"@helyx/status-client": ">=0.2.0 <0.3.0",
|
|
62
62
|
"@helyx/verification-contracts": ">=0.3.0 <0.4.0",
|
|
63
63
|
"discord.js": "14.27.0",
|
|
@@ -69,8 +69,8 @@
|
|
|
69
69
|
"zod": "4.4.3"
|
|
70
70
|
},
|
|
71
71
|
"peerDependencies": {
|
|
72
|
-
"@helyx/module-automod": ">=1.0.
|
|
73
|
-
"@helyx/module-moderation": ">=1.0.
|
|
72
|
+
"@helyx/module-automod": ">=1.0.2",
|
|
73
|
+
"@helyx/module-moderation": ">=1.0.2"
|
|
74
74
|
},
|
|
75
75
|
"peerDependenciesMeta": {
|
|
76
76
|
"@helyx/module-automod": {
|