chapterjs 0.4.0 → 0.5.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 +3 -1
- package/dist/assets/public.js +4 -4
- package/dist/cli/build.d.ts +6 -2
- package/dist/cli/build.js +3 -3
- package/dist/cli/dev.js +8 -5
- package/dist/cli/project.d.ts +31 -2
- package/dist/cli/project.js +166 -34
- package/dist/cli/redis.js +14 -2
- package/dist/cli/start.js +11 -1
- package/dist/cli/test.js +108 -16
- package/dist/commands/command.d.ts +140 -5
- package/dist/commands/command.js +17 -2
- package/dist/commands/declaration.d.ts +17 -4
- package/dist/commands/declaration.js +128 -2
- package/dist/commands/router.js +78 -18
- package/dist/commands/tree.d.ts +25 -4
- package/dist/commands/tree.js +95 -26
- package/dist/commands/twins.d.ts +8 -0
- package/dist/commands/twins.js +24 -6
- package/dist/components/button.d.ts +8 -3
- package/dist/components/declaration.d.ts +2 -2
- package/dist/components/declaration.js +16 -12
- package/dist/components/render.js +5 -0
- package/dist/components/router.js +41 -0
- package/dist/components/select.d.ts +12 -5
- package/dist/core/bot.d.ts +7 -0
- package/dist/core/bot.js +44 -0
- package/dist/discord/api.d.ts +2 -0
- package/dist/discord/api.js +2 -0
- package/dist/discord/permissions.d.ts +4 -4
- package/dist/discord/permissions.js +13 -0
- package/dist/events/registry.d.ts +19 -10
- package/dist/events/registry.js +10 -4
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/interactions/dispatch.js +4 -2
- package/dist/loader/loader.d.ts +16 -0
- package/dist/loader/loader.js +52 -7
- package/dist/loader/usage.d.ts +15 -0
- package/dist/loader/usage.js +40 -0
- package/dist/loader/watch.d.ts +5 -3
- package/dist/loader/watch.js +88 -17
- package/dist/messages/commands.js +3 -0
- package/dist/messages/messages.d.ts +1 -0
- package/dist/messages/translate.js +5 -1
- package/dist/presence/declaration.d.ts +49 -2
- package/dist/presence/declaration.js +122 -39
- package/dist/presence/presence.d.ts +65 -6
- package/dist/presence/presence.js +11 -0
- package/dist/presence/rotation.d.ts +32 -0
- package/dist/presence/rotation.js +167 -0
- package/dist/rest/errors.d.ts +0 -1
- package/dist/rest/errors.js +15 -1
- package/dist/rest/rest.js +10 -1
- package/dist/store/file-backend.d.ts +0 -1
- package/dist/store/file-backend.js +18 -1
- package/dist/store/scope.d.ts +6 -1
- package/dist/store/scope.js +9 -3
- package/dist/structures/channel.d.ts +13 -0
- package/dist/structures/channel.js +24 -0
- package/dist/structures/context.d.ts +3 -0
- package/dist/structures/entities.js +1 -0
- package/dist/structures/interaction.d.ts +6 -0
- package/dist/structures/interaction.js +20 -0
- package/dist/structures/message.d.ts +17 -0
- package/dist/structures/message.js +33 -0
- package/dist/structures/wait.d.ts +105 -0
- package/dist/structures/wait.js +85 -0
- package/dist/testing/harness.d.ts +48 -8
- package/dist/testing/harness.js +156 -36
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/smoke.js +112 -1
- package/dist/testing/world.js +51 -2
- package/dist/util/call-site.d.ts +4 -0
- package/dist/util/call-site.js +22 -0
- package/dist/voice/connection.d.ts +2 -0
- package/dist/voice/connection.js +15 -1
- package/dist/voice/usage.d.ts +3 -2
- package/dist/voice/usage.js +7 -19
- package/dist/waiting/usage.d.ts +5 -0
- package/dist/waiting/usage.js +19 -0
- package/dist/waiting/waiters.d.ts +48 -0
- package/dist/waiting/waiters.js +107 -0
- package/package.json +1 -1
|
@@ -31,8 +31,12 @@ export interface ButtonLook<Data extends DataShape = DataShape> {
|
|
|
31
31
|
export type ButtonContext<Data extends DataShape = {}, Where extends ComponentWhere = 'guild'> = InteractiveContext<Data> & TranslationContext & PlaceOf<Where, ComponentInteraction>;
|
|
32
32
|
/** What a file gives to `button()`. */
|
|
33
33
|
export interface ButtonConfig<Data extends DataShape = {}, Where extends ComponentWhere = 'guild'> extends ButtonLook<Data>, InteractiveConfig<Data, Where> {
|
|
34
|
-
/**
|
|
35
|
-
|
|
34
|
+
/**
|
|
35
|
+
* What to do when someone clicks the button. Leave it out for a button
|
|
36
|
+
* that is only waited for (`message.waitForClick()`): once nothing waits,
|
|
37
|
+
* a click answers that it is not available any more.
|
|
38
|
+
*/
|
|
39
|
+
run?: (context: ButtonContext<Data, Where>) => unknown;
|
|
36
40
|
}
|
|
37
41
|
/**
|
|
38
42
|
* What `button()` returns: a declaration the framework finds in the exports
|
|
@@ -71,7 +75,8 @@ export interface LoadedButton {
|
|
|
71
75
|
readonly where: ComponentWhere;
|
|
72
76
|
readonly who: 'everyone' | 'author';
|
|
73
77
|
readonly ephemeral: boolean;
|
|
74
|
-
|
|
78
|
+
/** `undefined` for a button that is only waited for. */
|
|
79
|
+
readonly run: ((context: ButtonContext<DataShape, ComponentWhere>) => unknown) | undefined;
|
|
75
80
|
}
|
|
76
81
|
/** Checks what a message gives to change the look of a button. */
|
|
77
82
|
export declare function readLook(what: string, look: unknown): ButtonLook;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Declaration } from '../loader/loader.js';
|
|
2
2
|
import { type LoadedButton } from './button.js';
|
|
3
3
|
import { isEmbedDeclaration } from './embed.js';
|
|
4
4
|
import { type ComponentKind } from './declared.js';
|
|
@@ -29,7 +29,7 @@ export declare function findDuplicates<T extends {
|
|
|
29
29
|
kind: ComponentKind;
|
|
30
30
|
name: string;
|
|
31
31
|
};
|
|
32
|
-
}>(entries: readonly T[]): {
|
|
32
|
+
}>(entries: readonly T[], running?: ReadonlySet<string>): {
|
|
33
33
|
valid: T[];
|
|
34
34
|
conflicts: {
|
|
35
35
|
file: string;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// bound to the name of its export, which is its id and what makes its
|
|
5
5
|
// instances.
|
|
6
6
|
import { Limits } from '../discord/api.js';
|
|
7
|
+
import { bySite } from '../loader/loader.js';
|
|
7
8
|
import { checkEmbed } from '../structures/payload.js';
|
|
8
9
|
import { isDynamic } from './component.js';
|
|
9
10
|
import { readLook, renderButton, } from './button.js';
|
|
@@ -93,6 +94,17 @@ function checkRun(what, value, example) {
|
|
|
93
94
|
fail(`${what} has no "run": the function to run when someone uses it, like ${example}`);
|
|
94
95
|
}
|
|
95
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* The `run` of a button or a menu: optional, since one may only be waited
|
|
99
|
+
* for (`message.waitForClick()`); when given, a function.
|
|
100
|
+
*/
|
|
101
|
+
function checkOptionalRun(what, value, example) {
|
|
102
|
+
if (value === undefined)
|
|
103
|
+
return;
|
|
104
|
+
if (typeof value !== 'function') {
|
|
105
|
+
fail(`"run" of ${what} is the function to run when someone uses it, like ${example}. Leave it out for one that is only waited for with message.waitForClick().`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
96
108
|
function readButton(name, config) {
|
|
97
109
|
if (!isRecord(config)) {
|
|
98
110
|
return fail(`button() needs an object: button({ label: '...', run({ interaction }) { ... } })`);
|
|
@@ -118,7 +130,7 @@ function readButton(name, config) {
|
|
|
118
130
|
if (look.label === undefined && look.emoji === undefined) {
|
|
119
131
|
fail('A button needs a label or an emoji.');
|
|
120
132
|
}
|
|
121
|
-
|
|
133
|
+
checkOptionalRun(what, config.run, `async run({ interaction }) { await interaction.update('Done!'); }`);
|
|
122
134
|
return Object.freeze({
|
|
123
135
|
kind: 'button',
|
|
124
136
|
name,
|
|
@@ -168,7 +180,7 @@ function readSelect(name, config) {
|
|
|
168
180
|
}
|
|
169
181
|
const options = type === 'string' ? readOptions(what, config.options) : [];
|
|
170
182
|
const placeholder = readPlaceholder(what, config.placeholder);
|
|
171
|
-
|
|
183
|
+
checkOptionalRun(what, config.run, `async run({ interaction, values }) { ... }`);
|
|
172
184
|
return Object.freeze({
|
|
173
185
|
kind: 'select',
|
|
174
186
|
name,
|
|
@@ -310,18 +322,10 @@ export const componentDeclaration = {
|
|
|
310
322
|
},
|
|
311
323
|
};
|
|
312
324
|
/** Two components of the same kind with the same name: the later one is left out. */
|
|
313
|
-
export function findDuplicates(entries) {
|
|
325
|
+
export function findDuplicates(entries, running) {
|
|
314
326
|
const seen = new Map();
|
|
315
327
|
const conflicts = [];
|
|
316
|
-
const valid =
|
|
317
|
-
.sort((a, b) => a.file === b.file
|
|
318
|
-
? a.export < b.export
|
|
319
|
-
? -1
|
|
320
|
-
: 1
|
|
321
|
-
: a.file < b.file
|
|
322
|
-
? -1
|
|
323
|
-
: 1)
|
|
324
|
-
.filter(entry => {
|
|
328
|
+
const valid = bySite(entries, running).filter(entry => {
|
|
325
329
|
const key = `${entry.component.kind}:${entry.component.name}`;
|
|
326
330
|
const first = seen.get(key);
|
|
327
331
|
if (!first) {
|
|
@@ -46,6 +46,11 @@ export function renderComponents(components, t = missingTranslator(null)) {
|
|
|
46
46
|
const v2 = rows.some(one => one.kind !== 'row');
|
|
47
47
|
// The texts of a piece may be computed now, for who will read the message.
|
|
48
48
|
const raw = rows.map(one => resolve(one, t).raw);
|
|
49
|
+
// Without the pieces of the new layout, a message is rows only, and
|
|
50
|
+
// Discord takes 5 of them.
|
|
51
|
+
if (!v2 && raw.length > Limits.MessageActionRows) {
|
|
52
|
+
throw new RangeError(`The message has ${raw.length} rows of components: Discord accepts ${Limits.MessageActionRows} at most. A menu takes a row by itself and up to 5 buttons share one: send the rest in another message.`);
|
|
53
|
+
}
|
|
49
54
|
const total = count(raw);
|
|
50
55
|
if (total > Limits.MessageComponents) {
|
|
51
56
|
throw new RangeError(`The message has ${total} components once its rows are made: Discord accepts ${Limits.MessageComponents} at most.`);
|
|
@@ -10,6 +10,7 @@ import { ComponentInteraction, ModalInteraction, } from '../structures/interacti
|
|
|
10
10
|
import { remember } from '../structures/known.js';
|
|
11
11
|
import { toCamelCase } from '../util/case.js';
|
|
12
12
|
import { decodeCustomId, readData } from './custom-id.js';
|
|
13
|
+
import { componentStateOf } from './declared.js';
|
|
13
14
|
import { SELECT_TYPES } from './select.js';
|
|
14
15
|
/** What a component is called, for the person and the developer. */
|
|
15
16
|
const WHAT_OF = {
|
|
@@ -130,6 +131,32 @@ export class ComponentRouter {
|
|
|
130
131
|
this.#options.onError(file, error);
|
|
131
132
|
return refuse(interaction, 'failed', { what });
|
|
132
133
|
}
|
|
134
|
+
// A click a function of the files waits for (`message.waitForClick()`)
|
|
135
|
+
// is theirs: the `run` of the component does not run.
|
|
136
|
+
if (component.kind !== 'modal' && ctx.waiters?.wants('click')) {
|
|
137
|
+
const click = Object.freeze({
|
|
138
|
+
messageId: message.id,
|
|
139
|
+
interaction: interaction,
|
|
140
|
+
user,
|
|
141
|
+
member: place.member ?? null,
|
|
142
|
+
name,
|
|
143
|
+
data: Object.freeze(values),
|
|
144
|
+
values: Object.freeze([
|
|
145
|
+
...(data.values ?? []),
|
|
146
|
+
]),
|
|
147
|
+
is: (declared) => {
|
|
148
|
+
const state = componentStateOf(declared);
|
|
149
|
+
return state?.kind === component.kind && state.bound?.name === name;
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
if (ctx.waiters.offer('click', click)) {
|
|
153
|
+
return this.#waited(interaction);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (!component.run) {
|
|
157
|
+
// Only waited for, and nothing waits any more.
|
|
158
|
+
return refuse(interaction, 'gone', { what });
|
|
159
|
+
}
|
|
133
160
|
const context = Object.freeze({
|
|
134
161
|
interaction,
|
|
135
162
|
user,
|
|
@@ -160,6 +187,20 @@ export class ComponentRouter {
|
|
|
160
187
|
run: () => component.run(context),
|
|
161
188
|
});
|
|
162
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* A click taken by a wait: the files answer it where they waited. When
|
|
192
|
+
* they did not after 2 seconds, nothing changes on screen
|
|
193
|
+
* (`deferUpdate()`), so the person never sees "This interaction failed".
|
|
194
|
+
*/
|
|
195
|
+
#waited(interaction) {
|
|
196
|
+
const timer = setTimeout(() => {
|
|
197
|
+
if (!interaction.answered && !interaction.deferred) {
|
|
198
|
+
interaction.deferUpdate().catch(() => { });
|
|
199
|
+
}
|
|
200
|
+
}, this.#options.deferAfter ?? 2000);
|
|
201
|
+
timer.unref();
|
|
202
|
+
return Promise.resolve();
|
|
203
|
+
}
|
|
163
204
|
/** What the person picked in a menu, as structures where it applies. */
|
|
164
205
|
#readValues(ctx, raw, type, data) {
|
|
165
206
|
const values = resolveValues(ctx, raw, type, data.values ?? [], data.resolved ?? {});
|
|
@@ -67,8 +67,11 @@ export interface StringSelectConfig<Options extends SelectOptions, Data extends
|
|
|
67
67
|
* shown to what your code receives, or a list of `RichOption`.
|
|
68
68
|
*/
|
|
69
69
|
options: Options;
|
|
70
|
-
/**
|
|
71
|
-
|
|
70
|
+
/**
|
|
71
|
+
* What to do when something is picked. Leave it out for a menu that is
|
|
72
|
+
* only waited for (`message.waitForClick()`).
|
|
73
|
+
*/
|
|
74
|
+
run?: (context: SelectContext<OptionValue<Options>, Data, Where>) => unknown;
|
|
72
75
|
}
|
|
73
76
|
/** A menu of users, roles, users and roles, or channels of the server. */
|
|
74
77
|
export interface EntitySelectConfig<Type extends EntitySelectType, Data extends DataShape = {}, Where extends ComponentWhere = 'guild'> extends SelectConfigBase<Data, Where> {
|
|
@@ -76,8 +79,11 @@ export interface EntitySelectConfig<Type extends EntitySelectType, Data extends
|
|
|
76
79
|
type: Type;
|
|
77
80
|
/** For a menu of channels: the kinds of channels listed. All by default. */
|
|
78
81
|
channelTypes?: readonly ChannelType[];
|
|
79
|
-
/**
|
|
80
|
-
|
|
82
|
+
/**
|
|
83
|
+
* What to do when something is picked. Leave it out for a menu that is
|
|
84
|
+
* only waited for (`message.waitForClick()`).
|
|
85
|
+
*/
|
|
86
|
+
run?: (context: SelectContext<EntityValues[Type], Data, Where>) => unknown;
|
|
81
87
|
}
|
|
82
88
|
/** What a select file gives to `select()`: a menu of texts, or of things of the server. */
|
|
83
89
|
export type SelectConfig<Options extends SelectOptions = SelectOptions, Type extends EntitySelectType = EntitySelectType, Data extends DataShape = {}, Where extends ComponentWhere = 'guild'> = StringSelectConfig<Options, Data, Where> | EntitySelectConfig<Type, Data, Where>;
|
|
@@ -126,7 +132,8 @@ export interface LoadedSelect {
|
|
|
126
132
|
readonly where: ComponentWhere;
|
|
127
133
|
readonly who: 'everyone' | 'author';
|
|
128
134
|
readonly ephemeral: boolean;
|
|
129
|
-
|
|
135
|
+
/** `undefined` for a menu that is only waited for. */
|
|
136
|
+
readonly run: ((context: SelectContext<unknown, DataShape, ComponentWhere>) => unknown) | undefined;
|
|
130
137
|
}
|
|
131
138
|
/** Checks `min` and `max` of a menu, a group of checkboxes or a file upload. */
|
|
132
139
|
export declare function readRange(what: string, given: {
|
package/dist/core/bot.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { Gateway, type GatewayOptions } from '../gateway/gateway.js';
|
|
|
5
5
|
import type { ShardEvent } from '../gateway/shard.js';
|
|
6
6
|
import { type RestOptions } from '../rest/rest.js';
|
|
7
7
|
import type { Context } from '../structures/context.js';
|
|
8
|
+
import { type Waiters } from '../waiting/waiters.js';
|
|
8
9
|
/** What a dispatched event is, besides its data. */
|
|
9
10
|
export interface DispatchInfo {
|
|
10
11
|
shardId: number;
|
|
@@ -71,11 +72,17 @@ export interface BotOptions {
|
|
|
71
72
|
* (an outage of Discord). Default: 15000.
|
|
72
73
|
*/
|
|
73
74
|
guildsTimeout?: number;
|
|
75
|
+
/**
|
|
76
|
+
* What the files wait for (`channel.waitForMessage()`...): in memory by
|
|
77
|
+
* default.
|
|
78
|
+
*/
|
|
79
|
+
waiters?: Waiters;
|
|
74
80
|
/** Only tests change these. */
|
|
75
81
|
rest?: Partial<RestOptions>;
|
|
76
82
|
gateway?: Pick<GatewayOptions, 'backoff' | 'identifyInterval'>;
|
|
77
83
|
voice?: {
|
|
78
84
|
secure?: boolean;
|
|
85
|
+
unavailable?: string;
|
|
79
86
|
};
|
|
80
87
|
}
|
|
81
88
|
export interface Bot {
|
package/dist/core/bot.js
CHANGED
|
@@ -9,6 +9,34 @@ import { createContext } from '../structures/entities.js';
|
|
|
9
9
|
import { GatewayOpcode } from '../discord/codes.js';
|
|
10
10
|
import { GatewayIntent } from '../discord/intents.js';
|
|
11
11
|
import { VoiceManager } from '../voice/connection.js';
|
|
12
|
+
import { MemoryWaiters } from '../waiting/waiters.js';
|
|
13
|
+
import { GetUser } from '../discord/endpoints.js';
|
|
14
|
+
import { toCamelCase } from '../util/case.js';
|
|
15
|
+
/**
|
|
16
|
+
* Gives a reaction to what waits for one, with who reacted: the member
|
|
17
|
+
* Discord sends in a server, else the user the bot knows, else asked.
|
|
18
|
+
* @see https://docs.discord.com/developers/events/gateway-events#message-reaction-add
|
|
19
|
+
*/
|
|
20
|
+
function offerReaction(ctx, data) {
|
|
21
|
+
const member = data.member && data.guild_id
|
|
22
|
+
? ctx.entities.member(data.guild_id, data.member)
|
|
23
|
+
: null;
|
|
24
|
+
const known = member?.user ?? ctx.cache.users.get(data.user_id);
|
|
25
|
+
const user = known
|
|
26
|
+
? Promise.resolve(known)
|
|
27
|
+
: ctx.rest
|
|
28
|
+
.request(GetUser, [data.user_id])
|
|
29
|
+
.then(raw => ctx.entities.user(raw));
|
|
30
|
+
user
|
|
31
|
+
.then(user => ctx.waiters?.offer('reaction', {
|
|
32
|
+
messageId: data.message_id,
|
|
33
|
+
emoji: toCamelCase(data.emoji),
|
|
34
|
+
user,
|
|
35
|
+
member,
|
|
36
|
+
}))
|
|
37
|
+
// Who reacted could not be known: nothing to give.
|
|
38
|
+
.catch(() => { });
|
|
39
|
+
}
|
|
12
40
|
/** The server a gateway event belongs to, when it belongs to one. */
|
|
13
41
|
function guildIdOf(event, data) {
|
|
14
42
|
if (typeof data !== 'object' || data === null)
|
|
@@ -38,6 +66,8 @@ export function createBot(options) {
|
|
|
38
66
|
...options.rest,
|
|
39
67
|
});
|
|
40
68
|
const ctx = createContext({ rest, cache: new Cache(options.cache) });
|
|
69
|
+
const waiters = options.waiters ?? new MemoryWaiters();
|
|
70
|
+
ctx.waiters = waiters;
|
|
41
71
|
/** Servers announced by Ready whose data has not come yet, per shard. */
|
|
42
72
|
const pending = new Map();
|
|
43
73
|
let guildsReady = null;
|
|
@@ -129,6 +159,15 @@ export function createBot(options) {
|
|
|
129
159
|
else if (event === 'VOICE_SERVER_UPDATE') {
|
|
130
160
|
voice.onVoiceServer(data);
|
|
131
161
|
}
|
|
162
|
+
// What the files wait for: built only when a wait wants it.
|
|
163
|
+
if (event === 'MESSAGE_CREATE' && waiters.wants('message')) {
|
|
164
|
+
const message = data;
|
|
165
|
+
waiters.offer('message', ctx.entities.message(message));
|
|
166
|
+
}
|
|
167
|
+
else if (event === 'MESSAGE_REACTION_ADD' &&
|
|
168
|
+
waiters.wants('reaction')) {
|
|
169
|
+
offerReaction(ctx, data);
|
|
170
|
+
}
|
|
132
171
|
options.onDispatch?.(event, data, { shardId, joined, before });
|
|
133
172
|
if (awaited)
|
|
134
173
|
checkGuilds();
|
|
@@ -143,6 +182,9 @@ export function createBot(options) {
|
|
|
143
182
|
...(options.voice?.secure === undefined
|
|
144
183
|
? {}
|
|
145
184
|
: { secure: options.voice.secure }),
|
|
185
|
+
...(options.voice?.unavailable === undefined
|
|
186
|
+
? {}
|
|
187
|
+
: { unavailable: options.voice.unavailable }),
|
|
146
188
|
});
|
|
147
189
|
ctx.voice = voice;
|
|
148
190
|
return {
|
|
@@ -167,6 +209,8 @@ export function createBot(options) {
|
|
|
167
209
|
clearTimeout(guildsTimer);
|
|
168
210
|
guildsTimer = null;
|
|
169
211
|
voice.closeAll();
|
|
212
|
+
// What the files waited for will not come: they go on with nothing.
|
|
213
|
+
waiters.close();
|
|
170
214
|
await gateway.close();
|
|
171
215
|
},
|
|
172
216
|
setPresence: presence => gateway.setPresence(presence),
|
package/dist/discord/api.d.ts
CHANGED
|
@@ -74,6 +74,8 @@ export declare const Limits: {
|
|
|
74
74
|
readonly CustomId: 100;
|
|
75
75
|
/** @see https://docs.discord.com/developers/components/reference#component-object */
|
|
76
76
|
readonly MessageComponents: 40;
|
|
77
|
+
/** @see https://docs.discord.com/developers/components/reference#legacy-message-component-behavior */
|
|
78
|
+
readonly MessageActionRows: 5;
|
|
77
79
|
/** @see https://docs.discord.com/developers/components/reference#action-row */
|
|
78
80
|
readonly ActionRowButtons: 5;
|
|
79
81
|
/** @see https://docs.discord.com/developers/components/reference#button-button-structure */
|
package/dist/discord/api.js
CHANGED
|
@@ -77,6 +77,8 @@ export const Limits = {
|
|
|
77
77
|
CustomId: 100,
|
|
78
78
|
/** @see https://docs.discord.com/developers/components/reference#component-object */
|
|
79
79
|
MessageComponents: 40,
|
|
80
|
+
/** @see https://docs.discord.com/developers/components/reference#legacy-message-component-behavior */
|
|
81
|
+
MessageActionRows: 5,
|
|
80
82
|
/** @see https://docs.discord.com/developers/components/reference#action-row */
|
|
81
83
|
ActionRowButtons: 5,
|
|
82
84
|
/** @see https://docs.discord.com/developers/components/reference#button-button-structure */
|
|
@@ -12,11 +12,11 @@ export declare const ALL_PERMISSIONS: bigint;
|
|
|
12
12
|
*/
|
|
13
13
|
export declare const ELEVATED_PERMISSIONS: readonly PermissionName[];
|
|
14
14
|
/**
|
|
15
|
-
* A
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* @see https://docs.discord.com/developers/topics/permissions
|
|
15
|
+
* A permission as people read it: `BanMembers` is "Ban Members". For what
|
|
16
|
+
* the bot says to the people of a server, never to the developer, who
|
|
17
|
+
* writes the names as they are.
|
|
19
18
|
*/
|
|
19
|
+
export declare function permissionWords(name: PermissionName): string;
|
|
20
20
|
export declare class Permissions {
|
|
21
21
|
/** The permissions as one number, each bit being a permission. */
|
|
22
22
|
readonly bits: bigint;
|
|
@@ -42,6 +42,19 @@ function resolve(permission) {
|
|
|
42
42
|
* regular number.
|
|
43
43
|
* @see https://docs.discord.com/developers/topics/permissions
|
|
44
44
|
*/
|
|
45
|
+
/** Letters that are said one by one in the name of a permission. */
|
|
46
|
+
const ACRONYMS = { Tts: 'TTS', Vad: 'VAD' };
|
|
47
|
+
/**
|
|
48
|
+
* A permission as people read it: `BanMembers` is "Ban Members". For what
|
|
49
|
+
* the bot says to the people of a server, never to the developer, who
|
|
50
|
+
* writes the names as they are.
|
|
51
|
+
*/
|
|
52
|
+
export function permissionWords(name) {
|
|
53
|
+
return name
|
|
54
|
+
.split(/(?=[A-Z])/)
|
|
55
|
+
.map(word => ACRONYMS[word] ?? word)
|
|
56
|
+
.join(' ');
|
|
57
|
+
}
|
|
45
58
|
export class Permissions {
|
|
46
59
|
/** The permissions as one number, each bit being a permission. */
|
|
47
60
|
bits;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CacheLimits } from '../cache/cache.js';
|
|
2
2
|
import type { Snowflake } from '../discord/types/common.js';
|
|
3
3
|
import type { GatewayDispatchEventName, GatewayDispatchEvents } from '../discord/types/gateway-events.js';
|
|
4
|
-
import type {
|
|
4
|
+
import type { DMChannel, GuildChannel, GuildTextBasedChannel, TextBasedChannel, ThreadChannel, VoiceChannel } from '../structures/channel.js';
|
|
5
5
|
import type { Context } from '../structures/context.js';
|
|
6
6
|
import type { GuildEmoji } from '../structures/emoji.js';
|
|
7
7
|
import type { AuditLogEntry, Guild, ScheduledEvent, VoiceState } from '../structures/guild.js';
|
|
@@ -220,23 +220,32 @@ export interface EventContexts {
|
|
|
220
220
|
/** A channel was created. */
|
|
221
221
|
channelCreate: {
|
|
222
222
|
/** The new channel or thread. */
|
|
223
|
-
channel: GuildChannel | ThreadChannel
|
|
224
|
-
/**
|
|
225
|
-
|
|
223
|
+
channel: GuildChannel | ThreadChannel;
|
|
224
|
+
/**
|
|
225
|
+
* The server of the channel: Discord only sends these events, with the
|
|
226
|
+
* Guilds intent, for the channels of a server.
|
|
227
|
+
*/
|
|
228
|
+
guild: Guild;
|
|
226
229
|
};
|
|
227
230
|
/** A channel changed: name, topic, permissions... */
|
|
228
231
|
channelUpdate: {
|
|
229
232
|
/** The channel or thread, as it is now. */
|
|
230
|
-
channel: GuildChannel | ThreadChannel
|
|
231
|
-
/**
|
|
232
|
-
|
|
233
|
+
channel: GuildChannel | ThreadChannel;
|
|
234
|
+
/**
|
|
235
|
+
* The server of the channel: Discord only sends these events, with the
|
|
236
|
+
* Guilds intent, for the channels of a server.
|
|
237
|
+
*/
|
|
238
|
+
guild: Guild;
|
|
233
239
|
};
|
|
234
240
|
/** A channel was deleted. */
|
|
235
241
|
channelDelete: {
|
|
236
242
|
/** The channel or thread as it was. */
|
|
237
|
-
channel: GuildChannel | ThreadChannel
|
|
238
|
-
/**
|
|
239
|
-
|
|
243
|
+
channel: GuildChannel | ThreadChannel;
|
|
244
|
+
/**
|
|
245
|
+
* The server of the channel: Discord only sends these events, with the
|
|
246
|
+
* Guilds intent, for the channels of a server.
|
|
247
|
+
*/
|
|
248
|
+
guild: Guild;
|
|
240
249
|
};
|
|
241
250
|
/** A role was created. */
|
|
242
251
|
roleCreate: {
|
package/dist/events/registry.js
CHANGED
|
@@ -15,16 +15,22 @@ const ofGuild = ({ guild }) => guild;
|
|
|
15
15
|
const channel = (on) => ({
|
|
16
16
|
on,
|
|
17
17
|
build: (ctx, data) => {
|
|
18
|
+
// A server's channel only: what the types promise.
|
|
18
19
|
const found = ctx.cache.channels.get(data.id);
|
|
19
|
-
return found
|
|
20
|
+
return found?.isGuildChannel() || found?.isThread()
|
|
21
|
+
? { channel: found, guild: found.guild }
|
|
22
|
+
: null;
|
|
20
23
|
},
|
|
21
24
|
});
|
|
22
25
|
const deletedChannel = (on) => ({
|
|
23
26
|
on,
|
|
24
27
|
before: (ctx, data) => ctx.cache.channels.get(data.id) ?? null,
|
|
25
|
-
build: (_ctx, _data, { before }) =>
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
build: (_ctx, _data, { before }) => {
|
|
29
|
+
const channel = before;
|
|
30
|
+
return channel?.isGuildChannel() || channel?.isThread()
|
|
31
|
+
? { channel, guild: channel.guild }
|
|
32
|
+
: null;
|
|
33
|
+
},
|
|
28
34
|
});
|
|
29
35
|
/**
|
|
30
36
|
* Discord refused what an event needs before it can be given to its files:
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { command } from './commands/command.js';
|
|
2
|
-
export type { AutocompleteContext, ChannelOption, CommandAutocomplete, CommandConfig, CommandContext, CommandInDm, CommandInGuild, CommandInPrivate, CommandWhere, CommandDeclaration, CommandLocales, CommandName, CommandOption, CommandOptions, CommandTranslation, Cooldown, Duration, NumberOption, OptionsSoFar, OptionTranslation, OptionValuesOf, SimpleOption, StringOption, Suggestion, } from './commands/command.js';
|
|
2
|
+
export type { AutocompleteContext, ChannelOption, CommandAutocomplete, CommandConfig, CommandContext, CommandInDm, CommandInGuild, CommandInPrivate, CommandWhere, CommandDeclaration, CommandLocales, CommandName, CommandOption, CommandOptions, CommandTranslation, Cooldown, MenuConfig, MenuContext, MenuDeclaration, MenuLocales, MenuTranslation, MenuType, Duration, NumberOption, OptionsSoFar, OptionTranslation, OptionValuesOf, SimpleOption, StringOption, Suggestion, } from './commands/command.js';
|
|
3
3
|
export type { CommandInteraction, ComponentInteraction, DmCommandInteraction, GuildCommandInteraction, PrivateCommandInteraction, Interaction, InteractionReply, InteractionReplyOptions, ModalInteraction, } from './structures/interaction.js';
|
|
4
4
|
export type { DynamicText, TextContext } from './components/component.js';
|
|
5
5
|
export { button } from './components/button.js';
|
|
@@ -25,10 +25,11 @@ export type { AuditLog, AuditLogEntry, AutoModerationRule, Ban, ChannelCreateOpt
|
|
|
25
25
|
export type { Invite } from './structures/invite.js';
|
|
26
26
|
export type { BanOptions, GuildMember, MemberEditOptions, } from './structures/member.js';
|
|
27
27
|
export type { Attachment, EmojiInput, DmMessage, GuildMessage, MemberMessage, Message, PrivateMessage, MessageFlagName, Reaction, ReactionEmoji, } from './structures/message.js';
|
|
28
|
+
export type { Click, CollectMessagesOptions, PersonInput, WaitedReaction, WaitForClickOptions, WaitForMessageOptions, WaitForReactionOptions, } from './structures/wait.js';
|
|
28
29
|
export { language } from './messages/messages.js';
|
|
29
30
|
export type { CommandsTranslations, FrameworkTexts, LanguageConfig, LanguageDeclaration, MergedMessages, MessagesOf, MessageText, MessageTexts, PluralText, ProjectCommands, ProjectLanguageDefaults, ParamsArgs, ParamsOf, Placeholders, ProjectLocale, ProjectLocales, LocaleGiven, ProjectMessages, TranslationContext, Translator, } from './messages/messages.js';
|
|
30
31
|
export { presence } from './presence/presence.js';
|
|
31
|
-
export type { Activity, ActivityKind, PlainActivity, PresenceConfig, PresenceDeclaration, PresenceStatusName, StreamingActivity, } from './presence/presence.js';
|
|
32
|
+
export type { Activities, Activity, ActivityKind, ActivityText, PlainActivity, PresenceConfig, PresenceContext, PresenceDeclaration, PresenceStatusName, RotatingPresenceConfig, SinglePresenceConfig, StreamingActivity, } from './presence/presence.js';
|
|
32
33
|
export { store } from './store/store.js';
|
|
33
34
|
export type { ScopedStore, Store, StoreConfig, StoreEntry, StoreKey, StoreScope, StoreSetOptions, StoreStorage, } from './store/store.js';
|
|
34
35
|
export { ranking } from './ranking/ranking.js';
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// (in events, commands...) and never creates them: classes are exported as
|
|
10
10
|
// types only, and nothing here lets user code connect to Discord or send
|
|
11
11
|
// requests by itself.
|
|
12
|
-
//
|
|
12
|
+
// Commands (slash and menu): what the files of src/commands/ are written with.
|
|
13
13
|
export { command } from './commands/command.js';
|
|
14
14
|
export { button } from './components/button.js';
|
|
15
15
|
export { select } from './components/select.js';
|
|
@@ -8,7 +8,7 @@ import { setTimeout as sleep } from 'node:timers/promises';
|
|
|
8
8
|
import { phrase } from '../messages/phrases.js';
|
|
9
9
|
import { DiscordApiError } from '../rest/errors.js';
|
|
10
10
|
import { ctxOf } from '../structures/base.js';
|
|
11
|
-
import { isUpdating } from '../structures/interaction.js';
|
|
11
|
+
import { answersSent, isUpdating, } from '../structures/interaction.js';
|
|
12
12
|
/** Who did it, as Discord sent it: the member in a server, the user elsewhere. */
|
|
13
13
|
export function authorOf(raw) {
|
|
14
14
|
const user = raw.member?.user ?? raw.user;
|
|
@@ -87,7 +87,9 @@ export function runInteraction(options) {
|
|
|
87
87
|
.then(() => options.defer())
|
|
88
88
|
.catch(() => { });
|
|
89
89
|
return new Promise(resolve => resolve(withGuild(options.guildId, options.run)))
|
|
90
|
-
.then(() => {
|
|
90
|
+
.then(async () => {
|
|
91
|
+
// An answer asked without await is on its way: not a missing one.
|
|
92
|
+
await answersSent(interaction);
|
|
91
93
|
if (!interaction.answered) {
|
|
92
94
|
reporter.onWarning(file, `${name} ${options.unanswered}`);
|
|
93
95
|
}
|
package/dist/loader/loader.d.ts
CHANGED
|
@@ -50,6 +50,22 @@ export declare const siteName: (site: {
|
|
|
50
50
|
file: string;
|
|
51
51
|
export?: string;
|
|
52
52
|
}) => string;
|
|
53
|
+
/** What tells one export of the project from another: `src/a.ts#ban`. */
|
|
54
|
+
export declare const siteKey: (site: {
|
|
55
|
+
file: string;
|
|
56
|
+
export: string;
|
|
57
|
+
}) => string;
|
|
58
|
+
/**
|
|
59
|
+
* The order in which declarations that can't both be kept (two with one
|
|
60
|
+
* name) are read, the first one winning: what already ran first (`running`,
|
|
61
|
+
* the `siteKey`s of the declarations of the last load), so that a file saved
|
|
62
|
+
* in dev never takes its name from one that works, then by file and export,
|
|
63
|
+
* so that the choice never depends on the order the files were found in.
|
|
64
|
+
*/
|
|
65
|
+
export declare function bySite<T extends {
|
|
66
|
+
file: string;
|
|
67
|
+
export: string;
|
|
68
|
+
}>(entries: readonly T[], running?: ReadonlySet<string>): T[];
|
|
53
69
|
/** A file of the project, found but not run. */
|
|
54
70
|
export interface FoundFile {
|
|
55
71
|
/** The path from the project folder, with `/`: `src/ping.ts`. */
|
package/dist/loader/loader.js
CHANGED
|
@@ -11,6 +11,28 @@ import { importFile } from './hot.js';
|
|
|
11
11
|
export const siteName = (site) => site.export === undefined || site.export === 'default'
|
|
12
12
|
? site.file
|
|
13
13
|
: `${site.file} (${site.export})`;
|
|
14
|
+
/** What tells one export of the project from another: `src/a.ts#ban`. */
|
|
15
|
+
export const siteKey = (site) => `${site.file}#${site.export}`;
|
|
16
|
+
/**
|
|
17
|
+
* The order in which declarations that can't both be kept (two with one
|
|
18
|
+
* name) are read, the first one winning: what already ran first (`running`,
|
|
19
|
+
* the `siteKey`s of the declarations of the last load), so that a file saved
|
|
20
|
+
* in dev never takes its name from one that works, then by file and export,
|
|
21
|
+
* so that the choice never depends on the order the files were found in.
|
|
22
|
+
*/
|
|
23
|
+
export function bySite(entries, running = new Set()) {
|
|
24
|
+
const before = (entry) => (running.has(siteKey(entry)) ? 0 : 1);
|
|
25
|
+
return [...entries].sort((a, b) => before(a) - before(b) ||
|
|
26
|
+
(a.file === b.file
|
|
27
|
+
? a.export < b.export
|
|
28
|
+
? -1
|
|
29
|
+
: a.export > b.export
|
|
30
|
+
? 1
|
|
31
|
+
: 0
|
|
32
|
+
: a.file < b.file
|
|
33
|
+
? -1
|
|
34
|
+
: 1));
|
|
35
|
+
}
|
|
14
36
|
const SOURCE = /\.(?:ts|mts|js|mjs)$/;
|
|
15
37
|
const DECLARATION = /\.d\.m?ts$/;
|
|
16
38
|
/** Tests next to the code: they are for a test runner, not for the bot. */
|
|
@@ -68,7 +90,7 @@ export async function loadFiles(files) {
|
|
|
68
90
|
result.modules.push({ file, exports: await importFile(path) });
|
|
69
91
|
}
|
|
70
92
|
catch (error) {
|
|
71
|
-
result.failed.push({ file, error });
|
|
93
|
+
result.failed.push({ file, error: readable(error) });
|
|
72
94
|
}
|
|
73
95
|
}));
|
|
74
96
|
const byFile = (a, b) => a.file < b.file ? -1 : 1;
|
|
@@ -76,6 +98,19 @@ export async function loadFiles(files) {
|
|
|
76
98
|
result.failed.sort(byFile);
|
|
77
99
|
return result;
|
|
78
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* A file Node can't read as TypeScript gets the words of its parser
|
|
103
|
+
* ("Expected ',', got ';'"): what they mean is said after them. The error
|
|
104
|
+
* is the same, so its line is still found.
|
|
105
|
+
*/
|
|
106
|
+
function readable(error) {
|
|
107
|
+
if (error instanceof SyntaxError &&
|
|
108
|
+
error.code === 'ERR_INVALID_TYPESCRIPT_SYNTAX' &&
|
|
109
|
+
!error.message.includes('This line can not be read')) {
|
|
110
|
+
error.message = `${error.message.replace(/\.?$/, '.')} This line can not be read: look for a bracket, a comma or a quote missing or in excess, here or just before.`;
|
|
111
|
+
}
|
|
112
|
+
return error;
|
|
113
|
+
}
|
|
79
114
|
/** The name of a file, without its folders and its extension. */
|
|
80
115
|
const baseName = (file) => file
|
|
81
116
|
.split('/')
|
|
@@ -103,22 +138,32 @@ export function readDeclarations(modules, declaration) {
|
|
|
103
138
|
export: exported,
|
|
104
139
|
name: exported === 'default' ? baseName(file) : exported,
|
|
105
140
|
};
|
|
106
|
-
const read = (one) => {
|
|
141
|
+
const read = (one, at) => {
|
|
107
142
|
if (seen.has(one))
|
|
108
143
|
return;
|
|
109
144
|
seen.add(one);
|
|
110
|
-
result.loaded.push({ ...
|
|
145
|
+
result.loaded.push({ ...at, value: declaration.read(one, at) });
|
|
111
146
|
};
|
|
112
147
|
try {
|
|
113
148
|
if (declaration.is(value))
|
|
114
|
-
read(value);
|
|
149
|
+
read(value, site);
|
|
115
150
|
else if (Array.isArray(value) && value.some(declaration.is)) {
|
|
116
151
|
if (!declaration.list) {
|
|
117
152
|
throw new TypeError(`This export is a list with a ${declaration.one} in it: export each ${declaration.one} on its own (export const ${site.name} = ...), its name is the name of its export.`);
|
|
118
153
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
154
|
+
// Each one of the list is told apart by its place in it (`events[2]`),
|
|
155
|
+
// and read on its own: one that is wrong leaves the others running.
|
|
156
|
+
value.forEach((one, index) => {
|
|
157
|
+
if (!declaration.is(one))
|
|
158
|
+
return;
|
|
159
|
+
const at = { ...site, export: `${exported}[${index}]` };
|
|
160
|
+
try {
|
|
161
|
+
read(one, at);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
result.failed.push({ file, export: at.export, error });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
122
167
|
}
|
|
123
168
|
}
|
|
124
169
|
catch (error) {
|