@helyx/bot 0.4.8 → 0.4.11
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 +50 -0
- package/dist/cloud-control-dispatcher.js +2 -1
- package/dist/control-server.d.ts +5 -0
- package/dist/control-server.js +106 -14
- package/dist/discord-owned-message-service.js +9 -1
- package/dist/events.d.ts +64 -1
- package/dist/events.js +709 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.js +69 -19
- package/dist/interactions.js +167 -28
- package/dist/management-module.js +42 -10
- package/dist/message-classification.d.ts +5 -0
- package/dist/message-classification.js +75 -0
- package/dist/modules.js +9 -0
- package/dist/numeric-write-behind-proxy.d.ts +62 -0
- package/dist/numeric-write-behind-proxy.js +313 -0
- package/dist/numeric-write-behind-redis.d.ts +123 -0
- package/dist/numeric-write-behind-redis.js +1281 -0
- package/dist/numeric-write-behind.d.ts +5 -0
- package/dist/numeric-write-behind.js +37 -0
- package/dist/scheduled-task-runner.js +11 -5
- package/dist/shard-contracts.d.ts +490 -4
- package/dist/shard-contracts.js +247 -2
- package/dist/shard-supervisor.d.ts +5 -1
- package/dist/shard-supervisor.js +227 -12
- package/dist/shard-worker.d.ts +1 -0
- package/dist/shard-worker.js +51 -7
- package/package.json +9 -9
package/dist/events.js
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import { HELYX_SERVICE_NAMES } from "@helyx/sdk";
|
|
2
|
-
import { ChannelType, Events, PermissionFlagsBits, RESTJSONErrorCodes, } from "discord.js";
|
|
2
|
+
import { ChannelType, Events, GatewayIntentBits, PermissionFlagsBits, RESTJSONErrorCodes, } from "discord.js";
|
|
3
3
|
import { toReplyOptions } from "./interactions.js";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { classifyGuildMessage, unavailableGuildMessageClassification, } from "./message-classification.js";
|
|
4
6
|
export function installModuleEvents(client, modules, logger, services) {
|
|
7
|
+
const privilegedMessageContributions = modules.flatMap((module) => (module.events?.privilegedMessageCreate ?? []).map((contribution) => ({
|
|
8
|
+
moduleId: module.manifest.id,
|
|
9
|
+
contribution,
|
|
10
|
+
})));
|
|
11
|
+
const guildMessageContributions = modules.flatMap((module) => (module.events?.guildMessageCreate ?? []).map((contribution) => ({
|
|
12
|
+
moduleId: module.manifest.id,
|
|
13
|
+
classificationRequested: module.requiredDiscordCapabilities?.includes("message-content") ??
|
|
14
|
+
false,
|
|
15
|
+
contribution,
|
|
16
|
+
})));
|
|
17
|
+
const messageClassificationRequested = modules.some((module) => module.requiredDiscordCapabilities?.includes("message-content"));
|
|
18
|
+
const messageClassificationAvailable = messageClassificationRequested &&
|
|
19
|
+
client.options.intents.has(GatewayIntentBits.MessageContent);
|
|
5
20
|
const reactionContributions = modules.flatMap((module) => (module.events?.messageReactionAdd ?? []).map((contribution) => ({
|
|
6
21
|
moduleId: module.manifest.id,
|
|
7
22
|
contribution,
|
|
@@ -18,6 +33,10 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
18
33
|
moduleId: module.manifest.id,
|
|
19
34
|
contribution,
|
|
20
35
|
})));
|
|
36
|
+
const memberRemoveContributions = modules.flatMap((module) => (module.events?.guildMemberRemove ?? []).map((contribution) => ({
|
|
37
|
+
moduleId: module.manifest.id,
|
|
38
|
+
contribution,
|
|
39
|
+
})));
|
|
21
40
|
const guildRoleDeleteContributions = modules.flatMap((module) => (module.events?.guildRoleDelete ?? []).map((contribution) => ({
|
|
22
41
|
moduleId: module.manifest.id,
|
|
23
42
|
contribution,
|
|
@@ -42,10 +61,13 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
42
61
|
moduleId: module.manifest.id,
|
|
43
62
|
contribution,
|
|
44
63
|
})));
|
|
45
|
-
if (
|
|
64
|
+
if (privilegedMessageContributions.length === 0 &&
|
|
65
|
+
guildMessageContributions.length === 0 &&
|
|
66
|
+
reactionContributions.length === 0 &&
|
|
46
67
|
reactionRemoveContributions.length === 0 &&
|
|
47
68
|
messageDeleteContributions.length === 0 &&
|
|
48
69
|
memberAddContributions.length === 0 &&
|
|
70
|
+
memberRemoveContributions.length === 0 &&
|
|
49
71
|
guildRoleDeleteContributions.length === 0 &&
|
|
50
72
|
guildRoleUpdateContributions.length === 0 &&
|
|
51
73
|
channelDeleteContributions.length === 0 &&
|
|
@@ -54,6 +76,59 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
54
76
|
guildEmojiUpdateContributions.length === 0)
|
|
55
77
|
return { stop: () => Promise.resolve(), queueDepth: () => 0 };
|
|
56
78
|
const queue = new BoundedGuildEventQueue(logger);
|
|
79
|
+
const privilegedEligibilityQueue = privilegedMessageContributions.length > 0
|
|
80
|
+
? new BoundedGuildEventQueue(logger)
|
|
81
|
+
: null;
|
|
82
|
+
const guildMessageQueue = guildMessageContributions.length > 0
|
|
83
|
+
? new BoundedGuildEventQueue(logger)
|
|
84
|
+
: null;
|
|
85
|
+
if (privilegedMessageContributions.length > 0) {
|
|
86
|
+
client.on(Events.MessageCreate, (message) => {
|
|
87
|
+
if (!message.guildId || !privilegedEligibilityQueue)
|
|
88
|
+
return;
|
|
89
|
+
privilegedEligibilityQueue.enqueue(message.guildId, async () => {
|
|
90
|
+
const installations = services.get(HELYX_SERVICE_NAMES.installations);
|
|
91
|
+
const eligible = [];
|
|
92
|
+
for (const item of privilegedMessageContributions) {
|
|
93
|
+
try {
|
|
94
|
+
if ((await installations.isModuleEnabled(message.guildId, item.moduleId)) &&
|
|
95
|
+
(await item.contribution.isEligible({
|
|
96
|
+
serverId: message.guildId,
|
|
97
|
+
channelId: message.channelId,
|
|
98
|
+
services,
|
|
99
|
+
}))) {
|
|
100
|
+
eligible.push(item);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
logger.warn({
|
|
105
|
+
event: "module.privileged_message_eligibility_failed",
|
|
106
|
+
moduleId: item.moduleId,
|
|
107
|
+
error: safeError(error),
|
|
108
|
+
guildId: message.guildId,
|
|
109
|
+
channelId: message.channelId,
|
|
110
|
+
}, "Module privileged message eligibility failed");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (eligible.length > 0) {
|
|
114
|
+
queue.enqueue(message.guildId, () => handlePrivilegedMessageCreate(message, eligible));
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
if (guildMessageContributions.length > 0) {
|
|
120
|
+
client.on(Events.MessageCreate, (message) => {
|
|
121
|
+
if (!message.guildId ||
|
|
122
|
+
!guildMessageQueue ||
|
|
123
|
+
message.author.bot ||
|
|
124
|
+
message.author.system ||
|
|
125
|
+
Boolean(message.webhookId) ||
|
|
126
|
+
message.system) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
guildMessageQueue.enqueue(message.guildId, () => handleGuildMessageCreate(message));
|
|
130
|
+
});
|
|
131
|
+
}
|
|
57
132
|
if (reactionContributions.length > 0) {
|
|
58
133
|
client.on(Events.MessageReactionAdd, (reaction, user, details) => {
|
|
59
134
|
const guildId = reaction.message.guildId;
|
|
@@ -79,6 +154,11 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
79
154
|
queue.enqueue(member.guild.id, () => handleMemberAdd(member));
|
|
80
155
|
});
|
|
81
156
|
}
|
|
157
|
+
if (memberRemoveContributions.length > 0) {
|
|
158
|
+
client.on(Events.GuildMemberRemove, (member) => {
|
|
159
|
+
queue.enqueue(member.guild.id, () => handleMemberRemove(member));
|
|
160
|
+
});
|
|
161
|
+
}
|
|
82
162
|
if (guildRoleDeleteContributions.length > 0) {
|
|
83
163
|
client.on(Events.GuildRoleDelete, (role) => {
|
|
84
164
|
queue.enqueue(role.guild.id, () => handleResourceLifecycle(guildRoleDeleteContributions, role.guild.id, "roleId", role.id, "guild_role_delete"));
|
|
@@ -137,6 +217,116 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
137
217
|
}
|
|
138
218
|
}
|
|
139
219
|
}
|
|
220
|
+
async function handlePrivilegedMessageCreate(message, contributions) {
|
|
221
|
+
if (!message.guildId || !message.channelId)
|
|
222
|
+
return;
|
|
223
|
+
for (const { moduleId, contribution } of contributions) {
|
|
224
|
+
try {
|
|
225
|
+
await contribution.execute({
|
|
226
|
+
serverId: message.guildId,
|
|
227
|
+
channelId: message.channelId,
|
|
228
|
+
messageId: message.id,
|
|
229
|
+
userId: message.author.id,
|
|
230
|
+
userIsBot: message.author.bot,
|
|
231
|
+
webhookId: message.webhookId,
|
|
232
|
+
content: message.content,
|
|
233
|
+
services,
|
|
234
|
+
deleteSource: async () => {
|
|
235
|
+
await message.delete();
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
logger.warn({
|
|
241
|
+
event: "module.privileged_message_create_failed",
|
|
242
|
+
moduleId,
|
|
243
|
+
error: safeError(error),
|
|
244
|
+
messageId: message.id,
|
|
245
|
+
guildId: message.guildId,
|
|
246
|
+
}, "Module privileged message-create event failed");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
async function handleGuildMessageCreate(message) {
|
|
251
|
+
if (!message.guildId || !message.channelId)
|
|
252
|
+
return;
|
|
253
|
+
const installations = services.get(HELYX_SERVICE_NAMES.installations);
|
|
254
|
+
const parentChannelId = message.channel.isThread()
|
|
255
|
+
? (message.channel.parentId ?? undefined)
|
|
256
|
+
: undefined;
|
|
257
|
+
const authorRoleIds = message.member
|
|
258
|
+
? [...message.member.roles.cache.keys()].sort((left, right) => left.localeCompare(right, "en"))
|
|
259
|
+
: [];
|
|
260
|
+
const authorDisplayName = message.member?.displayName ??
|
|
261
|
+
message.author.globalName ??
|
|
262
|
+
message.author.username;
|
|
263
|
+
let classifiedMessage;
|
|
264
|
+
for (const { moduleId, classificationRequested, contribution, } of guildMessageContributions) {
|
|
265
|
+
try {
|
|
266
|
+
if (!(await installations.isModuleEnabled(message.guildId, moduleId))) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
await contribution.execute({
|
|
270
|
+
serverId: message.guildId,
|
|
271
|
+
channelId: message.channelId,
|
|
272
|
+
...(parentChannelId ? { parentChannelId } : {}),
|
|
273
|
+
messageId: message.id,
|
|
274
|
+
authorId: message.author.id,
|
|
275
|
+
authorDisplayName,
|
|
276
|
+
authorRoleIds,
|
|
277
|
+
authorIsBot: false,
|
|
278
|
+
authorIsWebhook: false,
|
|
279
|
+
authorIsSystem: false,
|
|
280
|
+
get classification() {
|
|
281
|
+
if (!classificationRequested || !messageClassificationAvailable)
|
|
282
|
+
return unavailableGuildMessageClassification;
|
|
283
|
+
classifiedMessage ??= classifyGuildMessage(message);
|
|
284
|
+
return classifiedMessage;
|
|
285
|
+
},
|
|
286
|
+
createdAt: message.createdAt,
|
|
287
|
+
services,
|
|
288
|
+
addAuthorRole: async (roleId) => {
|
|
289
|
+
await services
|
|
290
|
+
.get(HELYX_SERVICE_NAMES.memberRoles)
|
|
291
|
+
.addApprovedRole({
|
|
292
|
+
guildId: message.guildId,
|
|
293
|
+
memberUserId: message.author.id,
|
|
294
|
+
roleId,
|
|
295
|
+
moduleId,
|
|
296
|
+
reason: "Guild message contribution author role addition",
|
|
297
|
+
});
|
|
298
|
+
},
|
|
299
|
+
removeAuthorRole: async (roleId) => {
|
|
300
|
+
await services
|
|
301
|
+
.get(HELYX_SERVICE_NAMES.memberRoles)
|
|
302
|
+
.removeApprovedRole({
|
|
303
|
+
guildId: message.guildId,
|
|
304
|
+
memberUserId: message.author.id,
|
|
305
|
+
roleId,
|
|
306
|
+
moduleId,
|
|
307
|
+
reason: "Guild message contribution author role removal",
|
|
308
|
+
});
|
|
309
|
+
},
|
|
310
|
+
sendToChannel: (channelId, response) => services
|
|
311
|
+
.get(HELYX_SERVICE_NAMES.discordMessages)
|
|
312
|
+
.send({
|
|
313
|
+
guildId: message.guildId,
|
|
314
|
+
channelId,
|
|
315
|
+
response,
|
|
316
|
+
}),
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
logger.warn({
|
|
321
|
+
event: "module.guild_message_create_failed",
|
|
322
|
+
moduleId,
|
|
323
|
+
error: safeError(error),
|
|
324
|
+
messageId: message.id,
|
|
325
|
+
guildId: message.guildId,
|
|
326
|
+
}, "Module guild message-create event failed");
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
140
330
|
async function handleReaction(receivedReaction, receivedUser, burst) {
|
|
141
331
|
try {
|
|
142
332
|
const user = receivedUser.partial
|
|
@@ -197,6 +387,8 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
197
387
|
}
|
|
198
388
|
}
|
|
199
389
|
async function handleMemberAdd(member) {
|
|
390
|
+
if (!member.joinedAt)
|
|
391
|
+
throw new Error("Discord member join timestamp is unavailable");
|
|
200
392
|
const installations = services.get(HELYX_SERVICE_NAMES.installations);
|
|
201
393
|
for (const { moduleId, contribution } of memberAddContributions) {
|
|
202
394
|
try {
|
|
@@ -209,6 +401,8 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
209
401
|
memberId: member.id,
|
|
210
402
|
memberDisplayName: member.displayName,
|
|
211
403
|
memberIsBot: member.user.bot,
|
|
404
|
+
accountCreatedAt: member.user.createdAt,
|
|
405
|
+
joinedAt: member.joinedAt,
|
|
212
406
|
services,
|
|
213
407
|
assignRole: (roleId) => assignMemberRole(member, roleId),
|
|
214
408
|
sendToChannel: (channelId, response) => sendToGuildChannel(member, channelId, response),
|
|
@@ -225,6 +419,33 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
225
419
|
}
|
|
226
420
|
}
|
|
227
421
|
}
|
|
422
|
+
async function handleMemberRemove(member) {
|
|
423
|
+
const installations = services.get(HELYX_SERVICE_NAMES.installations);
|
|
424
|
+
for (const { moduleId, contribution } of memberRemoveContributions) {
|
|
425
|
+
try {
|
|
426
|
+
if (!(await installations.isModuleEnabled(member.guild.id, moduleId))) {
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
await contribution.execute({
|
|
430
|
+
serverId: member.guild.id,
|
|
431
|
+
memberId: member.id,
|
|
432
|
+
memberIsBot: member.user.bot,
|
|
433
|
+
accountCreatedAt: member.user.createdAt,
|
|
434
|
+
joinedAt: member.joinedAt,
|
|
435
|
+
services,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
logger.warn({
|
|
440
|
+
event: "module.member_remove_failed",
|
|
441
|
+
moduleId,
|
|
442
|
+
error: safeError(error),
|
|
443
|
+
memberId: member.id,
|
|
444
|
+
guildId: member.guild.id,
|
|
445
|
+
}, "Module member-remove event failed");
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
228
449
|
async function handleReactionRemove(receivedReaction, receivedUser, burst) {
|
|
229
450
|
try {
|
|
230
451
|
if (receivedUser.bot)
|
|
@@ -293,7 +514,16 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
293
514
|
}
|
|
294
515
|
}
|
|
295
516
|
}
|
|
296
|
-
return {
|
|
517
|
+
return {
|
|
518
|
+
stop: async () => {
|
|
519
|
+
await privilegedEligibilityQueue?.stop();
|
|
520
|
+
await guildMessageQueue?.stop();
|
|
521
|
+
await queue.stop();
|
|
522
|
+
},
|
|
523
|
+
queueDepth: () => queue.queueDepth() +
|
|
524
|
+
(privilegedEligibilityQueue?.queueDepth() ?? 0) +
|
|
525
|
+
(guildMessageQueue?.queueDepth() ?? 0),
|
|
526
|
+
};
|
|
297
527
|
}
|
|
298
528
|
class BoundedGuildEventQueue {
|
|
299
529
|
logger;
|
|
@@ -405,11 +635,183 @@ const dangerousRolePermissionNames = new Set([
|
|
|
405
635
|
"MUTE_MEMBERS",
|
|
406
636
|
"VIEW_AUDIT_LOG",
|
|
407
637
|
]);
|
|
638
|
+
const directMessageAttemptTimeoutMs = 5_000;
|
|
639
|
+
const guildChannelAudienceInspectionLimit = 500;
|
|
640
|
+
export class DiscordMessageDeliveryService {
|
|
641
|
+
client;
|
|
642
|
+
constructor(client) {
|
|
643
|
+
this.client = client;
|
|
644
|
+
}
|
|
645
|
+
async send(input) {
|
|
646
|
+
validateSnowflake(input.guildId, "guild");
|
|
647
|
+
validateSnowflake(input.channelId, "channel");
|
|
648
|
+
const guild = await this.client.guilds.fetch(input.guildId);
|
|
649
|
+
if (guild.id !== input.guildId)
|
|
650
|
+
throw new Error("Discord guild validation failed");
|
|
651
|
+
const channel = await guild.channels.fetch(input.channelId);
|
|
652
|
+
if (!channel ||
|
|
653
|
+
channel.guild.id !== guild.id ||
|
|
654
|
+
!channel.isSendable() ||
|
|
655
|
+
channel.isDMBased()) {
|
|
656
|
+
throw new Error("Discord message channel is unavailable");
|
|
657
|
+
}
|
|
658
|
+
const me = guild.members.me ?? (await guild.members.fetchMe());
|
|
659
|
+
const effective = channel.permissionsFor(me);
|
|
660
|
+
if (!effective?.has(PermissionFlagsBits.ViewChannel) ||
|
|
661
|
+
!effective.has(PermissionFlagsBits.SendMessages)) {
|
|
662
|
+
throw new Error("Discord message channel permissions are unavailable");
|
|
663
|
+
}
|
|
664
|
+
const options = {
|
|
665
|
+
...toReplyOptions({ ...input.response, ephemeral: false }),
|
|
666
|
+
};
|
|
667
|
+
if (input.idempotencyKey) {
|
|
668
|
+
const nonce = createHash("sha256")
|
|
669
|
+
.update(input.idempotencyKey)
|
|
670
|
+
.digest("hex")
|
|
671
|
+
.slice(0, 24);
|
|
672
|
+
options.nonce = nonce;
|
|
673
|
+
options.enforceNonce = true;
|
|
674
|
+
}
|
|
675
|
+
const message = await channel.send(options);
|
|
676
|
+
if (message.guildId !== guild.id ||
|
|
677
|
+
message.channelId !== channel.id ||
|
|
678
|
+
!discordSnowflake.test(message.id)) {
|
|
679
|
+
throw new Error("Discord sent message validation failed");
|
|
680
|
+
}
|
|
681
|
+
return { messageId: message.id };
|
|
682
|
+
}
|
|
683
|
+
async sendDirect(input) {
|
|
684
|
+
validateSnowflake(input.userId, "user");
|
|
685
|
+
try {
|
|
686
|
+
const user = await this.client.users.fetch(input.userId);
|
|
687
|
+
if (user.id !== input.userId)
|
|
688
|
+
throw new Error("Discord user validation failed");
|
|
689
|
+
const options = toReplyOptions({ ...input.response, ephemeral: false });
|
|
690
|
+
const message = await user.send(options);
|
|
691
|
+
if (!discordSnowflake.test(message.id)) {
|
|
692
|
+
return { outcome: "failed" };
|
|
693
|
+
}
|
|
694
|
+
return { outcome: "sent", messageId: message.id };
|
|
695
|
+
}
|
|
696
|
+
catch (error) {
|
|
697
|
+
return {
|
|
698
|
+
outcome: isClosedDirectMessage(error) ? "closed" : "failed",
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
export class DiscordMemberEnforcementService {
|
|
704
|
+
client;
|
|
705
|
+
constructor(client) {
|
|
706
|
+
this.client = client;
|
|
707
|
+
}
|
|
708
|
+
async kick(input) {
|
|
709
|
+
validateEnforcementMetadata(input);
|
|
710
|
+
const directMessage = input.directMessage
|
|
711
|
+
? await this.#sendBestEffortDirectMessage(input.memberUserId, input.directMessage)
|
|
712
|
+
: "not_requested";
|
|
713
|
+
const guild = await this.client.guilds
|
|
714
|
+
.fetch(input.guildId)
|
|
715
|
+
.catch(() => null);
|
|
716
|
+
if (!guild)
|
|
717
|
+
return { outcome: "failed", directMessage };
|
|
718
|
+
if (guild.id !== input.guildId)
|
|
719
|
+
throw new Error("Discord guild validation failed");
|
|
720
|
+
let member;
|
|
721
|
+
try {
|
|
722
|
+
member = await guild.members.fetch(input.memberUserId);
|
|
723
|
+
}
|
|
724
|
+
catch (error) {
|
|
725
|
+
if (isUnknownMember(error))
|
|
726
|
+
return { outcome: "member_missing", directMessage };
|
|
727
|
+
return { outcome: "failed", directMessage };
|
|
728
|
+
}
|
|
729
|
+
if (!member)
|
|
730
|
+
return { outcome: "member_missing", directMessage };
|
|
731
|
+
if (member.id !== input.memberUserId || member.guild.id !== guild.id)
|
|
732
|
+
throw new Error("Discord member validation failed");
|
|
733
|
+
const me = guild.members.me ?? (await guild.members.fetchMe());
|
|
734
|
+
if (!me.permissions.has(PermissionFlagsBits.KickMembers)) {
|
|
735
|
+
return { outcome: "missing_permission", directMessage };
|
|
736
|
+
}
|
|
737
|
+
if (!member.kickable) {
|
|
738
|
+
return { outcome: "hierarchy_blocked", directMessage };
|
|
739
|
+
}
|
|
740
|
+
let stillRequired;
|
|
741
|
+
try {
|
|
742
|
+
stillRequired = await input.revalidate();
|
|
743
|
+
}
|
|
744
|
+
catch {
|
|
745
|
+
return { outcome: "failed", directMessage };
|
|
746
|
+
}
|
|
747
|
+
if (!stillRequired) {
|
|
748
|
+
return { outcome: "no_longer_required", directMessage };
|
|
749
|
+
}
|
|
750
|
+
try {
|
|
751
|
+
await member.kick(discordAuditReason(input));
|
|
752
|
+
return { outcome: "kicked", directMessage };
|
|
753
|
+
}
|
|
754
|
+
catch (error) {
|
|
755
|
+
if (isUnknownMember(error)) {
|
|
756
|
+
return { outcome: "member_missing", directMessage };
|
|
757
|
+
}
|
|
758
|
+
return { outcome: "failed", directMessage };
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
async #sendBestEffortDirectMessage(memberUserId, response) {
|
|
762
|
+
const attempt = async () => {
|
|
763
|
+
try {
|
|
764
|
+
const user = await this.client.users.fetch(memberUserId);
|
|
765
|
+
if (user.id !== memberUserId)
|
|
766
|
+
throw new Error("Discord user validation failed");
|
|
767
|
+
const options = toReplyOptions({ ...response, ephemeral: false });
|
|
768
|
+
await user.send(options);
|
|
769
|
+
return "sent";
|
|
770
|
+
}
|
|
771
|
+
catch (error) {
|
|
772
|
+
return isClosedDirectMessage(error) ? "closed" : "failed";
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
return new Promise((resolve) => {
|
|
776
|
+
let settled = false;
|
|
777
|
+
const timer = setTimeout(() => {
|
|
778
|
+
settled = true;
|
|
779
|
+
resolve("failed");
|
|
780
|
+
}, directMessageAttemptTimeoutMs);
|
|
781
|
+
timer.unref();
|
|
782
|
+
void attempt().then((result) => {
|
|
783
|
+
if (settled)
|
|
784
|
+
return;
|
|
785
|
+
settled = true;
|
|
786
|
+
clearTimeout(timer);
|
|
787
|
+
resolve(result);
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
}
|
|
408
792
|
export class DiscordMemberRoleService {
|
|
409
793
|
client;
|
|
410
794
|
constructor(client) {
|
|
411
795
|
this.client = client;
|
|
412
796
|
}
|
|
797
|
+
async createApprovedRole(input) {
|
|
798
|
+
validateRoleCreationMetadata(input);
|
|
799
|
+
const guild = await this.client.guilds.fetch(input.guildId);
|
|
800
|
+
if (guild.id !== input.guildId)
|
|
801
|
+
throw new Error("Discord guild validation failed");
|
|
802
|
+
const me = guild.members.me ?? (await guild.members.fetchMe());
|
|
803
|
+
if (!me.permissions.has(PermissionFlagsBits.ManageRoles))
|
|
804
|
+
throw new Error("Helyx requires Manage Roles to create this reward role");
|
|
805
|
+
const role = await guild.roles.create({
|
|
806
|
+
name: input.name,
|
|
807
|
+
permissions: [],
|
|
808
|
+
reason: discordAuditReason(input),
|
|
809
|
+
});
|
|
810
|
+
if (role.guild.id !== guild.id)
|
|
811
|
+
throw new Error("Created Discord role did not belong to this guild");
|
|
812
|
+
await fetchSafeRole(guild, role.id);
|
|
813
|
+
return { roleId: role.id, name: role.name };
|
|
814
|
+
}
|
|
413
815
|
async getCurrentRoleIds(input) {
|
|
414
816
|
const { member } = await this.fetchGuildAndMember(input);
|
|
415
817
|
return [...member.roles.cache.keys()];
|
|
@@ -452,10 +854,10 @@ export class DiscordMemberRoleService {
|
|
|
452
854
|
const addedRoles = addedIds.map((roleId) => approvedRoles.get(roleId));
|
|
453
855
|
const removedRoles = removedIds.map((roleId) => approvedRoles.get(roleId));
|
|
454
856
|
const reason = discordAuditReason(input);
|
|
455
|
-
if (removedRoles.length > 0)
|
|
456
|
-
await member.roles.remove(removedRoles, reason);
|
|
457
857
|
if (addedRoles.length > 0)
|
|
458
858
|
await member.roles.add(addedRoles, reason);
|
|
859
|
+
if (removedRoles.length > 0)
|
|
860
|
+
await member.roles.remove(removedRoles, reason);
|
|
459
861
|
return { added: addedIds, removed: removedIds };
|
|
460
862
|
}
|
|
461
863
|
async fetchGuildAndMember(input) {
|
|
@@ -475,6 +877,32 @@ export class DiscordResourceInspection {
|
|
|
475
877
|
constructor(client) {
|
|
476
878
|
this.client = client;
|
|
477
879
|
}
|
|
880
|
+
async inspectGuildCapabilities(input) {
|
|
881
|
+
validateSnowflake(input.guildId, "guild");
|
|
882
|
+
const guild = await this.client.guilds.fetch(input.guildId);
|
|
883
|
+
if (guild.id !== input.guildId)
|
|
884
|
+
throw new Error("Discord guild validation failed");
|
|
885
|
+
const me = guild.members.me ?? (await guild.members.fetchMe());
|
|
886
|
+
const effectivePermissionNames = me.permissions
|
|
887
|
+
.toArray()
|
|
888
|
+
.map(normalizePermissionName)
|
|
889
|
+
.sort();
|
|
890
|
+
return {
|
|
891
|
+
guildId: guild.id,
|
|
892
|
+
effectivePermissionNames,
|
|
893
|
+
manageRoles: me.permissions.has(PermissionFlagsBits.ManageRoles),
|
|
894
|
+
kickMembers: me.permissions.has(PermissionFlagsBits.KickMembers),
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
inspectRuntimeCapabilities() {
|
|
898
|
+
const intents = this.client.options.intents;
|
|
899
|
+
return Promise.resolve({
|
|
900
|
+
guildMembers: intents.has(GatewayIntentBits.GuildMembers),
|
|
901
|
+
guildMessages: intents.has(GatewayIntentBits.GuildMessages),
|
|
902
|
+
guildMessageReactions: intents.has(GatewayIntentBits.GuildMessageReactions),
|
|
903
|
+
messageContent: intents.has(GatewayIntentBits.MessageContent),
|
|
904
|
+
});
|
|
905
|
+
}
|
|
478
906
|
async resolveMemberSnapshots(input) {
|
|
479
907
|
const guild = await this.#resolveMemberGuild(input);
|
|
480
908
|
return Promise.all(input.userIds.map(async (userId) => {
|
|
@@ -502,12 +930,41 @@ export class DiscordResourceInspection {
|
|
|
502
930
|
return null;
|
|
503
931
|
throw error;
|
|
504
932
|
});
|
|
933
|
+
const avatarUrl = memberAvatarUrl(member);
|
|
505
934
|
return {
|
|
506
935
|
userId,
|
|
507
936
|
displayName: member?.displayName ?? null,
|
|
937
|
+
...(avatarUrl ? { avatarUrl } : {}),
|
|
508
938
|
};
|
|
509
939
|
}));
|
|
510
940
|
}
|
|
941
|
+
async searchMembers(input) {
|
|
942
|
+
validateSnowflake(input.guildId, "guild");
|
|
943
|
+
const query = input.query.trim();
|
|
944
|
+
if (query.length < 1 || query.length > 100)
|
|
945
|
+
throw new Error("Discord member search query is invalid");
|
|
946
|
+
if (!Number.isSafeInteger(input.limit) ||
|
|
947
|
+
input.limit < 1 ||
|
|
948
|
+
input.limit > 25)
|
|
949
|
+
throw new Error("Discord member search limit is invalid");
|
|
950
|
+
const guild = await this.client.guilds.fetch(input.guildId);
|
|
951
|
+
if (guild.id !== input.guildId)
|
|
952
|
+
throw new Error("Discord guild validation failed");
|
|
953
|
+
const members = await guild.members.search({
|
|
954
|
+
query,
|
|
955
|
+
limit: input.limit,
|
|
956
|
+
cache: false,
|
|
957
|
+
});
|
|
958
|
+
return [...members.values()].map((member) => {
|
|
959
|
+
const avatarUrl = memberAvatarUrl(member);
|
|
960
|
+
return {
|
|
961
|
+
userId: member.id,
|
|
962
|
+
displayName: member.displayName,
|
|
963
|
+
...(avatarUrl ? { avatarUrl } : {}),
|
|
964
|
+
isBot: member.user.bot,
|
|
965
|
+
};
|
|
966
|
+
});
|
|
967
|
+
}
|
|
511
968
|
async #resolveMemberGuild(input) {
|
|
512
969
|
validateSnowflake(input.guildId, "guild");
|
|
513
970
|
if (input.userIds.length > 50 ||
|
|
@@ -574,9 +1031,13 @@ export class DiscordResourceInspection {
|
|
|
574
1031
|
const guild = await this.client.guilds.fetch(input.guildId);
|
|
575
1032
|
if (guild.id !== input.guildId)
|
|
576
1033
|
throw new Error("Discord guild validation failed");
|
|
1034
|
+
const canonicalCustom = /^custom:(\d{17,20})$/u.exec(input.emojiIdentifier);
|
|
1035
|
+
const canonicalUnicode = /^unicode:(.+)$/u.exec(input.emojiIdentifier);
|
|
577
1036
|
const custom = customEmojiIdentifier.exec(input.emojiIdentifier);
|
|
578
|
-
if (!custom) {
|
|
579
|
-
const normalized = input.emojiIdentifier
|
|
1037
|
+
if (!custom && !canonicalCustom) {
|
|
1038
|
+
const normalized = (canonicalUnicode?.[1] ?? input.emojiIdentifier)
|
|
1039
|
+
.normalize("NFKC")
|
|
1040
|
+
.trim();
|
|
580
1041
|
const valid = isSingleUnicodeEmoji(normalized);
|
|
581
1042
|
return {
|
|
582
1043
|
guildId: guild.id,
|
|
@@ -590,7 +1051,9 @@ export class DiscordResourceInspection {
|
|
|
590
1051
|
: {}),
|
|
591
1052
|
};
|
|
592
1053
|
}
|
|
593
|
-
const emojiId = custom[2] ?? custom[4];
|
|
1054
|
+
const emojiId = canonicalCustom?.[1] ?? custom?.[2] ?? custom?.[4];
|
|
1055
|
+
if (!emojiId)
|
|
1056
|
+
throw new Error("Invalid bounded Discord emoji identifier");
|
|
594
1057
|
const emoji = await guild.emojis.fetch(emojiId).catch(() => null);
|
|
595
1058
|
if (!emoji)
|
|
596
1059
|
return {
|
|
@@ -612,7 +1075,7 @@ export class DiscordResourceInspection {
|
|
|
612
1075
|
kind: "custom",
|
|
613
1076
|
canonicalEmojiKey: `custom:${emoji.id}`,
|
|
614
1077
|
emojiId: emoji.id,
|
|
615
|
-
name: emoji.name ?? custom[1] ?? custom[3] ?? "emoji",
|
|
1078
|
+
name: emoji.name ?? custom?.[1] ?? custom?.[3] ?? "emoji",
|
|
616
1079
|
};
|
|
617
1080
|
}
|
|
618
1081
|
async inspectChannel(input) {
|
|
@@ -634,6 +1097,7 @@ export class DiscordResourceInspection {
|
|
|
634
1097
|
sendMessages: false,
|
|
635
1098
|
addReactions: false,
|
|
636
1099
|
readMessageHistory: false,
|
|
1100
|
+
manageMessages: false,
|
|
637
1101
|
};
|
|
638
1102
|
const guildMatches = channel.guild.id === guild.id;
|
|
639
1103
|
const me = guild.members.me ?? (await guild.members.fetchMe());
|
|
@@ -659,8 +1123,198 @@ export class DiscordResourceInspection {
|
|
|
659
1123
|
(effective?.has(PermissionFlagsBits.AddReactions) ?? false),
|
|
660
1124
|
readMessageHistory: guildMatches &&
|
|
661
1125
|
(effective?.has(PermissionFlagsBits.ReadMessageHistory) ?? false),
|
|
1126
|
+
manageMessages: guildMatches &&
|
|
1127
|
+
(effective?.has(PermissionFlagsBits.ManageMessages) ?? false),
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
async inspectMessage(input) {
|
|
1131
|
+
validateSnowflake(input.guildId, "guild");
|
|
1132
|
+
validateSnowflake(input.channelId, "channel");
|
|
1133
|
+
validateSnowflake(input.messageId, "message");
|
|
1134
|
+
const guild = await this.client.guilds.fetch(input.guildId);
|
|
1135
|
+
if (guild.id !== input.guildId)
|
|
1136
|
+
throw new Error("Discord guild validation failed");
|
|
1137
|
+
const channel = await guild.channels.fetch(input.channelId);
|
|
1138
|
+
if (!channel ||
|
|
1139
|
+
channel.guild.id !== guild.id ||
|
|
1140
|
+
!channel.isTextBased() ||
|
|
1141
|
+
channel.isDMBased()) {
|
|
1142
|
+
return missingMessageInspection(input, false);
|
|
1143
|
+
}
|
|
1144
|
+
const message = await channel.messages
|
|
1145
|
+
.fetch(input.messageId)
|
|
1146
|
+
.catch((error) => {
|
|
1147
|
+
if (isUnknownMessage(error))
|
|
1148
|
+
return null;
|
|
1149
|
+
throw error;
|
|
1150
|
+
});
|
|
1151
|
+
if (!message)
|
|
1152
|
+
return missingMessageInspection(input, true);
|
|
1153
|
+
const guildMatches = message.guildId === guild.id;
|
|
1154
|
+
const channelMatches = message.channelId === channel.id;
|
|
1155
|
+
return {
|
|
1156
|
+
guildId: guild.id,
|
|
1157
|
+
channelId: channel.id,
|
|
1158
|
+
messageId: message.id,
|
|
1159
|
+
exists: guildMatches && channelMatches && message.id === input.messageId,
|
|
1160
|
+
guildMatches,
|
|
1161
|
+
channelMatches,
|
|
1162
|
+
authorUserId: message.author.id,
|
|
662
1163
|
};
|
|
663
1164
|
}
|
|
1165
|
+
async inspectChannelAudience(input) {
|
|
1166
|
+
validateSnowflake(input.guildId, "guild");
|
|
1167
|
+
validateSnowflake(input.channelId, "channel");
|
|
1168
|
+
validateBoundedRoleSet(input.roleIds, "audience");
|
|
1169
|
+
const guild = await this.client.guilds.fetch(input.guildId);
|
|
1170
|
+
if (guild.id !== input.guildId)
|
|
1171
|
+
throw new Error("Discord guild validation failed");
|
|
1172
|
+
const roles = await Promise.all(input.roleIds.map(async (roleId) => ({
|
|
1173
|
+
roleId,
|
|
1174
|
+
role: await guild.roles.fetch(roleId),
|
|
1175
|
+
})));
|
|
1176
|
+
const channel = await guild.channels.fetch(input.channelId);
|
|
1177
|
+
if (!channel) {
|
|
1178
|
+
return {
|
|
1179
|
+
guildId: guild.id,
|
|
1180
|
+
channelId: input.channelId,
|
|
1181
|
+
exists: false,
|
|
1182
|
+
guildMatches: false,
|
|
1183
|
+
everyoneCanView: false,
|
|
1184
|
+
roles: roles.map(({ roleId, role }) => ({
|
|
1185
|
+
roleId,
|
|
1186
|
+
exists: role !== null && role.guild.id === guild.id,
|
|
1187
|
+
canView: false,
|
|
1188
|
+
})),
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
const guildMatches = channel.guild.id === guild.id;
|
|
1192
|
+
return {
|
|
1193
|
+
guildId: guild.id,
|
|
1194
|
+
channelId: channel.id,
|
|
1195
|
+
exists: true,
|
|
1196
|
+
guildMatches,
|
|
1197
|
+
everyoneCanView: guildMatches &&
|
|
1198
|
+
(channel
|
|
1199
|
+
.permissionsFor(guild.roles.everyone)
|
|
1200
|
+
?.has(PermissionFlagsBits.ViewChannel) ??
|
|
1201
|
+
false),
|
|
1202
|
+
roles: roles.map(({ roleId, role }) => {
|
|
1203
|
+
const exists = role !== null && role.guild.id === guild.id;
|
|
1204
|
+
return {
|
|
1205
|
+
roleId,
|
|
1206
|
+
exists,
|
|
1207
|
+
canView: guildMatches &&
|
|
1208
|
+
role !== null &&
|
|
1209
|
+
role.guild.id === guild.id &&
|
|
1210
|
+
(channel
|
|
1211
|
+
.permissionsFor(role)
|
|
1212
|
+
?.has(PermissionFlagsBits.ViewChannel) ??
|
|
1213
|
+
false),
|
|
1214
|
+
};
|
|
1215
|
+
}),
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
async inspectGuildChannelAudience(input) {
|
|
1219
|
+
validateSnowflake(input.guildId, "guild");
|
|
1220
|
+
validateBoundedRoleSet(input.roleIds, "audience");
|
|
1221
|
+
const guild = await this.client.guilds.fetch(input.guildId);
|
|
1222
|
+
if (guild.id !== input.guildId)
|
|
1223
|
+
throw new Error("Discord guild validation failed");
|
|
1224
|
+
const resolvedRoles = await Promise.all(input.roleIds.map(async (roleId) => ({
|
|
1225
|
+
roleId,
|
|
1226
|
+
role: await guild.roles.fetch(roleId),
|
|
1227
|
+
})));
|
|
1228
|
+
const missingRoleIds = resolvedRoles
|
|
1229
|
+
.filter(({ role }) => role === null || role.guild.id !== guild.id)
|
|
1230
|
+
.map(({ roleId }) => roleId);
|
|
1231
|
+
const fetched = await guild.channels.fetch();
|
|
1232
|
+
const relevant = [];
|
|
1233
|
+
for (const channel of fetched.values()) {
|
|
1234
|
+
if (channel &&
|
|
1235
|
+
(channel.type === ChannelType.GuildText ||
|
|
1236
|
+
channel.type === ChannelType.GuildAnnouncement)) {
|
|
1237
|
+
if (channel.guild.id !== guild.id)
|
|
1238
|
+
throw new Error("Discord channel guild validation failed");
|
|
1239
|
+
relevant.push(channel);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
relevant.sort((left, right) => left.id.localeCompare(right.id, "en"));
|
|
1243
|
+
const truncated = relevant.length > guildChannelAudienceInspectionLimit;
|
|
1244
|
+
const channels = relevant
|
|
1245
|
+
.slice(0, guildChannelAudienceInspectionLimit)
|
|
1246
|
+
.map((channel) => {
|
|
1247
|
+
const everyoneCanView = channel
|
|
1248
|
+
.permissionsFor(guild.roles.everyone)
|
|
1249
|
+
?.has(PermissionFlagsBits.ViewChannel) ?? false;
|
|
1250
|
+
const visibleRoleIds = resolvedRoles
|
|
1251
|
+
.filter(({ role }) => role !== null &&
|
|
1252
|
+
role.guild.id === guild.id &&
|
|
1253
|
+
(channel
|
|
1254
|
+
.permissionsFor(role)
|
|
1255
|
+
?.has(PermissionFlagsBits.ViewChannel) ??
|
|
1256
|
+
false))
|
|
1257
|
+
.map(({ roleId }) => roleId);
|
|
1258
|
+
return {
|
|
1259
|
+
channelId: channel.id,
|
|
1260
|
+
type: channel.type === ChannelType.GuildText
|
|
1261
|
+
? "guild_text"
|
|
1262
|
+
: "guild_announcement",
|
|
1263
|
+
everyoneCanView,
|
|
1264
|
+
visibleRoleIds,
|
|
1265
|
+
};
|
|
1266
|
+
})
|
|
1267
|
+
.filter(({ everyoneCanView, visibleRoleIds }) => everyoneCanView || visibleRoleIds.length > 0);
|
|
1268
|
+
return {
|
|
1269
|
+
guildId: guild.id,
|
|
1270
|
+
channels,
|
|
1271
|
+
missingRoleIds,
|
|
1272
|
+
truncated,
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
async seedMessageReaction(input) {
|
|
1276
|
+
validateSnowflake(input.guildId, "guild");
|
|
1277
|
+
validateSnowflake(input.channelId, "channel");
|
|
1278
|
+
validateSnowflake(input.messageId, "message");
|
|
1279
|
+
const guild = await this.client.guilds.fetch(input.guildId);
|
|
1280
|
+
if (guild.id !== input.guildId)
|
|
1281
|
+
throw new Error("Discord guild validation failed");
|
|
1282
|
+
const channel = await guild.channels.fetch(input.channelId);
|
|
1283
|
+
if (!channel ||
|
|
1284
|
+
channel.guild.id !== guild.id ||
|
|
1285
|
+
!channel.isTextBased() ||
|
|
1286
|
+
channel.isDMBased()) {
|
|
1287
|
+
throw new Error("Discord reaction message channel is unavailable");
|
|
1288
|
+
}
|
|
1289
|
+
const message = await channel.messages.fetch(input.messageId);
|
|
1290
|
+
if (message.id !== input.messageId ||
|
|
1291
|
+
message.guildId !== guild.id ||
|
|
1292
|
+
message.channelId !== channel.id) {
|
|
1293
|
+
throw new Error("Discord reaction message validation failed");
|
|
1294
|
+
}
|
|
1295
|
+
const emoji = await this.inspectEmoji({
|
|
1296
|
+
guildId: guild.id,
|
|
1297
|
+
emojiIdentifier: input.emojiIdentifier,
|
|
1298
|
+
});
|
|
1299
|
+
if (!emoji.exists || !emoji.guildMatches || !emoji.usable) {
|
|
1300
|
+
throw new Error("Discord reaction emoji is unavailable");
|
|
1301
|
+
}
|
|
1302
|
+
const cacheKey = emoji.kind === "custom" ? emoji.emojiId : (emoji.name ?? "");
|
|
1303
|
+
if (message.reactions.cache.get(cacheKey)?.me)
|
|
1304
|
+
return { added: false };
|
|
1305
|
+
const resolvedEmoji = emoji.kind === "custom"
|
|
1306
|
+
? await guild.emojis.fetch(emoji.emojiId)
|
|
1307
|
+
: emoji.name;
|
|
1308
|
+
if (!resolvedEmoji)
|
|
1309
|
+
throw new Error("Discord reaction emoji is unavailable");
|
|
1310
|
+
await message.react(resolvedEmoji);
|
|
1311
|
+
return { added: true };
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
function memberAvatarUrl(member) {
|
|
1315
|
+
return member && typeof member.displayAvatarURL === "function"
|
|
1316
|
+
? member.displayAvatarURL({ extension: "webp", size: 64 })
|
|
1317
|
+
: "";
|
|
664
1318
|
}
|
|
665
1319
|
async function fetchSafeRole(guild, roleId) {
|
|
666
1320
|
validateSnowflake(roleId, "role");
|
|
@@ -704,6 +1358,24 @@ function missingRoleInspection(input, hierarchy) {
|
|
|
704
1358
|
dangerousPermissionNames: [],
|
|
705
1359
|
};
|
|
706
1360
|
}
|
|
1361
|
+
function missingMessageInspection(input, channelMatches) {
|
|
1362
|
+
return {
|
|
1363
|
+
...input,
|
|
1364
|
+
exists: false,
|
|
1365
|
+
guildMatches: channelMatches,
|
|
1366
|
+
channelMatches,
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
function validateEnforcementMetadata(input) {
|
|
1370
|
+
validateSnowflake(input.guildId, "guild");
|
|
1371
|
+
validateSnowflake(input.memberUserId, "member");
|
|
1372
|
+
if (input.moduleId.length > 100 || !moduleIdentifier.test(input.moduleId))
|
|
1373
|
+
throw new Error("Member enforcement requires a valid Helyx module ID");
|
|
1374
|
+
if (input.reason.trim().length === 0 ||
|
|
1375
|
+
input.reason.length > 400 ||
|
|
1376
|
+
/[\r\n]/u.test(input.reason))
|
|
1377
|
+
throw new Error("Member enforcement requires a bounded audit reason");
|
|
1378
|
+
}
|
|
707
1379
|
function validateMutationMetadata(input) {
|
|
708
1380
|
validateSnowflake(input.guildId, "guild");
|
|
709
1381
|
validateSnowflake(input.memberUserId, "member");
|
|
@@ -716,6 +1388,22 @@ function validateMutationMetadata(input) {
|
|
|
716
1388
|
/[\r\n]/u.test(input.reason))
|
|
717
1389
|
throw new Error("Role mutation requires a bounded audit reason");
|
|
718
1390
|
}
|
|
1391
|
+
function validateRoleCreationMetadata(input) {
|
|
1392
|
+
validateSnowflake(input.guildId, "guild");
|
|
1393
|
+
if (input.name.trim() !== input.name ||
|
|
1394
|
+
input.name.length < 1 ||
|
|
1395
|
+
input.name.length > 100 ||
|
|
1396
|
+
/[\r\n\0]/u.test(input.name) ||
|
|
1397
|
+
input.name.toLowerCase() === "@everyone" ||
|
|
1398
|
+
input.name.toLowerCase() === "@here")
|
|
1399
|
+
throw new Error("Role creation requires a safe name from 1 to 100 characters");
|
|
1400
|
+
if (input.moduleId.length > 100 || !moduleIdentifier.test(input.moduleId))
|
|
1401
|
+
throw new Error("Role creation requires a valid Helyx module ID");
|
|
1402
|
+
if (input.reason.trim().length === 0 ||
|
|
1403
|
+
input.reason.length > 400 ||
|
|
1404
|
+
/[\r\n]/u.test(input.reason))
|
|
1405
|
+
throw new Error("Role creation requires a bounded audit reason");
|
|
1406
|
+
}
|
|
719
1407
|
async function fetchExactMember(guild, memberUserId, label) {
|
|
720
1408
|
validateSnowflake(memberUserId, label);
|
|
721
1409
|
const member = await guild.members.fetch(memberUserId);
|
|
@@ -729,6 +1417,18 @@ function isUnknownMember(error) {
|
|
|
729
1417
|
"code" in error &&
|
|
730
1418
|
error.code === RESTJSONErrorCodes.UnknownMember);
|
|
731
1419
|
}
|
|
1420
|
+
function isUnknownMessage(error) {
|
|
1421
|
+
return (typeof error === "object" &&
|
|
1422
|
+
error !== null &&
|
|
1423
|
+
"code" in error &&
|
|
1424
|
+
error.code === RESTJSONErrorCodes.UnknownMessage);
|
|
1425
|
+
}
|
|
1426
|
+
function isClosedDirectMessage(error) {
|
|
1427
|
+
return (typeof error === "object" &&
|
|
1428
|
+
error !== null &&
|
|
1429
|
+
"code" in error &&
|
|
1430
|
+
error.code === RESTJSONErrorCodes.CannotSendMessagesToThisUser);
|
|
1431
|
+
}
|
|
732
1432
|
function validateBoundedRoleSet(roleIds, label) {
|
|
733
1433
|
if (roleIds.length > 100 || new Set(roleIds).size !== roleIds.length)
|
|
734
1434
|
throw new Error(`${label} role IDs must be unique and bounded`);
|