@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.
Files changed (79) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/LICENSE +725 -0
  3. package/README.md +80 -0
  4. package/dist/activity-resource.d.ts +3 -0
  5. package/dist/activity-resource.js +114 -0
  6. package/dist/case-link-provider.d.ts +3 -0
  7. package/dist/case-link-provider.js +13 -0
  8. package/dist/configuration.d.ts +22 -0
  9. package/dist/configuration.js +85 -0
  10. package/dist/constants.d.ts +30 -0
  11. package/dist/constants.js +37 -0
  12. package/dist/contracts.d.ts +30 -0
  13. package/dist/contracts.js +51 -0
  14. package/dist/domain.d.ts +61 -0
  15. package/dist/domain.js +274 -0
  16. package/dist/engine/canonicalisation.d.ts +9 -0
  17. package/dist/engine/canonicalisation.js +150 -0
  18. package/dist/engine/compile.d.ts +4 -0
  19. package/dist/engine/compile.js +169 -0
  20. package/dist/engine/confidence.d.ts +13 -0
  21. package/dist/engine/confidence.js +57 -0
  22. package/dist/engine/configuration-cache.d.ts +29 -0
  23. package/dist/engine/configuration-cache.js +115 -0
  24. package/dist/engine/contracts.d.ts +82 -0
  25. package/dist/engine/contracts.js +25 -0
  26. package/dist/engine/index.d.ts +11 -0
  27. package/dist/engine/index.js +11 -0
  28. package/dist/engine/matcher.d.ts +3 -0
  29. package/dist/engine/matcher.js +206 -0
  30. package/dist/engine/observations.d.ts +45 -0
  31. package/dist/engine/observations.js +105 -0
  32. package/dist/engine/operation-id.d.ts +18 -0
  33. package/dist/engine/operation-id.js +24 -0
  34. package/dist/engine/similar-message-window.d.ts +68 -0
  35. package/dist/engine/similar-message-window.js +259 -0
  36. package/dist/engine/term-import.d.ts +15 -0
  37. package/dist/engine/term-import.js +43 -0
  38. package/dist/enhanced-configuration.d.ts +16 -0
  39. package/dist/enhanced-configuration.js +91 -0
  40. package/dist/events.d.ts +4 -0
  41. package/dist/events.js +371 -0
  42. package/dist/health.d.ts +9 -0
  43. package/dist/health.js +45 -0
  44. package/dist/index.d.ts +10 -0
  45. package/dist/index.js +141 -0
  46. package/dist/policy-provider.d.ts +3 -0
  47. package/dist/policy-provider.js +66 -0
  48. package/dist/publication-support.d.ts +14 -0
  49. package/dist/publication-support.js +69 -0
  50. package/dist/publication.d.ts +32 -0
  51. package/dist/publication.js +333 -0
  52. package/dist/receipt-statistics.d.ts +11 -0
  53. package/dist/receipt-statistics.js +58 -0
  54. package/dist/records.d.ts +299 -0
  55. package/dist/records.js +288 -0
  56. package/dist/repository-model.d.ts +54 -0
  57. package/dist/repository-model.js +132 -0
  58. package/dist/repository.d.ts +133 -0
  59. package/dist/repository.js +523 -0
  60. package/dist/resource-cursor.d.ts +5 -0
  61. package/dist/resource-cursor.js +45 -0
  62. package/dist/resource-presentation.d.ts +21 -0
  63. package/dist/resource-presentation.js +111 -0
  64. package/dist/resource-validation.d.ts +6 -0
  65. package/dist/resource-validation.js +80 -0
  66. package/dist/resources.d.ts +4 -0
  67. package/dist/resources.js +319 -0
  68. package/dist/scanner-configuration.d.ts +10 -0
  69. package/dist/scanner-configuration.js +146 -0
  70. package/dist/scanner-provider.d.ts +5 -0
  71. package/dist/scanner-provider.js +310 -0
  72. package/dist/scanner-result-validation.d.ts +3 -0
  73. package/dist/scanner-result-validation.js +61 -0
  74. package/dist/settings-preview.d.ts +6 -0
  75. package/dist/settings-preview.js +84 -0
  76. package/manifest.json +1601 -0
  77. package/migrations/0001_automod_standard.sql +114 -0
  78. package/migrations/0002_automod_enhanced_receipts.sql +40 -0
  79. package/package.json +61 -0
