@helyx/bot 0.4.9 → 0.4.13
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 +66 -0
- package/dist/cloud-control-dispatcher.js +2 -1
- package/dist/control-server.d.ts +43 -3
- package/dist/control-server.js +228 -18
- package/dist/events.d.ts +92 -1
- package/dist/events.js +1483 -5
- package/dist/guilds.d.ts +20 -1
- package/dist/guilds.js +149 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.js +89 -23
- package/dist/interactions.js +101 -22
- package/dist/management-module.d.ts +1 -1
- 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 +810 -0
- package/dist/shard-contracts.js +292 -3
- package/dist/shard-router.d.ts +3 -0
- package/dist/shard-router.js +42 -0
- package/dist/shard-supervisor.d.ts +4 -1
- package/dist/shard-supervisor.js +262 -7
- package/dist/shard-worker.d.ts +7 -0
- package/dist/shard-worker.js +64 -10
- package/dist/verification-service.d.ts +50 -0
- package/dist/verification-service.js +166 -0
- package/package.json +10 -9
package/dist/interactions.js
CHANGED
|
@@ -17,14 +17,18 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
17
17
|
if (guildCommands.length > 0 && !registration.internalGuildId) {
|
|
18
18
|
throw new Error("DISCORD_INTERNAL_GUILD_ID is required when guild commands are loaded");
|
|
19
19
|
}
|
|
20
|
-
const
|
|
20
|
+
const buttonItems = modules.flatMap((module) => (module.interactions?.buttons ?? []).map((contribution) => ({
|
|
21
21
|
moduleId: module.manifest.id,
|
|
22
22
|
contribution,
|
|
23
|
-
})))
|
|
24
|
-
const
|
|
23
|
+
})));
|
|
24
|
+
const buttons = uniqueMap(buttonItems.filter(({ contribution }) => contribution.customId !== undefined), ({ contribution }) => contribution.customId, "button custom ID");
|
|
25
|
+
const buttonPrefixes = componentPrefixes(buttonItems, "button");
|
|
26
|
+
const modalItems = modules.flatMap((module) => (module.interactions?.modals ?? []).map((contribution) => ({
|
|
25
27
|
moduleId: module.manifest.id,
|
|
26
28
|
contribution,
|
|
27
|
-
})))
|
|
29
|
+
})));
|
|
30
|
+
const modals = uniqueMap(modalItems.filter(({ contribution }) => contribution.customId !== undefined), ({ contribution }) => contribution.customId, "modal custom ID");
|
|
31
|
+
const modalPrefixes = componentPrefixes(modalItems, "modal");
|
|
28
32
|
const selects = uniqueMap(modules.flatMap((module) => (module.interactions?.selects ?? []).map((contribution) => ({
|
|
29
33
|
moduleId: module.manifest.id,
|
|
30
34
|
contribution,
|
|
@@ -124,7 +128,8 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
124
128
|
}
|
|
125
129
|
}
|
|
126
130
|
else if (interaction.isButton()) {
|
|
127
|
-
const button = buttons.get(interaction.customId)
|
|
131
|
+
const button = buttons.get(interaction.customId) ??
|
|
132
|
+
resolveComponentPrefix(buttonPrefixes, interaction.customId);
|
|
128
133
|
const context = button
|
|
129
134
|
? createContext(interaction, services)
|
|
130
135
|
: undefined;
|
|
@@ -132,6 +137,11 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
132
137
|
button?.contribution.acknowledgement === "deferred-ephemeral") {
|
|
133
138
|
await context.deferReply?.(true);
|
|
134
139
|
}
|
|
140
|
+
if (button &&
|
|
141
|
+
interaction.isButton() &&
|
|
142
|
+
button.contribution.acknowledgement === "deferred-update") {
|
|
143
|
+
await interaction.deferUpdate();
|
|
144
|
+
}
|
|
135
145
|
if (button &&
|
|
136
146
|
context &&
|
|
137
147
|
(await authorizeComponent(interaction, button.moduleId, services))) {
|
|
@@ -139,7 +149,8 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
139
149
|
}
|
|
140
150
|
}
|
|
141
151
|
else if (interaction.isModalSubmit()) {
|
|
142
|
-
const modal = modals.get(interaction.customId)
|
|
152
|
+
const modal = modals.get(interaction.customId) ??
|
|
153
|
+
resolveComponentPrefix(modalPrefixes, interaction.customId);
|
|
143
154
|
if (modal &&
|
|
144
155
|
(await authorizeComponent(interaction, modal.moduleId, services))) {
|
|
145
156
|
await modal.contribution.execute(createModalContext(interaction, services));
|
|
@@ -205,6 +216,9 @@ export function toCommandDefinition(registered) {
|
|
|
205
216
|
if (!!command.execute === !!command.subcommands?.length) {
|
|
206
217
|
throw new Error(`Command ${command.name} must define either execute or subcommands`);
|
|
207
218
|
}
|
|
219
|
+
if (command.options?.length && command.subcommands?.length) {
|
|
220
|
+
throw new Error(`Command ${command.name} cannot define both direct options and subcommands`);
|
|
221
|
+
}
|
|
208
222
|
const contextTypes = {
|
|
209
223
|
guild: InteractionContextType.Guild,
|
|
210
224
|
bot_dm: InteractionContextType.BotDM,
|
|
@@ -219,6 +233,9 @@ export function toCommandDefinition(registered) {
|
|
|
219
233
|
.setDescription(command.description)
|
|
220
234
|
.setContexts(command.contexts.map((context) => contextTypes[context]))
|
|
221
235
|
.setIntegrationTypes(command.integrationTypes.map((integrationType) => integrationTypes[integrationType]));
|
|
236
|
+
for (const option of command.options ?? []) {
|
|
237
|
+
addCommandOption(builder, option);
|
|
238
|
+
}
|
|
222
239
|
for (const subcommand of command.subcommands ?? []) {
|
|
223
240
|
builder.addSubcommand((subcommandBuilder) => {
|
|
224
241
|
subcommandBuilder
|
|
@@ -260,6 +277,13 @@ function addCommandOption(builder, option) {
|
|
|
260
277
|
.setRequired(option.required ?? false));
|
|
261
278
|
return;
|
|
262
279
|
}
|
|
280
|
+
if (option.type === "user") {
|
|
281
|
+
builder.addUserOption((optionBuilder) => optionBuilder
|
|
282
|
+
.setName(option.name)
|
|
283
|
+
.setDescription(option.description)
|
|
284
|
+
.setRequired(option.required ?? false));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
263
287
|
if (option.type === "role") {
|
|
264
288
|
builder.addRoleOption((optionBuilder) => optionBuilder
|
|
265
289
|
.setName(option.name)
|
|
@@ -270,6 +294,7 @@ function addCommandOption(builder, option) {
|
|
|
270
294
|
const channelTypes = {
|
|
271
295
|
guild_text: ChannelType.GuildText,
|
|
272
296
|
guild_announcement: ChannelType.GuildAnnouncement,
|
|
297
|
+
guild_category: ChannelType.GuildCategory,
|
|
273
298
|
};
|
|
274
299
|
builder.addChannelOption((optionBuilder) => {
|
|
275
300
|
optionBuilder
|
|
@@ -291,9 +316,43 @@ export function uniqueMap(items, keyOf, label) {
|
|
|
291
316
|
}
|
|
292
317
|
return result;
|
|
293
318
|
}
|
|
319
|
+
function componentPrefixes(items, label) {
|
|
320
|
+
const prefixes = [];
|
|
321
|
+
for (const item of items) {
|
|
322
|
+
const { customId, customIdPrefix } = item.contribution;
|
|
323
|
+
if ((customId === undefined) === (customIdPrefix === undefined)) {
|
|
324
|
+
throw new Error(`${label} contribution must declare exactly one custom ID or prefix`);
|
|
325
|
+
}
|
|
326
|
+
const route = customId ?? customIdPrefix;
|
|
327
|
+
if (route.length < 1 ||
|
|
328
|
+
route.length > 100 ||
|
|
329
|
+
!/^[A-Za-z0-9._:-]+$/u.test(route)) {
|
|
330
|
+
throw new Error(`${label} custom ID routing value is invalid`);
|
|
331
|
+
}
|
|
332
|
+
if (customIdPrefix !== undefined)
|
|
333
|
+
prefixes.push(item);
|
|
334
|
+
}
|
|
335
|
+
for (let index = 0; index < prefixes.length; index += 1) {
|
|
336
|
+
const prefix = prefixes[index].contribution.customIdPrefix;
|
|
337
|
+
if (prefixes.some((candidate, candidateIndex) => candidateIndex !== index &&
|
|
338
|
+
(candidate.contribution.customIdPrefix ?? "").startsWith(prefix))) {
|
|
339
|
+
throw new Error(`Ambiguous ${label} custom ID prefix: ${prefix}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return prefixes;
|
|
343
|
+
}
|
|
344
|
+
function resolveComponentPrefix(items, customId) {
|
|
345
|
+
if (customId.length < 1 ||
|
|
346
|
+
customId.length > 100 ||
|
|
347
|
+
!/^[A-Za-z0-9._:-]+$/u.test(customId)) {
|
|
348
|
+
return undefined;
|
|
349
|
+
}
|
|
350
|
+
return items.find(({ contribution }) => customId.startsWith(contribution.customIdPrefix ?? ""));
|
|
351
|
+
}
|
|
294
352
|
function createContext(interaction, services) {
|
|
295
353
|
return {
|
|
296
354
|
interactionId: interaction.id,
|
|
355
|
+
...("customId" in interaction ? { customId: interaction.customId } : {}),
|
|
297
356
|
userId: interaction.user.id,
|
|
298
357
|
userDisplayName: interaction.user.globalName ?? interaction.user.username,
|
|
299
358
|
userRoleIds: interaction.member instanceof GuildMember
|
|
@@ -318,10 +377,14 @@ function createContext(interaction, services) {
|
|
|
318
377
|
...(ephemeral ? { flags: MessageFlags.Ephemeral } : {}),
|
|
319
378
|
});
|
|
320
379
|
},
|
|
380
|
+
deleteReplyAfter: (milliseconds) => scheduleReplyDeletion(interaction, milliseconds),
|
|
321
381
|
reply: (response) => replyToInteraction(interaction, response),
|
|
322
382
|
...(interaction.isMessageComponent()
|
|
323
383
|
? {
|
|
324
384
|
updateResponse: async (response) => interaction.update(toUpdateOptions(response)).then(() => undefined),
|
|
385
|
+
editSourceMessage: async (response) => interaction.webhook
|
|
386
|
+
.editMessage(interaction.message.id, toUpdateOptions(response))
|
|
387
|
+
.then(() => undefined),
|
|
325
388
|
}
|
|
326
389
|
: {}),
|
|
327
390
|
showModal: async (modal) => interaction.showModal(toModal(modal)).then(() => undefined),
|
|
@@ -336,6 +399,16 @@ function createCommandContext(interaction, services) {
|
|
|
336
399
|
...createContext(interaction, services),
|
|
337
400
|
getStringOption: (name) => interaction.options.getString(name),
|
|
338
401
|
getBooleanOption: (name) => interaction.options.getBoolean(name),
|
|
402
|
+
getUserOption: (name) => {
|
|
403
|
+
const user = interaction.options.getUser(name);
|
|
404
|
+
return user
|
|
405
|
+
? {
|
|
406
|
+
id: user.id,
|
|
407
|
+
displayName: user.globalName ?? user.username,
|
|
408
|
+
isBot: user.bot,
|
|
409
|
+
}
|
|
410
|
+
: null;
|
|
411
|
+
},
|
|
339
412
|
getRoleOption: (name) => {
|
|
340
413
|
const role = interaction.options.getRole(name);
|
|
341
414
|
return role ? { id: role.id, name: role.name } : null;
|
|
@@ -397,6 +470,7 @@ function createModalContext(interaction, services) {
|
|
|
397
470
|
: undefined;
|
|
398
471
|
return {
|
|
399
472
|
interactionId: interaction.id,
|
|
473
|
+
customId: interaction.customId,
|
|
400
474
|
userId: interaction.user.id,
|
|
401
475
|
userDisplayName: interaction.user.globalName ?? interaction.user.username,
|
|
402
476
|
userRoleIds: interaction.member instanceof GuildMember
|
|
@@ -418,6 +492,7 @@ function createModalContext(interaction, services) {
|
|
|
418
492
|
...(ephemeral ? { flags: MessageFlags.Ephemeral } : {}),
|
|
419
493
|
});
|
|
420
494
|
},
|
|
495
|
+
deleteReplyAfter: (milliseconds) => scheduleReplyDeletion(interaction, milliseconds),
|
|
421
496
|
reply: (response) => replyToInteraction(interaction, response),
|
|
422
497
|
...(updateResponse ? { updateResponse } : {}),
|
|
423
498
|
getField: (customId) => interaction.fields.getTextInputValue(customId),
|
|
@@ -445,6 +520,16 @@ function createModalContext(interaction, services) {
|
|
|
445
520
|
addChannelMessageReaction: (channelId, messageId, emojiIdentifier) => addChannelMessageReaction(interaction.client, channelId, messageId, emojiIdentifier),
|
|
446
521
|
};
|
|
447
522
|
}
|
|
523
|
+
function scheduleReplyDeletion(interaction, milliseconds) {
|
|
524
|
+
if (!Number.isInteger(milliseconds) ||
|
|
525
|
+
milliseconds < 1_000 ||
|
|
526
|
+
milliseconds > 900_000)
|
|
527
|
+
throw new Error("Reply deletion delay must be between 1 second and 15 minutes");
|
|
528
|
+
const timer = setTimeout(() => {
|
|
529
|
+
void interaction.deleteReply().catch(() => undefined);
|
|
530
|
+
}, milliseconds);
|
|
531
|
+
timer.unref();
|
|
532
|
+
}
|
|
448
533
|
function createSelectContext(interaction, services) {
|
|
449
534
|
return {
|
|
450
535
|
...createContext(interaction, services),
|
|
@@ -667,9 +752,7 @@ function toSelectRows(response) {
|
|
|
667
752
|
.setPlaceholder(select.placeholder)
|
|
668
753
|
.setMinValues(select.minValues ?? 1)
|
|
669
754
|
.setMaxValues(select.maxValues ?? 1)
|
|
670
|
-
.setChannelTypes(...select.channelTypes.map(
|
|
671
|
-
? ChannelType.GuildAnnouncement
|
|
672
|
-
: ChannelType.GuildText))));
|
|
755
|
+
.setChannelTypes(...select.channelTypes.map(discordChannelType))));
|
|
673
756
|
continue;
|
|
674
757
|
}
|
|
675
758
|
if (select.type === "discord-role") {
|
|
@@ -701,19 +784,10 @@ function toSelectRows(response) {
|
|
|
701
784
|
return rows;
|
|
702
785
|
}
|
|
703
786
|
export function toUpdateOptions(response) {
|
|
704
|
-
|
|
787
|
+
return toReplyOptions({
|
|
705
788
|
...response,
|
|
706
789
|
ephemeral: false,
|
|
707
790
|
});
|
|
708
|
-
return response.componentsV2
|
|
709
|
-
? {
|
|
710
|
-
...options,
|
|
711
|
-
// Discord requires legacy fields to be cleared when an existing
|
|
712
|
-
// message is first converted to Components V2.
|
|
713
|
-
content: null,
|
|
714
|
-
embeds: [],
|
|
715
|
-
}
|
|
716
|
-
: options;
|
|
717
791
|
}
|
|
718
792
|
function toAllowedUserMentions(userIds) {
|
|
719
793
|
if (userIds === undefined)
|
|
@@ -818,9 +892,7 @@ export function toModal(definition) {
|
|
|
818
892
|
.setRequired(field.required ?? false)
|
|
819
893
|
.setMinValues(field.minValues ?? (field.required ? 1 : 0))
|
|
820
894
|
.setMaxValues(field.maxValues ?? 1)
|
|
821
|
-
.setChannelTypes(...field.channelTypes.map(
|
|
822
|
-
? ChannelType.GuildAnnouncement
|
|
823
|
-
: ChannelType.GuildText));
|
|
895
|
+
.setChannelTypes(...field.channelTypes.map(discordChannelType));
|
|
824
896
|
if (field.placeholder !== undefined)
|
|
825
897
|
input.setPlaceholder(field.placeholder);
|
|
826
898
|
if (field.defaultValues?.length)
|
|
@@ -831,6 +903,13 @@ export function toModal(definition) {
|
|
|
831
903
|
}
|
|
832
904
|
return modal;
|
|
833
905
|
}
|
|
906
|
+
function discordChannelType(type) {
|
|
907
|
+
if (type === "guild_announcement")
|
|
908
|
+
return ChannelType.GuildAnnouncement;
|
|
909
|
+
if (type === "guild_category")
|
|
910
|
+
return ChannelType.GuildCategory;
|
|
911
|
+
return ChannelType.GuildText;
|
|
912
|
+
}
|
|
834
913
|
async function sendFailure(interaction) {
|
|
835
914
|
if (interaction.isAutocomplete()) {
|
|
836
915
|
await interaction.respond([]).catch(() => undefined);
|
|
@@ -2,7 +2,7 @@ import { type ModuleDefinition } from "@helyx/sdk";
|
|
|
2
2
|
export interface ManagementGuildResources {
|
|
3
3
|
channels: readonly {
|
|
4
4
|
id: string;
|
|
5
|
-
type: "guild_text" | "guild_announcement";
|
|
5
|
+
type: "guild_text" | "guild_announcement" | "guild_category";
|
|
6
6
|
}[];
|
|
7
7
|
roles: readonly {
|
|
8
8
|
id: string;
|
|
@@ -18,6 +18,7 @@ const VALUE_MODAL_ID = "helyx.management.value.modal";
|
|
|
18
18
|
const VALUE_FIELD_ID = "value";
|
|
19
19
|
const SESSION_PURPOSE = "modules.manage";
|
|
20
20
|
const PAGE_SIZE = 25;
|
|
21
|
+
const MAX_DISCORD_SETTING_SELECTIONS = 25;
|
|
21
22
|
const SESSION_LIFETIME_MS = 15 * 60 * 1_000;
|
|
22
23
|
const VIOLET = 0x7c3aed;
|
|
23
24
|
const SUCCESS = 0x16a34a;
|
|
@@ -177,7 +178,14 @@ export function createManagementModule(configurableModules, resources) {
|
|
|
177
178
|
return;
|
|
178
179
|
}
|
|
179
180
|
try {
|
|
180
|
-
module.configuration?.validate(value);
|
|
181
|
+
const validated = module.configuration?.validate(value);
|
|
182
|
+
if (validated && module.configuration?.validateUpdate) {
|
|
183
|
+
await module.configuration.validateUpdate({
|
|
184
|
+
guildId: context.serverId,
|
|
185
|
+
value: validated,
|
|
186
|
+
services: context.services,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
181
189
|
}
|
|
182
190
|
catch (error) {
|
|
183
191
|
await context.reply(validationResponse(error instanceof Error
|
|
@@ -563,8 +571,12 @@ function settingValueResponse(setting) {
|
|
|
563
571
|
{
|
|
564
572
|
type: "discord-channel",
|
|
565
573
|
customId: VALUE_SELECT_ID,
|
|
566
|
-
placeholder:
|
|
574
|
+
placeholder: setting.multiple
|
|
575
|
+
? "Choose channels"
|
|
576
|
+
: "Choose a channel",
|
|
567
577
|
channelTypes: setting.channelTypes,
|
|
578
|
+
minValues: 1,
|
|
579
|
+
maxValues: setting.multiple ? MAX_DISCORD_SETTING_SELECTIONS : 1,
|
|
568
580
|
},
|
|
569
581
|
],
|
|
570
582
|
};
|
|
@@ -631,6 +643,8 @@ function initialDraft(module, existing) {
|
|
|
631
643
|
return result;
|
|
632
644
|
}
|
|
633
645
|
function emptyValue(setting) {
|
|
646
|
+
if (setting.type === "discord-channel" && setting.multiple)
|
|
647
|
+
return [];
|
|
634
648
|
if (setting.type === "discord-role" && setting.multiple)
|
|
635
649
|
return [];
|
|
636
650
|
if (setting.type === "string" ||
|
|
@@ -655,10 +669,13 @@ function selectedValue(setting, values) {
|
|
|
655
669
|
return values[0];
|
|
656
670
|
}
|
|
657
671
|
if (setting.type === "discord-channel") {
|
|
658
|
-
if (values.length
|
|
659
|
-
|
|
672
|
+
if (values.length < 1 ||
|
|
673
|
+
values.length > (setting.multiple ? MAX_DISCORD_SETTING_SELECTIONS : 1) ||
|
|
674
|
+
new Set(values).size !== values.length ||
|
|
675
|
+
values.some((value) => !isSnowflake(value))) {
|
|
676
|
+
throw new Error("Selected channel value is invalid");
|
|
660
677
|
}
|
|
661
|
-
return values[0];
|
|
678
|
+
return setting.multiple ? [...values] : values[0];
|
|
662
679
|
}
|
|
663
680
|
if (setting.type === "discord-role") {
|
|
664
681
|
if (values.length < 1 ||
|
|
@@ -723,9 +740,21 @@ async function validateDiscordSettings(directory, guildId, module, values) {
|
|
|
723
740
|
if (configured === "" || configured === undefined)
|
|
724
741
|
continue;
|
|
725
742
|
if (setting.type === "discord-channel") {
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
743
|
+
if (setting.multiple !== Array.isArray(configured)) {
|
|
744
|
+
return `${setting.label} has an invalid Discord channel selection.`;
|
|
745
|
+
}
|
|
746
|
+
const configuredChannels = Array.isArray(configured)
|
|
747
|
+
? configured
|
|
748
|
+
: [configured];
|
|
749
|
+
if (configuredChannels.length > MAX_DISCORD_SETTING_SELECTIONS ||
|
|
750
|
+
new Set(configuredChannels).size !== configuredChannels.length ||
|
|
751
|
+
configuredChannels.some((channelId) => {
|
|
752
|
+
if (typeof channelId !== "string")
|
|
753
|
+
return true;
|
|
754
|
+
const channel = channels.get(channelId);
|
|
755
|
+
return !channel || !setting.channelTypes.includes(channel.type);
|
|
756
|
+
})) {
|
|
757
|
+
return `${setting.label} must contain available supported Discord channels.`;
|
|
729
758
|
}
|
|
730
759
|
}
|
|
731
760
|
if (setting.type === "discord-role") {
|
|
@@ -776,8 +805,11 @@ function settingStatus(setting, value) {
|
|
|
776
805
|
}
|
|
777
806
|
if (setting.type === "boolean")
|
|
778
807
|
return value === true ? "Enabled" : "Disabled";
|
|
779
|
-
if (setting.type === "discord-channel")
|
|
780
|
-
return
|
|
808
|
+
if (setting.type === "discord-channel") {
|
|
809
|
+
return Array.isArray(value)
|
|
810
|
+
? `${String(value.length)} channels selected`
|
|
811
|
+
: "Channel selected";
|
|
812
|
+
}
|
|
781
813
|
if (setting.type === "discord-role") {
|
|
782
814
|
return Array.isArray(value)
|
|
783
815
|
? `${String(value.length)} roles selected`
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type Message } from "discord.js";
|
|
2
|
+
import type { GuildMessageClassification } from "@helyx/sdk";
|
|
3
|
+
export declare function classifyGuildMessage(message: Pick<Message, "attachments" | "content" | "embeds" | "flags" | "stickers">): GuildMessageClassification;
|
|
4
|
+
export declare const unavailableGuildMessageClassification: GuildMessageClassification;
|
|
5
|
+
//# sourceMappingURL=message-classification.d.ts.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { MessageFlags } from "discord.js";
|
|
2
|
+
const LINK_PATTERN = /https?:\/\/[^\s<>]+/giu;
|
|
3
|
+
const GIF_LINK_PATTERN = /(?:https?:\/\/[^\s<>]*(?:\.gif(?:[?#][^\s<>]*)?|giphy\.com|tenor\.com|media\.discordapp\.net[^\s<>]*\.gif))/giu;
|
|
4
|
+
const CUSTOM_EMOJI_PATTERN = /<a?:[a-zA-Z0-9_]{2,32}:\d{17,20}>/gu;
|
|
5
|
+
const UNICODE_EMOJI_PATTERN = /(?:\p{Regional_Indicator}{2}|[0-9#*]\uFE0F?\u20E3|\p{Extended_Pictographic}(?:\uFE0F|\p{Emoji_Modifier})?(?:\u200D\p{Extended_Pictographic}(?:\uFE0F|\p{Emoji_Modifier})?)*)/gu;
|
|
6
|
+
export function classifyGuildMessage(message) {
|
|
7
|
+
const kinds = new Set();
|
|
8
|
+
const content = message.content.trim();
|
|
9
|
+
const withoutLinks = content.replace(LINK_PATTERN, " ").trim();
|
|
10
|
+
const withoutEmoji = withoutLinks
|
|
11
|
+
.replace(CUSTOM_EMOJI_PATTERN, " ")
|
|
12
|
+
.replace(UNICODE_EMOJI_PATTERN, " ")
|
|
13
|
+
.replace(/\u200D|\uFE0F|\p{Emoji_Modifier}|\s/gu, "");
|
|
14
|
+
if (matches(LINK_PATTERN, content))
|
|
15
|
+
kinds.add("link");
|
|
16
|
+
if (withoutLinks.length > 0 &&
|
|
17
|
+
withoutEmoji.length === 0 &&
|
|
18
|
+
(matches(CUSTOM_EMOJI_PATTERN, withoutLinks) ||
|
|
19
|
+
matches(UNICODE_EMOJI_PATTERN, withoutLinks)))
|
|
20
|
+
kinds.add("emoji");
|
|
21
|
+
else if (withoutEmoji.length > 0)
|
|
22
|
+
kinds.add("text");
|
|
23
|
+
const voiceMessage = message.flags.has(MessageFlags.IsVoiceMessage);
|
|
24
|
+
if (voiceMessage)
|
|
25
|
+
kinds.add("voice-message");
|
|
26
|
+
for (const attachment of message.attachments.values()) {
|
|
27
|
+
const contentType = attachment.contentType?.toLowerCase() ?? "";
|
|
28
|
+
const filename = attachment.name.toLowerCase();
|
|
29
|
+
if (voiceMessage)
|
|
30
|
+
continue;
|
|
31
|
+
if (contentType === "image/gif" || filename.endsWith(".gif"))
|
|
32
|
+
kinds.add("gif");
|
|
33
|
+
else if (contentType.startsWith("image/"))
|
|
34
|
+
kinds.add("image");
|
|
35
|
+
else
|
|
36
|
+
kinds.add("file");
|
|
37
|
+
}
|
|
38
|
+
for (const embed of message.embeds) {
|
|
39
|
+
const urls = [embed.url, embed.image?.url, embed.thumbnail?.url]
|
|
40
|
+
.filter((value) => Boolean(value))
|
|
41
|
+
.join(" ");
|
|
42
|
+
if (matches(GIF_LINK_PATTERN, urls))
|
|
43
|
+
kinds.add("gif");
|
|
44
|
+
else if (embed.image || embed.thumbnail)
|
|
45
|
+
kinds.add("image");
|
|
46
|
+
}
|
|
47
|
+
if (matches(GIF_LINK_PATTERN, content))
|
|
48
|
+
kinds.add("gif");
|
|
49
|
+
if (message.stickers.size > 0)
|
|
50
|
+
kinds.add("sticker");
|
|
51
|
+
return {
|
|
52
|
+
available: true,
|
|
53
|
+
kinds: [
|
|
54
|
+
"text",
|
|
55
|
+
"emoji",
|
|
56
|
+
"link",
|
|
57
|
+
"image",
|
|
58
|
+
"gif",
|
|
59
|
+
"file",
|
|
60
|
+
"sticker",
|
|
61
|
+
"voice-message",
|
|
62
|
+
].filter((kind) => kinds.has(kind)),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export const unavailableGuildMessageClassification = {
|
|
66
|
+
available: false,
|
|
67
|
+
kinds: [],
|
|
68
|
+
};
|
|
69
|
+
function matches(pattern, value) {
|
|
70
|
+
pattern.lastIndex = 0;
|
|
71
|
+
const result = pattern.test(value);
|
|
72
|
+
pattern.lastIndex = 0;
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=message-classification.js.map
|
package/dist/modules.js
CHANGED
|
@@ -90,12 +90,21 @@ async function loadDefinitions(specifiers, importer) {
|
|
|
90
90
|
return orderedManifests.map((manifest) => byId.get(manifest.id));
|
|
91
91
|
}
|
|
92
92
|
export function validateContributionParity(definition) {
|
|
93
|
+
const requiredDiscordCapabilities = definition.requiredDiscordCapabilities ?? [];
|
|
94
|
+
if (new Set(requiredDiscordCapabilities).size !==
|
|
95
|
+
requiredDiscordCapabilities.length ||
|
|
96
|
+
requiredDiscordCapabilities.some((capability) => capability !== "guild-members" && capability !== "message-content")) {
|
|
97
|
+
throw new Error("Module requests an unsupported or duplicate Discord capability");
|
|
98
|
+
}
|
|
93
99
|
const executableCommands = (definition.interactions?.commands ?? []).map(({ name }) => name);
|
|
94
100
|
const executableEvents = [
|
|
101
|
+
...(definition.events?.privilegedMessageCreate ?? []),
|
|
102
|
+
...(definition.events?.guildMessageCreate ?? []),
|
|
95
103
|
...(definition.events?.messageReactionAdd ?? []),
|
|
96
104
|
...(definition.events?.messageReactionRemove ?? []),
|
|
97
105
|
...(definition.events?.messageDelete ?? []),
|
|
98
106
|
...(definition.events?.guildMemberAdd ?? []),
|
|
107
|
+
...(definition.events?.guildMemberRemove ?? []),
|
|
99
108
|
...(definition.events?.guildRoleDelete ?? []),
|
|
100
109
|
...(definition.events?.guildRoleUpdate ?? []),
|
|
101
110
|
...(definition.events?.channelDelete ?? []),
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type NumericWriteBehindCommittedBatch, type NumericWriteBehindEnqueueInput, type NumericWriteBehindMemberScope, type NumericWriteBehindScope, type NumericWriteBehindService } from "@helyx/sdk";
|
|
2
|
+
import { type ShardIdentity } from "./shard-contracts.js";
|
|
3
|
+
export declare class WorkerNumericWriteBehindProxy implements NumericWriteBehindService {
|
|
4
|
+
#private;
|
|
5
|
+
constructor(identity: ShardIdentity);
|
|
6
|
+
enqueue(input: NumericWriteBehindEnqueueInput): Promise<{
|
|
7
|
+
outcome: "accepted" | "cooldown" | "suppressed" | "paused" | "capacity" | "unavailable";
|
|
8
|
+
}>;
|
|
9
|
+
pendingMemberSnapshot(input: NumericWriteBehindMemberScope): Promise<{
|
|
10
|
+
increments: Readonly<Record<string, number>>;
|
|
11
|
+
maxima: Readonly<Record<string, Date>>;
|
|
12
|
+
oldestAcceptedAt: Date | null;
|
|
13
|
+
}>;
|
|
14
|
+
flushMember(input: NumericWriteBehindMemberScope): Promise<void>;
|
|
15
|
+
flushGuild(input: NumericWriteBehindScope & {
|
|
16
|
+
guildId: string;
|
|
17
|
+
}): Promise<void>;
|
|
18
|
+
flushSubject(input: NumericWriteBehindScope & {
|
|
19
|
+
subjectId: string;
|
|
20
|
+
}): Promise<void>;
|
|
21
|
+
flushModule(input: NumericWriteBehindScope): Promise<void>;
|
|
22
|
+
runMemberBarrier<Result>(input: NumericWriteBehindMemberScope, operation: () => Promise<Result>): Promise<Result>;
|
|
23
|
+
runGuildBarrier<Result>(input: NumericWriteBehindScope & {
|
|
24
|
+
guildId: string;
|
|
25
|
+
}, operation: () => Promise<Result>): Promise<Result>;
|
|
26
|
+
runSubjectBarrier<Result>(input: NumericWriteBehindScope & {
|
|
27
|
+
subjectId: string;
|
|
28
|
+
}, operation: () => Promise<Result>): Promise<Result>;
|
|
29
|
+
suppressSubject(input: NumericWriteBehindScope & {
|
|
30
|
+
subjectId: string;
|
|
31
|
+
cutoff: Date;
|
|
32
|
+
}): Promise<{
|
|
33
|
+
suppressionId: string;
|
|
34
|
+
discarded: number;
|
|
35
|
+
}>;
|
|
36
|
+
suppressGuild(input: NumericWriteBehindScope & {
|
|
37
|
+
guildId: string;
|
|
38
|
+
}): Promise<{
|
|
39
|
+
suppressionId: string;
|
|
40
|
+
discarded: number;
|
|
41
|
+
}>;
|
|
42
|
+
releaseGuild(input: NumericWriteBehindScope & {
|
|
43
|
+
guildId: string;
|
|
44
|
+
suppressionId: string;
|
|
45
|
+
}): Promise<{
|
|
46
|
+
released: boolean;
|
|
47
|
+
}>;
|
|
48
|
+
releaseSubject(input: NumericWriteBehindScope & {
|
|
49
|
+
subjectId: string;
|
|
50
|
+
suppressionId: string;
|
|
51
|
+
}): Promise<{
|
|
52
|
+
released: boolean;
|
|
53
|
+
}>;
|
|
54
|
+
clearMemberCooldown(input: NumericWriteBehindMemberScope): Promise<void>;
|
|
55
|
+
clearSubjectCooldown(input: NumericWriteBehindScope & {
|
|
56
|
+
subjectId: string;
|
|
57
|
+
}): Promise<void>;
|
|
58
|
+
subscribeCommitted(input: NumericWriteBehindScope, handler: (batch: NumericWriteBehindCommittedBatch) => void | Promise<void>): () => void;
|
|
59
|
+
stop(): void;
|
|
60
|
+
}
|
|
61
|
+
export declare function encodeNumericIpcValue(input: unknown): unknown;
|
|
62
|
+
//# sourceMappingURL=numeric-write-behind-proxy.d.ts.map
|