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
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { RawGatewayPresenceUpdate } from '../discord/types/gateway-events.js';
|
|
2
|
+
import { type LoadedActivity, type LoadedPresence } from './declaration.js';
|
|
3
|
+
import type { PresenceContext } from './presence.js';
|
|
4
|
+
export interface RotationOptions {
|
|
5
|
+
/** Sends a presence to the bot of the moment. */
|
|
6
|
+
apply(raw: RawGatewayPresenceUpdate): void;
|
|
7
|
+
/** What the functions receive, or `null` while there is no bot to show it. */
|
|
8
|
+
context(): PresenceContext | null;
|
|
9
|
+
/** A function of the file threw, or gave something wrong. Said once until it changes. */
|
|
10
|
+
onError(error: unknown): void;
|
|
11
|
+
/** The clock, in milliseconds (tests). */
|
|
12
|
+
now?: () => number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Which activity of a list the clock says to show, and how long until the
|
|
16
|
+
* next one.
|
|
17
|
+
*/
|
|
18
|
+
export declare function activityAt(activities: readonly LoadedActivity[], now: number): {
|
|
19
|
+
index: number;
|
|
20
|
+
left: number;
|
|
21
|
+
};
|
|
22
|
+
/** The presence of the project, shown in turn while it is started. */
|
|
23
|
+
export declare class PresenceRotation {
|
|
24
|
+
#private;
|
|
25
|
+
constructor(options: RotationOptions);
|
|
26
|
+
/** Changes the presence (a load, a reload): shown at once when started. */
|
|
27
|
+
set(presence: LoadedPresence | null): void;
|
|
28
|
+
/** Starts showing the presence. */
|
|
29
|
+
start(): void;
|
|
30
|
+
/** Stops: nothing runs after this. */
|
|
31
|
+
stop(): void;
|
|
32
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Shows the activities of the presence in turn, each for its `for`, and
|
|
2
|
+
// runs the functions of the file when their activity comes: the text of an
|
|
3
|
+
// activity, or the list itself.
|
|
4
|
+
//
|
|
5
|
+
// Which activity is shown is read from the clock: the list is a cycle
|
|
6
|
+
// starting at the epoch, so every process of a large bot shows the same
|
|
7
|
+
// activity at the same time, with nothing to coordinate.
|
|
8
|
+
import { checkText, DEFAULT_FOR, DEFAULT_PRESENCE, readActivities, toRaw, } from './declaration.js';
|
|
9
|
+
/** How long to wait for a bot before trying again. */
|
|
10
|
+
const NO_BOT_RETRY = 1_000;
|
|
11
|
+
/**
|
|
12
|
+
* Which activity of a list the clock says to show, and how long until the
|
|
13
|
+
* next one.
|
|
14
|
+
*/
|
|
15
|
+
export function activityAt(activities, now) {
|
|
16
|
+
const cycle = activities.reduce((sum, one) => sum + one.for, 0);
|
|
17
|
+
let position = now % cycle;
|
|
18
|
+
for (const [index, activity] of activities.entries()) {
|
|
19
|
+
if (position < activity.for) {
|
|
20
|
+
return { index, left: activity.for - position };
|
|
21
|
+
}
|
|
22
|
+
position -= activity.for;
|
|
23
|
+
}
|
|
24
|
+
// Not reached: the position is below the cycle.
|
|
25
|
+
return { index: 0, left: activities[0].for };
|
|
26
|
+
}
|
|
27
|
+
/** The presence of the project, shown in turn while it is started. */
|
|
28
|
+
export class PresenceRotation {
|
|
29
|
+
#options;
|
|
30
|
+
#now;
|
|
31
|
+
#presence = null;
|
|
32
|
+
#started = false;
|
|
33
|
+
#timer;
|
|
34
|
+
/** Each tick has its number: one started since then makes it give up. */
|
|
35
|
+
#generation = 0;
|
|
36
|
+
/** The list a function gave last, and until when it is used. */
|
|
37
|
+
#list = null;
|
|
38
|
+
#listUntil = 0;
|
|
39
|
+
/** The last text of each activity a function gave, by its place. */
|
|
40
|
+
#texts = new Map();
|
|
41
|
+
/** What was said last of each function (`list`, or the place of an activity). */
|
|
42
|
+
#problems = new Map();
|
|
43
|
+
constructor(options) {
|
|
44
|
+
this.#options = options;
|
|
45
|
+
this.#now = options.now ?? Date.now;
|
|
46
|
+
}
|
|
47
|
+
/** Changes the presence (a load, a reload): shown at once when started. */
|
|
48
|
+
set(presence) {
|
|
49
|
+
this.#presence = presence;
|
|
50
|
+
this.#list = null;
|
|
51
|
+
this.#texts.clear();
|
|
52
|
+
this.#problems.clear();
|
|
53
|
+
if (this.#started)
|
|
54
|
+
void this.#tick();
|
|
55
|
+
}
|
|
56
|
+
/** Starts showing the presence. */
|
|
57
|
+
start() {
|
|
58
|
+
this.#started = true;
|
|
59
|
+
void this.#tick();
|
|
60
|
+
}
|
|
61
|
+
/** Stops: nothing runs after this. */
|
|
62
|
+
stop() {
|
|
63
|
+
this.#started = false;
|
|
64
|
+
this.#generation++;
|
|
65
|
+
clearTimeout(this.#timer);
|
|
66
|
+
}
|
|
67
|
+
/** What is shown right now, as the gateway sends it: one step of the cycle. */
|
|
68
|
+
async #tick() {
|
|
69
|
+
const generation = ++this.#generation;
|
|
70
|
+
clearTimeout(this.#timer);
|
|
71
|
+
const current = () => this.#started && generation === this.#generation;
|
|
72
|
+
const presence = this.#presence;
|
|
73
|
+
if (!presence) {
|
|
74
|
+
this.#options.apply(DEFAULT_PRESENCE);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const context = this.#options.context();
|
|
78
|
+
if (!context) {
|
|
79
|
+
this.#later(NO_BOT_RETRY);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
let activities;
|
|
83
|
+
if (typeof presence.activities === 'function') {
|
|
84
|
+
if (this.#list === null || this.#now() >= this.#listUntil) {
|
|
85
|
+
try {
|
|
86
|
+
const given = await presence.activities(context);
|
|
87
|
+
if (!current())
|
|
88
|
+
return;
|
|
89
|
+
this.#list = readActivities(given);
|
|
90
|
+
this.#texts.clear();
|
|
91
|
+
this.#problems.delete('list');
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (!current())
|
|
95
|
+
return;
|
|
96
|
+
this.#problem('list', error);
|
|
97
|
+
// The last good list stays; none at all shows the status alone.
|
|
98
|
+
this.#list ??= [];
|
|
99
|
+
}
|
|
100
|
+
const cycle = this.#list.reduce((sum, one) => sum + one.for, 0);
|
|
101
|
+
this.#listUntil = this.#now() + (cycle || DEFAULT_FOR);
|
|
102
|
+
}
|
|
103
|
+
activities = this.#list;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
activities = presence.activities;
|
|
107
|
+
}
|
|
108
|
+
if (activities.length === 0) {
|
|
109
|
+
this.#options.apply(toRaw(presence.status, null, ''));
|
|
110
|
+
// A function may give activities later.
|
|
111
|
+
if (typeof presence.activities === 'function') {
|
|
112
|
+
this.#later(this.#listUntil - this.#now());
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const { index, left } = activityAt(activities, this.#now());
|
|
117
|
+
const activity = activities[index];
|
|
118
|
+
let text;
|
|
119
|
+
if (typeof activity.name === 'function') {
|
|
120
|
+
try {
|
|
121
|
+
text = checkText(await activity.name(context));
|
|
122
|
+
if (!current())
|
|
123
|
+
return;
|
|
124
|
+
this.#texts.set(index, text);
|
|
125
|
+
this.#problems.delete(`name ${index}`);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (!current())
|
|
129
|
+
return;
|
|
130
|
+
this.#problem(`name ${index}`, error);
|
|
131
|
+
text = this.#texts.get(index);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
text = activity.name;
|
|
136
|
+
}
|
|
137
|
+
if (text !== undefined) {
|
|
138
|
+
this.#options.apply(toRaw(presence.status, activity, text));
|
|
139
|
+
}
|
|
140
|
+
const moves = activities.length > 1 ||
|
|
141
|
+
typeof activity.name === 'function' ||
|
|
142
|
+
typeof presence.activities === 'function';
|
|
143
|
+
if (moves) {
|
|
144
|
+
// A list that ends before this activity does is asked again then.
|
|
145
|
+
const until = typeof presence.activities === 'function'
|
|
146
|
+
? Math.min(left, Math.max(0, this.#listUntil - this.#now()))
|
|
147
|
+
: left;
|
|
148
|
+
this.#later(until);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
#later(ms) {
|
|
152
|
+
if (!this.#started)
|
|
153
|
+
return;
|
|
154
|
+
// At least 1 ms: a list asked again on the very boundary.
|
|
155
|
+
this.#timer = setTimeout(() => void this.#tick(), Math.max(1, ms));
|
|
156
|
+
// Nothing of the bot waits for it: the gateway keeps the process up.
|
|
157
|
+
this.#timer.unref();
|
|
158
|
+
}
|
|
159
|
+
/** Says what went wrong with a function, once until it changes. */
|
|
160
|
+
#problem(what, error) {
|
|
161
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
162
|
+
if (this.#problems.get(what) === message)
|
|
163
|
+
return;
|
|
164
|
+
this.#problems.set(what, message);
|
|
165
|
+
this.#options.onError(error);
|
|
166
|
+
}
|
|
167
|
+
}
|
package/dist/rest/errors.d.ts
CHANGED
|
@@ -15,7 +15,6 @@ export interface DiscordFieldError {
|
|
|
15
15
|
* @see https://docs.discord.com/developers/reference#error-messages
|
|
16
16
|
*/
|
|
17
17
|
export declare function flattenFieldErrors(errors: unknown, path?: string): DiscordFieldError[];
|
|
18
|
-
/** Discord answered a request with an error. */
|
|
19
18
|
export declare class DiscordApiError extends Error {
|
|
20
19
|
readonly name = "DiscordApiError";
|
|
21
20
|
/** The HTTP status of the response (403, 404...). */
|
package/dist/rest/errors.js
CHANGED
|
@@ -32,6 +32,19 @@ export function flattenFieldErrors(errors, path = '') {
|
|
|
32
32
|
return found;
|
|
33
33
|
}
|
|
34
34
|
/** Discord answered a request with an error. */
|
|
35
|
+
/**
|
|
36
|
+
* What a refusal means, in plain words, for the codes a bot meets most: the
|
|
37
|
+
* text of Discord names the problem, not what to do about it.
|
|
38
|
+
* @see https://docs.discord.com/developers/topics/opcodes-and-status-codes#json-json-error-codes
|
|
39
|
+
*/
|
|
40
|
+
const HINTS = {
|
|
41
|
+
50013: "The bot is missing a permission for this, or acts on someone above it: the owner of the server, or a member or role higher than the bot's highest role.",
|
|
42
|
+
50001: 'The bot can not see this channel or server: give it View Channel there, or check that it is still in the server.',
|
|
43
|
+
10008: 'The message does not exist any more: it was deleted.',
|
|
44
|
+
10003: 'The channel does not exist any more: it was deleted.',
|
|
45
|
+
10007: 'This person is not a member of the server any more.',
|
|
46
|
+
50007: 'This person does not accept private messages from the bot: they closed them, or share no server with it.',
|
|
47
|
+
};
|
|
35
48
|
export class DiscordApiError extends Error {
|
|
36
49
|
name = 'DiscordApiError';
|
|
37
50
|
/** The HTTP status of the response (403, 404...). */
|
|
@@ -62,7 +75,8 @@ export class DiscordApiError extends Error {
|
|
|
62
75
|
const details = fieldErrors
|
|
63
76
|
.map(error => `\n ${error.path || 'request'}: ${error.message}`)
|
|
64
77
|
.join('');
|
|
65
|
-
|
|
78
|
+
const hint = HINTS[code] ? ` ${HINTS[code]}` : '';
|
|
79
|
+
super(`Discord refused ${input.method} ${input.route}: ${reason} (${code || input.status}).${hint}${details}`);
|
|
66
80
|
this.status = input.status;
|
|
67
81
|
this.code = code;
|
|
68
82
|
this.fieldErrors = fieldErrors;
|
package/dist/rest/rest.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// The only place that sends HTTP requests to Discord.
|
|
2
2
|
// https://docs.discord.com/developers/reference#http-api
|
|
3
|
+
import { callSite, fromSite } from '../util/call-site.js';
|
|
3
4
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
4
5
|
import { API_BASE_URL, API_VERSION, AUDIT_LOG_REASON_MAX_LENGTH, userAgent, } from '../discord/api.js';
|
|
5
6
|
import { HttpStatus } from '../discord/codes.js';
|
|
@@ -68,7 +69,15 @@ export class RestClient {
|
|
|
68
69
|
onWait: options.onRateLimit,
|
|
69
70
|
});
|
|
70
71
|
}
|
|
71
|
-
|
|
72
|
+
request(endpoint, params, options = {}) {
|
|
73
|
+
// Taken now, while the files are in the stack: a refusal that comes
|
|
74
|
+
// later still names their line, even when they did not await.
|
|
75
|
+
const site = callSite();
|
|
76
|
+
return this.#send(endpoint, params, options).catch((error) => {
|
|
77
|
+
throw fromSite(error, site);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
async #send(endpoint, params, options) {
|
|
72
81
|
const auth = options.auth ?? true;
|
|
73
82
|
if (auth && this.#tokenRefused)
|
|
74
83
|
throw new InvalidTokenError();
|
|
@@ -8,7 +8,6 @@ export interface FileStoreBackendOptions {
|
|
|
8
8
|
sweepEvery?: number;
|
|
9
9
|
now?: () => number;
|
|
10
10
|
}
|
|
11
|
-
/** The data of every store, one file each, loaded on first use. */
|
|
12
11
|
export declare class FileStoreBackend implements StoreBackend {
|
|
13
12
|
#private;
|
|
14
13
|
constructor(dir: string, options?: FileStoreBackendOptions);
|
|
@@ -12,6 +12,21 @@ export const DATA_FOLDER = 'data';
|
|
|
12
12
|
/** A name a file can be called: what an export is called. */
|
|
13
13
|
const FILE_NAME = /^[\w.-]+$/;
|
|
14
14
|
/** The data of every store, one file each, loaded on first use. */
|
|
15
|
+
/**
|
|
16
|
+
* One entry as this backend writes it: `[key parts, value, expiresAt]`. An
|
|
17
|
+
* entry of another shape makes the whole file one it did not write, rather
|
|
18
|
+
* than an entry dropped without a word the next time the file is written.
|
|
19
|
+
*/
|
|
20
|
+
function isEntry(entry) {
|
|
21
|
+
if (!Array.isArray(entry) || entry.length !== 3)
|
|
22
|
+
return false;
|
|
23
|
+
const [key, value, expiresAt] = entry;
|
|
24
|
+
return (Array.isArray(key) &&
|
|
25
|
+
key.length > 0 &&
|
|
26
|
+
key.every(part => typeof part === 'string') &&
|
|
27
|
+
value !== undefined &&
|
|
28
|
+
(expiresAt === null || typeof expiresAt === 'number'));
|
|
29
|
+
}
|
|
15
30
|
export class FileStoreBackend {
|
|
16
31
|
#dir;
|
|
17
32
|
#stores = new Map();
|
|
@@ -55,7 +70,9 @@ export class FileStoreBackend {
|
|
|
55
70
|
let parsed;
|
|
56
71
|
try {
|
|
57
72
|
parsed = JSON.parse(text);
|
|
58
|
-
if (parsed?.version !== 1 ||
|
|
73
|
+
if (parsed?.version !== 1 ||
|
|
74
|
+
!Array.isArray(parsed.entries) ||
|
|
75
|
+
!parsed.entries.every(isEntry)) {
|
|
59
76
|
throw new Error('not a store file');
|
|
60
77
|
}
|
|
61
78
|
}
|
package/dist/store/scope.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Runs `fn` for a server (or for none, with `null`). */
|
|
1
|
+
/** Runs `fn` for a server (or for none, with `null`), as one run. */
|
|
2
2
|
export declare function withGuild<T>(guildId: string | null, fn: () => T): T;
|
|
3
3
|
/**
|
|
4
4
|
* The id of the server the current function runs for: `null` when it runs
|
|
@@ -6,6 +6,11 @@ export declare function withGuild<T>(guildId: string | null, fn: () => T): T;
|
|
|
6
6
|
* the framework runs.
|
|
7
7
|
*/
|
|
8
8
|
export declare function currentGuildId(): string | null | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* The run of a function of the files the current code belongs to, or
|
|
11
|
+
* `undefined` outside any.
|
|
12
|
+
*/
|
|
13
|
+
export declare function currentRun(): object | undefined;
|
|
9
14
|
/**
|
|
10
15
|
* The scope part of a key, for data per server or for the whole bot: the
|
|
11
16
|
* id of the server the code runs for (`guildOf`), or nothing for a global
|
package/dist/store/scope.js
CHANGED
|
@@ -4,11 +4,10 @@
|
|
|
4
4
|
// the routers around every function they run, on an asynchronous context
|
|
5
5
|
// of Node, so it follows the `await`s into the files the function imports.
|
|
6
6
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
7
|
-
/** The id of the server the current function runs for, or `null` for none. */
|
|
8
7
|
const scope = new AsyncLocalStorage();
|
|
9
|
-
/** Runs `fn` for a server (or for none, with `null`). */
|
|
8
|
+
/** Runs `fn` for a server (or for none, with `null`), as one run. */
|
|
10
9
|
export function withGuild(guildId, fn) {
|
|
11
|
-
return scope.run(guildId, fn);
|
|
10
|
+
return scope.run({ guildId }, fn);
|
|
12
11
|
}
|
|
13
12
|
/**
|
|
14
13
|
* The id of the server the current function runs for: `null` when it runs
|
|
@@ -16,6 +15,13 @@ export function withGuild(guildId, fn) {
|
|
|
16
15
|
* the framework runs.
|
|
17
16
|
*/
|
|
18
17
|
export function currentGuildId() {
|
|
18
|
+
return scope.getStore()?.guildId;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The run of a function of the files the current code belongs to, or
|
|
22
|
+
* `undefined` outside any.
|
|
23
|
+
*/
|
|
24
|
+
export function currentRun() {
|
|
19
25
|
return scope.getStore();
|
|
20
26
|
}
|
|
21
27
|
/**
|
|
@@ -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
|
}
|
|
@@ -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) {
|