@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
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# @helyx/module-moderation
|
|
2
|
+
|
|
3
|
+
Moderation lets authorised Discord staff apply accountable member actions, keep durable case history, and recover safely from ambiguous Discord outcomes.
|
|
4
|
+
|
|
5
|
+
## Availability and deployment
|
|
6
|
+
|
|
7
|
+
Moderation is a Live module distributed under HSAL-1.0. It supports Hosted and Self-Hosted Helyx installations, requires no third-party credentials, and does not depend on Auto Moderation.
|
|
8
|
+
|
|
9
|
+
Package installation does not enable the module for a server.
|
|
10
|
+
|
|
11
|
+
## What it does
|
|
12
|
+
|
|
13
|
+
- Creates an immutable, server-numbered case before each Discord mutation.
|
|
14
|
+
- Supports warnings, timeouts, timeout removal, kicks, bans, configured-role demotion, unbans, and bounded member history.
|
|
15
|
+
- Records typed action attempts so succeeded, failed, ambiguous, and review-required outcomes can be distinguished without raw Discord errors.
|
|
16
|
+
- Opens optional user-visible private violation threads and keeps their evidence delivery outside the punitive-action transaction.
|
|
17
|
+
- Lets authorised dashboard users reconcile, correct, disassociate, or reveal a private note without rewriting the original action.
|
|
18
|
+
|
|
19
|
+
Moderation does not classify messages, own keyword rules, or scan content. Auto Moderation can use its closed action provider when both modules are enabled, but each remains independently useful.
|
|
20
|
+
|
|
21
|
+
## Commands and interactions
|
|
22
|
+
|
|
23
|
+
| Command path | Default access | Outcome |
|
|
24
|
+
| --------------------- | -------------- | -------------------------------------------------------------- |
|
|
25
|
+
| `/mod warn` | Administrators | Records a warning and optionally attempts a direct message. |
|
|
26
|
+
| `/mod timeout` | Administrators | Applies a timeout from one minute to 28 days. |
|
|
27
|
+
| `/mod timeout-remove` | Administrators | Removes an active timeout. |
|
|
28
|
+
| `/mod kick` | Administrators | Removes the member from the server. |
|
|
29
|
+
| `/mod ban` | Administrators | Bans the member with a bounded message-deletion choice. |
|
|
30
|
+
| `/mod demote` | Administrators | Removes every eligible configured role in a list of up to ten. |
|
|
31
|
+
| `/mod unban` | Administrators | Removes a ban by user ID. |
|
|
32
|
+
| `/mod history` | Administrators | Shows a bounded case history privately. |
|
|
33
|
+
|
|
34
|
+
All commands are enabled-guild, server-only commands with deferred ephemeral acknowledgement and one unique `moderation.*` permission identity per executable path. Staff actions require a trimmed 10–500 character reason. An optional same-server message link stores channel and message IDs only; an optional private note is limited to 1,000 characters.
|
|
35
|
+
|
|
36
|
+
Kick, ban, demotion, unban, and timeouts longer than 24 hours require a 60-second actor/server/revision-bound confirmation with an explicit cancel path. Before dispatch, Helyx rechecks current module access, staff authority, target state, role hierarchy, and its own Discord permissions.
|
|
37
|
+
|
|
38
|
+
Violation-thread controls are Kick, Ban, Unmute when currently timed out, and No Further Action. Close and the separately permission-checked Close and delete appear only after one of those staff resolutions succeeds. Close archives and locks the thread; Close and delete additionally permanently deletes that case's Discord thread and messages, not its moderation records. Each control resolves a durable opaque case token, then fresh-authorises the clicking staff member and rechecks the case revision.
|
|
39
|
+
|
|
40
|
+
## Dashboard configuration
|
|
41
|
+
|
|
42
|
+
**Case name prefix** controls new violation-thread names. Helyx appends the case number, padded to at least three digits (for example, `moderation-case-001` or `violation-012`), without an operation-hash suffix. Existing thread names are not changed when the prefix is edited. The default applies to older saved configurations automatically.
|
|
43
|
+
|
|
44
|
+
Settings cover the optional warning direct message, 30–730 day settled-case retention, the violation-thread parent and access role, up to ten moderator notification roles and ten individual notification users, and an opening message. The channel, access role and case prefix share one half-width group; staff role/user notifications occupy the other, with mobile stacking and the opening message below. Enter each staff member's Discord user ID and select **Add user**. Current human server membership is checked before settings are saved; an empty list is valid. Existing configurations default to no individual notifications.
|
|
45
|
+
|
|
46
|
+
Individual notification users are mentioned as staff and invited to new private threads. **Check channel** previews their minimum parent access; **Configure channel** can apply View Channel, Read Message History and Send Messages in Threads member overwrites, never Manage Threads or moderation powers. Notification selection does not grant Helyx action permissions. Removing a selection stops future notifications but does not remove existing Discord overwrites or membership; review these separately when staff leave. Notification IDs stay in server configuration until removed or erased through the core privacy workflow and are hidden from support disclosure. The fixed core configuration privacy provider covers these associations even when the module is disabled or removed, exports only the requesting user's membership, and preserves other recipients and settings during erasure.
|
|
47
|
+
|
|
48
|
+
Auto Moderation owns each rule's decision to open a thread; there is no second severity gate in Moderation. Channel and role selections can be saved during setup, and Configure channel can create the access role. Active warning delivery requires a non-empty template; thread delivery requires complete channel/role configuration and an opening message.
|
|
49
|
+
|
|
50
|
+
The **Cases / offences** resource lists at most 50 cases with exact case/member search, bounded filters, a maximum 31-day date window, stable newest-first cursors, and revision-checked detail. It has no dashboard create, update, delete, or export operation.
|
|
51
|
+
|
|
52
|
+
- **Reconcile** inspects current Discord state before settling or retrying recovery work.
|
|
53
|
+
- **Correct** creates a linked correction case; it never edits the original action.
|
|
54
|
+
- **Disassociate user** is a one-click authorised action with no reason or confirmation. It clears the subject association and repeat count eligibility while preserving the case number and anonymous action history.
|
|
55
|
+
- **Reveal private note** is separately authorised and audited. Notes remain concealed in ordinary list and detail responses.
|
|
56
|
+
|
|
57
|
+
Case detail also offers separately permission-checked **Mute**, **Unmute**, **Kick**, **Ban**, **Demote** and **No Further Action**. **Close thread** and **Close and delete** appear only after a successful staff resolution; Close and delete also remains available for an already-closed thread. Punitive dashboard actions require a reason and confirmation; No Further Action and Close need no reason. Dashboard Close and delete requires confirmation but no reason. The initial case facts stay immutable: subsequent staff actions are additional audited attempts, not new offences. Timeout and demotion parameters are persisted by additive migration `0002_staff_attempt_parameters.sql` for restart recovery.
|
|
58
|
+
|
|
59
|
+
Private notes come from the optional `private-note` input on staff `/mod` commands that offer it; automatic cases normally have no note. The dashboard states whether a concealed note exists.
|
|
60
|
+
|
|
61
|
+
**Thread message layout** selects Three containers, Single container, Compact incident, Conversation + evidence, or Spoiler evidence. A fictional dashboard preview changes with the selector without performing any moderation action. New threads place configured staff/user mentions, the opening message, rule name, confidence, original channel, escaped original message and staff buttons on one first message. Evidence exceeding Discord's aggregate display limit is delivered in a separate full evidence card without truncation. Refreshing buttons preserves the chosen containers and does not ping members again. Existing threads retain their layout when settings change and are not bulk-reformatted.
|
|
62
|
+
|
|
63
|
+
**Check channel** is read-only. **Configure channel** requires the checked revision and applies only the previewed minimum role/overwrite changes without removing unrelated Discord configuration.
|
|
64
|
+
|
|
65
|
+
## Discord requirements
|
|
66
|
+
|
|
67
|
+
Helyx may need **View Channel**, **Read Message History**, **Send Messages in Threads**, **Create Private Threads**, **Manage Threads**, **Manage Channels**, **Manage Messages**, **Manage Roles**, **Moderate Members**, **Kick Members**, and **Ban Members**. Each operation checks only the permissions it needs at execution time; the manifest union is not proof of current authority and the module never requests Administrator.
|
|
68
|
+
|
|
69
|
+
Moderation consumes structural channel-delete, channel-update, guild-role-delete, and guild-role-update events for configuration and thread-access reconciliation. It does not require Message Content or a privileged Developer Portal intent.
|
|
70
|
+
|
|
71
|
+
## Data, privacy and retention
|
|
72
|
+
|
|
73
|
+
| Record category | Retained data |
|
|
74
|
+
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
75
|
+
| Cases | Case/server identity, nullable subject and actor IDs, immutable action/reason fields, message-reference IDs, repeat metadata, state, safe outcome, revision, and timestamps. |
|
|
76
|
+
| Attempts | Case, operation and actor IDs, action/reason, typed outcome, safe code, removed-role IDs, and timestamps. |
|
|
77
|
+
| Operation receipts | Content-free idempotency input hash, case/attempt outcome references, and timestamp. |
|
|
78
|
+
| Direct-message receipts | Case/template revision, delivery state, safe code, optional Discord message ID, and timestamps. |
|
|
79
|
+
| Thread deliveries | Case/configuration IDs, private-thread/control message IDs, opaque control token, staged delivery states, fence/retry metadata, safe code, and timestamps. |
|
|
80
|
+
|
|
81
|
+
No offending message body, matched fragment, attachment, embed, or content-derived evidence is written to PostgreSQL, Logging, Audit, metrics, or traces. When transient evidence is available, it is delivered directly to Discord with mentions suppressed; absence never invalidates the case.
|
|
82
|
+
|
|
83
|
+
The versioned privacy provider covers cases directly and attempts, direct-message receipts, operation receipts, and thread deliveries through their case relationship, including while Moderation is disabled. Subject and actor associations can be detached; reasons, private notes, referenced Discord artefacts, and active safety constraints enter the declared manual-review or retention path. Disassociation is an operational correction, not a privacy erasure or hard delete.
|
|
84
|
+
|
|
85
|
+
Settled cases use the configured retention window. Active bans and open review cases remain protected; expiry removes attribution or private content without falsifying the anonymous safety history. Discord-owned threads and messages require separately tracked best-effort cleanup.
|
|
86
|
+
|
|
87
|
+
## Operations and failure behavior
|
|
88
|
+
|
|
89
|
+
Case number allocation, pending case creation, and the first immutable attempt commit atomically before Discord mutation. Exact replay returns the original outcome; reused operation identities with different canonical input fail closed. Ambiguous Discord results are never blindly replayed: a bounded reconciliation inspects current state and either settles the case or leaves it for review.
|
|
90
|
+
|
|
91
|
+
Warning direct messages and violation-thread delivery are best effort and cannot roll back an applied moderation action. Thread creation, evidence, member addition, notification, control rendering, close, and access cleanup have typed retry/review states. Scheduled action reconciliation, thread delivery, and thread-access reconciliation use durable platform tasks with bounded attempts and `requires_review` exhaustion.
|
|
92
|
+
|
|
93
|
+
Close and delete requires Manage Threads and verifies the exact server, parent, private-thread identity and bot creator before deletion. An already-missing bound thread is idempotent completion. Failed or uncertain deletion is not reported as success: its durable recovery marker remains available for **Reconcile case**. Automatic recovery inspects state without blindly repeating deletion; an authorised dashboard reconciliation can retry the same audited deletion after fresh staff, configuration and binding checks. Records, case numbers and immutable action history are retained.
|
|
94
|
+
|
|
95
|
+
Disabling Moderation stops new commands, automated provider requests, and behavioural tasks without deleting cases. Health is unhealthy while stopped or when a required runtime service is missing, and degraded when durable recovery needs staff attention.
|
|
96
|
+
|
|
97
|
+
Before release, run formatting, lint, type checks, focused tests, module contracts, packed lifecycle checks, and the repository release dry run. Live Discord behaviour remains unclaimed until the credentialed smoke matrix passes.
|
|
98
|
+
|
|
99
|
+
## Documentation and support
|
|
100
|
+
|
|
101
|
+
- [Moderation overview](https://docs.helyx.gg/modules/moderation/)
|
|
102
|
+
- [Moderation permissions](https://docs.helyx.gg/modules/moderation/permissions/)
|
|
103
|
+
- [Moderation troubleshooting](https://docs.helyx.gg/modules/moderation/troubleshooting/)
|
|
104
|
+
- [Moderation FAQ](https://docs.helyx.gg/modules/moderation/faq/)
|
|
105
|
+
- [Report an issue](https://github.com/ZyC0R3/Helyx/issues)
|
|
106
|
+
|
|
107
|
+
Publication, licensing, release-state promotion, push, deployment, and production changes require explicit owner approval.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type AuditService, type AtomicModerationCaseService, type InteractionSessionService, type ModalCapableInteractionContext, type ModerationActionAttemptTerminalOutcome, type ModerationEnforcementAction, type ModerationMemberEnforcementService, type ServiceAccess } from "@helyx/sdk";
|
|
2
|
+
import type { ModerationDispatchAction, ModerationStaffDraft } from "./service.js";
|
|
3
|
+
export declare function staffAction(draft: ModerationStaffDraft): ModerationDispatchAction;
|
|
4
|
+
export declare function claimModerationConfirmation(context: ModalCapableInteractionContext, session: NonNullable<Awaited<ReturnType<InteractionSessionService["get"]>>>, purpose: string): Promise<void>;
|
|
5
|
+
export declare function moderationPermissionForAction(action: ModerationStaffDraft["action"]): ModerationStaffDraft["permissionId"];
|
|
6
|
+
export declare function atomicCases(services: ServiceAccess): AtomicModerationCaseService;
|
|
7
|
+
export declare function moderationEnforcement(services: ServiceAccess): ModerationMemberEnforcementService;
|
|
8
|
+
export declare function moderationEnabled(services: ServiceAccess, guildId: string): Promise<boolean>;
|
|
9
|
+
export declare function protectedViolationRoleIds(services: ServiceAccess, guildId: string): Promise<readonly string[]>;
|
|
10
|
+
export declare function moderationConfiguration(services: ServiceAccess, guildId: string): Promise<import("./configuration.js").ModerationConfiguration>;
|
|
11
|
+
export declare function moderationAuditReason(caseNumber: number, reason: string): string;
|
|
12
|
+
export declare function moderationApplied(outcome: ModerationActionAttemptTerminalOutcome): boolean;
|
|
13
|
+
export declare function moderationRetentionExpiresAt(occurredAt: Date, retentionDays: number): Date;
|
|
14
|
+
export declare function scheduleActionReconciliation(services: ServiceAccess, input: {
|
|
15
|
+
guildId: string;
|
|
16
|
+
caseId: string;
|
|
17
|
+
attemptId: string;
|
|
18
|
+
operationKey: string;
|
|
19
|
+
}): Promise<void>;
|
|
20
|
+
export declare function sendWarningDirectMessage(services: ServiceAccess, input: {
|
|
21
|
+
guildId: string;
|
|
22
|
+
caseId: string;
|
|
23
|
+
caseNumber: number;
|
|
24
|
+
subjectUserId: string;
|
|
25
|
+
}): Promise<void>;
|
|
26
|
+
export declare function emitModerationActionLog(services: ServiceAccess, input: {
|
|
27
|
+
guildId: string;
|
|
28
|
+
action: ModerationStaffDraft["action"];
|
|
29
|
+
caseId: string;
|
|
30
|
+
caseNumber: number;
|
|
31
|
+
subjectUserId: string;
|
|
32
|
+
actorUserId: string;
|
|
33
|
+
durationSeconds?: number;
|
|
34
|
+
operationKey: string;
|
|
35
|
+
}): Promise<void>;
|
|
36
|
+
export declare function emitAutomaticModerationActionLog(services: ServiceAccess, input: {
|
|
37
|
+
guildId: string;
|
|
38
|
+
action: ModerationEnforcementAction;
|
|
39
|
+
caseId: string;
|
|
40
|
+
caseNumber: number;
|
|
41
|
+
subjectUserId: string;
|
|
42
|
+
operationKey: string;
|
|
43
|
+
}): Promise<void>;
|
|
44
|
+
export declare function appendModerationAudit(services: ServiceAccess, input: Parameters<AuditService["append"]>[0]): Promise<{
|
|
45
|
+
eventId: bigint;
|
|
46
|
+
createdAt: Date;
|
|
47
|
+
}>;
|
|
48
|
+
//# sourceMappingURL=action-support.d.ts.map
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { HELYX_SERVICE_NAMES, } from "@helyx/sdk";
|
|
3
|
+
import { DEFAULT_MODERATION_CONFIGURATION, parseModerationConfiguration, } from "./configuration.js";
|
|
4
|
+
import { MODERATION_COMMAND_IDS, MODERATION_MODULE_ID, MODERATION_TASK_KINDS, } from "./constants.js";
|
|
5
|
+
import { ModerationRepository } from "./repository.js";
|
|
6
|
+
export function staffAction(draft) {
|
|
7
|
+
if (draft.action === "warn")
|
|
8
|
+
return { type: "warn" };
|
|
9
|
+
if (draft.action === "timeout")
|
|
10
|
+
return {
|
|
11
|
+
type: "timeout",
|
|
12
|
+
durationSeconds: draft.requestedDurationSeconds,
|
|
13
|
+
};
|
|
14
|
+
if (draft.action === "kick")
|
|
15
|
+
return { type: "kick" };
|
|
16
|
+
if (draft.action === "ban")
|
|
17
|
+
return {
|
|
18
|
+
type: "ban",
|
|
19
|
+
deleteMessageSeconds: draft.deleteMessageSeconds ?? 0,
|
|
20
|
+
};
|
|
21
|
+
if (draft.action === "demote")
|
|
22
|
+
return { type: "demote", roleIds: draft.demoteRoleIds ?? [] };
|
|
23
|
+
if (draft.action === "timeout_remove")
|
|
24
|
+
return { type: "timeout_remove" };
|
|
25
|
+
if (draft.action === "unban")
|
|
26
|
+
return { type: "unban" };
|
|
27
|
+
return { type: "case_only" };
|
|
28
|
+
}
|
|
29
|
+
export async function claimModerationConfirmation(context, session, purpose) {
|
|
30
|
+
await context.services
|
|
31
|
+
.get(HELYX_SERVICE_NAMES.sessions)
|
|
32
|
+
.advance({
|
|
33
|
+
sessionId: session.sessionId,
|
|
34
|
+
guildId: session.guildId,
|
|
35
|
+
userId: session.userId,
|
|
36
|
+
purpose,
|
|
37
|
+
expectedVersion: session.version,
|
|
38
|
+
expectedStep: 0,
|
|
39
|
+
nextStep: 1,
|
|
40
|
+
state: { ...session.state, step: 1 },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
export function moderationPermissionForAction(action) {
|
|
44
|
+
return action === "timeout_remove"
|
|
45
|
+
? MODERATION_COMMAND_IDS.timeoutRemove
|
|
46
|
+
: MODERATION_COMMAND_IDS[action];
|
|
47
|
+
}
|
|
48
|
+
export function atomicCases(services) {
|
|
49
|
+
return services.get(HELYX_SERVICE_NAMES.atomicModerationCases);
|
|
50
|
+
}
|
|
51
|
+
export function moderationEnforcement(services) {
|
|
52
|
+
return services.get(HELYX_SERVICE_NAMES.memberEnforcement);
|
|
53
|
+
}
|
|
54
|
+
export async function moderationEnabled(services, guildId) {
|
|
55
|
+
return services.has(HELYX_SERVICE_NAMES.installations)
|
|
56
|
+
? services
|
|
57
|
+
.get(HELYX_SERVICE_NAMES.installations)
|
|
58
|
+
.isModuleEnabled(guildId, MODERATION_MODULE_ID)
|
|
59
|
+
: false;
|
|
60
|
+
}
|
|
61
|
+
export async function protectedViolationRoleIds(services, guildId) {
|
|
62
|
+
if (!services.has(HELYX_SERVICE_NAMES.configuration))
|
|
63
|
+
return [];
|
|
64
|
+
const current = await services
|
|
65
|
+
.get(HELYX_SERVICE_NAMES.configuration)
|
|
66
|
+
.get(guildId, MODERATION_MODULE_ID);
|
|
67
|
+
const configuration = current
|
|
68
|
+
? parseModerationConfiguration(current.value)
|
|
69
|
+
: DEFAULT_MODERATION_CONFIGURATION;
|
|
70
|
+
return configuration.violationAccessRoleId
|
|
71
|
+
? [configuration.violationAccessRoleId]
|
|
72
|
+
: [];
|
|
73
|
+
}
|
|
74
|
+
export async function moderationConfiguration(services, guildId) {
|
|
75
|
+
if (!services.has(HELYX_SERVICE_NAMES.configuration))
|
|
76
|
+
return DEFAULT_MODERATION_CONFIGURATION;
|
|
77
|
+
const current = await services
|
|
78
|
+
.get(HELYX_SERVICE_NAMES.configuration)
|
|
79
|
+
.get(guildId, MODERATION_MODULE_ID);
|
|
80
|
+
return current
|
|
81
|
+
? parseModerationConfiguration(current.value)
|
|
82
|
+
: DEFAULT_MODERATION_CONFIGURATION;
|
|
83
|
+
}
|
|
84
|
+
export function moderationAuditReason(caseNumber, reason) {
|
|
85
|
+
return `[Helyx case ${caseNumber}] ${reason}`.slice(0, 512);
|
|
86
|
+
}
|
|
87
|
+
export function moderationApplied(outcome) {
|
|
88
|
+
return outcome === "succeeded" || outcome === "already_applied";
|
|
89
|
+
}
|
|
90
|
+
export function moderationRetentionExpiresAt(occurredAt, retentionDays) {
|
|
91
|
+
return new Date(occurredAt.getTime() + retentionDays * 86_400_000);
|
|
92
|
+
}
|
|
93
|
+
export async function scheduleActionReconciliation(services, input) {
|
|
94
|
+
if (!services.has(HELYX_SERVICE_NAMES.scheduledTasks))
|
|
95
|
+
return;
|
|
96
|
+
await services
|
|
97
|
+
.get(HELYX_SERVICE_NAMES.scheduledTasks)
|
|
98
|
+
.schedule({
|
|
99
|
+
moduleId: MODERATION_MODULE_ID,
|
|
100
|
+
taskKind: MODERATION_TASK_KINDS.actionReconcile,
|
|
101
|
+
guildId: input.guildId,
|
|
102
|
+
idempotencyKey: `${input.operationKey}:reconcile`,
|
|
103
|
+
payload: { caseId: input.caseId, attemptId: input.attemptId },
|
|
104
|
+
scheduledFor: new Date(Date.now() + 2_000),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
export async function sendWarningDirectMessage(services, input) {
|
|
108
|
+
const configuration = await moderationConfiguration(services, input.guildId);
|
|
109
|
+
if (!configuration.warningDirectMessageEnabled)
|
|
110
|
+
return;
|
|
111
|
+
const attemptedAt = new Date();
|
|
112
|
+
let delivery = { outcome: "failed" };
|
|
113
|
+
if (services.has(HELYX_SERVICE_NAMES.discordMessages)) {
|
|
114
|
+
const content = configuration.warningDirectMessageTemplate
|
|
115
|
+
.replaceAll("{{caseNumber}}", String(input.caseNumber))
|
|
116
|
+
.replaceAll("{{serverName}}", "this server");
|
|
117
|
+
delivery = await services
|
|
118
|
+
.get(HELYX_SERVICE_NAMES.discordMessages)
|
|
119
|
+
.sendDirect({
|
|
120
|
+
userId: input.subjectUserId,
|
|
121
|
+
response: {
|
|
122
|
+
componentsV2: { text: [{ content, markdown: false }] },
|
|
123
|
+
allowedRoleMentionIds: [],
|
|
124
|
+
allowedUserMentionIds: [],
|
|
125
|
+
suppressEmbeds: true,
|
|
126
|
+
},
|
|
127
|
+
})
|
|
128
|
+
.catch(() => ({ outcome: "failed" }));
|
|
129
|
+
}
|
|
130
|
+
const now = new Date();
|
|
131
|
+
await new ModerationRepository(services)
|
|
132
|
+
.upsertDmReceipt({
|
|
133
|
+
receiptId: randomUUID(),
|
|
134
|
+
guildId: input.guildId,
|
|
135
|
+
caseId: input.caseId,
|
|
136
|
+
templateRevision: 1,
|
|
137
|
+
deliveryState: delivery.outcome === "sent" ? "succeeded" : "failed",
|
|
138
|
+
safeCode: delivery.outcome === "sent" ? null : `dm_${delivery.outcome}`,
|
|
139
|
+
messageId: delivery.outcome === "sent" ? delivery.messageId : null,
|
|
140
|
+
attemptedAt,
|
|
141
|
+
createdAt: now,
|
|
142
|
+
updatedAt: now,
|
|
143
|
+
})
|
|
144
|
+
.catch(() => undefined);
|
|
145
|
+
}
|
|
146
|
+
export async function emitModerationActionLog(services, input) {
|
|
147
|
+
await emitLog(services, input, input.action);
|
|
148
|
+
}
|
|
149
|
+
export async function emitAutomaticModerationActionLog(services, input) {
|
|
150
|
+
await emitLog(services, input, input.action.type);
|
|
151
|
+
}
|
|
152
|
+
async function emitLog(services, input, action) {
|
|
153
|
+
const eventId = actionLogEvent(action);
|
|
154
|
+
if (!eventId || !services.has(HELYX_SERVICE_NAMES.moduleLogging))
|
|
155
|
+
return;
|
|
156
|
+
await services
|
|
157
|
+
.get(HELYX_SERVICE_NAMES.moduleLogging)
|
|
158
|
+
.emit({
|
|
159
|
+
guildId: input.guildId,
|
|
160
|
+
moduleId: MODERATION_MODULE_ID,
|
|
161
|
+
eventId,
|
|
162
|
+
summary: `Moderation case ${input.caseNumber} was applied.`,
|
|
163
|
+
details: [
|
|
164
|
+
{ label: "Case", value: input.caseId },
|
|
165
|
+
{ label: "Member", value: input.subjectUserId },
|
|
166
|
+
{ label: "Action", value: action },
|
|
167
|
+
...(input.actorUserId
|
|
168
|
+
? [{ label: "Actor", value: input.actorUserId }]
|
|
169
|
+
: []),
|
|
170
|
+
...(input.durationSeconds
|
|
171
|
+
? [
|
|
172
|
+
{
|
|
173
|
+
label: "Duration seconds",
|
|
174
|
+
value: String(input.durationSeconds),
|
|
175
|
+
},
|
|
176
|
+
]
|
|
177
|
+
: []),
|
|
178
|
+
],
|
|
179
|
+
idempotencyKey: `${input.operationKey}:log`,
|
|
180
|
+
})
|
|
181
|
+
.catch(() => undefined);
|
|
182
|
+
}
|
|
183
|
+
function actionLogEvent(action) {
|
|
184
|
+
return {
|
|
185
|
+
warn: "member-warned",
|
|
186
|
+
timeout: "member-timed-out",
|
|
187
|
+
timeout_remove: "member-unmuted",
|
|
188
|
+
kick: "member-kicked",
|
|
189
|
+
ban: "member-banned",
|
|
190
|
+
demote: "member-demoted",
|
|
191
|
+
unban: "member-unbanned",
|
|
192
|
+
delete_message: null,
|
|
193
|
+
case_only: null,
|
|
194
|
+
}[action];
|
|
195
|
+
}
|
|
196
|
+
export function appendModerationAudit(services, input) {
|
|
197
|
+
return services
|
|
198
|
+
.get(HELYX_SERVICE_NAMES.audit)
|
|
199
|
+
.append({ ...input, moduleId: MODERATION_MODULE_ID });
|
|
200
|
+
}
|
|
201
|
+
//# sourceMappingURL=action-support.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ManagedResourceContext } from "@helyx/sdk";
|
|
2
|
+
import { type StoredModerationCase } from "./repository.js";
|
|
3
|
+
import type { ModerationService } from "./service.js";
|
|
4
|
+
export declare class ModerationCaseActions {
|
|
5
|
+
private readonly moderation;
|
|
6
|
+
constructor(moderation: ModerationService);
|
|
7
|
+
execute(context: ManagedResourceContext, moderationCase: StoredModerationCase, actionId: string, value: Readonly<Record<string, unknown>>): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=case-actions.d.ts.map
|