@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.
Files changed (65) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/LICENSE +725 -0
  3. package/README.md +107 -0
  4. package/dist/action-support.d.ts +48 -0
  5. package/dist/action-support.js +201 -0
  6. package/dist/case-actions.d.ts +9 -0
  7. package/dist/case-actions.js +311 -0
  8. package/dist/case-naming.d.ts +4 -0
  9. package/dist/case-naming.js +9 -0
  10. package/dist/case-repository.d.ts +29 -0
  11. package/dist/case-repository.js +199 -0
  12. package/dist/channel-actions.d.ts +3 -0
  13. package/dist/channel-actions.js +267 -0
  14. package/dist/commands.d.ts +4 -0
  15. package/dist/commands.js +309 -0
  16. package/dist/components.d.ts +15 -0
  17. package/dist/components.js +381 -0
  18. package/dist/configuration.d.ts +16 -0
  19. package/dist/configuration.js +94 -0
  20. package/dist/constants.d.ts +53 -0
  21. package/dist/constants.js +53 -0
  22. package/dist/contracts.d.ts +109 -0
  23. package/dist/contracts.js +84 -0
  24. package/dist/domain.d.ts +29 -0
  25. package/dist/domain.js +101 -0
  26. package/dist/events.d.ts +3 -0
  27. package/dist/events.js +60 -0
  28. package/dist/health.d.ts +10 -0
  29. package/dist/health.js +56 -0
  30. package/dist/index.d.ts +9 -0
  31. package/dist/index.js +92 -0
  32. package/dist/moderation-cases-resource.d.ts +14 -0
  33. package/dist/moderation-cases-resource.js +226 -0
  34. package/dist/presentation.d.ts +8 -0
  35. package/dist/presentation.js +69 -0
  36. package/dist/privacy.d.ts +8 -0
  37. package/dist/privacy.js +38 -0
  38. package/dist/provider.d.ts +4 -0
  39. package/dist/provider.js +335 -0
  40. package/dist/receipt-repository.d.ts +9 -0
  41. package/dist/receipt-repository.js +36 -0
  42. package/dist/records.d.ts +399 -0
  43. package/dist/records.js +303 -0
  44. package/dist/repository-model.d.ts +131 -0
  45. package/dist/repository-model.js +318 -0
  46. package/dist/repository.d.ts +21 -0
  47. package/dist/repository.js +41 -0
  48. package/dist/service.d.ts +75 -0
  49. package/dist/service.js +329 -0
  50. package/dist/tasks.d.ts +49 -0
  51. package/dist/tasks.js +391 -0
  52. package/dist/thread-controls.d.ts +23 -0
  53. package/dist/thread-controls.js +376 -0
  54. package/dist/thread-deletion-recovery.d.ts +13 -0
  55. package/dist/thread-deletion-recovery.js +46 -0
  56. package/dist/thread-delivery.d.ts +11 -0
  57. package/dist/thread-delivery.js +427 -0
  58. package/dist/thread-reconciliation.d.ts +24 -0
  59. package/dist/thread-reconciliation.js +181 -0
  60. package/dist/thread-repository.d.ts +16 -0
  61. package/dist/thread-repository.js +70 -0
  62. package/manifest.json +999 -0
  63. package/migrations/0001_moderation_foundation.sql +552 -0
  64. package/migrations/0002_staff_attempt_parameters.sql +27 -0
  65. package/package.json +56 -0
