@helyx/bot 0.4.9 → 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 +33 -0
- package/dist/cloud-control-dispatcher.js +2 -1
- package/dist/control-server.d.ts +5 -0
- package/dist/control-server.js +104 -13
- package/dist/events.d.ts +64 -1
- package/dist/events.js +699 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +69 -19
- package/dist/interactions.js +79 -16
- 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/shard-contracts.d.ts +486 -0
- package/dist/shard-contracts.js +245 -1
- package/dist/shard-supervisor.d.ts +2 -1
- package/dist/shard-supervisor.js +208 -1
- package/dist/shard-worker.d.ts +1 -0
- package/dist/shard-worker.js +42 -5
- 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 ||
|
|
@@ -640,6 +1097,7 @@ export class DiscordResourceInspection {
|
|
|
640
1097
|
sendMessages: false,
|
|
641
1098
|
addReactions: false,
|
|
642
1099
|
readMessageHistory: false,
|
|
1100
|
+
manageMessages: false,
|
|
643
1101
|
};
|
|
644
1102
|
const guildMatches = channel.guild.id === guild.id;
|
|
645
1103
|
const me = guild.members.me ?? (await guild.members.fetchMe());
|
|
@@ -665,8 +1123,198 @@ export class DiscordResourceInspection {
|
|
|
665
1123
|
(effective?.has(PermissionFlagsBits.AddReactions) ?? false),
|
|
666
1124
|
readMessageHistory: guildMatches &&
|
|
667
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,
|
|
668
1163
|
};
|
|
669
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
|
+
: "";
|
|
670
1318
|
}
|
|
671
1319
|
async function fetchSafeRole(guild, roleId) {
|
|
672
1320
|
validateSnowflake(roleId, "role");
|
|
@@ -710,6 +1358,24 @@ function missingRoleInspection(input, hierarchy) {
|
|
|
710
1358
|
dangerousPermissionNames: [],
|
|
711
1359
|
};
|
|
712
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
|
+
}
|
|
713
1379
|
function validateMutationMetadata(input) {
|
|
714
1380
|
validateSnowflake(input.guildId, "guild");
|
|
715
1381
|
validateSnowflake(input.memberUserId, "member");
|
|
@@ -722,6 +1388,22 @@ function validateMutationMetadata(input) {
|
|
|
722
1388
|
/[\r\n]/u.test(input.reason))
|
|
723
1389
|
throw new Error("Role mutation requires a bounded audit reason");
|
|
724
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
|
+
}
|
|
725
1407
|
async function fetchExactMember(guild, memberUserId, label) {
|
|
726
1408
|
validateSnowflake(memberUserId, label);
|
|
727
1409
|
const member = await guild.members.fetch(memberUserId);
|
|
@@ -735,6 +1417,18 @@ function isUnknownMember(error) {
|
|
|
735
1417
|
"code" in error &&
|
|
736
1418
|
error.code === RESTJSONErrorCodes.UnknownMember);
|
|
737
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
|
+
}
|
|
738
1432
|
function validateBoundedRoleSet(roleIds, label) {
|
|
739
1433
|
if (roleIds.length > 100 || new Set(roleIds).size !== roleIds.length)
|
|
740
1434
|
throw new Error(`${label} role IDs must be unique and bounded`);
|