@seedcord/http 0.10.2 → 0.11.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -9
- package/dist/index.d.mts +42 -57
- package/dist/index.mjs +144 -239
- package/dist/index.mjs.map +1 -1
- package/dist/shared-vsj1uwIH.d.mts +977 -0
- package/dist/{edge.index-DCP1BD6f.mjs → username-3UsqzTSd.mjs} +1634 -1519
- package/dist/username-3UsqzTSd.mjs.map +1 -0
- package/dist/workerd.index.d.mts +65 -0
- package/dist/workerd.index.mjs +177 -0
- package/dist/workerd.index.mjs.map +1 -0
- package/package.json +19 -27
- package/dist/Manifest-D5MEsTmH.d.mts +0 -485
- package/dist/edge.index-DCP1BD6f.mjs.map +0 -1
- package/dist/edge.index-DFszgCJp.d.mts +0 -502
- package/dist/edge.index.d.mts +0 -10
- package/dist/edge.index.mjs +0 -15
- package/dist/manifest.index.d.mts +0 -10
- package/dist/manifest.index.mjs +0 -26
- package/dist/manifest.index.mjs.map +0 -1
|
@@ -1,18 +1,242 @@
|
|
|
1
1
|
import "reflect-metadata";
|
|
2
|
-
import { GatedMetadataKey,
|
|
3
|
-
import {
|
|
4
|
-
import { Logger } from "@seedcord/logger";
|
|
5
|
-
import { Converters, Envapter } from "envapt";
|
|
6
|
-
import { DiscordAPIError, REST } from "@discordjs/rest";
|
|
7
|
-
import { ArraySourceBase, BaseHandler, BaseReplySender, Bus, CursorSourceBase, DispatchContext, Fault, InteractionKind, Notice, PaginatorBase, RepliableHandler, ResolvedEmoji, Silence, Subscriber, WebhookLog } from "@seedcord/core";
|
|
2
|
+
import { COMMAND_DEPLOY_TIMEOUT_MS, CommandRegistry, FixedScope, GatedMetadataKey, InteractionMetadataKey, InteractionMiddlewareMetadataKey, MiddlewareRegistry, PublishDefault, SubscriberLoader, accessorStore, attachmentsOf, clearStore, currentApplication, decodeComponentRoute, deferFlags, guardedAccessor, interactionMiddleware, interactionRoutesOf, logUnknownFault, outcomeFor, queuedMsFor, readBotToken, reportDispatch, reportedWrite, resolveEmojis, resultFor, runAfter, runHandlerGates, sendFlags, slowGateMonitor } from "@seedcord/core/internal";
|
|
3
|
+
import { Plugin } from "@seedcord/core/plugin";
|
|
8
4
|
import { SeedcordErrorCode, paint } from "@seedcord/errors";
|
|
9
|
-
import {
|
|
5
|
+
import { SeedcordError, SeedcordTypeError } from "@seedcord/errors/internal";
|
|
10
6
|
import { ApplicationCommandOptionType, ApplicationCommandType, ChannelType, ComponentType, InteractionResponseType, InteractionType, MessageFlags, RESTJSONErrorCodes, Routes } from "discord-api-types/v10";
|
|
7
|
+
import { Collection } from "@discordjs/collection";
|
|
8
|
+
import { ArraySourceBase, BaseHandler, BaseReplySender, Bus, CursorSourceBase, DispatchContext, Fault, InteractionKind, Notice, PaginatorBase, RepliableHandler, ResolvedEmoji, Silence, StartupPhase, Subscriber, WebhookLog } from "@seedcord/core";
|
|
9
|
+
import { Logger } from "@seedcord/logger";
|
|
10
|
+
import { HmrModuleHandler, HmrModuleHandler as HmrModuleHandler$1 } from "@seedcord/core/hmr";
|
|
11
|
+
import { MemoryRateLimiter } from "@seedcord/rate-limiter";
|
|
12
|
+
import { formatFilePath, importFiles } from "@seedcord/utils";
|
|
11
13
|
import { API } from "@discordjs/core/http-only";
|
|
12
|
-
import { buildSlashRoute } from "@seedcord/utils/internal";
|
|
14
|
+
import { asError, buildSlashRoute } from "@seedcord/utils/internal";
|
|
15
|
+
import { Envapter } from "envapt";
|
|
16
|
+
import { DiscordAPIError, REST } from "@discordjs/rest";
|
|
13
17
|
import { prefixOf } from "@seedcord/custom-id";
|
|
14
|
-
import { Collection } from "@discordjs/collection";
|
|
15
18
|
|
|
19
|
+
//#region src/Plugin.ts
|
|
20
|
+
const HTTP_SCOPE = { transport: "http" };
|
|
21
|
+
/**
|
|
22
|
+
* Base class for an HTTP plugin, binding `this.core` to the HTTP {@link Core}.
|
|
23
|
+
*
|
|
24
|
+
* A plugin that runs on either transport extends the base from `@seedcord/core/plugin`, whose
|
|
25
|
+
* `this.core` is typed to the shared members only.
|
|
26
|
+
*
|
|
27
|
+
* @typeParam Opts - The transport and runtime the plugin declares.
|
|
28
|
+
*/
|
|
29
|
+
var Plugin$1 = class extends Plugin {
|
|
30
|
+
/** @internal */
|
|
31
|
+
static [FixedScope] = HTTP_SCOPE;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/inputs/pick.ts
|
|
36
|
+
function pick(ids, bucket) {
|
|
37
|
+
const found = new Collection();
|
|
38
|
+
if (!ids || !bucket) return found;
|
|
39
|
+
for (const id of ids) if (Object.hasOwn(bucket, id)) found.set(id, bucket[id]);
|
|
40
|
+
return found;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/inputs/ModalFields.ts
|
|
45
|
+
const FIELD_KINDS = {
|
|
46
|
+
[ComponentType.TextInput]: {
|
|
47
|
+
label: "a text input",
|
|
48
|
+
getter: "getTextInputValue"
|
|
49
|
+
},
|
|
50
|
+
[ComponentType.StringSelect]: {
|
|
51
|
+
label: "a string select",
|
|
52
|
+
getter: "getStringSelectValues"
|
|
53
|
+
},
|
|
54
|
+
[ComponentType.UserSelect]: {
|
|
55
|
+
label: "a user select",
|
|
56
|
+
getter: "getSelectedUsers"
|
|
57
|
+
},
|
|
58
|
+
[ComponentType.RoleSelect]: {
|
|
59
|
+
label: "a role select",
|
|
60
|
+
getter: "getSelectedRoles"
|
|
61
|
+
},
|
|
62
|
+
[ComponentType.MentionableSelect]: {
|
|
63
|
+
label: "a mentionable select",
|
|
64
|
+
getter: "getSelectedMentionables"
|
|
65
|
+
},
|
|
66
|
+
[ComponentType.ChannelSelect]: {
|
|
67
|
+
label: "a channel select",
|
|
68
|
+
getter: "getSelectedChannels"
|
|
69
|
+
},
|
|
70
|
+
[ComponentType.FileUpload]: {
|
|
71
|
+
label: "a file upload",
|
|
72
|
+
getter: "getUploadedFiles"
|
|
73
|
+
},
|
|
74
|
+
[ComponentType.RadioGroup]: {
|
|
75
|
+
label: "a radio group",
|
|
76
|
+
getter: "getRadioGroup"
|
|
77
|
+
},
|
|
78
|
+
[ComponentType.CheckboxGroup]: {
|
|
79
|
+
label: "a checkbox group",
|
|
80
|
+
getter: "getCheckboxGroup"
|
|
81
|
+
},
|
|
82
|
+
[ComponentType.Checkbox]: {
|
|
83
|
+
label: "a checkbox",
|
|
84
|
+
getter: "getCheckbox"
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
function nameOfChannelType(type) {
|
|
88
|
+
return ChannelType[type] ?? String(type);
|
|
89
|
+
}
|
|
90
|
+
function listOf(values) {
|
|
91
|
+
return values ?? [];
|
|
92
|
+
}
|
|
93
|
+
function isKind(entry, kinds) {
|
|
94
|
+
return kinds.includes(entry.type);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The fields a modal carries, read by custom id.
|
|
98
|
+
*
|
|
99
|
+
* A select getter resolves the ids its field picked against the interaction's `resolved` data and returns a
|
|
100
|
+
* `Collection` keyed by id. A field that picked nothing returns null.
|
|
101
|
+
*
|
|
102
|
+
* @throws A **SeedcordTypeError** from every getter when no field in the modal has that custom id.
|
|
103
|
+
* @throws A **SeedcordTypeError** from every getter when the field holds a kind that getter does not read. The message gives the getter that does.
|
|
104
|
+
* @throws A **SeedcordTypeError** from a select getter called with `required` when its field picked nothing.
|
|
105
|
+
*/
|
|
106
|
+
var ModalFields = class {
|
|
107
|
+
entries = /* @__PURE__ */ new Map();
|
|
108
|
+
resolved;
|
|
109
|
+
constructor(data) {
|
|
110
|
+
this.resolved = data.resolved;
|
|
111
|
+
for (const component of data.components ?? []) {
|
|
112
|
+
if ("components" in component) for (const child of component.components) this.entries.set(child.custom_id, child);
|
|
113
|
+
if ("component" in component) this.entries.set(component.component.custom_id, component.component);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
entry(customId) {
|
|
117
|
+
const found = this.entries.get(customId);
|
|
118
|
+
if (!found) throw new SeedcordTypeError(SeedcordErrorCode.ModalFieldNotFound, [customId]);
|
|
119
|
+
return found;
|
|
120
|
+
}
|
|
121
|
+
field(customId, kinds) {
|
|
122
|
+
const entry = this.entry(customId);
|
|
123
|
+
if (!isKind(entry, kinds)) {
|
|
124
|
+
const known = FIELD_KINDS[entry.type];
|
|
125
|
+
const args = known ? [
|
|
126
|
+
customId,
|
|
127
|
+
known.label,
|
|
128
|
+
known.getter
|
|
129
|
+
] : [customId, `component type ${entry.type}`];
|
|
130
|
+
throw new SeedcordTypeError(SeedcordErrorCode.ModalFieldWrongKind, [...args]);
|
|
131
|
+
}
|
|
132
|
+
return entry;
|
|
133
|
+
}
|
|
134
|
+
selected(customId, kinds, bucket, required) {
|
|
135
|
+
const { values } = this.field(customId, kinds);
|
|
136
|
+
const found = pick(values, bucket);
|
|
137
|
+
if (found.size > 0) return found;
|
|
138
|
+
if (required) throw new SeedcordTypeError(SeedcordErrorCode.ModalFieldEmpty, [customId]);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
getField(customId, kind) {
|
|
142
|
+
return kind === void 0 ? this.entry(customId) : this.field(customId, [kind]);
|
|
143
|
+
}
|
|
144
|
+
getTextInputValue(customId) {
|
|
145
|
+
return this.field(customId, [ComponentType.TextInput]).value;
|
|
146
|
+
}
|
|
147
|
+
getStringSelectValues(customId) {
|
|
148
|
+
return listOf(this.field(customId, [ComponentType.StringSelect]).values);
|
|
149
|
+
}
|
|
150
|
+
getSelectedUsers(customId, required = false) {
|
|
151
|
+
const kinds = [ComponentType.UserSelect, ComponentType.MentionableSelect];
|
|
152
|
+
return this.selected(customId, kinds, this.resolved?.users, required);
|
|
153
|
+
}
|
|
154
|
+
/** The guild members behind the picked users. Discord resolves these only inside a guild. */
|
|
155
|
+
getSelectedMembers(customId) {
|
|
156
|
+
const kinds = [ComponentType.UserSelect, ComponentType.MentionableSelect];
|
|
157
|
+
return this.selected(customId, kinds, this.resolved?.members, false);
|
|
158
|
+
}
|
|
159
|
+
getSelectedRoles(customId, required = false) {
|
|
160
|
+
const kinds = [ComponentType.RoleSelect, ComponentType.MentionableSelect];
|
|
161
|
+
return this.selected(customId, kinds, this.resolved?.roles, required);
|
|
162
|
+
}
|
|
163
|
+
getSelectedChannels(customId, required = false, channelTypes = []) {
|
|
164
|
+
const kinds = [ComponentType.ChannelSelect];
|
|
165
|
+
const picked = this.selected(customId, kinds, this.resolved?.channels, required);
|
|
166
|
+
if (!picked || channelTypes.length === 0) return picked;
|
|
167
|
+
for (const found of picked.values()) {
|
|
168
|
+
if (channelTypes.includes(found.type)) continue;
|
|
169
|
+
throw new SeedcordTypeError(SeedcordErrorCode.ModalFieldChannelType, [
|
|
170
|
+
customId,
|
|
171
|
+
nameOfChannelType(found.type),
|
|
172
|
+
channelTypes.map(nameOfChannelType)
|
|
173
|
+
]);
|
|
174
|
+
}
|
|
175
|
+
return picked;
|
|
176
|
+
}
|
|
177
|
+
getSelectedMentionables(customId, required = false) {
|
|
178
|
+
const { values } = this.field(customId, [ComponentType.MentionableSelect]);
|
|
179
|
+
const resolved = this.resolved;
|
|
180
|
+
const picked = {
|
|
181
|
+
users: pick(values, resolved?.users),
|
|
182
|
+
members: pick(values, resolved?.members),
|
|
183
|
+
roles: pick(values, resolved?.roles)
|
|
184
|
+
};
|
|
185
|
+
if (picked.users.size + picked.members.size + picked.roles.size > 0) return picked;
|
|
186
|
+
if (required) throw new SeedcordTypeError(SeedcordErrorCode.ModalFieldEmpty, [customId]);
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
getUploadedFiles(customId, required = false) {
|
|
190
|
+
const kinds = [ComponentType.FileUpload];
|
|
191
|
+
return this.selected(customId, kinds, this.resolved?.attachments, required);
|
|
192
|
+
}
|
|
193
|
+
getRadioGroup(customId, required = false) {
|
|
194
|
+
const value = this.field(customId, [ComponentType.RadioGroup]).value ?? null;
|
|
195
|
+
if (value !== null) return value;
|
|
196
|
+
if (required) throw new SeedcordTypeError(SeedcordErrorCode.ModalFieldEmpty, [customId]);
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
getCheckboxGroup(customId) {
|
|
200
|
+
return listOf(this.field(customId, [ComponentType.CheckboxGroup]).values);
|
|
201
|
+
}
|
|
202
|
+
getCheckbox(customId) {
|
|
203
|
+
return this.field(customId, [ComponentType.Checkbox]).value;
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
//#endregion
|
|
208
|
+
//#region src/emojis/EmojiInjector.ts
|
|
209
|
+
const emojiStorage = accessorStore();
|
|
210
|
+
/**
|
|
211
|
+
* The bot's resolved emojis, keyed by {@link EmojiMap}. A node bot fills these during startup. An edge
|
|
212
|
+
* bot fills them on its first request, from the ids `seedcord build` wrote into the worker.
|
|
213
|
+
*
|
|
214
|
+
* @throws A **SeedcordError** when you read a key that was not filled.
|
|
215
|
+
*/
|
|
216
|
+
const Emojis = guardedAccessor("Emojis", emojiStorage);
|
|
217
|
+
function installEmojis(emojis) {
|
|
218
|
+
clearStore(emojiStorage);
|
|
219
|
+
for (const [key, { name, id, animated }] of Object.entries(emojis)) emojiStorage[key] = new ResolvedEmoji(name, id, animated);
|
|
220
|
+
}
|
|
221
|
+
var EmojiInjector = class {
|
|
222
|
+
core;
|
|
223
|
+
logger = new Logger("Emojis", { channel: "bot" });
|
|
224
|
+
constructor(core) {
|
|
225
|
+
this.core = core;
|
|
226
|
+
}
|
|
227
|
+
async init() {
|
|
228
|
+
const configured = this.core.config.bot.emojis;
|
|
229
|
+
if (!configured || Object.keys(configured).length === 0) {
|
|
230
|
+
installEmojis({});
|
|
231
|
+
this.logger.debug("No emojis configured, skipping emoji injection.");
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
installEmojis(await resolveEmojis(this.core.rest, this.core.applicationId, configured));
|
|
235
|
+
this.logger.utils.summary("Loaded emojis", { emojis: Object.keys(emojiStorage).length });
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
//#endregion
|
|
16
240
|
//#region src/reply/ReplySender.ts
|
|
17
241
|
function wireAllowedMentions(mentions) {
|
|
18
242
|
const { repliedUser, ...wire } = mentions;
|
|
@@ -170,484 +394,359 @@ var RepliableHandler$1 = class extends RepliableHandler {
|
|
|
170
394
|
};
|
|
171
395
|
|
|
172
396
|
//#endregion
|
|
173
|
-
//#region src/
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
397
|
+
//#region src/handlers/interaction/InteractionHandler.ts
|
|
398
|
+
/**
|
|
399
|
+
* Shared base the repliable interaction handlers extend.
|
|
400
|
+
*
|
|
401
|
+
* Not a public entry point. Extend {@link SlashHandler}, {@link ContextMenuHandler}, {@link ButtonHandler},
|
|
402
|
+
* {@link ModalHandler}, or one of the select menu bases instead. This class adds `showModal` on top
|
|
403
|
+
* of the reply members.
|
|
404
|
+
*
|
|
405
|
+
* @typeParam Event - The repliable interaction type this handler processes
|
|
406
|
+
*/
|
|
407
|
+
var InteractionHandler = class extends RepliableHandler$1 {
|
|
408
|
+
constructor(event, core, dispatch) {
|
|
409
|
+
super(event, core, dispatch);
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Open a modal. Must be the initial response to this interaction. The modal kind rejects this call at
|
|
413
|
+
* compile time (Discord forbids a modal in response to a modal).
|
|
414
|
+
*/
|
|
415
|
+
showModal(modal) {
|
|
416
|
+
return this.sender.showModal(modal);
|
|
417
|
+
}
|
|
418
|
+
};
|
|
193
419
|
|
|
194
420
|
//#endregion
|
|
195
|
-
//#region src/
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
421
|
+
//#region src/handlers/interaction/InteractionMiddleware.ts
|
|
422
|
+
/**
|
|
423
|
+
* Base class for interaction middleware on the HTTP transport.
|
|
424
|
+
*
|
|
425
|
+
* Middleware runs after the dispatcher builds the handler and before the gates, over the handler's own
|
|
426
|
+
* reply surface. Name the kinds in the generic and list the same ones in `{ kinds }`. Several kinds give
|
|
427
|
+
* `this.event` a union. Narrow it on `type` and `data.component_type`. A catchall reads every repliable
|
|
428
|
+
* kind.
|
|
429
|
+
*
|
|
430
|
+
* @typeParam Kind - One or more {@link InteractionKind} members. Defaults to every repliable kind.
|
|
431
|
+
*
|
|
432
|
+
* @example
|
|
433
|
+
* ```ts
|
|
434
|
+
* \@RegisterInteractionMiddleware({ kinds: [InteractionKind.Button] })
|
|
435
|
+
* class Audit extends InteractionMiddleware<InteractionKind.Button> {
|
|
436
|
+
* async execute() {
|
|
437
|
+
* this.logger.info(this.event.data.custom_id);
|
|
438
|
+
* }
|
|
439
|
+
* }
|
|
440
|
+
* ```
|
|
441
|
+
*/
|
|
442
|
+
var InteractionMiddleware = class extends RepliableHandler$1 {
|
|
443
|
+
constructor(event, core, dispatch, sender) {
|
|
444
|
+
super(event, core, dispatch, sender);
|
|
445
|
+
}
|
|
446
|
+
};
|
|
202
447
|
|
|
203
448
|
//#endregion
|
|
204
|
-
//#region src/
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
});
|
|
217
|
-
else core.bus[PublishDefault]("unknownException", {
|
|
218
|
-
...from,
|
|
219
|
-
error,
|
|
220
|
-
...actors(payload),
|
|
221
|
-
metadata: payload
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
function actors(payload) {
|
|
225
|
-
const user = payload.member?.user ?? payload.user;
|
|
226
|
-
return {
|
|
227
|
-
guild: void 0,
|
|
228
|
-
user: user ? {
|
|
229
|
-
id: user.id,
|
|
230
|
-
username: user.username
|
|
231
|
-
} : void 0
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
function interactionSource(payload) {
|
|
235
|
-
const user = payload.member?.user ?? payload.user;
|
|
236
|
-
if (!user) return null;
|
|
237
|
-
const common = {
|
|
238
|
-
kind: "interaction",
|
|
239
|
-
userId: user.id,
|
|
240
|
-
guildId: payload.guild_id ?? null,
|
|
241
|
-
channelId: payload.channel?.id ?? null,
|
|
242
|
-
interactionId: payload.id,
|
|
243
|
-
raw: payload
|
|
244
|
-
};
|
|
245
|
-
if (payload.type === InteractionType.ApplicationCommand) {
|
|
246
|
-
const { data } = payload;
|
|
247
|
-
const isSlash = data.type === ApplicationCommandType.ChatInput;
|
|
248
|
-
return {
|
|
249
|
-
...common,
|
|
250
|
-
interactionKind: isSlash ? "slash" : "context-menu",
|
|
251
|
-
command: isSlash ? slashRouteOf(data) : data.name,
|
|
252
|
-
customId: null
|
|
253
|
-
};
|
|
449
|
+
//#region src/inputs/HttpSlashOptions.ts
|
|
450
|
+
var HttpSlashOptions = class {
|
|
451
|
+
leaves = /* @__PURE__ */ new Map();
|
|
452
|
+
resolved;
|
|
453
|
+
constructor(data) {
|
|
454
|
+
this.resolved = data.resolved;
|
|
455
|
+
let options = data.options ?? [];
|
|
456
|
+
const first = options[0];
|
|
457
|
+
if (first?.type === ApplicationCommandOptionType.SubcommandGroup) options = first.options;
|
|
458
|
+
const second = options[0];
|
|
459
|
+
if (second?.type === ApplicationCommandOptionType.Subcommand) options = second.options ?? [];
|
|
460
|
+
for (const option of options) if (option.type !== ApplicationCommandOptionType.Subcommand && option.type !== ApplicationCommandOptionType.SubcommandGroup) this.leaves.set(option.name, option);
|
|
254
461
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
command: null,
|
|
259
|
-
customId: payload.data.custom_id
|
|
260
|
-
};
|
|
261
|
-
if (payload.type === InteractionType.MessageComponent) {
|
|
262
|
-
const isButton = payload.data.component_type === ComponentType.Button;
|
|
263
|
-
return {
|
|
264
|
-
...common,
|
|
265
|
-
interactionKind: isButton ? "button" : "select",
|
|
266
|
-
command: null,
|
|
267
|
-
customId: payload.data.custom_id
|
|
268
|
-
};
|
|
462
|
+
getString(name) {
|
|
463
|
+
const leaf = this.leaves.get(name);
|
|
464
|
+
return leaf?.type === ApplicationCommandOptionType.String ? leaf.value : null;
|
|
269
465
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
//#endregion
|
|
274
|
-
//#region src/dispatch/dispatchInteraction.ts
|
|
275
|
-
let dispatchLogger;
|
|
276
|
-
function logger() {
|
|
277
|
-
dispatchLogger ??= new Logger("Dispatcher", { channel: "interactions" });
|
|
278
|
-
return dispatchLogger;
|
|
279
|
-
}
|
|
280
|
-
function noLifecycle(accessor) {
|
|
281
|
-
throw new SeedcordError(SeedcordErrorCode.CoreLifecycleUnavailable, [accessor]);
|
|
282
|
-
}
|
|
283
|
-
const edgeShutdown = { addTask: () => noLifecycle("shutdown") };
|
|
284
|
-
const edgeStartup = { addTask: () => noLifecycle("startup") };
|
|
285
|
-
const EDGE_SWEEPERS = {
|
|
286
|
-
hashSweepInterval: 0,
|
|
287
|
-
handlerSweepInterval: 0
|
|
288
|
-
};
|
|
289
|
-
function edgeRestOptions(given = {}) {
|
|
290
|
-
for (const key of Object.keys(EDGE_SWEEPERS)) if (given[key] !== void 0) throw new SeedcordError(SeedcordErrorCode.ConfigEdgeRestSweeper, [key]);
|
|
291
|
-
return {
|
|
292
|
-
...given,
|
|
293
|
-
...EDGE_SWEEPERS
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
function createCore(config, token) {
|
|
297
|
-
const draft = {
|
|
298
|
-
config,
|
|
299
|
-
rateLimiter: config.store ?? new MemoryRateLimiter(),
|
|
300
|
-
rest: new REST(edgeRestOptions(config.bot.restOptions)).setToken(token),
|
|
301
|
-
shutdown: edgeShutdown,
|
|
302
|
-
startup: edgeStartup,
|
|
303
|
-
get applicationId() {
|
|
304
|
-
return applicationIdFromToken(token);
|
|
305
|
-
}
|
|
306
|
-
};
|
|
307
|
-
draft.bus = new Bus(draft);
|
|
308
|
-
return draft;
|
|
309
|
-
}
|
|
310
|
-
const HARMLESS_API_CODES = /* @__PURE__ */ new Set([
|
|
311
|
-
RESTJSONErrorCodes.UnknownInteraction,
|
|
312
|
-
RESTJSONErrorCodes.InteractionHasAlreadyBeenAcknowledged,
|
|
313
|
-
RESTJSONErrorCodes.UnknownMessage
|
|
314
|
-
]);
|
|
315
|
-
async function sendGuarded(routeId, send) {
|
|
316
|
-
try {
|
|
317
|
-
await send();
|
|
318
|
-
} catch (error) {
|
|
319
|
-
if (error instanceof DiscordAPIError && HARMLESS_API_CODES.has(error.code)) {
|
|
320
|
-
logger().debug(`boundary send hit harmless code ${paint.amber(String(error.code))}`);
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
logger().error(`boundary send failed for route ${paint.sky.bold(routeId)}`, error);
|
|
466
|
+
getInteger(name) {
|
|
467
|
+
const leaf = this.leaves.get(name);
|
|
468
|
+
return leaf?.type === ApplicationCommandOptionType.Integer ? numeric(leaf.value) : null;
|
|
324
469
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
bus: scope.core.bus,
|
|
329
|
-
dispatch: scope.dispatch,
|
|
330
|
-
interactionId: scope.payload.id
|
|
331
|
-
};
|
|
332
|
-
await reportedWrite(telemetry, "respond", () => scope.core.rest.post(Routes.interactionCallback(scope.payload.id, scope.payload.token), { body: {
|
|
333
|
-
type: InteractionResponseType.ApplicationCommandAutocompleteResult,
|
|
334
|
-
data: { choices: [] }
|
|
335
|
-
} }));
|
|
336
|
-
}
|
|
337
|
-
function renderContext(scope, uuid) {
|
|
338
|
-
const { dispatch } = scope;
|
|
339
|
-
const developerUsername = scope.core.config.notifications?.developerUsername;
|
|
340
|
-
return developerUsername === void 0 ? {
|
|
341
|
-
uuid,
|
|
342
|
-
dispatch
|
|
343
|
-
} : {
|
|
344
|
-
uuid,
|
|
345
|
-
developerUsername,
|
|
346
|
-
dispatch
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
async function handleNotice(notice, uuid, scope) {
|
|
350
|
-
if (notice.report) {
|
|
351
|
-
logger().error(`${notice.name}: ${paint.mute(uuid)}`, notice);
|
|
352
|
-
reportFault(notice, uuid, scope.dispatch, scope.payload, scope.core);
|
|
470
|
+
getNumber(name) {
|
|
471
|
+
const leaf = this.leaves.get(name);
|
|
472
|
+
return leaf?.type === ApplicationCommandOptionType.Number ? numeric(leaf.value) : null;
|
|
353
473
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
return;
|
|
474
|
+
getBoolean(name) {
|
|
475
|
+
const leaf = this.leaves.get(name);
|
|
476
|
+
return leaf?.type === ApplicationCommandOptionType.Boolean ? leaf.value : null;
|
|
358
477
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const { core, sender } = scope;
|
|
364
|
-
if (core.config.errors?.errorStack ?? false) logger().error(paint.mute(uuid), error);
|
|
365
|
-
else logger().error(`${paint.mute(uuid)} | ${error.message}`);
|
|
366
|
-
reportFault(error, uuid, scope.dispatch, scope.payload, core);
|
|
367
|
-
if (!sender) {
|
|
368
|
-
await sendGuarded(scope.routeId, () => respondEmptyChoices(scope));
|
|
369
|
-
return;
|
|
478
|
+
getUser(name) {
|
|
479
|
+
const leaf = this.leaves.get(name);
|
|
480
|
+
if (leaf?.type !== ApplicationCommandOptionType.User) return null;
|
|
481
|
+
return this.resolved?.users?.[leaf.value] ?? null;
|
|
370
482
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
async function handleFault(caught, scope) {
|
|
376
|
-
if (caught instanceof Silence) {
|
|
377
|
-
if (caught.reason !== void 0 && (scope.core.config.errors?.logSilences ?? true)) logger().debug(`Silence: ${caught.reason}`);
|
|
378
|
-
return;
|
|
483
|
+
getMember(name) {
|
|
484
|
+
const leaf = this.leaves.get(name);
|
|
485
|
+
if (leaf?.type !== ApplicationCommandOptionType.User) return null;
|
|
486
|
+
return this.resolved?.members?.[leaf.value] ?? null;
|
|
379
487
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
return;
|
|
488
|
+
getChannel(name) {
|
|
489
|
+
const leaf = this.leaves.get(name);
|
|
490
|
+
if (leaf?.type !== ApplicationCommandOptionType.Channel) return null;
|
|
491
|
+
return this.resolved?.channels?.[leaf.value] ?? null;
|
|
384
492
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
await handleRawFault(error, uuid, scope);
|
|
392
|
-
}
|
|
393
|
-
function unhandledRouteId(match) {
|
|
394
|
-
if (match.routeId) return match.routeId;
|
|
395
|
-
const key = match.attemptedKey ?? "";
|
|
396
|
-
return `${match.kind}:${key.length > 0 ? key : "unrouted"}`;
|
|
397
|
-
}
|
|
398
|
-
function freshScope(match, payload, core, dispatch) {
|
|
399
|
-
const ref = {
|
|
400
|
-
application_id: payload.application_id,
|
|
401
|
-
id: payload.id,
|
|
402
|
-
token: payload.token
|
|
403
|
-
};
|
|
404
|
-
return {
|
|
405
|
-
core,
|
|
406
|
-
payload,
|
|
407
|
-
routeId: unhandledRouteId(match),
|
|
408
|
-
dispatch,
|
|
409
|
-
sender: match.kind === InteractionKind.Autocomplete ? null : new ReplySender(ref, core.rest, dispatch, core.bus)
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
function dispatchReporter(match, payload, core, dispatchId) {
|
|
413
|
-
const startedAt = performance.now();
|
|
414
|
-
const queuedMs = queuedMsFor(payload.id);
|
|
415
|
-
return (outcome) => {
|
|
416
|
-
reportDispatch(core.bus, {
|
|
417
|
-
dispatchId,
|
|
418
|
-
routeId: unhandledRouteId(match),
|
|
419
|
-
interactionId: payload.id,
|
|
420
|
-
kind: match.kind,
|
|
421
|
-
outcome,
|
|
422
|
-
fallback: match.routeId === null,
|
|
423
|
-
userId: (payload.member?.user ?? payload.user)?.id ?? null,
|
|
424
|
-
guildId: payload.guild_id ?? null,
|
|
425
|
-
startedAt,
|
|
426
|
-
queuedMs
|
|
427
|
-
});
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
async function answer(caught, scope, report, outcome = outcomeFor(caught)) {
|
|
431
|
-
try {
|
|
432
|
-
await handleFault(caught, scope);
|
|
433
|
-
} finally {
|
|
434
|
-
report(outcome);
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
async function runMiddlewares(step, kind, sender) {
|
|
438
|
-
const { args, dispatch, ran } = step;
|
|
439
|
-
for (const Middleware of args.middlewares.chainFor(kind)) {
|
|
440
|
-
const event = args.payload;
|
|
441
|
-
const instance = new Middleware(event, args.core, dispatch, sender);
|
|
442
|
-
ran.push(instance);
|
|
443
|
-
await instance.execute();
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
async function refusalBeforeHandler(step) {
|
|
447
|
-
const { args, scope } = step;
|
|
448
|
-
const { kind } = args.match;
|
|
449
|
-
if (kind !== InteractionKind.Autocomplete && scope.sender) try {
|
|
450
|
-
await runMiddlewares(step, kind, scope.sender);
|
|
451
|
-
} catch (caught) {
|
|
452
|
-
return { caught };
|
|
453
|
-
}
|
|
454
|
-
return gateRefusal(step);
|
|
455
|
-
}
|
|
456
|
-
async function dispatchInteraction(args) {
|
|
457
|
-
const { match, payload, core } = args;
|
|
458
|
-
const routeId = unhandledRouteId(match);
|
|
459
|
-
const dispatch = new DispatchContext(routeId);
|
|
460
|
-
const report = dispatchReporter(match, payload, core, dispatch.id);
|
|
461
|
-
const Handler = match.ctor;
|
|
462
|
-
logger().debug(`Processing ${paint.sky.bold(routeId)} with ${paint.mute(Handler.name)}`);
|
|
463
|
-
let handler;
|
|
464
|
-
try {
|
|
465
|
-
handler = new Handler(payload, core, dispatch);
|
|
466
|
-
} catch (caught) {
|
|
467
|
-
await answer(caught, freshScope(match, payload, core, dispatch), report);
|
|
468
|
-
return null;
|
|
469
|
-
}
|
|
470
|
-
const scope = {
|
|
471
|
-
core,
|
|
472
|
-
payload,
|
|
473
|
-
routeId,
|
|
474
|
-
dispatch,
|
|
475
|
-
sender: handler instanceof RepliableHandler$1 ? handler.sender : null
|
|
476
|
-
};
|
|
477
|
-
const ran = [];
|
|
478
|
-
const refusal = await refusalBeforeHandler({
|
|
479
|
-
args,
|
|
480
|
-
Handler,
|
|
481
|
-
dispatch,
|
|
482
|
-
scope,
|
|
483
|
-
ran
|
|
484
|
-
});
|
|
485
|
-
if (refusal) {
|
|
486
|
-
try {
|
|
487
|
-
await answer(refusal.caught, scope, report);
|
|
488
|
-
} finally {
|
|
489
|
-
await runAfter(ran, resultFor(refusal.caught), logger());
|
|
490
|
-
}
|
|
491
|
-
return null;
|
|
492
|
-
}
|
|
493
|
-
return async () => {
|
|
494
|
-
let result = { outcome: "handled" };
|
|
495
|
-
try {
|
|
496
|
-
await handler.execute();
|
|
497
|
-
report("handled");
|
|
498
|
-
} catch (caught) {
|
|
499
|
-
result = resultFor(caught);
|
|
500
|
-
await answer(caught, scope, report);
|
|
501
|
-
} finally {
|
|
502
|
-
await runAfter(ran, result, logger());
|
|
503
|
-
}
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
async function gateRefusal(step) {
|
|
507
|
-
const { Handler, dispatch } = step;
|
|
508
|
-
const { match, payload, core } = step.args;
|
|
509
|
-
if (match.kind === InteractionKind.Autocomplete || payload.type === InteractionType.ApplicationCommandAutocomplete) return null;
|
|
510
|
-
const monitor = slowGateMonitor();
|
|
511
|
-
try {
|
|
512
|
-
await runHandlerGates(Handler, interactionGateContext(payload, core, dispatch), match.routeId ?? void 0, monitor?.observe);
|
|
513
|
-
return null;
|
|
514
|
-
} catch (caught) {
|
|
515
|
-
return { caught };
|
|
516
|
-
} finally {
|
|
517
|
-
monitor?.report(match.routeId);
|
|
493
|
+
getRole(name) {
|
|
494
|
+
const leaf = this.leaves.get(name);
|
|
495
|
+
if (leaf?.type !== ApplicationCommandOptionType.Role) return null;
|
|
496
|
+
return this.resolved?.roles?.[leaf.value] ?? null;
|
|
518
497
|
}
|
|
498
|
+
getAttachment(name) {
|
|
499
|
+
const leaf = this.leaves.get(name);
|
|
500
|
+
if (leaf?.type !== ApplicationCommandOptionType.Attachment) return null;
|
|
501
|
+
return this.resolved?.attachments?.[leaf.value] ?? null;
|
|
502
|
+
}
|
|
503
|
+
getMentionable(name) {
|
|
504
|
+
const leaf = this.leaves.get(name);
|
|
505
|
+
if (leaf?.type !== ApplicationCommandOptionType.Mentionable) return null;
|
|
506
|
+
const resolved = this.resolved;
|
|
507
|
+
return resolved?.members?.[leaf.value] ?? resolved?.users?.[leaf.value] ?? resolved?.roles?.[leaf.value] ?? null;
|
|
508
|
+
}
|
|
509
|
+
getFocused() {
|
|
510
|
+
for (const leaf of this.leaves.values()) if ("focused" in leaf && leaf.focused === true) return {
|
|
511
|
+
name: leaf.name,
|
|
512
|
+
value: String(leaf.value)
|
|
513
|
+
};
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
function numeric(value) {
|
|
518
|
+
if (typeof value === "number") return value;
|
|
519
|
+
if (value.trim() === "") return null;
|
|
520
|
+
const parsed = Number(value);
|
|
521
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
519
522
|
}
|
|
520
523
|
|
|
521
524
|
//#endregion
|
|
522
|
-
//#region src/
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
let options = data.options ?? [];
|
|
529
|
-
const first = options[0];
|
|
530
|
-
if (first?.type === ApplicationCommandOptionType.SubcommandGroup) options = first.options;
|
|
531
|
-
const second = options[0];
|
|
532
|
-
if (second?.type === ApplicationCommandOptionType.Subcommand) options = second.options ?? [];
|
|
533
|
-
for (const option of options) if (option.type !== ApplicationCommandOptionType.Subcommand && option.type !== ApplicationCommandOptionType.SubcommandGroup) this.leaves.set(option.name, option);
|
|
534
|
-
}
|
|
535
|
-
getString(name) {
|
|
536
|
-
const leaf = this.leaves.get(name);
|
|
537
|
-
return leaf?.type === ApplicationCommandOptionType.String ? leaf.value : null;
|
|
538
|
-
}
|
|
539
|
-
getInteger(name) {
|
|
540
|
-
const leaf = this.leaves.get(name);
|
|
541
|
-
return leaf?.type === ApplicationCommandOptionType.Integer ? numeric(leaf.value) : null;
|
|
542
|
-
}
|
|
543
|
-
getNumber(name) {
|
|
544
|
-
const leaf = this.leaves.get(name);
|
|
545
|
-
return leaf?.type === ApplicationCommandOptionType.Number ? numeric(leaf.value) : null;
|
|
546
|
-
}
|
|
547
|
-
getBoolean(name) {
|
|
548
|
-
const leaf = this.leaves.get(name);
|
|
549
|
-
return leaf?.type === ApplicationCommandOptionType.Boolean ? leaf.value : null;
|
|
550
|
-
}
|
|
551
|
-
getUser(name) {
|
|
552
|
-
const leaf = this.leaves.get(name);
|
|
553
|
-
if (leaf?.type !== ApplicationCommandOptionType.User) return null;
|
|
554
|
-
return this.resolved?.users?.[leaf.value] ?? null;
|
|
555
|
-
}
|
|
556
|
-
getMember(name) {
|
|
557
|
-
const leaf = this.leaves.get(name);
|
|
558
|
-
if (leaf?.type !== ApplicationCommandOptionType.User) return null;
|
|
559
|
-
return this.resolved?.members?.[leaf.value] ?? null;
|
|
560
|
-
}
|
|
561
|
-
getChannel(name) {
|
|
562
|
-
const leaf = this.leaves.get(name);
|
|
563
|
-
if (leaf?.type !== ApplicationCommandOptionType.Channel) return null;
|
|
564
|
-
return this.resolved?.channels?.[leaf.value] ?? null;
|
|
565
|
-
}
|
|
566
|
-
getRole(name) {
|
|
567
|
-
const leaf = this.leaves.get(name);
|
|
568
|
-
if (leaf?.type !== ApplicationCommandOptionType.Role) return null;
|
|
569
|
-
return this.resolved?.roles?.[leaf.value] ?? null;
|
|
570
|
-
}
|
|
571
|
-
getAttachment(name) {
|
|
572
|
-
const leaf = this.leaves.get(name);
|
|
573
|
-
if (leaf?.type !== ApplicationCommandOptionType.Attachment) return null;
|
|
574
|
-
return this.resolved?.attachments?.[leaf.value] ?? null;
|
|
575
|
-
}
|
|
576
|
-
getMentionable(name) {
|
|
577
|
-
const leaf = this.leaves.get(name);
|
|
578
|
-
if (leaf?.type !== ApplicationCommandOptionType.Mentionable) return null;
|
|
579
|
-
const resolved = this.resolved;
|
|
580
|
-
return resolved?.members?.[leaf.value] ?? resolved?.users?.[leaf.value] ?? resolved?.roles?.[leaf.value] ?? null;
|
|
581
|
-
}
|
|
582
|
-
getFocused() {
|
|
583
|
-
for (const leaf of this.leaves.values()) if ("focused" in leaf && leaf.focused === true) return {
|
|
584
|
-
name: leaf.name,
|
|
585
|
-
value: String(leaf.value)
|
|
586
|
-
};
|
|
587
|
-
return null;
|
|
588
|
-
}
|
|
589
|
-
};
|
|
590
|
-
function numeric(value) {
|
|
591
|
-
if (typeof value === "number") return value;
|
|
592
|
-
if (value.trim() === "") return null;
|
|
593
|
-
const parsed = Number(value);
|
|
594
|
-
return Number.isNaN(parsed) ? null : parsed;
|
|
525
|
+
//#region src/dispatch/slashRouteOf.ts
|
|
526
|
+
function slashRouteOf(data) {
|
|
527
|
+
const first = data.options?.[0];
|
|
528
|
+
if (first?.type === ApplicationCommandOptionType.SubcommandGroup) return buildSlashRoute(data.name, first.options[0]?.name, first.name);
|
|
529
|
+
if (first?.type === ApplicationCommandOptionType.Subcommand) return buildSlashRoute(data.name, first.name);
|
|
530
|
+
return data.name;
|
|
595
531
|
}
|
|
596
532
|
|
|
597
533
|
//#endregion
|
|
598
|
-
//#region src/handlers/interaction/
|
|
534
|
+
//#region src/handlers/interaction/SlashHandler.ts
|
|
599
535
|
/**
|
|
600
|
-
* Base class for
|
|
536
|
+
* Base class for a chat-input (slash) command handler on the HTTP transport.
|
|
601
537
|
*
|
|
602
|
-
* Pass the
|
|
603
|
-
* `this.match
|
|
604
|
-
*
|
|
538
|
+
* Pass the route from the generated registry as the generic, then read `this.options` for a single
|
|
539
|
+
* command or `this.match` for several. Command authoring stays plain discord.js, `seedcord codegen`
|
|
540
|
+
* reads its `toJSON()` to type these options.
|
|
605
541
|
*
|
|
606
|
-
* @typeParam Route - One or more route keys from {@link SlashRegistry}, e.g. `'
|
|
542
|
+
* @typeParam Route - One or more route keys from {@link SlashRegistry}, e.g. `'ban'` or `'ban' | 'kick'`.
|
|
607
543
|
*/
|
|
608
|
-
var
|
|
609
|
-
|
|
610
|
-
super(event, core, dispatch, "interactions");
|
|
611
|
-
}
|
|
544
|
+
var SlashHandler = class extends InteractionHandler {
|
|
545
|
+
resolver;
|
|
612
546
|
/**
|
|
613
|
-
*
|
|
547
|
+
* The typed options for this command's route. Only the getters for kinds this command declares appear.
|
|
548
|
+
* Each getter's name argument is restricted to that command's options of its kind.
|
|
549
|
+
*
|
|
550
|
+
* - A required option drops the null.
|
|
551
|
+
*
|
|
552
|
+
* - Choices narrow to their literal union.
|
|
553
|
+
*
|
|
554
|
+
* - A channel option declared with `addChannelTypes` narrows to those types.
|
|
555
|
+
*
|
|
556
|
+
* Rich kinds resolve from the interaction's `resolved` data.
|
|
614
557
|
*/
|
|
615
|
-
get
|
|
616
|
-
return apiFor(this.core.rest);
|
|
617
|
-
}
|
|
618
|
-
resolver;
|
|
619
|
-
get reader() {
|
|
558
|
+
get options() {
|
|
620
559
|
this.resolver ??= new HttpSlashOptions(this.event.data);
|
|
621
560
|
return this.resolver;
|
|
622
561
|
}
|
|
623
562
|
/**
|
|
624
|
-
*
|
|
625
|
-
*
|
|
626
|
-
*/
|
|
627
|
-
get focused() {
|
|
628
|
-
const focused = this.reader.getFocused();
|
|
629
|
-
if (!focused) throw new SeedcordError(SeedcordErrorCode.AutocompleteNoFocusedOption);
|
|
630
|
-
return focused;
|
|
631
|
-
}
|
|
632
|
-
/**
|
|
633
|
-
* Run the arm for the focused field. An autocomplete always dispatches by which field is focused.
|
|
634
|
-
* Match is how you read it.
|
|
563
|
+
* Run the arm for whichever command fired. Use this only when the handler is registered for several
|
|
564
|
+
* routes.
|
|
635
565
|
*
|
|
636
|
-
* Provide one arm per
|
|
637
|
-
*
|
|
638
|
-
*
|
|
639
|
-
* focused field with no arm, only reachable from a stale-deployed command, throws at runtime.
|
|
566
|
+
* Provide one arm per registered route, keyed by its route string, and each arm receives that route's
|
|
567
|
+
* own narrowed options. The arms must cover every route in the generic, a missing route or an unknown
|
|
568
|
+
* key is a compile error.
|
|
640
569
|
*
|
|
641
|
-
* @param arms - One handler per
|
|
570
|
+
* @param arms - One handler per registered route, keyed by route string.
|
|
642
571
|
* @returns The result of the arm that ran.
|
|
643
572
|
*/
|
|
644
573
|
async match(arms) {
|
|
645
|
-
const
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
574
|
+
const route = slashRouteOf(this.event.data);
|
|
575
|
+
const arm = Object.hasOwn(arms, route) ? arms[route] : void 0;
|
|
576
|
+
if (!arm) throw new SeedcordTypeError(SeedcordErrorCode.MatchArmMissing, [
|
|
577
|
+
this.constructor.name,
|
|
578
|
+
"command route",
|
|
579
|
+
route
|
|
580
|
+
]);
|
|
581
|
+
return await arm(this.options);
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
//#endregion
|
|
586
|
+
//#region src/handlers/interaction/ContextMenuHandler.ts
|
|
587
|
+
function armFor(handler, arms, name) {
|
|
588
|
+
const arm = Object.hasOwn(arms, name) ? arms[name] : void 0;
|
|
589
|
+
if (typeof arm !== "function") throw new SeedcordTypeError(SeedcordErrorCode.MatchArmMissing, [
|
|
590
|
+
handler.constructor.name,
|
|
591
|
+
"context menu command",
|
|
592
|
+
name
|
|
593
|
+
]);
|
|
594
|
+
return arm;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Base class for a user context-menu command handler on the HTTP transport (right-click a user).
|
|
598
|
+
*
|
|
599
|
+
* Pass the command name(s) as the generic, the same as `@UserContextMenuRoute`. Read the right-clicked user
|
|
600
|
+
* from `this.target` and that same user's guild member from `this.targetMember`.
|
|
601
|
+
*
|
|
602
|
+
* @typeParam Names - One or more command names from the generated user registry.
|
|
603
|
+
*/
|
|
604
|
+
var UserContextMenuHandler = class extends InteractionHandler {
|
|
605
|
+
get target() {
|
|
606
|
+
const { target_id: targetId, resolved } = this.event.data;
|
|
607
|
+
return resolved.users[targetId];
|
|
608
|
+
}
|
|
609
|
+
/** The right-clicked user's guild member, null outside a guild. */
|
|
610
|
+
get targetMember() {
|
|
611
|
+
const { target_id: targetId, resolved } = this.event.data;
|
|
612
|
+
return resolved.members?.[targetId] ?? null;
|
|
613
|
+
}
|
|
614
|
+
/** Which of the registered commands fired. */
|
|
615
|
+
get commandName() {
|
|
616
|
+
return this.event.data.name;
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Run the arm for whichever command fired. Use this only when the handler is registered for several
|
|
620
|
+
* names.
|
|
621
|
+
*
|
|
622
|
+
* Provide one arm per registered name, keyed by the name Discord shows in the right-click menu. Each
|
|
623
|
+
* arm receives the clicked user and that user's guild member. The arms must cover every name in the
|
|
624
|
+
* generic. A missing name or an unknown key is a compile error.
|
|
625
|
+
*
|
|
626
|
+
* @param arms - One handler per registered command name.
|
|
627
|
+
* @returns The result of the arm that ran.
|
|
628
|
+
*
|
|
629
|
+
* @example
|
|
630
|
+
* ```ts
|
|
631
|
+
* \@UserContextMenuRoute('View Profile', 'Warn')
|
|
632
|
+
* class Moderation extends UserContextMenuHandler<'View Profile' | 'Warn'> {
|
|
633
|
+
* async execute() {
|
|
634
|
+
* await this.match({
|
|
635
|
+
* 'View Profile': (user) => this.reply(`Profile for ${user.username}.`),
|
|
636
|
+
* Warn: (user, member) => this.reply(`Warned ${member?.nick ?? user.username}.`)
|
|
637
|
+
* });
|
|
638
|
+
* }
|
|
639
|
+
* }
|
|
640
|
+
* ```
|
|
641
|
+
*/
|
|
642
|
+
async match(arms) {
|
|
643
|
+
return await armFor(this, arms, this.event.data.name)(this.target, this.targetMember);
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
/**
|
|
647
|
+
* Base class for a message context-menu command handler on the HTTP transport (right-click a message).
|
|
648
|
+
*
|
|
649
|
+
* Pass the command name(s) as the generic, the same as `@MessageContextMenuRoute`. Read the right-clicked
|
|
650
|
+
* message from `this.target`.
|
|
651
|
+
*
|
|
652
|
+
* @typeParam Names - One or more command names from the generated message registry.
|
|
653
|
+
*/
|
|
654
|
+
var MessageContextMenuHandler = class extends InteractionHandler {
|
|
655
|
+
get target() {
|
|
656
|
+
const { target_id: targetId, resolved } = this.event.data;
|
|
657
|
+
return resolved.messages[targetId];
|
|
658
|
+
}
|
|
659
|
+
/** Which of the registered commands fired. */
|
|
660
|
+
get commandName() {
|
|
661
|
+
return this.event.data.name;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Run the arm for whichever command fired. Use this only when the handler is registered for several
|
|
665
|
+
* names.
|
|
666
|
+
*
|
|
667
|
+
* Provide one arm per registered name, keyed by the name Discord shows in the right-click menu. Each
|
|
668
|
+
* arm receives the clicked message. The arms must cover every name in the generic. A missing name or
|
|
669
|
+
* an unknown key is a compile error.
|
|
670
|
+
*
|
|
671
|
+
* @param arms - One handler per registered command name.
|
|
672
|
+
* @returns The result of the arm that ran.
|
|
673
|
+
*
|
|
674
|
+
* @example
|
|
675
|
+
* ```ts
|
|
676
|
+
* \@MessageContextMenuRoute('Report Message', 'Quote Message')
|
|
677
|
+
* class MessageTools extends MessageContextMenuHandler<'Report Message' | 'Quote Message'> {
|
|
678
|
+
* async execute() {
|
|
679
|
+
* await this.match({
|
|
680
|
+
* 'Report Message': (message) => this.report(message),
|
|
681
|
+
* 'Quote Message': (message) => this.reply(`> ${message.content}`)
|
|
682
|
+
* });
|
|
683
|
+
* }
|
|
684
|
+
* }
|
|
685
|
+
* ```
|
|
686
|
+
*/
|
|
687
|
+
async match(arms) {
|
|
688
|
+
return await armFor(this, arms, this.event.data.name)(this.target);
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
//#endregion
|
|
693
|
+
//#region src/handlers/interaction/AutocompleteHandler.ts
|
|
694
|
+
/**
|
|
695
|
+
* Base class for an autocomplete handler on the HTTP transport.
|
|
696
|
+
*
|
|
697
|
+
* Pass the command route(s) from the generated registry as the generic. Branch on the focused field with
|
|
698
|
+
* `this.match`, read already-entered sibling options with `this.options`, and find which command fired
|
|
699
|
+
* with `this.route`.
|
|
700
|
+
*
|
|
701
|
+
* @typeParam Route - One or more route keys from {@link SlashRegistry}, e.g. `'search'`.
|
|
702
|
+
*/
|
|
703
|
+
var AutocompleteHandler = class extends BaseHandler {
|
|
704
|
+
constructor(event, core, dispatch) {
|
|
705
|
+
super(event, core, dispatch, "interactions");
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Typed Discord REST API (`@discordjs/core/http-only`) over `core.rest`, one instance per core.
|
|
709
|
+
*/
|
|
710
|
+
get api() {
|
|
711
|
+
return apiFor(this.core.rest);
|
|
712
|
+
}
|
|
713
|
+
resolver;
|
|
714
|
+
get reader() {
|
|
715
|
+
this.resolver ??= new HttpSlashOptions(this.event.data);
|
|
716
|
+
return this.resolver;
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* The focused option for this interaction. `value` is always the raw partial string the user is typing,
|
|
720
|
+
* even for an integer or number option, so coerce it yourself when you need a number.
|
|
721
|
+
*/
|
|
722
|
+
get focused() {
|
|
723
|
+
const focused = this.reader.getFocused();
|
|
724
|
+
if (!focused) throw new SeedcordError(SeedcordErrorCode.AutocompleteNoFocusedOption);
|
|
725
|
+
return focused;
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Run the arm for the focused field. An autocomplete always dispatches by which field is focused.
|
|
729
|
+
* Match is how you read it.
|
|
730
|
+
*
|
|
731
|
+
* Provide one arm per autocompletable field across the registered commands, keyed by field name. Each
|
|
732
|
+
* arm receives the focused partial value and a `respond` typed to that field's choice value. The
|
|
733
|
+
* arms must cover every autocompletable field. A missing field or an unknown key is a compile error.
|
|
734
|
+
*
|
|
735
|
+
* @param arms - One handler per autocompletable field, keyed by field name.
|
|
736
|
+
* @returns The result of the arm that ran.
|
|
737
|
+
* @throws A **SeedcordTypeError** with `MatchArmMissing` when the focused field has no arm. Only a stale-deployed command can send one.
|
|
738
|
+
*/
|
|
739
|
+
async match(arms) {
|
|
740
|
+
const { name, value } = this.focused;
|
|
741
|
+
const respond = (choices) => this.respond(choices);
|
|
742
|
+
const arm = Object.hasOwn(arms, name) ? arms[name] : void 0;
|
|
743
|
+
if (!arm) throw new SeedcordTypeError(SeedcordErrorCode.MatchArmMissing, [
|
|
744
|
+
this.constructor.name,
|
|
745
|
+
"focused field",
|
|
746
|
+
name
|
|
747
|
+
]);
|
|
748
|
+
return await arm(value, respond);
|
|
749
|
+
}
|
|
651
750
|
/** Send autocomplete suggestions, callback type 8. Prefer {@link match}, which restricts each field's choices to its declared type. */
|
|
652
751
|
async respond(choices) {
|
|
653
752
|
await reportedWrite({
|
|
@@ -674,158 +773,558 @@ var AutocompleteHandler = class extends BaseHandler {
|
|
|
674
773
|
};
|
|
675
774
|
|
|
676
775
|
//#endregion
|
|
677
|
-
//#region src/handlers/interaction/
|
|
776
|
+
//#region src/handlers/interaction/components/ComponentHandler.ts
|
|
678
777
|
/**
|
|
679
|
-
* Shared base the
|
|
778
|
+
* Shared base the customId-routed component handlers extend on the HTTP transport.
|
|
680
779
|
*
|
|
681
|
-
* Not a public entry point. Extend {@link
|
|
682
|
-
*
|
|
683
|
-
*
|
|
780
|
+
* Not a public entry point. Extend {@link ButtonHandler}, {@link ModalHandler}, or one of the select menu
|
|
781
|
+
* bases instead. This class defines the customId decode, the route matching, and the source-message
|
|
782
|
+
* verbs `update` and `deferUpdate`.
|
|
684
783
|
*
|
|
685
|
-
* @typeParam Event - The
|
|
784
|
+
* @typeParam Event - The component or modal interaction type this handler processes
|
|
785
|
+
* @typeParam Defs - The customId route definitions registered on the concrete handler
|
|
686
786
|
*/
|
|
687
|
-
var
|
|
688
|
-
|
|
689
|
-
|
|
787
|
+
var ComponentHandler = class extends InteractionHandler {
|
|
788
|
+
/** Rewrite the source message this component interaction came from. */
|
|
789
|
+
update(response) {
|
|
790
|
+
return this.sender.update(response);
|
|
791
|
+
}
|
|
792
|
+
/** Acknowledge the component without changing the source message. */
|
|
793
|
+
deferUpdate() {
|
|
794
|
+
return this.sender.deferUpdate();
|
|
795
|
+
}
|
|
796
|
+
decoded;
|
|
797
|
+
get route() {
|
|
798
|
+
this.decoded ??= decodeComponentRoute(this.constructor, this.event.data.custom_id);
|
|
799
|
+
return this.decoded;
|
|
690
800
|
}
|
|
691
801
|
/**
|
|
692
|
-
*
|
|
693
|
-
*
|
|
802
|
+
* The decoded params of the single route this handler is registered for.
|
|
803
|
+
*
|
|
804
|
+
* Reading this decodes `this.event.data.custom_id` once and caches the result. On a handler
|
|
805
|
+
* registered for several routes this is `never`. Use {@link match} instead.
|
|
806
|
+
*
|
|
807
|
+
* @throws A **StaleCustomId** when the wire was encoded under an older shape. The dispatcher boundary turns it into a reply.
|
|
808
|
+
* @throws An **InvalidCustomId** when the wire is corrupt. The dispatcher boundary turns it into a reply.
|
|
694
809
|
*/
|
|
695
|
-
|
|
696
|
-
return this.
|
|
810
|
+
get params() {
|
|
811
|
+
return this.route.params;
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Run the arm for whichever route the component was minted from. Use this only when the handler is
|
|
815
|
+
* registered for several routes. A single-route handler reads `this.params` directly. On a multi-route
|
|
816
|
+
* handler `this.params` is `never`, so match is the only way to read the decoded params.
|
|
817
|
+
*
|
|
818
|
+
* Provide one arm per registered route, keyed by its prefix, and each arm receives that route's own
|
|
819
|
+
* decoded params. The compiler checks that the arms cover every registered route prefix. `match`
|
|
820
|
+
* decodes the wire before it runs an arm.
|
|
821
|
+
*
|
|
822
|
+
* @param arms - One callback per registered route, keyed by prefix.
|
|
823
|
+
* @returns The result of the arm that ran.
|
|
824
|
+
* @throws A **SeedcordTypeError** with `MatchArmMissing` when the wire's prefix has no arm.
|
|
825
|
+
* @throws A **StaleCustomId** when the wire was encoded under an older shape.
|
|
826
|
+
* @throws An **InvalidCustomId** when the wire is corrupt.
|
|
827
|
+
*
|
|
828
|
+
* @example
|
|
829
|
+
* ```ts
|
|
830
|
+
* \@ButtonRoute(Approve, Reject)
|
|
831
|
+
* class ReviewButtons extends ButtonHandler<[typeof Approve, typeof Reject]> {
|
|
832
|
+
* async execute() {
|
|
833
|
+
* await this.match({
|
|
834
|
+
* approve: ({ userId }) => this.reply(`approved <@${userId}>`),
|
|
835
|
+
* reject: ({ userId }) => this.reply(`rejected <@${userId}>`)
|
|
836
|
+
* });
|
|
837
|
+
* }
|
|
838
|
+
* }
|
|
839
|
+
* ```
|
|
840
|
+
*/
|
|
841
|
+
async match(arms) {
|
|
842
|
+
const { prefix, params } = this.route;
|
|
843
|
+
const arm = Object.hasOwn(arms, prefix) ? arms[prefix] : void 0;
|
|
844
|
+
if (!arm) throw new SeedcordTypeError(SeedcordErrorCode.MatchArmMissing, [
|
|
845
|
+
this.constructor.name,
|
|
846
|
+
"component route",
|
|
847
|
+
prefix
|
|
848
|
+
]);
|
|
849
|
+
return await arm(params);
|
|
697
850
|
}
|
|
698
851
|
};
|
|
699
852
|
|
|
700
853
|
//#endregion
|
|
701
|
-
//#region src/handlers/interaction/
|
|
854
|
+
//#region src/handlers/interaction/components/ButtonHandler.ts
|
|
702
855
|
/**
|
|
703
|
-
* Base class for interaction
|
|
856
|
+
* Base class for a button interaction handler on the HTTP transport.
|
|
704
857
|
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
707
|
-
*
|
|
708
|
-
*
|
|
858
|
+
* Register the customId definitions this handler decodes with `@ButtonRoute`, list the same ones in the
|
|
859
|
+
* generic, then read `this.params` for a single route or `this.match` for several. Reply through the
|
|
860
|
+
* handler members, or rewrite the source message with `this.update`. Passing different definitions to the
|
|
861
|
+
* decorator and the generic is a compile error.
|
|
709
862
|
*
|
|
710
|
-
* @typeParam
|
|
863
|
+
* @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ApproveId]`.
|
|
864
|
+
*/
|
|
865
|
+
var ButtonHandler = class extends ComponentHandler {};
|
|
866
|
+
|
|
867
|
+
//#endregion
|
|
868
|
+
//#region src/handlers/interaction/components/ModalHandler.ts
|
|
869
|
+
/**
|
|
870
|
+
* Base class for a modal submit handler on the HTTP transport.
|
|
871
|
+
*
|
|
872
|
+
* Register the customId definitions this handler decodes with `@ModalRoute` and list the same ones in the
|
|
873
|
+
* generic. Read the submitted inputs from `this.fields`. Reply through the handler members. `showModal`
|
|
874
|
+
* fails compilation on this kind, because Discord forbids opening a modal in response to a modal.
|
|
875
|
+
*
|
|
876
|
+
* @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ConfigId]`.
|
|
711
877
|
*
|
|
712
878
|
* @example
|
|
713
879
|
* ```ts
|
|
714
|
-
* \@
|
|
715
|
-
* class
|
|
880
|
+
* \@ModalRoute(ConfigId)
|
|
881
|
+
* class ConfigModal extends ModalHandler<[typeof ConfigId]> {
|
|
716
882
|
* async execute() {
|
|
717
|
-
* this.
|
|
718
|
-
*
|
|
883
|
+
* const { guildId } = this.params;
|
|
884
|
+
* const name = this.fields.getTextInputValue('name');
|
|
885
|
+
* await this.reply(`saved ${name} for ${guildId}`);
|
|
886
|
+
* }
|
|
719
887
|
* }
|
|
720
888
|
* ```
|
|
721
889
|
*/
|
|
722
|
-
var
|
|
723
|
-
|
|
724
|
-
|
|
890
|
+
var ModalHandler = class extends ComponentHandler {
|
|
891
|
+
reader;
|
|
892
|
+
/** The fields this modal submitted, read by custom id. */
|
|
893
|
+
get fields() {
|
|
894
|
+
this.reader ??= new ModalFields(this.event.data);
|
|
895
|
+
return this.reader;
|
|
896
|
+
}
|
|
897
|
+
/** Rewrite the message this modal was opened from. Throws when a command opened the modal. */
|
|
898
|
+
update(response) {
|
|
899
|
+
this.ensureSourceMessage("update");
|
|
900
|
+
return super.update(response);
|
|
901
|
+
}
|
|
902
|
+
/** Acknowledge the submit without changing the source message. Throws when a command opened the modal. */
|
|
903
|
+
deferUpdate() {
|
|
904
|
+
this.ensureSourceMessage("deferUpdate");
|
|
905
|
+
return super.deferUpdate();
|
|
906
|
+
}
|
|
907
|
+
ensureSourceMessage(method) {
|
|
908
|
+
if (this.event.message) return;
|
|
909
|
+
throw new SeedcordError(SeedcordErrorCode.ReplyUpdateWithoutSource, [method, this.routeId]);
|
|
725
910
|
}
|
|
726
911
|
};
|
|
727
912
|
|
|
728
913
|
//#endregion
|
|
729
|
-
//#region src/
|
|
730
|
-
function
|
|
731
|
-
|
|
732
|
-
return value.prototype instanceof InteractionHandler || value.prototype instanceof AutocompleteHandler;
|
|
733
|
-
}
|
|
734
|
-
function isMiddlewareClass(value) {
|
|
735
|
-
return typeof value === "function" && value.prototype instanceof InteractionMiddleware;
|
|
736
|
-
}
|
|
737
|
-
function isSubscriberClass(value) {
|
|
738
|
-
return typeof value === "function" && value.prototype instanceof Subscriber;
|
|
739
|
-
}
|
|
740
|
-
function wrongClass(array, value, base) {
|
|
741
|
-
const name = typeof value === "function" ? value.name : String(value);
|
|
742
|
-
return new SeedcordTypeError(SeedcordErrorCode.ManifestEntryWrongClass, [
|
|
743
|
-
array,
|
|
744
|
-
name,
|
|
745
|
-
base
|
|
746
|
-
]);
|
|
747
|
-
}
|
|
748
|
-
function noRoutes(array, className, decorator) {
|
|
749
|
-
return new SeedcordError(SeedcordErrorCode.ManifestEntryNoRoutes, [
|
|
750
|
-
array,
|
|
751
|
-
className,
|
|
752
|
-
decorator
|
|
753
|
-
]);
|
|
914
|
+
//#region src/handlers/interaction/components/SelectMenuHandler.ts
|
|
915
|
+
function resolvedOf(data) {
|
|
916
|
+
return data.resolved ?? {};
|
|
754
917
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
918
|
+
/**
|
|
919
|
+
* Shared base the select menu handlers extend on the HTTP transport.
|
|
920
|
+
*
|
|
921
|
+
* Not a public entry point. Extend {@link StringMenuHandler}, {@link UserMenuHandler},
|
|
922
|
+
* {@link RoleMenuHandler}, {@link ChannelMenuHandler}, or {@link MentionableMenuHandler} instead. This
|
|
923
|
+
* class defines the `values` getter those bases share.
|
|
924
|
+
*
|
|
925
|
+
* @typeParam Event - The select menu interaction type this handler processes
|
|
926
|
+
* @typeParam Defs - The customId route definitions registered on the concrete handler
|
|
927
|
+
*/
|
|
928
|
+
var SelectMenuHandler = class extends ComponentHandler {
|
|
929
|
+
/** The values this select picked. On every kind but the string menu they are snowflake ids. */
|
|
930
|
+
get values() {
|
|
931
|
+
return this.event.data.values ?? [];
|
|
763
932
|
}
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
933
|
+
};
|
|
934
|
+
/**
|
|
935
|
+
* Base class for a string select menu handler on the HTTP transport.
|
|
936
|
+
*
|
|
937
|
+
* Register the customId definitions with `@StringMenuRoute`, list the same ones in the generic, then read
|
|
938
|
+
* the chosen option values from `this.values`. Read `this.params` for a single route or `this.match` for
|
|
939
|
+
* several. Reply through the handler members, or rewrite the source message with `this.update`.
|
|
940
|
+
*
|
|
941
|
+
* @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof TopicsId]`.
|
|
942
|
+
*
|
|
943
|
+
* @example
|
|
944
|
+
* ```ts
|
|
945
|
+
* \@StringMenuRoute(TopicsId)
|
|
946
|
+
* class Topics extends StringMenuHandler<[typeof TopicsId]> {
|
|
947
|
+
* async execute() {
|
|
948
|
+
* const { userId } = this.params;
|
|
949
|
+
* await this.update(`<@${userId}> now follows ${this.values.join(', ')}`);
|
|
950
|
+
* }
|
|
951
|
+
* }
|
|
952
|
+
* ```
|
|
953
|
+
*/
|
|
954
|
+
var StringMenuHandler = class extends SelectMenuHandler {};
|
|
955
|
+
/**
|
|
956
|
+
* Base class for a user select menu handler on the HTTP transport.
|
|
957
|
+
*
|
|
958
|
+
* Beside `this.values` this handler declares `this.users` and `this.members`. Register the customId
|
|
959
|
+
* definitions with `@UserMenuRoute` and list the same ones in the generic.
|
|
960
|
+
*
|
|
961
|
+
* @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof AssignId]`.
|
|
962
|
+
*
|
|
963
|
+
* @example
|
|
964
|
+
* ```ts
|
|
965
|
+
* \@UserMenuRoute(AssignId)
|
|
966
|
+
* class Assign extends UserMenuHandler<[typeof AssignId]> {
|
|
967
|
+
* async execute() {
|
|
968
|
+
* const { roleId } = this.params;
|
|
969
|
+
* await this.reply(`assigning ${this.users.size} member(s) to <@&${roleId}>`);
|
|
970
|
+
* }
|
|
971
|
+
* }
|
|
972
|
+
* ```
|
|
973
|
+
*/
|
|
974
|
+
var UserMenuHandler = class extends SelectMenuHandler {
|
|
975
|
+
/** The users this select picked. */
|
|
976
|
+
get users() {
|
|
977
|
+
return pick(this.values, resolvedOf(this.event.data).users);
|
|
978
|
+
}
|
|
979
|
+
/** The guild members behind the picked users. Discord resolves these only inside a guild. */
|
|
980
|
+
get members() {
|
|
981
|
+
return pick(this.values, resolvedOf(this.event.data).members);
|
|
771
982
|
}
|
|
772
983
|
};
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
984
|
+
/**
|
|
985
|
+
* Base class for a role select menu handler on the HTTP transport.
|
|
986
|
+
*
|
|
987
|
+
* Beside `this.values` this handler declares `this.roles`. Register the customId definitions with
|
|
988
|
+
* `@RoleMenuRoute` and list the same ones in the generic.
|
|
989
|
+
*
|
|
990
|
+
* @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof GrantId]`.
|
|
991
|
+
*
|
|
992
|
+
* @example
|
|
993
|
+
* ```ts
|
|
994
|
+
* \@RoleMenuRoute(GrantId)
|
|
995
|
+
* class Grant extends RoleMenuHandler<[typeof GrantId]> {
|
|
996
|
+
* async execute() {
|
|
997
|
+
* await this.update(`granting ${this.roles.size} role(s)`);
|
|
998
|
+
* }
|
|
999
|
+
* }
|
|
1000
|
+
* ```
|
|
1001
|
+
*/
|
|
1002
|
+
var RoleMenuHandler = class extends SelectMenuHandler {
|
|
1003
|
+
/** The roles this select picked. */
|
|
1004
|
+
get roles() {
|
|
1005
|
+
return pick(this.values, resolvedOf(this.event.data).roles);
|
|
779
1006
|
}
|
|
780
1007
|
};
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
1008
|
+
/**
|
|
1009
|
+
* Base class for a channel select menu handler on the HTTP transport.
|
|
1010
|
+
*
|
|
1011
|
+
* Beside `this.values` this handler declares `this.channels`. Register the customId definitions with
|
|
1012
|
+
* `@ChannelMenuRoute` and list the same ones in the generic.
|
|
1013
|
+
*
|
|
1014
|
+
* @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof LogTargetId]`.
|
|
1015
|
+
*
|
|
1016
|
+
* @example
|
|
1017
|
+
* ```ts
|
|
1018
|
+
* \@ChannelMenuRoute(LogTargetId)
|
|
1019
|
+
* class LogTarget extends ChannelMenuHandler<[typeof LogTargetId]> {
|
|
1020
|
+
* async execute() {
|
|
1021
|
+
* await this.update(`logging to ${this.channels.size} channel(s)`);
|
|
1022
|
+
* }
|
|
1023
|
+
* }
|
|
1024
|
+
* ```
|
|
1025
|
+
*/
|
|
1026
|
+
var ChannelMenuHandler = class extends SelectMenuHandler {
|
|
1027
|
+
/** The channels this select picked. */
|
|
1028
|
+
get channels() {
|
|
1029
|
+
return pick(this.values, resolvedOf(this.event.data).channels);
|
|
790
1030
|
}
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1031
|
+
};
|
|
1032
|
+
/**
|
|
1033
|
+
* Base class for a mentionable select menu handler on the HTTP transport.
|
|
1034
|
+
*
|
|
1035
|
+
* A mentionable menu accepts users and roles together. `this.users`, `this.members`, and `this.roles`
|
|
1036
|
+
* can each come back empty.
|
|
1037
|
+
*
|
|
1038
|
+
* @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof InviteId]`.
|
|
1039
|
+
*
|
|
1040
|
+
* @example
|
|
1041
|
+
* ```ts
|
|
1042
|
+
* \@MentionableMenuRoute(InviteId)
|
|
1043
|
+
* class Invite extends MentionableMenuHandler<[typeof InviteId]> {
|
|
1044
|
+
* async execute() {
|
|
1045
|
+
* await this.update(`inviting ${this.users.size} user(s) and ${this.roles.size} role(s)`);
|
|
1046
|
+
* }
|
|
1047
|
+
* }
|
|
1048
|
+
* ```
|
|
1049
|
+
*/
|
|
1050
|
+
var MentionableMenuHandler = class extends SelectMenuHandler {
|
|
1051
|
+
/** The users this select picked. */
|
|
1052
|
+
get users() {
|
|
1053
|
+
return pick(this.values, resolvedOf(this.event.data).users);
|
|
801
1054
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
1055
|
+
/** The guild members behind the picked users. Discord resolves these only inside a guild. */
|
|
1056
|
+
get members() {
|
|
1057
|
+
return pick(this.values, resolvedOf(this.event.data).members);
|
|
1058
|
+
}
|
|
1059
|
+
/** The roles this select picked. */
|
|
1060
|
+
get roles() {
|
|
1061
|
+
return pick(this.values, resolvedOf(this.event.data).roles);
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
|
|
1065
|
+
//#endregion
|
|
1066
|
+
//#region src/subscribers/index.ts
|
|
1067
|
+
/**
|
|
1068
|
+
* Base class for an HTTP subscriber, binding `this.core` to the HTTP {@link Core}.
|
|
1069
|
+
*
|
|
1070
|
+
* @typeParam KeyOfSubscribers - The subscription key this handler receives
|
|
1071
|
+
*/
|
|
1072
|
+
var Subscriber$1 = class extends Subscriber {};
|
|
1073
|
+
/**
|
|
1074
|
+
* Base class for an HTTP subscriber that delivers its event to a Discord webhook, binding
|
|
1075
|
+
* `this.core` to the HTTP {@link Core}.
|
|
1076
|
+
*
|
|
1077
|
+
* @typeParam KeyOfSubscribers - The subscription key this reporter receives
|
|
1078
|
+
*/
|
|
1079
|
+
var WebhookLog$1 = class extends WebhookLog {};
|
|
1080
|
+
|
|
1081
|
+
//#endregion
|
|
1082
|
+
//#region src/pagination/Paginator.ts
|
|
1083
|
+
function contextOf(interaction, core) {
|
|
819
1084
|
return {
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
1085
|
+
interaction,
|
|
1086
|
+
user: interaction.member?.user ?? interaction.user,
|
|
1087
|
+
guildId: interaction.guild_id ?? null,
|
|
1088
|
+
core
|
|
824
1089
|
};
|
|
825
1090
|
}
|
|
826
1091
|
/**
|
|
827
|
-
*
|
|
828
|
-
*
|
|
1092
|
+
* A restart-proof paginator. Each nav button's customId encodes its full target page, so clicks are
|
|
1093
|
+
* idempotent and survive a restart. A persistent `@ButtonRoute` on `Handler` dispatches the clicks.
|
|
1094
|
+
*
|
|
1095
|
+
* @typeParam Item - The item type, inferred from the source.
|
|
1096
|
+
* @typeParam Prefix - The route prefix, inferred from `config.prefix`.
|
|
1097
|
+
*
|
|
1098
|
+
* @example
|
|
1099
|
+
* ```ts
|
|
1100
|
+
* export const Bans = new Paginator({
|
|
1101
|
+
* prefix: 'bans',
|
|
1102
|
+
* source: new ArraySource((ctx) => loadBans(ctx.guildId), { perPage: 10 }),
|
|
1103
|
+
* renderItem: (ban) => ban.user.username
|
|
1104
|
+
* });
|
|
1105
|
+
*
|
|
1106
|
+
* \@ButtonRoute(Bans.cursor)
|
|
1107
|
+
* export class BansNav extends Bans.Handler {}
|
|
1108
|
+
* ```
|
|
1109
|
+
*/
|
|
1110
|
+
var Paginator = class extends PaginatorBase {
|
|
1111
|
+
/** The nav handler base. Extend it with an empty body and decorate it, `@ButtonRoute(p.cursor)`. */
|
|
1112
|
+
Handler;
|
|
1113
|
+
constructor(config) {
|
|
1114
|
+
super(config);
|
|
1115
|
+
const pageFor = (handler, n) => this.page(handler, n);
|
|
1116
|
+
this.Handler = class Nav extends ButtonHandler {
|
|
1117
|
+
async execute() {
|
|
1118
|
+
await this.deferUpdate();
|
|
1119
|
+
const response = await pageFor(this, this.params.page);
|
|
1120
|
+
await this.update(response);
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Render a page as a {@link ReplyResponse} without sending anything.
|
|
1126
|
+
*
|
|
1127
|
+
* @param handler - The handler rendering the page, normally `this`.
|
|
1128
|
+
* @param n - The zero-based page.
|
|
1129
|
+
*/
|
|
1130
|
+
async page(handler, n) {
|
|
1131
|
+
return this.buildPage(contextOf(handler.getEvent(), handler.core), n);
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Send a page through the handler's sender. The sender picks reply, edit, or followUp from its ack state.
|
|
1135
|
+
*
|
|
1136
|
+
* @param handler - The handler starting the paginator, normally `this`.
|
|
1137
|
+
* @param n - The zero-based page to open on.
|
|
1138
|
+
*/
|
|
1139
|
+
async start(handler, n = 0) {
|
|
1140
|
+
const response = await this.page(handler, n);
|
|
1141
|
+
return handler.sender.send(response, { ephemeral: this.config.ephemeral ?? false });
|
|
1142
|
+
}
|
|
1143
|
+
};
|
|
1144
|
+
|
|
1145
|
+
//#endregion
|
|
1146
|
+
//#region src/pagination/sources.ts
|
|
1147
|
+
/**
|
|
1148
|
+
* A source for a bounded list you can load whole. It loads the full list on every click, then slices the
|
|
1149
|
+
* page, so the real total is known (a real last button, "Page X of Y"). Cache inside your loader if the
|
|
1150
|
+
* load is expensive.
|
|
1151
|
+
*
|
|
1152
|
+
* @typeParam Item - The item type, inferred from the loader's return.
|
|
1153
|
+
*/
|
|
1154
|
+
var ArraySource = class extends ArraySourceBase {};
|
|
1155
|
+
/**
|
|
1156
|
+
* A source for a large or unknown-length set you fetch one page at a time (SQL LIMIT/OFFSET, a paged API).
|
|
1157
|
+
* The fetcher receives the page index and the page size and reports whether a next page exists. A cursor
|
|
1158
|
+
* source has no cheap total, so `totalPages` is undefined, the last button is omitted, and the indicator
|
|
1159
|
+
* reads "Page X".
|
|
1160
|
+
*
|
|
1161
|
+
* @typeParam Item - The item type, inferred from the fetcher's slice.
|
|
1162
|
+
*/
|
|
1163
|
+
var CursorSource = class extends CursorSourceBase {};
|
|
1164
|
+
|
|
1165
|
+
//#endregion
|
|
1166
|
+
//#region src/gates/Gated.ts
|
|
1167
|
+
/**
|
|
1168
|
+
* Attaches gates to an HTTP interaction handler. The gates run before `execute`, and a gate refusing stops
|
|
1169
|
+
* the handler with the reply or drop the gate threw. Multiple gates are ANDed. A gate
|
|
1170
|
+
* that requires a context the handler does not provide (a button gate on a slash handler, a gate on an
|
|
1171
|
+
* autocomplete handler, or a `RequirePermissions([...], { in: 'guild' })` gate the http transport cannot
|
|
1172
|
+
* satisfy) is a compile error at this line, and the error names the gate and the handler kind. Place it
|
|
1173
|
+
* above the route decorator.
|
|
1174
|
+
*
|
|
1175
|
+
* Combine gates with {@link and} or {@link or} to build a single gate from multiple arms.
|
|
1176
|
+
*
|
|
1177
|
+
* @example
|
|
1178
|
+
* ```ts
|
|
1179
|
+
* import { PermissionFlagsBits } from 'discord-api-types/v10';
|
|
1180
|
+
*
|
|
1181
|
+
* \@Gated(GuildOnly(), RequirePermissions([PermissionFlagsBits.ManageMessages]))
|
|
1182
|
+
* \@SlashRoute('purge')
|
|
1183
|
+
* class Purge extends SlashHandler<'purge'> {
|
|
1184
|
+
* async execute() {
|
|
1185
|
+
* // both gates passed, so this runs in a guild with a caller who can moderate this channel
|
|
1186
|
+
* }
|
|
1187
|
+
* }
|
|
1188
|
+
* ```
|
|
1189
|
+
*
|
|
1190
|
+
* @typeParam Gates - The non-empty tuple of gates to run, in order, against the handler's context.
|
|
1191
|
+
* @typeParam TCtor - The decorated handler's constructor, checked against every gate's required context.
|
|
1192
|
+
* @param gates - One or more gates to attach, each ANDed so the handler runs only when all pass.
|
|
1193
|
+
* @param ctor - The handler class being decorated, rejected at compile time when a gate requires a context it lacks.
|
|
1194
|
+
*
|
|
1195
|
+
* @see {@link defineGate}
|
|
1196
|
+
* @see {@link defineEffectGate}
|
|
1197
|
+
* @decorator
|
|
1198
|
+
*/
|
|
1199
|
+
function Gated(...gates) {
|
|
1200
|
+
return function(ctor) {
|
|
1201
|
+
const existing = Reflect.getMetadata(GatedMetadataKey, ctor) ?? [];
|
|
1202
|
+
Reflect.defineMetadata(GatedMetadataKey, [...existing, ...gates], ctor);
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
//#endregion
|
|
1207
|
+
//#region src/version.ts
|
|
1208
|
+
const version = "0.11.0-next.0";
|
|
1209
|
+
|
|
1210
|
+
//#endregion
|
|
1211
|
+
//#region src/publicKey.ts
|
|
1212
|
+
async function publicKeyOf(rest) {
|
|
1213
|
+
const fromEnv = Envapter.get("DISCORD_PUBLIC_KEY");
|
|
1214
|
+
if (fromEnv !== void 0) return fromEnv;
|
|
1215
|
+
try {
|
|
1216
|
+
return (await currentApplication(rest)).verify_key;
|
|
1217
|
+
} catch (error) {
|
|
1218
|
+
throw new SeedcordError(SeedcordErrorCode.CoreDiscordReadFailed, [
|
|
1219
|
+
"the public key",
|
|
1220
|
+
asError(error).message,
|
|
1221
|
+
`The bot reads it there because ${paint.bold("DISCORD_PUBLIC_KEY")} is unset. Check ${paint.bold("DISCORD_BOT_TOKEN")}, or set ${paint.bold("DISCORD_PUBLIC_KEY")} to the key from your application's General Information page.`
|
|
1222
|
+
], { cause: error });
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
//#endregion
|
|
1227
|
+
//#region src/HttpStartupTasks.ts
|
|
1228
|
+
var HttpStartupTasks = class {
|
|
1229
|
+
host;
|
|
1230
|
+
service;
|
|
1231
|
+
commands;
|
|
1232
|
+
emojis;
|
|
1233
|
+
constructor(host, service) {
|
|
1234
|
+
this.host = host;
|
|
1235
|
+
this.service = service;
|
|
1236
|
+
this.commands = host.config.bot.commands.path ? new CommandRegistry(host) : void 0;
|
|
1237
|
+
this.emojis = new EmojiInjector(host);
|
|
1238
|
+
}
|
|
1239
|
+
registerHmr(hmr) {
|
|
1240
|
+
this.host.startup.addTask(StartupPhase.Configuration, "hmr-registration", () => {
|
|
1241
|
+
if (this.service.interactions) hmr.register(this.service.interactions);
|
|
1242
|
+
if (this.commands) hmr.register(this.commands);
|
|
1243
|
+
hmr.register(this.service.subscribers);
|
|
1244
|
+
for (const { instance } of attachmentsOf(this.host)) hmr.register(instance);
|
|
1245
|
+
return Promise.resolve();
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
addDiscordTasks() {
|
|
1249
|
+
const { startup } = this.host;
|
|
1250
|
+
startup.addTask(StartupPhase.Login, "interactions-engine", async () => {
|
|
1251
|
+
this.service.prepareEngine(await publicKeyOf(this.service.rest));
|
|
1252
|
+
});
|
|
1253
|
+
const { commands } = this;
|
|
1254
|
+
if (!commands) {
|
|
1255
|
+
startup.addTask(StartupPhase.Login, "emoji-injection", () => this.emojis.init());
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
startup.addTask(StartupPhase.Login, "command-deploy", async (signal) => {
|
|
1259
|
+
await this.emojis.init();
|
|
1260
|
+
await commands.deploy(signal, { routes: this.service.interactions });
|
|
1261
|
+
}, COMMAND_DEPLOY_TIMEOUT_MS);
|
|
1262
|
+
}
|
|
1263
|
+
};
|
|
1264
|
+
|
|
1265
|
+
//#endregion
|
|
1266
|
+
//#region src/handlers/defaults/UnhandledAutocomplete.ts
|
|
1267
|
+
var UnhandledAutocomplete = class extends AutocompleteHandler {
|
|
1268
|
+
async execute() {
|
|
1269
|
+
await this.respond([]);
|
|
1270
|
+
}
|
|
1271
|
+
};
|
|
1272
|
+
|
|
1273
|
+
//#endregion
|
|
1274
|
+
//#region src/handlers/defaults/UnhandledRepliable.ts
|
|
1275
|
+
var UnhandledRepliable = class extends InteractionHandler {
|
|
1276
|
+
async execute() {
|
|
1277
|
+
await this.reply("Feature not implemented yet.");
|
|
1278
|
+
}
|
|
1279
|
+
};
|
|
1280
|
+
|
|
1281
|
+
//#endregion
|
|
1282
|
+
//#region src/dispatch/resolve.ts
|
|
1283
|
+
function commandKind(type) {
|
|
1284
|
+
switch (type) {
|
|
1285
|
+
case ApplicationCommandType.ChatInput: return InteractionKind.Slash;
|
|
1286
|
+
case ApplicationCommandType.User: return InteractionKind.UserContextMenu;
|
|
1287
|
+
case ApplicationCommandType.Message: return InteractionKind.MessageContextMenu;
|
|
1288
|
+
default: return null;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
function componentMapKey(type) {
|
|
1292
|
+
switch (type) {
|
|
1293
|
+
case ComponentType.Button: return InteractionKind.Button;
|
|
1294
|
+
case ComponentType.StringSelect: return InteractionKind.StringMenu;
|
|
1295
|
+
case ComponentType.UserSelect: return InteractionKind.UserMenu;
|
|
1296
|
+
case ComponentType.RoleSelect: return InteractionKind.RoleMenu;
|
|
1297
|
+
case ComponentType.ChannelSelect: return InteractionKind.ChannelMenu;
|
|
1298
|
+
case ComponentType.MentionableSelect: return InteractionKind.MentionableMenu;
|
|
1299
|
+
default: return null;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
function emptyRouteMaps() {
|
|
1303
|
+
return {
|
|
1304
|
+
[InteractionKind.Slash]: /* @__PURE__ */ new Map(),
|
|
1305
|
+
[InteractionKind.UserContextMenu]: /* @__PURE__ */ new Map(),
|
|
1306
|
+
[InteractionKind.MessageContextMenu]: /* @__PURE__ */ new Map(),
|
|
1307
|
+
[InteractionKind.Autocomplete]: /* @__PURE__ */ new Map(),
|
|
1308
|
+
[InteractionKind.Button]: /* @__PURE__ */ new Map(),
|
|
1309
|
+
[InteractionKind.StringMenu]: /* @__PURE__ */ new Map(),
|
|
1310
|
+
[InteractionKind.UserMenu]: /* @__PURE__ */ new Map(),
|
|
1311
|
+
[InteractionKind.RoleMenu]: /* @__PURE__ */ new Map(),
|
|
1312
|
+
[InteractionKind.ChannelMenu]: /* @__PURE__ */ new Map(),
|
|
1313
|
+
[InteractionKind.MentionableMenu]: /* @__PURE__ */ new Map(),
|
|
1314
|
+
[InteractionKind.Modal]: /* @__PURE__ */ new Map()
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
function unhandled(kind, attemptedKey) {
|
|
1318
|
+
return {
|
|
1319
|
+
kind,
|
|
1320
|
+
routeId: null,
|
|
1321
|
+
attemptedKey,
|
|
1322
|
+
ctor: kind === InteractionKind.Autocomplete ? UnhandledAutocomplete : UnhandledRepliable
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* Matches a verified non-PING interaction to a registered handler. A known kind with no handler resolves
|
|
1327
|
+
* to the unhandled default. Null is an unrecognized payload shape, which the engine acks with a 202
|
|
829
1328
|
* without dispatching. Components and modals route by the stable customId prefix. A wire whose layout
|
|
830
1329
|
* hash drifted still reaches its handler, where decode refuses with `StaleCustomId`.
|
|
831
1330
|
*/
|
|
@@ -868,16 +1367,16 @@ var RouteRegistry = class {
|
|
|
868
1367
|
/**
|
|
869
1368
|
* Writes every route the class declares. A class declaring none returns false.
|
|
870
1369
|
*
|
|
871
|
-
* @throws A **
|
|
1370
|
+
* @throws A **SeedcordTypeError** when another class already holds one of them.
|
|
872
1371
|
*/
|
|
873
1372
|
register(ctor, from) {
|
|
874
1373
|
const writes = [];
|
|
875
1374
|
for (const [kind, keys] of interactionRoutesOf(ctor)) for (const key of keys) {
|
|
876
1375
|
const existing = this.owners.get(`${kind}:${key}`);
|
|
877
|
-
if (existing && existing.ctor !== ctor) throw new
|
|
1376
|
+
if (existing && existing.ctor !== ctor) throw new SeedcordTypeError(SeedcordErrorCode.CoreDuplicateName, [
|
|
1377
|
+
"route",
|
|
878
1378
|
`${kind}:${key}`,
|
|
879
|
-
`${existing.ctor.name} (${existing.from})`,
|
|
880
|
-
`${ctor.name} (${from})`
|
|
1379
|
+
[`${paint.bold(existing.ctor.name)} (${paint.path(existing.from)})`, `${paint.bold(ctor.name)} (${paint.path(from)})`]
|
|
881
1380
|
]);
|
|
882
1381
|
writes.push({
|
|
883
1382
|
kind,
|
|
@@ -917,1008 +1416,624 @@ var RouteRegistry = class {
|
|
|
917
1416
|
};
|
|
918
1417
|
|
|
919
1418
|
//#endregion
|
|
920
|
-
//#region src/
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1419
|
+
//#region src/dispatch/InteractionDispatcher.ts
|
|
1420
|
+
var InteractionDispatcher = class {
|
|
1421
|
+
handlersDir;
|
|
1422
|
+
middlewaresDir;
|
|
1423
|
+
/** @internal */
|
|
1424
|
+
middlewares = new MiddlewareRegistry(interactionMiddleware);
|
|
1425
|
+
/** @internal */
|
|
1426
|
+
logger = new Logger("Interactions", { channel: "interactions" });
|
|
1427
|
+
routes = new RouteRegistry();
|
|
1428
|
+
get maps() {
|
|
1429
|
+
return this.routes.maps;
|
|
1430
|
+
}
|
|
1431
|
+
isInitialized = false;
|
|
1432
|
+
hmrHandler;
|
|
1433
|
+
loading = false;
|
|
1434
|
+
loadedHandlers = [];
|
|
1435
|
+
loadedMiddlewares = [];
|
|
1436
|
+
constructor(handlersDir, middlewaresDir) {
|
|
1437
|
+
this.handlersDir = handlersDir;
|
|
1438
|
+
this.middlewaresDir = middlewaresDir;
|
|
1439
|
+
if (!Envapter.isDevelopment && !Envapter.isTest) return;
|
|
1440
|
+
this.hmrHandler = new HmrModuleHandler({
|
|
1441
|
+
handlersDir,
|
|
1442
|
+
...middlewaresDir && { middlewaresDir },
|
|
1443
|
+
isHandler: this.isHandler.bind(this),
|
|
1444
|
+
isMiddleware: this.isMiddleware.bind(this),
|
|
1445
|
+
registerHandler: this.registerHandler.bind(this),
|
|
1446
|
+
registerMiddleware: this.registerMiddleware.bind(this),
|
|
1447
|
+
unregisterHandler: this.routes.unregister.bind(this.routes),
|
|
1448
|
+
unregisterMiddleware: this.unregisterMiddleware.bind(this),
|
|
1449
|
+
getArtifacts: this.routes.routesOf.bind(this.routes),
|
|
1450
|
+
logger: this.logger
|
|
1451
|
+
});
|
|
938
1452
|
}
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
if (
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1453
|
+
/** @internal */
|
|
1454
|
+
async init() {
|
|
1455
|
+
if (this.isInitialized) return;
|
|
1456
|
+
this.isInitialized = true;
|
|
1457
|
+
this.loading = true;
|
|
1458
|
+
this.loadedHandlers.length = 0;
|
|
1459
|
+
this.loadedMiddlewares.length = 0;
|
|
1460
|
+
try {
|
|
1461
|
+
if (this.middlewaresDir) await this.loadMiddlewares(this.middlewaresDir);
|
|
1462
|
+
await this.loadHandlers(this.handlersDir);
|
|
1463
|
+
} finally {
|
|
1464
|
+
this.loading = false;
|
|
1465
|
+
}
|
|
1466
|
+
this.reportLoad();
|
|
947
1467
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
const MS_PER_SECOND = 1e3;
|
|
955
|
-
const TIMESTAMP_PATTERN = /^\d+$/;
|
|
956
|
-
var ReplayGuard = class {
|
|
957
|
-
seen = /* @__PURE__ */ new Map();
|
|
958
|
-
lastSweep = Date.now();
|
|
959
|
-
fresh(timestampHeader) {
|
|
960
|
-
if (!TIMESTAMP_PATTERN.test(timestampHeader)) return null;
|
|
961
|
-
const timestampMs = Number(timestampHeader) * MS_PER_SECOND;
|
|
962
|
-
return Math.abs(Date.now() - timestampMs) <= REPLAY_WINDOW_MS ? timestampMs : null;
|
|
1468
|
+
async loadHandlers(dir) {
|
|
1469
|
+
for await (const { fullPath, relativePath, imported } of importFiles(dir)) for (const value of Object.values(imported)) {
|
|
1470
|
+
if (!this.isHandler(value)) continue;
|
|
1471
|
+
this.registerHandler(value, relativePath);
|
|
1472
|
+
this.hmrHandler?.trackHandler(fullPath, value);
|
|
1473
|
+
}
|
|
963
1474
|
}
|
|
964
|
-
|
|
965
|
-
const
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
return true;
|
|
1475
|
+
async loadMiddlewares(dir) {
|
|
1476
|
+
for await (const { fullPath, relativePath, imported } of importFiles(dir)) for (const value of Object.values(imported)) {
|
|
1477
|
+
if (!this.isMiddleware(value)) continue;
|
|
1478
|
+
this.registerMiddleware(value, relativePath);
|
|
1479
|
+
this.hmrHandler?.trackMiddleware(fullPath, value);
|
|
1480
|
+
}
|
|
971
1481
|
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
this.
|
|
975
|
-
|
|
1482
|
+
reportLoad() {
|
|
1483
|
+
const { utils } = this.logger;
|
|
1484
|
+
if (this.loadedMiddlewares.length > 0) utils.block("Loaded middlewares", utils.entries(this.loadedMiddlewares), "debug");
|
|
1485
|
+
utils.block("Loaded handlers", [...utils.entries(this.loadedHandlers), ...utils.counts({ routes: this.routes.size })], "debug");
|
|
976
1486
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
const
|
|
986
|
-
const
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
const publicKey = Envapter.getRequired("DISCORD_PUBLIC_KEY", Converters.String);
|
|
995
|
-
const verifier = new Ed25519Verifier(publicKey);
|
|
996
|
-
const replays = new ReplayGuard();
|
|
997
|
-
const inFlight = /* @__PURE__ */ new Set();
|
|
998
|
-
const logger = new Logger("Engine", { channel: "bot" });
|
|
999
|
-
async function dispatchMatched(match, payload, ctx) {
|
|
1000
|
-
const start = await dispatchInteraction({
|
|
1001
|
-
match,
|
|
1002
|
-
payload,
|
|
1003
|
-
core,
|
|
1004
|
-
middlewares
|
|
1005
|
-
});
|
|
1006
|
-
if (!start) return;
|
|
1007
|
-
const work = start().catch(rootFault);
|
|
1008
|
-
if (ctx) ctx.waitUntil(work);
|
|
1009
|
-
else {
|
|
1010
|
-
inFlight.add(work);
|
|
1011
|
-
work.finally(() => inFlight.delete(work));
|
|
1487
|
+
/** @internal */
|
|
1488
|
+
async onHmr(event) {
|
|
1489
|
+
await this.hmrHandler?.handle(event);
|
|
1490
|
+
}
|
|
1491
|
+
warnUnhandledRoutes(commandLeaves) {
|
|
1492
|
+
this.warnMissing(commandLeaves, this.maps[InteractionKind.Slash], "Slash route", "@SlashRoute");
|
|
1493
|
+
}
|
|
1494
|
+
warnUnhandledContextMenuRoutes(leaves) {
|
|
1495
|
+
const user = this.maps[InteractionKind.UserContextMenu];
|
|
1496
|
+
const message = this.maps[InteractionKind.MessageContextMenu];
|
|
1497
|
+
this.warnMissing(leaves.user, user, "User context menu", "@ContextMenuRoute");
|
|
1498
|
+
this.warnMissing(leaves.message, message, "Message context menu", "@ContextMenuRoute");
|
|
1499
|
+
}
|
|
1500
|
+
warnMissing(names, map, label, decorator) {
|
|
1501
|
+
for (const name of names) {
|
|
1502
|
+
if (map.has(name)) continue;
|
|
1503
|
+
this.logger.warn(`${label} ${paint.sky.bold(name)} has no registered ${paint.bold(decorator)} handler.`);
|
|
1012
1504
|
}
|
|
1013
1505
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1506
|
+
isHandler(value) {
|
|
1507
|
+
if (typeof value !== "function" || !Reflect.hasMetadata(InteractionMetadataKey, value)) return false;
|
|
1508
|
+
return value.prototype instanceof InteractionHandler || value.prototype instanceof AutocompleteHandler;
|
|
1509
|
+
}
|
|
1510
|
+
isMiddleware(value) {
|
|
1511
|
+
if (typeof value !== "function") return false;
|
|
1512
|
+
return value.prototype instanceof InteractionMiddleware && Reflect.hasMetadata(InteractionMiddlewareMetadataKey, value);
|
|
1513
|
+
}
|
|
1514
|
+
registerMiddleware(ctor, relativePath) {
|
|
1515
|
+
const metadata = this.middlewares.register(ctor);
|
|
1516
|
+
if (!metadata || !this.loading) return;
|
|
1517
|
+
const scope = metadata.keys ? `${metadata.priority}, ${metadata.keys.join(", ")}` : metadata.priority;
|
|
1518
|
+
this.loadedMiddlewares.push({
|
|
1519
|
+
name: `${ctor.name} (${String(scope)})`,
|
|
1520
|
+
from: formatFilePath(relativePath)
|
|
1521
|
+
});
|
|
1018
1522
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
if (
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
})
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
try {
|
|
1038
|
-
payload = JSON.parse(decoder.decode(body));
|
|
1039
|
-
} catch {
|
|
1040
|
-
return [new Response(null, { status: BAD_REQUEST }), "bad-body"];
|
|
1041
|
-
}
|
|
1042
|
-
if (!hasInteractionType(payload)) return [new Response(null, { status: BAD_REQUEST }), "bad-body"];
|
|
1043
|
-
if (payload.type === PING) return [Response.json({ type: InteractionResponseType.Pong }), "ping"];
|
|
1044
|
-
if (!replays.unseen(signature, timestampMs)) return [new Response(null, { status: UNAUTHORIZED }), "replay"];
|
|
1045
|
-
try {
|
|
1046
|
-
core.bus[PublishDefault]("anyInteraction", { interaction: payload });
|
|
1047
|
-
const match = resolve(maps, payload);
|
|
1048
|
-
if (match) await dispatchMatched(match, payload, ctx);
|
|
1049
|
-
} catch (caught) {
|
|
1050
|
-
rootFault(caught);
|
|
1051
|
-
}
|
|
1052
|
-
return [new Response(null, { status: ACCEPTED }), "dispatched"];
|
|
1053
|
-
};
|
|
1523
|
+
unregisterMiddleware(ctor) {
|
|
1524
|
+
this.middlewares.unregister(ctor);
|
|
1525
|
+
}
|
|
1526
|
+
registerHandler(ctor, relativePath) {
|
|
1527
|
+
const from = formatFilePath(relativePath);
|
|
1528
|
+
if (!this.routes.register(ctor, from)) return;
|
|
1529
|
+
if (this.loading) this.loadedHandlers.push({
|
|
1530
|
+
name: ctor.name,
|
|
1531
|
+
from
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
};
|
|
1535
|
+
|
|
1536
|
+
//#endregion
|
|
1537
|
+
//#region src/gates/context.ts
|
|
1538
|
+
function interactionGateContext(payload, core, dispatch) {
|
|
1539
|
+
const member = payload.member ?? null;
|
|
1540
|
+
const resolved = member?.user ?? payload.user;
|
|
1054
1541
|
return {
|
|
1055
|
-
|
|
1056
|
-
|
|
1542
|
+
kind: "interaction",
|
|
1543
|
+
core,
|
|
1544
|
+
dispatch,
|
|
1545
|
+
interaction: payload,
|
|
1546
|
+
user: resolved,
|
|
1547
|
+
member,
|
|
1548
|
+
userId: resolved?.id ?? null,
|
|
1549
|
+
guildId: payload.guild_id ?? null,
|
|
1550
|
+
channelId: payload.channel?.id ?? null,
|
|
1551
|
+
memberRoleIds: member?.roles ?? [],
|
|
1552
|
+
memberPermissions: member ? BigInt(member.permissions) : null,
|
|
1553
|
+
appPermissions: BigInt(payload.app_permissions),
|
|
1554
|
+
declaredRoute: null
|
|
1057
1555
|
};
|
|
1058
1556
|
}
|
|
1059
1557
|
|
|
1060
1558
|
//#endregion
|
|
1061
|
-
//#region src/
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
Logger.configure(config.logger ?? {});
|
|
1081
|
-
const token = validateDiscordToken(Envapter.get("DISCORD_BOT_TOKEN"));
|
|
1082
|
-
const core = createCore(config, token);
|
|
1083
|
-
core.bus[RegisterDefaults]();
|
|
1084
|
-
registerSubscribers(core.bus, manifest.subscribers);
|
|
1085
|
-
const routes = new RouteRegistry();
|
|
1086
|
-
for (const handler of manifest.handlers) {
|
|
1087
|
-
if (!isHandlerClass(handler)) throw wrongClass("handlers", handler, "InteractionHandler or AutocompleteHandler");
|
|
1088
|
-
if (!routes.register(handler, MANIFEST_ORIGIN)) throw noRoutes("handlers", handler.name, "route decorator");
|
|
1089
|
-
}
|
|
1090
|
-
const middlewares = new MiddlewareRegistry(interactionMiddleware);
|
|
1091
|
-
for (const middleware of manifest.middleware) {
|
|
1092
|
-
if (!isMiddlewareClass(middleware)) throw wrongClass("middleware", middleware, "InteractionMiddleware");
|
|
1093
|
-
if (!middlewares.register(middleware)) throw noRoutes("middleware", middleware.name, "@RegisterInteractionMiddleware");
|
|
1094
|
-
}
|
|
1095
|
-
return buildEngine(core, routes.maps, middlewares).handle;
|
|
1559
|
+
//#region src/dispatch/reportFault.ts
|
|
1560
|
+
function reportFault(error, uuid, dispatch, payload, core) {
|
|
1561
|
+
const source = interactionSource(payload);
|
|
1562
|
+
const from = {
|
|
1563
|
+
uuid,
|
|
1564
|
+
dispatchId: dispatch.id,
|
|
1565
|
+
origin: dispatch.routeId
|
|
1566
|
+
};
|
|
1567
|
+
if (error instanceof Notice && source) core.bus[PublishDefault]("handledException", {
|
|
1568
|
+
denial: error,
|
|
1569
|
+
...from,
|
|
1570
|
+
source
|
|
1571
|
+
});
|
|
1572
|
+
else core.bus[PublishDefault]("unknownException", {
|
|
1573
|
+
...from,
|
|
1574
|
+
error,
|
|
1575
|
+
...actors(payload),
|
|
1576
|
+
metadata: payload
|
|
1577
|
+
});
|
|
1096
1578
|
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1579
|
+
function actors(payload) {
|
|
1580
|
+
const user = payload.member?.user ?? payload.user;
|
|
1581
|
+
return {
|
|
1582
|
+
guild: void 0,
|
|
1583
|
+
user: user ? {
|
|
1584
|
+
id: user.id,
|
|
1585
|
+
username: user.username
|
|
1586
|
+
} : void 0
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
function interactionSource(payload) {
|
|
1590
|
+
const user = payload.member?.user ?? payload.user;
|
|
1591
|
+
if (!user) return null;
|
|
1592
|
+
const common = {
|
|
1593
|
+
kind: "interaction",
|
|
1594
|
+
userId: user.id,
|
|
1595
|
+
guildId: payload.guild_id ?? null,
|
|
1596
|
+
channelId: payload.channel?.id ?? null,
|
|
1597
|
+
interactionId: payload.id,
|
|
1598
|
+
raw: payload
|
|
1599
|
+
};
|
|
1600
|
+
if (payload.type === InteractionType.ApplicationCommand) {
|
|
1601
|
+
const { data } = payload;
|
|
1602
|
+
const isSlash = data.type === ApplicationCommandType.ChatInput;
|
|
1603
|
+
return {
|
|
1604
|
+
...common,
|
|
1605
|
+
interactionKind: isSlash ? "slash" : "context-menu",
|
|
1606
|
+
command: isSlash ? slashRouteOf(data) : data.name,
|
|
1607
|
+
customId: null
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
if (payload.type === InteractionType.ModalSubmit) return {
|
|
1611
|
+
...common,
|
|
1612
|
+
interactionKind: "modal",
|
|
1613
|
+
command: null,
|
|
1614
|
+
customId: payload.data.custom_id
|
|
1615
|
+
};
|
|
1616
|
+
if (payload.type === InteractionType.MessageComponent) {
|
|
1617
|
+
const isButton = payload.data.component_type === ComponentType.Button;
|
|
1618
|
+
return {
|
|
1619
|
+
...common,
|
|
1620
|
+
interactionKind: isButton ? "button" : "select",
|
|
1621
|
+
command: null,
|
|
1622
|
+
customId: payload.data.custom_id
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
return null;
|
|
1105
1626
|
}
|
|
1106
1627
|
|
|
1107
1628
|
//#endregion
|
|
1108
|
-
//#region src/
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
getter: "getSelectedMentionables"
|
|
1129
|
-
},
|
|
1130
|
-
[ComponentType.ChannelSelect]: {
|
|
1131
|
-
label: "a channel select",
|
|
1132
|
-
getter: "getSelectedChannels"
|
|
1133
|
-
},
|
|
1134
|
-
[ComponentType.FileUpload]: {
|
|
1135
|
-
label: "a file upload",
|
|
1136
|
-
getter: "getUploadedFiles"
|
|
1137
|
-
},
|
|
1138
|
-
[ComponentType.RadioGroup]: {
|
|
1139
|
-
label: "a radio group",
|
|
1140
|
-
getter: "getRadioGroup"
|
|
1141
|
-
},
|
|
1142
|
-
[ComponentType.CheckboxGroup]: {
|
|
1143
|
-
label: "a checkbox group",
|
|
1144
|
-
getter: "getCheckboxGroup"
|
|
1145
|
-
},
|
|
1146
|
-
[ComponentType.Checkbox]: {
|
|
1147
|
-
label: "a checkbox",
|
|
1148
|
-
getter: "getCheckbox"
|
|
1629
|
+
//#region src/dispatch/dispatchInteraction.ts
|
|
1630
|
+
let dispatchLogger;
|
|
1631
|
+
function logger() {
|
|
1632
|
+
dispatchLogger ??= new Logger("Dispatcher", { channel: "interactions" });
|
|
1633
|
+
return dispatchLogger;
|
|
1634
|
+
}
|
|
1635
|
+
const HARMLESS_API_CODES = /* @__PURE__ */ new Set([
|
|
1636
|
+
RESTJSONErrorCodes.UnknownInteraction,
|
|
1637
|
+
RESTJSONErrorCodes.InteractionHasAlreadyBeenAcknowledged,
|
|
1638
|
+
RESTJSONErrorCodes.UnknownMessage
|
|
1639
|
+
]);
|
|
1640
|
+
async function sendGuarded(routeId, send) {
|
|
1641
|
+
try {
|
|
1642
|
+
await send();
|
|
1643
|
+
} catch (error) {
|
|
1644
|
+
if (error instanceof DiscordAPIError && HARMLESS_API_CODES.has(error.code)) {
|
|
1645
|
+
logger().debug(`boundary send hit harmless code ${paint.amber(String(error.code))}`);
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
logger().error(`boundary send failed for route ${paint.sky.bold(routeId)}`, error);
|
|
1149
1649
|
}
|
|
1150
|
-
};
|
|
1151
|
-
function nameOfChannelType(type) {
|
|
1152
|
-
return ChannelType[type] ?? String(type);
|
|
1153
1650
|
}
|
|
1154
|
-
function
|
|
1155
|
-
|
|
1651
|
+
async function respondEmptyChoices(scope) {
|
|
1652
|
+
const telemetry = {
|
|
1653
|
+
bus: scope.core.bus,
|
|
1654
|
+
dispatch: scope.dispatch,
|
|
1655
|
+
interactionId: scope.payload.id
|
|
1656
|
+
};
|
|
1657
|
+
await reportedWrite(telemetry, "respond", () => scope.core.rest.post(Routes.interactionCallback(scope.payload.id, scope.payload.token), { body: {
|
|
1658
|
+
type: InteractionResponseType.ApplicationCommandAutocompleteResult,
|
|
1659
|
+
data: { choices: [] }
|
|
1660
|
+
} }));
|
|
1156
1661
|
}
|
|
1157
|
-
function
|
|
1158
|
-
|
|
1662
|
+
function renderContext(scope, uuid) {
|
|
1663
|
+
const { dispatch } = scope;
|
|
1664
|
+
const developerUsername = scope.core.config.notifications?.developerUsername;
|
|
1665
|
+
return developerUsername === void 0 ? {
|
|
1666
|
+
uuid,
|
|
1667
|
+
dispatch
|
|
1668
|
+
} : {
|
|
1669
|
+
uuid,
|
|
1670
|
+
developerUsername,
|
|
1671
|
+
dispatch
|
|
1672
|
+
};
|
|
1159
1673
|
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
* kind that getter does not read. The message names the getter that does.
|
|
1165
|
-
*
|
|
1166
|
-
* A select getter resolves the ids its field picked against the interaction's `resolved` data and returns a
|
|
1167
|
-
* `Collection` keyed by id. A field that picked nothing returns null. Pass `required` to throw there instead.
|
|
1168
|
-
*/
|
|
1169
|
-
var ModalFields = class {
|
|
1170
|
-
entries = /* @__PURE__ */ new Map();
|
|
1171
|
-
resolved;
|
|
1172
|
-
constructor(data) {
|
|
1173
|
-
this.resolved = data.resolved;
|
|
1174
|
-
for (const component of data.components ?? []) {
|
|
1175
|
-
if ("components" in component) for (const child of component.components) this.entries.set(child.custom_id, child);
|
|
1176
|
-
if ("component" in component) this.entries.set(component.component.custom_id, component.component);
|
|
1177
|
-
}
|
|
1674
|
+
async function handleNotice(notice, uuid, scope) {
|
|
1675
|
+
if (notice.report) {
|
|
1676
|
+
logger().error(`${notice.name}: ${paint.mute(uuid)}`, notice);
|
|
1677
|
+
reportFault(notice, uuid, scope.dispatch, scope.payload, scope.core);
|
|
1178
1678
|
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
return
|
|
1679
|
+
const { sender } = scope;
|
|
1680
|
+
if (!sender) {
|
|
1681
|
+
await sendGuarded(scope.routeId, () => respondEmptyChoices(scope));
|
|
1682
|
+
return;
|
|
1183
1683
|
}
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
}
|
|
1195
|
-
return entry;
|
|
1684
|
+
const response = notice.render(renderContext(scope, uuid));
|
|
1685
|
+
await sendGuarded(scope.routeId, () => sender.send(response, { ephemeral: notice.ephemeral }));
|
|
1686
|
+
}
|
|
1687
|
+
async function handleRawFault(error, uuid, scope) {
|
|
1688
|
+
const { core, sender } = scope;
|
|
1689
|
+
logUnknownFault(logger(), paint.mute(uuid), error, core.config.errors);
|
|
1690
|
+
reportFault(error, uuid, scope.dispatch, scope.payload, core);
|
|
1691
|
+
if (!sender) {
|
|
1692
|
+
await sendGuarded(scope.routeId, () => respondEmptyChoices(scope));
|
|
1693
|
+
return;
|
|
1196
1694
|
}
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1695
|
+
const Override = core.config.errors?.defaultError;
|
|
1696
|
+
const response = (Override ? new Override(uuid) : new Fault()).render(renderContext(scope, uuid));
|
|
1697
|
+
await sendGuarded(scope.routeId, () => sender.send(response, { ephemeral: true }));
|
|
1698
|
+
}
|
|
1699
|
+
async function handleFault(caught, scope) {
|
|
1700
|
+
if (caught instanceof Silence) {
|
|
1701
|
+
if (caught.reason !== void 0 && (scope.core.config.errors?.logSilences ?? true)) logger().debug(`Silence: ${caught.reason}`);
|
|
1702
|
+
return;
|
|
1203
1703
|
}
|
|
1204
|
-
|
|
1205
|
-
|
|
1704
|
+
const uuid = crypto.randomUUID();
|
|
1705
|
+
if (caught instanceof Notice) {
|
|
1706
|
+
await handleNotice(caught, uuid, scope);
|
|
1707
|
+
return;
|
|
1206
1708
|
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1709
|
+
const error = asError(caught);
|
|
1710
|
+
const ignore = new Set(scope.core.config.errors?.ignoreApiCodes ?? []);
|
|
1711
|
+
if (error instanceof DiscordAPIError && ignore.has(error.code)) {
|
|
1712
|
+
logger().debug(`swallowed api code ${paint.amber(String(error.code))}`);
|
|
1713
|
+
return;
|
|
1209
1714
|
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1715
|
+
await handleRawFault(error, uuid, scope);
|
|
1716
|
+
}
|
|
1717
|
+
function unhandledRouteId(match) {
|
|
1718
|
+
if (match.routeId) return match.routeId;
|
|
1719
|
+
const key = match.attemptedKey ?? "";
|
|
1720
|
+
return `${match.kind}:${key.length > 0 ? key : "unrouted"}`;
|
|
1721
|
+
}
|
|
1722
|
+
function freshScope(match, payload, core, dispatch) {
|
|
1723
|
+
const ref = {
|
|
1724
|
+
application_id: payload.application_id,
|
|
1725
|
+
id: payload.id,
|
|
1726
|
+
token: payload.token
|
|
1727
|
+
};
|
|
1728
|
+
return {
|
|
1729
|
+
core,
|
|
1730
|
+
payload,
|
|
1731
|
+
routeId: unhandledRouteId(match),
|
|
1732
|
+
dispatch,
|
|
1733
|
+
sender: match.kind === InteractionKind.Autocomplete ? null : new ReplySender(ref, core.rest, dispatch, core.bus)
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
function dispatchReporter(match, payload, core, dispatchId) {
|
|
1737
|
+
const startedAt = performance.now();
|
|
1738
|
+
const queuedMs = queuedMsFor(payload.id);
|
|
1739
|
+
return (outcome) => {
|
|
1740
|
+
reportDispatch(core.bus, {
|
|
1741
|
+
dispatchId,
|
|
1742
|
+
routeId: unhandledRouteId(match),
|
|
1743
|
+
interactionId: payload.id,
|
|
1744
|
+
kind: match.kind,
|
|
1745
|
+
outcome,
|
|
1746
|
+
fallback: match.routeId === null,
|
|
1747
|
+
userId: (payload.member?.user ?? payload.user)?.id ?? null,
|
|
1748
|
+
guildId: payload.guild_id ?? null,
|
|
1749
|
+
startedAt,
|
|
1750
|
+
queuedMs
|
|
1751
|
+
});
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
async function answer(caught, scope, report, outcome = outcomeFor(caught)) {
|
|
1755
|
+
try {
|
|
1756
|
+
await handleFault(caught, scope);
|
|
1757
|
+
} finally {
|
|
1758
|
+
report(outcome);
|
|
1212
1759
|
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1760
|
+
}
|
|
1761
|
+
async function runMiddlewares(step, kind, sender) {
|
|
1762
|
+
const { args, dispatch, ran } = step;
|
|
1763
|
+
for (const Middleware of args.middlewares.chainFor(kind)) {
|
|
1764
|
+
const event = args.payload;
|
|
1765
|
+
const instance = new Middleware(event, args.core, dispatch, sender);
|
|
1766
|
+
ran.push(instance);
|
|
1767
|
+
await instance.execute();
|
|
1216
1768
|
}
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1769
|
+
}
|
|
1770
|
+
async function refusalBeforeHandler(step) {
|
|
1771
|
+
const { args, scope } = step;
|
|
1772
|
+
const { kind } = args.match;
|
|
1773
|
+
if (kind !== InteractionKind.Autocomplete && scope.sender) try {
|
|
1774
|
+
await runMiddlewares(step, kind, scope.sender);
|
|
1775
|
+
} catch (caught) {
|
|
1776
|
+
return { caught };
|
|
1221
1777
|
}
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1778
|
+
return gateRefusal(step);
|
|
1779
|
+
}
|
|
1780
|
+
async function dispatchInteraction(args) {
|
|
1781
|
+
const { match, payload, core } = args;
|
|
1782
|
+
const routeId = unhandledRouteId(match);
|
|
1783
|
+
const dispatch = new DispatchContext(routeId);
|
|
1784
|
+
const report = dispatchReporter(match, payload, core, dispatch.id);
|
|
1785
|
+
const Handler = match.ctor;
|
|
1786
|
+
logger().debug(`Processing ${paint.sky.bold(routeId)} with ${paint.mute(Handler.name)}`);
|
|
1787
|
+
let handler;
|
|
1788
|
+
try {
|
|
1789
|
+
handler = new Handler(payload, core, dispatch);
|
|
1790
|
+
} catch (caught) {
|
|
1791
|
+
await answer(caught, freshScope(match, payload, core, dispatch), report);
|
|
1792
|
+
return null;
|
|
1225
1793
|
}
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1794
|
+
const scope = {
|
|
1795
|
+
core,
|
|
1796
|
+
payload,
|
|
1797
|
+
routeId,
|
|
1798
|
+
dispatch,
|
|
1799
|
+
sender: handler instanceof RepliableHandler$1 ? handler.sender : null
|
|
1800
|
+
};
|
|
1801
|
+
const ran = [];
|
|
1802
|
+
const refusal = await refusalBeforeHandler({
|
|
1803
|
+
args,
|
|
1804
|
+
Handler,
|
|
1805
|
+
dispatch,
|
|
1806
|
+
scope,
|
|
1807
|
+
ran
|
|
1808
|
+
});
|
|
1809
|
+
if (refusal) {
|
|
1810
|
+
try {
|
|
1811
|
+
await answer(refusal.caught, scope, report);
|
|
1812
|
+
} finally {
|
|
1813
|
+
await runAfter(ran, resultFor(refusal.caught), logger());
|
|
1238
1814
|
}
|
|
1239
|
-
return picked;
|
|
1240
|
-
}
|
|
1241
|
-
getSelectedMentionables(customId, required = false) {
|
|
1242
|
-
const { values } = this.field(customId, [ComponentType.MentionableSelect]);
|
|
1243
|
-
const resolved = this.resolved;
|
|
1244
|
-
const picked = {
|
|
1245
|
-
users: pick(values, resolved?.users),
|
|
1246
|
-
members: pick(values, resolved?.members),
|
|
1247
|
-
roles: pick(values, resolved?.roles)
|
|
1248
|
-
};
|
|
1249
|
-
if (picked.users.size + picked.members.size + picked.roles.size > 0) return picked;
|
|
1250
|
-
if (required) throw new SeedcordTypeError(SeedcordErrorCode.ModalFieldEmpty, [customId]);
|
|
1251
1815
|
return null;
|
|
1252
1816
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1817
|
+
return async () => {
|
|
1818
|
+
let result = { outcome: "handled" };
|
|
1819
|
+
try {
|
|
1820
|
+
await handler.execute();
|
|
1821
|
+
report("handled");
|
|
1822
|
+
} catch (caught) {
|
|
1823
|
+
result = resultFor(caught);
|
|
1824
|
+
await answer(caught, scope, report);
|
|
1825
|
+
} finally {
|
|
1826
|
+
await runAfter(ran, result, logger());
|
|
1827
|
+
}
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
async function gateRefusal(step) {
|
|
1831
|
+
const { Handler, dispatch } = step;
|
|
1832
|
+
const { match, payload, core } = step.args;
|
|
1833
|
+
if (match.kind === InteractionKind.Autocomplete || payload.type === InteractionType.ApplicationCommandAutocomplete) return null;
|
|
1834
|
+
const monitor = slowGateMonitor();
|
|
1835
|
+
try {
|
|
1836
|
+
await runHandlerGates(Handler, interactionGateContext(payload, core, dispatch), match.routeId ?? void 0, monitor?.observe);
|
|
1261
1837
|
return null;
|
|
1838
|
+
} catch (caught) {
|
|
1839
|
+
return { caught };
|
|
1840
|
+
} finally {
|
|
1841
|
+
monitor?.report(match.routeId);
|
|
1262
1842
|
}
|
|
1263
|
-
|
|
1264
|
-
return listOf(this.field(customId, [ComponentType.CheckboxGroup]).values);
|
|
1265
|
-
}
|
|
1266
|
-
getCheckbox(customId) {
|
|
1267
|
-
return this.field(customId, [ComponentType.Checkbox]).value;
|
|
1268
|
-
}
|
|
1269
|
-
};
|
|
1843
|
+
}
|
|
1270
1844
|
|
|
1271
1845
|
//#endregion
|
|
1272
|
-
//#region src/
|
|
1273
|
-
const
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
function
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1846
|
+
//#region src/receiver/Ed25519Verifier.ts
|
|
1847
|
+
const PUBLIC_KEY_BYTES = 32;
|
|
1848
|
+
const SIGNATURE_BYTES = 64;
|
|
1849
|
+
const HEX_RADIX = 16;
|
|
1850
|
+
const HEX_PATTERN = /^[0-9a-f]+$/i;
|
|
1851
|
+
function hexToBytes(hex, byteLength) {
|
|
1852
|
+
if (hex.length !== byteLength * 2 || !HEX_PATTERN.test(hex)) return null;
|
|
1853
|
+
const bytes = new Uint8Array(byteLength);
|
|
1854
|
+
for (let index = 0; index < byteLength; index++) bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), HEX_RADIX);
|
|
1855
|
+
return bytes;
|
|
1281
1856
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
const
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
logger = new Logger("Emojis", { channel: "bot" });
|
|
1290
|
-
constructor(core) {
|
|
1291
|
-
this.core = core;
|
|
1292
|
-
}
|
|
1293
|
-
async init() {
|
|
1294
|
-
clearStore(emojiStorage);
|
|
1295
|
-
const configured = this.core.config.bot.emojis;
|
|
1296
|
-
if (!configured || Object.keys(configured).length === 0) {
|
|
1297
|
-
this.logger.debug("No emojis configured, skipping emoji injection.");
|
|
1298
|
-
return;
|
|
1299
|
-
}
|
|
1300
|
-
const failures = [];
|
|
1301
|
-
const guilds = /* @__PURE__ */ new Map();
|
|
1302
|
-
let application;
|
|
1303
|
-
for (const [key, value] of Object.entries(configured)) if (isEmojiTuple(value)) await this.resolveTuple(key, value, guilds, failures);
|
|
1304
|
-
else if (typeof value === "string") {
|
|
1305
|
-
application ??= await this.applicationEmojis(failures);
|
|
1306
|
-
this.resolve(key, value, application, failures, "the application");
|
|
1307
|
-
} else failures.push(` - "${key}" has an invalid value (expected a name or [name, guildId])`);
|
|
1308
|
-
if (failures.length > 0) throw new SeedcordError(SeedcordErrorCode.ConfigEmojiUnresolved, [failures.length, failures.join("\n")]);
|
|
1309
|
-
this.logger.utils.summary("Loaded emojis", { emojis: Object.keys(emojiStorage).length });
|
|
1310
|
-
}
|
|
1311
|
-
async applicationEmojis(failures) {
|
|
1312
|
-
try {
|
|
1313
|
-
const appId = this.core.applicationId;
|
|
1314
|
-
return byName((await this.core.rest.get(Routes.applicationEmojis(appId))).items);
|
|
1315
|
-
} catch (error) {
|
|
1316
|
-
failures.push(` - the application emojis could not be read (${reasonOf(error)})`);
|
|
1317
|
-
return /* @__PURE__ */ new Map();
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
async resolveTuple(key, [name, guildId], guilds, failures) {
|
|
1321
|
-
let guild = guilds.get(guildId);
|
|
1322
|
-
if (!guild) {
|
|
1323
|
-
try {
|
|
1324
|
-
guild = byName(await this.core.rest.get(Routes.guildEmojis(guildId)));
|
|
1325
|
-
} catch (error) {
|
|
1326
|
-
failures.push(` - "${name}" for "${key}" targets guild ${guildId}, which could not be read (${reasonOf(error)})`);
|
|
1327
|
-
return;
|
|
1328
|
-
}
|
|
1329
|
-
guilds.set(guildId, guild);
|
|
1330
|
-
}
|
|
1331
|
-
this.resolve(key, name, guild, failures, `guild ${guildId}`);
|
|
1332
|
-
}
|
|
1333
|
-
resolve(key, name, source, failures, where) {
|
|
1334
|
-
const found = source.get(name);
|
|
1335
|
-
if (!found?.id) {
|
|
1336
|
-
failures.push(` - "${name}" for "${key}" was not found in ${where}`);
|
|
1337
|
-
return;
|
|
1338
|
-
}
|
|
1339
|
-
emojiStorage[key] = new ResolvedEmoji(name, found.id, found.animated ?? false);
|
|
1340
|
-
}
|
|
1341
|
-
};
|
|
1342
|
-
|
|
1343
|
-
//#endregion
|
|
1344
|
-
//#region src/handlers/interaction/SlashHandler.ts
|
|
1345
|
-
/**
|
|
1346
|
-
* Base class for a chat-input (slash) command handler on the HTTP transport.
|
|
1347
|
-
*
|
|
1348
|
-
* Pass the route from the generated registry as the generic, then read `this.options` for a single
|
|
1349
|
-
* command or `this.match` for several. Command authoring stays plain discord.js, `seedcord codegen`
|
|
1350
|
-
* reads its `toJSON()` to type these options.
|
|
1351
|
-
*
|
|
1352
|
-
* @typeParam Route - One or more route keys from {@link SlashRegistry}, e.g. `'ban'` or `'ban' | 'kick'`.
|
|
1353
|
-
*/
|
|
1354
|
-
var SlashHandler = class extends InteractionHandler {
|
|
1355
|
-
resolver;
|
|
1356
|
-
/**
|
|
1357
|
-
* The typed options for this command's route. Only the getters for kinds this command declares appear.
|
|
1358
|
-
* Each getter's name argument is restricted to that command's options of its kind.
|
|
1359
|
-
*
|
|
1360
|
-
* - A required option drops the null.
|
|
1361
|
-
*
|
|
1362
|
-
* - Choices narrow to their literal union.
|
|
1363
|
-
*
|
|
1364
|
-
* - A channel option declared with `addChannelTypes` narrows to those types.
|
|
1365
|
-
*
|
|
1366
|
-
* Rich kinds resolve from the interaction's `resolved` data.
|
|
1367
|
-
*/
|
|
1368
|
-
get options() {
|
|
1369
|
-
this.resolver ??= new HttpSlashOptions(this.event.data);
|
|
1370
|
-
return this.resolver;
|
|
1371
|
-
}
|
|
1372
|
-
/**
|
|
1373
|
-
* Run the arm for whichever command fired. Use this only when the handler is registered for several
|
|
1374
|
-
* routes.
|
|
1375
|
-
*
|
|
1376
|
-
* Provide one arm per registered route, keyed by its route string, and each arm receives that route's
|
|
1377
|
-
* own narrowed options. The arms must cover every route in the generic, a missing route or an unknown
|
|
1378
|
-
* key is a compile error.
|
|
1379
|
-
*
|
|
1380
|
-
* @param arms - One handler per registered route, keyed by route string.
|
|
1381
|
-
* @returns The result of the arm that ran.
|
|
1382
|
-
*/
|
|
1383
|
-
async match(arms) {
|
|
1384
|
-
const route = slashRouteOf(this.event.data);
|
|
1385
|
-
const arm = Object.hasOwn(arms, route) ? arms[route] : void 0;
|
|
1386
|
-
if (!arm) throw new SeedcordTypeError(SeedcordErrorCode.SlashMatchArmMissing, [route]);
|
|
1387
|
-
return await arm(this.options);
|
|
1388
|
-
}
|
|
1389
|
-
};
|
|
1390
|
-
|
|
1391
|
-
//#endregion
|
|
1392
|
-
//#region src/handlers/interaction/ContextMenuHandler.ts
|
|
1393
|
-
function armFor(arms, name) {
|
|
1394
|
-
const arm = Object.hasOwn(arms, name) ? arms[name] : void 0;
|
|
1395
|
-
if (typeof arm !== "function") throw new SeedcordTypeError(SeedcordErrorCode.ContextMenuMatchArmMissing, [name]);
|
|
1396
|
-
return arm;
|
|
1397
|
-
}
|
|
1398
|
-
/**
|
|
1399
|
-
* Base class for a user context-menu command handler on the HTTP transport (right-click a user).
|
|
1400
|
-
*
|
|
1401
|
-
* Pass the command name(s) as the generic, the same as `@UserContextMenuRoute`. Read the right-clicked user
|
|
1402
|
-
* from `this.target` and that same user's guild member from `this.targetMember`.
|
|
1403
|
-
*
|
|
1404
|
-
* @typeParam Names - One or more command names from the generated user registry.
|
|
1405
|
-
*/
|
|
1406
|
-
var UserContextMenuHandler = class extends InteractionHandler {
|
|
1407
|
-
get target() {
|
|
1408
|
-
const { target_id: targetId, resolved } = this.event.data;
|
|
1409
|
-
return resolved.users[targetId];
|
|
1410
|
-
}
|
|
1411
|
-
/** The right-clicked user's guild member, null outside a guild. */
|
|
1412
|
-
get targetMember() {
|
|
1413
|
-
const { target_id: targetId, resolved } = this.event.data;
|
|
1414
|
-
return resolved.members?.[targetId] ?? null;
|
|
1415
|
-
}
|
|
1416
|
-
/** Which of the registered commands fired. */
|
|
1417
|
-
get commandName() {
|
|
1418
|
-
return this.event.data.name;
|
|
1419
|
-
}
|
|
1420
|
-
/**
|
|
1421
|
-
* Run the arm for whichever command fired. Use this only when the handler is registered for several
|
|
1422
|
-
* names.
|
|
1423
|
-
*
|
|
1424
|
-
* Provide one arm per registered name, keyed by the name Discord shows in the right-click menu. Each
|
|
1425
|
-
* arm receives the clicked user and that user's guild member. The arms must cover every name in the
|
|
1426
|
-
* generic. A missing name or an unknown key is a compile error.
|
|
1427
|
-
*
|
|
1428
|
-
* @param arms - One handler per registered command name.
|
|
1429
|
-
* @returns The result of the arm that ran.
|
|
1430
|
-
*
|
|
1431
|
-
* @example
|
|
1432
|
-
* ```ts
|
|
1433
|
-
* \@UserContextMenuRoute('View Profile', 'Warn')
|
|
1434
|
-
* class Moderation extends UserContextMenuHandler<'View Profile' | 'Warn'> {
|
|
1435
|
-
* async execute() {
|
|
1436
|
-
* await this.match({
|
|
1437
|
-
* 'View Profile': (user) => this.reply(`Profile for ${user.username}.`),
|
|
1438
|
-
* Warn: (user, member) => this.reply(`Warned ${member?.nick ?? user.username}.`)
|
|
1439
|
-
* });
|
|
1440
|
-
* }
|
|
1441
|
-
* }
|
|
1442
|
-
* ```
|
|
1443
|
-
*/
|
|
1444
|
-
async match(arms) {
|
|
1445
|
-
return await armFor(arms, this.event.data.name)(this.target, this.targetMember);
|
|
1446
|
-
}
|
|
1447
|
-
};
|
|
1448
|
-
/**
|
|
1449
|
-
* Base class for a message context-menu command handler on the HTTP transport (right-click a message).
|
|
1450
|
-
*
|
|
1451
|
-
* Pass the command name(s) as the generic, the same as `@MessageContextMenuRoute`. Read the right-clicked
|
|
1452
|
-
* message from `this.target`.
|
|
1453
|
-
*
|
|
1454
|
-
* @typeParam Names - One or more command names from the generated message registry.
|
|
1455
|
-
*/
|
|
1456
|
-
var MessageContextMenuHandler = class extends InteractionHandler {
|
|
1457
|
-
get target() {
|
|
1458
|
-
const { target_id: targetId, resolved } = this.event.data;
|
|
1459
|
-
return resolved.messages[targetId];
|
|
1460
|
-
}
|
|
1461
|
-
/** Which of the registered commands fired. */
|
|
1462
|
-
get commandName() {
|
|
1463
|
-
return this.event.data.name;
|
|
1464
|
-
}
|
|
1465
|
-
/**
|
|
1466
|
-
* Run the arm for whichever command fired. Use this only when the handler is registered for several
|
|
1467
|
-
* names.
|
|
1468
|
-
*
|
|
1469
|
-
* Provide one arm per registered name, keyed by the name Discord shows in the right-click menu. Each
|
|
1470
|
-
* arm receives the clicked message. The arms must cover every name in the generic. A missing name or
|
|
1471
|
-
* an unknown key is a compile error.
|
|
1472
|
-
*
|
|
1473
|
-
* @param arms - One handler per registered command name.
|
|
1474
|
-
* @returns The result of the arm that ran.
|
|
1475
|
-
*
|
|
1476
|
-
* @example
|
|
1477
|
-
* ```ts
|
|
1478
|
-
* \@MessageContextMenuRoute('Report Message', 'Quote Message')
|
|
1479
|
-
* class MessageTools extends MessageContextMenuHandler<'Report Message' | 'Quote Message'> {
|
|
1480
|
-
* async execute() {
|
|
1481
|
-
* await this.match({
|
|
1482
|
-
* 'Report Message': (message) => this.report(message),
|
|
1483
|
-
* 'Quote Message': (message) => this.reply(`> ${message.content}`)
|
|
1484
|
-
* });
|
|
1485
|
-
* }
|
|
1486
|
-
* }
|
|
1487
|
-
* ```
|
|
1488
|
-
*/
|
|
1489
|
-
async match(arms) {
|
|
1490
|
-
return await armFor(arms, this.event.data.name)(this.target);
|
|
1491
|
-
}
|
|
1492
|
-
};
|
|
1493
|
-
|
|
1494
|
-
//#endregion
|
|
1495
|
-
//#region src/handlers/interaction/components/ComponentHandler.ts
|
|
1496
|
-
/**
|
|
1497
|
-
* Shared base the customId-routed component handlers extend on the HTTP transport.
|
|
1498
|
-
*
|
|
1499
|
-
* Not a public entry point. Extend {@link ButtonHandler}, {@link ModalHandler}, or one of the select menu
|
|
1500
|
-
* bases instead. This class defines the customId decode, the route matching, and the source-message
|
|
1501
|
-
* verbs `update` and `deferUpdate`.
|
|
1502
|
-
*
|
|
1503
|
-
* @typeParam Event - The component or modal interaction type this handler processes
|
|
1504
|
-
* @typeParam Defs - The customId route definitions registered on the concrete handler
|
|
1505
|
-
*/
|
|
1506
|
-
var ComponentHandler = class extends InteractionHandler {
|
|
1507
|
-
/** Rewrite the source message this component interaction came from. */
|
|
1508
|
-
update(response) {
|
|
1509
|
-
return this.sender.update(response);
|
|
1510
|
-
}
|
|
1511
|
-
/** Acknowledge the component without changing the source message. */
|
|
1512
|
-
deferUpdate() {
|
|
1513
|
-
return this.sender.deferUpdate();
|
|
1514
|
-
}
|
|
1515
|
-
decoded;
|
|
1516
|
-
get route() {
|
|
1517
|
-
this.decoded ??= decodeComponentRoute(this.constructor, this.event.data.custom_id);
|
|
1518
|
-
return this.decoded;
|
|
1519
|
-
}
|
|
1520
|
-
/**
|
|
1521
|
-
* The decoded params of the single route this handler is registered for.
|
|
1522
|
-
*
|
|
1523
|
-
* Reading this decodes `this.event.data.custom_id` once (cached after the first read) and throws
|
|
1524
|
-
* `StaleCustomId` or `InvalidCustomId` when the wire no longer matches the current shape, which the
|
|
1525
|
-
* dispatcher boundary turns into a reply. On a handler registered for several routes this is
|
|
1526
|
-
* `never`, so use {@link match} instead.
|
|
1527
|
-
*/
|
|
1528
|
-
get params() {
|
|
1529
|
-
return this.route.params;
|
|
1530
|
-
}
|
|
1531
|
-
/**
|
|
1532
|
-
* Run the arm for whichever route the component was minted from. Use this only when the handler is
|
|
1533
|
-
* registered for several routes. A single-route handler reads `this.params` directly. On a multi-route
|
|
1534
|
-
* handler `this.params` is `never`, so match is the only way to read the decoded params.
|
|
1535
|
-
*
|
|
1536
|
-
* Provide one arm per registered route, keyed by its prefix, and each arm receives that route's own
|
|
1537
|
-
* decoded params. The arms cover every registered route prefix, checked at compile time, and a prefix
|
|
1538
|
-
* unmatched at runtime throws `CustomIdMatchArmMissing`. Decoding runs before any arm, so a stale or
|
|
1539
|
-
* corrupt wire throws before an arm body executes.
|
|
1540
|
-
*
|
|
1541
|
-
* @param arms - One callback per registered route, keyed by prefix.
|
|
1542
|
-
* @returns The result of the arm that ran.
|
|
1543
|
-
*
|
|
1544
|
-
* @example
|
|
1545
|
-
* ```ts
|
|
1546
|
-
* \@ButtonRoute(Approve, Reject)
|
|
1547
|
-
* class ReviewButtons extends ButtonHandler<[typeof Approve, typeof Reject]> {
|
|
1548
|
-
* async execute() {
|
|
1549
|
-
* await this.match({
|
|
1550
|
-
* approve: ({ userId }) => this.reply(`approved <@${userId}>`),
|
|
1551
|
-
* reject: ({ userId }) => this.reply(`rejected <@${userId}>`)
|
|
1552
|
-
* });
|
|
1553
|
-
* }
|
|
1554
|
-
* }
|
|
1555
|
-
* ```
|
|
1556
|
-
*/
|
|
1557
|
-
async match(arms) {
|
|
1558
|
-
const { prefix, params } = this.route;
|
|
1559
|
-
const arm = Object.hasOwn(arms, prefix) ? arms[prefix] : void 0;
|
|
1560
|
-
if (!arm) throw new SeedcordTypeError(SeedcordErrorCode.CustomIdMatchArmMissing, [prefix]);
|
|
1561
|
-
return await arm(params);
|
|
1562
|
-
}
|
|
1563
|
-
};
|
|
1564
|
-
|
|
1565
|
-
//#endregion
|
|
1566
|
-
//#region src/handlers/interaction/components/ButtonHandler.ts
|
|
1567
|
-
/**
|
|
1568
|
-
* Base class for a button interaction handler on the HTTP transport.
|
|
1569
|
-
*
|
|
1570
|
-
* Register the customId definitions this handler decodes with `@ButtonRoute`, list the same ones in the
|
|
1571
|
-
* generic, then read `this.params` for a single route or `this.match` for several. Reply through the
|
|
1572
|
-
* handler members, or rewrite the source message with `this.update`. Passing different definitions to the
|
|
1573
|
-
* decorator and the generic is a compile error.
|
|
1574
|
-
*
|
|
1575
|
-
* @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ApproveId]`.
|
|
1576
|
-
*/
|
|
1577
|
-
var ButtonHandler = class extends ComponentHandler {};
|
|
1578
|
-
|
|
1579
|
-
//#endregion
|
|
1580
|
-
//#region src/handlers/interaction/components/ModalHandler.ts
|
|
1581
|
-
/**
|
|
1582
|
-
* Base class for a modal submit handler on the HTTP transport.
|
|
1583
|
-
*
|
|
1584
|
-
* Register the customId definitions this handler decodes with `@ModalRoute` and list the same ones in the
|
|
1585
|
-
* generic. Read the submitted inputs from `this.fields`. Reply through the handler members. `showModal`
|
|
1586
|
-
* fails compilation on this kind, because Discord forbids opening a modal in response to a modal.
|
|
1587
|
-
*
|
|
1588
|
-
* @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ConfigId]`.
|
|
1589
|
-
*
|
|
1590
|
-
* @example
|
|
1591
|
-
* ```ts
|
|
1592
|
-
* \@ModalRoute(ConfigId)
|
|
1593
|
-
* class ConfigModal extends ModalHandler<[typeof ConfigId]> {
|
|
1594
|
-
* async execute() {
|
|
1595
|
-
* const { guildId } = this.params;
|
|
1596
|
-
* const name = this.fields.getTextInputValue('name');
|
|
1597
|
-
* await this.reply(`saved ${name} for ${guildId}`);
|
|
1598
|
-
* }
|
|
1599
|
-
* }
|
|
1600
|
-
* ```
|
|
1601
|
-
*/
|
|
1602
|
-
var ModalHandler = class extends ComponentHandler {
|
|
1603
|
-
reader;
|
|
1604
|
-
/** The fields this modal submitted, read by custom id. */
|
|
1605
|
-
get fields() {
|
|
1606
|
-
this.reader ??= new ModalFields(this.event.data);
|
|
1607
|
-
return this.reader;
|
|
1608
|
-
}
|
|
1609
|
-
/** Rewrite the message this modal was opened from. Throws when a command opened the modal. */
|
|
1610
|
-
update(response) {
|
|
1611
|
-
this.ensureSourceMessage("update");
|
|
1612
|
-
return super.update(response);
|
|
1613
|
-
}
|
|
1614
|
-
/** Acknowledge the submit without changing the source message. Throws when a command opened the modal. */
|
|
1615
|
-
deferUpdate() {
|
|
1616
|
-
this.ensureSourceMessage("deferUpdate");
|
|
1617
|
-
return super.deferUpdate();
|
|
1618
|
-
}
|
|
1619
|
-
ensureSourceMessage(method) {
|
|
1620
|
-
if (this.event.message) return;
|
|
1621
|
-
throw new SeedcordError(SeedcordErrorCode.ReplyUpdateWithoutSource, [method, this.routeId]);
|
|
1622
|
-
}
|
|
1623
|
-
};
|
|
1624
|
-
|
|
1625
|
-
//#endregion
|
|
1626
|
-
//#region src/handlers/interaction/components/SelectMenuHandler.ts
|
|
1627
|
-
function resolvedOf(data) {
|
|
1628
|
-
return data.resolved ?? {};
|
|
1629
|
-
}
|
|
1630
|
-
/**
|
|
1631
|
-
* Shared base the select menu handlers extend on the HTTP transport.
|
|
1632
|
-
*
|
|
1633
|
-
* Not a public entry point. Extend {@link StringMenuHandler}, {@link UserMenuHandler},
|
|
1634
|
-
* {@link RoleMenuHandler}, {@link ChannelMenuHandler}, or {@link MentionableMenuHandler} instead. This
|
|
1635
|
-
* class defines the `values` getter those bases share.
|
|
1636
|
-
*
|
|
1637
|
-
* @typeParam Event - The select menu interaction type this handler processes
|
|
1638
|
-
* @typeParam Defs - The customId route definitions registered on the concrete handler
|
|
1639
|
-
*/
|
|
1640
|
-
var SelectMenuHandler = class extends ComponentHandler {
|
|
1641
|
-
/** The values this select picked. On every kind but the string menu they are snowflake ids. */
|
|
1642
|
-
get values() {
|
|
1643
|
-
return this.event.data.values ?? [];
|
|
1644
|
-
}
|
|
1645
|
-
};
|
|
1646
|
-
/**
|
|
1647
|
-
* Base class for a string select menu handler on the HTTP transport.
|
|
1648
|
-
*
|
|
1649
|
-
* Register the customId definitions with `@StringMenuRoute`, list the same ones in the generic, then read
|
|
1650
|
-
* the chosen option values from `this.values`. Read `this.params` for a single route or `this.match` for
|
|
1651
|
-
* several. Reply through the handler members, or rewrite the source message with `this.update`.
|
|
1652
|
-
*
|
|
1653
|
-
* @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof TopicsId]`.
|
|
1654
|
-
*
|
|
1655
|
-
* @example
|
|
1656
|
-
* ```ts
|
|
1657
|
-
* \@StringMenuRoute(TopicsId)
|
|
1658
|
-
* class Topics extends StringMenuHandler<[typeof TopicsId]> {
|
|
1659
|
-
* async execute() {
|
|
1660
|
-
* const { userId } = this.params;
|
|
1661
|
-
* await this.update(`<@${userId}> now follows ${this.values.join(', ')}`);
|
|
1662
|
-
* }
|
|
1663
|
-
* }
|
|
1664
|
-
* ```
|
|
1665
|
-
*/
|
|
1666
|
-
var StringMenuHandler = class extends SelectMenuHandler {};
|
|
1667
|
-
/**
|
|
1668
|
-
* Base class for a user select menu handler on the HTTP transport.
|
|
1669
|
-
*
|
|
1670
|
-
* Beside `this.values` this handler declares `this.users` and `this.members`. Register the customId
|
|
1671
|
-
* definitions with `@UserMenuRoute` and list the same ones in the generic.
|
|
1672
|
-
*
|
|
1673
|
-
* @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof AssignId]`.
|
|
1674
|
-
*
|
|
1675
|
-
* @example
|
|
1676
|
-
* ```ts
|
|
1677
|
-
* \@UserMenuRoute(AssignId)
|
|
1678
|
-
* class Assign extends UserMenuHandler<[typeof AssignId]> {
|
|
1679
|
-
* async execute() {
|
|
1680
|
-
* const { roleId } = this.params;
|
|
1681
|
-
* await this.reply(`assigning ${this.users.size} member(s) to <@&${roleId}>`);
|
|
1682
|
-
* }
|
|
1683
|
-
* }
|
|
1684
|
-
* ```
|
|
1685
|
-
*/
|
|
1686
|
-
var UserMenuHandler = class extends SelectMenuHandler {
|
|
1687
|
-
/** The users this select picked. */
|
|
1688
|
-
get users() {
|
|
1689
|
-
return pick(this.values, resolvedOf(this.event.data).users);
|
|
1690
|
-
}
|
|
1691
|
-
/** The guild members behind the picked users. Discord resolves these only inside a guild. */
|
|
1692
|
-
get members() {
|
|
1693
|
-
return pick(this.values, resolvedOf(this.event.data).members);
|
|
1694
|
-
}
|
|
1695
|
-
};
|
|
1696
|
-
/**
|
|
1697
|
-
* Base class for a role select menu handler on the HTTP transport.
|
|
1698
|
-
*
|
|
1699
|
-
* Beside `this.values` this handler declares `this.roles`. Register the customId definitions with
|
|
1700
|
-
* `@RoleMenuRoute` and list the same ones in the generic.
|
|
1701
|
-
*
|
|
1702
|
-
* @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof GrantId]`.
|
|
1703
|
-
*
|
|
1704
|
-
* @example
|
|
1705
|
-
* ```ts
|
|
1706
|
-
* \@RoleMenuRoute(GrantId)
|
|
1707
|
-
* class Grant extends RoleMenuHandler<[typeof GrantId]> {
|
|
1708
|
-
* async execute() {
|
|
1709
|
-
* await this.update(`granting ${this.roles.size} role(s)`);
|
|
1710
|
-
* }
|
|
1711
|
-
* }
|
|
1712
|
-
* ```
|
|
1713
|
-
*/
|
|
1714
|
-
var RoleMenuHandler = class extends SelectMenuHandler {
|
|
1715
|
-
/** The roles this select picked. */
|
|
1716
|
-
get roles() {
|
|
1717
|
-
return pick(this.values, resolvedOf(this.event.data).roles);
|
|
1857
|
+
const encoder = new TextEncoder();
|
|
1858
|
+
var Ed25519Verifier = class {
|
|
1859
|
+
key;
|
|
1860
|
+
constructor(publicKeyHex) {
|
|
1861
|
+
const publicKey = hexToBytes(publicKeyHex, PUBLIC_KEY_BYTES);
|
|
1862
|
+
if (!publicKey) throw new SeedcordError(SeedcordErrorCode.ConfigInvalidEnv, ["DISCORD_PUBLIC_KEY", "64 hex characters"]);
|
|
1863
|
+
this.key = crypto.subtle.importKey("raw", publicKey, "Ed25519", false, ["verify"]);
|
|
1718
1864
|
}
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
*
|
|
1728
|
-
* @example
|
|
1729
|
-
* ```ts
|
|
1730
|
-
* \@ChannelMenuRoute(LogTargetId)
|
|
1731
|
-
* class LogTarget extends ChannelMenuHandler<[typeof LogTargetId]> {
|
|
1732
|
-
* async execute() {
|
|
1733
|
-
* await this.update(`logging to ${this.channels.size} channel(s)`);
|
|
1734
|
-
* }
|
|
1735
|
-
* }
|
|
1736
|
-
* ```
|
|
1737
|
-
*/
|
|
1738
|
-
var ChannelMenuHandler = class extends SelectMenuHandler {
|
|
1739
|
-
/** The channels this select picked. */
|
|
1740
|
-
get channels() {
|
|
1741
|
-
return pick(this.values, resolvedOf(this.event.data).channels);
|
|
1865
|
+
async verify(signatureHex, timestamp, body) {
|
|
1866
|
+
const signature = hexToBytes(signatureHex, SIGNATURE_BYTES);
|
|
1867
|
+
if (!signature) return false;
|
|
1868
|
+
const timestampBytes = encoder.encode(timestamp);
|
|
1869
|
+
const message = new Uint8Array(timestampBytes.length + body.length);
|
|
1870
|
+
message.set(timestampBytes, 0);
|
|
1871
|
+
message.set(body, timestampBytes.length);
|
|
1872
|
+
return crypto.subtle.verify("Ed25519", await this.key, signature, message);
|
|
1742
1873
|
}
|
|
1743
1874
|
};
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
* }
|
|
1759
|
-
* }
|
|
1760
|
-
* ```
|
|
1761
|
-
*/
|
|
1762
|
-
var MentionableMenuHandler = class extends SelectMenuHandler {
|
|
1763
|
-
/** The users this select picked. */
|
|
1764
|
-
get users() {
|
|
1765
|
-
return pick(this.values, resolvedOf(this.event.data).users);
|
|
1875
|
+
|
|
1876
|
+
//#endregion
|
|
1877
|
+
//#region src/receiver/ReplayGuard.ts
|
|
1878
|
+
const REPLAY_WINDOW_MS = 3e5;
|
|
1879
|
+
const SWEEP_GAP_MS = 6e4;
|
|
1880
|
+
const MS_PER_SECOND = 1e3;
|
|
1881
|
+
const TIMESTAMP_PATTERN = /^\d+$/;
|
|
1882
|
+
var ReplayGuard = class {
|
|
1883
|
+
seen = /* @__PURE__ */ new Map();
|
|
1884
|
+
lastSweep = Date.now();
|
|
1885
|
+
fresh(timestampHeader) {
|
|
1886
|
+
if (!TIMESTAMP_PATTERN.test(timestampHeader)) return null;
|
|
1887
|
+
const timestampMs = Number(timestampHeader) * MS_PER_SECOND;
|
|
1888
|
+
return Math.abs(Date.now() - timestampMs) <= REPLAY_WINDOW_MS ? timestampMs : null;
|
|
1766
1889
|
}
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1890
|
+
unseen(signatureHex, timestampMs) {
|
|
1891
|
+
const now = Date.now();
|
|
1892
|
+
this.maybeSweep(now);
|
|
1893
|
+
const rejectUntil = this.seen.get(signatureHex);
|
|
1894
|
+
if (rejectUntil !== void 0 && rejectUntil >= now) return false;
|
|
1895
|
+
this.seen.set(signatureHex, timestampMs + REPLAY_WINDOW_MS);
|
|
1896
|
+
return true;
|
|
1770
1897
|
}
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1898
|
+
maybeSweep(now) {
|
|
1899
|
+
if (now - this.lastSweep < SWEEP_GAP_MS) return;
|
|
1900
|
+
this.lastSweep = now;
|
|
1901
|
+
for (const [signature, rejectUntil] of this.seen) if (rejectUntil < now) this.seen.delete(signature);
|
|
1774
1902
|
}
|
|
1775
1903
|
};
|
|
1776
1904
|
|
|
1777
1905
|
//#endregion
|
|
1778
|
-
//#region src/
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
//#endregion
|
|
1794
|
-
//#region src/pagination/Paginator.ts
|
|
1795
|
-
function contextOf(interaction, core) {
|
|
1796
|
-
return {
|
|
1797
|
-
interaction,
|
|
1798
|
-
user: interaction.member?.user ?? interaction.user,
|
|
1799
|
-
guildId: interaction.guild_id ?? null,
|
|
1800
|
-
core
|
|
1801
|
-
};
|
|
1906
|
+
//#region src/engine.ts
|
|
1907
|
+
const SIGNATURE_HEADER = "x-signature-ed25519";
|
|
1908
|
+
const TIMESTAMP_HEADER = "x-signature-timestamp";
|
|
1909
|
+
const ACCEPTED = 202;
|
|
1910
|
+
const BAD_REQUEST = 400;
|
|
1911
|
+
const UNAUTHORIZED = 401;
|
|
1912
|
+
const METHOD_NOT_ALLOWED = 405;
|
|
1913
|
+
const PING = InteractionType.Ping;
|
|
1914
|
+
const decoder = new TextDecoder();
|
|
1915
|
+
function rejectNonPost(request) {
|
|
1916
|
+
if (request.method === "POST") return void 0;
|
|
1917
|
+
return new Response(null, {
|
|
1918
|
+
status: METHOD_NOT_ALLOWED,
|
|
1919
|
+
headers: { allow: "POST" }
|
|
1920
|
+
});
|
|
1802
1921
|
}
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
* ```
|
|
1821
|
-
*/
|
|
1822
|
-
var Paginator = class extends PaginatorBase {
|
|
1823
|
-
/** The nav handler base. Extend it with an empty body and decorate it, `@ButtonRoute(p.cursor)`. */
|
|
1824
|
-
Handler;
|
|
1825
|
-
constructor(config) {
|
|
1826
|
-
super(config);
|
|
1827
|
-
const pageFor = (handler, n) => this.page(handler, n);
|
|
1828
|
-
this.Handler = class Nav extends ButtonHandler {
|
|
1829
|
-
async execute() {
|
|
1830
|
-
await this.deferUpdate();
|
|
1831
|
-
const response = await pageFor(this, this.params.page);
|
|
1832
|
-
await this.update(response);
|
|
1833
|
-
}
|
|
1834
|
-
};
|
|
1835
|
-
}
|
|
1836
|
-
/**
|
|
1837
|
-
* Render a page as a {@link ReplyResponse} without sending anything.
|
|
1838
|
-
*
|
|
1839
|
-
* @param handler - The handler rendering the page, normally `this`.
|
|
1840
|
-
* @param n - The zero-based page.
|
|
1841
|
-
*/
|
|
1842
|
-
async page(handler, n) {
|
|
1843
|
-
return this.buildPage(contextOf(handler.getEvent(), handler.core), n);
|
|
1922
|
+
function hasInteractionType(payload) {
|
|
1923
|
+
return typeof payload === "object" && payload !== null && "type" in payload && typeof payload.type === "number";
|
|
1924
|
+
}
|
|
1925
|
+
function buildEngine(core, maps, middlewares, publicKey) {
|
|
1926
|
+
const verifier = new Ed25519Verifier(publicKey);
|
|
1927
|
+
const replays = new ReplayGuard();
|
|
1928
|
+
const logger = new Logger("Engine", { channel: "bot" });
|
|
1929
|
+
async function dispatchMatched(match, payload, ctx) {
|
|
1930
|
+
const start = await dispatchInteraction({
|
|
1931
|
+
match,
|
|
1932
|
+
payload,
|
|
1933
|
+
core,
|
|
1934
|
+
middlewares
|
|
1935
|
+
});
|
|
1936
|
+
if (!start) return;
|
|
1937
|
+
const work = start().catch(rootFault);
|
|
1938
|
+
ctx?.waitUntil(work);
|
|
1844
1939
|
}
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
* @param n - The zero-based page to open on.
|
|
1850
|
-
*/
|
|
1851
|
-
async start(handler, n = 0) {
|
|
1852
|
-
const response = await this.page(handler, n);
|
|
1853
|
-
return handler.sender.send(response, { ephemeral: this.config.ephemeral ?? false });
|
|
1940
|
+
function rootFault(caught) {
|
|
1941
|
+
const error = asError(caught);
|
|
1942
|
+
logger.error("dispatch failed past the boundary", error);
|
|
1943
|
+
core.bus[PublishDefault]("unhandledInteractionError", { error });
|
|
1854
1944
|
}
|
|
1855
|
-
|
|
1945
|
+
const handle = async (request, ctx) => {
|
|
1946
|
+
const [response, outcome] = await respond(request, ctx);
|
|
1947
|
+
logger.debug(`${paint.sky.bold(request.method)} ${outcome} ${paint.amber(String(response.status))}`);
|
|
1948
|
+
return response;
|
|
1949
|
+
};
|
|
1950
|
+
const respond = async (request, ctx) => {
|
|
1951
|
+
const notPost = rejectNonPost(request);
|
|
1952
|
+
if (notPost) return [notPost, "not-post"];
|
|
1953
|
+
const signature = request.headers.get(SIGNATURE_HEADER);
|
|
1954
|
+
const timestamp = request.headers.get(TIMESTAMP_HEADER);
|
|
1955
|
+
if (signature === null || timestamp === null) return [new Response(null, { status: UNAUTHORIZED }), "unsigned"];
|
|
1956
|
+
const timestampMs = replays.fresh(timestamp);
|
|
1957
|
+
if (timestampMs === null) return [new Response(null, { status: UNAUTHORIZED }), "stale"];
|
|
1958
|
+
const body = new Uint8Array(await request.arrayBuffer());
|
|
1959
|
+
if (!await verifier.verify(signature, timestamp, body)) return [new Response(null, { status: UNAUTHORIZED }), "bad-signature"];
|
|
1960
|
+
let payload;
|
|
1961
|
+
try {
|
|
1962
|
+
payload = JSON.parse(decoder.decode(body));
|
|
1963
|
+
} catch {
|
|
1964
|
+
return [new Response(null, { status: BAD_REQUEST }), "bad-body"];
|
|
1965
|
+
}
|
|
1966
|
+
if (!hasInteractionType(payload)) return [new Response(null, { status: BAD_REQUEST }), "bad-body"];
|
|
1967
|
+
if (payload.type === PING) return [Response.json({ type: InteractionResponseType.Pong }), "ping"];
|
|
1968
|
+
if (!replays.unseen(signature, timestampMs)) return [new Response(null, { status: UNAUTHORIZED }), "replay"];
|
|
1969
|
+
try {
|
|
1970
|
+
core.bus[PublishDefault]("anyInteraction", { interaction: payload });
|
|
1971
|
+
const match = resolve(maps, payload);
|
|
1972
|
+
if (match) await dispatchMatched(match, payload, ctx);
|
|
1973
|
+
} catch (caught) {
|
|
1974
|
+
rootFault(caught);
|
|
1975
|
+
}
|
|
1976
|
+
return [new Response(null, { status: ACCEPTED }), "dispatched"];
|
|
1977
|
+
};
|
|
1978
|
+
return handle;
|
|
1979
|
+
}
|
|
1856
1980
|
|
|
1857
1981
|
//#endregion
|
|
1858
|
-
//#region src/
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1982
|
+
//#region src/InteractionsService.ts
|
|
1983
|
+
var InteractionsService = class {
|
|
1984
|
+
host;
|
|
1985
|
+
rest;
|
|
1986
|
+
rateLimiter;
|
|
1987
|
+
bus;
|
|
1988
|
+
subscribers;
|
|
1989
|
+
interactions;
|
|
1990
|
+
token;
|
|
1991
|
+
builtEngine;
|
|
1992
|
+
constructor(host, config, restOptions) {
|
|
1993
|
+
this.host = host;
|
|
1994
|
+
this.rest = new REST(restOptions);
|
|
1995
|
+
this.rateLimiter = config.store ?? new MemoryRateLimiter();
|
|
1996
|
+
this.bus = new Bus(host);
|
|
1997
|
+
this.subscribers = new SubscriberLoader(this.bus, config.subscribers.path);
|
|
1998
|
+
const interactions = config.bot.interactions;
|
|
1999
|
+
this.interactions = interactions.path ? new InteractionDispatcher(interactions.path, interactions.middlewares) : void 0;
|
|
2000
|
+
}
|
|
2001
|
+
get applicationId() {
|
|
2002
|
+
if (!this.token) throw new SeedcordError(SeedcordErrorCode.CoreApplicationUnavailable);
|
|
2003
|
+
return this.token.applicationId;
|
|
2004
|
+
}
|
|
2005
|
+
authenticate() {
|
|
2006
|
+
this.token = readBotToken();
|
|
2007
|
+
this.rest.setToken(this.token.value);
|
|
2008
|
+
}
|
|
2009
|
+
get engine() {
|
|
2010
|
+
if (!this.builtEngine) throw new SeedcordError(SeedcordErrorCode.CoreFetchBeforeStart);
|
|
2011
|
+
return this.builtEngine;
|
|
2012
|
+
}
|
|
2013
|
+
prepareEngine(publicKey) {
|
|
2014
|
+
this.builtEngine ??= this.createEngine(publicKey);
|
|
2015
|
+
return this.builtEngine;
|
|
2016
|
+
}
|
|
2017
|
+
createEngine(publicKey) {
|
|
2018
|
+
const maps = this.interactions?.maps ?? emptyRouteMaps();
|
|
2019
|
+
const middlewares = this.interactions?.middlewares ?? new MiddlewareRegistry(interactionMiddleware);
|
|
2020
|
+
return buildEngine(this.host, maps, middlewares, publicKey);
|
|
2021
|
+
}
|
|
2022
|
+
};
|
|
1876
2023
|
|
|
1877
2024
|
//#endregion
|
|
1878
|
-
//#region src/
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
*
|
|
1889
|
-
* @example
|
|
1890
|
-
* ```ts
|
|
1891
|
-
* import { PermissionFlagsBits } from 'discord-api-types/v10';
|
|
1892
|
-
*
|
|
1893
|
-
* \@Gated(GuildOnly(), RequirePermissions([PermissionFlagsBits.ManageMessages]))
|
|
1894
|
-
* \@SlashRoute('purge')
|
|
1895
|
-
* class Purge extends SlashHandler<'purge'> {
|
|
1896
|
-
* async execute() {
|
|
1897
|
-
* // both gates passed, so this runs in a guild with a caller who can moderate this channel
|
|
1898
|
-
* }
|
|
1899
|
-
* }
|
|
1900
|
-
* ```
|
|
1901
|
-
*
|
|
1902
|
-
* @typeParam Gates - The non-empty tuple of gates to run, in order, against the handler's context.
|
|
1903
|
-
* @typeParam TCtor - The decorated handler's constructor, checked against every gate's required context.
|
|
1904
|
-
* @param gates - One or more gates to attach, each ANDed so the handler runs only when all pass.
|
|
1905
|
-
* @param ctor - The handler class being decorated, rejected at compile time when a gate requires a context it lacks.
|
|
1906
|
-
*
|
|
1907
|
-
* @see {@link defineGate}
|
|
1908
|
-
* @see {@link defineEffectGate}
|
|
1909
|
-
* @decorator
|
|
1910
|
-
*/
|
|
1911
|
-
function Gated(...gates) {
|
|
1912
|
-
return function(ctor) {
|
|
1913
|
-
const existing = Reflect.getMetadata(GatedMetadataKey, ctor) ?? [];
|
|
1914
|
-
Reflect.defineMetadata(GatedMetadataKey, [...existing, ...gates], ctor);
|
|
1915
|
-
};
|
|
2025
|
+
//#region src/username.ts
|
|
2026
|
+
async function usernameOf(rest, logger) {
|
|
2027
|
+
try {
|
|
2028
|
+
const me = await rest.get(Routes.user("@me"));
|
|
2029
|
+
if (me.username) logger.info(`Running as ${paint.sky.bold(me.username)}`);
|
|
2030
|
+
return me.username;
|
|
2031
|
+
} catch (caught) {
|
|
2032
|
+
logger.warn("could not fetch the bot identity", caught);
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
1916
2035
|
}
|
|
1917
2036
|
|
|
1918
2037
|
//#endregion
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
//#endregion
|
|
1923
|
-
export { InteractionHandler as A, createSeedcord as C, isHandlerClass as D, emptyRouteMaps as E, RepliableHandler$1 as M, ReplySender as N, isMiddlewareClass as O, ModalFields as S, RouteRegistry as T, MessageContextMenuHandler as _, Paginator as a, EmojiInjector as b, ChannelMenuHandler as c, SelectMenuHandler as d, StringMenuHandler as f, ComponentHandler as g, ButtonHandler as h, CursorSource as i, AutocompleteHandler as j, InteractionMiddleware as k, MentionableMenuHandler as l, ModalHandler as m, Gated as n, Subscriber$1 as o, UserMenuHandler as p, ArraySource as r, WebhookLog$1 as s, version as t, RoleMenuHandler as u, UserContextMenuHandler as v, buildEngine as w, Emojis as x, SlashHandler as y };
|
|
1924
|
-
//# sourceMappingURL=edge.index-DCP1BD6f.mjs.map
|
|
2038
|
+
export { Emojis as A, MessageContextMenuHandler as C, InteractionHandler as D, InteractionMiddleware as E, ModalFields as M, Plugin$1 as N, RepliableHandler$1 as O, AutocompleteHandler as S, SlashHandler as T, StringMenuHandler as _, HmrModuleHandler$1 as a, ButtonHandler as b, ArraySource as c, Subscriber$1 as d, WebhookLog$1 as f, SelectMenuHandler as g, RoleMenuHandler as h, HttpStartupTasks as i, installEmojis as j, ReplySender as k, CursorSource as l, MentionableMenuHandler as m, InteractionsService as n, version as o, ChannelMenuHandler as p, rejectNonPost as r, Gated as s, usernameOf as t, Paginator as u, UserMenuHandler as v, UserContextMenuHandler as w, ComponentHandler as x, ModalHandler as y };
|
|
2039
|
+
//# sourceMappingURL=username-3UsqzTSd.mjs.map
|