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.
Files changed (115) hide show
  1. package/README.md +5 -3
  2. package/dist/assets/public.js +4 -4
  3. package/dist/cli/build.d.ts +6 -2
  4. package/dist/cli/build.js +17 -4
  5. package/dist/cli/dev.js +37 -6
  6. package/dist/cli/env.d.ts +5 -0
  7. package/dist/cli/env.js +39 -3
  8. package/dist/cli/project.d.ts +61 -9
  9. package/dist/cli/project.js +250 -44
  10. package/dist/cli/redis.d.ts +23 -0
  11. package/dist/cli/redis.js +69 -0
  12. package/dist/cli/start.js +54 -18
  13. package/dist/cli/test.js +112 -18
  14. package/dist/commands/command.d.ts +140 -5
  15. package/dist/commands/command.js +17 -2
  16. package/dist/commands/declaration.d.ts +17 -4
  17. package/dist/commands/declaration.js +128 -2
  18. package/dist/commands/router.js +78 -18
  19. package/dist/commands/tree.d.ts +25 -4
  20. package/dist/commands/tree.js +95 -26
  21. package/dist/commands/twins.d.ts +8 -0
  22. package/dist/commands/twins.js +24 -6
  23. package/dist/components/button.d.ts +8 -3
  24. package/dist/components/declaration.d.ts +2 -2
  25. package/dist/components/declaration.js +16 -12
  26. package/dist/components/render.js +5 -0
  27. package/dist/components/router.js +41 -0
  28. package/dist/components/select.d.ts +12 -5
  29. package/dist/core/bot.d.ts +7 -0
  30. package/dist/core/bot.js +44 -0
  31. package/dist/discord/api.d.ts +2 -0
  32. package/dist/discord/api.js +2 -0
  33. package/dist/discord/permissions.d.ts +4 -4
  34. package/dist/discord/permissions.js +13 -0
  35. package/dist/events/registry.d.ts +265 -243
  36. package/dist/events/registry.js +131 -100
  37. package/dist/index.d.ts +7 -4
  38. package/dist/index.js +2 -1
  39. package/dist/interactions/dispatch.js +4 -2
  40. package/dist/loader/loader.d.ts +16 -0
  41. package/dist/loader/loader.js +52 -7
  42. package/dist/loader/usage.d.ts +15 -0
  43. package/dist/loader/usage.js +40 -0
  44. package/dist/loader/watch.d.ts +5 -3
  45. package/dist/loader/watch.js +88 -17
  46. package/dist/messages/commands.js +3 -0
  47. package/dist/messages/messages.d.ts +1 -0
  48. package/dist/messages/translate.js +5 -1
  49. package/dist/presence/declaration.d.ts +49 -2
  50. package/dist/presence/declaration.js +122 -39
  51. package/dist/presence/presence.d.ts +65 -6
  52. package/dist/presence/presence.js +11 -0
  53. package/dist/presence/rotation.d.ts +32 -0
  54. package/dist/presence/rotation.js +167 -0
  55. package/dist/ranking/declaration.d.ts +12 -0
  56. package/dist/ranking/declaration.js +48 -0
  57. package/dist/ranking/ranking.d.ts +102 -0
  58. package/dist/ranking/ranking.js +129 -0
  59. package/dist/redis/client.d.ts +36 -0
  60. package/dist/redis/client.js +252 -0
  61. package/dist/redis/protocol.d.ts +22 -0
  62. package/dist/redis/protocol.js +99 -0
  63. package/dist/redis/url.d.ts +24 -0
  64. package/dist/redis/url.js +50 -0
  65. package/dist/rest/errors.d.ts +0 -1
  66. package/dist/rest/errors.js +15 -1
  67. package/dist/rest/rest.js +10 -1
  68. package/dist/store/backend.d.ts +49 -4
  69. package/dist/store/backend.js +66 -11
  70. package/dist/store/declaration.d.ts +3 -0
  71. package/dist/store/declaration.js +8 -3
  72. package/dist/store/file-backend.d.ts +3 -2
  73. package/dist/store/file-backend.js +47 -1
  74. package/dist/store/ipc-backend.d.ts +24 -1
  75. package/dist/store/ipc-backend.js +49 -0
  76. package/dist/store/memory-backend.d.ts +4 -2
  77. package/dist/store/memory-backend.js +27 -1
  78. package/dist/store/ranking-backend.d.ts +45 -0
  79. package/dist/store/ranking-backend.js +82 -0
  80. package/dist/store/redis-backend.d.ts +38 -0
  81. package/dist/store/redis-backend.js +245 -0
  82. package/dist/store/redis-ranking.d.ts +15 -0
  83. package/dist/store/redis-ranking.js +69 -0
  84. package/dist/store/scope.d.ts +17 -1
  85. package/dist/store/scope.js +32 -3
  86. package/dist/store/store.d.ts +39 -5
  87. package/dist/store/store.js +79 -32
  88. package/dist/structures/channel.d.ts +13 -0
  89. package/dist/structures/channel.js +24 -0
  90. package/dist/structures/context.d.ts +3 -0
  91. package/dist/structures/entities.js +1 -0
  92. package/dist/structures/interaction.d.ts +6 -0
  93. package/dist/structures/interaction.js +20 -0
  94. package/dist/structures/invite.d.ts +6 -0
  95. package/dist/structures/invite.js +11 -0
  96. package/dist/structures/message.d.ts +17 -0
  97. package/dist/structures/message.js +33 -0
  98. package/dist/structures/wait.d.ts +105 -0
  99. package/dist/structures/wait.js +85 -0
  100. package/dist/testing/harness.d.ts +48 -8
  101. package/dist/testing/harness.js +156 -36
  102. package/dist/testing/index.d.ts +1 -1
  103. package/dist/testing/smoke.js +112 -1
  104. package/dist/testing/world.js +51 -2
  105. package/dist/util/call-site.d.ts +4 -0
  106. package/dist/util/call-site.js +22 -0
  107. package/dist/voice/connection.d.ts +2 -0
  108. package/dist/voice/connection.js +15 -1
  109. package/dist/voice/usage.d.ts +3 -2
  110. package/dist/voice/usage.js +7 -19
  111. package/dist/waiting/usage.d.ts +5 -0
  112. package/dist/waiting/usage.js +19 -0
  113. package/dist/waiting/waiters.d.ts +48 -0
  114. package/dist/waiting/waiters.js +107 -0
  115. 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): {
@@ -220,6 +220,7 @@ export function createContext(input) {
220
220
  self: null,
221
221
  messages: null,
222
222
  voice: null,
223
+ waiters: null,
223
224
  };
224
225
  ctx.entities = new Entities(ctx);
225
226
  return ctx;
@@ -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
- /** Checks that the bot opened a form, this one when given. */
194
- opensForm(form?: ModalDeclaration<ModalFields, never> | TestForm): TestReply;
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
- /** Uses a slash command, as a person would. Resolves once the bot answered. */
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
- /** Forgets the interactions seen so far: a new world starts. */
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
- /** Resolves once the files are done with the interaction `id`. */
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
- /** Resolves once the files are done with the next event that matches. */
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
- /** Resolves once nothing the files started is still running. */
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.