@helyx/bot 1.1.0 → 1.2.0

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 (85) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +16 -1
  3. package/dist/auto-moderation-scanner-registry.d.ts +27 -0
  4. package/dist/auto-moderation-scanner-registry.js +172 -0
  5. package/dist/cloud-connector.js +3 -0
  6. package/dist/cloud-control-dispatcher.js +32 -0
  7. package/dist/command-path-contract.js +1 -0
  8. package/dist/control-permissions.d.ts +2 -2
  9. package/dist/control-permissions.js +4 -2
  10. package/dist/control-server.d.ts +72 -16
  11. package/dist/control-server.js +150 -250
  12. package/dist/dashboard-action-control.d.ts +25 -0
  13. package/dist/dashboard-action-control.js +73 -0
  14. package/dist/dashboard-action-routes.d.ts +9 -0
  15. package/dist/dashboard-action-routes.js +36 -0
  16. package/dist/dashboard-feature-control.d.ts +37 -0
  17. package/dist/dashboard-feature-control.js +206 -0
  18. package/dist/dashboard-module-projection.d.ts +9 -0
  19. package/dist/dashboard-module-projection.js +15 -0
  20. package/dist/dashboard-preview-import-routes.d.ts +6 -0
  21. package/dist/dashboard-preview-import-routes.js +121 -0
  22. package/dist/discord-analytics.d.ts +3 -0
  23. package/dist/discord-analytics.js +49 -5
  24. package/dist/discord-auto-moderation-service.d.ts +21 -0
  25. package/dist/discord-auto-moderation-service.js +425 -0
  26. package/dist/discord-guild-health.d.ts +2 -2
  27. package/dist/discord-member-enforcement-service.d.ts +26 -0
  28. package/dist/discord-member-enforcement-service.js +590 -0
  29. package/dist/discord-message-enforcement-service.d.ts +9 -0
  30. package/dist/discord-message-enforcement-service.js +102 -0
  31. package/dist/discord-violation-thread-service.d.ts +33 -0
  32. package/dist/discord-violation-thread-service.js +789 -0
  33. package/dist/ecko-evidence-receiver.d.ts +32 -0
  34. package/dist/ecko-evidence-receiver.js +173 -0
  35. package/dist/ecko-transport-auth.d.ts +19 -0
  36. package/dist/ecko-transport-auth.js +80 -0
  37. package/dist/ecko-transport-routes.d.ts +19 -0
  38. package/dist/ecko-transport-routes.js +130 -0
  39. package/dist/embedded-auto-moderation-pool.d.ts +46 -0
  40. package/dist/embedded-auto-moderation-pool.js +324 -0
  41. package/dist/embedded-auto-moderation-scanner.d.ts +34 -0
  42. package/dist/embedded-auto-moderation-scanner.js +263 -0
  43. package/dist/embedded-auto-moderation-worker-runtime.d.ts +37 -0
  44. package/dist/embedded-auto-moderation-worker-runtime.js +37 -0
  45. package/dist/embedded-auto-moderation-worker.d.ts +2 -0
  46. package/dist/embedded-auto-moderation-worker.js +13 -0
  47. package/dist/events.d.ts +5 -1
  48. package/dist/events.js +243 -4
  49. package/dist/guilds.d.ts +2 -2
  50. package/dist/guilds.js +14 -8
  51. package/dist/hosted-server-moderation.d.ts +16 -5
  52. package/dist/hosted-server-moderation.js +32 -4
  53. package/dist/index.d.ts +1 -1
  54. package/dist/index.js +84 -14
  55. package/dist/moderation-action-registry.d.ts +11 -0
  56. package/dist/moderation-action-registry.js +375 -0
  57. package/dist/moderation-case-disassociation.d.ts +13 -0
  58. package/dist/moderation-case-disassociation.js +83 -0
  59. package/dist/moderation-platform-services.d.ts +27 -0
  60. package/dist/moderation-platform-services.js +47 -0
  61. package/dist/modules.d.ts +6 -0
  62. package/dist/modules.js +65 -3
  63. package/dist/privileged-message-metadata-cache.d.ts +35 -0
  64. package/dist/privileged-message-metadata-cache.js +70 -0
  65. package/dist/scheduled-task-runner.js +24 -1
  66. package/dist/shard-contracts.d.ts +151 -1
  67. package/dist/shard-contracts.js +142 -1
  68. package/dist/shard-coordinator.d.ts +1 -1
  69. package/dist/shard-ecko-scanner-registry.d.ts +38 -0
  70. package/dist/shard-ecko-scanner-registry.js +96 -0
  71. package/dist/shard-router.d.ts +23 -0
  72. package/dist/shard-router.js +69 -5
  73. package/dist/shard-supervisor.d.ts +11 -0
  74. package/dist/shard-supervisor.js +67 -4
  75. package/dist/shard-worker.d.ts +15 -1
  76. package/dist/shard-worker.js +95 -17
  77. package/dist/transient-violation-evidence.d.ts +39 -0
  78. package/dist/transient-violation-evidence.js +132 -0
  79. package/documentation/README.md +2 -0
  80. package/documentation/examples/.env.example +12 -0
  81. package/documentation/guides/self-hosting.md +17 -1
  82. package/documentation/modules/auto-moderation.md +51 -0
  83. package/documentation/modules/moderation.md +42 -0
  84. package/documentation/reference/environment.md +11 -0
  85. package/package.json +17 -5
