spearkit 0.2.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 +9 -0
- package/dist/index.cjs +1767 -228
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1223 -361
- package/dist/index.d.ts +1223 -361
- package/dist/index.js +1726 -233
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -2,13 +2,1237 @@
|
|
|
2
2
|
|
|
3
3
|
var discord_js = require('discord.js');
|
|
4
4
|
var fs = require('fs');
|
|
5
|
-
var
|
|
5
|
+
var util = require('util');
|
|
6
6
|
var promises = require('fs/promises');
|
|
7
|
+
var path = require('path');
|
|
7
8
|
var url = require('url');
|
|
8
9
|
|
|
9
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
|
+
}
|
|
522
|
+
function withEphemeralFlag(flags) {
|
|
523
|
+
if (flags == null) return discord_js.MessageFlags.Ephemeral;
|
|
524
|
+
if (typeof flags === "number" || typeof flags === "bigint") {
|
|
525
|
+
return Number(flags) | discord_js.MessageFlags.Ephemeral;
|
|
526
|
+
}
|
|
527
|
+
if (Array.isArray(flags)) return [...flags, discord_js.MessageFlags.Ephemeral];
|
|
528
|
+
return [flags, discord_js.MessageFlags.Ephemeral];
|
|
529
|
+
}
|
|
530
|
+
function normalizeReply(input) {
|
|
531
|
+
if (typeof input === "string") return { content: input };
|
|
532
|
+
const { ephemeral, ...rest } = input;
|
|
533
|
+
if (ephemeral) return { ...rest, flags: withEphemeralFlag(rest.flags) };
|
|
534
|
+
return rest;
|
|
535
|
+
}
|
|
536
|
+
function normalizeEdit(input) {
|
|
537
|
+
if (typeof input === "string") return { content: input };
|
|
538
|
+
const { ephemeral: _ephemeral, flags: _flags, ...rest } = input;
|
|
539
|
+
return rest;
|
|
540
|
+
}
|
|
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
|
+
}
|
|
10
974
|
|
|
11
|
-
// src/
|
|
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
|
+
}
|
|
12
1236
|
var RANK = {
|
|
13
1237
|
debug: 10,
|
|
14
1238
|
info: 20,
|
|
@@ -31,6 +1255,71 @@ function consoleSink(entry) {
|
|
|
31
1255
|
write(line);
|
|
32
1256
|
if (entry.error !== void 0) write(entry.error.stack ?? String(entry.error));
|
|
33
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
|
+
}
|
|
34
1323
|
var Logger = class _Logger {
|
|
35
1324
|
state;
|
|
36
1325
|
/** The scope prefix applied to every entry, if any. */
|
|
@@ -38,7 +1327,7 @@ var Logger = class _Logger {
|
|
|
38
1327
|
constructor(options = {}) {
|
|
39
1328
|
this.state = {
|
|
40
1329
|
threshold: options.level ?? "info",
|
|
41
|
-
|
|
1330
|
+
transports: resolveTransports(options)
|
|
42
1331
|
};
|
|
43
1332
|
this.scope = options.scope;
|
|
44
1333
|
}
|
|
@@ -51,6 +1340,16 @@ var Logger = class _Logger {
|
|
|
51
1340
|
this.state.threshold = level;
|
|
52
1341
|
return this;
|
|
53
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
|
+
}
|
|
54
1353
|
/** Whether an entry of `level` would currently be emitted. */
|
|
55
1354
|
enabled(level) {
|
|
56
1355
|
return RANK[level] >= RANK[this.state.threshold];
|
|
@@ -65,14 +1364,20 @@ var Logger = class _Logger {
|
|
|
65
1364
|
/** Emit an entry at an explicit level. */
|
|
66
1365
|
log(level, message, options) {
|
|
67
1366
|
if (!this.enabled(level)) return;
|
|
68
|
-
|
|
1367
|
+
const entry = {
|
|
69
1368
|
level,
|
|
70
1369
|
message,
|
|
71
1370
|
scope: this.scope,
|
|
72
1371
|
timestamp: /* @__PURE__ */ new Date(),
|
|
73
1372
|
error: options?.error,
|
|
74
1373
|
data: options?.data
|
|
75
|
-
}
|
|
1374
|
+
};
|
|
1375
|
+
for (const sink of this.state.transports) {
|
|
1376
|
+
try {
|
|
1377
|
+
sink(entry);
|
|
1378
|
+
} catch {
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
76
1381
|
}
|
|
77
1382
|
/** Verbose diagnostics, off by default. */
|
|
78
1383
|
debug(message, options) {
|
|
@@ -165,98 +1470,14 @@ function envRequire(key) {
|
|
|
165
1470
|
if (value === void 0) {
|
|
166
1471
|
throw new Error(`spearkit: required environment variable "${key}" is missing or empty`);
|
|
167
1472
|
}
|
|
168
|
-
return value;
|
|
169
|
-
}
|
|
170
|
-
var env = {
|
|
171
|
-
string: envString,
|
|
172
|
-
number: envNumber,
|
|
173
|
-
boolean: envBoolean,
|
|
174
|
-
require: envRequire
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
// src/cooldown.ts
|
|
178
|
-
function normalizeCooldown(input) {
|
|
179
|
-
return typeof input === "number" ? { duration: input } : input;
|
|
180
|
-
}
|
|
181
|
-
function scopeKey(scope, actor) {
|
|
182
|
-
switch (scope) {
|
|
183
|
-
case "guild":
|
|
184
|
-
return `g:${actor.guildId ?? "dm"}`;
|
|
185
|
-
case "channel":
|
|
186
|
-
return `c:${actor.channelId ?? "dm"}`;
|
|
187
|
-
case "global":
|
|
188
|
-
return "global";
|
|
189
|
-
case "user":
|
|
190
|
-
return `u:${actor.userId}`;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
function effectiveDuration(config, actor) {
|
|
194
|
-
if (config.exempt?.users?.includes(actor.userId) === true) return null;
|
|
195
|
-
if (config.exempt?.roles?.some((roleId) => actor.roleIds.includes(roleId)) === true) return null;
|
|
196
|
-
const userOverride = config.overrides?.users?.[actor.userId];
|
|
197
|
-
if (userOverride !== void 0) return userOverride;
|
|
198
|
-
const roleOverrides = config.overrides?.roles;
|
|
199
|
-
if (roleOverrides !== void 0) {
|
|
200
|
-
let best;
|
|
201
|
-
for (const roleId of actor.roleIds) {
|
|
202
|
-
const candidate = roleOverrides[roleId];
|
|
203
|
-
if (candidate !== void 0) best = best === void 0 ? candidate : Math.min(best, candidate);
|
|
204
|
-
}
|
|
205
|
-
if (best !== void 0) return best;
|
|
206
|
-
}
|
|
207
|
-
return config.duration;
|
|
208
|
-
}
|
|
209
|
-
function keyFor(bucket, config, actor) {
|
|
210
|
-
return `${bucket}|${scopeKey(config.scope ?? "user", actor)}`;
|
|
211
|
-
}
|
|
212
|
-
var CooldownManager = class {
|
|
213
|
-
hits = /* @__PURE__ */ new Map();
|
|
214
|
-
/** Number of tracked buckets. */
|
|
215
|
-
get size() {
|
|
216
|
-
return this.hits.size;
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* Check whether `actor` may use `bucket`, recording the use when allowed.
|
|
220
|
-
* Exempt actors and non-positive durations are always allowed (no record).
|
|
221
|
-
*/
|
|
222
|
-
consume(bucket, input, actor, now = Date.now()) {
|
|
223
|
-
const config = normalizeCooldown(input);
|
|
224
|
-
const duration = effectiveDuration(config, actor);
|
|
225
|
-
if (duration === null || duration <= 0) return { allowed: true };
|
|
226
|
-
const key = keyFor(bucket, config, actor);
|
|
227
|
-
const last = this.hits.get(key);
|
|
228
|
-
if (last !== void 0 && now - last < duration) {
|
|
229
|
-
return { allowed: false, remaining: duration - (now - last) };
|
|
230
|
-
}
|
|
231
|
-
this.hits.set(key, now);
|
|
232
|
-
return { allowed: true };
|
|
233
|
-
}
|
|
234
|
-
/** Like {@link consume} but never records — a read-only check. */
|
|
235
|
-
peek(bucket, input, actor, now = Date.now()) {
|
|
236
|
-
const config = normalizeCooldown(input);
|
|
237
|
-
const duration = effectiveDuration(config, actor);
|
|
238
|
-
if (duration === null || duration <= 0) return { allowed: true };
|
|
239
|
-
const last = this.hits.get(keyFor(bucket, config, actor));
|
|
240
|
-
if (last !== void 0 && now - last < duration) {
|
|
241
|
-
return { allowed: false, remaining: duration - (now - last) };
|
|
242
|
-
}
|
|
243
|
-
return { allowed: true };
|
|
244
|
-
}
|
|
245
|
-
/** Clear a single actor's cooldown for a bucket. Returns whether one existed. */
|
|
246
|
-
reset(bucket, actor, scope = "user") {
|
|
247
|
-
return this.hits.delete(`${bucket}|${scopeKey(scope, actor)}`);
|
|
248
|
-
}
|
|
249
|
-
/** Drop every tracked cooldown. */
|
|
250
|
-
clear() {
|
|
251
|
-
this.hits.clear();
|
|
252
|
-
}
|
|
253
|
-
};
|
|
254
|
-
function formatCooldownMessage(config, remainingMs) {
|
|
255
|
-
if (typeof config.message === "function") return config.message(remainingMs);
|
|
256
|
-
if (typeof config.message === "string") return config.message;
|
|
257
|
-
const seconds = Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
258
|
-
return `You're on cooldown \u2014 try again in ${seconds}s.`;
|
|
1473
|
+
return value;
|
|
259
1474
|
}
|
|
1475
|
+
var env = {
|
|
1476
|
+
string: envString,
|
|
1477
|
+
number: envNumber,
|
|
1478
|
+
boolean: envBoolean,
|
|
1479
|
+
require: envRequire
|
|
1480
|
+
};
|
|
260
1481
|
|
|
261
1482
|
// src/scheduler.ts
|
|
262
1483
|
var ALIASES = {
|
|
@@ -349,7 +1570,7 @@ var CronExpression = class {
|
|
|
349
1570
|
const date = new Date(from.getTime());
|
|
350
1571
|
date.setSeconds(0, 0);
|
|
351
1572
|
date.setMinutes(date.getMinutes() + 1);
|
|
352
|
-
for (let
|
|
1573
|
+
for (let guard2 = 0; guard2 < 1e5; guard2++) {
|
|
353
1574
|
if (!this.months.has(date.getMonth() + 1)) {
|
|
354
1575
|
date.setMonth(date.getMonth() + 1, 1);
|
|
355
1576
|
date.setHours(0, 0, 0, 0);
|
|
@@ -399,6 +1620,7 @@ var TaskScheduler = class {
|
|
|
399
1620
|
running = false;
|
|
400
1621
|
client;
|
|
401
1622
|
logger;
|
|
1623
|
+
reconcilers = [];
|
|
402
1624
|
/** Number of registered tasks. */
|
|
403
1625
|
get size() {
|
|
404
1626
|
return this.tasks.size;
|
|
@@ -429,12 +1651,93 @@ var TaskScheduler = class {
|
|
|
429
1651
|
this.cancel(name);
|
|
430
1652
|
return this.tasks.delete(name);
|
|
431
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
|
+
}
|
|
432
1733
|
/** Start every task. Safe to call once; later calls are ignored. */
|
|
433
1734
|
start(client) {
|
|
434
1735
|
if (this.running) return;
|
|
435
1736
|
this.client = client;
|
|
436
1737
|
this.running = true;
|
|
437
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);
|
|
438
1741
|
}
|
|
439
1742
|
/** Stop the scheduler and cancel every pending timer. */
|
|
440
1743
|
stop() {
|
|
@@ -488,28 +1791,33 @@ var TaskScheduler = class {
|
|
|
488
1791
|
|
|
489
1792
|
// src/prefix.ts
|
|
490
1793
|
function prefixCommand(config) {
|
|
1794
|
+
const parser = config.args !== void 0 ? config.args(prefixArgs()).compile() : void 0;
|
|
491
1795
|
return {
|
|
492
1796
|
kind: "prefixCommand",
|
|
493
1797
|
name: config.name,
|
|
494
1798
|
aliases: config.aliases ?? [],
|
|
495
1799
|
description: config.description,
|
|
496
1800
|
cooldown: config.cooldown !== void 0 ? normalizeCooldown(config.cooldown) : void 0,
|
|
1801
|
+
guards: config.guards,
|
|
1802
|
+
parser,
|
|
497
1803
|
run: async (ctx) => {
|
|
498
1804
|
await config.run(ctx);
|
|
499
1805
|
}
|
|
500
1806
|
};
|
|
501
1807
|
}
|
|
502
1808
|
var PrefixContext = class {
|
|
503
|
-
constructor(message, commandName, args, rest) {
|
|
1809
|
+
constructor(message, commandName, args, rest, options = {}) {
|
|
504
1810
|
this.message = message;
|
|
505
1811
|
this.commandName = commandName;
|
|
506
1812
|
this.args = args;
|
|
507
1813
|
this.rest = rest;
|
|
1814
|
+
this.options = options;
|
|
508
1815
|
}
|
|
509
1816
|
message;
|
|
510
1817
|
commandName;
|
|
511
1818
|
args;
|
|
512
1819
|
rest;
|
|
1820
|
+
options;
|
|
513
1821
|
get client() {
|
|
514
1822
|
return this.message.client;
|
|
515
1823
|
}
|
|
@@ -579,6 +1887,7 @@ var PrefixRegistry = class {
|
|
|
579
1887
|
cooldowns;
|
|
580
1888
|
defaultCooldown;
|
|
581
1889
|
errorHandler;
|
|
1890
|
+
defaultGuards = [];
|
|
582
1891
|
onUsage;
|
|
583
1892
|
/** Configure prefixes and matching behaviour. */
|
|
584
1893
|
setOptions(input) {
|
|
@@ -601,6 +1910,11 @@ var PrefixRegistry = class {
|
|
|
601
1910
|
this.defaultCooldown = defaultCooldown;
|
|
602
1911
|
return this;
|
|
603
1912
|
}
|
|
1913
|
+
/** Guards that run before every prefix command's own guards. */
|
|
1914
|
+
setDefaultGuards(guards) {
|
|
1915
|
+
this.defaultGuards = guards;
|
|
1916
|
+
return this;
|
|
1917
|
+
}
|
|
604
1918
|
/** Set the handler used when a prefix command throws. */
|
|
605
1919
|
onError(handler) {
|
|
606
1920
|
this.errorHandler = handler;
|
|
@@ -666,12 +1980,44 @@ var PrefixRegistry = class {
|
|
|
666
1980
|
return true;
|
|
667
1981
|
}
|
|
668
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
|
+
}
|
|
669
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();
|
|
670
2014
|
try {
|
|
671
|
-
await command2.run(new PrefixContext(message, name, args, rest));
|
|
2015
|
+
await command2.run(new PrefixContext(message, name, args, rest, options));
|
|
672
2016
|
this.onUsage?.({
|
|
673
2017
|
type: "prefix",
|
|
674
2018
|
name: command2.name,
|
|
2019
|
+
outcome: "success",
|
|
2020
|
+
durationMs: Date.now() - start,
|
|
675
2021
|
userId: message.author.id,
|
|
676
2022
|
userTag: message.author.tag,
|
|
677
2023
|
guildId: message.guildId,
|
|
@@ -681,11 +2027,46 @@ var PrefixRegistry = class {
|
|
|
681
2027
|
} catch (error) {
|
|
682
2028
|
const err = toError(error);
|
|
683
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
|
+
});
|
|
684
2042
|
if (this.errorHandler !== void 0) await this.errorHandler(err, message);
|
|
685
2043
|
}
|
|
686
2044
|
return true;
|
|
687
2045
|
}
|
|
688
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
|
+
}
|
|
689
2070
|
var MemoryUsageStore = class {
|
|
690
2071
|
constructor(limit = Number.POSITIVE_INFINITY) {
|
|
691
2072
|
this.limit = limit;
|
|
@@ -800,107 +2181,6 @@ var UsageTracker = class {
|
|
|
800
2181
|
}
|
|
801
2182
|
}
|
|
802
2183
|
};
|
|
803
|
-
function withEphemeralFlag(flags) {
|
|
804
|
-
if (flags == null) return discord_js.MessageFlags.Ephemeral;
|
|
805
|
-
if (typeof flags === "number" || typeof flags === "bigint") {
|
|
806
|
-
return Number(flags) | discord_js.MessageFlags.Ephemeral;
|
|
807
|
-
}
|
|
808
|
-
if (Array.isArray(flags)) return [...flags, discord_js.MessageFlags.Ephemeral];
|
|
809
|
-
return [flags, discord_js.MessageFlags.Ephemeral];
|
|
810
|
-
}
|
|
811
|
-
function normalizeReply(input) {
|
|
812
|
-
if (typeof input === "string") return { content: input };
|
|
813
|
-
const { ephemeral, ...rest } = input;
|
|
814
|
-
if (ephemeral) return { ...rest, flags: withEphemeralFlag(rest.flags) };
|
|
815
|
-
return rest;
|
|
816
|
-
}
|
|
817
|
-
function normalizeEdit(input) {
|
|
818
|
-
if (typeof input === "string") return { content: input };
|
|
819
|
-
const { ephemeral: _ephemeral, flags: _flags, ...rest } = input;
|
|
820
|
-
return rest;
|
|
821
|
-
}
|
|
822
|
-
function asEphemeral(input) {
|
|
823
|
-
if (typeof input === "string") return { content: input, ephemeral: true };
|
|
824
|
-
return { ...input, ephemeral: true };
|
|
825
|
-
}
|
|
826
|
-
var BaseContext = class {
|
|
827
|
-
constructor(interaction) {
|
|
828
|
-
this.interaction = interaction;
|
|
829
|
-
}
|
|
830
|
-
interaction;
|
|
831
|
-
get client() {
|
|
832
|
-
return this.interaction.client;
|
|
833
|
-
}
|
|
834
|
-
get user() {
|
|
835
|
-
return this.interaction.user;
|
|
836
|
-
}
|
|
837
|
-
get member() {
|
|
838
|
-
return this.interaction.member;
|
|
839
|
-
}
|
|
840
|
-
get guild() {
|
|
841
|
-
return this.interaction.guild;
|
|
842
|
-
}
|
|
843
|
-
get guildId() {
|
|
844
|
-
return this.interaction.guildId;
|
|
845
|
-
}
|
|
846
|
-
get channel() {
|
|
847
|
-
return this.interaction.channel;
|
|
848
|
-
}
|
|
849
|
-
get channelId() {
|
|
850
|
-
return this.interaction.channelId;
|
|
851
|
-
}
|
|
852
|
-
get locale() {
|
|
853
|
-
return this.interaction.locale;
|
|
854
|
-
}
|
|
855
|
-
/** Whether the interaction is already deferred. */
|
|
856
|
-
get deferred() {
|
|
857
|
-
return this.interaction.deferred;
|
|
858
|
-
}
|
|
859
|
-
/** Whether the interaction already received an initial response. */
|
|
860
|
-
get replied() {
|
|
861
|
-
return this.interaction.replied;
|
|
862
|
-
}
|
|
863
|
-
/** Send the initial response to the interaction. */
|
|
864
|
-
reply(input) {
|
|
865
|
-
return this.interaction.reply(normalizeReply(input));
|
|
866
|
-
}
|
|
867
|
-
/** Reply, but always hidden to everyone except the invoking user. */
|
|
868
|
-
replyEphemeral(input) {
|
|
869
|
-
return this.reply(asEphemeral(input));
|
|
870
|
-
}
|
|
871
|
-
/** Acknowledge now and respond later via {@link editReply}. */
|
|
872
|
-
defer(options = {}) {
|
|
873
|
-
return this.interaction.deferReply(
|
|
874
|
-
options.ephemeral ? { flags: discord_js.MessageFlags.Ephemeral } : {}
|
|
875
|
-
);
|
|
876
|
-
}
|
|
877
|
-
/** Edit the original (or deferred) response. */
|
|
878
|
-
editReply(input) {
|
|
879
|
-
return this.interaction.editReply(normalizeEdit(input));
|
|
880
|
-
}
|
|
881
|
-
/** Add an additional message after the initial response. */
|
|
882
|
-
followUp(input) {
|
|
883
|
-
return this.interaction.followUp(normalizeReply(input));
|
|
884
|
-
}
|
|
885
|
-
/**
|
|
886
|
-
* State-aware send: replies, edits a deferred response, or follows up —
|
|
887
|
-
* whichever is valid given the current interaction state. The single method
|
|
888
|
-
* most handlers ever need.
|
|
889
|
-
*/
|
|
890
|
-
async send(input) {
|
|
891
|
-
if (this.interaction.deferred) {
|
|
892
|
-
await this.editReply(input);
|
|
893
|
-
} else if (this.interaction.replied) {
|
|
894
|
-
await this.followUp(input);
|
|
895
|
-
} else {
|
|
896
|
-
await this.reply(input);
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
/** State-aware ephemeral error message. */
|
|
900
|
-
error(message) {
|
|
901
|
-
return this.send(asEphemeral(message));
|
|
902
|
-
}
|
|
903
|
-
};
|
|
904
2184
|
function makeOption(type, config) {
|
|
905
2185
|
return { type, ...config, required: config.required ?? false };
|
|
906
2186
|
}
|
|
@@ -1082,6 +2362,8 @@ var SlashCommand = class {
|
|
|
1082
2362
|
autocompleter;
|
|
1083
2363
|
/** Resolved cooldown configuration for this command, if any. */
|
|
1084
2364
|
cooldown;
|
|
2365
|
+
/** Resolved guard list for this command, if any. */
|
|
2366
|
+
guards;
|
|
1085
2367
|
/** @internal */
|
|
1086
2368
|
constructor(spec) {
|
|
1087
2369
|
this.name = spec.name;
|
|
@@ -1090,6 +2372,7 @@ var SlashCommand = class {
|
|
|
1090
2372
|
this.executor = spec.executor;
|
|
1091
2373
|
this.autocompleter = spec.autocompleter;
|
|
1092
2374
|
this.cooldown = spec.cooldown;
|
|
2375
|
+
this.guards = spec.guards;
|
|
1093
2376
|
}
|
|
1094
2377
|
/** Serialise to the discord REST chat-input command payload. */
|
|
1095
2378
|
toJSON() {
|
|
@@ -1124,7 +2407,7 @@ function makeAutocompleter(options) {
|
|
|
1124
2407
|
if (!interaction.responded) await ctx.respond(choices);
|
|
1125
2408
|
};
|
|
1126
2409
|
}
|
|
1127
|
-
function
|
|
2410
|
+
function baseJSON2(meta, options) {
|
|
1128
2411
|
return {
|
|
1129
2412
|
type: discord_js.ApplicationCommandType.ChatInput,
|
|
1130
2413
|
name: meta.name,
|
|
@@ -1164,11 +2447,12 @@ function command(config) {
|
|
|
1164
2447
|
};
|
|
1165
2448
|
return new SlashCommand({
|
|
1166
2449
|
name: config.name,
|
|
1167
|
-
json:
|
|
2450
|
+
json: baseJSON2(config, leafOptionsJSON(options)),
|
|
1168
2451
|
hasAutocomplete: optionsHaveAutocomplete(options),
|
|
1169
2452
|
executor,
|
|
1170
2453
|
autocompleter: makeAutocompleter(options),
|
|
1171
|
-
cooldown: config.cooldown !== void 0 ? normalizeCooldown(config.cooldown) : void 0
|
|
2454
|
+
cooldown: config.cooldown !== void 0 ? normalizeCooldown(config.cooldown) : void 0,
|
|
2455
|
+
guards: config.guards
|
|
1172
2456
|
});
|
|
1173
2457
|
}
|
|
1174
2458
|
function subcommand(config) {
|
|
@@ -1236,11 +2520,12 @@ function commandGroup(config) {
|
|
|
1236
2520
|
};
|
|
1237
2521
|
return new SlashCommand({
|
|
1238
2522
|
name: config.name,
|
|
1239
|
-
json:
|
|
2523
|
+
json: baseJSON2(config, options),
|
|
1240
2524
|
hasAutocomplete,
|
|
1241
2525
|
executor,
|
|
1242
2526
|
autocompleter,
|
|
1243
|
-
cooldown: config.cooldown !== void 0 ? normalizeCooldown(config.cooldown) : void 0
|
|
2527
|
+
cooldown: config.cooldown !== void 0 ? normalizeCooldown(config.cooldown) : void 0,
|
|
2528
|
+
guards: config.guards
|
|
1244
2529
|
});
|
|
1245
2530
|
}
|
|
1246
2531
|
var CommandRegistry = class {
|
|
@@ -1249,6 +2534,7 @@ var CommandRegistry = class {
|
|
|
1249
2534
|
logger;
|
|
1250
2535
|
cooldowns;
|
|
1251
2536
|
defaultCooldown;
|
|
2537
|
+
defaultGuards = [];
|
|
1252
2538
|
onUsage;
|
|
1253
2539
|
/** Register one or more commands. Later registrations override by name. */
|
|
1254
2540
|
add(...commands) {
|
|
@@ -1291,6 +2577,11 @@ var CommandRegistry = class {
|
|
|
1291
2577
|
this.defaultCooldown = defaultCooldown;
|
|
1292
2578
|
return this;
|
|
1293
2579
|
}
|
|
2580
|
+
/** Guards that run before every command's own guards. */
|
|
2581
|
+
setDefaultGuards(guards) {
|
|
2582
|
+
this.defaultGuards = guards;
|
|
2583
|
+
return this;
|
|
2584
|
+
}
|
|
1294
2585
|
/** Attach a hook called after each successful command execution. */
|
|
1295
2586
|
setUsageHook(hook) {
|
|
1296
2587
|
this.onUsage = hook;
|
|
@@ -1309,17 +2600,35 @@ var CommandRegistry = class {
|
|
|
1309
2600
|
});
|
|
1310
2601
|
const cooldown = command2.cooldown ?? this.defaultCooldown;
|
|
1311
2602
|
if (cooldown !== void 0 && this.cooldowns !== void 0) {
|
|
1312
|
-
const result = this.cooldowns.consume(command2.name, cooldown,
|
|
2603
|
+
const result = this.cooldowns.consume(command2.name, cooldown, actorOf2(interaction));
|
|
1313
2604
|
if (!result.allowed) {
|
|
1314
2605
|
await this.replyCooldown(interaction, cooldown, result.remaining);
|
|
1315
2606
|
return;
|
|
1316
2607
|
}
|
|
1317
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();
|
|
1318
2625
|
try {
|
|
1319
2626
|
await command2.execute(interaction);
|
|
1320
2627
|
this.onUsage?.({
|
|
1321
2628
|
type: "command",
|
|
1322
2629
|
name: command2.name,
|
|
2630
|
+
outcome: "success",
|
|
2631
|
+
durationMs: Date.now() - start,
|
|
1323
2632
|
userId: interaction.user.id,
|
|
1324
2633
|
userTag: interaction.user.tag,
|
|
1325
2634
|
guildId: interaction.guildId,
|
|
@@ -1328,6 +2637,18 @@ var CommandRegistry = class {
|
|
|
1328
2637
|
});
|
|
1329
2638
|
} catch (error) {
|
|
1330
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
|
+
});
|
|
1331
2652
|
if (this.errorHandler !== void 0) {
|
|
1332
2653
|
await this.errorHandler(err, interaction);
|
|
1333
2654
|
} else {
|
|
@@ -1389,8 +2710,28 @@ var CommandRegistry = class {
|
|
|
1389
2710
|
} catch {
|
|
1390
2711
|
}
|
|
1391
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
|
+
}
|
|
1392
2724
|
};
|
|
1393
|
-
function
|
|
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) {
|
|
1394
2735
|
const member = interaction.member;
|
|
1395
2736
|
let roleIds = [];
|
|
1396
2737
|
if (member !== null) {
|
|
@@ -1642,6 +2983,7 @@ var ComponentRegistry = class {
|
|
|
1642
2983
|
errorHandler;
|
|
1643
2984
|
logger;
|
|
1644
2985
|
onUsage;
|
|
2986
|
+
defaultGuards = [];
|
|
1645
2987
|
/** Register one or more components. Later registrations override by namespace. */
|
|
1646
2988
|
add(...defs) {
|
|
1647
2989
|
for (const def of defs) {
|
|
@@ -1686,6 +3028,11 @@ var ComponentRegistry = class {
|
|
|
1686
3028
|
this.onUsage = hook;
|
|
1687
3029
|
return this;
|
|
1688
3030
|
}
|
|
3031
|
+
/** Guards that run before every component's own guards. */
|
|
3032
|
+
setDefaultGuards(guards) {
|
|
3033
|
+
this.defaultGuards = guards;
|
|
3034
|
+
return this;
|
|
3035
|
+
}
|
|
1689
3036
|
/** Total number of registered components. */
|
|
1690
3037
|
get size() {
|
|
1691
3038
|
return this.buttons.size + this.stringSelects.size + this.userSelects.size + this.roleSelects.size + this.channelSelects.size + this.mentionableSelects.size + this.modals.size;
|
|
@@ -1721,13 +3068,27 @@ var ComponentRegistry = class {
|
|
|
1721
3068
|
async exec(route, interaction) {
|
|
1722
3069
|
if (route === void 0) return false;
|
|
1723
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
|
+
}
|
|
1724
3082
|
const { values } = parseCustomId(interaction.customId);
|
|
1725
3083
|
const params = paramsFromValues(route.paramNames, values);
|
|
3084
|
+
const start = Date.now();
|
|
1726
3085
|
try {
|
|
1727
3086
|
await route.handle(interaction, params);
|
|
1728
3087
|
this.onUsage?.({
|
|
1729
3088
|
type: "component",
|
|
1730
3089
|
name: route.namespace,
|
|
3090
|
+
outcome: "success",
|
|
3091
|
+
durationMs: Date.now() - start,
|
|
1731
3092
|
userId: interaction.user.id,
|
|
1732
3093
|
userTag: interaction.user.tag,
|
|
1733
3094
|
guildId: interaction.guildId,
|
|
@@ -1736,6 +3097,18 @@ var ComponentRegistry = class {
|
|
|
1736
3097
|
});
|
|
1737
3098
|
} catch (error) {
|
|
1738
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
|
+
});
|
|
1739
3112
|
if (this.errorHandler !== void 0) {
|
|
1740
3113
|
await this.errorHandler(err, interaction);
|
|
1741
3114
|
} else {
|
|
@@ -1751,6 +3124,25 @@ var ComponentRegistry = class {
|
|
|
1751
3124
|
function namespaceOf(customId) {
|
|
1752
3125
|
return parseCustomId(customId).namespace;
|
|
1753
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
|
+
}
|
|
1754
3146
|
function resolveButtonStyle(input) {
|
|
1755
3147
|
if (input === void 0) return discord_js.ButtonStyle.Secondary;
|
|
1756
3148
|
return typeof input === "number" ? input : discord_js.ButtonStyle[input];
|
|
@@ -1762,6 +3154,7 @@ function button(config) {
|
|
|
1762
3154
|
kind: "button",
|
|
1763
3155
|
namespace: compiled.namespace,
|
|
1764
3156
|
paramNames: compiled.paramNames,
|
|
3157
|
+
guards: config.guards,
|
|
1765
3158
|
async handle(interaction, params) {
|
|
1766
3159
|
await config.run(new ButtonContext(interaction, params));
|
|
1767
3160
|
},
|
|
@@ -1794,6 +3187,7 @@ function stringSelect(config) {
|
|
|
1794
3187
|
kind: "stringSelect",
|
|
1795
3188
|
namespace: compiled.namespace,
|
|
1796
3189
|
paramNames: compiled.paramNames,
|
|
3190
|
+
guards: config.guards,
|
|
1797
3191
|
async handle(interaction, params) {
|
|
1798
3192
|
await config.run(new StringSelectContext(interaction, params));
|
|
1799
3193
|
},
|
|
@@ -1811,6 +3205,7 @@ function userSelect(config) {
|
|
|
1811
3205
|
kind: "userSelect",
|
|
1812
3206
|
namespace: compiled.namespace,
|
|
1813
3207
|
paramNames: compiled.paramNames,
|
|
3208
|
+
guards: config.guards,
|
|
1814
3209
|
async handle(interaction, params) {
|
|
1815
3210
|
await config.run(new UserSelectContext(interaction, params));
|
|
1816
3211
|
},
|
|
@@ -1828,6 +3223,7 @@ function roleSelect(config) {
|
|
|
1828
3223
|
kind: "roleSelect",
|
|
1829
3224
|
namespace: compiled.namespace,
|
|
1830
3225
|
paramNames: compiled.paramNames,
|
|
3226
|
+
guards: config.guards,
|
|
1831
3227
|
async handle(interaction, params) {
|
|
1832
3228
|
await config.run(new RoleSelectContext(interaction, params));
|
|
1833
3229
|
},
|
|
@@ -1845,6 +3241,7 @@ function channelSelect(config) {
|
|
|
1845
3241
|
kind: "channelSelect",
|
|
1846
3242
|
namespace: compiled.namespace,
|
|
1847
3243
|
paramNames: compiled.paramNames,
|
|
3244
|
+
guards: config.guards,
|
|
1848
3245
|
async handle(interaction, params) {
|
|
1849
3246
|
await config.run(new ChannelSelectContext(interaction, params));
|
|
1850
3247
|
},
|
|
@@ -1863,6 +3260,7 @@ function mentionableSelect(config) {
|
|
|
1863
3260
|
kind: "mentionableSelect",
|
|
1864
3261
|
namespace: compiled.namespace,
|
|
1865
3262
|
paramNames: compiled.paramNames,
|
|
3263
|
+
guards: config.guards,
|
|
1866
3264
|
async handle(interaction, params) {
|
|
1867
3265
|
await config.run(new MentionableSelectContext(interaction, params));
|
|
1868
3266
|
},
|
|
@@ -1905,6 +3303,7 @@ function modal(config) {
|
|
|
1905
3303
|
kind: "modal",
|
|
1906
3304
|
namespace: compiled.namespace,
|
|
1907
3305
|
paramNames: compiled.paramNames,
|
|
3306
|
+
guards: config.guards,
|
|
1908
3307
|
async handle(interaction, params) {
|
|
1909
3308
|
const fields = {};
|
|
1910
3309
|
for (const key of fieldKeys) {
|
|
@@ -2011,40 +3410,54 @@ var SpearClient = class extends discord_js.Client {
|
|
|
2011
3410
|
prefix = new PrefixRegistry();
|
|
2012
3411
|
/** Usage tracker: records who used what to a store and/or a Discord channel. */
|
|
2013
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();
|
|
2014
3417
|
envConfig;
|
|
2015
3418
|
constructor(options = {}) {
|
|
2016
|
-
const { intents, logger, dotenv, cooldown, prefix, usage, ...rest } = options;
|
|
3419
|
+
const { intents, logger, dotenv, cooldown, prefix, usage, embeds, guards, ...rest } = options;
|
|
2017
3420
|
super({ ...rest, intents: intents ?? Intents.default });
|
|
3421
|
+
this.embeds = embeds instanceof Embeds ? embeds : new Embeds(embeds);
|
|
2018
3422
|
this.envConfig = dotenv === false ? false : dotenv === void 0 || dotenv === true ? {} : dotenv;
|
|
2019
3423
|
this.logger = logger instanceof Logger ? logger : new Logger(logger);
|
|
2020
|
-
this.commands.setLogger(this.logger.child("commands"));
|
|
2021
3424
|
const defaultCooldown = cooldown !== void 0 ? normalizeCooldown(cooldown) : void 0;
|
|
3425
|
+
this.commands.setLogger(this.logger.child("commands"));
|
|
2022
3426
|
this.commands.setCooldowns(this.cooldowns, defaultCooldown);
|
|
2023
3427
|
this.components.setLogger(this.logger.child("components"));
|
|
2024
|
-
this.
|
|
2025
|
-
this.
|
|
2026
|
-
this.on("error", (error) => this.logger.error("client error", { error: toError(error) }));
|
|
2027
|
-
this.scheduler.setLogger(this.logger.child("scheduler"));
|
|
2028
|
-
this.once("clientReady", () => this.scheduler.start(this));
|
|
3428
|
+
this.contextMenus.setLogger(this.logger.child("contextMenus"));
|
|
3429
|
+
this.contextMenus.setCooldowns(this.cooldowns, defaultCooldown);
|
|
2029
3430
|
this.prefix.setLogger(this.logger.child("prefix"));
|
|
2030
3431
|
this.prefix.setCooldowns(this.cooldowns, defaultCooldown);
|
|
2031
3432
|
if (prefix !== void 0) this.prefix.setOptions(prefix);
|
|
2032
|
-
this.
|
|
2033
|
-
|
|
2034
|
-
|
|
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
|
+
}
|
|
2035
3440
|
this.usage.setClient(this).setLogger(this.logger.child("usage"));
|
|
2036
3441
|
if (usage !== void 0) {
|
|
2037
3442
|
if (usage.store !== void 0) this.usage.setStore(usage.store);
|
|
2038
3443
|
if (usage.channel !== void 0) this.usage.reportTo(usage.channel, usage.format);
|
|
2039
3444
|
const onUsage = (event2) => this.usage.track(event2);
|
|
2040
3445
|
this.commands.setUsageHook(onUsage);
|
|
3446
|
+
this.contextMenus.setUsageHook(onUsage);
|
|
2041
3447
|
this.components.setUsageHook(onUsage);
|
|
2042
3448
|
this.prefix.setUsageHook(onUsage);
|
|
2043
3449
|
}
|
|
3450
|
+
this.events.attachAll(this);
|
|
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
|
+
});
|
|
2044
3457
|
}
|
|
2045
3458
|
/**
|
|
2046
|
-
* Register commands, events
|
|
2047
|
-
*
|
|
3459
|
+
* Register commands, events, components, scheduled tasks, prefix commands
|
|
3460
|
+
* and context menus in one call. Each item is routed to its matching registry.
|
|
2048
3461
|
*/
|
|
2049
3462
|
register(...items) {
|
|
2050
3463
|
for (const item of items) {
|
|
@@ -2056,6 +3469,10 @@ var SpearClient = class extends discord_js.Client {
|
|
|
2056
3469
|
this.scheduler.add(item);
|
|
2057
3470
|
} else if (item.kind === "prefixCommand") {
|
|
2058
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);
|
|
2059
3476
|
} else {
|
|
2060
3477
|
this.components.add(item);
|
|
2061
3478
|
}
|
|
@@ -2090,8 +3507,9 @@ var SpearClient = class extends discord_js.Client {
|
|
|
2090
3507
|
return this;
|
|
2091
3508
|
}
|
|
2092
3509
|
/**
|
|
2093
|
-
* Push the registered slash commands to
|
|
2094
|
-
*
|
|
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.
|
|
2095
3513
|
*/
|
|
2096
3514
|
async deployCommands(options = {}) {
|
|
2097
3515
|
const applicationId = this.application?.id ?? this.user?.id;
|
|
@@ -2100,14 +3518,36 @@ var SpearClient = class extends discord_js.Client {
|
|
|
2100
3518
|
}
|
|
2101
3519
|
return this.commands.deploy({ rest: this.rest, applicationId, guildId: options.guildId });
|
|
2102
3520
|
}
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
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
|
+
}
|
|
2110
3549
|
}
|
|
3550
|
+
return await this.rest.put(route, { body });
|
|
2111
3551
|
}
|
|
2112
3552
|
/** Define and register a scheduled task in one call. */
|
|
2113
3553
|
schedule(config) {
|
|
@@ -2120,7 +3560,60 @@ var SpearClient = class extends discord_js.Client {
|
|
|
2120
3560
|
this.scheduler.stop();
|
|
2121
3561
|
await super.destroy();
|
|
2122
3562
|
}
|
|
3563
|
+
async route(interaction) {
|
|
3564
|
+
if (interaction.isChatInputCommand()) {
|
|
3565
|
+
await this.commands.handle(interaction);
|
|
3566
|
+
} else if (interaction.isAutocomplete()) {
|
|
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);
|
|
3572
|
+
} else {
|
|
3573
|
+
await this.components.handle(interaction);
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
2123
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
|
+
}
|
|
2124
3617
|
|
|
2125
3618
|
// src/plugin.ts
|
|
2126
3619
|
function definePlugin(plugin) {
|
|
@@ -2134,17 +3627,25 @@ exports.ChannelSelectContext = ChannelSelectContext;
|
|
|
2134
3627
|
exports.CommandContext = CommandContext;
|
|
2135
3628
|
exports.CommandRegistry = CommandRegistry;
|
|
2136
3629
|
exports.ComponentRegistry = ComponentRegistry;
|
|
3630
|
+
exports.ContextMenuRegistry = ContextMenuRegistry;
|
|
2137
3631
|
exports.CooldownManager = CooldownManager;
|
|
2138
3632
|
exports.CronExpression = CronExpression;
|
|
3633
|
+
exports.DEFAULT_EMBED_COLORS = DEFAULT_EMBED_COLORS;
|
|
3634
|
+
exports.DEFAULT_EMBED_ICONS = DEFAULT_EMBED_ICONS;
|
|
3635
|
+
exports.Embeds = Embeds;
|
|
2139
3636
|
exports.EventRegistry = EventRegistry;
|
|
2140
3637
|
exports.Intents = Intents;
|
|
2141
3638
|
exports.JsonFileUsageStore = JsonFileUsageStore;
|
|
3639
|
+
exports.KeyedLock = KeyedLock;
|
|
2142
3640
|
exports.Logger = Logger;
|
|
2143
3641
|
exports.MAX_CUSTOM_ID_LENGTH = MAX_CUSTOM_ID_LENGTH;
|
|
3642
|
+
exports.MemoryCache = MemoryCache;
|
|
2144
3643
|
exports.MemoryUsageStore = MemoryUsageStore;
|
|
2145
3644
|
exports.MentionableSelectContext = MentionableSelectContext;
|
|
2146
3645
|
exports.MessageComponentContext = MessageComponentContext;
|
|
3646
|
+
exports.MessageContextMenuContext = MessageContextMenuContext;
|
|
2147
3647
|
exports.ModalContext = ModalContext;
|
|
3648
|
+
exports.PrefixArgsBuilder = PrefixArgsBuilder;
|
|
2148
3649
|
exports.PrefixContext = PrefixContext;
|
|
2149
3650
|
exports.PrefixRegistry = PrefixRegistry;
|
|
2150
3651
|
exports.RoleSelectContext = RoleSelectContext;
|
|
@@ -2153,39 +3654,74 @@ exports.SpearClient = SpearClient;
|
|
|
2153
3654
|
exports.StringSelectContext = StringSelectContext;
|
|
2154
3655
|
exports.TaskScheduler = TaskScheduler;
|
|
2155
3656
|
exports.UsageTracker = UsageTracker;
|
|
3657
|
+
exports.UserContextMenuContext = UserContextMenuContext;
|
|
2156
3658
|
exports.UserSelectContext = UserSelectContext;
|
|
2157
3659
|
exports.asEphemeral = asEphemeral;
|
|
2158
3660
|
exports.buildCustomId = buildCustomId;
|
|
3661
|
+
exports.buildPaginatorPage = buildPaginatorPage;
|
|
2159
3662
|
exports.button = button;
|
|
2160
3663
|
exports.channelSelect = channelSelect;
|
|
2161
3664
|
exports.collectModules = collectModules;
|
|
2162
3665
|
exports.command = command;
|
|
2163
3666
|
exports.commandGroup = commandGroup;
|
|
2164
3667
|
exports.compilePattern = compilePattern;
|
|
3668
|
+
exports.confirm = confirm;
|
|
2165
3669
|
exports.consoleSink = consoleSink;
|
|
3670
|
+
exports.createCache = createCache;
|
|
2166
3671
|
exports.cron = cron;
|
|
3672
|
+
exports.defaultEmbeds = defaultEmbeds;
|
|
2167
3673
|
exports.definePlugin = definePlugin;
|
|
3674
|
+
exports.denied = denied;
|
|
3675
|
+
exports.discordTimestamp = discordTimestamp;
|
|
3676
|
+
exports.dmOnly = dmOnly;
|
|
2168
3677
|
exports.effectiveDuration = effectiveDuration;
|
|
2169
3678
|
exports.env = env;
|
|
2170
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;
|
|
2171
3686
|
exports.formatCooldownMessage = formatCooldownMessage;
|
|
3687
|
+
exports.formatDuration = formatDuration;
|
|
2172
3688
|
exports.formatUsage = formatUsage;
|
|
3689
|
+
exports.guard = guard;
|
|
3690
|
+
exports.guildOnly = guildOnly;
|
|
3691
|
+
exports.jsonlSink = jsonlSink;
|
|
2173
3692
|
exports.linkButton = linkButton;
|
|
3693
|
+
exports.loadConfig = loadConfig;
|
|
3694
|
+
exports.loadConfigAsync = loadConfigAsync;
|
|
2174
3695
|
exports.loadEnv = loadEnv;
|
|
2175
3696
|
exports.loadInto = loadInto;
|
|
3697
|
+
exports.lookup = lookup;
|
|
3698
|
+
exports.lookupOptional = lookupOptional;
|
|
2176
3699
|
exports.mentionableSelect = mentionableSelect;
|
|
3700
|
+
exports.messageCommand = messageCommand;
|
|
2177
3701
|
exports.modal = modal;
|
|
2178
3702
|
exports.normalizeCooldown = normalizeCooldown;
|
|
2179
3703
|
exports.normalizeReply = normalizeReply;
|
|
2180
3704
|
exports.option = option;
|
|
2181
3705
|
exports.optionsHaveAutocomplete = optionsHaveAutocomplete;
|
|
3706
|
+
exports.paginate = paginate;
|
|
2182
3707
|
exports.paramsFromValues = paramsFromValues;
|
|
2183
3708
|
exports.parseCustomId = parseCustomId;
|
|
3709
|
+
exports.parseDuration = parseDuration;
|
|
2184
3710
|
exports.parseEnv = parseEnv;
|
|
3711
|
+
exports.prefixArgs = prefixArgs;
|
|
2185
3712
|
exports.prefixCommand = prefixCommand;
|
|
2186
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;
|
|
2187
3720
|
exports.roleSelect = roleSelect;
|
|
2188
3721
|
exports.row = row;
|
|
3722
|
+
exports.runGuards = runGuards;
|
|
3723
|
+
exports.safeFetch = safeFetch;
|
|
3724
|
+
exports.safeTry = safeTry;
|
|
2189
3725
|
exports.stringSelect = stringSelect;
|
|
2190
3726
|
exports.subcommand = subcommand;
|
|
2191
3727
|
exports.subcommandGroup = subcommandGroup;
|
|
@@ -2193,7 +3729,10 @@ exports.task = task;
|
|
|
2193
3729
|
exports.textInput = textInput;
|
|
2194
3730
|
exports.toAPIOption = toAPIOption;
|
|
2195
3731
|
exports.toError = toError;
|
|
3732
|
+
exports.userCommand = userCommand;
|
|
2196
3733
|
exports.userSelect = userSelect;
|
|
3734
|
+
exports.webhookSink = webhookSink;
|
|
3735
|
+
exports.withSafeTimeout = withSafeTimeout;
|
|
2197
3736
|
Object.keys(discord_js).forEach(function (k) {
|
|
2198
3737
|
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
|
|
2199
3738
|
enumerable: true,
|