@helyx/module-moderation 1.0.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 +41 -0
- package/LICENSE +725 -0
- package/README.md +107 -0
- package/dist/action-support.d.ts +48 -0
- package/dist/action-support.js +201 -0
- package/dist/case-actions.d.ts +9 -0
- package/dist/case-actions.js +311 -0
- package/dist/case-naming.d.ts +4 -0
- package/dist/case-naming.js +9 -0
- package/dist/case-repository.d.ts +29 -0
- package/dist/case-repository.js +199 -0
- package/dist/channel-actions.d.ts +3 -0
- package/dist/channel-actions.js +267 -0
- package/dist/commands.d.ts +4 -0
- package/dist/commands.js +309 -0
- package/dist/components.d.ts +15 -0
- package/dist/components.js +381 -0
- package/dist/configuration.d.ts +16 -0
- package/dist/configuration.js +94 -0
- package/dist/constants.d.ts +53 -0
- package/dist/constants.js +53 -0
- package/dist/contracts.d.ts +109 -0
- package/dist/contracts.js +84 -0
- package/dist/domain.d.ts +29 -0
- package/dist/domain.js +101 -0
- package/dist/events.d.ts +3 -0
- package/dist/events.js +60 -0
- package/dist/health.d.ts +10 -0
- package/dist/health.js +56 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +92 -0
- package/dist/moderation-cases-resource.d.ts +14 -0
- package/dist/moderation-cases-resource.js +226 -0
- package/dist/presentation.d.ts +8 -0
- package/dist/presentation.js +69 -0
- package/dist/privacy.d.ts +8 -0
- package/dist/privacy.js +38 -0
- package/dist/provider.d.ts +4 -0
- package/dist/provider.js +335 -0
- package/dist/receipt-repository.d.ts +9 -0
- package/dist/receipt-repository.js +36 -0
- package/dist/records.d.ts +399 -0
- package/dist/records.js +303 -0
- package/dist/repository-model.d.ts +131 -0
- package/dist/repository-model.js +318 -0
- package/dist/repository.d.ts +21 -0
- package/dist/repository.js +41 -0
- package/dist/service.d.ts +75 -0
- package/dist/service.js +329 -0
- package/dist/tasks.d.ts +49 -0
- package/dist/tasks.js +391 -0
- package/dist/thread-controls.d.ts +23 -0
- package/dist/thread-controls.js +376 -0
- package/dist/thread-deletion-recovery.d.ts +13 -0
- package/dist/thread-deletion-recovery.js +46 -0
- package/dist/thread-delivery.d.ts +11 -0
- package/dist/thread-delivery.js +427 -0
- package/dist/thread-reconciliation.d.ts +24 -0
- package/dist/thread-reconciliation.js +181 -0
- package/dist/thread-repository.d.ts +16 -0
- package/dist/thread-repository.js +70 -0
- package/manifest.json +999 -0
- package/migrations/0001_moderation_foundation.sql +552 -0
- package/migrations/0002_staff_attempt_parameters.sql +27 -0
- package/package.json +56 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { MODERATION_COMMAND_IDS } from "./constants.js";
|
|
2
|
+
export type ModerationCommandId = (typeof MODERATION_COMMAND_IDS)[keyof typeof MODERATION_COMMAND_IDS];
|
|
3
|
+
export type ModerationCaseAction = "warn" | "timeout" | "timeout_remove" | "kick" | "ban" | "demote" | "unban" | "delete_message" | "case_only";
|
|
4
|
+
export type ModerationCaseSource = "staff_command" | "auto_moderation";
|
|
5
|
+
export type ModerationCaseState = "pending" | "applied" | "failed" | "review" | "rejected" | "corrected" | "void";
|
|
6
|
+
export type ModerationAttemptOutcome = "pending" | "succeeded" | "already_applied" | "failed" | "ambiguous" | "rejected" | "no_longer_required" | "member_missing" | "missing_permission" | "hierarchy_blocked" | "review_required";
|
|
7
|
+
export type ModerationDeliveryState = "not_requested" | "pending" | "succeeded" | "failed" | "retry_pending" | "review_required";
|
|
8
|
+
export declare const moderationDefaultPermissions: {
|
|
9
|
+
readonly module: {
|
|
10
|
+
readonly everyone: true;
|
|
11
|
+
readonly requiredPermissions: readonly [];
|
|
12
|
+
readonly allowedRoleIds: readonly [];
|
|
13
|
+
readonly deniedRoleIds: readonly [];
|
|
14
|
+
readonly allowedUserIds: readonly [];
|
|
15
|
+
readonly deniedUserIds: readonly [];
|
|
16
|
+
};
|
|
17
|
+
readonly commands: {
|
|
18
|
+
readonly "moderation.cases.read": {
|
|
19
|
+
readonly everyone: false;
|
|
20
|
+
readonly accessLevel: "administrator";
|
|
21
|
+
readonly requiredPermissions: readonly [];
|
|
22
|
+
readonly allowedRoleIds: readonly [];
|
|
23
|
+
readonly deniedRoleIds: readonly [];
|
|
24
|
+
readonly allowedUserIds: readonly [];
|
|
25
|
+
readonly deniedUserIds: readonly [];
|
|
26
|
+
};
|
|
27
|
+
readonly "moderation.cases.reconcile": {
|
|
28
|
+
readonly everyone: false;
|
|
29
|
+
readonly accessLevel: "administrator";
|
|
30
|
+
readonly requiredPermissions: readonly [];
|
|
31
|
+
readonly allowedRoleIds: readonly [];
|
|
32
|
+
readonly deniedRoleIds: readonly [];
|
|
33
|
+
readonly allowedUserIds: readonly [];
|
|
34
|
+
readonly deniedUserIds: readonly [];
|
|
35
|
+
};
|
|
36
|
+
readonly "moderation.cases.correct": {
|
|
37
|
+
readonly everyone: false;
|
|
38
|
+
readonly accessLevel: "administrator";
|
|
39
|
+
readonly requiredPermissions: readonly [];
|
|
40
|
+
readonly allowedRoleIds: readonly [];
|
|
41
|
+
readonly deniedRoleIds: readonly [];
|
|
42
|
+
readonly allowedUserIds: readonly [];
|
|
43
|
+
readonly deniedUserIds: readonly [];
|
|
44
|
+
};
|
|
45
|
+
readonly "moderation.cases.disassociate": {
|
|
46
|
+
readonly everyone: false;
|
|
47
|
+
readonly accessLevel: "administrator";
|
|
48
|
+
readonly requiredPermissions: readonly [];
|
|
49
|
+
readonly allowedRoleIds: readonly [];
|
|
50
|
+
readonly deniedRoleIds: readonly [];
|
|
51
|
+
readonly allowedUserIds: readonly [];
|
|
52
|
+
readonly deniedUserIds: readonly [];
|
|
53
|
+
};
|
|
54
|
+
readonly "moderation.private-notes": {
|
|
55
|
+
readonly everyone: false;
|
|
56
|
+
readonly accessLevel: "administrator";
|
|
57
|
+
readonly requiredPermissions: readonly [];
|
|
58
|
+
readonly allowedRoleIds: readonly [];
|
|
59
|
+
readonly deniedRoleIds: readonly [];
|
|
60
|
+
readonly allowedUserIds: readonly [];
|
|
61
|
+
readonly deniedUserIds: readonly [];
|
|
62
|
+
};
|
|
63
|
+
readonly "moderation.threads.act": {
|
|
64
|
+
readonly everyone: false;
|
|
65
|
+
readonly accessLevel: "administrator";
|
|
66
|
+
readonly requiredPermissions: readonly [];
|
|
67
|
+
readonly allowedRoleIds: readonly [];
|
|
68
|
+
readonly deniedRoleIds: readonly [];
|
|
69
|
+
readonly allowedUserIds: readonly [];
|
|
70
|
+
readonly deniedUserIds: readonly [];
|
|
71
|
+
};
|
|
72
|
+
readonly "moderation.threads.close": {
|
|
73
|
+
readonly everyone: false;
|
|
74
|
+
readonly accessLevel: "administrator";
|
|
75
|
+
readonly requiredPermissions: readonly [];
|
|
76
|
+
readonly allowedRoleIds: readonly [];
|
|
77
|
+
readonly deniedRoleIds: readonly [];
|
|
78
|
+
readonly allowedUserIds: readonly [];
|
|
79
|
+
readonly deniedUserIds: readonly [];
|
|
80
|
+
};
|
|
81
|
+
readonly "moderation.threads.close-delete": {
|
|
82
|
+
readonly everyone: false;
|
|
83
|
+
readonly accessLevel: "administrator";
|
|
84
|
+
readonly requiredPermissions: readonly [];
|
|
85
|
+
readonly allowedRoleIds: readonly [];
|
|
86
|
+
readonly deniedRoleIds: readonly [];
|
|
87
|
+
readonly allowedUserIds: readonly [];
|
|
88
|
+
readonly deniedUserIds: readonly [];
|
|
89
|
+
};
|
|
90
|
+
readonly "moderation.settings.manage": {
|
|
91
|
+
readonly everyone: false;
|
|
92
|
+
readonly accessLevel: "administrator";
|
|
93
|
+
readonly requiredPermissions: readonly [];
|
|
94
|
+
readonly allowedRoleIds: readonly [];
|
|
95
|
+
readonly deniedRoleIds: readonly [];
|
|
96
|
+
readonly allowedUserIds: readonly [];
|
|
97
|
+
readonly deniedUserIds: readonly [];
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
export declare const moderationLogging: {
|
|
102
|
+
events: {
|
|
103
|
+
id: string;
|
|
104
|
+
name: string;
|
|
105
|
+
description: string;
|
|
106
|
+
defaultEnabled: false;
|
|
107
|
+
}[];
|
|
108
|
+
};
|
|
109
|
+
//# sourceMappingURL=contracts.d.ts.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { MODERATION_CASE_ACTIONS, MODERATION_COMMAND_IDS, } from "./constants.js";
|
|
2
|
+
const administrators = {
|
|
3
|
+
everyone: false,
|
|
4
|
+
accessLevel: "administrator",
|
|
5
|
+
requiredPermissions: [],
|
|
6
|
+
allowedRoleIds: [],
|
|
7
|
+
deniedRoleIds: [],
|
|
8
|
+
allowedUserIds: [],
|
|
9
|
+
deniedUserIds: [],
|
|
10
|
+
};
|
|
11
|
+
const everyone = {
|
|
12
|
+
everyone: true,
|
|
13
|
+
requiredPermissions: [],
|
|
14
|
+
allowedRoleIds: [],
|
|
15
|
+
deniedRoleIds: [],
|
|
16
|
+
allowedUserIds: [],
|
|
17
|
+
deniedUserIds: [],
|
|
18
|
+
};
|
|
19
|
+
export const moderationDefaultPermissions = {
|
|
20
|
+
module: everyone,
|
|
21
|
+
commands: {
|
|
22
|
+
...Object.fromEntries(Object.values(MODERATION_COMMAND_IDS).map((id) => [id, administrators])),
|
|
23
|
+
...Object.fromEntries(Object.values(MODERATION_CASE_ACTIONS).map((id) => [id, administrators])),
|
|
24
|
+
"moderation.cases.read": administrators,
|
|
25
|
+
"moderation.cases.reconcile": administrators,
|
|
26
|
+
"moderation.cases.correct": administrators,
|
|
27
|
+
"moderation.cases.disassociate": administrators,
|
|
28
|
+
"moderation.private-notes": administrators,
|
|
29
|
+
"moderation.threads.act": administrators,
|
|
30
|
+
"moderation.threads.close": administrators,
|
|
31
|
+
"moderation.threads.close-delete": administrators,
|
|
32
|
+
"moderation.settings.manage": administrators,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
export const moderationLogging = {
|
|
36
|
+
events: [
|
|
37
|
+
[
|
|
38
|
+
"member-warned",
|
|
39
|
+
"Member warned",
|
|
40
|
+
"Records a warning without copying its reason or private note.",
|
|
41
|
+
],
|
|
42
|
+
[
|
|
43
|
+
"member-timed-out",
|
|
44
|
+
"Member timed out",
|
|
45
|
+
"Records a timeout using case and duration metadata.",
|
|
46
|
+
],
|
|
47
|
+
[
|
|
48
|
+
"member-kicked",
|
|
49
|
+
"Member kicked",
|
|
50
|
+
"Records a kick using case identifiers only.",
|
|
51
|
+
],
|
|
52
|
+
[
|
|
53
|
+
"member-banned",
|
|
54
|
+
"Member banned",
|
|
55
|
+
"Records a ban using case identifiers and bounded deletion duration.",
|
|
56
|
+
],
|
|
57
|
+
[
|
|
58
|
+
"member-demoted",
|
|
59
|
+
"Member demoted",
|
|
60
|
+
"Records a demotion using case and role identifiers.",
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
"member-unbanned",
|
|
64
|
+
"Member unbanned",
|
|
65
|
+
"Records an unban using case identifiers only.",
|
|
66
|
+
],
|
|
67
|
+
[
|
|
68
|
+
"member-unmuted",
|
|
69
|
+
"Member unmuted",
|
|
70
|
+
"Records timeout removal using case identifiers only.",
|
|
71
|
+
],
|
|
72
|
+
[
|
|
73
|
+
"case-disassociated",
|
|
74
|
+
"Case user disassociated",
|
|
75
|
+
"Records that a case was detached from its user without retaining the former user identifier.",
|
|
76
|
+
],
|
|
77
|
+
].map(([id, name, description]) => ({
|
|
78
|
+
id: id,
|
|
79
|
+
name: name,
|
|
80
|
+
description: description,
|
|
81
|
+
defaultEnabled: false,
|
|
82
|
+
})),
|
|
83
|
+
};
|
|
84
|
+
//# sourceMappingURL=contracts.js.map
|
package/dist/domain.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ModerationBanDeleteMessageSeconds } from "@helyx/sdk";
|
|
2
|
+
import type { ModerationAttemptOutcome, ModerationCaseState, ModerationDeliveryState } from "./contracts.js";
|
|
3
|
+
export interface ModerationCaseStateSnapshot {
|
|
4
|
+
state: ModerationCaseState;
|
|
5
|
+
revision: number;
|
|
6
|
+
safeOutcome: string | null;
|
|
7
|
+
settledAt: Date | null;
|
|
8
|
+
disassociatedAt: Date | null;
|
|
9
|
+
}
|
|
10
|
+
export declare function validateReason(input: unknown): string;
|
|
11
|
+
export declare function validatePrivateNote(input: unknown): string | undefined;
|
|
12
|
+
export declare function validateTimeoutSeconds(input: unknown): number;
|
|
13
|
+
export declare function validateBanDeleteMessageSeconds(input: unknown): ModerationBanDeleteMessageSeconds;
|
|
14
|
+
export declare function validateDemotionRoleIds(input: unknown): string[];
|
|
15
|
+
export declare function validateEvidenceReference(input: unknown): {
|
|
16
|
+
channelId: string;
|
|
17
|
+
messageId: string;
|
|
18
|
+
} | undefined;
|
|
19
|
+
export declare function caseOutcomeState(outcome: ModerationAttemptOutcome): Extract<ModerationCaseState, "pending" | "applied" | "failed" | "review" | "rejected">;
|
|
20
|
+
export declare function transitionCase(current: ModerationCaseStateSnapshot, input: {
|
|
21
|
+
outcome: ModerationAttemptOutcome;
|
|
22
|
+
safeCode?: string;
|
|
23
|
+
at: Date;
|
|
24
|
+
}): ModerationCaseStateSnapshot;
|
|
25
|
+
export declare function canReconcileCase(state: ModerationCaseState): boolean;
|
|
26
|
+
export declare function canCorrectCase(state: ModerationCaseState): boolean;
|
|
27
|
+
export declare function disassociateCaseDomain(current: ModerationCaseStateSnapshot, at: Date): ModerationCaseStateSnapshot;
|
|
28
|
+
export declare function threadDeliveryStateForAttempt(attempt: number, attemptLimit: number, retryable: boolean): ModerationDeliveryState;
|
|
29
|
+
//# sourceMappingURL=domain.d.ts.map
|
package/dist/domain.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { MODERATION_LIMITS } from "./constants.js";
|
|
2
|
+
export function validateReason(input) {
|
|
3
|
+
if (typeof input !== "string")
|
|
4
|
+
throw new Error("A reason is required.");
|
|
5
|
+
const value = input.trim();
|
|
6
|
+
if (value.length < MODERATION_LIMITS.reasonMinCharacters ||
|
|
7
|
+
value.length > MODERATION_LIMITS.reasonMaxCharacters)
|
|
8
|
+
throw new Error("The reason must be between 10 and 500 characters.");
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
export function validatePrivateNote(input) {
|
|
12
|
+
if (input === undefined || input === null || input === "")
|
|
13
|
+
return undefined;
|
|
14
|
+
if (typeof input !== "string")
|
|
15
|
+
throw new Error("The private note is invalid.");
|
|
16
|
+
const value = input.trim();
|
|
17
|
+
if (!value || value.length > MODERATION_LIMITS.privateNoteMaxCharacters)
|
|
18
|
+
throw new Error("The private note must be no more than 1,000 characters.");
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
export function validateTimeoutSeconds(input) {
|
|
22
|
+
if (!Number.isInteger(input) ||
|
|
23
|
+
input < MODERATION_LIMITS.timeoutMinimumSeconds ||
|
|
24
|
+
input > MODERATION_LIMITS.timeoutMaximumSeconds)
|
|
25
|
+
throw new Error("Timeout duration must be between 1 minute and 28 days.");
|
|
26
|
+
return input;
|
|
27
|
+
}
|
|
28
|
+
const BAN_DELETE_MESSAGE_SECONDS = new Set([
|
|
29
|
+
0, 3_600, 21_600, 86_400, 259_200, 604_800,
|
|
30
|
+
]);
|
|
31
|
+
export function validateBanDeleteMessageSeconds(input) {
|
|
32
|
+
if (!Number.isInteger(input) ||
|
|
33
|
+
!BAN_DELETE_MESSAGE_SECONDS.has(input))
|
|
34
|
+
throw new Error("The ban message deletion duration is invalid.");
|
|
35
|
+
return input;
|
|
36
|
+
}
|
|
37
|
+
export function validateDemotionRoleIds(input) {
|
|
38
|
+
if (!Array.isArray(input) ||
|
|
39
|
+
input.length < 1 ||
|
|
40
|
+
input.length > MODERATION_LIMITS.demotionRoles ||
|
|
41
|
+
input.some((roleId) => typeof roleId !== "string" || !/^\d{17,20}$/u.test(roleId)))
|
|
42
|
+
throw new Error("Choose between 1 and 10 valid roles to remove.");
|
|
43
|
+
const unique = [...new Set(input)];
|
|
44
|
+
if (unique.length !== input.length)
|
|
45
|
+
throw new Error("Demotion roles must be unique.");
|
|
46
|
+
return unique;
|
|
47
|
+
}
|
|
48
|
+
export function validateEvidenceReference(input) {
|
|
49
|
+
if (input === undefined || input === null || input === "")
|
|
50
|
+
return undefined;
|
|
51
|
+
if (typeof input !== "string")
|
|
52
|
+
throw new Error("The message link is invalid.");
|
|
53
|
+
const match = /^https:\/\/discord(?:app)?\.com\/channels\/(\d{17,20})\/(\d{17,20})\/(\d{17,20})$/u.exec(input.trim());
|
|
54
|
+
if (!match?.[2] || !match[3])
|
|
55
|
+
throw new Error("The message link is invalid.");
|
|
56
|
+
return { channelId: match[2], messageId: match[3] };
|
|
57
|
+
}
|
|
58
|
+
export function caseOutcomeState(outcome) {
|
|
59
|
+
if (outcome === "pending")
|
|
60
|
+
return "pending";
|
|
61
|
+
if (outcome === "succeeded" ||
|
|
62
|
+
outcome === "already_applied" ||
|
|
63
|
+
outcome === "no_longer_required")
|
|
64
|
+
return "applied";
|
|
65
|
+
if (outcome === "ambiguous")
|
|
66
|
+
return "review";
|
|
67
|
+
if (outcome === "review_required")
|
|
68
|
+
return "review";
|
|
69
|
+
if (outcome === "rejected")
|
|
70
|
+
return "rejected";
|
|
71
|
+
return "failed";
|
|
72
|
+
}
|
|
73
|
+
export function transitionCase(current, input) {
|
|
74
|
+
if (current.state !== "pending" && current.state !== "review")
|
|
75
|
+
throw new Error("This moderation case cannot transition from its current state.");
|
|
76
|
+
const state = caseOutcomeState(input.outcome);
|
|
77
|
+
return {
|
|
78
|
+
...current,
|
|
79
|
+
state,
|
|
80
|
+
revision: current.revision + 1,
|
|
81
|
+
safeOutcome: input.safeCode ?? input.outcome,
|
|
82
|
+
settledAt: state === "pending" || state === "review" ? null : input.at,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export function canReconcileCase(state) {
|
|
86
|
+
return state === "review" || state === "failed";
|
|
87
|
+
}
|
|
88
|
+
export function canCorrectCase(state) {
|
|
89
|
+
return state !== "pending" && state !== "corrected" && state !== "void";
|
|
90
|
+
}
|
|
91
|
+
export function disassociateCaseDomain(current, at) {
|
|
92
|
+
if (current.disassociatedAt)
|
|
93
|
+
return current;
|
|
94
|
+
return { ...current, revision: current.revision + 1, disassociatedAt: at };
|
|
95
|
+
}
|
|
96
|
+
export function threadDeliveryStateForAttempt(attempt, attemptLimit, retryable) {
|
|
97
|
+
if (!retryable)
|
|
98
|
+
return "review_required";
|
|
99
|
+
return attempt < attemptLimit ? "retry_pending" : "review_required";
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=domain.js.map
|
package/dist/events.d.ts
ADDED
package/dist/events.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { HELYX_SERVICE_NAMES, } from "@helyx/sdk";
|
|
2
|
+
import { MODERATION_MODULE_ID, MODERATION_TASK_KINDS } from "./constants.js";
|
|
3
|
+
import { DEFAULT_MODERATION_CONFIGURATION, parseModerationConfiguration, } from "./configuration.js";
|
|
4
|
+
export function createModerationEvents() {
|
|
5
|
+
return {
|
|
6
|
+
channelDelete: [
|
|
7
|
+
{
|
|
8
|
+
type: "discord.channel-delete",
|
|
9
|
+
execute: (context) => signalStructuralReconciliation(context.services, context.serverId, "channel", context.channelId),
|
|
10
|
+
},
|
|
11
|
+
],
|
|
12
|
+
channelUpdate: [
|
|
13
|
+
{
|
|
14
|
+
type: "discord.channel-update",
|
|
15
|
+
execute: (context) => signalStructuralReconciliation(context.services, context.serverId, "channel", context.channelId),
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
guildRoleDelete: [
|
|
19
|
+
{
|
|
20
|
+
type: "discord.guild-role-delete",
|
|
21
|
+
execute: (context) => signalStructuralReconciliation(context.services, context.serverId, "role", context.roleId),
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
guildRoleUpdate: [
|
|
25
|
+
{
|
|
26
|
+
type: "discord.guild-role-update",
|
|
27
|
+
execute: (context) => signalStructuralReconciliation(context.services, context.serverId, "role", context.roleId),
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
async function signalStructuralReconciliation(services, guildId, kind, resourceId) {
|
|
33
|
+
if (!services.has(HELYX_SERVICE_NAMES.scheduledTasks))
|
|
34
|
+
return;
|
|
35
|
+
const configuration = services.has(HELYX_SERVICE_NAMES.configuration)
|
|
36
|
+
? await services
|
|
37
|
+
.get(HELYX_SERVICE_NAMES.configuration)
|
|
38
|
+
.get(guildId, MODERATION_MODULE_ID)
|
|
39
|
+
: null;
|
|
40
|
+
const settings = configuration
|
|
41
|
+
? parseModerationConfiguration(configuration.value)
|
|
42
|
+
: DEFAULT_MODERATION_CONFIGURATION;
|
|
43
|
+
const relevant = kind === "channel"
|
|
44
|
+
? settings.violationThreadChannelId === resourceId
|
|
45
|
+
: settings.violationAccessRoleId === resourceId ||
|
|
46
|
+
settings.violationModeratorRoleIds.includes(resourceId);
|
|
47
|
+
if (!relevant)
|
|
48
|
+
return;
|
|
49
|
+
await services
|
|
50
|
+
.get(HELYX_SERVICE_NAMES.scheduledTasks)
|
|
51
|
+
.signal({
|
|
52
|
+
moduleId: MODERATION_MODULE_ID,
|
|
53
|
+
taskKind: MODERATION_TASK_KINDS.threadAccessReconcile,
|
|
54
|
+
guildId,
|
|
55
|
+
coalesceKey: `guild:${guildId}`,
|
|
56
|
+
payload: {},
|
|
57
|
+
mode: "normal",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=events.js.map
|
package/dist/health.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HealthResult, RuntimeContext } from "@helyx/sdk";
|
|
2
|
+
export declare class ModerationHealth {
|
|
3
|
+
#private;
|
|
4
|
+
start(context: RuntimeContext): void;
|
|
5
|
+
stop(): void;
|
|
6
|
+
recordTaskSuccess(taskKind: string): void;
|
|
7
|
+
recordTaskFailure(taskKind: string, safeCode: string): void;
|
|
8
|
+
snapshot(): HealthResult;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=health.d.ts.map
|
package/dist/health.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { HELYX_SERVICE_NAMES } from "@helyx/sdk";
|
|
2
|
+
const REQUIRED_SERVICES = [
|
|
3
|
+
HELYX_SERVICE_NAMES.records,
|
|
4
|
+
HELYX_SERVICE_NAMES.atomicModerationCases,
|
|
5
|
+
HELYX_SERVICE_NAMES.memberEnforcement,
|
|
6
|
+
HELYX_SERVICE_NAMES.memberRoleGrants,
|
|
7
|
+
HELYX_SERVICE_NAMES.messageEnforcement,
|
|
8
|
+
HELYX_SERVICE_NAMES.violationThreads,
|
|
9
|
+
HELYX_SERVICE_NAMES.scheduledTasks,
|
|
10
|
+
HELYX_SERVICE_NAMES.sessions,
|
|
11
|
+
HELYX_SERVICE_NAMES.secureRandom,
|
|
12
|
+
HELYX_SERVICE_NAMES.configuration,
|
|
13
|
+
HELYX_SERVICE_NAMES.audit,
|
|
14
|
+
HELYX_SERVICE_NAMES.resourceAuthorization,
|
|
15
|
+
];
|
|
16
|
+
export class ModerationHealth {
|
|
17
|
+
#running = false;
|
|
18
|
+
#missingServices = [];
|
|
19
|
+
#recoveryFailures = new Map();
|
|
20
|
+
start(context) {
|
|
21
|
+
this.#running = true;
|
|
22
|
+
this.#missingServices = REQUIRED_SERVICES.filter((service) => !context.services.has(service));
|
|
23
|
+
this.#recoveryFailures.clear();
|
|
24
|
+
}
|
|
25
|
+
stop() {
|
|
26
|
+
this.#running = false;
|
|
27
|
+
this.#missingServices = [];
|
|
28
|
+
this.#recoveryFailures.clear();
|
|
29
|
+
}
|
|
30
|
+
recordTaskSuccess(taskKind) {
|
|
31
|
+
this.#recoveryFailures.delete(taskKind);
|
|
32
|
+
}
|
|
33
|
+
recordTaskFailure(taskKind, safeCode) {
|
|
34
|
+
this.#recoveryFailures.set(taskKind, safeCode);
|
|
35
|
+
}
|
|
36
|
+
snapshot() {
|
|
37
|
+
if (!this.#running)
|
|
38
|
+
return { status: "unhealthy", message: "Moderation is stopped" };
|
|
39
|
+
if (this.#missingServices.length > 0)
|
|
40
|
+
return {
|
|
41
|
+
status: "unhealthy",
|
|
42
|
+
message: "Moderation runtime services are unavailable",
|
|
43
|
+
};
|
|
44
|
+
const safeCode = this.#recoveryFailures.values().next().value;
|
|
45
|
+
if (safeCode)
|
|
46
|
+
return {
|
|
47
|
+
status: "degraded",
|
|
48
|
+
message: `Moderation recovery requires attention (${safeCode})`,
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
status: "healthy",
|
|
52
|
+
message: "Moderation case and recovery workflows are registered",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=health.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const moderationModule: import("@helyx/sdk").ModuleDefinition;
|
|
2
|
+
export default moderationModule;
|
|
3
|
+
export * from "./configuration.js";
|
|
4
|
+
export * from "./constants.js";
|
|
5
|
+
export * from "./contracts.js";
|
|
6
|
+
export * from "./domain.js";
|
|
7
|
+
export * from "./records.js";
|
|
8
|
+
export * from "./repository.js";
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { parseModuleManifest } from "@helyx/module-manifest";
|
|
3
|
+
import { defineModule, getModulePrivacyDeclaration } from "@helyx/sdk";
|
|
4
|
+
import { createModerationChannelActions } from "./channel-actions.js";
|
|
5
|
+
import { createModerationCommands } from "./commands.js";
|
|
6
|
+
import { createModerationInteractions } from "./components.js";
|
|
7
|
+
import { parseModerationConfiguration } from "./configuration.js";
|
|
8
|
+
import { moderationDefaultPermissions, moderationLogging, } from "./contracts.js";
|
|
9
|
+
import { createModerationEvents } from "./events.js";
|
|
10
|
+
import { ModerationHealth } from "./health.js";
|
|
11
|
+
import { createModerationCasesResource } from "./moderation-cases-resource.js";
|
|
12
|
+
import { createModerationPrivacyProvider } from "./privacy.js";
|
|
13
|
+
import { createModerationActionProvider } from "./provider.js";
|
|
14
|
+
import { moderationRecords } from "./records.js";
|
|
15
|
+
import { ModerationService } from "./service.js";
|
|
16
|
+
import { ModerationCaseActions } from "./case-actions.js";
|
|
17
|
+
import { createModerationTasks, reconcileDashboardCase, reconcileModerationAction, } from "./tasks.js";
|
|
18
|
+
import { ModerationThreadControls } from "./thread-controls.js";
|
|
19
|
+
import { ModerationThreadDelivery } from "./thread-delivery.js";
|
|
20
|
+
const manifest = parseModuleManifest(JSON.parse(readFileSync(new URL("../manifest.json", import.meta.url), "utf8")));
|
|
21
|
+
const service = new ModerationService();
|
|
22
|
+
const threadDelivery = new ModerationThreadDelivery();
|
|
23
|
+
const threadControls = new ModerationThreadControls(service);
|
|
24
|
+
const health = new ModerationHealth();
|
|
25
|
+
service.attachThreadRuntime(threadDelivery);
|
|
26
|
+
const componentInteractions = createModerationInteractions(service, threadControls);
|
|
27
|
+
const tasks = createModerationTasks({
|
|
28
|
+
deliverPendingThread: (services, input) => threadDelivery.deliverPending(services, input),
|
|
29
|
+
reconcileAction: (services, input) => reconcileModerationAction(services, input),
|
|
30
|
+
reconcileThreadAccess: (services, input) => threadDelivery.reconcileAccess(services, input.guildId),
|
|
31
|
+
}, health);
|
|
32
|
+
export const moderationModule = defineModule({
|
|
33
|
+
manifest,
|
|
34
|
+
configuration: {
|
|
35
|
+
schemaVersion: 1,
|
|
36
|
+
validate: (input) => ({ ...parseModerationConfiguration(input) }),
|
|
37
|
+
},
|
|
38
|
+
defaultPermissions: moderationDefaultPermissions,
|
|
39
|
+
permissionOperations: Object.entries(moderationDefaultPermissions.commands)
|
|
40
|
+
.filter(([id]) => id.startsWith("moderation.cases.") ||
|
|
41
|
+
id.startsWith("moderation.threads.") ||
|
|
42
|
+
id === "moderation.private-notes" ||
|
|
43
|
+
id === "moderation.settings.manage")
|
|
44
|
+
.map(([id]) => ({ id, name: id, description: `Authorises ${id}.` })),
|
|
45
|
+
records: moderationRecords,
|
|
46
|
+
interactions: {
|
|
47
|
+
commands: createModerationCommands(service),
|
|
48
|
+
buttons: componentInteractions.buttons ?? [],
|
|
49
|
+
...(componentInteractions.modals
|
|
50
|
+
? { modals: componentInteractions.modals }
|
|
51
|
+
: {}),
|
|
52
|
+
...(componentInteractions.selects
|
|
53
|
+
? { selects: componentInteractions.selects }
|
|
54
|
+
: {}),
|
|
55
|
+
},
|
|
56
|
+
events: createModerationEvents(),
|
|
57
|
+
logging: moderationLogging,
|
|
58
|
+
privacy: {
|
|
59
|
+
providerVersion: 1,
|
|
60
|
+
create: (executor, services) => createModerationPrivacyProvider({
|
|
61
|
+
id: manifest.id,
|
|
62
|
+
declaration: getModulePrivacyDeclaration(manifest),
|
|
63
|
+
executor,
|
|
64
|
+
...(services ? { services } : {}),
|
|
65
|
+
}),
|
|
66
|
+
},
|
|
67
|
+
scheduledTasks: tasks,
|
|
68
|
+
managedResources: [
|
|
69
|
+
createModerationCasesResource({ reconcileCase: reconcileDashboardCase }, new ModerationCaseActions(service)),
|
|
70
|
+
],
|
|
71
|
+
dashboardActions: createModerationChannelActions(),
|
|
72
|
+
moderationActionProvider: createModerationActionProvider(service),
|
|
73
|
+
start(context) {
|
|
74
|
+
health.start(context);
|
|
75
|
+
context.logger.info({ event: "moderation.started" }, "Moderation module started");
|
|
76
|
+
return Promise.resolve();
|
|
77
|
+
},
|
|
78
|
+
stop(context) {
|
|
79
|
+
health.stop();
|
|
80
|
+
context.logger.info({ event: "moderation.stopped" }, "Moderation module stopped");
|
|
81
|
+
return Promise.resolve();
|
|
82
|
+
},
|
|
83
|
+
health: () => health.snapshot(),
|
|
84
|
+
});
|
|
85
|
+
export default moderationModule;
|
|
86
|
+
export * from "./configuration.js";
|
|
87
|
+
export * from "./constants.js";
|
|
88
|
+
export * from "./contracts.js";
|
|
89
|
+
export * from "./domain.js";
|
|
90
|
+
export * from "./records.js";
|
|
91
|
+
export * from "./repository.js";
|
|
92
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ModuleManagedResource, type ServiceAccess } from "@helyx/sdk";
|
|
2
|
+
import type { ModerationCaseActions } from "./case-actions.js";
|
|
3
|
+
export interface ModerationCaseResourceReconciler {
|
|
4
|
+
reconcileCase(input: {
|
|
5
|
+
guildId: string;
|
|
6
|
+
caseId: string;
|
|
7
|
+
expectedRevision: number;
|
|
8
|
+
actorUserId: string;
|
|
9
|
+
operationKey: string;
|
|
10
|
+
services: ServiceAccess;
|
|
11
|
+
}): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export declare function createModerationCasesResource(reconciler: ModerationCaseResourceReconciler, caseActions?: Pick<ModerationCaseActions, "execute">): ModuleManagedResource;
|
|
14
|
+
//# sourceMappingURL=moderation-cases-resource.d.ts.map
|