chapterjs 0.3.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 +5 -3
- package/dist/assets/public.js +4 -4
- package/dist/cli/build.d.ts +6 -2
- package/dist/cli/build.js +17 -4
- package/dist/cli/dev.js +37 -6
- package/dist/cli/env.d.ts +5 -0
- package/dist/cli/env.js +39 -3
- package/dist/cli/project.d.ts +61 -9
- package/dist/cli/project.js +250 -44
- package/dist/cli/redis.d.ts +23 -0
- package/dist/cli/redis.js +69 -0
- package/dist/cli/start.js +54 -18
- package/dist/cli/test.js +112 -18
- 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 +265 -243
- package/dist/events/registry.js +131 -100
- package/dist/index.d.ts +7 -4
- package/dist/index.js +2 -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/ranking/declaration.d.ts +12 -0
- package/dist/ranking/declaration.js +48 -0
- package/dist/ranking/ranking.d.ts +102 -0
- package/dist/ranking/ranking.js +129 -0
- package/dist/redis/client.d.ts +36 -0
- package/dist/redis/client.js +252 -0
- package/dist/redis/protocol.d.ts +22 -0
- package/dist/redis/protocol.js +99 -0
- package/dist/redis/url.d.ts +24 -0
- package/dist/redis/url.js +50 -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/backend.d.ts +49 -4
- package/dist/store/backend.js +66 -11
- package/dist/store/declaration.d.ts +3 -0
- package/dist/store/declaration.js +8 -3
- package/dist/store/file-backend.d.ts +3 -2
- package/dist/store/file-backend.js +47 -1
- package/dist/store/ipc-backend.d.ts +24 -1
- package/dist/store/ipc-backend.js +49 -0
- package/dist/store/memory-backend.d.ts +4 -2
- package/dist/store/memory-backend.js +27 -1
- package/dist/store/ranking-backend.d.ts +45 -0
- package/dist/store/ranking-backend.js +82 -0
- package/dist/store/redis-backend.d.ts +38 -0
- package/dist/store/redis-backend.js +245 -0
- package/dist/store/redis-ranking.d.ts +15 -0
- package/dist/store/redis-ranking.js +69 -0
- package/dist/store/scope.d.ts +17 -1
- package/dist/store/scope.js +32 -3
- package/dist/store/store.d.ts +39 -5
- package/dist/store/store.js +79 -32
- 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/invite.d.ts +6 -0
- package/dist/structures/invite.js +11 -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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CollectMessagesOptions, type WaitForMessageOptions } from './wait.js';
|
|
1
2
|
import type { CacheStore } from '../cache/store.js';
|
|
2
3
|
import { Permissions, type PermissionResolvable } from '../discord/permissions.js';
|
|
3
4
|
import { ChannelType, type CreateChannelInviteJSONParams, type ModifyGuildChannelJSONParams, type ModifyThreadJSONParams, type RawChannel, type RawForumTag, type RawThreadMember, type StartThreadWithoutMessageJSONParams } from '../discord/types/channel.js';
|
|
@@ -117,6 +118,18 @@ export interface TextBasedMethods {
|
|
|
117
118
|
* @see https://docs.discord.com/developers/resources/message#get-channel-pins
|
|
118
119
|
*/
|
|
119
120
|
fetchPins(): Promise<Message[]>;
|
|
121
|
+
/**
|
|
122
|
+
* Waits for the next message of the channel that fits: of a person
|
|
123
|
+
* (`from`), saying something (`matches`). Anyone's but a bot's by
|
|
124
|
+
* default. Resolves with the message, or `null` when `within` (one
|
|
125
|
+
* minute by default) passed first. A restart of the bot ends the wait.
|
|
126
|
+
*/
|
|
127
|
+
waitForMessage(options?: WaitForMessageOptions): Promise<Message | null>;
|
|
128
|
+
/**
|
|
129
|
+
* Gathers the messages of the channel that fit, until `within` (one
|
|
130
|
+
* minute by default) passed or `max` came. Resolves with them, in order.
|
|
131
|
+
*/
|
|
132
|
+
collectMessages(options?: CollectMessagesOptions): Promise<Message[]>;
|
|
120
133
|
}
|
|
121
134
|
/** What a role or a member is allowed or denied in one channel. */
|
|
122
135
|
export interface PermissionOverwrite {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { personId, readMatches, readWithin, waitFor, } from './wait.js';
|
|
1
2
|
import { Limits } from '../discord/api.js';
|
|
2
3
|
import { AddThreadMember, BulkDeleteMessages, CreateChannelInvite, CreateWebhook, DeleteChannelPermission, DeleteCloseChannel, DeleteMessage, EditChannelPermissions, FollowAnnouncementChannel, GetChannel, GetChannelInvites, GetChannelMessage, GetChannelMessages, GetChannelPins, GetChannelWebhooks, JoinThread, LeaveThread, ListThreadMembers, ModifyChannel, RemoveThreadMember, SetVoiceChannelStatus, StartThreadInForumOrMediaChannel, StartThreadWithoutMessage, TriggerTypingIndicator, } from '../discord/endpoints.js';
|
|
3
4
|
import { channelMention } from '../discord/formatting.js';
|
|
@@ -180,7 +181,30 @@ const textBasedMethods = {
|
|
|
180
181
|
before = last.pinned_at;
|
|
181
182
|
}
|
|
182
183
|
},
|
|
184
|
+
async waitForMessage(options = {}) {
|
|
185
|
+
const [message] = await waitForMessages(this, options, 1);
|
|
186
|
+
return message ?? null;
|
|
187
|
+
},
|
|
188
|
+
collectMessages(options = {}) {
|
|
189
|
+
const { max } = options;
|
|
190
|
+
if (max !== undefined && (!Number.isInteger(max) || max < 1)) {
|
|
191
|
+
throw new RangeError(`"max" is how many messages to gather at most, a whole number from 1: got ${max}.`);
|
|
192
|
+
}
|
|
193
|
+
return waitForMessages(this, options, max ?? Infinity);
|
|
194
|
+
},
|
|
183
195
|
};
|
|
196
|
+
/**
|
|
197
|
+
* The next messages of a channel that fit the options: what
|
|
198
|
+
* `waitForMessage()` and `collectMessages()` share.
|
|
199
|
+
*/
|
|
200
|
+
function waitForMessages(channel, options, max) {
|
|
201
|
+
const within = readWithin(options.within);
|
|
202
|
+
const from = personId(options.from);
|
|
203
|
+
const says = readMatches(options.matches);
|
|
204
|
+
return waitFor(channel, 'message', message => message.channelId === channel.id &&
|
|
205
|
+
(from ? message.author.id === from : !message.author.bot) &&
|
|
206
|
+
says(message), within, max);
|
|
207
|
+
}
|
|
184
208
|
/**
|
|
185
209
|
* A channel of a server (not a thread).
|
|
186
210
|
* @see https://docs.discord.com/developers/resources/channel#channel-object
|
|
@@ -3,6 +3,7 @@ import type { Snowflake } from '../discord/types/common.js';
|
|
|
3
3
|
import type { LoadedMessages } from '../messages/translate.js';
|
|
4
4
|
import type { Rest } from '../rest/rest.js';
|
|
5
5
|
import type { VoiceManager } from '../voice/connection.js';
|
|
6
|
+
import type { Waiters } from '../waiting/waiters.js';
|
|
6
7
|
import type { Entities } from './entities.js';
|
|
7
8
|
/**
|
|
8
9
|
* What every structure works with. It never leaves the framework: user code
|
|
@@ -22,6 +23,8 @@ export interface Context {
|
|
|
22
23
|
messages: LoadedMessages | null;
|
|
23
24
|
/** Where the bot is in voice; `null` outside a running bot. */
|
|
24
25
|
voice: VoiceManager | null;
|
|
26
|
+
/** What the files are waiting for; `null` outside a running bot. */
|
|
27
|
+
waiters: Waiters | null;
|
|
25
28
|
}
|
|
26
29
|
/** The identity of the bot, for actions that need it. */
|
|
27
30
|
export declare function selfOf(ctx: Context): {
|
|
@@ -13,6 +13,12 @@ import type { User } from './user.js';
|
|
|
13
13
|
export type InteractionData = Omit<RawInteraction, 'user' | 'member' | 'message'>;
|
|
14
14
|
/** Only the framework reads how far an interaction was answered. */
|
|
15
15
|
export declare let isUpdating: (interaction: Interaction) => boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Resolves once every answer asked so far was sent (or refused): what the
|
|
18
|
+
* framework waits for before saying a function never answered, since an
|
|
19
|
+
* answer asked without `await` is still on its way when it returns.
|
|
20
|
+
*/
|
|
21
|
+
export declare let answersSent: (interaction: Interaction) => Promise<void>;
|
|
16
22
|
/** An answer to an interaction: its text, or its options. */
|
|
17
23
|
export type InteractionReply = string | InteractionReplyOptions;
|
|
18
24
|
/** The options of an answer: everything a message takes, except `replyTo` and `stickers`. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { callSite, fromSite } from '../util/call-site.js';
|
|
1
2
|
import { CreateFollowupMessage, CreateInteractionResponse, DeleteOriginalInteractionResponse, EditOriginalInteractionResponse, GetOriginalInteractionResponse, } from '../discord/endpoints.js';
|
|
2
3
|
import { renderedOf } from '../components/instance.js';
|
|
3
4
|
import { InteractionCallbackType, } from '../discord/types/interaction.js';
|
|
@@ -9,6 +10,12 @@ import { resolve } from '../components/instance.js';
|
|
|
9
10
|
import { translatorOf } from '../messages/translate.js';
|
|
10
11
|
/** Only the framework reads how far an interaction was answered. */
|
|
11
12
|
export let isUpdating;
|
|
13
|
+
/**
|
|
14
|
+
* Resolves once every answer asked so far was sent (or refused): what the
|
|
15
|
+
* framework waits for before saying a function never answered, since an
|
|
16
|
+
* answer asked without `await` is still on its way when it returns.
|
|
17
|
+
*/
|
|
18
|
+
export let answersSent;
|
|
12
19
|
/** Answers with a change of the message: see `ComponentInteraction.update()`. */
|
|
13
20
|
let updateMessage;
|
|
14
21
|
let deferUpdateMessage;
|
|
@@ -33,6 +40,14 @@ export class Interaction extends IdStructure {
|
|
|
33
40
|
#queue = Promise.resolve();
|
|
34
41
|
static {
|
|
35
42
|
isUpdating = interaction => interaction.#state === 'updating';
|
|
43
|
+
answersSent = async (interaction) => {
|
|
44
|
+
// Answers asked while waiting are queued after the ones before.
|
|
45
|
+
let last = null;
|
|
46
|
+
while (last !== interaction.#queue) {
|
|
47
|
+
last = interaction.#queue;
|
|
48
|
+
await last;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
36
51
|
updateMessage = (interaction, message) => {
|
|
37
52
|
const { body, files } = buildMessage(message, {
|
|
38
53
|
edit: true,
|
|
@@ -151,6 +166,8 @@ export class Interaction extends IdStructure {
|
|
|
151
166
|
* error keeps the line of user code that asked for the answer.
|
|
152
167
|
*/
|
|
153
168
|
async #run(action) {
|
|
169
|
+
// Where the files asked, for an answer refused later (`callSite`).
|
|
170
|
+
const site = callSite();
|
|
154
171
|
const previous = this.#queue;
|
|
155
172
|
let release;
|
|
156
173
|
this.#queue = new Promise(resolve => (release = resolve));
|
|
@@ -158,6 +175,9 @@ export class Interaction extends IdStructure {
|
|
|
158
175
|
try {
|
|
159
176
|
return await action();
|
|
160
177
|
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
throw fromSite(error, site);
|
|
180
|
+
}
|
|
161
181
|
finally {
|
|
162
182
|
release();
|
|
163
183
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Snowflake } from '../discord/types/common.js';
|
|
2
2
|
import type { RawInvite, RawInviteMetadata } from '../discord/types/invite.js';
|
|
3
3
|
import { Structure } from './base.js';
|
|
4
|
+
import type { GuildChannel } from './channel.js';
|
|
4
5
|
import type { Context } from './context.js';
|
|
5
6
|
import type { User } from './user.js';
|
|
6
7
|
export type InviteData = Omit<RawInvite, 'inviter'> & Partial<RawInviteMetadata>;
|
|
@@ -20,6 +21,11 @@ export declare class Invite extends Structure<InviteData> {
|
|
|
20
21
|
get guildId(): Snowflake | null;
|
|
21
22
|
/** The id of the channel the invite leads to. */
|
|
22
23
|
get channelId(): Snowflake | null;
|
|
24
|
+
/**
|
|
25
|
+
* The channel the invite leads to, when the bot knows it. It never sends
|
|
26
|
+
* a request.
|
|
27
|
+
*/
|
|
28
|
+
get channel(): GuildChannel | null;
|
|
23
29
|
/** When the invite stops working; `null` when it never does. */
|
|
24
30
|
get expiresAt(): Date | null;
|
|
25
31
|
/** How many times the invite was used, when Discord says it. */
|
|
@@ -27,6 +27,17 @@ export class Invite extends Structure {
|
|
|
27
27
|
get channelId() {
|
|
28
28
|
return dataOf(this).channel?.id ?? null;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* The channel the invite leads to, when the bot knows it. It never sends
|
|
32
|
+
* a request.
|
|
33
|
+
*/
|
|
34
|
+
get channel() {
|
|
35
|
+
const id = this.channelId;
|
|
36
|
+
const found = id ? ctxOf(this).cache.channels.get(id) : undefined;
|
|
37
|
+
return found && found.guildId !== null && !found.isThread()
|
|
38
|
+
? found
|
|
39
|
+
: null;
|
|
40
|
+
}
|
|
30
41
|
/** When the invite stops working; `null` when it never does. */
|
|
31
42
|
get expiresAt() {
|
|
32
43
|
return toDate(dataOf(this).expires_at);
|
|
@@ -10,6 +10,7 @@ import type { Guild } from './guild.js';
|
|
|
10
10
|
import type { GuildMember } from './member.js';
|
|
11
11
|
import { type Embed, type MessageEditOptions, type MessageInput } from './payload.js';
|
|
12
12
|
import type { User } from './user.js';
|
|
13
|
+
import { type WaitedReaction, type Click, type WaitForClickOptions, type WaitForReactionOptions } from './wait.js';
|
|
13
14
|
/** What the message stores: users are kept as structures, not copies. */
|
|
14
15
|
export type MessageData = Omit<RawMessage, 'author' | 'mentions'> & {
|
|
15
16
|
guild_id?: Snowflake;
|
|
@@ -148,6 +149,22 @@ export declare class Message extends IdStructure<MessageData> {
|
|
|
148
149
|
* @see https://docs.discord.com/developers/resources/channel#start-thread-from-message
|
|
149
150
|
*/
|
|
150
151
|
startThread(options: Camelize<StartThreadFromMessageJSONParams>, reason?: string): Promise<ThreadChannel>;
|
|
152
|
+
/**
|
|
153
|
+
* Waits for a click on a button or a menu of this message: of a person
|
|
154
|
+
* (`from`), anyone by default. Resolves with the click, to answer with
|
|
155
|
+
* `click.interaction`, or `null` when `within` (one minute by default)
|
|
156
|
+
* passed first. A click taken here does not run the `run` of the button;
|
|
157
|
+
* a button that is only waited for needs none. A restart of the bot ends
|
|
158
|
+
* the wait.
|
|
159
|
+
*/
|
|
160
|
+
waitForClick(options?: WaitForClickOptions): Promise<Click | null>;
|
|
161
|
+
/**
|
|
162
|
+
* Waits for a reaction added to this message: of a person (`from`),
|
|
163
|
+
* anyone's but a bot's by default, with an emoji (`emoji`), any by
|
|
164
|
+
* default. Resolves with it, or `null` when `within` (one minute by
|
|
165
|
+
* default) passed first. A restart of the bot ends the wait.
|
|
166
|
+
*/
|
|
167
|
+
waitForReaction(options?: WaitForReactionOptions): Promise<WaitedReaction | null>;
|
|
151
168
|
}
|
|
152
169
|
/** One mechanism to send a message, used by channels, replies and users. */
|
|
153
170
|
export declare function sendMessage(ctx: Context, channelId: Snowflake, message: MessageInput, guildId?: Snowflake): Promise<Message>;
|
|
@@ -6,6 +6,7 @@ import { ctxOf, dataOf, idOf, IdStructure, toDate } from './base.js';
|
|
|
6
6
|
import { findGuild, knownChannel, knownMember } from './known.js';
|
|
7
7
|
import { buildMessage, } from './payload.js';
|
|
8
8
|
import { translatorOf } from '../messages/translate.js';
|
|
9
|
+
import { personId, readEmoji, readWithin, waitFor, } from './wait.js';
|
|
9
10
|
function emojiPath(emoji) {
|
|
10
11
|
if (typeof emoji !== 'string')
|
|
11
12
|
return reactionEmoji(emoji);
|
|
@@ -251,6 +252,38 @@ export class Message extends IdStructure {
|
|
|
251
252
|
const raw = await rest.request(StartThreadFromMessage, [this.channelId, this.id], { body: toSnakeCase(options), reason });
|
|
252
253
|
return entities.channel(raw);
|
|
253
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* Waits for a click on a button or a menu of this message: of a person
|
|
257
|
+
* (`from`), anyone by default. Resolves with the click, to answer with
|
|
258
|
+
* `click.interaction`, or `null` when `within` (one minute by default)
|
|
259
|
+
* passed first. A click taken here does not run the `run` of the button;
|
|
260
|
+
* a button that is only waited for needs none. A restart of the bot ends
|
|
261
|
+
* the wait.
|
|
262
|
+
*/
|
|
263
|
+
async waitForClick(options = {}) {
|
|
264
|
+
const within = readWithin(options.within);
|
|
265
|
+
const from = personId(options.from);
|
|
266
|
+
const [click] = await waitFor(this, 'click', offered => offered.messageId === this.id && (!from || offered.user.id === from), within);
|
|
267
|
+
return click ?? null;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Waits for a reaction added to this message: of a person (`from`),
|
|
271
|
+
* anyone's but a bot's by default, with an emoji (`emoji`), any by
|
|
272
|
+
* default. Resolves with it, or `null` when `within` (one minute by
|
|
273
|
+
* default) passed first. A restart of the bot ends the wait.
|
|
274
|
+
*/
|
|
275
|
+
async waitForReaction(options = {}) {
|
|
276
|
+
const within = readWithin(options.within);
|
|
277
|
+
const from = personId(options.from);
|
|
278
|
+
const isEmoji = readEmoji(options.emoji);
|
|
279
|
+
const [reaction] = await waitFor(this, 'reaction', offered => offered.messageId === this.id &&
|
|
280
|
+
(from ? offered.user.id === from : !offered.user.bot) &&
|
|
281
|
+
isEmoji(offered.emoji), within);
|
|
282
|
+
if (!reaction)
|
|
283
|
+
return null;
|
|
284
|
+
const { messageId: _, ...rest } = reaction;
|
|
285
|
+
return Object.freeze(rest);
|
|
286
|
+
}
|
|
254
287
|
}
|
|
255
288
|
/** One mechanism to send a message, used by channels, replies and users. */
|
|
256
289
|
export async function sendMessage(ctx, channelId, message, guildId) {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { Snowflake } from '../discord/types/common.js';
|
|
2
|
+
import type { Duration } from '../commands/command.js';
|
|
3
|
+
import type { ButtonDeclaration } from '../components/button.js';
|
|
4
|
+
import type { SelectDeclaration } from '../components/select.js';
|
|
5
|
+
import type { WaitKind } from '../waiting/waiters.js';
|
|
6
|
+
import type { Context } from './context.js';
|
|
7
|
+
import type { ComponentInteraction } from './interaction.js';
|
|
8
|
+
import type { GuildMember } from './member.js';
|
|
9
|
+
import type { Message, ReactionEmoji } from './message.js';
|
|
10
|
+
import type { User } from './user.js';
|
|
11
|
+
/** A person, as a wait takes it: a user, a member, or an id. */
|
|
12
|
+
export type PersonInput = User | GuildMember | Snowflake;
|
|
13
|
+
/** What `channel.waitForMessage()` takes. */
|
|
14
|
+
export interface WaitForMessageOptions {
|
|
15
|
+
/** Only a message of this person. Anyone but a bot by default. */
|
|
16
|
+
from?: PersonInput;
|
|
17
|
+
/**
|
|
18
|
+
* Only a message that says this: a text (the same, whatever the capitals
|
|
19
|
+
* and the spaces around), a regular expression, or a function of the
|
|
20
|
+
* message.
|
|
21
|
+
*/
|
|
22
|
+
matches?: string | RegExp | ((message: Message) => boolean);
|
|
23
|
+
/**
|
|
24
|
+
* How long to wait: `'30s'`, `'2m'`. One minute by default, a second at
|
|
25
|
+
* least, a day at most.
|
|
26
|
+
*/
|
|
27
|
+
within?: Duration;
|
|
28
|
+
}
|
|
29
|
+
/** What `channel.collectMessages()` takes. */
|
|
30
|
+
export interface CollectMessagesOptions extends WaitForMessageOptions {
|
|
31
|
+
/** Stops once this many messages came. No limit by default: until `within`. */
|
|
32
|
+
max?: number;
|
|
33
|
+
}
|
|
34
|
+
/** What `message.waitForClick()` takes. */
|
|
35
|
+
export interface WaitForClickOptions {
|
|
36
|
+
/** Only a click of this person. Anyone by default. */
|
|
37
|
+
from?: PersonInput;
|
|
38
|
+
/** How long to wait: `'30s'`, `'2m'`. One minute by default. */
|
|
39
|
+
within?: Duration;
|
|
40
|
+
}
|
|
41
|
+
/** What `message.waitForReaction()` takes. */
|
|
42
|
+
export interface WaitForReactionOptions {
|
|
43
|
+
/** Only a reaction of this person. Anyone but a bot by default. */
|
|
44
|
+
from?: PersonInput;
|
|
45
|
+
/**
|
|
46
|
+
* Only this emoji: a standard one (`'👍'`), or a custom one by its name,
|
|
47
|
+
* its id or as Discord writes it (`'<:party:123>'`).
|
|
48
|
+
*/
|
|
49
|
+
emoji?: string;
|
|
50
|
+
/** How long to wait: `'30s'`, `'2m'`. One minute by default. */
|
|
51
|
+
within?: Duration;
|
|
52
|
+
}
|
|
53
|
+
/** A click on a button or a menu of a message, as `waitForClick()` gives it. */
|
|
54
|
+
export interface Click {
|
|
55
|
+
/**
|
|
56
|
+
* The click itself: answer it as in the `run` of a button, with
|
|
57
|
+
* `update()`, `reply()` or `deferUpdate()`. Without an answer within 2
|
|
58
|
+
* seconds, the bot changes nothing on screen for you (`deferUpdate()`).
|
|
59
|
+
*/
|
|
60
|
+
readonly interaction: ComponentInteraction;
|
|
61
|
+
/** Who clicked. */
|
|
62
|
+
readonly user: User;
|
|
63
|
+
/** Who clicked, as a member of the server; `null` in a private message. */
|
|
64
|
+
readonly member: GuildMember | null;
|
|
65
|
+
/** The name of the button or menu: the name of its export. */
|
|
66
|
+
readonly name: string;
|
|
67
|
+
/** The data the button or menu carries. */
|
|
68
|
+
readonly data: Readonly<Record<string, string | number | boolean>>;
|
|
69
|
+
/**
|
|
70
|
+
* What was picked in a menu: the values of a menu of texts, the ids of
|
|
71
|
+
* the people, roles or channels of the others. Empty for a button.
|
|
72
|
+
*/
|
|
73
|
+
readonly values: readonly string[];
|
|
74
|
+
/** Whether it is this button or this menu. */
|
|
75
|
+
is(component: ButtonDeclaration<never> | SelectDeclaration<never>): boolean;
|
|
76
|
+
}
|
|
77
|
+
/** A reaction added to a message, as `waitForReaction()` gives it. */
|
|
78
|
+
export interface WaitedReaction {
|
|
79
|
+
/** The emoji. */
|
|
80
|
+
readonly emoji: ReactionEmoji;
|
|
81
|
+
/** Who reacted. */
|
|
82
|
+
readonly user: User;
|
|
83
|
+
/** Who reacted, as a member of the server; `null` in a private message. */
|
|
84
|
+
readonly member: GuildMember | null;
|
|
85
|
+
}
|
|
86
|
+
/** What the bot offers the waits for a click: `components/router.ts` builds it. */
|
|
87
|
+
export interface OfferedClick extends Click {
|
|
88
|
+
readonly messageId: Snowflake;
|
|
89
|
+
}
|
|
90
|
+
/** What the bot offers the waits for a reaction: `core/bot.ts` builds it. */
|
|
91
|
+
export interface OfferedReaction extends WaitedReaction {
|
|
92
|
+
readonly messageId: Snowflake;
|
|
93
|
+
}
|
|
94
|
+
/** The milliseconds of `within`, checked. */
|
|
95
|
+
export declare function readWithin(within: unknown): number;
|
|
96
|
+
/** The id of a person a wait is given, checked. */
|
|
97
|
+
export declare function personId(from: unknown): Snowflake | undefined;
|
|
98
|
+
/** Whether a message says what `matches` asks. */
|
|
99
|
+
export declare function readMatches(matches: unknown): (message: Message) => boolean;
|
|
100
|
+
/** Whether an emoji is the one `emoji` names. */
|
|
101
|
+
export declare function readEmoji(emoji: unknown): (given: ReactionEmoji) => boolean;
|
|
102
|
+
/** The waits of the bot, or why there are none. */
|
|
103
|
+
export declare function waitersOf(structure: object): NonNullable<Context['waiters']>;
|
|
104
|
+
/** Waits for what fits, `max` at most: what was taken, in order. */
|
|
105
|
+
export declare function waitFor<T>(structure: object, kind: WaitKind, match: (offered: T) => boolean, within: number, max?: number): Promise<T[]>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// What a structure waits for: the options of `channel.waitForMessage()`,
|
|
2
|
+
// `channel.collectMessages()`, `message.waitForClick()` and
|
|
3
|
+
// `message.waitForReaction()`, checked, and turned into the test a wait of
|
|
4
|
+
// `waiting/` runs on what the bot offers it.
|
|
5
|
+
import { DURATION_EXAMPLE, parseDuration } from '../util/duration.js';
|
|
6
|
+
import { ctxOf } from './base.js';
|
|
7
|
+
/** One minute: how long a wait lasts when the file does not say. */
|
|
8
|
+
const DEFAULT_WITHIN = 60_000;
|
|
9
|
+
const MIN_WITHIN = 1_000;
|
|
10
|
+
/** A day: longer would be lost by any restart anyway. */
|
|
11
|
+
const MAX_WITHIN = 86_400_000;
|
|
12
|
+
/** The milliseconds of `within`, checked. */
|
|
13
|
+
export function readWithin(within) {
|
|
14
|
+
if (within === undefined)
|
|
15
|
+
return DEFAULT_WITHIN;
|
|
16
|
+
const ms = typeof within === 'string' ? parseDuration(within) : null;
|
|
17
|
+
if (ms === null) {
|
|
18
|
+
throw new TypeError(`"within" is how long to wait, a duration (${DURATION_EXAMPLE}): got ${JSON.stringify(within) ?? typeof within}.`);
|
|
19
|
+
}
|
|
20
|
+
if (ms < MIN_WITHIN || ms > MAX_WITHIN) {
|
|
21
|
+
throw new RangeError(`"within" is ${JSON.stringify(within)}: a wait lasts between 1 second and 1 day.`);
|
|
22
|
+
}
|
|
23
|
+
return ms;
|
|
24
|
+
}
|
|
25
|
+
/** The id of a person a wait is given, checked. */
|
|
26
|
+
export function personId(from) {
|
|
27
|
+
if (from === undefined)
|
|
28
|
+
return undefined;
|
|
29
|
+
if (typeof from === 'string' && /^\d+$/.test(from))
|
|
30
|
+
return from;
|
|
31
|
+
if (typeof from === 'object' &&
|
|
32
|
+
from !== null &&
|
|
33
|
+
typeof from.id === 'string') {
|
|
34
|
+
return from.id;
|
|
35
|
+
}
|
|
36
|
+
throw new TypeError(`"from" is the person to wait for: a user, a member or their id, got ${JSON.stringify(from) ?? typeof from}.`);
|
|
37
|
+
}
|
|
38
|
+
/** Whether a message says what `matches` asks. */
|
|
39
|
+
export function readMatches(matches) {
|
|
40
|
+
if (matches === undefined)
|
|
41
|
+
return () => true;
|
|
42
|
+
if (typeof matches === 'string') {
|
|
43
|
+
const wanted = matches.trim().toLowerCase();
|
|
44
|
+
return message => message.content.trim().toLowerCase() === wanted;
|
|
45
|
+
}
|
|
46
|
+
if (matches instanceof RegExp) {
|
|
47
|
+
// A global or sticky expression keeps where it stopped: test from the start.
|
|
48
|
+
return message => {
|
|
49
|
+
matches.lastIndex = 0;
|
|
50
|
+
return matches.test(message.content);
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (typeof matches === 'function') {
|
|
54
|
+
return message => matches(message) === true;
|
|
55
|
+
}
|
|
56
|
+
throw new TypeError(`"matches" is a text, a regular expression or a function of the message, like matches: 'paris' or matches: /^paris$/i.`);
|
|
57
|
+
}
|
|
58
|
+
/** Whether an emoji is the one `emoji` names. */
|
|
59
|
+
export function readEmoji(emoji) {
|
|
60
|
+
if (emoji === undefined)
|
|
61
|
+
return () => true;
|
|
62
|
+
if (typeof emoji !== 'string' || emoji.trim() === '') {
|
|
63
|
+
throw new TypeError(`"emoji" is the emoji to wait for: '👍', or a custom one by its name, its id or as '<:name:id>'.`);
|
|
64
|
+
}
|
|
65
|
+
const custom = /^<a?:(\w+):(\d+)>$/.exec(emoji.trim());
|
|
66
|
+
const wanted = custom ? custom[2] : emoji.trim();
|
|
67
|
+
return given => given.id === wanted || given.name === wanted;
|
|
68
|
+
}
|
|
69
|
+
/** The waits of the bot, or why there are none. */
|
|
70
|
+
export function waitersOf(structure) {
|
|
71
|
+
const { waiters } = ctxOf(structure);
|
|
72
|
+
if (!waiters) {
|
|
73
|
+
throw new Error('Waiting for what comes next only works in a running bot: in a command, an event, a component or a task.');
|
|
74
|
+
}
|
|
75
|
+
return waiters;
|
|
76
|
+
}
|
|
77
|
+
/** Waits for what fits, `max` at most: what was taken, in order. */
|
|
78
|
+
export function waitFor(structure, kind, match, within, max = 1) {
|
|
79
|
+
return waitersOf(structure).wait({
|
|
80
|
+
kind,
|
|
81
|
+
match: offered => match(offered),
|
|
82
|
+
max,
|
|
83
|
+
within,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { PermissionName } from '../discord/permissions.js';
|
|
2
2
|
import type { Locale } from '../discord/types/common.js';
|
|
3
3
|
import { type RawEmbed } from '../discord/types/message.js';
|
|
4
|
-
import type { CommandDeclaration, CommandOption, CommandOptions, CommandWhere } from '../commands/command.js';
|
|
4
|
+
import type { CommandDeclaration, CommandOption, CommandOptions, CommandWhere, MenuDeclaration, MenuType } from '../commands/command.js';
|
|
5
5
|
import type { ButtonDeclaration } from '../components/button.js';
|
|
6
6
|
import { type ButtonComponent, type ModalComponent, type SelectComponent } from '../components/instance.js';
|
|
7
7
|
import type { CheckboxesField, CheckboxField, EntityField, FilesField, ModalDeclaration, ModalField, ModalFields, NoteField, RadioField, SelectField, TextField } from '../components/modal.js';
|
|
8
|
+
import type { DataShape } from '../components/custom-id.js';
|
|
8
9
|
import type { OptionValue } from '../components/options.js';
|
|
9
10
|
import type { SelectDeclaration } from '../components/select.js';
|
|
10
11
|
import type { Bot } from '../core/bot.js';
|
|
@@ -190,8 +191,12 @@ export interface TestReply extends TestAssertions<TestReply> {
|
|
|
190
191
|
isDeferred(): TestReply;
|
|
191
192
|
/** Checks that the bot changed the message of the button or menu. */
|
|
192
193
|
updatesMessage(): TestReply;
|
|
193
|
-
/**
|
|
194
|
-
|
|
194
|
+
/**
|
|
195
|
+
* Checks that the bot opened a form, this one when given: its export
|
|
196
|
+
* (whatever data it carries), the form called with its data, or one found
|
|
197
|
+
* on an answer.
|
|
198
|
+
*/
|
|
199
|
+
opensForm<Fields extends ModalFields, Data extends DataShape>(form?: ModalDeclaration<Fields, Data> | ModalComponent | TestForm): TestReply;
|
|
195
200
|
/** Checks that the bot answered with nothing visible (a deferred update). */
|
|
196
201
|
changesNothing(): TestReply;
|
|
197
202
|
/** The button with this label, to click it. Fails when there is none. */
|
|
@@ -258,6 +263,14 @@ export type SelectInput = string | TestMember | TestRole | TestChannel;
|
|
|
258
263
|
export type ButtonTarget = ButtonDeclaration<never> | ButtonComponent | TestButton;
|
|
259
264
|
export type SelectTarget = SelectDeclaration<never> | SelectComponent | TestMenu;
|
|
260
265
|
export type FormTarget<Fields extends ModalFields = ModalFields> = ModalDeclaration<Fields, never> | ModalComponent | TestForm;
|
|
266
|
+
/**
|
|
267
|
+
* What a menu command is used on in a test: a person for a `'user'` menu,
|
|
268
|
+
* a message for a `'message'` menu.
|
|
269
|
+
*/
|
|
270
|
+
export type TestMenuTarget<Type extends MenuType = MenuType> = {
|
|
271
|
+
/** The person or the message the menu is used on. */
|
|
272
|
+
readonly on: Type extends 'user' ? TestMember : TestMessage;
|
|
273
|
+
};
|
|
261
274
|
/** The arguments of `bot.use()`: the options may be left out when none is required. */
|
|
262
275
|
type UseArgs<Options extends CommandOptions> = {} extends OptionInputsOf<Options> ? [options?: OptionInputsOf<Options>, where?: TestWhere] : [options: OptionInputsOf<Options>, where?: TestWhere];
|
|
263
276
|
/**
|
|
@@ -291,7 +304,13 @@ export interface TestBot {
|
|
|
291
304
|
role(name: string, options?: TestRoleOptions): TestRole;
|
|
292
305
|
/** A channel of the server, made the first time the name is given. `#general` exists. */
|
|
293
306
|
channel(name: string, options?: TestChannelOptions): TestChannel;
|
|
294
|
-
/**
|
|
307
|
+
/**
|
|
308
|
+
* Uses a command, as a person would: a slash command with its options,
|
|
309
|
+
* or a menu command on what it is used on (`{ on: message }`,
|
|
310
|
+
* `{ on: member }`; a message menu is used in the channel of its
|
|
311
|
+
* message). Resolves once the bot answered.
|
|
312
|
+
*/
|
|
313
|
+
use<Type extends MenuType, Where extends CommandWhere>(command: MenuDeclaration<Type, Where>, target: TestMenuTarget<Type>, where?: TestWhere): Promise<TestReply>;
|
|
295
314
|
use<Options extends CommandOptions, Where extends CommandWhere>(command: CommandDeclaration<Options, Where>, ...args: UseArgs<Options>): Promise<TestReply>;
|
|
296
315
|
/** Clicks a button. Resolves once the bot answered. */
|
|
297
316
|
click(button: ButtonTarget, where?: TestWhere): Promise<TestReply>;
|
|
@@ -311,6 +330,12 @@ export interface TestBot {
|
|
|
311
330
|
leave(member: TestMember): Promise<void>;
|
|
312
331
|
/** Runs a task now, as if its time had come. Resolves once it is done. */
|
|
313
332
|
run(task: TaskDeclaration, at?: Date): Promise<void>;
|
|
333
|
+
/**
|
|
334
|
+
* Ends everything the bot waits for (`waitForMessage()`,
|
|
335
|
+
* `waitForClick()`...) as if its time was up: time does not pass in the
|
|
336
|
+
* test server. Resolves once the bot handled it.
|
|
337
|
+
*/
|
|
338
|
+
endWaits(): Promise<void>;
|
|
314
339
|
/** Waits for everything the bot started to be done. */
|
|
315
340
|
settle(): Promise<void>;
|
|
316
341
|
}
|
|
@@ -329,13 +354,26 @@ export declare function captureLog(): {
|
|
|
329
354
|
export declare class Observer {
|
|
330
355
|
#private;
|
|
331
356
|
readonly observe: ProjectObserver;
|
|
332
|
-
/**
|
|
357
|
+
/**
|
|
358
|
+
* Forgets everything of the world before: a new world starts. What the
|
|
359
|
+
* files of the last one still run (a case given up on) is never waited
|
|
360
|
+
* for again.
|
|
361
|
+
*/
|
|
333
362
|
reset(): void;
|
|
334
|
-
/**
|
|
363
|
+
/**
|
|
364
|
+
* Resolves once the files were given the interaction `id`: `settle()`
|
|
365
|
+
* then waits for them to be done with it.
|
|
366
|
+
*/
|
|
335
367
|
interaction(id: string, timeout: number): Promise<void>;
|
|
336
|
-
/**
|
|
368
|
+
/**
|
|
369
|
+
* Resolves once the files were given the next event that matches:
|
|
370
|
+
* `settle()` then waits for them to be done with it.
|
|
371
|
+
*/
|
|
337
372
|
event(match: (event: string, data: unknown) => boolean, timeout: number): Promise<void>;
|
|
338
|
-
/**
|
|
373
|
+
/**
|
|
374
|
+
* Resolves once nothing the files started is still running, but what
|
|
375
|
+
* waits for the test to act (a run waiting for a message, a click...).
|
|
376
|
+
*/
|
|
339
377
|
settle(): Promise<void>;
|
|
340
378
|
}
|
|
341
379
|
/** The bot in its world, with the handles a test works with. */
|
|
@@ -362,6 +400,7 @@ export declare class Session implements TestBot {
|
|
|
362
400
|
member(name: string, options?: TestMemberOptions): TestMember;
|
|
363
401
|
role(name: string, options?: TestRoleOptions): TestRole;
|
|
364
402
|
channel(name: string, options?: TestChannelOptions): TestChannel;
|
|
403
|
+
use<Type extends MenuType, Where extends CommandWhere>(command: MenuDeclaration<Type, Where>, target: TestMenuTarget<Type>, where?: TestWhere): Promise<TestReply>;
|
|
365
404
|
use<Options extends CommandOptions, Where extends CommandWhere>(command: CommandDeclaration<Options, Where>, ...args: UseArgs<Options>): Promise<TestReply>;
|
|
366
405
|
suggest<Options extends CommandOptions, Where extends CommandWhere>(command: CommandDeclaration<Options, Where>, option: keyof Options & string, typed: string, where?: TestWhere): Promise<TestSuggestion[]>;
|
|
367
406
|
click(button: ButtonTarget, where?: TestWhere): Promise<TestReply>;
|
|
@@ -373,6 +412,7 @@ export declare class Session implements TestBot {
|
|
|
373
412
|
leave(member: TestMember): Promise<void>;
|
|
374
413
|
run(task: TaskDeclaration, at?: Date): Promise<void>;
|
|
375
414
|
settle(): Promise<void>;
|
|
415
|
+
endWaits(): Promise<void>;
|
|
376
416
|
/**
|
|
377
417
|
* Sends a gateway event to the bot and waits for the files to be done
|
|
378
418
|
* with it. For the checks of `chapterjs test`, not for scenarios.
|