spearkit 0.1.0 → 0.3.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 +20 -0
- package/dist/index.cjs +2572 -71
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1656 -247
- package/dist/index.d.ts +1656 -247
- package/dist/index.js +2509 -75
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1,11 +1,524 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var discord_js = require('discord.js');
|
|
4
|
+
var fs = require('fs');
|
|
5
|
+
var util = require('util');
|
|
4
6
|
var promises = require('fs/promises');
|
|
5
7
|
var path = require('path');
|
|
6
8
|
var url = require('url');
|
|
7
9
|
|
|
8
10
|
// src/index.ts
|
|
11
|
+
var DEFAULT_EMBED_COLORS = {
|
|
12
|
+
error: 15747655,
|
|
13
|
+
success: 4437378,
|
|
14
|
+
info: 3447003,
|
|
15
|
+
warn: 16361509
|
|
16
|
+
};
|
|
17
|
+
var DEFAULT_EMBED_ICONS = {
|
|
18
|
+
error: "\u26D4",
|
|
19
|
+
success: "\u2705",
|
|
20
|
+
info: "\u2139\uFE0F",
|
|
21
|
+
warn: "\u26A0\uFE0F"
|
|
22
|
+
};
|
|
23
|
+
var Embeds = class {
|
|
24
|
+
/** The resolved colors for every preset. */
|
|
25
|
+
colors;
|
|
26
|
+
/** The resolved icons for every preset. */
|
|
27
|
+
icons;
|
|
28
|
+
constructor(options = {}) {
|
|
29
|
+
this.colors = { ...DEFAULT_EMBED_COLORS, ...options.colors };
|
|
30
|
+
this.icons = { ...DEFAULT_EMBED_ICONS, ...options.icons };
|
|
31
|
+
}
|
|
32
|
+
/** Red preset — something went wrong. */
|
|
33
|
+
error(input) {
|
|
34
|
+
return this.build("error", input);
|
|
35
|
+
}
|
|
36
|
+
/** Green preset — something succeeded. */
|
|
37
|
+
success(input) {
|
|
38
|
+
return this.build("success", input);
|
|
39
|
+
}
|
|
40
|
+
/** Blue preset — neutral information. */
|
|
41
|
+
info(input) {
|
|
42
|
+
return this.build("info", input);
|
|
43
|
+
}
|
|
44
|
+
/** Yellow preset — caution. */
|
|
45
|
+
warn(input) {
|
|
46
|
+
return this.build("warn", input);
|
|
47
|
+
}
|
|
48
|
+
/** Build an embed at a chosen level. */
|
|
49
|
+
build(level, input) {
|
|
50
|
+
const builder = new discord_js.EmbedBuilder().setColor(this.colors[level]);
|
|
51
|
+
const icon = this.icons[level];
|
|
52
|
+
const prefix = icon.length > 0 ? `${icon} ` : "";
|
|
53
|
+
if (typeof input === "string") {
|
|
54
|
+
builder.setDescription(`${prefix}${input}`);
|
|
55
|
+
return builder;
|
|
56
|
+
}
|
|
57
|
+
if (input.title !== void 0) builder.setTitle(input.title);
|
|
58
|
+
if (input.description !== void 0) {
|
|
59
|
+
builder.setDescription(`${prefix}${input.description}`);
|
|
60
|
+
}
|
|
61
|
+
if (input.fields !== void 0) builder.addFields(...input.fields);
|
|
62
|
+
if (input.footer !== void 0) builder.setFooter(input.footer);
|
|
63
|
+
if (input.author !== void 0) builder.setAuthor(input.author);
|
|
64
|
+
if (input.url !== void 0) builder.setURL(input.url);
|
|
65
|
+
if (input.timestamp !== void 0) {
|
|
66
|
+
builder.setTimestamp(
|
|
67
|
+
input.timestamp instanceof Date ? input.timestamp : new Date(input.timestamp)
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
if (input.thumbnail !== void 0) builder.setThumbnail(input.thumbnail.url);
|
|
71
|
+
if (input.image !== void 0) builder.setImage(input.image.url);
|
|
72
|
+
return builder;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
var defaultEmbeds = new Embeds();
|
|
76
|
+
|
|
77
|
+
// src/lock.ts
|
|
78
|
+
var KeyedLock = class {
|
|
79
|
+
entries = /* @__PURE__ */ new Map();
|
|
80
|
+
defaultTtl;
|
|
81
|
+
sweepTimer;
|
|
82
|
+
constructor(options = {}) {
|
|
83
|
+
this.defaultTtl = options.ttl ?? 6e4;
|
|
84
|
+
const sweep = options.sweep ?? 15e3;
|
|
85
|
+
if (sweep > 0) {
|
|
86
|
+
this.sweepTimer = setInterval(() => this.sweep(), sweep);
|
|
87
|
+
if (typeof this.sweepTimer.unref === "function") this.sweepTimer.unref();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** Try to acquire `key`. Returns a release function, or `null` if already held. */
|
|
91
|
+
tryAcquire(key, ttl = this.defaultTtl) {
|
|
92
|
+
const existing = this.entries.get(key);
|
|
93
|
+
if (existing !== void 0 && Date.now() - existing.createdAt < existing.ttl) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
this.entries.set(key, { createdAt: Date.now(), ttl });
|
|
97
|
+
let released = false;
|
|
98
|
+
return () => {
|
|
99
|
+
if (released) return;
|
|
100
|
+
released = true;
|
|
101
|
+
this.entries.delete(key);
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** Whether `key` is currently held and not expired. */
|
|
105
|
+
isHeld(key) {
|
|
106
|
+
const entry = this.entries.get(key);
|
|
107
|
+
return entry !== void 0 && Date.now() - entry.createdAt < entry.ttl;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Run `fn` while holding `key`. If the key is already held, calls `onBusy`
|
|
111
|
+
* (or returns `undefined`) without ever calling `fn`. Always releases on
|
|
112
|
+
* return or throw.
|
|
113
|
+
*/
|
|
114
|
+
async run(key, fn, options = {}) {
|
|
115
|
+
const release = this.tryAcquire(key, options.ttl ?? this.defaultTtl);
|
|
116
|
+
if (release === null) {
|
|
117
|
+
return options.onBusy !== void 0 ? await options.onBusy() : void 0;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
return await fn();
|
|
121
|
+
} finally {
|
|
122
|
+
release();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** Number of currently-tracked leases (including expired-but-unswept). */
|
|
126
|
+
get size() {
|
|
127
|
+
return this.entries.size;
|
|
128
|
+
}
|
|
129
|
+
/** Drop all known leases and stop the sweep timer. */
|
|
130
|
+
dispose() {
|
|
131
|
+
this.entries.clear();
|
|
132
|
+
if (this.sweepTimer !== void 0) clearInterval(this.sweepTimer);
|
|
133
|
+
}
|
|
134
|
+
/** Manually remove a single key without running anything. */
|
|
135
|
+
forget(key) {
|
|
136
|
+
return this.entries.delete(key);
|
|
137
|
+
}
|
|
138
|
+
sweep() {
|
|
139
|
+
const now = Date.now();
|
|
140
|
+
for (const [key, entry] of this.entries.entries()) {
|
|
141
|
+
if (now - entry.createdAt > entry.ttl) this.entries.delete(key);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// src/safe-fetch.ts
|
|
147
|
+
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
148
|
+
async function withTimeout(promise, ms) {
|
|
149
|
+
let timer;
|
|
150
|
+
try {
|
|
151
|
+
const timeout = new Promise((resolve) => {
|
|
152
|
+
timer = setTimeout(() => resolve(null), ms);
|
|
153
|
+
});
|
|
154
|
+
return await Promise.race([promise.catch(() => null), timeout]);
|
|
155
|
+
} finally {
|
|
156
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function fetchMember(guild, userId, options = {}) {
|
|
160
|
+
if (guild == null || userId == null || userId.length === 0) return null;
|
|
161
|
+
if (options.cache !== false && options.force !== true) {
|
|
162
|
+
const cached = guild.members.cache.get(userId);
|
|
163
|
+
if (cached !== void 0) return cached;
|
|
164
|
+
}
|
|
165
|
+
return withTimeout(
|
|
166
|
+
guild.members.fetch({ user: userId, force: options.force ?? false }),
|
|
167
|
+
options.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
async function fetchChannel(client, channelId, options = {}) {
|
|
171
|
+
if (client == null || channelId == null || channelId.length === 0) return null;
|
|
172
|
+
if (options.cache !== false && options.force !== true) {
|
|
173
|
+
const cached = client.channels.cache.get(channelId);
|
|
174
|
+
if (cached !== void 0) return cached;
|
|
175
|
+
}
|
|
176
|
+
return withTimeout(
|
|
177
|
+
client.channels.fetch(channelId, { force: options.force ?? false }),
|
|
178
|
+
options.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
async function fetchMessage(messages, messageId, options = {}) {
|
|
182
|
+
if (messages == null || messageId == null || messageId.length === 0) return null;
|
|
183
|
+
if (options.cache !== false && options.force !== true) {
|
|
184
|
+
const cached = messages.cache.get(messageId);
|
|
185
|
+
if (cached !== void 0) return cached;
|
|
186
|
+
}
|
|
187
|
+
return withTimeout(
|
|
188
|
+
messages.fetch({ message: messageId, force: options.force ?? false }),
|
|
189
|
+
options.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
async function fetchUser(client, userId, options = {}) {
|
|
193
|
+
if (client == null || userId == null || userId.length === 0) return null;
|
|
194
|
+
if (options.cache !== false && options.force !== true) {
|
|
195
|
+
const cached = client.users.cache.get(userId);
|
|
196
|
+
if (cached !== void 0) return cached;
|
|
197
|
+
}
|
|
198
|
+
return withTimeout(
|
|
199
|
+
client.users.fetch(userId, { force: options.force ?? false }),
|
|
200
|
+
options.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
async function fetchGuild(client, guildId, options = {}) {
|
|
204
|
+
if (client == null || guildId == null || guildId.length === 0) return null;
|
|
205
|
+
if (options.cache !== false && options.force !== true) {
|
|
206
|
+
const cached = client.guilds.cache.get(guildId);
|
|
207
|
+
if (cached !== void 0) return cached;
|
|
208
|
+
}
|
|
209
|
+
return withTimeout(
|
|
210
|
+
client.guilds.fetch({ guild: guildId, force: options.force ?? false }),
|
|
211
|
+
options.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
async function fetchRole(guild, roleId, options = {}) {
|
|
215
|
+
if (guild == null || roleId == null || roleId.length === 0) return null;
|
|
216
|
+
if (options.cache !== false && options.force !== true) {
|
|
217
|
+
const cached = guild.roles.cache.get(roleId);
|
|
218
|
+
if (cached !== void 0) return cached;
|
|
219
|
+
}
|
|
220
|
+
return withTimeout(
|
|
221
|
+
guild.roles.fetch(roleId, { force: options.force ?? false }),
|
|
222
|
+
options.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
async function safeTry(op) {
|
|
226
|
+
try {
|
|
227
|
+
return await op();
|
|
228
|
+
} catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async function withSafeTimeout(promise, timeoutMs) {
|
|
233
|
+
return withTimeout(promise, timeoutMs);
|
|
234
|
+
}
|
|
235
|
+
var safeFetch = {
|
|
236
|
+
member: fetchMember,
|
|
237
|
+
channel: fetchChannel,
|
|
238
|
+
message: fetchMessage,
|
|
239
|
+
user: fetchUser,
|
|
240
|
+
guild: fetchGuild,
|
|
241
|
+
role: fetchRole,
|
|
242
|
+
try: safeTry
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// src/format.ts
|
|
246
|
+
var EN = {
|
|
247
|
+
week: ["week", "weeks"],
|
|
248
|
+
day: ["day", "days"],
|
|
249
|
+
hour: ["hour", "hours"],
|
|
250
|
+
minute: ["minute", "minutes"],
|
|
251
|
+
second: ["second", "seconds"],
|
|
252
|
+
separator: " ",
|
|
253
|
+
zero: "0 seconds"
|
|
254
|
+
};
|
|
255
|
+
var TR = {
|
|
256
|
+
week: ["hafta", "hafta"],
|
|
257
|
+
day: ["g\xFCn", "g\xFCn"],
|
|
258
|
+
hour: ["saat", "saat"],
|
|
259
|
+
minute: ["dakika", "dakika"],
|
|
260
|
+
second: ["saniye", "saniye"],
|
|
261
|
+
separator: " ",
|
|
262
|
+
zero: "0 saniye"
|
|
263
|
+
};
|
|
264
|
+
var LABELS = {
|
|
265
|
+
en: EN,
|
|
266
|
+
"en-US": EN,
|
|
267
|
+
"en-GB": EN,
|
|
268
|
+
tr: TR,
|
|
269
|
+
"tr-TR": TR
|
|
270
|
+
};
|
|
271
|
+
var UNIT_MS = {
|
|
272
|
+
week: 7 * 864e5,
|
|
273
|
+
day: 864e5,
|
|
274
|
+
hour: 36e5,
|
|
275
|
+
minute: 6e4,
|
|
276
|
+
second: 1e3
|
|
277
|
+
};
|
|
278
|
+
var UNIT_ORDER = ["week", "day", "hour", "minute", "second"];
|
|
279
|
+
function resolveLabels(locale) {
|
|
280
|
+
if (typeof locale === "object" && locale !== null) return locale;
|
|
281
|
+
if (locale === void 0) return EN;
|
|
282
|
+
return LABELS[locale] ?? LABELS[locale.split("-")[0] ?? ""] ?? EN;
|
|
283
|
+
}
|
|
284
|
+
function formatDuration(ms, options = {}) {
|
|
285
|
+
const labels = resolveLabels(options.locale);
|
|
286
|
+
if (!Number.isFinite(ms) || ms <= 0) return labels.zero;
|
|
287
|
+
const limit = options.largest ?? 2;
|
|
288
|
+
const units = options.units ?? UNIT_ORDER;
|
|
289
|
+
const parts = [];
|
|
290
|
+
let remaining = Math.floor(ms);
|
|
291
|
+
for (const unit of units) {
|
|
292
|
+
if (parts.length >= limit) break;
|
|
293
|
+
const size = UNIT_MS[unit];
|
|
294
|
+
if (remaining < size) continue;
|
|
295
|
+
const value = Math.floor(remaining / size);
|
|
296
|
+
remaining -= value * size;
|
|
297
|
+
const word = value === 1 ? labels[unit][0] : labels[unit][1];
|
|
298
|
+
parts.push(`${value} ${word}`);
|
|
299
|
+
}
|
|
300
|
+
return parts.length > 0 ? parts.join(labels.separator) : labels.zero;
|
|
301
|
+
}
|
|
302
|
+
var DURATION_PATTERN = /(\d+(?:\.\d+)?)\s*(milliseconds|millisecond|seconds|minutes|saniye|dakika|minute|second|weeks|hours|hafta|saat|week|hour|days|day|gün|gun|min|sec|ms|wk|hr|dk|m|s|h|d|w)/gi;
|
|
303
|
+
var SHORT_TO_MS = {
|
|
304
|
+
ms: 1,
|
|
305
|
+
millisecond: 1,
|
|
306
|
+
milliseconds: 1,
|
|
307
|
+
s: 1e3,
|
|
308
|
+
sec: 1e3,
|
|
309
|
+
second: 1e3,
|
|
310
|
+
seconds: 1e3,
|
|
311
|
+
saniye: 1e3,
|
|
312
|
+
m: 6e4,
|
|
313
|
+
min: 6e4,
|
|
314
|
+
minute: 6e4,
|
|
315
|
+
minutes: 6e4,
|
|
316
|
+
dakika: 6e4,
|
|
317
|
+
dk: 6e4,
|
|
318
|
+
h: 36e5,
|
|
319
|
+
hr: 36e5,
|
|
320
|
+
hour: 36e5,
|
|
321
|
+
hours: 36e5,
|
|
322
|
+
saat: 36e5,
|
|
323
|
+
d: 864e5,
|
|
324
|
+
day: 864e5,
|
|
325
|
+
days: 864e5,
|
|
326
|
+
g\u00FCn: 864e5,
|
|
327
|
+
gun: 864e5,
|
|
328
|
+
w: 6048e5,
|
|
329
|
+
wk: 6048e5,
|
|
330
|
+
week: 6048e5,
|
|
331
|
+
weeks: 6048e5,
|
|
332
|
+
hafta: 6048e5
|
|
333
|
+
};
|
|
334
|
+
function parseDuration(input) {
|
|
335
|
+
const trimmed = input.trim().toLowerCase();
|
|
336
|
+
if (trimmed.length === 0) return null;
|
|
337
|
+
DURATION_PATTERN.lastIndex = 0;
|
|
338
|
+
let total = 0;
|
|
339
|
+
let matched = false;
|
|
340
|
+
for (; ; ) {
|
|
341
|
+
const match = DURATION_PATTERN.exec(trimmed);
|
|
342
|
+
if (match === null) break;
|
|
343
|
+
matched = true;
|
|
344
|
+
const value = Number(match[1]);
|
|
345
|
+
const unit = match[2] ?? "";
|
|
346
|
+
const ms = SHORT_TO_MS[unit];
|
|
347
|
+
if (ms !== void 0 && Number.isFinite(value)) total += value * ms;
|
|
348
|
+
}
|
|
349
|
+
return matched ? total : null;
|
|
350
|
+
}
|
|
351
|
+
function toEpochSeconds(date) {
|
|
352
|
+
return Math.floor((date instanceof Date ? date.getTime() : date) / 1e3);
|
|
353
|
+
}
|
|
354
|
+
function discordTimestamp(date, style = "f") {
|
|
355
|
+
return `<t:${toEpochSeconds(date)}:${style}>`;
|
|
356
|
+
}
|
|
357
|
+
function relativeTimestamp(date) {
|
|
358
|
+
return discordTimestamp(date, "R");
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// src/cache.ts
|
|
362
|
+
var MemoryCache = class {
|
|
363
|
+
store = /* @__PURE__ */ new Map();
|
|
364
|
+
/** Total number of stored (possibly expired) entries — primarily for tests. */
|
|
365
|
+
get size() {
|
|
366
|
+
return this.store.size;
|
|
367
|
+
}
|
|
368
|
+
async get(key) {
|
|
369
|
+
const entry = this.store.get(key);
|
|
370
|
+
if (entry === void 0) return void 0;
|
|
371
|
+
if (entry.expiresAt !== void 0 && entry.expiresAt <= Date.now()) {
|
|
372
|
+
this.store.delete(key);
|
|
373
|
+
return void 0;
|
|
374
|
+
}
|
|
375
|
+
return entry.value;
|
|
376
|
+
}
|
|
377
|
+
async set(key, value, options) {
|
|
378
|
+
const ttl = options?.ttl;
|
|
379
|
+
this.store.set(key, {
|
|
380
|
+
value,
|
|
381
|
+
expiresAt: ttl !== void 0 && ttl > 0 ? Date.now() + ttl : void 0
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
async delete(key) {
|
|
385
|
+
return this.store.delete(key);
|
|
386
|
+
}
|
|
387
|
+
async has(key) {
|
|
388
|
+
return await this.get(key) !== void 0;
|
|
389
|
+
}
|
|
390
|
+
async increment(key, delta = 1, options) {
|
|
391
|
+
const current = await this.get(key) ?? 0;
|
|
392
|
+
const next = current + delta;
|
|
393
|
+
const existing = this.store.get(key);
|
|
394
|
+
const ttl = options?.ttl;
|
|
395
|
+
if (ttl !== void 0 && ttl > 0) {
|
|
396
|
+
await this.set(key, next, { ttl });
|
|
397
|
+
} else if (existing?.expiresAt !== void 0) {
|
|
398
|
+
this.store.set(key, { value: next, expiresAt: existing.expiresAt });
|
|
399
|
+
} else {
|
|
400
|
+
await this.set(key, next);
|
|
401
|
+
}
|
|
402
|
+
return next;
|
|
403
|
+
}
|
|
404
|
+
async rateLimit(key, options) {
|
|
405
|
+
const now = Date.now();
|
|
406
|
+
const bucketKey = `__rl__:${key}`;
|
|
407
|
+
const entry = this.store.get(bucketKey);
|
|
408
|
+
const expired = entry === void 0 || entry.expiresAt === void 0 || entry.expiresAt <= now;
|
|
409
|
+
if (expired) {
|
|
410
|
+
const resetAt2 = now + options.windowMs;
|
|
411
|
+
this.store.set(bucketKey, { value: 1, expiresAt: resetAt2 });
|
|
412
|
+
return { allowed: true, remaining: Math.max(0, options.limit - 1), resetAt: resetAt2 };
|
|
413
|
+
}
|
|
414
|
+
const count = entry.value;
|
|
415
|
+
const resetAt = entry.expiresAt ?? now;
|
|
416
|
+
if (count >= options.limit) {
|
|
417
|
+
return { allowed: false, remaining: 0, resetAt };
|
|
418
|
+
}
|
|
419
|
+
this.store.set(bucketKey, { value: count + 1, expiresAt: resetAt });
|
|
420
|
+
return { allowed: true, remaining: Math.max(0, options.limit - count - 1), resetAt };
|
|
421
|
+
}
|
|
422
|
+
async clear() {
|
|
423
|
+
this.store.clear();
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
function createCache() {
|
|
427
|
+
return new MemoryCache();
|
|
428
|
+
}
|
|
429
|
+
var readFileAsync = util.promisify(fs.readFile);
|
|
430
|
+
function loadConfig(options) {
|
|
431
|
+
const text = fs.readFileSync(options.file, options.encoding ?? "utf8");
|
|
432
|
+
const parser = options.parser ?? JSON.parse;
|
|
433
|
+
const parsed = parser(text);
|
|
434
|
+
return options.schema !== void 0 ? options.schema(parsed) : parsed;
|
|
435
|
+
}
|
|
436
|
+
async function loadConfigAsync(options) {
|
|
437
|
+
const text = await readFileAsync(options.file, options.encoding ?? "utf8");
|
|
438
|
+
const parser = options.parser ?? JSON.parse;
|
|
439
|
+
const parsed = parser(text);
|
|
440
|
+
return options.schema !== void 0 ? options.schema(parsed) : parsed;
|
|
441
|
+
}
|
|
442
|
+
function lookup(table, resourceName = "key") {
|
|
443
|
+
return (key) => {
|
|
444
|
+
const value = table[key];
|
|
445
|
+
if (value === void 0) {
|
|
446
|
+
throw new Error(`spearkit: ${resourceName} "${String(key)}" not found in config`);
|
|
447
|
+
}
|
|
448
|
+
return value;
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function lookupOptional(table) {
|
|
452
|
+
return (key) => table[key];
|
|
453
|
+
}
|
|
454
|
+
function denied(reason) {
|
|
455
|
+
return { allowed: false, reason };
|
|
456
|
+
}
|
|
457
|
+
async function runGuards(ctx, guards) {
|
|
458
|
+
if (guards === void 0 || guards.length === 0) return { allowed: true };
|
|
459
|
+
for (const guard2 of guards) {
|
|
460
|
+
const result = await guard2(ctx);
|
|
461
|
+
if (result === true) continue;
|
|
462
|
+
if (result === false) return { allowed: false, reason: void 0 };
|
|
463
|
+
if (result.allowed === false) return { allowed: false, reason: result.reason };
|
|
464
|
+
}
|
|
465
|
+
return { allowed: true };
|
|
466
|
+
}
|
|
467
|
+
function guildOnly(reason = "This can only be used in a server.") {
|
|
468
|
+
return (ctx) => ctx.guildId !== null ? true : denied(reason);
|
|
469
|
+
}
|
|
470
|
+
function dmOnly(reason = "This can only be used in DMs.") {
|
|
471
|
+
return (ctx) => ctx.guildId === null ? true : denied(reason);
|
|
472
|
+
}
|
|
473
|
+
function memberRoleIds(member) {
|
|
474
|
+
if (member === null) return [];
|
|
475
|
+
const roles = member.roles;
|
|
476
|
+
if (Array.isArray(roles)) return roles;
|
|
477
|
+
return [...roles.cache.keys()];
|
|
478
|
+
}
|
|
479
|
+
function memberPermissionsBitField(member) {
|
|
480
|
+
if (member === null) return null;
|
|
481
|
+
const perms = member.permissions;
|
|
482
|
+
if (perms instanceof discord_js.PermissionsBitField) return perms;
|
|
483
|
+
if (typeof perms === "string") return new discord_js.PermissionsBitField(BigInt(perms));
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
function requireAnyRole(roleIds, reason = "You don't have permission to use this.") {
|
|
487
|
+
const set = new Set(roleIds);
|
|
488
|
+
return (ctx) => {
|
|
489
|
+
const ids = memberRoleIds(ctx.member);
|
|
490
|
+
return ids.some((id) => set.has(id)) ? true : denied(reason);
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
function requireAllRoles(roleIds, reason = "You're missing one of the required roles.") {
|
|
494
|
+
return (ctx) => {
|
|
495
|
+
const ids = new Set(memberRoleIds(ctx.member));
|
|
496
|
+
return roleIds.every((id) => ids.has(id)) ? true : denied(reason);
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
function requireOwner(ownerIds, reason = "This is owner-only.") {
|
|
500
|
+
const set = new Set(ownerIds);
|
|
501
|
+
return (ctx) => set.has(ctx.user.id) ? true : denied(reason);
|
|
502
|
+
}
|
|
503
|
+
function requireUserPermissions(permission, reason = "You don't have permission to use this.") {
|
|
504
|
+
return (ctx) => {
|
|
505
|
+
const bits = memberPermissionsBitField(ctx.member);
|
|
506
|
+
if (bits === null) return denied(reason);
|
|
507
|
+
return bits.has(permission) ? true : denied(reason);
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
function requireBotPermissions(permission, reason = "I don't have permission to do that here.") {
|
|
511
|
+
return async (ctx) => {
|
|
512
|
+
const guild = ctx.guild;
|
|
513
|
+
if (guild === null) return denied(reason);
|
|
514
|
+
const me = guild.members.me ?? await guild.members.fetchMe().catch(() => null);
|
|
515
|
+
if (me === null) return denied(reason);
|
|
516
|
+
return me.permissions.has(permission) ? true : denied(reason);
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
function guard(predicate) {
|
|
520
|
+
return predicate;
|
|
521
|
+
}
|
|
9
522
|
function withEphemeralFlag(flags) {
|
|
10
523
|
if (flags == null) return discord_js.MessageFlags.Ephemeral;
|
|
11
524
|
if (typeof flags === "number" || typeof flags === "bigint") {
|
|
@@ -25,86 +538,1647 @@ function normalizeEdit(input) {
|
|
|
25
538
|
const { ephemeral: _ephemeral, flags: _flags, ...rest } = input;
|
|
26
539
|
return rest;
|
|
27
540
|
}
|
|
28
|
-
function asEphemeral(input) {
|
|
29
|
-
if (typeof input === "string") return { content: input, ephemeral: true };
|
|
30
|
-
return { ...input, ephemeral: true };
|
|
541
|
+
function asEphemeral(input) {
|
|
542
|
+
if (typeof input === "string") return { content: input, ephemeral: true };
|
|
543
|
+
return { ...input, ephemeral: true };
|
|
544
|
+
}
|
|
545
|
+
var BaseContext = class {
|
|
546
|
+
constructor(interaction) {
|
|
547
|
+
this.interaction = interaction;
|
|
548
|
+
}
|
|
549
|
+
interaction;
|
|
550
|
+
get client() {
|
|
551
|
+
return this.interaction.client;
|
|
552
|
+
}
|
|
553
|
+
get user() {
|
|
554
|
+
return this.interaction.user;
|
|
555
|
+
}
|
|
556
|
+
get member() {
|
|
557
|
+
return this.interaction.member;
|
|
558
|
+
}
|
|
559
|
+
get guild() {
|
|
560
|
+
return this.interaction.guild;
|
|
561
|
+
}
|
|
562
|
+
get guildId() {
|
|
563
|
+
return this.interaction.guildId;
|
|
564
|
+
}
|
|
565
|
+
get channel() {
|
|
566
|
+
return this.interaction.channel;
|
|
567
|
+
}
|
|
568
|
+
get channelId() {
|
|
569
|
+
return this.interaction.channelId;
|
|
570
|
+
}
|
|
571
|
+
get locale() {
|
|
572
|
+
return this.interaction.locale;
|
|
573
|
+
}
|
|
574
|
+
/** Whether the interaction is already deferred. */
|
|
575
|
+
get deferred() {
|
|
576
|
+
return this.interaction.deferred;
|
|
577
|
+
}
|
|
578
|
+
/** Whether the interaction already received an initial response. */
|
|
579
|
+
get replied() {
|
|
580
|
+
return this.interaction.replied;
|
|
581
|
+
}
|
|
582
|
+
/** Send the initial response to the interaction. */
|
|
583
|
+
reply(input) {
|
|
584
|
+
return this.interaction.reply(normalizeReply(input));
|
|
585
|
+
}
|
|
586
|
+
/** Reply, but always hidden to everyone except the invoking user. */
|
|
587
|
+
replyEphemeral(input) {
|
|
588
|
+
return this.reply(asEphemeral(input));
|
|
589
|
+
}
|
|
590
|
+
/** Acknowledge now and respond later via {@link editReply}. */
|
|
591
|
+
defer(options = {}) {
|
|
592
|
+
return this.interaction.deferReply(
|
|
593
|
+
options.ephemeral ? { flags: discord_js.MessageFlags.Ephemeral } : {}
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
/** Edit the original (or deferred) response. */
|
|
597
|
+
editReply(input) {
|
|
598
|
+
return this.interaction.editReply(normalizeEdit(input));
|
|
599
|
+
}
|
|
600
|
+
/** Add an additional message after the initial response. */
|
|
601
|
+
followUp(input) {
|
|
602
|
+
return this.interaction.followUp(normalizeReply(input));
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* State-aware send: replies, edits a deferred response, or follows up —
|
|
606
|
+
* whichever is valid given the current interaction state. The single method
|
|
607
|
+
* most handlers ever need.
|
|
608
|
+
*/
|
|
609
|
+
async send(input) {
|
|
610
|
+
if (this.interaction.deferred) {
|
|
611
|
+
await this.editReply(input);
|
|
612
|
+
} else if (this.interaction.replied) {
|
|
613
|
+
await this.followUp(input);
|
|
614
|
+
} else {
|
|
615
|
+
await this.reply(input);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
/** Get the configured {@link Embeds} factory — `client.embeds` or the default. */
|
|
619
|
+
getEmbeds() {
|
|
620
|
+
return this.interaction.client.embeds ?? defaultEmbeds;
|
|
621
|
+
}
|
|
622
|
+
/** State-aware send of a red error embed. Defaults to ephemeral. */
|
|
623
|
+
error(input, options = {}) {
|
|
624
|
+
return this.sendPreset("error", input, { ephemeral: options.ephemeral ?? true });
|
|
625
|
+
}
|
|
626
|
+
/** State-aware send of a green success embed. */
|
|
627
|
+
success(input, options = {}) {
|
|
628
|
+
return this.sendPreset("success", input, options);
|
|
629
|
+
}
|
|
630
|
+
/** State-aware send of a blue info embed. */
|
|
631
|
+
info(input, options = {}) {
|
|
632
|
+
return this.sendPreset("info", input, options);
|
|
633
|
+
}
|
|
634
|
+
/** State-aware send of a yellow warn embed. */
|
|
635
|
+
warn(input, options = {}) {
|
|
636
|
+
return this.sendPreset("warn", input, options);
|
|
637
|
+
}
|
|
638
|
+
/** Initial-reply variant of {@link error} (always `reply`, never `editReply`/`followUp`). */
|
|
639
|
+
replyError(input, options = {}) {
|
|
640
|
+
return this.replyPreset("error", input, { ephemeral: options.ephemeral ?? true });
|
|
641
|
+
}
|
|
642
|
+
/** Initial-reply variant of {@link success}. */
|
|
643
|
+
replySuccess(input, options = {}) {
|
|
644
|
+
return this.replyPreset("success", input, options);
|
|
645
|
+
}
|
|
646
|
+
/** Initial-reply variant of {@link info}. */
|
|
647
|
+
replyInfo(input, options = {}) {
|
|
648
|
+
return this.replyPreset("info", input, options);
|
|
649
|
+
}
|
|
650
|
+
/** Initial-reply variant of {@link warn}. */
|
|
651
|
+
replyWarn(input, options = {}) {
|
|
652
|
+
return this.replyPreset("warn", input, options);
|
|
653
|
+
}
|
|
654
|
+
sendPreset(level, input, options) {
|
|
655
|
+
const embed = this.getEmbeds().build(level, input);
|
|
656
|
+
return this.send({ embeds: [embed], ephemeral: options.ephemeral });
|
|
657
|
+
}
|
|
658
|
+
replyPreset(level, input, options) {
|
|
659
|
+
const embed = this.getEmbeds().build(level, input);
|
|
660
|
+
return this.reply({ embeds: [embed], ephemeral: options.ephemeral });
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
// src/cooldown.ts
|
|
665
|
+
function normalizeCooldown(input) {
|
|
666
|
+
return typeof input === "number" ? { duration: input } : input;
|
|
667
|
+
}
|
|
668
|
+
function scopeKey(scope, actor) {
|
|
669
|
+
switch (scope) {
|
|
670
|
+
case "guild":
|
|
671
|
+
return `g:${actor.guildId ?? "dm"}`;
|
|
672
|
+
case "channel":
|
|
673
|
+
return `c:${actor.channelId ?? "dm"}`;
|
|
674
|
+
case "global":
|
|
675
|
+
return "global";
|
|
676
|
+
case "user":
|
|
677
|
+
return `u:${actor.userId}`;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
function effectiveDuration(config, actor) {
|
|
681
|
+
if (config.exempt?.users?.includes(actor.userId) === true) return null;
|
|
682
|
+
if (config.exempt?.roles?.some((roleId) => actor.roleIds.includes(roleId)) === true) return null;
|
|
683
|
+
const userOverride = config.overrides?.users?.[actor.userId];
|
|
684
|
+
if (userOverride !== void 0) return userOverride;
|
|
685
|
+
const roleOverrides = config.overrides?.roles;
|
|
686
|
+
if (roleOverrides !== void 0) {
|
|
687
|
+
let best;
|
|
688
|
+
for (const roleId of actor.roleIds) {
|
|
689
|
+
const candidate = roleOverrides[roleId];
|
|
690
|
+
if (candidate !== void 0) best = best === void 0 ? candidate : Math.min(best, candidate);
|
|
691
|
+
}
|
|
692
|
+
if (best !== void 0) return best;
|
|
693
|
+
}
|
|
694
|
+
return config.duration;
|
|
695
|
+
}
|
|
696
|
+
function keyFor(bucket, config, actor) {
|
|
697
|
+
return `${bucket}|${scopeKey(config.scope ?? "user", actor)}`;
|
|
698
|
+
}
|
|
699
|
+
var CooldownManager = class {
|
|
700
|
+
hits = /* @__PURE__ */ new Map();
|
|
701
|
+
/** Number of tracked buckets. */
|
|
702
|
+
get size() {
|
|
703
|
+
return this.hits.size;
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Check whether `actor` may use `bucket`, recording the use when allowed.
|
|
707
|
+
* Exempt actors and non-positive durations are always allowed (no record).
|
|
708
|
+
*/
|
|
709
|
+
consume(bucket, input, actor, now = Date.now()) {
|
|
710
|
+
const config = normalizeCooldown(input);
|
|
711
|
+
const duration = effectiveDuration(config, actor);
|
|
712
|
+
if (duration === null || duration <= 0) return { allowed: true };
|
|
713
|
+
const key = keyFor(bucket, config, actor);
|
|
714
|
+
const last = this.hits.get(key);
|
|
715
|
+
if (last !== void 0 && now - last < duration) {
|
|
716
|
+
return { allowed: false, remaining: duration - (now - last) };
|
|
717
|
+
}
|
|
718
|
+
this.hits.set(key, now);
|
|
719
|
+
return { allowed: true };
|
|
720
|
+
}
|
|
721
|
+
/** Like {@link consume} but never records — a read-only check. */
|
|
722
|
+
peek(bucket, input, actor, now = Date.now()) {
|
|
723
|
+
const config = normalizeCooldown(input);
|
|
724
|
+
const duration = effectiveDuration(config, actor);
|
|
725
|
+
if (duration === null || duration <= 0) return { allowed: true };
|
|
726
|
+
const last = this.hits.get(keyFor(bucket, config, actor));
|
|
727
|
+
if (last !== void 0 && now - last < duration) {
|
|
728
|
+
return { allowed: false, remaining: duration - (now - last) };
|
|
729
|
+
}
|
|
730
|
+
return { allowed: true };
|
|
731
|
+
}
|
|
732
|
+
/** Clear a single actor's cooldown for a bucket. Returns whether one existed. */
|
|
733
|
+
reset(bucket, actor, scope = "user") {
|
|
734
|
+
return this.hits.delete(`${bucket}|${scopeKey(scope, actor)}`);
|
|
735
|
+
}
|
|
736
|
+
/** Drop every tracked cooldown. */
|
|
737
|
+
clear() {
|
|
738
|
+
this.hits.clear();
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
function formatCooldownMessage(config, remainingMs) {
|
|
742
|
+
if (typeof config.message === "function") return config.message(remainingMs);
|
|
743
|
+
if (typeof config.message === "string") return config.message;
|
|
744
|
+
const seconds = Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
745
|
+
return `You're on cooldown \u2014 try again in ${seconds}s.`;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// src/context-menus.ts
|
|
749
|
+
var UserContextMenuContext = class extends BaseContext {
|
|
750
|
+
/** The user the menu was invoked on. */
|
|
751
|
+
get targetUser() {
|
|
752
|
+
return this.interaction.targetUser;
|
|
753
|
+
}
|
|
754
|
+
/** The member version of the target, if available. */
|
|
755
|
+
get targetMember() {
|
|
756
|
+
return this.interaction.targetMember;
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
var MessageContextMenuContext = class extends BaseContext {
|
|
760
|
+
/** The message the menu was invoked on. */
|
|
761
|
+
get targetMessage() {
|
|
762
|
+
return this.interaction.targetMessage;
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
function baseJSON(meta, type) {
|
|
766
|
+
return {
|
|
767
|
+
type,
|
|
768
|
+
name: meta.name,
|
|
769
|
+
name_localizations: meta.nameLocalizations,
|
|
770
|
+
nsfw: meta.nsfw,
|
|
771
|
+
default_member_permissions: meta.defaultMemberPermissions == null ? meta.defaultMemberPermissions : new discord_js.PermissionsBitField(meta.defaultMemberPermissions).bitfield.toString(),
|
|
772
|
+
contexts: meta.guildOnly ? [discord_js.InteractionContextType.Guild] : void 0
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
function userCommand(config) {
|
|
776
|
+
const cooldown = config.cooldown !== void 0 ? normalizeCooldown(config.cooldown) : void 0;
|
|
777
|
+
return {
|
|
778
|
+
kind: "userMenu",
|
|
779
|
+
name: config.name,
|
|
780
|
+
cooldown,
|
|
781
|
+
guards: config.guards,
|
|
782
|
+
toJSON: () => baseJSON(config, discord_js.ApplicationCommandType.User),
|
|
783
|
+
execute: async (interaction) => {
|
|
784
|
+
await config.run(new UserContextMenuContext(interaction));
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
function messageCommand(config) {
|
|
789
|
+
const cooldown = config.cooldown !== void 0 ? normalizeCooldown(config.cooldown) : void 0;
|
|
790
|
+
return {
|
|
791
|
+
kind: "messageMenu",
|
|
792
|
+
name: config.name,
|
|
793
|
+
cooldown,
|
|
794
|
+
guards: config.guards,
|
|
795
|
+
toJSON: () => baseJSON(config, discord_js.ApplicationCommandType.Message),
|
|
796
|
+
execute: async (interaction) => {
|
|
797
|
+
await config.run(new MessageContextMenuContext(interaction));
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
var ContextMenuRegistry = class {
|
|
802
|
+
users = /* @__PURE__ */ new Map();
|
|
803
|
+
messages = /* @__PURE__ */ new Map();
|
|
804
|
+
logger;
|
|
805
|
+
cooldowns;
|
|
806
|
+
defaultCooldown;
|
|
807
|
+
defaultGuards = [];
|
|
808
|
+
onUsage;
|
|
809
|
+
/** Register one or more context-menu commands. */
|
|
810
|
+
add(...commands) {
|
|
811
|
+
for (const command2 of commands) {
|
|
812
|
+
if (command2.kind === "userMenu") this.users.set(command2.name, command2);
|
|
813
|
+
else this.messages.set(command2.name, command2);
|
|
814
|
+
}
|
|
815
|
+
return this;
|
|
816
|
+
}
|
|
817
|
+
/** Total number of registered context-menu commands. */
|
|
818
|
+
get size() {
|
|
819
|
+
return this.users.size + this.messages.size;
|
|
820
|
+
}
|
|
821
|
+
/** Every registered command, both kinds. */
|
|
822
|
+
all() {
|
|
823
|
+
return [...this.users.values(), ...this.messages.values()];
|
|
824
|
+
}
|
|
825
|
+
/** Serialise every command for the REST `applicationCommands` PUT body. */
|
|
826
|
+
toJSON() {
|
|
827
|
+
return this.all().map((c) => c.toJSON());
|
|
828
|
+
}
|
|
829
|
+
setLogger(logger) {
|
|
830
|
+
this.logger = logger;
|
|
831
|
+
return this;
|
|
832
|
+
}
|
|
833
|
+
setCooldowns(manager, defaultCooldown) {
|
|
834
|
+
this.cooldowns = manager;
|
|
835
|
+
this.defaultCooldown = defaultCooldown;
|
|
836
|
+
return this;
|
|
837
|
+
}
|
|
838
|
+
setDefaultGuards(guards) {
|
|
839
|
+
this.defaultGuards = guards;
|
|
840
|
+
return this;
|
|
841
|
+
}
|
|
842
|
+
setUsageHook(hook) {
|
|
843
|
+
this.onUsage = hook;
|
|
844
|
+
return this;
|
|
845
|
+
}
|
|
846
|
+
/** Dispatch a user-target interaction. */
|
|
847
|
+
async handleUser(interaction) {
|
|
848
|
+
const command2 = this.users.get(interaction.commandName);
|
|
849
|
+
if (command2 === void 0) return;
|
|
850
|
+
await this.dispatch(command2, interaction);
|
|
851
|
+
}
|
|
852
|
+
/** Dispatch a message-target interaction. */
|
|
853
|
+
async handleMessage(interaction) {
|
|
854
|
+
const command2 = this.messages.get(interaction.commandName);
|
|
855
|
+
if (command2 === void 0) return;
|
|
856
|
+
await this.dispatch(command2, interaction);
|
|
857
|
+
}
|
|
858
|
+
async dispatch(command2, interaction) {
|
|
859
|
+
this.logger?.debug("contextMenu", {
|
|
860
|
+
data: { kind: command2.kind, name: command2.name, user: interaction.user.id }
|
|
861
|
+
});
|
|
862
|
+
const cooldown = command2.cooldown ?? this.defaultCooldown;
|
|
863
|
+
if (cooldown !== void 0 && this.cooldowns !== void 0) {
|
|
864
|
+
const result = this.cooldowns.consume(
|
|
865
|
+
`${command2.kind}:${command2.name}`,
|
|
866
|
+
cooldown,
|
|
867
|
+
actorOf(interaction)
|
|
868
|
+
);
|
|
869
|
+
if (!result.allowed) {
|
|
870
|
+
await replyCooldown(interaction, cooldown, result.remaining);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
const guards = combineGuards(this.defaultGuards, command2.guards);
|
|
875
|
+
if (guards.length > 0) {
|
|
876
|
+
const guardResult = await runGuards(interaction, guards);
|
|
877
|
+
if (!guardResult.allowed) {
|
|
878
|
+
this.logger?.debug("contextMenu denied", {
|
|
879
|
+
data: { name: command2.name, user: interaction.user.id, reason: guardResult.reason ?? "" }
|
|
880
|
+
});
|
|
881
|
+
await replyDenied(interaction, guardResult.reason);
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
const start = Date.now();
|
|
886
|
+
try {
|
|
887
|
+
if (command2.kind === "userMenu") {
|
|
888
|
+
await command2.execute(interaction);
|
|
889
|
+
} else {
|
|
890
|
+
await command2.execute(interaction);
|
|
891
|
+
}
|
|
892
|
+
this.onUsage?.({
|
|
893
|
+
type: "command",
|
|
894
|
+
name: command2.name,
|
|
895
|
+
detail: command2.kind,
|
|
896
|
+
outcome: "success",
|
|
897
|
+
durationMs: Date.now() - start,
|
|
898
|
+
userId: interaction.user.id,
|
|
899
|
+
userTag: interaction.user.tag,
|
|
900
|
+
guildId: interaction.guildId,
|
|
901
|
+
channelId: interaction.channelId,
|
|
902
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
903
|
+
});
|
|
904
|
+
} catch (error) {
|
|
905
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
906
|
+
this.onUsage?.({
|
|
907
|
+
type: "command",
|
|
908
|
+
name: command2.name,
|
|
909
|
+
detail: command2.kind,
|
|
910
|
+
outcome: "error",
|
|
911
|
+
errorMessage: err.message,
|
|
912
|
+
durationMs: Date.now() - start,
|
|
913
|
+
userId: interaction.user.id,
|
|
914
|
+
userTag: interaction.user.tag,
|
|
915
|
+
guildId: interaction.guildId,
|
|
916
|
+
channelId: interaction.channelId,
|
|
917
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
918
|
+
});
|
|
919
|
+
interaction.client.emit("error", err);
|
|
920
|
+
try {
|
|
921
|
+
if (!interaction.replied && !interaction.deferred) {
|
|
922
|
+
await interaction.reply({
|
|
923
|
+
content: "Something went wrong.",
|
|
924
|
+
flags: discord_js.MessageFlags.Ephemeral
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
} catch {
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
};
|
|
932
|
+
function combineGuards(defaults, own) {
|
|
933
|
+
if (own === void 0 || own.length === 0) return defaults;
|
|
934
|
+
if (defaults.length === 0) return own;
|
|
935
|
+
return [...defaults, ...own];
|
|
936
|
+
}
|
|
937
|
+
function actorOf(interaction) {
|
|
938
|
+
const member = interaction.member;
|
|
939
|
+
let roleIds = [];
|
|
940
|
+
if (member !== null) {
|
|
941
|
+
const roles = member.roles;
|
|
942
|
+
roleIds = Array.isArray(roles) ? roles : [...roles.cache.keys()];
|
|
943
|
+
}
|
|
944
|
+
return {
|
|
945
|
+
userId: interaction.user.id,
|
|
946
|
+
roleIds,
|
|
947
|
+
guildId: interaction.guildId,
|
|
948
|
+
channelId: interaction.channelId
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
function clientEmbeds(client) {
|
|
952
|
+
return client.embeds ?? defaultEmbeds;
|
|
953
|
+
}
|
|
954
|
+
async function replyCooldown(interaction, config, remaining) {
|
|
955
|
+
const content = formatCooldownMessage(config, remaining);
|
|
956
|
+
try {
|
|
957
|
+
if (interaction.deferred) await interaction.editReply({ content });
|
|
958
|
+
else if (interaction.replied) await interaction.followUp({ content, flags: discord_js.MessageFlags.Ephemeral });
|
|
959
|
+
else await interaction.reply({ content, flags: discord_js.MessageFlags.Ephemeral });
|
|
960
|
+
} catch {
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
async function replyDenied(interaction, reason) {
|
|
964
|
+
const embeds = clientEmbeds(interaction.client);
|
|
965
|
+
const text = reason ?? "You don't have permission to use this.";
|
|
966
|
+
try {
|
|
967
|
+
const payload = { embeds: [embeds.error(text)], flags: discord_js.MessageFlags.Ephemeral };
|
|
968
|
+
if (interaction.deferred) await interaction.editReply({ embeds: payload.embeds });
|
|
969
|
+
else if (interaction.replied) await interaction.followUp(payload);
|
|
970
|
+
else await interaction.reply(payload);
|
|
971
|
+
} catch {
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// src/prefix-args.ts
|
|
976
|
+
var SNOWFLAKE_RE = /^\d{15,21}$/;
|
|
977
|
+
var USER_MENTION_RE = /^<@!?(\d{15,21})>$/;
|
|
978
|
+
var CHANNEL_MENTION_RE = /^<#(\d{15,21})>$/;
|
|
979
|
+
var ROLE_MENTION_RE = /^<@&(\d{15,21})>$/;
|
|
980
|
+
function extractSnowflake(input) {
|
|
981
|
+
if (SNOWFLAKE_RE.test(input)) return input;
|
|
982
|
+
const m = USER_MENTION_RE.exec(input) ?? CHANNEL_MENTION_RE.exec(input) ?? ROLE_MENTION_RE.exec(input);
|
|
983
|
+
return m === null ? null : m[1] ?? null;
|
|
984
|
+
}
|
|
985
|
+
var PrefixArgsBuilder = class _PrefixArgsBuilder {
|
|
986
|
+
specs;
|
|
987
|
+
/** @internal */
|
|
988
|
+
constructor(specs = []) {
|
|
989
|
+
this.specs = specs;
|
|
990
|
+
}
|
|
991
|
+
/** A raw string token. */
|
|
992
|
+
string(name, options) {
|
|
993
|
+
return this.push({ name, kind: "string", required: options?.required ?? false, defaultValue: options?.default });
|
|
994
|
+
}
|
|
995
|
+
/** A whole integer. */
|
|
996
|
+
integer(name, options) {
|
|
997
|
+
return this.push({ name, kind: "integer", required: options?.required ?? false, defaultValue: options?.default });
|
|
998
|
+
}
|
|
999
|
+
/** A floating-point number. */
|
|
1000
|
+
number(name, options) {
|
|
1001
|
+
return this.push({ name, kind: "number", required: options?.required ?? false, defaultValue: options?.default });
|
|
1002
|
+
}
|
|
1003
|
+
/** A boolean (`true`/`yes`/`1`/`on` vs `false`/`no`/`0`/`off`). */
|
|
1004
|
+
boolean(name, options) {
|
|
1005
|
+
return this.push({ name, kind: "boolean", required: options?.required ?? false, defaultValue: options?.default });
|
|
1006
|
+
}
|
|
1007
|
+
/** A Discord snowflake id — accepts raw ids and `<@u>` / `<#c>` / `<@&r>` mentions. */
|
|
1008
|
+
snowflake(name, options) {
|
|
1009
|
+
return this.push({ name, kind: "snowflake", required: options?.required ?? false, defaultValue: options?.default });
|
|
1010
|
+
}
|
|
1011
|
+
/** A duration like `"1h30m"` or `"1 saat"` parsed to milliseconds. */
|
|
1012
|
+
duration(name, options) {
|
|
1013
|
+
return this.push({ name, kind: "duration", required: options?.required ?? false, defaultValue: options?.default });
|
|
1014
|
+
}
|
|
1015
|
+
/** The remainder of the message (everything after previous args). */
|
|
1016
|
+
rest(name, options) {
|
|
1017
|
+
return this.push({ name, kind: "rest", required: options?.required ?? false, defaultValue: options?.default });
|
|
1018
|
+
}
|
|
1019
|
+
push(spec) {
|
|
1020
|
+
return new _PrefixArgsBuilder([...this.specs, spec]);
|
|
1021
|
+
}
|
|
1022
|
+
/** Compile this builder into a parser. */
|
|
1023
|
+
compile() {
|
|
1024
|
+
const specs = this.specs;
|
|
1025
|
+
return {
|
|
1026
|
+
specs,
|
|
1027
|
+
parse(tokens, rest) {
|
|
1028
|
+
const out = {};
|
|
1029
|
+
let idx = 0;
|
|
1030
|
+
for (let i = 0; i < specs.length; i++) {
|
|
1031
|
+
const spec = specs[i];
|
|
1032
|
+
if (spec.kind === "rest") {
|
|
1033
|
+
const tail = idx === 0 ? rest : tokens.slice(idx).join(" ");
|
|
1034
|
+
if (tail.length === 0) {
|
|
1035
|
+
if (spec.required) return { ok: false, arg: spec.name, reason: `missing required argument "${spec.name}"` };
|
|
1036
|
+
out[spec.name] = spec.defaultValue;
|
|
1037
|
+
} else {
|
|
1038
|
+
out[spec.name] = tail;
|
|
1039
|
+
}
|
|
1040
|
+
idx = tokens.length;
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
const token = tokens[idx];
|
|
1044
|
+
if (token === void 0) {
|
|
1045
|
+
if (spec.required) {
|
|
1046
|
+
return { ok: false, arg: spec.name, reason: `missing required argument "${spec.name}"` };
|
|
1047
|
+
}
|
|
1048
|
+
out[spec.name] = spec.defaultValue;
|
|
1049
|
+
continue;
|
|
1050
|
+
}
|
|
1051
|
+
const parsed = coerce(spec, token);
|
|
1052
|
+
if (parsed.ok === false) return { ok: false, arg: spec.name, reason: parsed.reason };
|
|
1053
|
+
out[spec.name] = parsed.value;
|
|
1054
|
+
idx += 1;
|
|
1055
|
+
}
|
|
1056
|
+
return { ok: true, values: out };
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
};
|
|
1061
|
+
function coerce(spec, token) {
|
|
1062
|
+
switch (spec.kind) {
|
|
1063
|
+
case "string":
|
|
1064
|
+
return { ok: true, value: token };
|
|
1065
|
+
case "integer": {
|
|
1066
|
+
const n = Number(token);
|
|
1067
|
+
if (!Number.isInteger(n)) return { ok: false, reason: `"${token}" is not an integer` };
|
|
1068
|
+
return { ok: true, value: n };
|
|
1069
|
+
}
|
|
1070
|
+
case "number": {
|
|
1071
|
+
const n = Number(token);
|
|
1072
|
+
if (!Number.isFinite(n)) return { ok: false, reason: `"${token}" is not a number` };
|
|
1073
|
+
return { ok: true, value: n };
|
|
1074
|
+
}
|
|
1075
|
+
case "boolean": {
|
|
1076
|
+
const low = token.toLowerCase();
|
|
1077
|
+
if (["true", "1", "yes", "on"].includes(low)) return { ok: true, value: true };
|
|
1078
|
+
if (["false", "0", "no", "off"].includes(low)) return { ok: true, value: false };
|
|
1079
|
+
return { ok: false, reason: `"${token}" is not a boolean` };
|
|
1080
|
+
}
|
|
1081
|
+
case "snowflake": {
|
|
1082
|
+
const id = extractSnowflake(token);
|
|
1083
|
+
if (id === null) return { ok: false, reason: `"${token}" is not a snowflake or mention` };
|
|
1084
|
+
return { ok: true, value: id };
|
|
1085
|
+
}
|
|
1086
|
+
case "duration": {
|
|
1087
|
+
const ms = parseDuration(token);
|
|
1088
|
+
if (ms === null) return { ok: false, reason: `"${token}" is not a duration` };
|
|
1089
|
+
return { ok: true, value: ms };
|
|
1090
|
+
}
|
|
1091
|
+
default:
|
|
1092
|
+
return { ok: false, reason: `unknown arg kind for "${spec.name}"` };
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
function prefixArgs() {
|
|
1096
|
+
return new PrefixArgsBuilder();
|
|
1097
|
+
}
|
|
1098
|
+
function normalisePayload(render) {
|
|
1099
|
+
if (typeof render.setColor === "function" && typeof render.toJSON === "function") {
|
|
1100
|
+
return { embeds: [render] };
|
|
1101
|
+
}
|
|
1102
|
+
if (Array.isArray(render)) {
|
|
1103
|
+
return { embeds: render };
|
|
1104
|
+
}
|
|
1105
|
+
return render;
|
|
1106
|
+
}
|
|
1107
|
+
function controlsRow(page, pages, ns, controls, labels) {
|
|
1108
|
+
const buttons = [];
|
|
1109
|
+
if (controls === "first-prev-next-last") {
|
|
1110
|
+
buttons.push(
|
|
1111
|
+
new discord_js.ButtonBuilder().setCustomId(`${ns}:first`).setStyle(discord_js.ButtonStyle.Secondary).setLabel(labels.first).setDisabled(page === 0)
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
buttons.push(
|
|
1115
|
+
new discord_js.ButtonBuilder().setCustomId(`${ns}:prev`).setStyle(discord_js.ButtonStyle.Primary).setLabel(labels.prev).setDisabled(page === 0),
|
|
1116
|
+
new discord_js.ButtonBuilder().setCustomId(`${ns}:next`).setStyle(discord_js.ButtonStyle.Primary).setLabel(labels.next).setDisabled(page >= pages - 1)
|
|
1117
|
+
);
|
|
1118
|
+
if (controls === "first-prev-next-last") {
|
|
1119
|
+
buttons.push(
|
|
1120
|
+
new discord_js.ButtonBuilder().setCustomId(`${ns}:last`).setStyle(discord_js.ButtonStyle.Secondary).setLabel(labels.last).setDisabled(page >= pages - 1)
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
return new discord_js.ActionRowBuilder().addComponents(...buttons);
|
|
1124
|
+
}
|
|
1125
|
+
function resolveLabels2(input) {
|
|
1126
|
+
return {
|
|
1127
|
+
first: input?.first ?? "\xAB",
|
|
1128
|
+
prev: input?.prev ?? "\u2039",
|
|
1129
|
+
next: input?.next ?? "\u203A",
|
|
1130
|
+
last: input?.last ?? "\xBB"
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
async function buildPaginatorPage(items, page, options) {
|
|
1134
|
+
const pageSize = options.pageSize ?? 10;
|
|
1135
|
+
const pages = Math.max(1, Math.ceil(items.length / pageSize));
|
|
1136
|
+
const ns = options.namespace ?? "spk-page";
|
|
1137
|
+
const controls = options.controls ?? "prev-next";
|
|
1138
|
+
const labels = resolveLabels2(options.labels);
|
|
1139
|
+
const slice = items.slice(page * pageSize, page * pageSize + pageSize);
|
|
1140
|
+
const body = await options.render(slice, { page, pages });
|
|
1141
|
+
const payload = normalisePayload(body);
|
|
1142
|
+
const components = pages > 1 ? [controlsRow(page, pages, ns, controls, labels)] : [];
|
|
1143
|
+
return { payload: { ...payload, components }, pages };
|
|
1144
|
+
}
|
|
1145
|
+
async function paginate(interaction, items, options) {
|
|
1146
|
+
const pageSize = options.pageSize ?? 10;
|
|
1147
|
+
const ns = options.namespace ?? "spk-page";
|
|
1148
|
+
const controls = options.controls ?? "prev-next";
|
|
1149
|
+
const labels = resolveLabels2(options.labels);
|
|
1150
|
+
const allowedUser = options.user ?? interaction.user.id;
|
|
1151
|
+
let page = 0;
|
|
1152
|
+
const buildPage = async () => {
|
|
1153
|
+
return buildPaginatorPage(items, page, { ...options, pageSize, namespace: ns, controls, labels });
|
|
1154
|
+
};
|
|
1155
|
+
const { payload: initial, pages } = await buildPage();
|
|
1156
|
+
const sent = interaction.deferred ? await interaction.editReply(initial) : (await interaction.reply({
|
|
1157
|
+
...initial,
|
|
1158
|
+
flags: options.ephemeral === true ? 64 : void 0
|
|
1159
|
+
}), await interaction.fetchReply());
|
|
1160
|
+
if (pages <= 1) return;
|
|
1161
|
+
const collector = sent.createMessageComponentCollector({
|
|
1162
|
+
componentType: discord_js.ComponentType.Button,
|
|
1163
|
+
time: options.timeoutMs ?? 5 * 6e4,
|
|
1164
|
+
filter: (i) => i.user.id === allowedUser && i.customId.startsWith(`${ns}:`)
|
|
1165
|
+
});
|
|
1166
|
+
collector.on("collect", async (button2) => {
|
|
1167
|
+
const action = button2.customId.slice(ns.length + 1);
|
|
1168
|
+
if (action === "first") page = 0;
|
|
1169
|
+
else if (action === "prev") page = Math.max(0, page - 1);
|
|
1170
|
+
else if (action === "next") page = Math.min(pages - 1, page + 1);
|
|
1171
|
+
else if (action === "last") page = pages - 1;
|
|
1172
|
+
const next = await buildPage();
|
|
1173
|
+
await button2.update(next.payload).catch(() => void 0);
|
|
1174
|
+
});
|
|
1175
|
+
collector.on("end", async () => {
|
|
1176
|
+
const disabledRow = controlsRow(page, pages, ns, controls, labels);
|
|
1177
|
+
for (const c of disabledRow.components) c.setDisabled(true);
|
|
1178
|
+
const { payload: final } = await buildPage();
|
|
1179
|
+
await interaction.editReply({ ...final, components: [disabledRow] }).catch(() => void 0);
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
var STYLE_MAP = {
|
|
1183
|
+
Primary: discord_js.ButtonStyle.Primary,
|
|
1184
|
+
Secondary: discord_js.ButtonStyle.Secondary,
|
|
1185
|
+
Success: discord_js.ButtonStyle.Success,
|
|
1186
|
+
Danger: discord_js.ButtonStyle.Danger
|
|
1187
|
+
};
|
|
1188
|
+
function clientEmbeds2(client) {
|
|
1189
|
+
return client.embeds ?? defaultEmbeds;
|
|
1190
|
+
}
|
|
1191
|
+
async function confirm(interaction, options) {
|
|
1192
|
+
const ns = options.namespace ?? "spk-confirm";
|
|
1193
|
+
const confirmLabel = options.confirm?.label ?? "Confirm";
|
|
1194
|
+
const cancelLabel = options.cancel?.label ?? "Cancel";
|
|
1195
|
+
const confirmStyle = STYLE_MAP[options.confirm?.style ?? "Success"];
|
|
1196
|
+
const cancelStyle = STYLE_MAP[options.cancel?.style ?? "Secondary"];
|
|
1197
|
+
const user = options.user ?? interaction.user.id;
|
|
1198
|
+
const ephemeral = options.ephemeral !== false;
|
|
1199
|
+
const embeds = clientEmbeds2(interaction.client);
|
|
1200
|
+
const promptEmbed = embeds.info(
|
|
1201
|
+
options.title !== void 0 ? { title: options.title, description: options.body } : options.body
|
|
1202
|
+
);
|
|
1203
|
+
const row2 = new discord_js.ActionRowBuilder().addComponents(
|
|
1204
|
+
new discord_js.ButtonBuilder().setCustomId(`${ns}:yes`).setLabel(confirmLabel).setStyle(confirmStyle),
|
|
1205
|
+
new discord_js.ButtonBuilder().setCustomId(`${ns}:no`).setLabel(cancelLabel).setStyle(cancelStyle)
|
|
1206
|
+
);
|
|
1207
|
+
const payload = { embeds: [promptEmbed], components: [row2] };
|
|
1208
|
+
const sent = interaction.deferred ? await interaction.editReply(payload) : (await interaction.reply({
|
|
1209
|
+
...payload,
|
|
1210
|
+
flags: ephemeral ? 64 : void 0
|
|
1211
|
+
}), await interaction.fetchReply());
|
|
1212
|
+
return new Promise((resolve) => {
|
|
1213
|
+
const collector = sent.createMessageComponentCollector({
|
|
1214
|
+
componentType: discord_js.ComponentType.Button,
|
|
1215
|
+
time: options.timeoutMs ?? 3e4,
|
|
1216
|
+
max: 1,
|
|
1217
|
+
filter: (i) => i.user.id === user && i.customId.startsWith(`${ns}:`)
|
|
1218
|
+
});
|
|
1219
|
+
let outcome = null;
|
|
1220
|
+
collector.on("collect", async (button2) => {
|
|
1221
|
+
const action = button2.customId.slice(ns.length + 1);
|
|
1222
|
+
outcome = {
|
|
1223
|
+
confirmed: action === "yes",
|
|
1224
|
+
reason: action === "yes" ? "confirm" : "cancel",
|
|
1225
|
+
interaction: button2
|
|
1226
|
+
};
|
|
1227
|
+
await button2.deferUpdate().catch(() => void 0);
|
|
1228
|
+
});
|
|
1229
|
+
collector.on("end", async () => {
|
|
1230
|
+
for (const c of row2.components) c.setDisabled(true);
|
|
1231
|
+
await interaction.editReply({ embeds: [promptEmbed], components: [row2] }).catch(() => void 0);
|
|
1232
|
+
resolve(outcome ?? { confirmed: false, reason: "timeout" });
|
|
1233
|
+
});
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
var RANK = {
|
|
1237
|
+
debug: 10,
|
|
1238
|
+
info: 20,
|
|
1239
|
+
warn: 30,
|
|
1240
|
+
error: 40,
|
|
1241
|
+
silent: Number.POSITIVE_INFINITY
|
|
1242
|
+
};
|
|
1243
|
+
function formatValue(value) {
|
|
1244
|
+
return typeof value === "string" ? value : String(value);
|
|
1245
|
+
}
|
|
1246
|
+
function consoleSink(entry) {
|
|
1247
|
+
const scope = entry.scope !== void 0 ? ` [${entry.scope}]` : "";
|
|
1248
|
+
let suffix = "";
|
|
1249
|
+
if (entry.data !== void 0) {
|
|
1250
|
+
const parts = Object.entries(entry.data).map(([k, v]) => `${k}=${formatValue(v)}`);
|
|
1251
|
+
if (parts.length > 0) suffix = ` ${parts.join(" ")}`;
|
|
1252
|
+
}
|
|
1253
|
+
const line = `${entry.timestamp.toISOString()} ${entry.level.toUpperCase()}${scope} ${entry.message}${suffix}`;
|
|
1254
|
+
const write = entry.level === "warn" || entry.level === "error" ? console.error : console.log;
|
|
1255
|
+
write(line);
|
|
1256
|
+
if (entry.error !== void 0) write(entry.error.stack ?? String(entry.error));
|
|
1257
|
+
}
|
|
1258
|
+
function jsonlSink(path$1, options = {}) {
|
|
1259
|
+
const min = options.minLevel ?? "debug";
|
|
1260
|
+
let dirReady = false;
|
|
1261
|
+
let chain = Promise.resolve();
|
|
1262
|
+
return (entry) => {
|
|
1263
|
+
if (RANK[entry.level] < RANK[min]) return;
|
|
1264
|
+
const record = {
|
|
1265
|
+
...entry,
|
|
1266
|
+
timestamp: entry.timestamp.toISOString(),
|
|
1267
|
+
error: entry.error ? { name: entry.error.name, message: entry.error.message, stack: entry.error.stack } : void 0
|
|
1268
|
+
};
|
|
1269
|
+
const line = `${JSON.stringify(record)}
|
|
1270
|
+
`;
|
|
1271
|
+
chain = chain.then(async () => {
|
|
1272
|
+
try {
|
|
1273
|
+
if (!dirReady) {
|
|
1274
|
+
await promises.mkdir(path.dirname(path$1), { recursive: true });
|
|
1275
|
+
dirReady = true;
|
|
1276
|
+
}
|
|
1277
|
+
await promises.appendFile(path$1, line, "utf8");
|
|
1278
|
+
} catch {
|
|
1279
|
+
}
|
|
1280
|
+
});
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
function webhookSink(options) {
|
|
1284
|
+
const min = options.minLevel ?? "warn";
|
|
1285
|
+
return (entry) => {
|
|
1286
|
+
if (RANK[entry.level] < RANK[min]) return;
|
|
1287
|
+
const color = entry.level === "error" ? 15747655 : entry.level === "warn" ? 16361509 : 3447003;
|
|
1288
|
+
const fields = [];
|
|
1289
|
+
if (entry.scope !== void 0) fields.push({ name: "Scope", value: entry.scope, inline: true });
|
|
1290
|
+
if (entry.data !== void 0) {
|
|
1291
|
+
for (const [k, v] of Object.entries(entry.data)) {
|
|
1292
|
+
fields.push({ name: k, value: formatValue(v).slice(0, 1e3), inline: true });
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
const desc = entry.error?.stack !== void 0 ? `${entry.message}
|
|
1296
|
+
\`\`\`
|
|
1297
|
+
${entry.error.stack.slice(0, 1800)}
|
|
1298
|
+
\`\`\`` : entry.message;
|
|
1299
|
+
const body = {
|
|
1300
|
+
username: options.username ?? "spearkit",
|
|
1301
|
+
embeds: [
|
|
1302
|
+
{
|
|
1303
|
+
title: `[${entry.level.toUpperCase()}] ${entry.message.slice(0, 240)}`,
|
|
1304
|
+
description: desc.slice(0, 4e3),
|
|
1305
|
+
color,
|
|
1306
|
+
timestamp: entry.timestamp.toISOString(),
|
|
1307
|
+
fields: fields.slice(0, 25)
|
|
1308
|
+
}
|
|
1309
|
+
]
|
|
1310
|
+
};
|
|
1311
|
+
void fetch(options.url, {
|
|
1312
|
+
method: "POST",
|
|
1313
|
+
headers: { "content-type": "application/json" },
|
|
1314
|
+
body: JSON.stringify(body)
|
|
1315
|
+
}).catch(() => void 0);
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
function resolveTransports(options) {
|
|
1319
|
+
if (options.transports !== void 0 && options.transports.length > 0) return options.transports;
|
|
1320
|
+
if (options.sink !== void 0) return [options.sink];
|
|
1321
|
+
return [consoleSink];
|
|
1322
|
+
}
|
|
1323
|
+
var Logger = class _Logger {
|
|
1324
|
+
state;
|
|
1325
|
+
/** The scope prefix applied to every entry, if any. */
|
|
1326
|
+
scope;
|
|
1327
|
+
constructor(options = {}) {
|
|
1328
|
+
this.state = {
|
|
1329
|
+
threshold: options.level ?? "info",
|
|
1330
|
+
transports: resolveTransports(options)
|
|
1331
|
+
};
|
|
1332
|
+
this.scope = options.scope;
|
|
1333
|
+
}
|
|
1334
|
+
/** The current minimum threshold. */
|
|
1335
|
+
get level() {
|
|
1336
|
+
return this.state.threshold;
|
|
1337
|
+
}
|
|
1338
|
+
/** Change the threshold for this logger and every child sharing its state. */
|
|
1339
|
+
setLevel(level) {
|
|
1340
|
+
this.state.threshold = level;
|
|
1341
|
+
return this;
|
|
1342
|
+
}
|
|
1343
|
+
/** Replace the transport list for this logger and every child sharing its state. */
|
|
1344
|
+
setTransports(transports) {
|
|
1345
|
+
this.state.transports = transports;
|
|
1346
|
+
return this;
|
|
1347
|
+
}
|
|
1348
|
+
/** Append a transport to the existing list. */
|
|
1349
|
+
addTransport(sink) {
|
|
1350
|
+
this.state.transports = [...this.state.transports, sink];
|
|
1351
|
+
return this;
|
|
1352
|
+
}
|
|
1353
|
+
/** Whether an entry of `level` would currently be emitted. */
|
|
1354
|
+
enabled(level) {
|
|
1355
|
+
return RANK[level] >= RANK[this.state.threshold];
|
|
1356
|
+
}
|
|
1357
|
+
/** A child logger with an extra scope segment, sharing this logger's state. */
|
|
1358
|
+
child(scope) {
|
|
1359
|
+
const combined = this.scope !== void 0 ? `${this.scope}:${scope}` : scope;
|
|
1360
|
+
const child = new _Logger({ scope: combined });
|
|
1361
|
+
child.state = this.state;
|
|
1362
|
+
return child;
|
|
1363
|
+
}
|
|
1364
|
+
/** Emit an entry at an explicit level. */
|
|
1365
|
+
log(level, message, options) {
|
|
1366
|
+
if (!this.enabled(level)) return;
|
|
1367
|
+
const entry = {
|
|
1368
|
+
level,
|
|
1369
|
+
message,
|
|
1370
|
+
scope: this.scope,
|
|
1371
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1372
|
+
error: options?.error,
|
|
1373
|
+
data: options?.data
|
|
1374
|
+
};
|
|
1375
|
+
for (const sink of this.state.transports) {
|
|
1376
|
+
try {
|
|
1377
|
+
sink(entry);
|
|
1378
|
+
} catch {
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
/** Verbose diagnostics, off by default. */
|
|
1383
|
+
debug(message, options) {
|
|
1384
|
+
this.log("debug", message, options);
|
|
1385
|
+
}
|
|
1386
|
+
/** Normal operational messages. */
|
|
1387
|
+
info(message, options) {
|
|
1388
|
+
this.log("info", message, options);
|
|
1389
|
+
}
|
|
1390
|
+
/** Recoverable problems worth attention. */
|
|
1391
|
+
warn(message, options) {
|
|
1392
|
+
this.log("warn", message, options);
|
|
1393
|
+
}
|
|
1394
|
+
/** Failures. Attach the cause via `{ error }`. */
|
|
1395
|
+
error(message, options) {
|
|
1396
|
+
this.log("error", message, options);
|
|
1397
|
+
}
|
|
1398
|
+
};
|
|
1399
|
+
function toError(value) {
|
|
1400
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
1401
|
+
}
|
|
1402
|
+
function stripInlineComment(value) {
|
|
1403
|
+
const match = /\s#/.exec(value);
|
|
1404
|
+
return match !== null ? value.slice(0, match.index).trimEnd() : value;
|
|
1405
|
+
}
|
|
1406
|
+
function unquote(raw) {
|
|
1407
|
+
if (raw.length >= 2) {
|
|
1408
|
+
const quote = raw[0];
|
|
1409
|
+
if ((quote === '"' || quote === "'") && raw.endsWith(quote)) {
|
|
1410
|
+
const inner = raw.slice(1, -1);
|
|
1411
|
+
return quote === '"' ? inner.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ") : inner;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
return stripInlineComment(raw);
|
|
1415
|
+
}
|
|
1416
|
+
function parseEnv(content) {
|
|
1417
|
+
const out = {};
|
|
1418
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
1419
|
+
const line = rawLine.trim();
|
|
1420
|
+
if (line.length === 0 || line.startsWith("#")) continue;
|
|
1421
|
+
const body = line.startsWith("export ") ? line.slice(7).trimStart() : line;
|
|
1422
|
+
const eq = body.indexOf("=");
|
|
1423
|
+
if (eq <= 0) continue;
|
|
1424
|
+
const key = body.slice(0, eq).trim();
|
|
1425
|
+
if (key.length === 0) continue;
|
|
1426
|
+
out[key] = unquote(body.slice(eq + 1).trim());
|
|
1427
|
+
}
|
|
1428
|
+
return out;
|
|
1429
|
+
}
|
|
1430
|
+
function loadEnv(options = {}) {
|
|
1431
|
+
const path$1 = options.path ?? path.join(process.cwd(), ".env");
|
|
1432
|
+
let content;
|
|
1433
|
+
try {
|
|
1434
|
+
content = fs.readFileSync(path$1, "utf8");
|
|
1435
|
+
} catch {
|
|
1436
|
+
return {};
|
|
1437
|
+
}
|
|
1438
|
+
const parsed = parseEnv(content);
|
|
1439
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
1440
|
+
if (options.override === true || process.env[key] === void 0) {
|
|
1441
|
+
process.env[key] = value;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
return parsed;
|
|
1445
|
+
}
|
|
1446
|
+
var TRUTHY = /* @__PURE__ */ new Set(["true", "1", "yes", "on"]);
|
|
1447
|
+
var FALSY = /* @__PURE__ */ new Set(["false", "0", "no", "off"]);
|
|
1448
|
+
function read(key) {
|
|
1449
|
+
const value = process.env[key];
|
|
1450
|
+
return value !== void 0 && value !== "" ? value : void 0;
|
|
1451
|
+
}
|
|
1452
|
+
function envString(key, fallback) {
|
|
1453
|
+
return read(key) ?? fallback;
|
|
1454
|
+
}
|
|
1455
|
+
function envNumber(key, fallback) {
|
|
1456
|
+
const value = read(key);
|
|
1457
|
+
if (value === void 0) return fallback;
|
|
1458
|
+
const parsed = Number(value);
|
|
1459
|
+
return Number.isNaN(parsed) ? fallback : parsed;
|
|
1460
|
+
}
|
|
1461
|
+
function envBoolean(key, fallback) {
|
|
1462
|
+
const value = read(key)?.toLowerCase();
|
|
1463
|
+
if (value === void 0) return fallback;
|
|
1464
|
+
if (TRUTHY.has(value)) return true;
|
|
1465
|
+
if (FALSY.has(value)) return false;
|
|
1466
|
+
return fallback;
|
|
1467
|
+
}
|
|
1468
|
+
function envRequire(key) {
|
|
1469
|
+
const value = read(key);
|
|
1470
|
+
if (value === void 0) {
|
|
1471
|
+
throw new Error(`spearkit: required environment variable "${key}" is missing or empty`);
|
|
1472
|
+
}
|
|
1473
|
+
return value;
|
|
1474
|
+
}
|
|
1475
|
+
var env = {
|
|
1476
|
+
string: envString,
|
|
1477
|
+
number: envNumber,
|
|
1478
|
+
boolean: envBoolean,
|
|
1479
|
+
require: envRequire
|
|
1480
|
+
};
|
|
1481
|
+
|
|
1482
|
+
// src/scheduler.ts
|
|
1483
|
+
var ALIASES = {
|
|
1484
|
+
"@yearly": "0 0 1 1 *",
|
|
1485
|
+
"@annually": "0 0 1 1 *",
|
|
1486
|
+
"@monthly": "0 0 1 * *",
|
|
1487
|
+
"@weekly": "0 0 * * 0",
|
|
1488
|
+
"@daily": "0 0 * * *",
|
|
1489
|
+
"@midnight": "0 0 * * *",
|
|
1490
|
+
"@hourly": "0 * * * *"
|
|
1491
|
+
};
|
|
1492
|
+
function parseField(spec, min, max, label) {
|
|
1493
|
+
const set = /* @__PURE__ */ new Set();
|
|
1494
|
+
for (const part of spec.split(",")) {
|
|
1495
|
+
let range = part;
|
|
1496
|
+
let step = 1;
|
|
1497
|
+
const slash = part.indexOf("/");
|
|
1498
|
+
if (slash >= 0) {
|
|
1499
|
+
step = Number(part.slice(slash + 1));
|
|
1500
|
+
range = part.slice(0, slash);
|
|
1501
|
+
if (!Number.isInteger(step) || step <= 0) {
|
|
1502
|
+
throw new Error(`spearkit: invalid step in cron ${label} field "${part}"`);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
let lo;
|
|
1506
|
+
let hi;
|
|
1507
|
+
if (range === "*") {
|
|
1508
|
+
lo = min;
|
|
1509
|
+
hi = max;
|
|
1510
|
+
} else if (range.includes("-")) {
|
|
1511
|
+
const dash = range.indexOf("-");
|
|
1512
|
+
lo = Number(range.slice(0, dash));
|
|
1513
|
+
hi = Number(range.slice(dash + 1));
|
|
1514
|
+
} else {
|
|
1515
|
+
lo = Number(range);
|
|
1516
|
+
hi = lo;
|
|
1517
|
+
}
|
|
1518
|
+
if (!Number.isInteger(lo) || !Number.isInteger(hi) || lo < min || hi > max || lo > hi) {
|
|
1519
|
+
throw new Error(`spearkit: cron ${label} field out of range ${min}-${max}: "${part}"`);
|
|
1520
|
+
}
|
|
1521
|
+
for (let value = lo; value <= hi; value += step) set.add(value);
|
|
1522
|
+
}
|
|
1523
|
+
return set;
|
|
1524
|
+
}
|
|
1525
|
+
var CronExpression = class {
|
|
1526
|
+
/** The original expression string. */
|
|
1527
|
+
source;
|
|
1528
|
+
minutes;
|
|
1529
|
+
hours;
|
|
1530
|
+
daysOfMonth;
|
|
1531
|
+
months;
|
|
1532
|
+
daysOfWeek;
|
|
1533
|
+
domRestricted;
|
|
1534
|
+
dowRestricted;
|
|
1535
|
+
constructor(expression) {
|
|
1536
|
+
const trimmed = expression.trim();
|
|
1537
|
+
const normalized = ALIASES[trimmed] ?? trimmed;
|
|
1538
|
+
const parts = normalized.split(/\s+/);
|
|
1539
|
+
if (parts.length !== 5) {
|
|
1540
|
+
throw new Error(`spearkit: cron expression must have 5 fields, got "${expression}"`);
|
|
1541
|
+
}
|
|
1542
|
+
const [minute, hour, dom, month, dow] = parts;
|
|
1543
|
+
if (minute === void 0 || hour === void 0 || dom === void 0 || month === void 0 || dow === void 0) {
|
|
1544
|
+
throw new Error(`spearkit: invalid cron expression "${expression}"`);
|
|
1545
|
+
}
|
|
1546
|
+
this.source = expression;
|
|
1547
|
+
this.minutes = parseField(minute, 0, 59, "minute");
|
|
1548
|
+
this.hours = parseField(hour, 0, 23, "hour");
|
|
1549
|
+
this.daysOfMonth = parseField(dom, 1, 31, "day-of-month");
|
|
1550
|
+
this.months = parseField(month, 1, 12, "month");
|
|
1551
|
+
const weekdays = parseField(dow, 0, 7, "day-of-week");
|
|
1552
|
+
if (weekdays.has(7)) {
|
|
1553
|
+
weekdays.delete(7);
|
|
1554
|
+
weekdays.add(0);
|
|
1555
|
+
}
|
|
1556
|
+
this.daysOfWeek = weekdays;
|
|
1557
|
+
this.domRestricted = dom !== "*";
|
|
1558
|
+
this.dowRestricted = dow !== "*";
|
|
1559
|
+
}
|
|
1560
|
+
dayMatches(date) {
|
|
1561
|
+
const dom = this.daysOfMonth.has(date.getDate());
|
|
1562
|
+
const dow = this.daysOfWeek.has(date.getDay());
|
|
1563
|
+
if (this.domRestricted && this.dowRestricted) return dom || dow;
|
|
1564
|
+
if (this.domRestricted) return dom;
|
|
1565
|
+
if (this.dowRestricted) return dow;
|
|
1566
|
+
return true;
|
|
1567
|
+
}
|
|
1568
|
+
/** The next time strictly after `from` (default now) that matches. */
|
|
1569
|
+
next(from = /* @__PURE__ */ new Date()) {
|
|
1570
|
+
const date = new Date(from.getTime());
|
|
1571
|
+
date.setSeconds(0, 0);
|
|
1572
|
+
date.setMinutes(date.getMinutes() + 1);
|
|
1573
|
+
for (let guard2 = 0; guard2 < 1e5; guard2++) {
|
|
1574
|
+
if (!this.months.has(date.getMonth() + 1)) {
|
|
1575
|
+
date.setMonth(date.getMonth() + 1, 1);
|
|
1576
|
+
date.setHours(0, 0, 0, 0);
|
|
1577
|
+
continue;
|
|
1578
|
+
}
|
|
1579
|
+
if (!this.dayMatches(date)) {
|
|
1580
|
+
date.setDate(date.getDate() + 1);
|
|
1581
|
+
date.setHours(0, 0, 0, 0);
|
|
1582
|
+
continue;
|
|
1583
|
+
}
|
|
1584
|
+
if (!this.hours.has(date.getHours())) {
|
|
1585
|
+
date.setHours(date.getHours() + 1, 0, 0, 0);
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
if (!this.minutes.has(date.getMinutes())) {
|
|
1589
|
+
date.setMinutes(date.getMinutes() + 1, 0, 0);
|
|
1590
|
+
continue;
|
|
1591
|
+
}
|
|
1592
|
+
return new Date(date.getTime());
|
|
1593
|
+
}
|
|
1594
|
+
throw new Error(`spearkit: cron expression "${this.source}" has no upcoming match`);
|
|
1595
|
+
}
|
|
1596
|
+
};
|
|
1597
|
+
function cron(expression) {
|
|
1598
|
+
return new CronExpression(expression);
|
|
1599
|
+
}
|
|
1600
|
+
function task(config) {
|
|
1601
|
+
if (config.cron === void 0 && config.interval === void 0) {
|
|
1602
|
+
throw new Error(`spearkit: task "${config.name}" needs a cron expression or an interval`);
|
|
1603
|
+
}
|
|
1604
|
+
if (config.interval !== void 0 && config.interval <= 0) {
|
|
1605
|
+
throw new Error(`spearkit: task "${config.name}" interval must be positive`);
|
|
1606
|
+
}
|
|
1607
|
+
return {
|
|
1608
|
+
kind: "task",
|
|
1609
|
+
name: config.name,
|
|
1610
|
+
interval: config.interval,
|
|
1611
|
+
cron: config.cron !== void 0 ? new CronExpression(config.cron) : void 0,
|
|
1612
|
+
runOnStart: config.runOnStart ?? false,
|
|
1613
|
+
run: config.run
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
var MAX_TIMEOUT = 2147483647;
|
|
1617
|
+
var TaskScheduler = class {
|
|
1618
|
+
tasks = /* @__PURE__ */ new Map();
|
|
1619
|
+
timers = /* @__PURE__ */ new Map();
|
|
1620
|
+
running = false;
|
|
1621
|
+
client;
|
|
1622
|
+
logger;
|
|
1623
|
+
reconcilers = [];
|
|
1624
|
+
/** Number of registered tasks. */
|
|
1625
|
+
get size() {
|
|
1626
|
+
return this.tasks.size;
|
|
1627
|
+
}
|
|
1628
|
+
/** Whether the scheduler is currently running. */
|
|
1629
|
+
get active() {
|
|
1630
|
+
return this.running;
|
|
1631
|
+
}
|
|
1632
|
+
/** Every registered task. */
|
|
1633
|
+
list() {
|
|
1634
|
+
return [...this.tasks.values()];
|
|
1635
|
+
}
|
|
1636
|
+
/** Attach a logger for task error reporting. */
|
|
1637
|
+
setLogger(logger) {
|
|
1638
|
+
this.logger = logger;
|
|
1639
|
+
return this;
|
|
1640
|
+
}
|
|
1641
|
+
/** Register one or more tasks. If already running, they are scheduled now. */
|
|
1642
|
+
add(...tasks) {
|
|
1643
|
+
for (const task2 of tasks) {
|
|
1644
|
+
this.tasks.set(task2.name, task2);
|
|
1645
|
+
if (this.running) this.begin(task2);
|
|
1646
|
+
}
|
|
1647
|
+
return this;
|
|
1648
|
+
}
|
|
1649
|
+
/** Remove a task and cancel its timer. */
|
|
1650
|
+
remove(name) {
|
|
1651
|
+
this.cancel(name);
|
|
1652
|
+
return this.tasks.delete(name);
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* Schedule a one-shot job: run `fn` once after `ms` milliseconds, then forget.
|
|
1656
|
+
* Returns a cancel handle. Replaces hand-rolled `setTimeout` calls for things
|
|
1657
|
+
* like "remind the moderator in 10 minutes if no claim happened".
|
|
1658
|
+
*/
|
|
1659
|
+
delay(name, ms, fn) {
|
|
1660
|
+
const key = `delay:${name}`;
|
|
1661
|
+
this.cancel(key);
|
|
1662
|
+
const timer = setTimeout(async () => {
|
|
1663
|
+
this.timers.delete(key);
|
|
1664
|
+
try {
|
|
1665
|
+
await fn();
|
|
1666
|
+
} catch (error) {
|
|
1667
|
+
this.logger?.error(`delay "${name}" failed`, { error: toError(error) });
|
|
1668
|
+
}
|
|
1669
|
+
}, Math.max(0, ms));
|
|
1670
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
1671
|
+
this.timers.set(key, timer);
|
|
1672
|
+
return {
|
|
1673
|
+
cancel: () => {
|
|
1674
|
+
const had = this.timers.has(key);
|
|
1675
|
+
this.cancel(key);
|
|
1676
|
+
return had;
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
/**
|
|
1681
|
+
* Schedule a series of follow-up fires from a single start point. Each
|
|
1682
|
+
* delay is measured from "now"; the callback receives the index of the
|
|
1683
|
+
* fire. Generalises the 10s/30s/60s retry pattern in real bots.
|
|
1684
|
+
*/
|
|
1685
|
+
followUp(name, delays, fn) {
|
|
1686
|
+
const keys = delays.map((_, i) => `followUp:${name}:${i}`);
|
|
1687
|
+
for (const key of keys) this.cancel(key);
|
|
1688
|
+
delays.forEach((delay, i) => {
|
|
1689
|
+
const key = keys[i];
|
|
1690
|
+
const timer = setTimeout(async () => {
|
|
1691
|
+
this.timers.delete(key);
|
|
1692
|
+
try {
|
|
1693
|
+
await fn(i);
|
|
1694
|
+
} catch (error) {
|
|
1695
|
+
this.logger?.error(`followUp "${name}" fire ${i} failed`, { error: toError(error) });
|
|
1696
|
+
}
|
|
1697
|
+
}, Math.max(0, delay));
|
|
1698
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
1699
|
+
this.timers.set(key, timer);
|
|
1700
|
+
});
|
|
1701
|
+
return {
|
|
1702
|
+
cancel: () => {
|
|
1703
|
+
let any = false;
|
|
1704
|
+
for (const key of keys) {
|
|
1705
|
+
if (this.timers.has(key)) any = true;
|
|
1706
|
+
this.cancel(key);
|
|
1707
|
+
}
|
|
1708
|
+
return any;
|
|
1709
|
+
}
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* Register a once-on-ready reconciler — runs the first time the scheduler
|
|
1714
|
+
* starts (typically when the client becomes ready) and never again. Use
|
|
1715
|
+
* for restart-recovery work like closing orphaned voice sessions or
|
|
1716
|
+
* reapplying cached channel state.
|
|
1717
|
+
*/
|
|
1718
|
+
reconcile(name, fn) {
|
|
1719
|
+
if (this.running && this.client !== void 0) {
|
|
1720
|
+
void this.runReconciler(name, fn, this.client);
|
|
1721
|
+
} else {
|
|
1722
|
+
this.reconcilers.push({ name, run: fn });
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
async runReconciler(name, run, client) {
|
|
1726
|
+
this.logger?.debug("reconcile", { data: { name } });
|
|
1727
|
+
try {
|
|
1728
|
+
await run(client);
|
|
1729
|
+
} catch (error) {
|
|
1730
|
+
this.logger?.error(`reconciler "${name}" failed`, { error: toError(error) });
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
/** Start every task. Safe to call once; later calls are ignored. */
|
|
1734
|
+
start(client) {
|
|
1735
|
+
if (this.running) return;
|
|
1736
|
+
this.client = client;
|
|
1737
|
+
this.running = true;
|
|
1738
|
+
for (const task2 of this.tasks.values()) this.begin(task2);
|
|
1739
|
+
const pending = this.reconcilers.splice(0);
|
|
1740
|
+
for (const { name, run } of pending) void this.runReconciler(name, run, client);
|
|
1741
|
+
}
|
|
1742
|
+
/** Stop the scheduler and cancel every pending timer. */
|
|
1743
|
+
stop() {
|
|
1744
|
+
this.running = false;
|
|
1745
|
+
for (const name of [...this.timers.keys()]) this.cancel(name);
|
|
1746
|
+
}
|
|
1747
|
+
cancel(name) {
|
|
1748
|
+
const timer = this.timers.get(name);
|
|
1749
|
+
if (timer !== void 0) {
|
|
1750
|
+
clearTimeout(timer);
|
|
1751
|
+
this.timers.delete(name);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
begin(task2) {
|
|
1755
|
+
if (task2.runOnStart) void this.runTask(task2);
|
|
1756
|
+
this.scheduleNext(task2);
|
|
1757
|
+
}
|
|
1758
|
+
delayFor(task2) {
|
|
1759
|
+
if (task2.interval !== void 0) return task2.interval;
|
|
1760
|
+
if (task2.cron !== void 0) return Math.max(0, task2.cron.next().getTime() - Date.now());
|
|
1761
|
+
return MAX_TIMEOUT;
|
|
1762
|
+
}
|
|
1763
|
+
scheduleNext(task2) {
|
|
1764
|
+
if (!this.running) return;
|
|
1765
|
+
this.arm(task2.name, this.delayFor(task2), () => {
|
|
1766
|
+
void this.runTask(task2);
|
|
1767
|
+
this.scheduleNext(task2);
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
arm(name, delay, fire) {
|
|
1771
|
+
if (delay > MAX_TIMEOUT) {
|
|
1772
|
+
const timer2 = setTimeout(() => this.arm(name, delay - MAX_TIMEOUT, fire), MAX_TIMEOUT);
|
|
1773
|
+
if (typeof timer2.unref === "function") timer2.unref();
|
|
1774
|
+
this.timers.set(name, timer2);
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
const timer = setTimeout(fire, Math.max(0, delay));
|
|
1778
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
1779
|
+
this.timers.set(name, timer);
|
|
1780
|
+
}
|
|
1781
|
+
async runTask(task2) {
|
|
1782
|
+
if (this.client === void 0) return;
|
|
1783
|
+
this.logger?.debug("task", { data: { task: task2.name } });
|
|
1784
|
+
try {
|
|
1785
|
+
await task2.run(this.client);
|
|
1786
|
+
} catch (error) {
|
|
1787
|
+
this.logger?.error(`task "${task2.name}" failed`, { error: toError(error) });
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
};
|
|
1791
|
+
|
|
1792
|
+
// src/prefix.ts
|
|
1793
|
+
function prefixCommand(config) {
|
|
1794
|
+
const parser = config.args !== void 0 ? config.args(prefixArgs()).compile() : void 0;
|
|
1795
|
+
return {
|
|
1796
|
+
kind: "prefixCommand",
|
|
1797
|
+
name: config.name,
|
|
1798
|
+
aliases: config.aliases ?? [],
|
|
1799
|
+
description: config.description,
|
|
1800
|
+
cooldown: config.cooldown !== void 0 ? normalizeCooldown(config.cooldown) : void 0,
|
|
1801
|
+
guards: config.guards,
|
|
1802
|
+
parser,
|
|
1803
|
+
run: async (ctx) => {
|
|
1804
|
+
await config.run(ctx);
|
|
1805
|
+
}
|
|
1806
|
+
};
|
|
31
1807
|
}
|
|
32
|
-
var
|
|
33
|
-
constructor(
|
|
34
|
-
this.
|
|
1808
|
+
var PrefixContext = class {
|
|
1809
|
+
constructor(message, commandName, args, rest, options = {}) {
|
|
1810
|
+
this.message = message;
|
|
1811
|
+
this.commandName = commandName;
|
|
1812
|
+
this.args = args;
|
|
1813
|
+
this.rest = rest;
|
|
1814
|
+
this.options = options;
|
|
35
1815
|
}
|
|
36
|
-
|
|
1816
|
+
message;
|
|
1817
|
+
commandName;
|
|
1818
|
+
args;
|
|
1819
|
+
rest;
|
|
1820
|
+
options;
|
|
37
1821
|
get client() {
|
|
38
|
-
return this.
|
|
1822
|
+
return this.message.client;
|
|
39
1823
|
}
|
|
40
|
-
get
|
|
41
|
-
return this.
|
|
1824
|
+
get author() {
|
|
1825
|
+
return this.message.author;
|
|
42
1826
|
}
|
|
43
1827
|
get member() {
|
|
44
|
-
return this.
|
|
1828
|
+
return this.message.member;
|
|
45
1829
|
}
|
|
46
1830
|
get guild() {
|
|
47
|
-
return this.
|
|
1831
|
+
return this.message.guild;
|
|
48
1832
|
}
|
|
49
1833
|
get guildId() {
|
|
50
|
-
return this.
|
|
1834
|
+
return this.message.guildId;
|
|
51
1835
|
}
|
|
52
1836
|
get channel() {
|
|
53
|
-
return this.
|
|
1837
|
+
return this.message.channel;
|
|
54
1838
|
}
|
|
55
1839
|
get channelId() {
|
|
56
|
-
return this.
|
|
1840
|
+
return this.message.channelId;
|
|
57
1841
|
}
|
|
58
|
-
|
|
59
|
-
|
|
1842
|
+
/** Reply to the triggering message. */
|
|
1843
|
+
reply(content) {
|
|
1844
|
+
return this.message.reply(content);
|
|
60
1845
|
}
|
|
61
|
-
/**
|
|
62
|
-
|
|
63
|
-
|
|
1846
|
+
/** Send a message to the same channel (no reply reference). */
|
|
1847
|
+
async send(content) {
|
|
1848
|
+
const channel = this.message.channel;
|
|
1849
|
+
if ("send" in channel) return channel.send(content);
|
|
1850
|
+
return void 0;
|
|
64
1851
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
1852
|
+
};
|
|
1853
|
+
function resolveOptions(input) {
|
|
1854
|
+
if (typeof input === "string") return { prefixes: [input], mention: true, ignoreBots: true, caseInsensitive: true };
|
|
1855
|
+
if (Array.isArray(input)) {
|
|
1856
|
+
return { prefixes: [...input], mention: true, ignoreBots: true, caseInsensitive: true };
|
|
68
1857
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
1858
|
+
const options = input;
|
|
1859
|
+
const prefix = options.prefix ?? [];
|
|
1860
|
+
return {
|
|
1861
|
+
prefixes: typeof prefix === "string" ? [prefix] : [...prefix],
|
|
1862
|
+
mention: options.mention ?? true,
|
|
1863
|
+
ignoreBots: options.ignoreBots ?? true,
|
|
1864
|
+
caseInsensitive: options.caseInsensitive ?? true
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
function actorFromMessage(message) {
|
|
1868
|
+
const member = message.member;
|
|
1869
|
+
const roleIds = member !== null ? [...member.roles.cache.keys()] : [];
|
|
1870
|
+
return {
|
|
1871
|
+
userId: message.author.id,
|
|
1872
|
+
roleIds,
|
|
1873
|
+
guildId: message.guildId,
|
|
1874
|
+
channelId: message.channelId
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
var PrefixRegistry = class {
|
|
1878
|
+
commands = /* @__PURE__ */ new Map();
|
|
1879
|
+
lookup = /* @__PURE__ */ new Map();
|
|
1880
|
+
options = {
|
|
1881
|
+
prefixes: [],
|
|
1882
|
+
mention: true,
|
|
1883
|
+
ignoreBots: true,
|
|
1884
|
+
caseInsensitive: true
|
|
1885
|
+
};
|
|
1886
|
+
logger;
|
|
1887
|
+
cooldowns;
|
|
1888
|
+
defaultCooldown;
|
|
1889
|
+
errorHandler;
|
|
1890
|
+
defaultGuards = [];
|
|
1891
|
+
onUsage;
|
|
1892
|
+
/** Configure prefixes and matching behaviour. */
|
|
1893
|
+
setOptions(input) {
|
|
1894
|
+
this.options = resolveOptions(input);
|
|
1895
|
+
return this;
|
|
72
1896
|
}
|
|
73
|
-
/**
|
|
74
|
-
|
|
75
|
-
|
|
1897
|
+
/** Attach a logger for dispatch tracing and error reporting. */
|
|
1898
|
+
setLogger(logger) {
|
|
1899
|
+
this.logger = logger;
|
|
1900
|
+
return this;
|
|
76
1901
|
}
|
|
77
|
-
/**
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
);
|
|
1902
|
+
/** Attach a hook called after each successful prefix command run. */
|
|
1903
|
+
setUsageHook(hook) {
|
|
1904
|
+
this.onUsage = hook;
|
|
1905
|
+
return this;
|
|
82
1906
|
}
|
|
83
|
-
/**
|
|
84
|
-
|
|
85
|
-
|
|
1907
|
+
/** Share a cooldown manager and an optional default cooldown. */
|
|
1908
|
+
setCooldowns(manager, defaultCooldown) {
|
|
1909
|
+
this.cooldowns = manager;
|
|
1910
|
+
this.defaultCooldown = defaultCooldown;
|
|
1911
|
+
return this;
|
|
86
1912
|
}
|
|
87
|
-
/**
|
|
88
|
-
|
|
89
|
-
|
|
1913
|
+
/** Guards that run before every prefix command's own guards. */
|
|
1914
|
+
setDefaultGuards(guards) {
|
|
1915
|
+
this.defaultGuards = guards;
|
|
1916
|
+
return this;
|
|
1917
|
+
}
|
|
1918
|
+
/** Set the handler used when a prefix command throws. */
|
|
1919
|
+
onError(handler) {
|
|
1920
|
+
this.errorHandler = handler;
|
|
1921
|
+
return this;
|
|
1922
|
+
}
|
|
1923
|
+
/** Register one or more prefix commands (and their aliases). */
|
|
1924
|
+
add(...commands) {
|
|
1925
|
+
for (const command2 of commands) {
|
|
1926
|
+
this.commands.set(command2.name, command2);
|
|
1927
|
+
this.index(command2.name, command2);
|
|
1928
|
+
for (const alias of command2.aliases) this.index(alias, command2);
|
|
1929
|
+
}
|
|
1930
|
+
return this;
|
|
1931
|
+
}
|
|
1932
|
+
index(key, command2) {
|
|
1933
|
+
this.lookup.set(this.options.caseInsensitive ? key.toLowerCase() : key, command2);
|
|
1934
|
+
}
|
|
1935
|
+
/** Look up a command by name or alias. */
|
|
1936
|
+
get(nameOrAlias) {
|
|
1937
|
+
return this.lookup.get(this.options.caseInsensitive ? nameOrAlias.toLowerCase() : nameOrAlias);
|
|
1938
|
+
}
|
|
1939
|
+
/** Number of registered commands (excluding aliases). */
|
|
1940
|
+
get size() {
|
|
1941
|
+
return this.commands.size;
|
|
1942
|
+
}
|
|
1943
|
+
/** Every registered command. */
|
|
1944
|
+
list() {
|
|
1945
|
+
return [...this.commands.values()];
|
|
1946
|
+
}
|
|
1947
|
+
/** Strip a matching prefix (or bot mention) from `content`, or return `null`. */
|
|
1948
|
+
stripPrefix(content, botId) {
|
|
1949
|
+
for (const prefix of this.options.prefixes) {
|
|
1950
|
+
if (prefix.length > 0 && content.startsWith(prefix)) return content.slice(prefix.length);
|
|
1951
|
+
}
|
|
1952
|
+
if (this.options.mention && botId !== void 0) {
|
|
1953
|
+
const match = /^<@!?(\d+)>\s*/.exec(content);
|
|
1954
|
+
if (match !== null && match[1] === botId) return content.slice(match[0].length);
|
|
1955
|
+
}
|
|
1956
|
+
return null;
|
|
90
1957
|
}
|
|
91
1958
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* most handlers ever need.
|
|
1959
|
+
* Parse and dispatch a message. Returns `true` when a command ran (or was
|
|
1960
|
+
* blocked by a cooldown), `false` when the message was not a prefix command.
|
|
95
1961
|
*/
|
|
96
|
-
async
|
|
97
|
-
if (this.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
1962
|
+
async handle(message) {
|
|
1963
|
+
if (this.options.prefixes.length === 0 && !this.options.mention) return false;
|
|
1964
|
+
if (this.options.ignoreBots && message.author.bot) return false;
|
|
1965
|
+
const stripped = this.stripPrefix(message.content, message.client.user?.id);
|
|
1966
|
+
if (stripped === null) return false;
|
|
1967
|
+
const trimmed = stripped.trimStart();
|
|
1968
|
+
const match = /^(\S+)\s*([\s\S]*)$/.exec(trimmed);
|
|
1969
|
+
if (match === null) return false;
|
|
1970
|
+
const name = match[1] ?? "";
|
|
1971
|
+
const rest = match[2] ?? "";
|
|
1972
|
+
const command2 = this.get(name);
|
|
1973
|
+
if (command2 === void 0) return false;
|
|
1974
|
+
this.logger?.debug("prefix", { data: { command: command2.name, user: message.author.id } });
|
|
1975
|
+
const cooldown = command2.cooldown ?? this.defaultCooldown;
|
|
1976
|
+
if (cooldown !== void 0 && this.cooldowns !== void 0) {
|
|
1977
|
+
const result = this.cooldowns.consume(`prefix:${command2.name}`, cooldown, actorFromMessage(message));
|
|
1978
|
+
if (!result.allowed) {
|
|
1979
|
+
await message.reply(formatCooldownMessage(cooldown, result.remaining)).catch(() => void 0);
|
|
1980
|
+
return true;
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
const guards = combineGuards2(this.defaultGuards, command2.guards);
|
|
1984
|
+
if (guards.length > 0) {
|
|
1985
|
+
const guardCtx = guardContextFromMessage(message);
|
|
1986
|
+
const guardResult = await runGuards(guardCtx, guards);
|
|
1987
|
+
if (!guardResult.allowed) {
|
|
1988
|
+
this.logger?.debug("prefix denied", {
|
|
1989
|
+
data: {
|
|
1990
|
+
command: command2.name,
|
|
1991
|
+
user: message.author.id,
|
|
1992
|
+
reason: guardResult.reason ?? ""
|
|
1993
|
+
}
|
|
1994
|
+
});
|
|
1995
|
+
await replyDeniedMessage(message, guardResult.reason);
|
|
1996
|
+
return true;
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
const args = rest.length > 0 ? rest.split(/\s+/) : [];
|
|
2000
|
+
let options = {};
|
|
2001
|
+
if (command2.parser !== void 0) {
|
|
2002
|
+
const parsed = command2.parser.parse(args, rest);
|
|
2003
|
+
if (!parsed.ok) {
|
|
2004
|
+
this.logger?.debug("prefix arg error", {
|
|
2005
|
+
data: { command: command2.name, user: message.author.id, arg: parsed.arg, reason: parsed.reason }
|
|
2006
|
+
});
|
|
2007
|
+
const embeds = clientEmbeds3(message.client);
|
|
2008
|
+
await message.reply({ embeds: [embeds.error(`Argument \`${parsed.arg}\`: ${parsed.reason}`)] }).catch(() => void 0);
|
|
2009
|
+
return true;
|
|
2010
|
+
}
|
|
2011
|
+
options = parsed.values;
|
|
2012
|
+
}
|
|
2013
|
+
const start = Date.now();
|
|
2014
|
+
try {
|
|
2015
|
+
await command2.run(new PrefixContext(message, name, args, rest, options));
|
|
2016
|
+
this.onUsage?.({
|
|
2017
|
+
type: "prefix",
|
|
2018
|
+
name: command2.name,
|
|
2019
|
+
outcome: "success",
|
|
2020
|
+
durationMs: Date.now() - start,
|
|
2021
|
+
userId: message.author.id,
|
|
2022
|
+
userTag: message.author.tag,
|
|
2023
|
+
guildId: message.guildId,
|
|
2024
|
+
channelId: message.channelId,
|
|
2025
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
2026
|
+
});
|
|
2027
|
+
} catch (error) {
|
|
2028
|
+
const err = toError(error);
|
|
2029
|
+
this.logger?.error(`prefix command "${command2.name}" failed`, { error: err });
|
|
2030
|
+
this.onUsage?.({
|
|
2031
|
+
type: "prefix",
|
|
2032
|
+
name: command2.name,
|
|
2033
|
+
outcome: "error",
|
|
2034
|
+
errorMessage: err.message,
|
|
2035
|
+
durationMs: Date.now() - start,
|
|
2036
|
+
userId: message.author.id,
|
|
2037
|
+
userTag: message.author.tag,
|
|
2038
|
+
guildId: message.guildId,
|
|
2039
|
+
channelId: message.channelId,
|
|
2040
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
2041
|
+
});
|
|
2042
|
+
if (this.errorHandler !== void 0) await this.errorHandler(err, message);
|
|
2043
|
+
}
|
|
2044
|
+
return true;
|
|
2045
|
+
}
|
|
2046
|
+
};
|
|
2047
|
+
function combineGuards2(defaults, own) {
|
|
2048
|
+
if (own === void 0 || own.length === 0) return defaults;
|
|
2049
|
+
if (defaults.length === 0) return own;
|
|
2050
|
+
return [...defaults, ...own];
|
|
2051
|
+
}
|
|
2052
|
+
function guardContextFromMessage(message) {
|
|
2053
|
+
return {
|
|
2054
|
+
client: message.client,
|
|
2055
|
+
user: message.author,
|
|
2056
|
+
member: message.member,
|
|
2057
|
+
guild: message.guild,
|
|
2058
|
+
guildId: message.guildId,
|
|
2059
|
+
channelId: message.channelId
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
function clientEmbeds3(client) {
|
|
2063
|
+
return client.embeds ?? defaultEmbeds;
|
|
2064
|
+
}
|
|
2065
|
+
async function replyDeniedMessage(message, reason) {
|
|
2066
|
+
const embeds = clientEmbeds3(message.client);
|
|
2067
|
+
const text = reason ?? "You don't have permission to use this.";
|
|
2068
|
+
await message.reply({ embeds: [embeds.error(text)] }).catch(() => void 0);
|
|
2069
|
+
}
|
|
2070
|
+
var MemoryUsageStore = class {
|
|
2071
|
+
constructor(limit = Number.POSITIVE_INFINITY) {
|
|
2072
|
+
this.limit = limit;
|
|
2073
|
+
}
|
|
2074
|
+
limit;
|
|
2075
|
+
events = [];
|
|
2076
|
+
record(event2) {
|
|
2077
|
+
this.events.push(event2);
|
|
2078
|
+
if (this.events.length > this.limit) this.events.splice(0, this.events.length - this.limit);
|
|
2079
|
+
}
|
|
2080
|
+
all() {
|
|
2081
|
+
return this.events;
|
|
2082
|
+
}
|
|
2083
|
+
/** Total recorded events. */
|
|
2084
|
+
get size() {
|
|
2085
|
+
return this.events.length;
|
|
2086
|
+
}
|
|
2087
|
+
/** Events recorded for a given user id. */
|
|
2088
|
+
byUser(userId) {
|
|
2089
|
+
return this.events.filter((event2) => event2.userId === userId);
|
|
2090
|
+
}
|
|
2091
|
+
/** Forget everything. */
|
|
2092
|
+
clear() {
|
|
2093
|
+
this.events.length = 0;
|
|
2094
|
+
}
|
|
2095
|
+
};
|
|
2096
|
+
var JsonFileUsageStore = class {
|
|
2097
|
+
constructor(path) {
|
|
2098
|
+
this.path = path;
|
|
2099
|
+
}
|
|
2100
|
+
path;
|
|
2101
|
+
async record(event2) {
|
|
2102
|
+
const line = `${JSON.stringify({ ...event2, timestamp: event2.timestamp.toISOString() })}
|
|
2103
|
+
`;
|
|
2104
|
+
await promises.mkdir(path.dirname(this.path), { recursive: true });
|
|
2105
|
+
await promises.appendFile(this.path, line, "utf8");
|
|
2106
|
+
}
|
|
2107
|
+
async all() {
|
|
2108
|
+
let content;
|
|
2109
|
+
try {
|
|
2110
|
+
content = await promises.readFile(this.path, "utf8");
|
|
2111
|
+
} catch {
|
|
2112
|
+
return [];
|
|
103
2113
|
}
|
|
2114
|
+
const events = [];
|
|
2115
|
+
for (const line of content.split("\n")) {
|
|
2116
|
+
if (line.trim().length === 0) continue;
|
|
2117
|
+
const parsed = JSON.parse(line);
|
|
2118
|
+
events.push({ ...parsed, timestamp: new Date(parsed.timestamp) });
|
|
2119
|
+
}
|
|
2120
|
+
return events;
|
|
2121
|
+
}
|
|
2122
|
+
};
|
|
2123
|
+
function formatUsage(event2) {
|
|
2124
|
+
const who = event2.userTag ?? (event2.userId !== void 0 ? `<@${event2.userId}>` : "unknown");
|
|
2125
|
+
const where = event2.channelId !== void 0 && event2.channelId !== null ? ` in <#${event2.channelId}>` : "";
|
|
2126
|
+
const detail = event2.detail !== void 0 ? ` \u2014 ${event2.detail}` : "";
|
|
2127
|
+
return `\`${event2.type}\` **${event2.name}** by ${who}${where}${detail}`;
|
|
2128
|
+
}
|
|
2129
|
+
var UsageTracker = class {
|
|
2130
|
+
/** The configured store, if any. Directly queryable. */
|
|
2131
|
+
store;
|
|
2132
|
+
reporter;
|
|
2133
|
+
client;
|
|
2134
|
+
logger;
|
|
2135
|
+
/** Whether anything will happen on {@link track}. */
|
|
2136
|
+
get enabled() {
|
|
2137
|
+
return this.store !== void 0 || this.reporter !== void 0;
|
|
2138
|
+
}
|
|
2139
|
+
/** @internal Used by the client to resolve report channels. */
|
|
2140
|
+
setClient(client) {
|
|
2141
|
+
this.client = client;
|
|
2142
|
+
return this;
|
|
2143
|
+
}
|
|
2144
|
+
setLogger(logger) {
|
|
2145
|
+
this.logger = logger;
|
|
2146
|
+
return this;
|
|
2147
|
+
}
|
|
2148
|
+
/** Persist events to a store (a database). */
|
|
2149
|
+
setStore(store) {
|
|
2150
|
+
this.store = store;
|
|
2151
|
+
return this;
|
|
2152
|
+
}
|
|
2153
|
+
/** Mirror events into a Discord channel. */
|
|
2154
|
+
reportTo(channelId, format = formatUsage) {
|
|
2155
|
+
this.reporter = { channelId, format };
|
|
2156
|
+
return this;
|
|
104
2157
|
}
|
|
105
|
-
/**
|
|
106
|
-
|
|
107
|
-
|
|
2158
|
+
/** Record a use. Returns immediately; storing/reporting happen in the background. */
|
|
2159
|
+
track(event2) {
|
|
2160
|
+
if (!this.enabled) return;
|
|
2161
|
+
void this.run(event2);
|
|
2162
|
+
}
|
|
2163
|
+
async run(event2) {
|
|
2164
|
+
if (this.store !== void 0) {
|
|
2165
|
+
try {
|
|
2166
|
+
await this.store.record(event2);
|
|
2167
|
+
} catch (error) {
|
|
2168
|
+
this.logger?.error("usage store failed", { error: toError(error) });
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
if (this.reporter !== void 0 && this.client !== void 0) {
|
|
2172
|
+
try {
|
|
2173
|
+
const cache = this.client.channels.cache.get(this.reporter.channelId);
|
|
2174
|
+
const channel = cache ?? await this.client.channels.fetch(this.reporter.channelId);
|
|
2175
|
+
if (channel !== null && "send" in channel) {
|
|
2176
|
+
await channel.send(this.reporter.format(event2));
|
|
2177
|
+
}
|
|
2178
|
+
} catch (error) {
|
|
2179
|
+
this.logger?.error("usage report failed", { error: toError(error) });
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
108
2182
|
}
|
|
109
2183
|
};
|
|
110
2184
|
function makeOption(type, config) {
|
|
@@ -286,6 +2360,10 @@ var SlashCommand = class {
|
|
|
286
2360
|
json;
|
|
287
2361
|
executor;
|
|
288
2362
|
autocompleter;
|
|
2363
|
+
/** Resolved cooldown configuration for this command, if any. */
|
|
2364
|
+
cooldown;
|
|
2365
|
+
/** Resolved guard list for this command, if any. */
|
|
2366
|
+
guards;
|
|
289
2367
|
/** @internal */
|
|
290
2368
|
constructor(spec) {
|
|
291
2369
|
this.name = spec.name;
|
|
@@ -293,6 +2371,8 @@ var SlashCommand = class {
|
|
|
293
2371
|
this.json = spec.json;
|
|
294
2372
|
this.executor = spec.executor;
|
|
295
2373
|
this.autocompleter = spec.autocompleter;
|
|
2374
|
+
this.cooldown = spec.cooldown;
|
|
2375
|
+
this.guards = spec.guards;
|
|
296
2376
|
}
|
|
297
2377
|
/** Serialise to the discord REST chat-input command payload. */
|
|
298
2378
|
toJSON() {
|
|
@@ -307,7 +2387,7 @@ var SlashCommand = class {
|
|
|
307
2387
|
return this.autocompleter(interaction);
|
|
308
2388
|
}
|
|
309
2389
|
};
|
|
310
|
-
function
|
|
2390
|
+
function resolveOptions2(interaction, options) {
|
|
311
2391
|
const resolved = {};
|
|
312
2392
|
for (const [name, def] of Object.entries(options)) {
|
|
313
2393
|
resolved[name] = readOption(interaction.options, name, def);
|
|
@@ -327,7 +2407,7 @@ function makeAutocompleter(options) {
|
|
|
327
2407
|
if (!interaction.responded) await ctx.respond(choices);
|
|
328
2408
|
};
|
|
329
2409
|
}
|
|
330
|
-
function
|
|
2410
|
+
function baseJSON2(meta, options) {
|
|
331
2411
|
return {
|
|
332
2412
|
type: discord_js.ApplicationCommandType.ChatInput,
|
|
333
2413
|
name: meta.name,
|
|
@@ -362,22 +2442,24 @@ function command(config) {
|
|
|
362
2442
|
const options = config.options ?? {};
|
|
363
2443
|
const { run } = config;
|
|
364
2444
|
const executor = async (interaction) => {
|
|
365
|
-
const resolved =
|
|
2445
|
+
const resolved = resolveOptions2(interaction, options);
|
|
366
2446
|
await run(new CommandContext(interaction, resolved));
|
|
367
2447
|
};
|
|
368
2448
|
return new SlashCommand({
|
|
369
2449
|
name: config.name,
|
|
370
|
-
json:
|
|
2450
|
+
json: baseJSON2(config, leafOptionsJSON(options)),
|
|
371
2451
|
hasAutocomplete: optionsHaveAutocomplete(options),
|
|
372
2452
|
executor,
|
|
373
|
-
autocompleter: makeAutocompleter(options)
|
|
2453
|
+
autocompleter: makeAutocompleter(options),
|
|
2454
|
+
cooldown: config.cooldown !== void 0 ? normalizeCooldown(config.cooldown) : void 0,
|
|
2455
|
+
guards: config.guards
|
|
374
2456
|
});
|
|
375
2457
|
}
|
|
376
2458
|
function subcommand(config) {
|
|
377
2459
|
const options = config.options ?? {};
|
|
378
2460
|
const { run } = config;
|
|
379
2461
|
const execute = async (interaction) => {
|
|
380
|
-
const resolved =
|
|
2462
|
+
const resolved = resolveOptions2(interaction, options);
|
|
381
2463
|
await run(new CommandContext(interaction, resolved));
|
|
382
2464
|
};
|
|
383
2465
|
return {
|
|
@@ -438,15 +2520,22 @@ function commandGroup(config) {
|
|
|
438
2520
|
};
|
|
439
2521
|
return new SlashCommand({
|
|
440
2522
|
name: config.name,
|
|
441
|
-
json:
|
|
2523
|
+
json: baseJSON2(config, options),
|
|
442
2524
|
hasAutocomplete,
|
|
443
2525
|
executor,
|
|
444
|
-
autocompleter
|
|
2526
|
+
autocompleter,
|
|
2527
|
+
cooldown: config.cooldown !== void 0 ? normalizeCooldown(config.cooldown) : void 0,
|
|
2528
|
+
guards: config.guards
|
|
445
2529
|
});
|
|
446
2530
|
}
|
|
447
2531
|
var CommandRegistry = class {
|
|
448
2532
|
commands = /* @__PURE__ */ new Map();
|
|
449
2533
|
errorHandler;
|
|
2534
|
+
logger;
|
|
2535
|
+
cooldowns;
|
|
2536
|
+
defaultCooldown;
|
|
2537
|
+
defaultGuards = [];
|
|
2538
|
+
onUsage;
|
|
450
2539
|
/** Register one or more commands. Later registrations override by name. */
|
|
451
2540
|
add(...commands) {
|
|
452
2541
|
for (const command2 of commands) this.commands.set(command2.name, command2);
|
|
@@ -477,6 +2566,27 @@ var CommandRegistry = class {
|
|
|
477
2566
|
this.errorHandler = handler;
|
|
478
2567
|
return this;
|
|
479
2568
|
}
|
|
2569
|
+
/** Attach a logger used for dispatch tracing (debug level). */
|
|
2570
|
+
setLogger(logger) {
|
|
2571
|
+
this.logger = logger;
|
|
2572
|
+
return this;
|
|
2573
|
+
}
|
|
2574
|
+
/** Wire a shared cooldown manager and an optional default cooldown for every command. */
|
|
2575
|
+
setCooldowns(manager, defaultCooldown) {
|
|
2576
|
+
this.cooldowns = manager;
|
|
2577
|
+
this.defaultCooldown = defaultCooldown;
|
|
2578
|
+
return this;
|
|
2579
|
+
}
|
|
2580
|
+
/** Guards that run before every command's own guards. */
|
|
2581
|
+
setDefaultGuards(guards) {
|
|
2582
|
+
this.defaultGuards = guards;
|
|
2583
|
+
return this;
|
|
2584
|
+
}
|
|
2585
|
+
/** Attach a hook called after each successful command execution. */
|
|
2586
|
+
setUsageHook(hook) {
|
|
2587
|
+
this.onUsage = hook;
|
|
2588
|
+
return this;
|
|
2589
|
+
}
|
|
480
2590
|
/** Serialise every command to discord REST payloads. */
|
|
481
2591
|
toJSON() {
|
|
482
2592
|
return this.all().map((c) => c.toJSON());
|
|
@@ -485,10 +2595,60 @@ var CommandRegistry = class {
|
|
|
485
2595
|
async handle(interaction) {
|
|
486
2596
|
const command2 = this.commands.get(interaction.commandName);
|
|
487
2597
|
if (command2 === void 0) return;
|
|
2598
|
+
this.logger?.debug("command", {
|
|
2599
|
+
data: { command: interaction.commandName, user: interaction.user.id }
|
|
2600
|
+
});
|
|
2601
|
+
const cooldown = command2.cooldown ?? this.defaultCooldown;
|
|
2602
|
+
if (cooldown !== void 0 && this.cooldowns !== void 0) {
|
|
2603
|
+
const result = this.cooldowns.consume(command2.name, cooldown, actorOf2(interaction));
|
|
2604
|
+
if (!result.allowed) {
|
|
2605
|
+
await this.replyCooldown(interaction, cooldown, result.remaining);
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
const guards = combineGuards3(this.defaultGuards, command2.guards);
|
|
2610
|
+
if (guards.length > 0) {
|
|
2611
|
+
const guardResult = await runGuards(interaction, guards);
|
|
2612
|
+
if (!guardResult.allowed) {
|
|
2613
|
+
this.logger?.debug("command denied", {
|
|
2614
|
+
data: {
|
|
2615
|
+
command: command2.name,
|
|
2616
|
+
user: interaction.user.id,
|
|
2617
|
+
reason: guardResult.reason ?? ""
|
|
2618
|
+
}
|
|
2619
|
+
});
|
|
2620
|
+
await this.replyDenied(interaction, guardResult.reason);
|
|
2621
|
+
return;
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
const start = Date.now();
|
|
488
2625
|
try {
|
|
489
2626
|
await command2.execute(interaction);
|
|
2627
|
+
this.onUsage?.({
|
|
2628
|
+
type: "command",
|
|
2629
|
+
name: command2.name,
|
|
2630
|
+
outcome: "success",
|
|
2631
|
+
durationMs: Date.now() - start,
|
|
2632
|
+
userId: interaction.user.id,
|
|
2633
|
+
userTag: interaction.user.tag,
|
|
2634
|
+
guildId: interaction.guildId,
|
|
2635
|
+
channelId: interaction.channelId,
|
|
2636
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
2637
|
+
});
|
|
490
2638
|
} catch (error) {
|
|
491
2639
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
2640
|
+
this.onUsage?.({
|
|
2641
|
+
type: "command",
|
|
2642
|
+
name: command2.name,
|
|
2643
|
+
outcome: "error",
|
|
2644
|
+
errorMessage: err.message,
|
|
2645
|
+
durationMs: Date.now() - start,
|
|
2646
|
+
userId: interaction.user.id,
|
|
2647
|
+
userTag: interaction.user.tag,
|
|
2648
|
+
guildId: interaction.guildId,
|
|
2649
|
+
channelId: interaction.channelId,
|
|
2650
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
2651
|
+
});
|
|
492
2652
|
if (this.errorHandler !== void 0) {
|
|
493
2653
|
await this.errorHandler(err, interaction);
|
|
494
2654
|
} else {
|
|
@@ -538,10 +2698,56 @@ var CommandRegistry = class {
|
|
|
538
2698
|
} catch {
|
|
539
2699
|
}
|
|
540
2700
|
}
|
|
2701
|
+
async replyCooldown(interaction, config, remaining) {
|
|
2702
|
+
this.logger?.debug("cooldown", {
|
|
2703
|
+
data: { command: interaction.commandName, user: interaction.user.id, remaining }
|
|
2704
|
+
});
|
|
2705
|
+
const content = formatCooldownMessage(config, remaining);
|
|
2706
|
+
try {
|
|
2707
|
+
if (interaction.deferred) await interaction.editReply({ content });
|
|
2708
|
+
else if (interaction.replied) await interaction.followUp({ content, flags: discord_js.MessageFlags.Ephemeral });
|
|
2709
|
+
else await interaction.reply({ content, flags: discord_js.MessageFlags.Ephemeral });
|
|
2710
|
+
} catch {
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
async replyDenied(interaction, reason) {
|
|
2714
|
+
const embeds = clientEmbeds4(interaction.client);
|
|
2715
|
+
const text = reason ?? "You don't have permission to use this.";
|
|
2716
|
+
try {
|
|
2717
|
+
const payload = { embeds: [embeds.error(text)], flags: discord_js.MessageFlags.Ephemeral };
|
|
2718
|
+
if (interaction.deferred) await interaction.editReply({ embeds: payload.embeds });
|
|
2719
|
+
else if (interaction.replied) await interaction.followUp(payload);
|
|
2720
|
+
else await interaction.reply(payload);
|
|
2721
|
+
} catch {
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
541
2724
|
};
|
|
2725
|
+
function combineGuards3(defaults, own) {
|
|
2726
|
+
if (own === void 0 || own.length === 0) return defaults;
|
|
2727
|
+
if (defaults.length === 0) return own;
|
|
2728
|
+
return [...defaults, ...own];
|
|
2729
|
+
}
|
|
2730
|
+
function clientEmbeds4(client) {
|
|
2731
|
+
const host = client;
|
|
2732
|
+
return host.embeds ?? defaultEmbeds;
|
|
2733
|
+
}
|
|
2734
|
+
function actorOf2(interaction) {
|
|
2735
|
+
const member = interaction.member;
|
|
2736
|
+
let roleIds = [];
|
|
2737
|
+
if (member !== null) {
|
|
2738
|
+
const roles = member.roles;
|
|
2739
|
+
roleIds = Array.isArray(roles) ? roles : [...roles.cache.keys()];
|
|
2740
|
+
}
|
|
2741
|
+
return {
|
|
2742
|
+
userId: interaction.user.id,
|
|
2743
|
+
roleIds,
|
|
2744
|
+
guildId: interaction.guildId,
|
|
2745
|
+
channelId: interaction.channelId
|
|
2746
|
+
};
|
|
2747
|
+
}
|
|
542
2748
|
|
|
543
2749
|
// src/events.ts
|
|
544
|
-
function
|
|
2750
|
+
function toError2(value) {
|
|
545
2751
|
return value instanceof Error ? value : new Error(String(value));
|
|
546
2752
|
}
|
|
547
2753
|
function build(name, once, run) {
|
|
@@ -554,10 +2760,10 @@ function build(name, once, run) {
|
|
|
554
2760
|
try {
|
|
555
2761
|
const result = run(...args);
|
|
556
2762
|
if (result instanceof Promise) {
|
|
557
|
-
result.catch((error) => client.emit("error",
|
|
2763
|
+
result.catch((error) => client.emit("error", toError2(error)));
|
|
558
2764
|
}
|
|
559
2765
|
} catch (error) {
|
|
560
|
-
client.emit("error",
|
|
2766
|
+
client.emit("error", toError2(error));
|
|
561
2767
|
}
|
|
562
2768
|
};
|
|
563
2769
|
listeners.set(client, listener);
|
|
@@ -763,7 +2969,7 @@ var ModalContext = class extends BaseContext {
|
|
|
763
2969
|
return this.interaction.customId;
|
|
764
2970
|
}
|
|
765
2971
|
};
|
|
766
|
-
function
|
|
2972
|
+
function toError3(value) {
|
|
767
2973
|
return value instanceof Error ? value : new Error(String(value));
|
|
768
2974
|
}
|
|
769
2975
|
var ComponentRegistry = class {
|
|
@@ -775,6 +2981,9 @@ var ComponentRegistry = class {
|
|
|
775
2981
|
mentionableSelects = /* @__PURE__ */ new Map();
|
|
776
2982
|
modals = /* @__PURE__ */ new Map();
|
|
777
2983
|
errorHandler;
|
|
2984
|
+
logger;
|
|
2985
|
+
onUsage;
|
|
2986
|
+
defaultGuards = [];
|
|
778
2987
|
/** Register one or more components. Later registrations override by namespace. */
|
|
779
2988
|
add(...defs) {
|
|
780
2989
|
for (const def of defs) {
|
|
@@ -809,6 +3018,21 @@ var ComponentRegistry = class {
|
|
|
809
3018
|
this.errorHandler = handler;
|
|
810
3019
|
return this;
|
|
811
3020
|
}
|
|
3021
|
+
/** Attach a logger used for dispatch tracing (debug level). */
|
|
3022
|
+
setLogger(logger) {
|
|
3023
|
+
this.logger = logger;
|
|
3024
|
+
return this;
|
|
3025
|
+
}
|
|
3026
|
+
/** Attach a hook called after each successful component handler run. */
|
|
3027
|
+
setUsageHook(hook) {
|
|
3028
|
+
this.onUsage = hook;
|
|
3029
|
+
return this;
|
|
3030
|
+
}
|
|
3031
|
+
/** Guards that run before every component's own guards. */
|
|
3032
|
+
setDefaultGuards(guards) {
|
|
3033
|
+
this.defaultGuards = guards;
|
|
3034
|
+
return this;
|
|
3035
|
+
}
|
|
812
3036
|
/** Total number of registered components. */
|
|
813
3037
|
get size() {
|
|
814
3038
|
return this.buttons.size + this.stringSelects.size + this.userSelects.size + this.roleSelects.size + this.channelSelects.size + this.mentionableSelects.size + this.modals.size;
|
|
@@ -843,12 +3067,48 @@ var ComponentRegistry = class {
|
|
|
843
3067
|
}
|
|
844
3068
|
async exec(route, interaction) {
|
|
845
3069
|
if (route === void 0) return false;
|
|
3070
|
+
this.logger?.debug("component", { data: { customId: interaction.customId } });
|
|
3071
|
+
const guards = combineGuards4(this.defaultGuards, route.guards);
|
|
3072
|
+
if (guards.length > 0) {
|
|
3073
|
+
const guardResult = await runGuards(interaction, guards);
|
|
3074
|
+
if (!guardResult.allowed) {
|
|
3075
|
+
this.logger?.debug("component denied", {
|
|
3076
|
+
data: { customId: interaction.customId, reason: guardResult.reason ?? "" }
|
|
3077
|
+
});
|
|
3078
|
+
await replyDeniedComponent(interaction, guardResult.reason);
|
|
3079
|
+
return true;
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
846
3082
|
const { values } = parseCustomId(interaction.customId);
|
|
847
3083
|
const params = paramsFromValues(route.paramNames, values);
|
|
3084
|
+
const start = Date.now();
|
|
848
3085
|
try {
|
|
849
3086
|
await route.handle(interaction, params);
|
|
3087
|
+
this.onUsage?.({
|
|
3088
|
+
type: "component",
|
|
3089
|
+
name: route.namespace,
|
|
3090
|
+
outcome: "success",
|
|
3091
|
+
durationMs: Date.now() - start,
|
|
3092
|
+
userId: interaction.user.id,
|
|
3093
|
+
userTag: interaction.user.tag,
|
|
3094
|
+
guildId: interaction.guildId,
|
|
3095
|
+
channelId: interaction.channelId,
|
|
3096
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
3097
|
+
});
|
|
850
3098
|
} catch (error) {
|
|
851
|
-
const err =
|
|
3099
|
+
const err = toError3(error);
|
|
3100
|
+
this.onUsage?.({
|
|
3101
|
+
type: "component",
|
|
3102
|
+
name: route.namespace,
|
|
3103
|
+
outcome: "error",
|
|
3104
|
+
errorMessage: err.message,
|
|
3105
|
+
durationMs: Date.now() - start,
|
|
3106
|
+
userId: interaction.user.id,
|
|
3107
|
+
userTag: interaction.user.tag,
|
|
3108
|
+
guildId: interaction.guildId,
|
|
3109
|
+
channelId: interaction.channelId,
|
|
3110
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
3111
|
+
});
|
|
852
3112
|
if (this.errorHandler !== void 0) {
|
|
853
3113
|
await this.errorHandler(err, interaction);
|
|
854
3114
|
} else {
|
|
@@ -864,6 +3124,25 @@ var ComponentRegistry = class {
|
|
|
864
3124
|
function namespaceOf(customId) {
|
|
865
3125
|
return parseCustomId(customId).namespace;
|
|
866
3126
|
}
|
|
3127
|
+
function combineGuards4(defaults, own) {
|
|
3128
|
+
if (own === void 0 || own.length === 0) return defaults;
|
|
3129
|
+
if (defaults.length === 0) return own;
|
|
3130
|
+
return [...defaults, ...own];
|
|
3131
|
+
}
|
|
3132
|
+
function clientEmbeds5(client) {
|
|
3133
|
+
return client.embeds ?? defaultEmbeds;
|
|
3134
|
+
}
|
|
3135
|
+
async function replyDeniedComponent(interaction, reason) {
|
|
3136
|
+
const embeds = clientEmbeds5(interaction.client);
|
|
3137
|
+
const text = reason ?? "You don't have permission to use this.";
|
|
3138
|
+
try {
|
|
3139
|
+
const payload = { embeds: [embeds.error(text)], flags: discord_js.MessageFlags.Ephemeral };
|
|
3140
|
+
if (interaction.deferred) await interaction.editReply({ embeds: payload.embeds });
|
|
3141
|
+
else if (interaction.replied) await interaction.followUp(payload);
|
|
3142
|
+
else await interaction.reply(payload);
|
|
3143
|
+
} catch {
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
867
3146
|
function resolveButtonStyle(input) {
|
|
868
3147
|
if (input === void 0) return discord_js.ButtonStyle.Secondary;
|
|
869
3148
|
return typeof input === "number" ? input : discord_js.ButtonStyle[input];
|
|
@@ -875,6 +3154,7 @@ function button(config) {
|
|
|
875
3154
|
kind: "button",
|
|
876
3155
|
namespace: compiled.namespace,
|
|
877
3156
|
paramNames: compiled.paramNames,
|
|
3157
|
+
guards: config.guards,
|
|
878
3158
|
async handle(interaction, params) {
|
|
879
3159
|
await config.run(new ButtonContext(interaction, params));
|
|
880
3160
|
},
|
|
@@ -907,6 +3187,7 @@ function stringSelect(config) {
|
|
|
907
3187
|
kind: "stringSelect",
|
|
908
3188
|
namespace: compiled.namespace,
|
|
909
3189
|
paramNames: compiled.paramNames,
|
|
3190
|
+
guards: config.guards,
|
|
910
3191
|
async handle(interaction, params) {
|
|
911
3192
|
await config.run(new StringSelectContext(interaction, params));
|
|
912
3193
|
},
|
|
@@ -924,6 +3205,7 @@ function userSelect(config) {
|
|
|
924
3205
|
kind: "userSelect",
|
|
925
3206
|
namespace: compiled.namespace,
|
|
926
3207
|
paramNames: compiled.paramNames,
|
|
3208
|
+
guards: config.guards,
|
|
927
3209
|
async handle(interaction, params) {
|
|
928
3210
|
await config.run(new UserSelectContext(interaction, params));
|
|
929
3211
|
},
|
|
@@ -941,6 +3223,7 @@ function roleSelect(config) {
|
|
|
941
3223
|
kind: "roleSelect",
|
|
942
3224
|
namespace: compiled.namespace,
|
|
943
3225
|
paramNames: compiled.paramNames,
|
|
3226
|
+
guards: config.guards,
|
|
944
3227
|
async handle(interaction, params) {
|
|
945
3228
|
await config.run(new RoleSelectContext(interaction, params));
|
|
946
3229
|
},
|
|
@@ -958,6 +3241,7 @@ function channelSelect(config) {
|
|
|
958
3241
|
kind: "channelSelect",
|
|
959
3242
|
namespace: compiled.namespace,
|
|
960
3243
|
paramNames: compiled.paramNames,
|
|
3244
|
+
guards: config.guards,
|
|
961
3245
|
async handle(interaction, params) {
|
|
962
3246
|
await config.run(new ChannelSelectContext(interaction, params));
|
|
963
3247
|
},
|
|
@@ -976,6 +3260,7 @@ function mentionableSelect(config) {
|
|
|
976
3260
|
kind: "mentionableSelect",
|
|
977
3261
|
namespace: compiled.namespace,
|
|
978
3262
|
paramNames: compiled.paramNames,
|
|
3263
|
+
guards: config.guards,
|
|
979
3264
|
async handle(interaction, params) {
|
|
980
3265
|
await config.run(new MentionableSelectContext(interaction, params));
|
|
981
3266
|
},
|
|
@@ -1018,6 +3303,7 @@ function modal(config) {
|
|
|
1018
3303
|
kind: "modal",
|
|
1019
3304
|
namespace: compiled.namespace,
|
|
1020
3305
|
paramNames: compiled.paramNames,
|
|
3306
|
+
guards: config.guards,
|
|
1021
3307
|
async handle(interaction, params) {
|
|
1022
3308
|
const fields = {};
|
|
1023
3309
|
for (const key of fieldKeys) {
|
|
@@ -1057,6 +3343,9 @@ function isRegisterable(value) {
|
|
|
1057
3343
|
if (typeof record["kind"] === "string" && typeof record["handle"] === "function") {
|
|
1058
3344
|
return true;
|
|
1059
3345
|
}
|
|
3346
|
+
if ((record["kind"] === "task" || record["kind"] === "prefixCommand") && typeof record["run"] === "function") {
|
|
3347
|
+
return true;
|
|
3348
|
+
}
|
|
1060
3349
|
return false;
|
|
1061
3350
|
}
|
|
1062
3351
|
async function collectModules(dir, options = {}) {
|
|
@@ -1111,15 +3400,64 @@ var SpearClient = class extends discord_js.Client {
|
|
|
1111
3400
|
events = new EventRegistry();
|
|
1112
3401
|
/** Button / select / modal registry and router. */
|
|
1113
3402
|
components = new ComponentRegistry();
|
|
3403
|
+
/** Structured logger shared across spearkit and available to your code. */
|
|
3404
|
+
logger;
|
|
3405
|
+
/** Shared cooldown manager used by command dispatch; also usable directly. */
|
|
3406
|
+
cooldowns = new CooldownManager();
|
|
3407
|
+
/** Cron/interval task scheduler; started on ready and stopped on destroy. */
|
|
3408
|
+
scheduler = new TaskScheduler();
|
|
3409
|
+
/** Prefix (text) command registry, dispatched from `messageCreate`. */
|
|
3410
|
+
prefix = new PrefixRegistry();
|
|
3411
|
+
/** Usage tracker: records who used what to a store and/or a Discord channel. */
|
|
3412
|
+
usage = new UsageTracker();
|
|
3413
|
+
/** Preset embed factory used by `ctx.error/success/info/warn` and available to your code. */
|
|
3414
|
+
embeds;
|
|
3415
|
+
/** User- and message-context-menu command registry. */
|
|
3416
|
+
contextMenus = new ContextMenuRegistry();
|
|
3417
|
+
envConfig;
|
|
1114
3418
|
constructor(options = {}) {
|
|
1115
|
-
const { intents, ...rest } = options;
|
|
3419
|
+
const { intents, logger, dotenv, cooldown, prefix, usage, embeds, guards, ...rest } = options;
|
|
1116
3420
|
super({ ...rest, intents: intents ?? Intents.default });
|
|
3421
|
+
this.embeds = embeds instanceof Embeds ? embeds : new Embeds(embeds);
|
|
3422
|
+
this.envConfig = dotenv === false ? false : dotenv === void 0 || dotenv === true ? {} : dotenv;
|
|
3423
|
+
this.logger = logger instanceof Logger ? logger : new Logger(logger);
|
|
3424
|
+
const defaultCooldown = cooldown !== void 0 ? normalizeCooldown(cooldown) : void 0;
|
|
3425
|
+
this.commands.setLogger(this.logger.child("commands"));
|
|
3426
|
+
this.commands.setCooldowns(this.cooldowns, defaultCooldown);
|
|
3427
|
+
this.components.setLogger(this.logger.child("components"));
|
|
3428
|
+
this.contextMenus.setLogger(this.logger.child("contextMenus"));
|
|
3429
|
+
this.contextMenus.setCooldowns(this.cooldowns, defaultCooldown);
|
|
3430
|
+
this.prefix.setLogger(this.logger.child("prefix"));
|
|
3431
|
+
this.prefix.setCooldowns(this.cooldowns, defaultCooldown);
|
|
3432
|
+
if (prefix !== void 0) this.prefix.setOptions(prefix);
|
|
3433
|
+
this.scheduler.setLogger(this.logger.child("scheduler"));
|
|
3434
|
+
if (guards !== void 0 && guards.length > 0) {
|
|
3435
|
+
this.commands.setDefaultGuards(guards);
|
|
3436
|
+
this.contextMenus.setDefaultGuards(guards);
|
|
3437
|
+
this.components.setDefaultGuards(guards);
|
|
3438
|
+
this.prefix.setDefaultGuards(guards);
|
|
3439
|
+
}
|
|
3440
|
+
this.usage.setClient(this).setLogger(this.logger.child("usage"));
|
|
3441
|
+
if (usage !== void 0) {
|
|
3442
|
+
if (usage.store !== void 0) this.usage.setStore(usage.store);
|
|
3443
|
+
if (usage.channel !== void 0) this.usage.reportTo(usage.channel, usage.format);
|
|
3444
|
+
const onUsage = (event2) => this.usage.track(event2);
|
|
3445
|
+
this.commands.setUsageHook(onUsage);
|
|
3446
|
+
this.contextMenus.setUsageHook(onUsage);
|
|
3447
|
+
this.components.setUsageHook(onUsage);
|
|
3448
|
+
this.prefix.setUsageHook(onUsage);
|
|
3449
|
+
}
|
|
1117
3450
|
this.events.attachAll(this);
|
|
1118
3451
|
this.on("interactionCreate", (interaction) => this.route(interaction));
|
|
3452
|
+
this.on("error", (error) => this.logger.error("client error", { error: toError(error) }));
|
|
3453
|
+
this.once("clientReady", () => this.scheduler.start(this));
|
|
3454
|
+
this.on("messageCreate", (message) => {
|
|
3455
|
+
void this.prefix.handle(message);
|
|
3456
|
+
});
|
|
1119
3457
|
}
|
|
1120
3458
|
/**
|
|
1121
|
-
* Register commands, events
|
|
1122
|
-
*
|
|
3459
|
+
* Register commands, events, components, scheduled tasks, prefix commands
|
|
3460
|
+
* and context menus in one call. Each item is routed to its matching registry.
|
|
1123
3461
|
*/
|
|
1124
3462
|
register(...items) {
|
|
1125
3463
|
for (const item of items) {
|
|
@@ -1127,6 +3465,14 @@ var SpearClient = class extends discord_js.Client {
|
|
|
1127
3465
|
this.commands.add(item);
|
|
1128
3466
|
} else if ("attach" in item) {
|
|
1129
3467
|
this.events.add(item);
|
|
3468
|
+
} else if (item.kind === "task") {
|
|
3469
|
+
this.scheduler.add(item);
|
|
3470
|
+
} else if (item.kind === "prefixCommand") {
|
|
3471
|
+
this.prefix.add(item);
|
|
3472
|
+
} else if (item.kind === "userMenu") {
|
|
3473
|
+
this.contextMenus.add(item);
|
|
3474
|
+
} else if (item.kind === "messageMenu") {
|
|
3475
|
+
this.contextMenus.add(item);
|
|
1130
3476
|
} else {
|
|
1131
3477
|
this.components.add(item);
|
|
1132
3478
|
}
|
|
@@ -1152,6 +3498,7 @@ var SpearClient = class extends discord_js.Client {
|
|
|
1152
3498
|
* token is passed.
|
|
1153
3499
|
*/
|
|
1154
3500
|
async start(token) {
|
|
3501
|
+
if (this.envConfig !== false) loadEnv(this.envConfig);
|
|
1155
3502
|
const resolved = token ?? process.env.DISCORD_TOKEN;
|
|
1156
3503
|
if (resolved === void 0 || resolved.length === 0) {
|
|
1157
3504
|
throw new Error("spearkit: start() needs a token (pass one or set DISCORD_TOKEN)");
|
|
@@ -1160,8 +3507,9 @@ var SpearClient = class extends discord_js.Client {
|
|
|
1160
3507
|
return this;
|
|
1161
3508
|
}
|
|
1162
3509
|
/**
|
|
1163
|
-
* Push the registered slash commands to
|
|
1164
|
-
*
|
|
3510
|
+
* Push the registered slash commands to Discord using the client's REST
|
|
3511
|
+
* connection. Slash-only — use {@link deployAllCommands} to include context
|
|
3512
|
+
* menus in the same request.
|
|
1165
3513
|
*/
|
|
1166
3514
|
async deployCommands(options = {}) {
|
|
1167
3515
|
const applicationId = this.application?.id ?? this.user?.id;
|
|
@@ -1170,16 +3518,102 @@ var SpearClient = class extends discord_js.Client {
|
|
|
1170
3518
|
}
|
|
1171
3519
|
return this.commands.deploy({ rest: this.rest, applicationId, guildId: options.guildId });
|
|
1172
3520
|
}
|
|
3521
|
+
/**
|
|
3522
|
+
* Deploy slash commands AND context menus together to Discord in a single
|
|
3523
|
+
* PUT. Use this once you mix `userCommand` / `messageCommand` with `command`.
|
|
3524
|
+
*/
|
|
3525
|
+
/**
|
|
3526
|
+
* Deploy slash commands AND context menus together. Supports a `diff`
|
|
3527
|
+
* strategy that fetches the remote set first and skips the PUT when
|
|
3528
|
+
* nothing changed, and a `dryRun` flag that returns the body without
|
|
3529
|
+
* touching Discord (perfect for CI deploy gates).
|
|
3530
|
+
*
|
|
3531
|
+
* @returns the API's DeployResult on PUT, or a skipped report when
|
|
3532
|
+
* `dryRun: true` is set OR `strategy: "diff"` finds no changes.
|
|
3533
|
+
*/
|
|
3534
|
+
async deployAllCommands(options = {}) {
|
|
3535
|
+
const applicationId = options.applicationId ?? this.application?.id ?? this.user?.id;
|
|
3536
|
+
if (applicationId == null) {
|
|
3537
|
+
throw new Error("spearkit: deployAllCommands() must run after the client is ready");
|
|
3538
|
+
}
|
|
3539
|
+
const body = [...this.commands.toJSON(), ...this.contextMenus.toJSON()];
|
|
3540
|
+
const route = options.guildId !== void 0 ? discord_js.Routes.applicationGuildCommands(applicationId, options.guildId) : discord_js.Routes.applicationCommands(applicationId);
|
|
3541
|
+
if (options.dryRun === true) {
|
|
3542
|
+
return { skipped: true, reason: "dry-run", body };
|
|
3543
|
+
}
|
|
3544
|
+
if (options.strategy === "diff") {
|
|
3545
|
+
const remote = await this.rest.get(route);
|
|
3546
|
+
if (commandsEqual(body, remote)) {
|
|
3547
|
+
return { skipped: true, reason: "no-changes", body };
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
return await this.rest.put(route, { body });
|
|
3551
|
+
}
|
|
3552
|
+
/** Define and register a scheduled task in one call. */
|
|
3553
|
+
schedule(config) {
|
|
3554
|
+
const scheduled = task(config);
|
|
3555
|
+
this.scheduler.add(scheduled);
|
|
3556
|
+
return scheduled;
|
|
3557
|
+
}
|
|
3558
|
+
/** Stop the scheduler, then tear down the discord.js client. */
|
|
3559
|
+
async destroy() {
|
|
3560
|
+
this.scheduler.stop();
|
|
3561
|
+
await super.destroy();
|
|
3562
|
+
}
|
|
1173
3563
|
async route(interaction) {
|
|
1174
3564
|
if (interaction.isChatInputCommand()) {
|
|
1175
3565
|
await this.commands.handle(interaction);
|
|
1176
3566
|
} else if (interaction.isAutocomplete()) {
|
|
1177
3567
|
await this.commands.handleAutocomplete(interaction);
|
|
3568
|
+
} else if (interaction.isUserContextMenuCommand()) {
|
|
3569
|
+
await this.contextMenus.handleUser(interaction);
|
|
3570
|
+
} else if (interaction.isMessageContextMenuCommand()) {
|
|
3571
|
+
await this.contextMenus.handleMessage(interaction);
|
|
1178
3572
|
} else {
|
|
1179
3573
|
await this.components.handle(interaction);
|
|
1180
3574
|
}
|
|
1181
3575
|
}
|
|
1182
3576
|
};
|
|
3577
|
+
function commandsEqual(local, remote) {
|
|
3578
|
+
if (local.length !== remote.length) return false;
|
|
3579
|
+
const lset = new Set(local.map(commandHash));
|
|
3580
|
+
for (const r of remote) {
|
|
3581
|
+
if (!lset.has(commandHash(r))) return false;
|
|
3582
|
+
}
|
|
3583
|
+
return true;
|
|
3584
|
+
}
|
|
3585
|
+
function commandHash(cmd) {
|
|
3586
|
+
const c = cmd;
|
|
3587
|
+
return JSON.stringify({
|
|
3588
|
+
name: c.name,
|
|
3589
|
+
type: c.type ?? 1,
|
|
3590
|
+
description: c.description ?? "",
|
|
3591
|
+
nsfw: c.nsfw === true,
|
|
3592
|
+
default_member_permissions: c.default_member_permissions ?? null,
|
|
3593
|
+
contexts: Array.isArray(c.contexts) ? [...c.contexts].sort((a, b) => a - b) : null,
|
|
3594
|
+
options: normaliseOptions(c.options)
|
|
3595
|
+
});
|
|
3596
|
+
}
|
|
3597
|
+
function normaliseOptions(options) {
|
|
3598
|
+
if (!Array.isArray(options)) return [];
|
|
3599
|
+
return options.map((o) => {
|
|
3600
|
+
const opt = o;
|
|
3601
|
+
return {
|
|
3602
|
+
name: opt.name,
|
|
3603
|
+
type: opt.type,
|
|
3604
|
+
description: opt.description ?? "",
|
|
3605
|
+
required: opt.required === true,
|
|
3606
|
+
choices: Array.isArray(opt.choices) ? [...opt.choices].map((ch) => ({ name: ch.name, value: ch.value })).sort((a, b) => String(a.name).localeCompare(String(b.name))) : null,
|
|
3607
|
+
options: normaliseOptions(opt.options),
|
|
3608
|
+
channel_types: Array.isArray(opt.channel_types) ? [...opt.channel_types].sort((a, b) => a - b) : null,
|
|
3609
|
+
min_value: opt.min_value ?? null,
|
|
3610
|
+
max_value: opt.max_value ?? null,
|
|
3611
|
+
min_length: opt.min_length ?? null,
|
|
3612
|
+
max_length: opt.max_length ?? null,
|
|
3613
|
+
autocomplete: opt.autocomplete === true
|
|
3614
|
+
};
|
|
3615
|
+
}).sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
|
3616
|
+
}
|
|
1183
3617
|
|
|
1184
3618
|
// src/plugin.ts
|
|
1185
3619
|
function definePlugin(plugin) {
|
|
@@ -1193,45 +3627,112 @@ exports.ChannelSelectContext = ChannelSelectContext;
|
|
|
1193
3627
|
exports.CommandContext = CommandContext;
|
|
1194
3628
|
exports.CommandRegistry = CommandRegistry;
|
|
1195
3629
|
exports.ComponentRegistry = ComponentRegistry;
|
|
3630
|
+
exports.ContextMenuRegistry = ContextMenuRegistry;
|
|
3631
|
+
exports.CooldownManager = CooldownManager;
|
|
3632
|
+
exports.CronExpression = CronExpression;
|
|
3633
|
+
exports.DEFAULT_EMBED_COLORS = DEFAULT_EMBED_COLORS;
|
|
3634
|
+
exports.DEFAULT_EMBED_ICONS = DEFAULT_EMBED_ICONS;
|
|
3635
|
+
exports.Embeds = Embeds;
|
|
1196
3636
|
exports.EventRegistry = EventRegistry;
|
|
1197
3637
|
exports.Intents = Intents;
|
|
3638
|
+
exports.JsonFileUsageStore = JsonFileUsageStore;
|
|
3639
|
+
exports.KeyedLock = KeyedLock;
|
|
3640
|
+
exports.Logger = Logger;
|
|
1198
3641
|
exports.MAX_CUSTOM_ID_LENGTH = MAX_CUSTOM_ID_LENGTH;
|
|
3642
|
+
exports.MemoryCache = MemoryCache;
|
|
3643
|
+
exports.MemoryUsageStore = MemoryUsageStore;
|
|
1199
3644
|
exports.MentionableSelectContext = MentionableSelectContext;
|
|
1200
3645
|
exports.MessageComponentContext = MessageComponentContext;
|
|
3646
|
+
exports.MessageContextMenuContext = MessageContextMenuContext;
|
|
1201
3647
|
exports.ModalContext = ModalContext;
|
|
3648
|
+
exports.PrefixArgsBuilder = PrefixArgsBuilder;
|
|
3649
|
+
exports.PrefixContext = PrefixContext;
|
|
3650
|
+
exports.PrefixRegistry = PrefixRegistry;
|
|
1202
3651
|
exports.RoleSelectContext = RoleSelectContext;
|
|
1203
3652
|
exports.SlashCommand = SlashCommand;
|
|
1204
3653
|
exports.SpearClient = SpearClient;
|
|
1205
3654
|
exports.StringSelectContext = StringSelectContext;
|
|
3655
|
+
exports.TaskScheduler = TaskScheduler;
|
|
3656
|
+
exports.UsageTracker = UsageTracker;
|
|
3657
|
+
exports.UserContextMenuContext = UserContextMenuContext;
|
|
1206
3658
|
exports.UserSelectContext = UserSelectContext;
|
|
1207
3659
|
exports.asEphemeral = asEphemeral;
|
|
1208
3660
|
exports.buildCustomId = buildCustomId;
|
|
3661
|
+
exports.buildPaginatorPage = buildPaginatorPage;
|
|
1209
3662
|
exports.button = button;
|
|
1210
3663
|
exports.channelSelect = channelSelect;
|
|
1211
3664
|
exports.collectModules = collectModules;
|
|
1212
3665
|
exports.command = command;
|
|
1213
3666
|
exports.commandGroup = commandGroup;
|
|
1214
3667
|
exports.compilePattern = compilePattern;
|
|
3668
|
+
exports.confirm = confirm;
|
|
3669
|
+
exports.consoleSink = consoleSink;
|
|
3670
|
+
exports.createCache = createCache;
|
|
3671
|
+
exports.cron = cron;
|
|
3672
|
+
exports.defaultEmbeds = defaultEmbeds;
|
|
1215
3673
|
exports.definePlugin = definePlugin;
|
|
3674
|
+
exports.denied = denied;
|
|
3675
|
+
exports.discordTimestamp = discordTimestamp;
|
|
3676
|
+
exports.dmOnly = dmOnly;
|
|
3677
|
+
exports.effectiveDuration = effectiveDuration;
|
|
3678
|
+
exports.env = env;
|
|
1216
3679
|
exports.event = event;
|
|
3680
|
+
exports.fetchChannel = fetchChannel;
|
|
3681
|
+
exports.fetchGuild = fetchGuild;
|
|
3682
|
+
exports.fetchMember = fetchMember;
|
|
3683
|
+
exports.fetchMessage = fetchMessage;
|
|
3684
|
+
exports.fetchRole = fetchRole;
|
|
3685
|
+
exports.fetchUser = fetchUser;
|
|
3686
|
+
exports.formatCooldownMessage = formatCooldownMessage;
|
|
3687
|
+
exports.formatDuration = formatDuration;
|
|
3688
|
+
exports.formatUsage = formatUsage;
|
|
3689
|
+
exports.guard = guard;
|
|
3690
|
+
exports.guildOnly = guildOnly;
|
|
3691
|
+
exports.jsonlSink = jsonlSink;
|
|
1217
3692
|
exports.linkButton = linkButton;
|
|
3693
|
+
exports.loadConfig = loadConfig;
|
|
3694
|
+
exports.loadConfigAsync = loadConfigAsync;
|
|
3695
|
+
exports.loadEnv = loadEnv;
|
|
1218
3696
|
exports.loadInto = loadInto;
|
|
3697
|
+
exports.lookup = lookup;
|
|
3698
|
+
exports.lookupOptional = lookupOptional;
|
|
1219
3699
|
exports.mentionableSelect = mentionableSelect;
|
|
3700
|
+
exports.messageCommand = messageCommand;
|
|
1220
3701
|
exports.modal = modal;
|
|
3702
|
+
exports.normalizeCooldown = normalizeCooldown;
|
|
1221
3703
|
exports.normalizeReply = normalizeReply;
|
|
1222
3704
|
exports.option = option;
|
|
1223
3705
|
exports.optionsHaveAutocomplete = optionsHaveAutocomplete;
|
|
3706
|
+
exports.paginate = paginate;
|
|
1224
3707
|
exports.paramsFromValues = paramsFromValues;
|
|
1225
3708
|
exports.parseCustomId = parseCustomId;
|
|
3709
|
+
exports.parseDuration = parseDuration;
|
|
3710
|
+
exports.parseEnv = parseEnv;
|
|
3711
|
+
exports.prefixArgs = prefixArgs;
|
|
3712
|
+
exports.prefixCommand = prefixCommand;
|
|
1226
3713
|
exports.readOption = readOption;
|
|
3714
|
+
exports.relativeTimestamp = relativeTimestamp;
|
|
3715
|
+
exports.requireAllRoles = requireAllRoles;
|
|
3716
|
+
exports.requireAnyRole = requireAnyRole;
|
|
3717
|
+
exports.requireBotPermissions = requireBotPermissions;
|
|
3718
|
+
exports.requireOwner = requireOwner;
|
|
3719
|
+
exports.requireUserPermissions = requireUserPermissions;
|
|
1227
3720
|
exports.roleSelect = roleSelect;
|
|
1228
3721
|
exports.row = row;
|
|
3722
|
+
exports.runGuards = runGuards;
|
|
3723
|
+
exports.safeFetch = safeFetch;
|
|
3724
|
+
exports.safeTry = safeTry;
|
|
1229
3725
|
exports.stringSelect = stringSelect;
|
|
1230
3726
|
exports.subcommand = subcommand;
|
|
1231
3727
|
exports.subcommandGroup = subcommandGroup;
|
|
3728
|
+
exports.task = task;
|
|
1232
3729
|
exports.textInput = textInput;
|
|
1233
3730
|
exports.toAPIOption = toAPIOption;
|
|
3731
|
+
exports.toError = toError;
|
|
3732
|
+
exports.userCommand = userCommand;
|
|
1234
3733
|
exports.userSelect = userSelect;
|
|
3734
|
+
exports.webhookSink = webhookSink;
|
|
3735
|
+
exports.withSafeTimeout = withSafeTimeout;
|
|
1235
3736
|
Object.keys(discord_js).forEach(function (k) {
|
|
1236
3737
|
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
|
|
1237
3738
|
enumerable: true,
|