@@ -0,0 +1,381 @@
1
+ import { HELYX_SERVICE_NAMES, } from "@helyx/sdk";
2
+ import { MODERATION_COMPONENT_IDS } from "./constants.js";
3
+ import { MODERATION_MODULE_ID } from "./constants.js";
4
+ import { claimModerationConfirmation, moderationPermissionForAction, } from "./action-support.js";
5
+ const CONFIRMATION_PURPOSE = "moderation.staff-action-confirmation";
6
+ const CONFIRMATION_TTL_MS = 60_000;
7
+ export function createModerationInteractions(service, threadControls) {
8
+ return {
9
+ buttons: [
10
+ {
11
+ customIdPrefix: MODERATION_COMPONENT_IDS.confirm,
12
+ acknowledgement: "deferred-ephemeral",
13
+ execute: (context) => confirmModerationAction(context, service),
14
+ },
15
+ {
16
+ customIdPrefix: MODERATION_COMPONENT_IDS.cancel,
17
+ acknowledgement: "deferred-ephemeral",
18
+ execute: cancelModerationAction,
19
+ },
20
+ ...[
21
+ [MODERATION_COMPONENT_IDS.threadKick, "kick"],
22
+ [MODERATION_COMPONENT_IDS.threadBan, "ban"],
23
+ [MODERATION_COMPONENT_IDS.threadUnmute, "unmute"],
24
+ [MODERATION_COMPONENT_IDS.threadNoFurtherAction, "no_further_action"],
25
+ [MODERATION_COMPONENT_IDS.threadClose, "close"],
26
+ [MODERATION_COMPONENT_IDS.threadCloseDelete, "close_delete"],
27
+ ].map(([prefix, action]) => ({
28
+ customIdPrefix: prefix,
29
+ acknowledgement: "deferred-ephemeral",
30
+ execute: (context) => threadControls.executeControl(context, action, componentToken(context.customId, prefix)),
31
+ })),
32
+ ],
33
+ };
34
+ }
35
+ export async function requestModerationConfirmation(context, draft, _service) {
36
+ const session = await sessionService(context).create({
37
+ guildId: draft.guildId,
38
+ userId: context.userId,
39
+ purpose: CONFIRMATION_PURPOSE,
40
+ state: { step: 0, draftRevision: 1, draft: serialiseDraft(draft) },
41
+ expiresAt: new Date(Date.now() + CONFIRMATION_TTL_MS),
42
+ replace: false,
43
+ });
44
+ await context.reply({
45
+ ephemeral: true,
46
+ embed: {
47
+ title: "Confirm moderation action",
48
+ description: [
49
+ `**Member:** ${draft.subjectDisplayName} (${draft.subjectUserId})`,
50
+ `**Action:** ${actionLabel(draft)}`,
51
+ `**Reason:** ${draft.reason}`,
52
+ "Confirm within 60 seconds. Access, membership and hierarchy are checked again before Discord is changed.",
53
+ ].join("\n"),
54
+ color: 0xf59e0b,
55
+ },
56
+ buttons: [
57
+ {
58
+ type: "button",
59
+ customId: `${MODERATION_COMPONENT_IDS.confirm}${session.sessionId}`,
60
+ label: "Confirm action",
61
+ style: "danger",
62
+ },
63
+ {
64
+ type: "button",
65
+ customId: `${MODERATION_COMPONENT_IDS.cancel}${session.sessionId}`,
66
+ label: "Cancel",
67
+ style: "secondary",
68
+ },
69
+ ],
70
+ });
71
+ void _service;
72
+ }
73
+ async function confirmModerationAction(context, service) {
74
+ try {
75
+ const sessionId = componentToken(context.customId, MODERATION_COMPONENT_IDS.confirm);
76
+ const session = await sessionService(context).get(sessionId);
77
+ if (!session || !validSession(context, session))
78
+ throw new Error("This moderation confirmation expired. Run the command again.");
79
+ const draft = parseDraft(session.state, context);
80
+ await claimModerationConfirmation(context, session, CONFIRMATION_PURPOSE).catch(() => {
81
+ throw new Error("This moderation confirmation expired. Run the command again.");
82
+ });
83
+ if (!(await service.canExecuteStaffAction(context.services, draft)))
84
+ throw new Error("The action is no longer allowed. Check access, membership and role hierarchy.");
85
+ const result = await service.executeStaffAction(context.services, draft);
86
+ await sessionService(context)
87
+ .delete(sessionId)
88
+ .catch(() => undefined);
89
+ await context.reply(resultMessage(result));
90
+ }
91
+ catch (error) {
92
+ await context.reply(errorMessage(error));
93
+ }
94
+ }
95
+ async function cancelModerationAction(context) {
96
+ try {
97
+ const sessionId = componentToken(context.customId, MODERATION_COMPONENT_IDS.cancel);
98
+ const session = await sessionService(context).get(sessionId);
99
+ if (!session || !validSession(context, session))
100
+ throw new Error("This moderation confirmation already expired.");
101
+ await claimModerationConfirmation(context, session, CONFIRMATION_PURPOSE).catch(() => {
102
+ throw new Error("This moderation confirmation already expired.");
103
+ });
104
+ await sessionService(context).delete(sessionId);
105
+ await context.reply(textMessage("Moderation action cancelled.", 0x64748b));
106
+ }
107
+ catch (error) {
108
+ await context.reply(errorMessage(error));
109
+ }
110
+ }
111
+ function validSession(context, session) {
112
+ return Boolean(session &&
113
+ context.serverId &&
114
+ session.guildId === context.serverId &&
115
+ session.userId === context.userId &&
116
+ session.purpose === CONFIRMATION_PURPOSE &&
117
+ session.expiresAt.getTime() > Date.now() &&
118
+ session.state.step === 0 &&
119
+ session.state.draftRevision === 1);
120
+ }
121
+ function parseDraft(state, context) {
122
+ const value = state.draft;
123
+ if (!record(value))
124
+ throw new Error("The moderation confirmation is invalid.");
125
+ const actor = parseActor(value.actor);
126
+ const action = value.action;
127
+ const permissionId = value.permissionId;
128
+ if (typeof value.operationKey !== "string" ||
129
+ typeof value.guildId !== "string" ||
130
+ typeof value.subjectUserId !== "string" ||
131
+ typeof value.subjectDisplayName !== "string" ||
132
+ typeof value.reason !== "string" ||
133
+ !isAction(action) ||
134
+ typeof permissionId !== "string")
135
+ throw new Error("The moderation confirmation is invalid.");
136
+ const draft = {
137
+ operationKey: value.operationKey,
138
+ guildId: value.guildId,
139
+ actor,
140
+ permissionId: permissionId,
141
+ action,
142
+ subjectUserId: value.subjectUserId,
143
+ subjectDisplayName: value.subjectDisplayName,
144
+ reason: value.reason,
145
+ ...(typeof value.privateNote === "string"
146
+ ? { privateNote: value.privateNote }
147
+ : {}),
148
+ ...(record(value.evidenceMessage) &&
149
+ typeof value.evidenceMessage.channelId === "string" &&
150
+ typeof value.evidenceMessage.messageId === "string"
151
+ ? {
152
+ evidenceMessage: {
153
+ channelId: value.evidenceMessage.channelId,
154
+ messageId: value.evidenceMessage.messageId,
155
+ },
156
+ }
157
+ : {}),
158
+ ...(typeof value.requestedDurationSeconds === "number"
159
+ ? { requestedDurationSeconds: value.requestedDurationSeconds }
160
+ : {}),
161
+ ...(typeof value.deleteMessageSeconds === "number"
162
+ ? { deleteMessageSeconds: value.deleteMessageSeconds }
163
+ : {}),
164
+ ...(Array.isArray(value.demoteRoleIds)
165
+ ? { demoteRoleIds: value.demoteRoleIds.filter(isString) }
166
+ : {}),
167
+ };
168
+ if (draft.guildId !== context.serverId ||
169
+ draft.actor.userId !== context.userId ||
170
+ draft.permissionId !== moderationPermissionForAction(draft.action))
171
+ throw new Error("The moderation confirmation is invalid.");
172
+ return draft;
173
+ }
174
+ function serialiseDraft(draft) {
175
+ return {
176
+ ...draft,
177
+ actor: {
178
+ ...draft.actor,
179
+ roleIds: [...draft.actor.roleIds],
180
+ permissions: [...draft.actor.permissions],
181
+ },
182
+ ...(draft.demoteRoleIds ? { demoteRoleIds: [...draft.demoteRoleIds] } : {}),
183
+ };
184
+ }
185
+ function parseActor(input) {
186
+ if (!record(input) ||
187
+ typeof input.userId !== "string" ||
188
+ !Array.isArray(input.roleIds) ||
189
+ !Array.isArray(input.permissions) ||
190
+ typeof input.isServerOwner !== "boolean" ||
191
+ typeof input.isAdministrator !== "boolean")
192
+ throw new Error("The moderation confirmation actor is invalid.");
193
+ return {
194
+ userId: input.userId,
195
+ roleIds: input.roleIds.filter(isString),
196
+ permissions: input.permissions.filter(isString),
197
+ isServerOwner: input.isServerOwner,
198
+ isAdministrator: input.isAdministrator,
199
+ ...(typeof input.highestRolePosition === "number"
200
+ ? { highestRolePosition: input.highestRolePosition }
201
+ : {}),
202
+ };
203
+ }
204
+ function isAction(input) {
205
+ return [
206
+ "warn",
207
+ "timeout",
208
+ "timeout_remove",
209
+ "kick",
210
+ "ban",
211
+ "demote",
212
+ "unban",
213
+ ].includes(input);
214
+ }
215
+ function actionLabel(draft) {
216
+ if (draft.action === "timeout")
217
+ return `Timeout for ${draft.requestedDurationSeconds} seconds`;
218
+ if (draft.action === "ban")
219
+ return `Ban (delete ${draft.deleteMessageSeconds ?? 0} seconds of messages)`;
220
+ if (draft.action === "demote")
221
+ return `Demote by removing ${(draft.demoteRoleIds ?? []).length} role(s)`;
222
+ return draft.action.replace("_", " ");
223
+ }
224
+ function sessionService(context) {
225
+ return context.services.get(HELYX_SERVICE_NAMES.sessions);
226
+ }
227
+ function componentToken(customId, prefix) {
228
+ if (!customId?.startsWith(prefix))
229
+ throw new Error("This control is invalid.");
230
+ const token = customId.slice(prefix.length);
231
+ if (!/^[A-Za-z0-9_-]{16,100}$/u.test(token))
232
+ throw new Error("This control is invalid.");
233
+ return token;
234
+ }
235
+ function resultMessage(result) {
236
+ return textMessage(result.outcome === "applied"
237
+ ? `Moderation action applied. Case ${result.caseNumber}.`
238
+ : result.outcome === "review_required"
239
+ ? `Case ${result.caseNumber} needs staff review.`
240
+ : "The moderation action was not applied.", result.outcome === "applied" ? 0x16a34a : 0xf59e0b);
241
+ }
242
+ function errorMessage(error) {
243
+ return textMessage(error instanceof Error && safeComponentError(error.message)
244
+ ? error.message
245
+ : "Moderation is temporarily unavailable. Nothing was changed.", 0xdc2626);
246
+ }
247
+ function safeComponentError(message) {
248
+ return SAFE_COMPONENT_ERRORS.has(message);
249
+ }
250
+ const SAFE_COMPONENT_ERRORS = new Set([
251
+ "This moderation confirmation expired. Run the command again.",
252
+ "This moderation confirmation already expired.",
253
+ "The moderation confirmation is invalid.",
254
+ "The moderation confirmation actor is invalid.",
255
+ "The action is no longer allowed. Check access, membership and role hierarchy.",
256
+ "This control is invalid.",
257
+ ]);
258
+ function textMessage(content, accentColor) {
259
+ return {
260
+ ephemeral: true,
261
+ componentsV2: { accentColor, text: [{ content, markdown: false }] },
262
+ };
263
+ }
264
+ function record(input) {
265
+ return Boolean(input) && typeof input === "object" && !Array.isArray(input);
266
+ }
267
+ function isString(input) {
268
+ return typeof input === "string";
269
+ }
270
+ export async function currentThreadActor(context, permissionId) {
271
+ if (!context.serverId)
272
+ return null;
273
+ const member = await context.services
274
+ .get(HELYX_SERVICE_NAMES.memberEnforcement)
275
+ .inspectMember({ guildId: context.serverId, memberUserId: context.userId });
276
+ if (member.outcome !== "present")
277
+ return null;
278
+ const actor = {
279
+ userId: context.userId,
280
+ roleIds: member.roleIds,
281
+ permissions: member.effectivePermissionNames,
282
+ isServerOwner: member.isGuildOwner,
283
+ isAdministrator: member.isAdministrator,
284
+ };
285
+ const allowed = await context.services
286
+ .get(HELYX_SERVICE_NAMES.resourceAuthorization)
287
+ .canUse({
288
+ guildId: context.serverId,
289
+ moduleId: MODERATION_MODULE_ID,
290
+ permissionId,
291
+ actor,
292
+ });
293
+ return allowed ? actor : null;
294
+ }
295
+ export function initialThreadControls(token, timedOut) {
296
+ return {
297
+ suppressNotifications: true,
298
+ buttons: [
299
+ threadButton(MODERATION_COMPONENT_IDS.threadKick, token, "Kick", "danger"),
300
+ threadButton(MODERATION_COMPONENT_IDS.threadBan, token, "Ban", "danger"),
301
+ ...(timedOut
302
+ ? [
303
+ threadButton(MODERATION_COMPONENT_IDS.threadUnmute, token, "Unmute", "primary"),
304
+ ]
305
+ : []),
306
+ threadButton(MODERATION_COMPONENT_IDS.threadNoFurtherAction, token, "No Further Action", "secondary"),
307
+ ],
308
+ };
309
+ }
310
+ export function closeThreadControl(token) {
311
+ return {
312
+ suppressNotifications: true,
313
+ buttons: [
314
+ threadButton(MODERATION_COMPONENT_IDS.threadClose, token, "Close", "secondary"),
315
+ threadButton(MODERATION_COMPONENT_IDS.threadCloseDelete, token, "Close and delete", "danger"),
316
+ ],
317
+ };
318
+ }
319
+ function threadButton(prefix, token, label, style) {
320
+ return {
321
+ type: "button",
322
+ customId: `${prefix}${token}`,
323
+ label,
324
+ style,
325
+ };
326
+ }
327
+ export function notificationThreadMessage(roleIds, subjectUserId, staffUserIds = []) {
328
+ return {
329
+ componentsV2: {
330
+ text: [
331
+ {
332
+ content: [
333
+ ...(roleIds.length || staffUserIds.length
334
+ ? [
335
+ `**Staff:** ${[
336
+ ...roleIds.map((id) => `<@&${id}>`),
337
+ ...staffUserIds.map((id) => `<@${id}>`),
338
+ ].join(" ")}`,
339
+ ]
340
+ : []),
341
+ ...(subjectUserId ? [`**User:** <@${subjectUserId}>`] : []),
342
+ ].join("\n") || "Moderation staff notification",
343
+ markdown: true,
344
+ },
345
+ ],
346
+ },
347
+ allowedRoleMentionIds: roleIds,
348
+ allowedUserMentionIds: [
349
+ ...new Set([...staffUserIds, ...(subjectUserId ? [subjectUserId] : [])]),
350
+ ],
351
+ suppressEmbeds: true,
352
+ };
353
+ }
354
+ export function safeViolationThreadMessage(content) {
355
+ return {
356
+ componentsV2: { text: [{ content, markdown: false }] },
357
+ allowedRoleMentionIds: [],
358
+ allowedUserMentionIds: [],
359
+ suppressEmbeds: true,
360
+ };
361
+ }
362
+ export function violationOpeningMessage(roleIds, subjectUserId, opening, controls, staffUserIds = []) {
363
+ const notification = notificationThreadMessage(roleIds, subjectUserId, staffUserIds);
364
+ return {
365
+ ...notification,
366
+ componentsV2: {
367
+ text: [
368
+ ...notification.componentsV2.text,
369
+ { content: opening, markdown: false },
370
+ ],
371
+ },
372
+ ...(controls.buttons ? { buttons: controls.buttons } : {}),
373
+ };
374
+ }
375
+ export function privateThreadControlMessage(content) {
376
+ return {
377
+ ephemeral: true,
378
+ componentsV2: { text: [{ content, markdown: false }] },
379
+ };
380
+ }
381
+ //# sourceMappingURL=components.js.map
@@ -0,0 +1,16 @@
1
+ import { type ViolationThreadLayout } from "@helyx/sdk";
2
+ export interface ModerationConfiguration {
3
+ warningDirectMessageEnabled: boolean;
4
+ warningDirectMessageTemplate: string;
5
+ caseRetentionDays: number;
6
+ violationThreadChannelId: string | null;
7
+ violationAccessRoleId: string | null;
8
+ violationModeratorRoleIds: readonly string[];
9
+ violationModeratorUserIds: readonly string[];
10
+ violationCaseNamePrefix: string;
11
+ violationMessageLayout: ViolationThreadLayout;
12
+ violationOpeningMessageTemplate: string;
13
+ }
14
+ export declare const DEFAULT_MODERATION_CONFIGURATION: ModerationConfiguration;
15
+ export declare function parseModerationConfiguration(input: unknown): ModerationConfiguration;
16
+ //# sourceMappingURL=configuration.d.ts.map
@@ -0,0 +1,94 @@
1
+ import { DashboardActionValidationError, VIOLATION_THREAD_LAYOUTS, } from "@helyx/sdk";
2
+ import { MODERATION_LIMITS } from "./constants.js";
3
+ export const DEFAULT_MODERATION_CONFIGURATION = Object.freeze({
4
+ warningDirectMessageEnabled: false,
5
+ warningDirectMessageTemplate: "You have received a moderation warning in {{serverName}}. Case: {{caseNumber}}.",
6
+ caseRetentionDays: 365,
7
+ violationThreadChannelId: null,
8
+ violationAccessRoleId: null,
9
+ violationModeratorRoleIds: [],
10
+ violationModeratorUserIds: [],
11
+ violationCaseNamePrefix: "moderation-case",
12
+ violationMessageLayout: "three_containers",
13
+ violationOpeningMessageTemplate: "A moderation case has been opened so staff can discuss this violation with you.",
14
+ });
15
+ export function parseModerationConfiguration(input) {
16
+ if (!isObject(input))
17
+ return { ...DEFAULT_MODERATION_CONFIGURATION };
18
+ const configuration = {
19
+ warningDirectMessageEnabled: booleanValue(input.warningDirectMessageEnabled, DEFAULT_MODERATION_CONFIGURATION.warningDirectMessageEnabled),
20
+ warningDirectMessageTemplate: boundedString(input.warningDirectMessageTemplate, DEFAULT_MODERATION_CONFIGURATION.warningDirectMessageTemplate, MODERATION_LIMITS.warningTemplateMaxCharacters),
21
+ caseRetentionDays: boundedInteger(input.caseRetentionDays, MODERATION_LIMITS.caseRetentionMinimumDays, MODERATION_LIMITS.caseRetentionMaximumDays, DEFAULT_MODERATION_CONFIGURATION.caseRetentionDays),
22
+ violationThreadChannelId: optionalDiscordId(input.violationThreadChannelId, "violationThreadChannelId"),
23
+ violationAccessRoleId: optionalDiscordId(input.violationAccessRoleId, "violationAccessRoleId"),
24
+ violationModeratorRoleIds: discordIdList(input.violationModeratorRoleIds, MODERATION_LIMITS.moderatorRoles, "violationModeratorRoleIds"),
25
+ violationModeratorUserIds: discordIdList(input.violationModeratorUserIds, MODERATION_LIMITS.moderatorUsers, "violationModeratorUserIds"),
26
+ violationCaseNamePrefix: caseNamePrefix(input.violationCaseNamePrefix),
27
+ violationMessageLayout: messageLayout(input.violationMessageLayout),
28
+ violationOpeningMessageTemplate: boundedString(input.violationOpeningMessageTemplate, DEFAULT_MODERATION_CONFIGURATION.violationOpeningMessageTemplate, MODERATION_LIMITS.openingTemplateMaxCharacters),
29
+ };
30
+ if (configuration.warningDirectMessageEnabled &&
31
+ !configuration.warningDirectMessageTemplate.trim())
32
+ throw new DashboardActionValidationError("A warning direct-message template is required while delivery is enabled.");
33
+ if (configuration.violationThreadChannelId &&
34
+ configuration.violationAccessRoleId &&
35
+ !configuration.violationOpeningMessageTemplate.trim())
36
+ throw new DashboardActionValidationError("Enter an opening message for the configured violation channel.");
37
+ return configuration;
38
+ }
39
+ function messageLayout(value) {
40
+ if (value === undefined)
41
+ return DEFAULT_MODERATION_CONFIGURATION.violationMessageLayout;
42
+ if (!VIOLATION_THREAD_LAYOUTS.includes(value))
43
+ throw new DashboardActionValidationError("Choose a supported thread message layout.");
44
+ return value;
45
+ }
46
+ function caseNamePrefix(value) {
47
+ const prefix = boundedString(value, DEFAULT_MODERATION_CONFIGURATION.violationCaseNamePrefix, 80).trim();
48
+ if (!prefix || /[\p{Cc}\p{Cf}]/u.test(prefix))
49
+ throw new DashboardActionValidationError("Enter a case name prefix without line breaks or hidden control characters.");
50
+ return prefix;
51
+ }
52
+ function isObject(input) {
53
+ return Boolean(input) && typeof input === "object" && !Array.isArray(input);
54
+ }
55
+ function booleanValue(value, fallback) {
56
+ if (value === undefined)
57
+ return fallback;
58
+ if (typeof value !== "boolean")
59
+ throw new DashboardActionValidationError("Moderation configuration requires boolean values.");
60
+ return value;
61
+ }
62
+ function boundedString(value, fallback, maximum) {
63
+ if (value === undefined)
64
+ return fallback;
65
+ if (typeof value !== "string" || value.length > maximum)
66
+ throw new DashboardActionValidationError("Moderation configuration contains an invalid template.");
67
+ return value;
68
+ }
69
+ function boundedInteger(value, minimum, maximum, fallback) {
70
+ if (value === undefined)
71
+ return fallback;
72
+ if (!Number.isInteger(value) ||
73
+ value < minimum ||
74
+ value > maximum)
75
+ throw new DashboardActionValidationError("Moderation configuration contains an invalid integer.");
76
+ return value;
77
+ }
78
+ function optionalDiscordId(value, key) {
79
+ if (value === undefined || value === null || value === "")
80
+ return null;
81
+ if (typeof value !== "string" || !/^\d{17,20}$/u.test(value))
82
+ throw new DashboardActionValidationError(`Moderation configuration contains an invalid ${key}.`);
83
+ return value;
84
+ }
85
+ function discordIdList(value, maximum, key) {
86
+ if (value === undefined)
87
+ return [];
88
+ if (!Array.isArray(value) ||
89
+ value.length > maximum ||
90
+ value.some((item) => typeof item !== "string" || !/^\d{17,20}$/u.test(item)))
91
+ throw new DashboardActionValidationError(`Moderation configuration contains an invalid ${key}.`);
92
+ return [...new Set(value)];
93
+ }
94
+ //# sourceMappingURL=configuration.js.map
@@ -0,0 +1,53 @@
1
+ export declare const MODERATION_MODULE_ID = "helyx.moderation";
2
+ export declare const MODERATION_CASE_ACTIONS: {
3
+ readonly mute: "moderation.cases.timeout";
4
+ readonly unmute: "moderation.cases.timeout-remove";
5
+ readonly kick: "moderation.cases.kick";
6
+ readonly ban: "moderation.cases.ban";
7
+ readonly demote: "moderation.cases.demote";
8
+ readonly "no-further-action": "moderation.cases.resolve";
9
+ readonly "close-thread": "moderation.cases.close-thread";
10
+ readonly "close-and-delete-thread": "moderation.cases.close-and-delete-thread";
11
+ };
12
+ export declare const MODERATION_COMMAND_IDS: Readonly<{
13
+ readonly warn: "moderation.warn";
14
+ readonly timeout: "moderation.timeout";
15
+ readonly timeoutRemove: "moderation.timeout-remove";
16
+ readonly kick: "moderation.kick";
17
+ readonly ban: "moderation.ban";
18
+ readonly demote: "moderation.demote";
19
+ readonly unban: "moderation.unban";
20
+ readonly history: "moderation.history";
21
+ }>;
22
+ export declare const MODERATION_COMPONENT_IDS: Readonly<{
23
+ readonly confirm: "helyx.moderation.confirm:";
24
+ readonly cancel: "helyx.moderation.cancel:";
25
+ readonly threadKick: "helyx.moderation.thread-kick:";
26
+ readonly threadBan: "helyx.moderation.thread-ban:";
27
+ readonly threadUnmute: "helyx.moderation.thread-unmute:";
28
+ readonly threadNoFurtherAction: "helyx.moderation.thread-no-further-action:";
29
+ readonly threadClose: "helyx.moderation.thread-close:";
30
+ readonly threadCloseDelete: "helyx.moderation.thread-close-delete:";
31
+ }>;
32
+ export declare const MODERATION_TASK_KINDS: Readonly<{
33
+ readonly threadDelivery: "moderation.thread-delivery";
34
+ readonly actionReconcile: "moderation.action-reconcile";
35
+ readonly threadAccessReconcile: "moderation.thread-access-reconcile";
36
+ }>;
37
+ export declare const MODERATION_LIMITS: Readonly<{
38
+ readonly reasonMinCharacters: 10;
39
+ readonly reasonMaxCharacters: 500;
40
+ readonly privateNoteMaxCharacters: 1000;
41
+ readonly warningTemplateMaxCharacters: 2000;
42
+ readonly openingTemplateMaxCharacters: 2000;
43
+ readonly moderatorRoles: 10;
44
+ readonly moderatorUsers: 10;
45
+ readonly demotionRoles: 10;
46
+ readonly casePageSize: 50;
47
+ readonly caseDateWindowDays: 31;
48
+ readonly caseRetentionMinimumDays: 30;
49
+ readonly caseRetentionMaximumDays: 730;
50
+ readonly timeoutMinimumSeconds: 60;
51
+ readonly timeoutMaximumSeconds: 2419200;
52
+ }>;
53
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1,53 @@
1
+ export const MODERATION_MODULE_ID = "helyx.moderation";
2
+ export const MODERATION_CASE_ACTIONS = {
3
+ mute: "moderation.cases.timeout",
4
+ unmute: "moderation.cases.timeout-remove",
5
+ kick: "moderation.cases.kick",
6
+ ban: "moderation.cases.ban",
7
+ demote: "moderation.cases.demote",
8
+ "no-further-action": "moderation.cases.resolve",
9
+ "close-thread": "moderation.cases.close-thread",
10
+ "close-and-delete-thread": "moderation.cases.close-and-delete-thread",
11
+ };
12
+ export const MODERATION_COMMAND_IDS = Object.freeze({
13
+ warn: "moderation.warn",
14
+ timeout: "moderation.timeout",
15
+ timeoutRemove: "moderation.timeout-remove",
16
+ kick: "moderation.kick",
17
+ ban: "moderation.ban",
18
+ demote: "moderation.demote",
19
+ unban: "moderation.unban",
20
+ history: "moderation.history",
21
+ });
22
+ export const MODERATION_COMPONENT_IDS = Object.freeze({
23
+ confirm: "helyx.moderation.confirm:",
24
+ cancel: "helyx.moderation.cancel:",
25
+ threadKick: "helyx.moderation.thread-kick:",
26
+ threadBan: "helyx.moderation.thread-ban:",
27
+ threadUnmute: "helyx.moderation.thread-unmute:",
28
+ threadNoFurtherAction: "helyx.moderation.thread-no-further-action:",
29
+ threadClose: "helyx.moderation.thread-close:",
30
+ threadCloseDelete: "helyx.moderation.thread-close-delete:",
31
+ });
32
+ export const MODERATION_TASK_KINDS = Object.freeze({
33
+ threadDelivery: "moderation.thread-delivery",
34
+ actionReconcile: "moderation.action-reconcile",
35
+ threadAccessReconcile: "moderation.thread-access-reconcile",
36
+ });
37
+ export const MODERATION_LIMITS = Object.freeze({
38
+ reasonMinCharacters: 10,
39
+ reasonMaxCharacters: 500,
40
+ privateNoteMaxCharacters: 1_000,
41
+ warningTemplateMaxCharacters: 2_000,
42
+ openingTemplateMaxCharacters: 2_000,
43
+ moderatorRoles: 10,
44
+ moderatorUsers: 10,
45
+ demotionRoles: 10,
46
+ casePageSize: 50,
47
+ caseDateWindowDays: 31,
48
+ caseRetentionMinimumDays: 30,
49
+ caseRetentionMaximumDays: 730,
50
+ timeoutMinimumSeconds: 60,
51
+ timeoutMaximumSeconds: 2_419_200,
52
+ });
53
+ //# sourceMappingURL=constants.js.map