@helyx/module-automod 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 +30 -0
- package/LICENSE +725 -0
- package/README.md +80 -0
- package/dist/activity-resource.d.ts +3 -0
- package/dist/activity-resource.js +114 -0
- package/dist/case-link-provider.d.ts +3 -0
- package/dist/case-link-provider.js +13 -0
- package/dist/configuration.d.ts +22 -0
- package/dist/configuration.js +85 -0
- package/dist/constants.d.ts +30 -0
- package/dist/constants.js +37 -0
- package/dist/contracts.d.ts +30 -0
- package/dist/contracts.js +51 -0
- package/dist/domain.d.ts +61 -0
- package/dist/domain.js +274 -0
- package/dist/engine/canonicalisation.d.ts +9 -0
- package/dist/engine/canonicalisation.js +150 -0
- package/dist/engine/compile.d.ts +4 -0
- package/dist/engine/compile.js +169 -0
- package/dist/engine/confidence.d.ts +13 -0
- package/dist/engine/confidence.js +57 -0
- package/dist/engine/configuration-cache.d.ts +29 -0
- package/dist/engine/configuration-cache.js +115 -0
- package/dist/engine/contracts.d.ts +82 -0
- package/dist/engine/contracts.js +25 -0
- package/dist/engine/index.d.ts +11 -0
- package/dist/engine/index.js +11 -0
- package/dist/engine/matcher.d.ts +3 -0
- package/dist/engine/matcher.js +206 -0
- package/dist/engine/observations.d.ts +45 -0
- package/dist/engine/observations.js +105 -0
- package/dist/engine/operation-id.d.ts +18 -0
- package/dist/engine/operation-id.js +24 -0
- package/dist/engine/similar-message-window.d.ts +68 -0
- package/dist/engine/similar-message-window.js +259 -0
- package/dist/engine/term-import.d.ts +15 -0
- package/dist/engine/term-import.js +43 -0
- package/dist/enhanced-configuration.d.ts +16 -0
- package/dist/enhanced-configuration.js +91 -0
- package/dist/events.d.ts +4 -0
- package/dist/events.js +371 -0
- package/dist/health.d.ts +9 -0
- package/dist/health.js +45 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +141 -0
- package/dist/policy-provider.d.ts +3 -0
- package/dist/policy-provider.js +66 -0
- package/dist/publication-support.d.ts +14 -0
- package/dist/publication-support.js +69 -0
- package/dist/publication.d.ts +32 -0
- package/dist/publication.js +333 -0
- package/dist/receipt-statistics.d.ts +11 -0
- package/dist/receipt-statistics.js +58 -0
- package/dist/records.d.ts +299 -0
- package/dist/records.js +288 -0
- package/dist/repository-model.d.ts +54 -0
- package/dist/repository-model.js +132 -0
- package/dist/repository.d.ts +133 -0
- package/dist/repository.js +523 -0
- package/dist/resource-cursor.d.ts +5 -0
- package/dist/resource-cursor.js +45 -0
- package/dist/resource-presentation.d.ts +21 -0
- package/dist/resource-presentation.js +111 -0
- package/dist/resource-validation.d.ts +6 -0
- package/dist/resource-validation.js +80 -0
- package/dist/resources.d.ts +4 -0
- package/dist/resources.js +319 -0
- package/dist/scanner-configuration.d.ts +10 -0
- package/dist/scanner-configuration.js +146 -0
- package/dist/scanner-provider.d.ts +5 -0
- package/dist/scanner-provider.js +310 -0
- package/dist/scanner-result-validation.d.ts +3 -0
- package/dist/scanner-result-validation.js +61 -0
- package/dist/settings-preview.d.ts +6 -0
- package/dist/settings-preview.js +84 -0
- package/manifest.json +1601 -0
- package/migrations/0001_automod_standard.sql +114 -0
- package/migrations/0002_automod_enhanced_receipts.sql +40 -0
- package/package.json +61 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import { HELYX_SERVICE_NAMES, } from "@helyx/sdk";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { AUTOMOD_LIMITS, AUTOMOD_MODULE_ID } from "./constants.js";
|
|
4
|
+
import { RECEIPT_FIELDS, RULE_FIELDS, mapReceipt, mapRule, } from "./repository-model.js";
|
|
5
|
+
export class AutoModerationRepository {
|
|
6
|
+
#records;
|
|
7
|
+
constructor(services) {
|
|
8
|
+
this.#records = isServiceAccess(services)
|
|
9
|
+
? services.get(HELYX_SERVICE_NAMES.records)
|
|
10
|
+
: services;
|
|
11
|
+
}
|
|
12
|
+
async listRules(input) {
|
|
13
|
+
if (!Number.isInteger(input.limit) ||
|
|
14
|
+
input.limit < 1 ||
|
|
15
|
+
input.limit > AUTOMOD_LIMITS.rulePageSize)
|
|
16
|
+
throw new Error("Auto Moderation rule page size is invalid.");
|
|
17
|
+
const page = await this.#records.findMany({
|
|
18
|
+
moduleId: AUTOMOD_MODULE_ID,
|
|
19
|
+
collection: "rules",
|
|
20
|
+
select: RULE_FIELDS,
|
|
21
|
+
where: {
|
|
22
|
+
guild_id: input.guildId,
|
|
23
|
+
...(input.status ? { status: input.status } : {}),
|
|
24
|
+
},
|
|
25
|
+
orderBy: [
|
|
26
|
+
{ field: "updated_at", direction: "desc" },
|
|
27
|
+
{ field: "rule_id", direction: "desc" },
|
|
28
|
+
],
|
|
29
|
+
...(input.cursor ? { cursor: input.cursor } : {}),
|
|
30
|
+
...(input.search ? { search: input.search } : {}),
|
|
31
|
+
limit: input.limit,
|
|
32
|
+
});
|
|
33
|
+
return { items: page.records.map(mapRule), nextCursor: page.nextCursor };
|
|
34
|
+
}
|
|
35
|
+
async findRule(guildId, ruleId) {
|
|
36
|
+
const row = await this.#records.findOne({
|
|
37
|
+
moduleId: AUTOMOD_MODULE_ID,
|
|
38
|
+
collection: "rules",
|
|
39
|
+
select: RULE_FIELDS,
|
|
40
|
+
where: { guild_id: guildId, rule_id: ruleId },
|
|
41
|
+
});
|
|
42
|
+
return row ? mapRule(row) : null;
|
|
43
|
+
}
|
|
44
|
+
async findByNativeRuleId(guildId, nativeRuleId) {
|
|
45
|
+
const row = await this.#records.findOne({
|
|
46
|
+
moduleId: AUTOMOD_MODULE_ID,
|
|
47
|
+
collection: "rules",
|
|
48
|
+
select: RULE_FIELDS,
|
|
49
|
+
where: { guild_id: guildId, native_rule_id: nativeRuleId },
|
|
50
|
+
});
|
|
51
|
+
return row ? mapRule(row) : null;
|
|
52
|
+
}
|
|
53
|
+
async listPublishedGuildIds(limit = 5_000) {
|
|
54
|
+
const guildIds = [];
|
|
55
|
+
let after;
|
|
56
|
+
while (guildIds.length < limit) {
|
|
57
|
+
const pageLimit = Math.min(25, limit - guildIds.length);
|
|
58
|
+
const groups = await this.#records.groupCount({
|
|
59
|
+
moduleId: AUTOMOD_MODULE_ID,
|
|
60
|
+
collection: "rules",
|
|
61
|
+
where: { status: "published" },
|
|
62
|
+
groupBy: "guild_id",
|
|
63
|
+
...(after ? { after } : {}),
|
|
64
|
+
limit: pageLimit,
|
|
65
|
+
});
|
|
66
|
+
const page = groups
|
|
67
|
+
.map(({ value }) => value)
|
|
68
|
+
.filter((value) => typeof value === "string");
|
|
69
|
+
guildIds.push(...page);
|
|
70
|
+
if (page.length < pageLimit)
|
|
71
|
+
break;
|
|
72
|
+
after = page.at(-1);
|
|
73
|
+
if (!after)
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
return guildIds;
|
|
77
|
+
}
|
|
78
|
+
async listAllPublishedRules(guildId) {
|
|
79
|
+
const items = [];
|
|
80
|
+
let cursor;
|
|
81
|
+
do {
|
|
82
|
+
const page = await this.listRules({
|
|
83
|
+
guildId,
|
|
84
|
+
status: "published",
|
|
85
|
+
limit: AUTOMOD_LIMITS.rulePageSize,
|
|
86
|
+
...(cursor ? { cursor } : {}),
|
|
87
|
+
});
|
|
88
|
+
items.push(...page.items);
|
|
89
|
+
cursor = page.nextCursor ?? undefined;
|
|
90
|
+
} while (cursor);
|
|
91
|
+
return items;
|
|
92
|
+
}
|
|
93
|
+
async createDraft(input) {
|
|
94
|
+
const ruleId = input.ruleId ?? randomUUID();
|
|
95
|
+
const operationKey = `automod:${ruleId}:1`;
|
|
96
|
+
const result = await this.#records.appendUnique({
|
|
97
|
+
moduleId: AUTOMOD_MODULE_ID,
|
|
98
|
+
collection: "rules",
|
|
99
|
+
values: {
|
|
100
|
+
rule_id: ruleId,
|
|
101
|
+
guild_id: input.guildId,
|
|
102
|
+
name: input.draft.name,
|
|
103
|
+
name_key: input.draft.name.toLocaleLowerCase("en"),
|
|
104
|
+
category: input.draft.category,
|
|
105
|
+
rule_kind: input.draft.kind,
|
|
106
|
+
status: "draft",
|
|
107
|
+
document_json: JSON.stringify(input.draft),
|
|
108
|
+
operation_key: operationKey,
|
|
109
|
+
configuration_revision: "0",
|
|
110
|
+
revision: 1,
|
|
111
|
+
created_by_user_id: input.actorUserId,
|
|
112
|
+
updated_by_user_id: input.actorUserId,
|
|
113
|
+
created_at: input.occurredAt,
|
|
114
|
+
updated_at: input.occurredAt,
|
|
115
|
+
},
|
|
116
|
+
uniqueBy: ["guild_id", "name_key"],
|
|
117
|
+
quotaGuards: [
|
|
118
|
+
{
|
|
119
|
+
collection: "rules",
|
|
120
|
+
where: { guild_id: input.guildId },
|
|
121
|
+
maximum: AUTOMOD_LIMITS.rulesPerGuild,
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
audit: audit(input.guildId, input.actorUserId, "automod.rule-created", ruleId, operationKey),
|
|
125
|
+
});
|
|
126
|
+
if (!result.created)
|
|
127
|
+
throw new Error("An Auto Moderation rule with this name already exists.");
|
|
128
|
+
return {
|
|
129
|
+
ruleId,
|
|
130
|
+
guildId: input.guildId,
|
|
131
|
+
name: input.draft.name,
|
|
132
|
+
category: input.draft.category,
|
|
133
|
+
kind: input.draft.kind,
|
|
134
|
+
status: "draft",
|
|
135
|
+
draft: input.draft,
|
|
136
|
+
nativeRuleId: null,
|
|
137
|
+
nativeFingerprint: null,
|
|
138
|
+
operationKey,
|
|
139
|
+
configurationRevision: "0",
|
|
140
|
+
revision: 1,
|
|
141
|
+
createdByUserId: input.actorUserId,
|
|
142
|
+
updatedByUserId: input.actorUserId,
|
|
143
|
+
createdAt: input.occurredAt,
|
|
144
|
+
updatedAt: input.occurredAt,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
async updateRule(input) {
|
|
148
|
+
const nextRevision = input.current.revision + 1;
|
|
149
|
+
const draft = input.draft ?? input.current.draft;
|
|
150
|
+
const updated = await this.#records.updateOne({
|
|
151
|
+
moduleId: AUTOMOD_MODULE_ID,
|
|
152
|
+
collection: "rules",
|
|
153
|
+
where: { guild_id: input.current.guildId, rule_id: input.current.ruleId },
|
|
154
|
+
expected: { field: "revision", value: input.current.revision },
|
|
155
|
+
values: {
|
|
156
|
+
name: draft.name,
|
|
157
|
+
name_key: draft.name.toLocaleLowerCase("en"),
|
|
158
|
+
category: draft.category,
|
|
159
|
+
rule_kind: draft.kind,
|
|
160
|
+
document_json: JSON.stringify(draft),
|
|
161
|
+
status: input.status ?? input.current.status,
|
|
162
|
+
native_rule_id: input.nativeRuleId === undefined
|
|
163
|
+
? input.current.nativeRuleId
|
|
164
|
+
: input.nativeRuleId,
|
|
165
|
+
native_fingerprint: input.nativeFingerprint === undefined
|
|
166
|
+
? input.current.nativeFingerprint
|
|
167
|
+
: input.nativeFingerprint,
|
|
168
|
+
configuration_revision: input.configurationRevision ?? input.current.configurationRevision,
|
|
169
|
+
operation_key: `automod:${input.current.ruleId}:${nextRevision}`,
|
|
170
|
+
revision: nextRevision,
|
|
171
|
+
updated_by_user_id: input.actorUserId,
|
|
172
|
+
updated_at: input.occurredAt,
|
|
173
|
+
},
|
|
174
|
+
audit: audit(input.current.guildId, input.actorUserId ?? undefined, input.action, input.current.ruleId, `automod:${input.current.ruleId}:${nextRevision}`),
|
|
175
|
+
});
|
|
176
|
+
if (!updated.updated)
|
|
177
|
+
return null;
|
|
178
|
+
return {
|
|
179
|
+
...input.current,
|
|
180
|
+
name: draft.name,
|
|
181
|
+
category: draft.category,
|
|
182
|
+
kind: draft.kind,
|
|
183
|
+
draft,
|
|
184
|
+
status: input.status ?? input.current.status,
|
|
185
|
+
nativeRuleId: input.nativeRuleId === undefined
|
|
186
|
+
? input.current.nativeRuleId
|
|
187
|
+
: input.nativeRuleId,
|
|
188
|
+
nativeFingerprint: input.nativeFingerprint === undefined
|
|
189
|
+
? input.current.nativeFingerprint
|
|
190
|
+
: input.nativeFingerprint,
|
|
191
|
+
configurationRevision: input.configurationRevision ?? input.current.configurationRevision,
|
|
192
|
+
operationKey: `automod:${input.current.ruleId}:${nextRevision}`,
|
|
193
|
+
revision: nextRevision,
|
|
194
|
+
updatedByUserId: input.actorUserId,
|
|
195
|
+
updatedAt: input.occurredAt,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
deleteDraft(input) {
|
|
199
|
+
return this.#records.deleteOne({
|
|
200
|
+
moduleId: AUTOMOD_MODULE_ID,
|
|
201
|
+
collection: "rules",
|
|
202
|
+
where: { guild_id: input.current.guildId, rule_id: input.current.ruleId },
|
|
203
|
+
expected: { field: "revision", value: input.current.revision },
|
|
204
|
+
audit: audit(input.current.guildId, input.actorUserId, "automod.rule-draft-deleted", input.current.ruleId, `${input.current.operationKey}:delete`),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
async appendReceipt(input) {
|
|
208
|
+
const receiptId = randomUUID();
|
|
209
|
+
const retentionExpiresAt = addDays(input.occurredAt, AUTOMOD_LIMITS.actionReceiptRetentionDays);
|
|
210
|
+
return this.#records.appendUnique({
|
|
211
|
+
moduleId: AUTOMOD_MODULE_ID,
|
|
212
|
+
collection: "action_receipts",
|
|
213
|
+
values: {
|
|
214
|
+
receipt_id: receiptId,
|
|
215
|
+
guild_id: input.guildId,
|
|
216
|
+
event_key: input.eventKey,
|
|
217
|
+
rule_id: input.rule.ruleId,
|
|
218
|
+
rule_revision: input.rule.revision,
|
|
219
|
+
configuration_revision: input.rule.configurationRevision,
|
|
220
|
+
native_rule_id: input.nativeRuleId,
|
|
221
|
+
source: "native",
|
|
222
|
+
subject_user_id: input.subjectUserId,
|
|
223
|
+
...(input.channelId ? { channel_id: input.channelId } : {}),
|
|
224
|
+
...(input.messageId ? { message_id: input.messageId } : {}),
|
|
225
|
+
...(input.alertSystemMessageId
|
|
226
|
+
? { alert_system_message_id: input.alertSystemMessageId }
|
|
227
|
+
: {}),
|
|
228
|
+
action_type: input.actionType,
|
|
229
|
+
claim_key: input.claimKey,
|
|
230
|
+
outcome: "observed",
|
|
231
|
+
occurred_at: input.occurredAt,
|
|
232
|
+
retention_expires_at: retentionExpiresAt,
|
|
233
|
+
created_at: new Date(),
|
|
234
|
+
},
|
|
235
|
+
uniqueBy: ["guild_id", "event_key"],
|
|
236
|
+
retention: [
|
|
237
|
+
{
|
|
238
|
+
collection: "action_receipts",
|
|
239
|
+
where: { guild_id: input.guildId, receipt_id: receiptId },
|
|
240
|
+
scheduledFor: retentionExpiresAt,
|
|
241
|
+
idempotencyKey: `receipt:${receiptId}`,
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
async appendEnhancedReceipt(input) {
|
|
247
|
+
const { result } = input;
|
|
248
|
+
const receiptId = randomUUID();
|
|
249
|
+
const retentionExpiresAt = addDays(input.occurredAt, AUTOMOD_LIMITS.actionReceiptRetentionDays);
|
|
250
|
+
return this.#records.appendUnique({
|
|
251
|
+
moduleId: AUTOMOD_MODULE_ID,
|
|
252
|
+
collection: "action_receipts",
|
|
253
|
+
values: {
|
|
254
|
+
receipt_id: receiptId,
|
|
255
|
+
guild_id: result.guildId,
|
|
256
|
+
event_key: result.operationId,
|
|
257
|
+
rule_id: input.rule.ruleId,
|
|
258
|
+
rule_revision: input.rule.revision,
|
|
259
|
+
configuration_revision: input.rule.configurationRevision,
|
|
260
|
+
source: "enhanced",
|
|
261
|
+
detector_version: result.detectorVersion,
|
|
262
|
+
scoring_version: result.scoringVersion,
|
|
263
|
+
term_id: result.termId,
|
|
264
|
+
detection_method: result.method,
|
|
265
|
+
confidence_class: result.confidenceClass,
|
|
266
|
+
confidence_score: result.confidenceScore,
|
|
267
|
+
effective_threshold: result.effectiveThreshold,
|
|
268
|
+
correlation_id: result.correlationId,
|
|
269
|
+
subject_user_id: result.authorUserId,
|
|
270
|
+
channel_id: result.channelId,
|
|
271
|
+
message_id: result.messageId,
|
|
272
|
+
action_type: "detection",
|
|
273
|
+
claim_key: input.claimKey,
|
|
274
|
+
outcome: "observed",
|
|
275
|
+
occurred_at: input.occurredAt,
|
|
276
|
+
retention_expires_at: retentionExpiresAt,
|
|
277
|
+
created_at: new Date(),
|
|
278
|
+
},
|
|
279
|
+
uniqueBy: ["guild_id", "event_key"],
|
|
280
|
+
retention: [
|
|
281
|
+
{
|
|
282
|
+
collection: "action_receipts",
|
|
283
|
+
where: { guild_id: result.guildId, receipt_id: receiptId },
|
|
284
|
+
scheduledFor: retentionExpiresAt,
|
|
285
|
+
idempotencyKey: `receipt:${receiptId}`,
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
async settleReceipt(input) {
|
|
291
|
+
await this.#records.updateOne({
|
|
292
|
+
moduleId: AUTOMOD_MODULE_ID,
|
|
293
|
+
collection: "action_receipts",
|
|
294
|
+
where: { guild_id: input.guildId, event_key: input.eventKey },
|
|
295
|
+
expected: { field: "outcome", value: "observed" },
|
|
296
|
+
values: {
|
|
297
|
+
outcome: input.outcome,
|
|
298
|
+
moderation_case_id: input.moderationCaseId ?? null,
|
|
299
|
+
safe_code: input.safeCode ?? null,
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
async findReceiptByEventKey(input) {
|
|
304
|
+
const current = await this.#records.findOne({
|
|
305
|
+
moduleId: AUTOMOD_MODULE_ID,
|
|
306
|
+
collection: "action_receipts",
|
|
307
|
+
select: ["outcome", "moderation_case_id", "safe_code"],
|
|
308
|
+
where: { guild_id: input.guildId, event_key: input.eventKey },
|
|
309
|
+
});
|
|
310
|
+
if (!current || typeof current.outcome !== "string")
|
|
311
|
+
return null;
|
|
312
|
+
return {
|
|
313
|
+
outcome: current.outcome,
|
|
314
|
+
moderationCaseId: typeof current.moderation_case_id === "string"
|
|
315
|
+
? current.moderation_case_id
|
|
316
|
+
: null,
|
|
317
|
+
safeCode: typeof current.safe_code === "string" ? current.safe_code : null,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
async claimEnforcement(input) {
|
|
321
|
+
// Pending claims are deliberately not scheduled for deletion. They may
|
|
322
|
+
// represent an interrupted enforcement hand-off and must remain available
|
|
323
|
+
// for reconciliation until they reach a terminal outcome.
|
|
324
|
+
const retentionExpiresAt = addDays(input.occurredAt, AUTOMOD_LIMITS.settledClaimRetentionDays);
|
|
325
|
+
const result = await this.#records.appendUnique({
|
|
326
|
+
moduleId: AUTOMOD_MODULE_ID,
|
|
327
|
+
collection: "enforcement_claims",
|
|
328
|
+
values: {
|
|
329
|
+
claim_key: input.claimKey,
|
|
330
|
+
guild_id: input.guildId,
|
|
331
|
+
rule_id: input.rule.ruleId,
|
|
332
|
+
rule_revision: input.rule.revision,
|
|
333
|
+
configuration_revision: input.rule.configurationRevision,
|
|
334
|
+
subject_user_id: input.subjectUserId,
|
|
335
|
+
...(input.channelId ? { channel_id: input.channelId } : {}),
|
|
336
|
+
...(input.messageId ? { message_id: input.messageId } : {}),
|
|
337
|
+
category: input.rule.category,
|
|
338
|
+
action_family: input.actionFamily,
|
|
339
|
+
source: input.source,
|
|
340
|
+
request_fingerprint: input.requestFingerprint,
|
|
341
|
+
outcome: "pending",
|
|
342
|
+
revision: 1,
|
|
343
|
+
occurred_at: input.occurredAt,
|
|
344
|
+
retention_expires_at: retentionExpiresAt,
|
|
345
|
+
created_at: new Date(),
|
|
346
|
+
updated_at: new Date(),
|
|
347
|
+
},
|
|
348
|
+
uniqueBy: ["guild_id", "claim_key"],
|
|
349
|
+
});
|
|
350
|
+
if (result.created)
|
|
351
|
+
return {
|
|
352
|
+
created: true,
|
|
353
|
+
conflict: false,
|
|
354
|
+
source: input.source,
|
|
355
|
+
outcome: "pending",
|
|
356
|
+
moderationCaseId: null,
|
|
357
|
+
repeatOrdinal: null,
|
|
358
|
+
selectedAction: null,
|
|
359
|
+
safeCode: null,
|
|
360
|
+
};
|
|
361
|
+
const current = await this.#records.findOne({
|
|
362
|
+
moduleId: AUTOMOD_MODULE_ID,
|
|
363
|
+
collection: "enforcement_claims",
|
|
364
|
+
select: [
|
|
365
|
+
"request_fingerprint",
|
|
366
|
+
"source",
|
|
367
|
+
"outcome",
|
|
368
|
+
"moderation_case_id",
|
|
369
|
+
"repeat_ordinal",
|
|
370
|
+
"selected_action",
|
|
371
|
+
"safe_code",
|
|
372
|
+
],
|
|
373
|
+
where: { guild_id: input.guildId, claim_key: input.claimKey },
|
|
374
|
+
});
|
|
375
|
+
return {
|
|
376
|
+
created: false,
|
|
377
|
+
conflict: !current || current.request_fingerprint !== input.requestFingerprint,
|
|
378
|
+
source: current?.source ?? input.source,
|
|
379
|
+
outcome: typeof current?.outcome === "string" ? current.outcome : "pending",
|
|
380
|
+
moderationCaseId: typeof current?.moderation_case_id === "string"
|
|
381
|
+
? current.moderation_case_id
|
|
382
|
+
: null,
|
|
383
|
+
repeatOrdinal: typeof current?.repeat_ordinal === "number"
|
|
384
|
+
? current.repeat_ordinal
|
|
385
|
+
: null,
|
|
386
|
+
selectedAction: typeof current?.selected_action === "string"
|
|
387
|
+
? current.selected_action
|
|
388
|
+
: null,
|
|
389
|
+
safeCode: typeof current?.safe_code === "string" ? current.safe_code : null,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
async disassociateCaseLinks(input) {
|
|
393
|
+
let detachedRecords = 0;
|
|
394
|
+
for (const collection of [
|
|
395
|
+
"action_receipts",
|
|
396
|
+
"enforcement_claims",
|
|
397
|
+
]) {
|
|
398
|
+
const idField = collection === "action_receipts" ? "receipt_id" : "claim_key";
|
|
399
|
+
let cursor;
|
|
400
|
+
do {
|
|
401
|
+
const page = await this.#records.findMany({
|
|
402
|
+
moduleId: AUTOMOD_MODULE_ID,
|
|
403
|
+
collection,
|
|
404
|
+
select: [idField, "guild_id", "subject_user_id", "occurred_at"],
|
|
405
|
+
where: {
|
|
406
|
+
guild_id: input.guildId,
|
|
407
|
+
moderation_case_id: input.moderationCaseId,
|
|
408
|
+
},
|
|
409
|
+
orderBy: [
|
|
410
|
+
{ field: "occurred_at", direction: "desc" },
|
|
411
|
+
...(collection === "enforcement_claims"
|
|
412
|
+
? [{ field: "guild_id", direction: "desc" }]
|
|
413
|
+
: []),
|
|
414
|
+
{ field: idField, direction: "desc" },
|
|
415
|
+
],
|
|
416
|
+
limit: 100,
|
|
417
|
+
...(cursor ? { cursor } : {}),
|
|
418
|
+
});
|
|
419
|
+
for (const row of page.records) {
|
|
420
|
+
if (typeof row.subject_user_id !== "string")
|
|
421
|
+
continue;
|
|
422
|
+
const recordId = row[idField];
|
|
423
|
+
if (typeof recordId !== "string")
|
|
424
|
+
continue;
|
|
425
|
+
const updated = await this.#records.updateOne({
|
|
426
|
+
moduleId: AUTOMOD_MODULE_ID,
|
|
427
|
+
collection,
|
|
428
|
+
where: collection === "action_receipts"
|
|
429
|
+
? { receipt_id: recordId }
|
|
430
|
+
: { guild_id: input.guildId, claim_key: recordId },
|
|
431
|
+
expected: {
|
|
432
|
+
field: "subject_user_id",
|
|
433
|
+
value: row.subject_user_id,
|
|
434
|
+
},
|
|
435
|
+
values: { subject_user_id: null },
|
|
436
|
+
});
|
|
437
|
+
if (updated.updated)
|
|
438
|
+
detachedRecords += 1;
|
|
439
|
+
}
|
|
440
|
+
cursor = page.nextCursor ?? undefined;
|
|
441
|
+
} while (cursor);
|
|
442
|
+
}
|
|
443
|
+
return { detachedRecords };
|
|
444
|
+
}
|
|
445
|
+
async settleClaim(input) {
|
|
446
|
+
const retentionExpiresAt = addDays(input.occurredAt, AUTOMOD_LIMITS.settledClaimRetentionDays);
|
|
447
|
+
await this.#records.updateOne({
|
|
448
|
+
moduleId: AUTOMOD_MODULE_ID,
|
|
449
|
+
collection: "enforcement_claims",
|
|
450
|
+
where: { guild_id: input.guildId, claim_key: input.claimKey },
|
|
451
|
+
expected: { field: "revision", value: 1 },
|
|
452
|
+
values: {
|
|
453
|
+
moderation_case_id: input.moderationCaseId ?? null,
|
|
454
|
+
repeat_ordinal: input.repeatOrdinal ?? null,
|
|
455
|
+
selected_action: input.selectedAction ?? null,
|
|
456
|
+
outcome: input.outcome,
|
|
457
|
+
safe_code: input.safeCode ?? null,
|
|
458
|
+
revision: 2,
|
|
459
|
+
settled_at: input.occurredAt,
|
|
460
|
+
retention_expires_at: retentionExpiresAt,
|
|
461
|
+
updated_at: input.occurredAt,
|
|
462
|
+
},
|
|
463
|
+
retention: [
|
|
464
|
+
{
|
|
465
|
+
collection: "enforcement_claims",
|
|
466
|
+
where: { guild_id: input.guildId, claim_key: input.claimKey },
|
|
467
|
+
scheduledFor: retentionExpiresAt,
|
|
468
|
+
idempotencyKey: `claim:${input.claimKey}`,
|
|
469
|
+
},
|
|
470
|
+
],
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
async listReceipts(input) {
|
|
474
|
+
if (!Number.isInteger(input.limit) ||
|
|
475
|
+
input.limit < 1 ||
|
|
476
|
+
input.limit > AUTOMOD_LIMITS.activityPageSize)
|
|
477
|
+
throw new Error("Auto Moderation activity page size is invalid.");
|
|
478
|
+
const page = await this.#records.findMany({
|
|
479
|
+
moduleId: AUTOMOD_MODULE_ID,
|
|
480
|
+
collection: "action_receipts",
|
|
481
|
+
select: RECEIPT_FIELDS,
|
|
482
|
+
where: {
|
|
483
|
+
guild_id: input.guildId,
|
|
484
|
+
...(input.ruleId ? { rule_id: input.ruleId } : {}),
|
|
485
|
+
...(input.outcome ? { outcome: input.outcome } : {}),
|
|
486
|
+
},
|
|
487
|
+
orderBy: [
|
|
488
|
+
{ field: "occurred_at", direction: "desc" },
|
|
489
|
+
{ field: "receipt_id", direction: "desc" },
|
|
490
|
+
],
|
|
491
|
+
...(input.cursor ? { cursor: input.cursor } : {}),
|
|
492
|
+
limit: input.limit,
|
|
493
|
+
});
|
|
494
|
+
return { items: page.records.map(mapReceipt), nextCursor: page.nextCursor };
|
|
495
|
+
}
|
|
496
|
+
async findReceipt(guildId, receiptId) {
|
|
497
|
+
const row = await this.#records.findOne({
|
|
498
|
+
moduleId: AUTOMOD_MODULE_ID,
|
|
499
|
+
collection: "action_receipts",
|
|
500
|
+
select: RECEIPT_FIELDS,
|
|
501
|
+
where: { guild_id: guildId, receipt_id: receiptId },
|
|
502
|
+
});
|
|
503
|
+
return row ? mapReceipt(row) : null;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function audit(guildId, actorUserId, action, targetId, idempotencyKey) {
|
|
507
|
+
return {
|
|
508
|
+
guildId,
|
|
509
|
+
...(actorUserId ? { actorUserId } : {}),
|
|
510
|
+
action,
|
|
511
|
+
source: actorUserId ? "dashboard" : "system",
|
|
512
|
+
idempotencyKey,
|
|
513
|
+
targetType: "automod_rule",
|
|
514
|
+
targetId,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
function isServiceAccess(value) {
|
|
518
|
+
return "get" in value && typeof value.get === "function";
|
|
519
|
+
}
|
|
520
|
+
function addDays(value, days) {
|
|
521
|
+
return new Date(value.getTime() + days * 86_400_000);
|
|
522
|
+
}
|
|
523
|
+
//# sourceMappingURL=repository.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type ModuleRecordWriteValue } from "@helyx/sdk";
|
|
2
|
+
export declare function cursorBinding(guildId: string, search?: string, filters?: Readonly<Record<string, string>>): string;
|
|
3
|
+
export declare function encodeCursor(values: readonly ModuleRecordWriteValue[], binding: string): string;
|
|
4
|
+
export declare function decodeCursor(cursor: string | undefined, binding: string): readonly ModuleRecordWriteValue[] | undefined;
|
|
5
|
+
//# sourceMappingURL=resource-cursor.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { DashboardActionValidationError, } from "@helyx/sdk";
|
|
2
|
+
export function cursorBinding(guildId, search, filters) {
|
|
3
|
+
return JSON.stringify({
|
|
4
|
+
version: 1,
|
|
5
|
+
guildId,
|
|
6
|
+
search: search?.trim() ?? null,
|
|
7
|
+
status: filters?.status ?? null,
|
|
8
|
+
order: "updated_desc",
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
export function encodeCursor(values, binding) {
|
|
12
|
+
return Buffer.from(JSON.stringify({
|
|
13
|
+
version: 1,
|
|
14
|
+
binding,
|
|
15
|
+
values: values.map((value) => value instanceof Date ? { date: value.toISOString() } : value),
|
|
16
|
+
}), "utf8").toString("base64url");
|
|
17
|
+
}
|
|
18
|
+
export function decodeCursor(cursor, binding) {
|
|
19
|
+
if (!cursor)
|
|
20
|
+
return undefined;
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
23
|
+
if (parsed.version !== 1 ||
|
|
24
|
+
parsed.binding !== binding ||
|
|
25
|
+
!Array.isArray(parsed.values))
|
|
26
|
+
throw new Error();
|
|
27
|
+
return parsed.values.map(cursorValue);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
throw new DashboardActionValidationError("The Auto Moderation cursor is invalid.");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function cursorValue(value) {
|
|
34
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
35
|
+
const candidate = value;
|
|
36
|
+
if (typeof candidate.date === "string")
|
|
37
|
+
return new Date(candidate.date);
|
|
38
|
+
}
|
|
39
|
+
if (typeof value === "string" ||
|
|
40
|
+
typeof value === "number" ||
|
|
41
|
+
typeof value === "boolean")
|
|
42
|
+
return value;
|
|
43
|
+
throw new Error("Invalid cursor value");
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=resource-cursor.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ManagedResourceDetail, MessageResponse } from "@helyx/sdk";
|
|
2
|
+
import type { AutoModerationRuleDraft } from "./domain.js";
|
|
3
|
+
import type { StoredAutoModerationRule } from "./repository.js";
|
|
4
|
+
export declare function previewMessage(draft: AutoModerationRuleDraft): MessageResponse;
|
|
5
|
+
export declare function ruleSummary(rule: StoredAutoModerationRule): {
|
|
6
|
+
id: string;
|
|
7
|
+
revision: number;
|
|
8
|
+
status: import("./domain.js").AutoModerationRuleStatus;
|
|
9
|
+
title: string;
|
|
10
|
+
description: string;
|
|
11
|
+
attributes: {
|
|
12
|
+
filter: string;
|
|
13
|
+
action: string;
|
|
14
|
+
moderationThread: string;
|
|
15
|
+
ignoredRoles: readonly string[];
|
|
16
|
+
repeatOffender: string;
|
|
17
|
+
};
|
|
18
|
+
updatedAt: string;
|
|
19
|
+
};
|
|
20
|
+
export declare function ruleDetail(rule: StoredAutoModerationRule): ManagedResourceDetail;
|
|
21
|
+
//# sourceMappingURL=resource-presentation.d.ts.map
|