@@ -0,0 +1,590 @@
1
+ import { PermissionFlagsBits, RESTJSONErrorCodes, } from "discord.js";
2
+ import { DiscordMemberEnforcementService } from "./events.js";
3
+ import { toReplyOptions } from "./interactions.js";
4
+ const SNOWFLAKE_PATTERN = /^\d{17,20}$/u;
5
+ const MODULE_ID_PATTERN = /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)+$/u;
6
+ const OPERATION_KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/u;
7
+ const MAX_TIMEOUT_SECONDS = 2_419_200;
8
+ const MAX_DEMOTION_ROLES = 10;
9
+ // Global and rule-specific exemptions can each contain 25 roles. The guild's
10
+ // @everyone role is included separately by the Moderation caller.
11
+ const MAX_PROTECTED_ROLES = 51;
12
+ const DIRECT_MESSAGE_TIMEOUT_MS = 2_000;
13
+ /** Extended Moderation adapter, leaving the legacy enforcement class unchanged. */
14
+ export class DiscordModerationMemberEnforcementAdapter {
15
+ client;
16
+ #legacy;
17
+ constructor(client) {
18
+ this.client = client;
19
+ this.#legacy = new DiscordMemberEnforcementService(client);
20
+ }
21
+ kick(input) {
22
+ return this.#legacy.kick(input);
23
+ }
24
+ ban(input) {
25
+ return this.#legacy.ban(input);
26
+ }
27
+ async inspectMember(input) {
28
+ validateSnowflake(input.guildId);
29
+ validateSnowflake(input.memberUserId);
30
+ if (input.actorUserId)
31
+ validateSnowflake(input.actorUserId);
32
+ const guild = this.#ownedGuild(input.guildId);
33
+ if (!guild)
34
+ return { outcome: "failed", safeCode: "guild_not_owned" };
35
+ const memberInspection = await fetchMemberInspection(guild, input.memberUserId);
36
+ if (memberInspection.outcome === "missing")
37
+ return { outcome: "member_missing" };
38
+ if (memberInspection.outcome === "failed")
39
+ return { outcome: "failed", safeCode: "member_inspection_failed" };
40
+ const member = memberInspection.member;
41
+ const actorInspection = input.actorUserId
42
+ ? await fetchMemberInspection(guild, input.actorUserId)
43
+ : null;
44
+ const actor = actorInspection?.outcome === "present" ? actorInspection.member : null;
45
+ const me = guild.members.me ?? (await guild.members.fetchMe().catch(() => null));
46
+ return {
47
+ outcome: "present",
48
+ memberUserId: member.id,
49
+ isGuildOwner: member.id === guild.ownerId,
50
+ isAdministrator: member.permissions.has(PermissionFlagsBits.Administrator),
51
+ isBot: member.user.bot,
52
+ roleIds: [...member.roles.cache.keys()].sort(),
53
+ effectivePermissionNames: member.permissions.toArray().sort(),
54
+ actorHierarchy: hierarchy(actor, member, guild.ownerId),
55
+ helyxHierarchy: hierarchy(me, member, guild.ownerId),
56
+ timeoutEndsAt: member.communicationDisabledUntil,
57
+ };
58
+ }
59
+ async inspectBan(input) {
60
+ validateSnowflake(input.guildId);
61
+ validateSnowflake(input.userId);
62
+ const guild = this.#ownedGuild(input.guildId);
63
+ if (!guild)
64
+ return { outcome: "failed", safeCode: "guild_not_owned" };
65
+ if (!guild.members.me?.permissions.has(PermissionFlagsBits.BanMembers))
66
+ return { outcome: "missing_permission", safeCode: "ban_members" };
67
+ try {
68
+ const ban = await guild.bans.fetch(input.userId);
69
+ return ban.user.id === input.userId
70
+ ? { outcome: "banned", userId: ban.user.id }
71
+ : { outcome: "failed", safeCode: "ban_identity_mismatch" };
72
+ }
73
+ catch (error) {
74
+ return hasDiscordCode(error, RESTJSONErrorCodes.UnknownBan)
75
+ ? { outcome: "not_banned" }
76
+ : { outcome: "failed", safeCode: "ban_inspection_failed" };
77
+ }
78
+ }
79
+ async kickMember(input) {
80
+ validateModernInput(input);
81
+ const initial = await this.#inspectKick(input);
82
+ if (initial.outcome !== "ready")
83
+ return initial.result;
84
+ if (!(await safelyRevalidate(() => input.revalidate())))
85
+ return {
86
+ outcome: "no_longer_required",
87
+ directMessage: "not_requested",
88
+ };
89
+ // Re-fetch after policy revalidation so a hierarchy, membership or
90
+ // permission change cannot slip between the check and the Discord effect.
91
+ const current = await this.#inspectKick(input, true);
92
+ if (current.outcome !== "ready")
93
+ return current.result;
94
+ const directMessage = await this.#directMessage(input.memberUserId, input.directMessage);
95
+ try {
96
+ await current.member.kick(auditReason(input));
97
+ return { outcome: "succeeded", directMessage };
98
+ }
99
+ catch (error) {
100
+ return failure(error, directMessage);
101
+ }
102
+ }
103
+ async banMember(input) {
104
+ validateModernInput(input);
105
+ const guild = this.#ownedGuild(input.guildId);
106
+ if (!guild)
107
+ return {
108
+ outcome: "failed",
109
+ directMessage: "not_requested",
110
+ safeCode: "guild_not_owned",
111
+ };
112
+ if (!guild.members.me?.permissions.has(PermissionFlagsBits.BanMembers))
113
+ return {
114
+ outcome: "missing_permission",
115
+ directMessage: "not_requested",
116
+ safeCode: "ban_members",
117
+ };
118
+ const memberInspection = await fetchMemberInspection(guild, input.memberUserId);
119
+ if (memberInspection.outcome === "failed")
120
+ return {
121
+ outcome: "failed",
122
+ directMessage: "not_requested",
123
+ safeCode: "member_inspection_failed",
124
+ };
125
+ const member = memberInspection.outcome === "present" ? memberInspection.member : null;
126
+ if (member && !member.bannable)
127
+ return { outcome: "hierarchy_blocked", directMessage: "not_requested" };
128
+ let existingBan;
129
+ try {
130
+ existingBan = await fetchBan(guild, input.memberUserId);
131
+ }
132
+ catch {
133
+ return {
134
+ outcome: "failed",
135
+ directMessage: "not_requested",
136
+ safeCode: "ban_inspection_failed",
137
+ };
138
+ }
139
+ if (existingBan)
140
+ return { outcome: "already_applied", directMessage: "not_requested" };
141
+ if (!(await safelyRevalidate(() => input.revalidate())))
142
+ return {
143
+ outcome: "no_longer_required",
144
+ directMessage: "not_requested",
145
+ };
146
+ const currentBan = await fetchBan(guild, input.memberUserId).catch(() => undefined);
147
+ if (currentBan === undefined)
148
+ return {
149
+ outcome: "failed",
150
+ directMessage: "not_requested",
151
+ safeCode: "ban_inspection_failed",
152
+ };
153
+ if (currentBan)
154
+ return { outcome: "already_applied", directMessage: "not_requested" };
155
+ const currentMember = await fetchMemberInspection(guild, input.memberUserId, true);
156
+ if (currentMember.outcome === "failed")
157
+ return {
158
+ outcome: "failed",
159
+ directMessage: "not_requested",
160
+ safeCode: "member_inspection_failed",
161
+ };
162
+ if (currentMember.outcome === "present" && !currentMember.member.bannable)
163
+ return { outcome: "hierarchy_blocked", directMessage: "not_requested" };
164
+ const directMessage = await this.#directMessage(input.memberUserId, input.directMessage);
165
+ try {
166
+ await guild.members.ban(input.memberUserId, {
167
+ deleteMessageSeconds: input.deleteMessageSeconds,
168
+ reason: auditReason(input),
169
+ });
170
+ return { outcome: "succeeded", directMessage };
171
+ }
172
+ catch (error) {
173
+ return failure(error, directMessage);
174
+ }
175
+ }
176
+ async timeout(input) {
177
+ validateModernInput(input);
178
+ if (!Number.isInteger(input.durationSeconds) ||
179
+ input.durationSeconds < 60 ||
180
+ input.durationSeconds > MAX_TIMEOUT_SECONDS)
181
+ throw new Error("Member timeout duration is invalid");
182
+ const resolved = await this.#moderatableMember(input);
183
+ if (resolved.outcome)
184
+ return {
185
+ outcome: resolved.outcome,
186
+ directMessage: "not_requested",
187
+ ...(resolved.safeCode ? { safeCode: resolved.safeCode } : {}),
188
+ };
189
+ const expectedEnd = Date.now() + input.durationSeconds * 1_000;
190
+ if (resolved.member.communicationDisabledUntilTimestamp &&
191
+ resolved.member.communicationDisabledUntilTimestamp >= expectedEnd - 1_000)
192
+ return { outcome: "already_applied", directMessage: "not_requested" };
193
+ if (!(await safelyRevalidate(() => input.revalidate())))
194
+ return {
195
+ outcome: "no_longer_required",
196
+ directMessage: "not_requested",
197
+ };
198
+ const current = await this.#moderatableMember(input, true);
199
+ if (current.outcome)
200
+ return {
201
+ outcome: current.outcome,
202
+ directMessage: "not_requested",
203
+ ...(current.safeCode ? { safeCode: current.safeCode } : {}),
204
+ };
205
+ if (current.member.communicationDisabledUntilTimestamp &&
206
+ current.member.communicationDisabledUntilTimestamp >= expectedEnd - 1_000)
207
+ return { outcome: "already_applied", directMessage: "not_requested" };
208
+ const directMessage = await this.#directMessage(input.memberUserId, input.directMessage);
209
+ try {
210
+ await current.member.timeout(input.durationSeconds * 1_000, auditReason(input));
211
+ return { outcome: "succeeded", directMessage };
212
+ }
213
+ catch (error) {
214
+ return failure(error, directMessage);
215
+ }
216
+ }
217
+ async removeTimeout(input) {
218
+ validateModernInput(input);
219
+ const resolved = await this.#moderatableMember(input);
220
+ if (resolved.outcome)
221
+ return withoutDirectMessage(resolved);
222
+ if (!resolved.member.communicationDisabledUntilTimestamp)
223
+ return { outcome: "already_applied" };
224
+ if (!(await safelyRevalidate(() => input.revalidate())))
225
+ return { outcome: "no_longer_required" };
226
+ const current = await this.#moderatableMember(input, true);
227
+ if (current.outcome)
228
+ return withoutDirectMessage(current);
229
+ if (!current.member.communicationDisabledUntilTimestamp)
230
+ return { outcome: "already_applied" };
231
+ try {
232
+ await current.member.timeout(null, auditReason(input));
233
+ return { outcome: "succeeded" };
234
+ }
235
+ catch (error) {
236
+ return withoutDirectMessage(failure(error, "not_requested"));
237
+ }
238
+ }
239
+ async unban(input) {
240
+ validateModernInput({ ...input, memberUserId: input.userId });
241
+ const guild = this.#ownedGuild(input.guildId);
242
+ if (!guild)
243
+ return { outcome: "failed", safeCode: "guild_not_owned" };
244
+ if (!guild.members.me?.permissions.has(PermissionFlagsBits.BanMembers))
245
+ return { outcome: "missing_permission", safeCode: "ban_members" };
246
+ try {
247
+ if (!(await fetchBan(guild, input.userId)))
248
+ return { outcome: "already_applied" };
249
+ }
250
+ catch {
251
+ return { outcome: "failed", safeCode: "ban_inspection_failed" };
252
+ }
253
+ if (!(await safelyRevalidate(() => input.revalidate())))
254
+ return { outcome: "no_longer_required" };
255
+ let currentBan;
256
+ try {
257
+ currentBan = await fetchBan(guild, input.userId);
258
+ }
259
+ catch {
260
+ return { outcome: "failed", safeCode: "ban_inspection_failed" };
261
+ }
262
+ if (!currentBan)
263
+ return { outcome: "already_applied" };
264
+ try {
265
+ await guild.members.unban(input.userId, auditReason(input));
266
+ return { outcome: "succeeded" };
267
+ }
268
+ catch (error) {
269
+ return withoutDirectMessage(failure(error, "not_requested"));
270
+ }
271
+ }
272
+ async removeConfiguredRoles(input) {
273
+ validateModernInput(input);
274
+ validateRoleRemovalInput(input.roleIds, input.protectedRoleIds);
275
+ const initial = await this.#inspectRoleRemoval(input);
276
+ if (initial.outcome !== "ready")
277
+ return initial.result;
278
+ if (initial.removableRoleIds.length === 0)
279
+ return {
280
+ outcome: "already_applied",
281
+ removedRoleIds: [],
282
+ alreadyAbsentRoleIds: initial.alreadyAbsentRoleIds,
283
+ };
284
+ if (!(await safelyRevalidate(() => input.revalidate())))
285
+ return { outcome: "no_longer_required" };
286
+ // Refresh the member and every selected role after policy revalidation. No
287
+ // mutation occurs unless the complete second preflight still succeeds.
288
+ const current = await this.#inspectRoleRemoval(input, true);
289
+ if (current.outcome !== "ready")
290
+ return current.result;
291
+ if (current.removableRoleIds.length === 0)
292
+ return {
293
+ outcome: "already_applied",
294
+ removedRoleIds: [],
295
+ alreadyAbsentRoleIds: current.alreadyAbsentRoleIds,
296
+ };
297
+ try {
298
+ await current.member.roles.remove(current.removableRoleIds, auditReason(input));
299
+ return {
300
+ outcome: "succeeded",
301
+ removedRoleIds: current.removableRoleIds,
302
+ alreadyAbsentRoleIds: current.alreadyAbsentRoleIds,
303
+ };
304
+ }
305
+ catch (error) {
306
+ if (hasDiscordCode(error, RESTJSONErrorCodes.UnknownMember))
307
+ return { outcome: "member_missing" };
308
+ if (isMissingPermissionError(error))
309
+ return { outcome: "missing_permission", safeCode: "manage_roles" };
310
+ return typeof error === "object" && error !== null && "code" in error
311
+ ? {
312
+ outcome: "failed",
313
+ safeCode: `discord_${String(error.code)}`.slice(0, 80),
314
+ }
315
+ : { outcome: "ambiguous", safeCode: "discord_transport_unknown" };
316
+ }
317
+ }
318
+ #ownedGuild(guildId) {
319
+ return this.client.guilds.cache.get(guildId) ?? null;
320
+ }
321
+ async #moderatableMember(input, force = false) {
322
+ const guild = this.#ownedGuild(input.guildId);
323
+ if (!guild)
324
+ return { outcome: "failed", safeCode: "guild_not_owned" };
325
+ if (!guild.members.me?.permissions.has(PermissionFlagsBits.ModerateMembers))
326
+ return { outcome: "missing_permission", safeCode: "moderate_members" };
327
+ const inspection = await fetchMemberInspection(guild, input.memberUserId, force);
328
+ if (inspection.outcome === "missing")
329
+ return { outcome: "member_missing" };
330
+ if (inspection.outcome === "failed")
331
+ return { outcome: "failed", safeCode: "member_inspection_failed" };
332
+ const member = inspection.member;
333
+ return member.moderatable
334
+ ? { outcome: null, member }
335
+ : { outcome: "hierarchy_blocked" };
336
+ }
337
+ async #inspectKick(input, force = false) {
338
+ const guild = this.#ownedGuild(input.guildId);
339
+ if (!guild)
340
+ return {
341
+ outcome: "blocked",
342
+ result: {
343
+ outcome: "failed",
344
+ directMessage: "not_requested",
345
+ safeCode: "guild_not_owned",
346
+ },
347
+ };
348
+ const me = guild.members.me ?? (await guild.members.fetchMe().catch(() => null));
349
+ if (!me?.permissions.has(PermissionFlagsBits.KickMembers))
350
+ return {
351
+ outcome: "blocked",
352
+ result: {
353
+ outcome: "missing_permission",
354
+ directMessage: "not_requested",
355
+ safeCode: "kick_members",
356
+ },
357
+ };
358
+ const inspection = await fetchMemberForRoleRemoval(guild, input.memberUserId, force);
359
+ if (inspection.outcome === "missing")
360
+ return {
361
+ outcome: "blocked",
362
+ result: { outcome: "member_missing", directMessage: "not_requested" },
363
+ };
364
+ if (inspection.outcome === "failed")
365
+ return {
366
+ outcome: "blocked",
367
+ result: {
368
+ outcome: "failed",
369
+ directMessage: "not_requested",
370
+ safeCode: "member_inspection_failed",
371
+ },
372
+ };
373
+ if (!inspection.member.kickable)
374
+ return {
375
+ outcome: "blocked",
376
+ result: {
377
+ outcome: "hierarchy_blocked",
378
+ directMessage: "not_requested",
379
+ },
380
+ };
381
+ return { outcome: "ready", member: inspection.member };
382
+ }
383
+ async #inspectRoleRemoval(input, force = false) {
384
+ const guild = this.#ownedGuild(input.guildId);
385
+ if (!guild)
386
+ return {
387
+ outcome: "blocked",
388
+ result: { outcome: "failed", safeCode: "guild_not_owned" },
389
+ };
390
+ const me = guild.members.me ?? (await guild.members.fetchMe().catch(() => null));
391
+ if (!me?.permissions.has(PermissionFlagsBits.ManageRoles))
392
+ return {
393
+ outcome: "blocked",
394
+ result: { outcome: "missing_permission", safeCode: "manage_roles" },
395
+ };
396
+ const memberInspection = await fetchMemberForRoleRemoval(guild, input.memberUserId, force);
397
+ if (memberInspection.outcome === "missing")
398
+ return { outcome: "blocked", result: { outcome: "member_missing" } };
399
+ if (memberInspection.outcome === "failed")
400
+ return {
401
+ outcome: "blocked",
402
+ result: { outcome: "failed", safeCode: "member_inspection_failed" },
403
+ };
404
+ const member = memberInspection.member;
405
+ const alreadyAbsentRoleIds = [];
406
+ const removableRoleIds = [];
407
+ const blockedRoleIds = [];
408
+ const protectedRoleIds = new Set(input.protectedRoleIds);
409
+ for (const roleId of input.roleIds) {
410
+ if (!member.roles.cache.has(roleId)) {
411
+ alreadyAbsentRoleIds.push(roleId);
412
+ continue;
413
+ }
414
+ const role = await guild.roles.fetch(roleId).catch(() => null);
415
+ if (!role ||
416
+ role.id === guild.roles.everyone.id ||
417
+ protectedRoleIds.has(role.id) ||
418
+ role.managed ||
419
+ !role.editable ||
420
+ role.comparePositionTo(me.roles.highest) >= 0 ||
421
+ !member.manageable) {
422
+ blockedRoleIds.push(roleId);
423
+ continue;
424
+ }
425
+ removableRoleIds.push(roleId);
426
+ }
427
+ if (blockedRoleIds.length > 0)
428
+ return {
429
+ outcome: "blocked",
430
+ result: { outcome: "hierarchy_blocked", blockedRoleIds },
431
+ };
432
+ return {
433
+ outcome: "ready",
434
+ member,
435
+ removableRoleIds,
436
+ alreadyAbsentRoleIds,
437
+ };
438
+ }
439
+ async #directMessage(userId, response) {
440
+ if (!response)
441
+ return "not_requested";
442
+ const attempt = async () => {
443
+ try {
444
+ const user = await this.client.users.fetch(userId);
445
+ await user.send(toReplyOptions({
446
+ ...response,
447
+ ephemeral: false,
448
+ }));
449
+ return "sent";
450
+ }
451
+ catch (error) {
452
+ return hasDiscordCode(error, RESTJSONErrorCodes.CannotSendMessagesToThisUser)
453
+ ? "closed"
454
+ : "failed";
455
+ }
456
+ };
457
+ return await withTimeout(attempt(), DIRECT_MESSAGE_TIMEOUT_MS, "failed");
458
+ }
459
+ }
460
+ async function fetchMemberInspection(guild, userId, force = false) {
461
+ try {
462
+ const member = await guild.members.fetch(force ? { user: userId, force: true } : userId);
463
+ return member.id === userId && member.guild.id === guild.id
464
+ ? { outcome: "present", member }
465
+ : { outcome: "failed" };
466
+ }
467
+ catch (error) {
468
+ return hasDiscordCode(error, RESTJSONErrorCodes.UnknownMember)
469
+ ? { outcome: "missing" }
470
+ : { outcome: "failed" };
471
+ }
472
+ }
473
+ async function fetchMemberForRoleRemoval(guild, userId, force) {
474
+ try {
475
+ const member = await guild.members.fetch({ user: userId, force });
476
+ return member.id === userId && member.guild.id === guild.id
477
+ ? { outcome: "present", member }
478
+ : { outcome: "failed" };
479
+ }
480
+ catch (error) {
481
+ return hasDiscordCode(error, RESTJSONErrorCodes.UnknownMember)
482
+ ? { outcome: "missing" }
483
+ : { outcome: "failed" };
484
+ }
485
+ }
486
+ async function fetchBan(guild, userId) {
487
+ try {
488
+ return await guild.bans.fetch(userId);
489
+ }
490
+ catch (error) {
491
+ if (hasDiscordCode(error, RESTJSONErrorCodes.UnknownBan))
492
+ return null;
493
+ throw error;
494
+ }
495
+ }
496
+ function hierarchy(actor, target, ownerId) {
497
+ if (!actor)
498
+ return "unknown";
499
+ if (actor.id === ownerId)
500
+ return "above";
501
+ return actor.roles.highest.comparePositionTo(target.roles.highest) > 0
502
+ ? "above"
503
+ : "equal_or_below";
504
+ }
505
+ function validateModernInput(input) {
506
+ validateSnowflake(input.guildId);
507
+ validateSnowflake(input.memberUserId);
508
+ if (!MODULE_ID_PATTERN.test(input.moduleId) ||
509
+ !OPERATION_KEY_PATTERN.test(input.operationKey) ||
510
+ input.reason.trim().length < 1 ||
511
+ input.reason.length > 400 ||
512
+ /[\r\n]/u.test(input.reason))
513
+ throw new Error("Member enforcement metadata is invalid");
514
+ }
515
+ function validateRoleRemovalInput(roleIds, protectedRoleIds) {
516
+ if (roleIds.length < 1 ||
517
+ roleIds.length > MAX_DEMOTION_ROLES ||
518
+ protectedRoleIds.length > MAX_PROTECTED_ROLES ||
519
+ new Set(roleIds).size !== roleIds.length ||
520
+ new Set(protectedRoleIds).size !== protectedRoleIds.length)
521
+ throw new Error("Member role removal selection is invalid");
522
+ for (const roleId of [...roleIds, ...protectedRoleIds])
523
+ validateSnowflake(roleId);
524
+ }
525
+ function validateSnowflake(value) {
526
+ if (!SNOWFLAKE_PATTERN.test(value))
527
+ throw new Error("Member enforcement Discord identity is invalid");
528
+ }
529
+ function auditReason(input) {
530
+ return `Helyx ${input.moduleId} ${input.operationKey}: ${input.reason.trim()}`.slice(0, 512);
531
+ }
532
+ async function safelyRevalidate(callback) {
533
+ try {
534
+ return await callback();
535
+ }
536
+ catch {
537
+ return false;
538
+ }
539
+ }
540
+ function failure(error, directMessage) {
541
+ if (hasDiscordCode(error, RESTJSONErrorCodes.UnknownMember))
542
+ return { outcome: "already_applied", directMessage };
543
+ if (typeof error === "object" && error !== null && "code" in error)
544
+ return {
545
+ outcome: "failed",
546
+ directMessage,
547
+ safeCode: `discord_${String(error.code)}`.slice(0, 80),
548
+ };
549
+ return {
550
+ outcome: "ambiguous",
551
+ directMessage,
552
+ safeCode: "discord_transport_unknown",
553
+ };
554
+ }
555
+ function withoutDirectMessage(input) {
556
+ return {
557
+ outcome: input.outcome,
558
+ ...(input.safeCode ? { safeCode: input.safeCode } : {}),
559
+ };
560
+ }
561
+ function hasDiscordCode(error, code) {
562
+ return (typeof error === "object" &&
563
+ error !== null &&
564
+ "code" in error &&
565
+ error.code === code);
566
+ }
567
+ function isMissingPermissionError(error) {
568
+ return (typeof error === "object" &&
569
+ error !== null &&
570
+ "status" in error &&
571
+ error.status === 403);
572
+ }
573
+ async function withTimeout(promise, timeoutMs, fallback) {
574
+ return await new Promise((resolve) => {
575
+ let settled = false;
576
+ const timer = setTimeout(() => {
577
+ settled = true;
578
+ resolve(fallback);
579
+ }, timeoutMs);
580
+ timer.unref();
581
+ void promise.then((value) => {
582
+ if (settled)
583
+ return;
584
+ settled = true;
585
+ clearTimeout(timer);
586
+ resolve(value);
587
+ });
588
+ });
589
+ }
590
+ //# sourceMappingURL=discord-member-enforcement-service.js.map
@@ -0,0 +1,9 @@
1
+ import type { MessageEnforcementService } from "@helyx/sdk";
2
+ import { type Client } from "discord.js";
3
+ /** Deletes only a positively identified member-authored guild message. */
4
+ export declare class DiscordMessageEnforcementAdapter implements MessageEnforcementService {
5
+ private readonly client;
6
+ constructor(client: Client);
7
+ deleteMemberMessage(input: Parameters<MessageEnforcementService["deleteMemberMessage"]>[0]): ReturnType<MessageEnforcementService["deleteMemberMessage"]>;
8
+ }
9
+ //# sourceMappingURL=discord-message-enforcement-service.d.ts.map