@@ -0,0 +1,69 @@
1
+ import { HELYX_SERVICE_NAMES, } from "@helyx/sdk";
2
+ import { DEFAULT_AUTOMOD_CONFIGURATION, parseAutoModerationConfiguration, } from "./configuration.js";
3
+ import { AUTOMOD_MODULE_ID } from "./constants.js";
4
+ import { toNativeDefinition } from "./domain.js";
5
+ export function isKindEnabled(draft, configuration) {
6
+ if (draft.kind === "keyword_preset")
7
+ return configuration.presetRuleEnabled;
8
+ if (draft.kind === "spam")
9
+ return configuration.genericSpamRuleEnabled;
10
+ if (draft.kind === "mention_spam")
11
+ return configuration.mentionSpamRuleEnabled;
12
+ return true;
13
+ }
14
+ export function validateKindEnabled(draft, configuration) {
15
+ if (isKindEnabled(draft, configuration))
16
+ return;
17
+ throw new Error(`Enable the ${draft.kind.replaceAll("_", " ")} rule setting before publishing this rule.`);
18
+ }
19
+ export async function currentConfiguration(services, guildId) {
20
+ if (!services.has(HELYX_SERVICE_NAMES.configuration))
21
+ return { value: DEFAULT_AUTOMOD_CONFIGURATION, revision: "0" };
22
+ const stored = await services
23
+ .get(HELYX_SERVICE_NAMES.configuration)
24
+ .get(guildId, AUTOMOD_MODULE_ID);
25
+ return stored
26
+ ? {
27
+ value: parseAutoModerationConfiguration(stored.value),
28
+ revision: stored.version.toString(),
29
+ }
30
+ : { value: DEFAULT_AUTOMOD_CONFIGURATION, revision: "0" };
31
+ }
32
+ export function definitionFor(ruleId, draft, configuration, enabled) {
33
+ return toNativeDefinition({
34
+ ruleId,
35
+ draft,
36
+ enabled,
37
+ globalIgnoredRoleIds: configuration.defaultExemptRoleIds,
38
+ globalIgnoredChannelIds: configuration.defaultExemptChannelIds,
39
+ defaultAlertChannelId: configuration.defaultNativeAlertChannelId,
40
+ });
41
+ }
42
+ export function snapshotDefinition(snapshot) {
43
+ return {
44
+ name: snapshot.name,
45
+ enabled: snapshot.enabled,
46
+ eventType: snapshot.eventType,
47
+ trigger: snapshot.trigger,
48
+ actions: snapshot.actions,
49
+ exemptRoleIds: snapshot.exemptRoleIds,
50
+ exemptChannelIds: snapshot.exemptChannelIds,
51
+ };
52
+ }
53
+ export function publicationError(outcome, safeCode) {
54
+ const explanations = {
55
+ capacity_exceeded: "Discord has no remaining capacity for this Auto Moderation rule type.",
56
+ missing_permission: "Helyx is missing a Discord permission required to publish this rule.",
57
+ invalid_rule: "Discord rejected this Auto Moderation rule definition.",
58
+ ambiguous: "Discord did not confirm whether the rule changed. Review the mapping before retrying.",
59
+ already_absent: "The owned Discord rule is missing.",
60
+ };
61
+ return new Error(explanations[outcome] ??
62
+ `The Discord Auto Moderation operation failed${safeCode ? ` (${safeCode})` : ""}.`);
63
+ }
64
+ export function mutationSafeCode(result) {
65
+ return "safeCode" in result && typeof result.safeCode === "string"
66
+ ? result.safeCode
67
+ : undefined;
68
+ }
69
+ //# sourceMappingURL=publication-support.js.map
@@ -0,0 +1,32 @@
1
+ import { type DiscordAutoModerationRuleSnapshot, type ServiceAccess } from "@helyx/sdk";
2
+ import { type AutoModerationRuleDraft } from "./domain.js";
3
+ import { AutoModerationRepository, type StoredAutoModerationRule } from "./repository.js";
4
+ export declare class AutoModerationPublicationService {
5
+ #private;
6
+ private readonly onEnhancedConfigurationChanged;
7
+ constructor(onEnhancedConfigurationChanged?: (services: ServiceAccess) => void | Promise<void>);
8
+ repository(services: ServiceAccess): AutoModerationRepository;
9
+ publish(input: {
10
+ services: ServiceAccess;
11
+ current: StoredAutoModerationRule;
12
+ draft: AutoModerationRuleDraft;
13
+ actorUserId: string;
14
+ revalidate(): Promise<boolean>;
15
+ }): Promise<StoredAutoModerationRule>;
16
+ disable(input: {
17
+ services: ServiceAccess;
18
+ current: StoredAutoModerationRule;
19
+ actorUserId: string;
20
+ revalidate(): Promise<boolean>;
21
+ }): Promise<StoredAutoModerationRule>;
22
+ cleanup(input: {
23
+ services: ServiceAccess;
24
+ current: StoredAutoModerationRule;
25
+ actorUserId: string;
26
+ revalidate(): Promise<boolean>;
27
+ }): Promise<StoredAutoModerationRule>;
28
+ observeNativeRule(services: ServiceAccess, snapshot: DiscordAutoModerationRuleSnapshot | null, guildId: string, nativeRuleId: string): Promise<void>;
29
+ synchronizeOwnedRules(services: ServiceAccess, guildId: string, moduleEnabled: boolean): Promise<void>;
30
+ }
31
+ export { currentConfiguration, definitionFor } from "./publication-support.js";
32
+ //# sourceMappingURL=publication.d.ts.map
@@ -0,0 +1,333 @@
1
+ import { HELYX_SERVICE_NAMES, } from "@helyx/sdk";
2
+ import { AUTOMOD_MODULE_ID } from "./constants.js";
3
+ import { canonicalRuleFingerprint, nativeRuleOwnershipMarker, } from "./domain.js";
4
+ import { currentConfiguration, definitionFor, isKindEnabled, mutationSafeCode, publicationError, snapshotDefinition, validateKindEnabled, } from "./publication-support.js";
5
+ import { AutoModerationRepository, } from "./repository.js";
6
+ export class AutoModerationPublicationService {
7
+ onEnhancedConfigurationChanged;
8
+ #mutatingNativeRules = new Set();
9
+ constructor(onEnhancedConfigurationChanged = () => { }) {
10
+ this.onEnhancedConfigurationChanged = onEnhancedConfigurationChanged;
11
+ }
12
+ repository(services) {
13
+ return new AutoModerationRepository(services);
14
+ }
15
+ async publish(input) {
16
+ const configuration = await currentConfiguration(input.services, input.current.guildId);
17
+ validateKindEnabled(input.draft, configuration.value);
18
+ if (!(await this.#revalidatePublication(input, configuration.revision)))
19
+ throw new Error("The rule, settings or your permission changed. Reload and try again.");
20
+ const definition = definitionFor(input.current.ruleId, input.draft, configuration.value, true);
21
+ const native = input.services.get(HELYX_SERVICE_NAMES.discordAutoModeration);
22
+ return input.current.nativeRuleId
23
+ ? this.#updateNative(input, native, definition, configuration.revision)
24
+ : this.#createNative(input, native, definition, configuration.revision);
25
+ }
26
+ async #createNative(input, native, definition, configurationRevision) {
27
+ const ownershipMarker = `${nativeRuleOwnershipMarker(input.current.ruleId)} `;
28
+ const existing = await native
29
+ .list({ guildId: input.current.guildId })
30
+ .catch(() => {
31
+ throw new Error("Discord rule inspection is temporarily unavailable. Try again.");
32
+ });
33
+ if (existing.some((rule) => rule.name.startsWith(ownershipMarker) &&
34
+ canonicalRuleFingerprint(snapshotDefinition(rule)) !==
35
+ canonicalRuleFingerprint(definition)))
36
+ throw new Error("An unconfirmed Discord rule already exists for this rule. Review its Discord settings before retrying Save.");
37
+ const result = await native.create({
38
+ guildId: input.current.guildId,
39
+ callerModuleId: AUTOMOD_MODULE_ID,
40
+ operationKey: input.current.operationKey,
41
+ rule: definition,
42
+ revalidate: () => this.#revalidatePublication(input, configurationRevision),
43
+ });
44
+ if ((result.outcome !== "succeeded" &&
45
+ result.outcome !== "already_applied") ||
46
+ !result.rule)
47
+ throw publicationError(result.outcome, mutationSafeCode(result));
48
+ const stored = await this.repository(input.services)
49
+ .updateRule({
50
+ current: input.current,
51
+ draft: input.draft,
52
+ status: "published",
53
+ nativeRuleId: result.rule.ruleId,
54
+ nativeFingerprint: canonicalRuleFingerprint(definition),
55
+ configurationRevision,
56
+ actorUserId: input.actorUserId,
57
+ action: "automod.rule-published",
58
+ occurredAt: new Date(),
59
+ })
60
+ .catch(() => null);
61
+ if (!stored) {
62
+ if (result.outcome === "succeeded")
63
+ await native.delete({
64
+ guildId: input.current.guildId,
65
+ ruleId: result.rule.ruleId,
66
+ callerModuleId: AUTOMOD_MODULE_ID,
67
+ operationKey: `${input.current.operationKey}:compensate`,
68
+ revalidate: async () => {
69
+ const latest = await this.repository(input.services).findRule(input.current.guildId, input.current.ruleId);
70
+ return Boolean(latest && latest.nativeRuleId !== result.rule.ruleId);
71
+ },
72
+ });
73
+ throw new Error("The rule changed while it was being published. Discord was compensated where safe.");
74
+ }
75
+ await this.onEnhancedConfigurationChanged(input.services);
76
+ return stored;
77
+ }
78
+ async #updateNative(input, native, definition, configurationRevision) {
79
+ const nativeRuleId = input.current.nativeRuleId;
80
+ const mutationKey = `${input.current.guildId}:${nativeRuleId}`;
81
+ this.#mutatingNativeRules.add(mutationKey);
82
+ try {
83
+ const previous = await native.read({
84
+ guildId: input.current.guildId,
85
+ ruleId: nativeRuleId,
86
+ });
87
+ if (!previous) {
88
+ await this.#mark(input.services, input.current, "missing", "automod.rule-missing");
89
+ throw new Error("The owned Discord rule is missing. Review it before creating a replacement.");
90
+ }
91
+ const result = await native.update({
92
+ guildId: input.current.guildId,
93
+ ruleId: nativeRuleId,
94
+ callerModuleId: AUTOMOD_MODULE_ID,
95
+ operationKey: input.current.operationKey,
96
+ rule: definition,
97
+ revalidate: () => this.#revalidatePublication(input, configurationRevision),
98
+ });
99
+ if (result.outcome !== "succeeded" &&
100
+ result.outcome !== "already_applied")
101
+ throw publicationError(result.outcome, mutationSafeCode(result));
102
+ const stored = await this.repository(input.services)
103
+ .updateRule({
104
+ current: input.current,
105
+ draft: input.draft,
106
+ status: "published",
107
+ nativeRuleId,
108
+ nativeFingerprint: canonicalRuleFingerprint(definition),
109
+ configurationRevision,
110
+ actorUserId: input.actorUserId,
111
+ action: "automod.rule-published",
112
+ occurredAt: new Date(),
113
+ })
114
+ .catch(() => null);
115
+ if (!stored) {
116
+ if (result.outcome === "succeeded")
117
+ await native.update({
118
+ guildId: input.current.guildId,
119
+ ruleId: nativeRuleId,
120
+ callerModuleId: AUTOMOD_MODULE_ID,
121
+ operationKey: `${input.current.operationKey}:compensate`,
122
+ rule: snapshotDefinition(previous),
123
+ revalidate: async () => {
124
+ const latest = await this.repository(input.services).findRule(input.current.guildId, input.current.ruleId);
125
+ const actual = await native.read({
126
+ guildId: input.current.guildId,
127
+ ruleId: nativeRuleId,
128
+ });
129
+ return Boolean(latest &&
130
+ latest.nativeRuleId === nativeRuleId &&
131
+ latest.nativeFingerprint === input.current.nativeFingerprint &&
132
+ actual &&
133
+ canonicalRuleFingerprint(snapshotDefinition(actual)) ===
134
+ canonicalRuleFingerprint(definition));
135
+ },
136
+ });
137
+ throw new Error("The rule changed while it was being published. Discord was restored where safe.");
138
+ }
139
+ await this.onEnhancedConfigurationChanged(input.services);
140
+ return stored;
141
+ }
142
+ finally {
143
+ this.#mutatingNativeRules.delete(mutationKey);
144
+ }
145
+ }
146
+ async #revalidatePublication(input, configurationRevision) {
147
+ const current = await this.repository(input.services).findRule(input.current.guildId, input.current.ruleId);
148
+ const configuration = await currentConfiguration(input.services, input.current.guildId);
149
+ return Boolean(current &&
150
+ current.revision === input.current.revision &&
151
+ configuration.revision === configurationRevision &&
152
+ (await input.revalidate()));
153
+ }
154
+ async disable(input) {
155
+ if (!input.current.nativeRuleId)
156
+ throw new Error("This rule has no owned Discord mapping.");
157
+ const configuration = await currentConfiguration(input.services, input.current.guildId);
158
+ const definition = definitionFor(input.current.ruleId, input.current.draft, configuration.value, false);
159
+ const mutationKey = `${input.current.guildId}:${input.current.nativeRuleId}`;
160
+ this.#mutatingNativeRules.add(mutationKey);
161
+ try {
162
+ const result = await input.services
163
+ .get(HELYX_SERVICE_NAMES.discordAutoModeration)
164
+ .update({
165
+ guildId: input.current.guildId,
166
+ ruleId: input.current.nativeRuleId,
167
+ callerModuleId: AUTOMOD_MODULE_ID,
168
+ operationKey: `${input.current.operationKey}:disable`,
169
+ rule: definition,
170
+ revalidate: () => input.revalidate(),
171
+ });
172
+ if (result.outcome !== "succeeded" &&
173
+ result.outcome !== "already_applied")
174
+ throw publicationError(result.outcome, mutationSafeCode(result));
175
+ const updated = await this.repository(input.services).updateRule({
176
+ current: input.current,
177
+ status: "disabled",
178
+ nativeFingerprint: canonicalRuleFingerprint(definition),
179
+ configurationRevision: configuration.revision,
180
+ actorUserId: input.actorUserId,
181
+ action: "automod.rule-disabled",
182
+ occurredAt: new Date(),
183
+ });
184
+ if (!updated)
185
+ throw new Error("The rule changed while it was being disabled.");
186
+ await this.onEnhancedConfigurationChanged(input.services);
187
+ return updated;
188
+ }
189
+ finally {
190
+ this.#mutatingNativeRules.delete(mutationKey);
191
+ }
192
+ }
193
+ async cleanup(input) {
194
+ if (!input.current.nativeRuleId)
195
+ return input.current;
196
+ const mutationKey = `${input.current.guildId}:${input.current.nativeRuleId}`;
197
+ this.#mutatingNativeRules.add(mutationKey);
198
+ try {
199
+ const result = await input.services
200
+ .get(HELYX_SERVICE_NAMES.discordAutoModeration)
201
+ .delete({
202
+ guildId: input.current.guildId,
203
+ ruleId: input.current.nativeRuleId,
204
+ callerModuleId: AUTOMOD_MODULE_ID,
205
+ operationKey: `${input.current.operationKey}:cleanup`,
206
+ revalidate: () => input.revalidate(),
207
+ });
208
+ if (result.outcome !== "succeeded" && result.outcome !== "already_absent")
209
+ throw publicationError(result.outcome, mutationSafeCode(result));
210
+ const updated = await this.repository(input.services).updateRule({
211
+ current: input.current,
212
+ status: "draft",
213
+ nativeRuleId: null,
214
+ nativeFingerprint: null,
215
+ actorUserId: input.actorUserId,
216
+ action: "automod.rule-native-cleaned",
217
+ occurredAt: new Date(),
218
+ });
219
+ if (!updated)
220
+ throw new Error("The rule changed while cleanup completed.");
221
+ await this.onEnhancedConfigurationChanged(input.services);
222
+ return updated;
223
+ }
224
+ finally {
225
+ this.#mutatingNativeRules.delete(mutationKey);
226
+ }
227
+ }
228
+ async observeNativeRule(services, snapshot, guildId, nativeRuleId) {
229
+ if (this.#mutatingNativeRules.has(`${guildId}:${nativeRuleId}`))
230
+ return;
231
+ const current = await this.repository(services).findByNativeRuleId(guildId, nativeRuleId);
232
+ if (!current)
233
+ return;
234
+ if (!snapshot) {
235
+ await this.#mark(services, current, "missing", "automod.rule-missing");
236
+ return;
237
+ }
238
+ const configuration = await currentConfiguration(services, guildId);
239
+ const expected = definitionFor(current.ruleId, current.draft, configuration.value, current.status !== "disabled" &&
240
+ isKindEnabled(current.draft, configuration.value));
241
+ if (canonicalRuleFingerprint(snapshotDefinition(snapshot)) !==
242
+ canonicalRuleFingerprint(expected))
243
+ await this.#mark(services, current, "drifted", "automod.rule-drifted");
244
+ }
245
+ async synchronizeOwnedRules(services, guildId, moduleEnabled) {
246
+ let cursor;
247
+ do {
248
+ const page = await this.repository(services).listRules({
249
+ guildId,
250
+ limit: 50,
251
+ ...(cursor ? { cursor } : {}),
252
+ });
253
+ for (const current of page.items) {
254
+ if (!current.nativeRuleId ||
255
+ current.status === "missing" ||
256
+ current.status === "review")
257
+ continue;
258
+ const mutationKey = `${guildId}:${current.nativeRuleId}`;
259
+ this.#mutatingNativeRules.add(mutationKey);
260
+ try {
261
+ const configuration = await currentConfiguration(services, guildId);
262
+ const enabled = moduleEnabled &&
263
+ current.status === "published" &&
264
+ isKindEnabled(current.draft, configuration.value);
265
+ const definition = definitionFor(current.ruleId, current.draft, configuration.value, enabled);
266
+ const result = await services
267
+ .get(HELYX_SERVICE_NAMES.discordAutoModeration)
268
+ .update({
269
+ guildId,
270
+ ruleId: current.nativeRuleId,
271
+ callerModuleId: AUTOMOD_MODULE_ID,
272
+ operationKey: `${current.operationKey}:${moduleEnabled ? "synchronise" : "deactivate"}`,
273
+ rule: definition,
274
+ revalidate: () => Promise.resolve(true),
275
+ });
276
+ if (result.outcome === "succeeded" ||
277
+ result.outcome === "already_applied")
278
+ await this.repository(services).updateRule({
279
+ current,
280
+ status: current.status,
281
+ nativeFingerprint: canonicalRuleFingerprint(definition),
282
+ configurationRevision: configuration.revision,
283
+ actorUserId: null,
284
+ action: moduleEnabled
285
+ ? "automod.rule-synchronised"
286
+ : "automod.rule-deactivated",
287
+ occurredAt: new Date(),
288
+ });
289
+ else
290
+ await this.#mark(services, current, "review", "automod.rule-sync-failed");
291
+ }
292
+ catch {
293
+ await this.#mark(services, current, "review", "automod.rule-sync-failed").catch(() => undefined);
294
+ }
295
+ finally {
296
+ this.#mutatingNativeRules.delete(mutationKey);
297
+ }
298
+ }
299
+ cursor = page.nextCursor ?? undefined;
300
+ } while (cursor);
301
+ await this.onEnhancedConfigurationChanged(services);
302
+ }
303
+ async #mark(services, current, status, action) {
304
+ const updated = await this.repository(services).updateRule({
305
+ current,
306
+ status,
307
+ actorUserId: null,
308
+ action,
309
+ occurredAt: new Date(),
310
+ });
311
+ if (updated &&
312
+ status === "drifted" &&
313
+ services.has(HELYX_SERVICE_NAMES.moduleLogging))
314
+ await services
315
+ .get(HELYX_SERVICE_NAMES.moduleLogging)
316
+ .emit({
317
+ guildId: current.guildId,
318
+ moduleId: AUTOMOD_MODULE_ID,
319
+ eventId: "automod-rule-drifted",
320
+ summary: "A Helyx-owned Discord Auto Moderation rule drifted.",
321
+ details: [
322
+ { label: "Rule", value: current.ruleId },
323
+ { label: "Outcome", value: "Manager review required" },
324
+ ],
325
+ idempotencyKey: `${current.ruleId}:${current.revision}:drift`,
326
+ })
327
+ .catch(() => undefined);
328
+ if (updated)
329
+ await this.onEnhancedConfigurationChanged(services);
330
+ }
331
+ }
332
+ export { currentConfiguration, definitionFor } from "./publication-support.js";
333
+ //# sourceMappingURL=publication.js.map
@@ -0,0 +1,11 @@
1
+ import { type ServiceAccess } from "@helyx/sdk";
2
+ /** Counts an observed receipt once through the transactional record contract. */
3
+ export declare function countAutoModerationReceipt(services: ServiceAccess, input: {
4
+ guildId: string;
5
+ ruleId: string;
6
+ eventKey: string;
7
+ source: "native" | "enhanced";
8
+ actionType: string;
9
+ occurredAt: Date;
10
+ }): Promise<void>;
11
+ //# sourceMappingURL=receipt-statistics.d.ts.map
@@ -0,0 +1,58 @@
1
+ import { HELYX_SERVICE_NAMES, } from "@helyx/sdk";
2
+ import { createHash } from "node:crypto";
3
+ import { AUTOMOD_LIMITS, AUTOMOD_MODULE_ID } from "./constants.js";
4
+ /** Counts an observed receipt once through the transactional record contract. */
5
+ export async function countAutoModerationReceipt(services, input) {
6
+ if (!services.has(HELYX_SERVICE_NAMES.records))
7
+ return;
8
+ const day = input.occurredAt.toISOString().slice(0, 10);
9
+ const retentionAt = new Date(`${day}T00:00:00.000Z`);
10
+ retentionAt.setUTCDate(retentionAt.getUTCDate() + AUTOMOD_LIMITS.settledClaimRetentionDays + 1);
11
+ const identity = {
12
+ guild_id: input.guildId,
13
+ rule_id: input.ruleId,
14
+ day,
15
+ source: input.source,
16
+ action_type: input.actionType,
17
+ };
18
+ // The numeric record API requires a UUID, not the receipt's string key.
19
+ // A namespaced UUIDv8 keeps retries deterministic without exposing content.
20
+ const bytes = createHash("sha256")
21
+ .update(JSON.stringify([AUTOMOD_MODULE_ID, "daily-count", input.eventKey]))
22
+ .digest()
23
+ .subarray(0, 16);
24
+ bytes[6] = (bytes[6] & 0x0f) | 0x80;
25
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
26
+ const hex = bytes.toString("hex");
27
+ const operationId = `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
28
+ await services
29
+ .get(HELYX_SERVICE_NAMES.records)
30
+ .mutateNumericBatch({
31
+ moduleId: AUTOMOD_MODULE_ID,
32
+ collection: "daily_counts",
33
+ operationId,
34
+ uniqueBy: ["guild_id", "rule_id", "day", "source", "action_type"],
35
+ mutations: [
36
+ {
37
+ identity,
38
+ insert: {
39
+ ...identity,
40
+ observed_count: 0,
41
+ last_observed_at: input.occurredAt,
42
+ },
43
+ increments: { observed_count: 1 },
44
+ maxima: { last_observed_at: input.occurredAt },
45
+ },
46
+ ],
47
+ select: ["observed_count", "last_observed_at"],
48
+ retention: [
49
+ {
50
+ collection: "daily_counts",
51
+ where: { guild_id: input.guildId, day },
52
+ scheduledFor: retentionAt,
53
+ idempotencyKey: `daily-counts:${input.guildId}:${day}`,
54
+ },
55
+ ],
56
+ });
57
+ }
58
+ //# sourceMappingURL=receipt-statistics.js.map