spearkit 0.3.1 → 0.4.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/.claude/skills/spearkit/SKILL.md +11 -0
- package/.claude/skills/spearkit/reference/cheatsheet.md +117 -6
- package/AGENTS.md +98 -1
- package/README.md +10 -3
- package/dist/index.cjs +599 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +524 -2
- package/dist/index.d.ts +524 -2
- package/dist/index.js +576 -19
- package/dist/index.js.map +1 -1
- package/docs/README.md +21 -12
- package/docs/api-reference.md +222 -34
- package/docs/auto-defer.md +74 -0
- package/docs/client.md +60 -22
- package/docs/collectors.md +65 -0
- package/docs/commands.md +5 -0
- package/docs/components.md +7 -0
- package/docs/context-menus.md +121 -0
- package/docs/context.md +94 -2
- package/docs/cooldown.md +2 -1
- package/docs/errors.md +73 -0
- package/docs/guards.md +146 -0
- package/docs/loading.md +7 -5
- package/docs/messages.md +35 -0
- package/docs/permissions.md +68 -0
- package/docs/prefix.md +54 -0
- package/docs/scheduler.md +26 -2
- package/docs/shutdown.md +42 -0
- package/docs/store.md +90 -0
- package/docs/usage.md +20 -10
- package/llms-full.txt +1337 -85
- package/llms.txt +91 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { ButtonStyle, GatewayIntentBits, EmbedBuilder, MessageFlags, ApplicationCommandType, ComponentType, ActionRowBuilder, ButtonBuilder, ApplicationCommandOptionType, REST, Routes, StringSelectMenuBuilder, UserSelectMenuBuilder, RoleSelectMenuBuilder, ChannelSelectMenuBuilder, MentionableSelectMenuBuilder, ModalBuilder, Client,
|
|
1
|
+
import { RESTJSONErrorCodes, ButtonStyle, GatewayIntentBits, EmbedBuilder, PermissionsBitField, DiscordAPIError, HTTPError, MessageFlags, ApplicationCommandType, ComponentType, ActionRowBuilder, ButtonBuilder, ApplicationCommandOptionType, REST, Routes, StringSelectMenuBuilder, UserSelectMenuBuilder, RoleSelectMenuBuilder, ChannelSelectMenuBuilder, MentionableSelectMenuBuilder, ModalBuilder, Client, InteractionContextType, TextInputStyle, TextInputBuilder } from 'discord.js';
|
|
2
2
|
export * from 'discord.js';
|
|
3
3
|
import { readFile, readFileSync } from 'fs';
|
|
4
4
|
import { promisify } from 'util';
|
|
5
|
-
import {
|
|
5
|
+
import { readFile as readFile$1, mkdir, writeFile, rename, appendFile, readdir } from 'fs/promises';
|
|
6
6
|
import { dirname, join, extname } from 'path';
|
|
7
7
|
import { pathToFileURL } from 'url';
|
|
8
8
|
|
|
@@ -356,6 +356,58 @@ function discordTimestamp(date, style = "f") {
|
|
|
356
356
|
function relativeTimestamp(date) {
|
|
357
357
|
return discordTimestamp(date, "R");
|
|
358
358
|
}
|
|
359
|
+
var MESSAGE_CHARACTER_LIMIT = 2e3;
|
|
360
|
+
function truncate(text, max, suffix = "\u2026") {
|
|
361
|
+
if (max <= 0) return "";
|
|
362
|
+
if (text.length <= max) return text;
|
|
363
|
+
if (suffix.length >= max) return suffix.slice(0, max);
|
|
364
|
+
return text.slice(0, max - suffix.length) + suffix;
|
|
365
|
+
}
|
|
366
|
+
function hardSplit(text, max) {
|
|
367
|
+
const out = [];
|
|
368
|
+
let rest = text;
|
|
369
|
+
while (rest.length > max) {
|
|
370
|
+
const space = rest.lastIndexOf(" ", max);
|
|
371
|
+
const cut = space > Math.floor(max / 2) ? space : max;
|
|
372
|
+
out.push(rest.slice(0, cut));
|
|
373
|
+
rest = rest.slice(cut).replace(/^ /, "");
|
|
374
|
+
}
|
|
375
|
+
if (rest.length > 0) out.push(rest);
|
|
376
|
+
return out;
|
|
377
|
+
}
|
|
378
|
+
function chunkMessage(text, options = {}) {
|
|
379
|
+
const max = options.max ?? MESSAGE_CHARACTER_LIMIT;
|
|
380
|
+
if (max <= 0) throw new RangeError("spearkit: chunkMessage max must be positive");
|
|
381
|
+
if (text.length === 0) return [];
|
|
382
|
+
if (text.length <= max) return [text];
|
|
383
|
+
const chunks = [];
|
|
384
|
+
let current = "";
|
|
385
|
+
const flush = () => {
|
|
386
|
+
if (current.length > 0) {
|
|
387
|
+
chunks.push(current);
|
|
388
|
+
current = "";
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
for (const line of text.split("\n")) {
|
|
392
|
+
if (line.length > max) {
|
|
393
|
+
flush();
|
|
394
|
+
const pieces = hardSplit(line, max);
|
|
395
|
+
for (let i = 0; i < pieces.length - 1; i++) chunks.push(pieces[i]);
|
|
396
|
+
current = pieces[pieces.length - 1];
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
const candidate = current.length > 0 ? `${current}
|
|
400
|
+
${line}` : line;
|
|
401
|
+
if (candidate.length > max) {
|
|
402
|
+
flush();
|
|
403
|
+
current = line;
|
|
404
|
+
} else {
|
|
405
|
+
current = candidate;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
flush();
|
|
409
|
+
return chunks;
|
|
410
|
+
}
|
|
359
411
|
|
|
360
412
|
// src/cache.ts
|
|
361
413
|
var MemoryCache = class {
|
|
@@ -450,6 +502,130 @@ function lookup(table, resourceName = "key") {
|
|
|
450
502
|
function lookupOptional(table) {
|
|
451
503
|
return (key) => table[key];
|
|
452
504
|
}
|
|
505
|
+
function clone(value) {
|
|
506
|
+
if (value === void 0 || value === null) return value;
|
|
507
|
+
if (typeof structuredClone === "function") return structuredClone(value);
|
|
508
|
+
return JSON.parse(JSON.stringify(value));
|
|
509
|
+
}
|
|
510
|
+
var MemoryStore = class {
|
|
511
|
+
map = /* @__PURE__ */ new Map();
|
|
512
|
+
async get(key) {
|
|
513
|
+
return this.map.has(key) ? clone(this.map.get(key)) : void 0;
|
|
514
|
+
}
|
|
515
|
+
async set(key, value) {
|
|
516
|
+
this.map.set(key, clone(value));
|
|
517
|
+
}
|
|
518
|
+
async has(key) {
|
|
519
|
+
return this.map.has(key);
|
|
520
|
+
}
|
|
521
|
+
async delete(key) {
|
|
522
|
+
return this.map.delete(key);
|
|
523
|
+
}
|
|
524
|
+
async keys() {
|
|
525
|
+
return [...this.map.keys()];
|
|
526
|
+
}
|
|
527
|
+
async clear() {
|
|
528
|
+
this.map.clear();
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
var JsonStore = class {
|
|
532
|
+
constructor(path) {
|
|
533
|
+
this.path = path;
|
|
534
|
+
}
|
|
535
|
+
path;
|
|
536
|
+
cache = /* @__PURE__ */ new Map();
|
|
537
|
+
loading;
|
|
538
|
+
writeChain = Promise.resolve();
|
|
539
|
+
ensureLoaded() {
|
|
540
|
+
if (this.loading === void 0) this.loading = this.load();
|
|
541
|
+
return this.loading;
|
|
542
|
+
}
|
|
543
|
+
async load() {
|
|
544
|
+
try {
|
|
545
|
+
const raw = await readFile$1(this.path, "utf8");
|
|
546
|
+
const parsed = JSON.parse(raw);
|
|
547
|
+
for (const [key, value] of Object.entries(parsed)) this.cache.set(key, value);
|
|
548
|
+
} catch (error) {
|
|
549
|
+
if (error.code !== "ENOENT") throw error;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
/** Queue an atomic write of the current cache; serialised against prior writes. */
|
|
553
|
+
persist() {
|
|
554
|
+
this.writeChain = this.writeChain.then(async () => {
|
|
555
|
+
const body = JSON.stringify(Object.fromEntries(this.cache), null, 2);
|
|
556
|
+
await mkdir(dirname(this.path), { recursive: true });
|
|
557
|
+
const tmp = `${this.path}.${process.pid}.${Date.now()}.tmp`;
|
|
558
|
+
await writeFile(tmp, body, "utf8");
|
|
559
|
+
await rename(tmp, this.path);
|
|
560
|
+
});
|
|
561
|
+
return this.writeChain;
|
|
562
|
+
}
|
|
563
|
+
async get(key) {
|
|
564
|
+
await this.ensureLoaded();
|
|
565
|
+
return this.cache.has(key) ? clone(this.cache.get(key)) : void 0;
|
|
566
|
+
}
|
|
567
|
+
async set(key, value) {
|
|
568
|
+
await this.ensureLoaded();
|
|
569
|
+
this.cache.set(key, clone(value));
|
|
570
|
+
await this.persist();
|
|
571
|
+
}
|
|
572
|
+
async has(key) {
|
|
573
|
+
await this.ensureLoaded();
|
|
574
|
+
return this.cache.has(key);
|
|
575
|
+
}
|
|
576
|
+
async delete(key) {
|
|
577
|
+
await this.ensureLoaded();
|
|
578
|
+
const existed = this.cache.delete(key);
|
|
579
|
+
if (existed) await this.persist();
|
|
580
|
+
return existed;
|
|
581
|
+
}
|
|
582
|
+
async keys() {
|
|
583
|
+
await this.ensureLoaded();
|
|
584
|
+
return [...this.cache.keys()];
|
|
585
|
+
}
|
|
586
|
+
async clear() {
|
|
587
|
+
await this.ensureLoaded();
|
|
588
|
+
this.cache.clear();
|
|
589
|
+
await this.persist();
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
function namespaced(store, prefix) {
|
|
593
|
+
const tag = `${prefix}:`;
|
|
594
|
+
return {
|
|
595
|
+
get: (key) => store.get(tag + key),
|
|
596
|
+
set: (key, value) => store.set(tag + key, value),
|
|
597
|
+
has: (key) => store.has(tag + key),
|
|
598
|
+
delete: (key) => store.delete(tag + key),
|
|
599
|
+
async keys() {
|
|
600
|
+
return (await store.keys()).filter((k) => k.startsWith(tag)).map((k) => k.slice(tag.length));
|
|
601
|
+
},
|
|
602
|
+
async clear() {
|
|
603
|
+
for (const key of await this.keys()) await store.delete(tag + key);
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
function createSettings(options) {
|
|
608
|
+
const { store, defaults } = options;
|
|
609
|
+
const ns = options.namespace ?? "settings";
|
|
610
|
+
const keyFor2 = (id) => `${ns}:${id}`;
|
|
611
|
+
return {
|
|
612
|
+
defaults,
|
|
613
|
+
store,
|
|
614
|
+
async get(id) {
|
|
615
|
+
const stored = await store.get(keyFor2(id));
|
|
616
|
+
return { ...defaults, ...stored ?? {} };
|
|
617
|
+
},
|
|
618
|
+
async set(id, patch) {
|
|
619
|
+
const stored = await store.get(keyFor2(id)) ?? {};
|
|
620
|
+
const merged = { ...stored, ...patch };
|
|
621
|
+
await store.set(keyFor2(id), merged);
|
|
622
|
+
return { ...defaults, ...merged };
|
|
623
|
+
},
|
|
624
|
+
async reset(id) {
|
|
625
|
+
await store.delete(keyFor2(id));
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
}
|
|
453
629
|
function denied(reason) {
|
|
454
630
|
return { allowed: false, reason };
|
|
455
631
|
}
|
|
@@ -518,6 +694,278 @@ function requireBotPermissions(permission, reason = "I don't have permission to
|
|
|
518
694
|
function guard(predicate) {
|
|
519
695
|
return predicate;
|
|
520
696
|
}
|
|
697
|
+
function missingPermissions(channel, who, required) {
|
|
698
|
+
const held = channel.permissionsFor(who);
|
|
699
|
+
if (held === null) return new PermissionsBitField(required).toArray();
|
|
700
|
+
return held.missing(required);
|
|
701
|
+
}
|
|
702
|
+
function botMissingPermissions(channel, required) {
|
|
703
|
+
const me = channel.guild.members.me;
|
|
704
|
+
if (me === null) return new PermissionsBitField(required).toArray();
|
|
705
|
+
return missingPermissions(channel, me, required);
|
|
706
|
+
}
|
|
707
|
+
function hasPermissions(channel, who, required) {
|
|
708
|
+
return missingPermissions(channel, who, required).length === 0;
|
|
709
|
+
}
|
|
710
|
+
function compareRoles(a, b) {
|
|
711
|
+
return a.roles.highest.comparePositionTo(b.roles.highest);
|
|
712
|
+
}
|
|
713
|
+
function canActOn(actor, target) {
|
|
714
|
+
if (actor.id === target.id) return false;
|
|
715
|
+
if (target.id === target.guild.ownerId) return false;
|
|
716
|
+
if (actor.id === actor.guild.ownerId) return true;
|
|
717
|
+
return compareRoles(actor, target) > 0;
|
|
718
|
+
}
|
|
719
|
+
function moderationCheck(options) {
|
|
720
|
+
const { moderator, target } = options;
|
|
721
|
+
const action = options.action ?? "moderate";
|
|
722
|
+
const me = options.me === void 0 ? target.guild.members.me : options.me;
|
|
723
|
+
const name = target.user.username;
|
|
724
|
+
if (moderator.id === target.id) return { ok: false, reason: `You can't ${action} yourself.` };
|
|
725
|
+
if (target.id === target.guild.ownerId) {
|
|
726
|
+
return { ok: false, reason: `You can't ${action} the server owner.` };
|
|
727
|
+
}
|
|
728
|
+
if (moderator.id !== moderator.guild.ownerId && compareRoles(moderator, target) <= 0) {
|
|
729
|
+
return {
|
|
730
|
+
ok: false,
|
|
731
|
+
reason: `You can't ${action} **${name}** \u2014 their highest role is above or equal to yours.`
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
if (me !== null) {
|
|
735
|
+
if (me.id === target.id) return { ok: false, reason: `I can't ${action} myself.` };
|
|
736
|
+
if (me.id !== me.guild.ownerId && compareRoles(me, target) <= 0) {
|
|
737
|
+
return {
|
|
738
|
+
ok: false,
|
|
739
|
+
reason: `I can't ${action} **${name}** \u2014 move my role above theirs and try again.`
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return { ok: true };
|
|
744
|
+
}
|
|
745
|
+
var PERMISSION_LABELS = {
|
|
746
|
+
CreateInstantInvite: "Create Invite",
|
|
747
|
+
KickMembers: "Kick Members",
|
|
748
|
+
BanMembers: "Ban Members",
|
|
749
|
+
Administrator: "Administrator",
|
|
750
|
+
ManageChannels: "Manage Channels",
|
|
751
|
+
ManageGuild: "Manage Server",
|
|
752
|
+
AddReactions: "Add Reactions",
|
|
753
|
+
ViewAuditLog: "View Audit Log",
|
|
754
|
+
PrioritySpeaker: "Priority Speaker",
|
|
755
|
+
Stream: "Video",
|
|
756
|
+
ViewChannel: "View Channel",
|
|
757
|
+
SendMessages: "Send Messages",
|
|
758
|
+
SendTTSMessages: "Send TTS Messages",
|
|
759
|
+
ManageMessages: "Manage Messages",
|
|
760
|
+
EmbedLinks: "Embed Links",
|
|
761
|
+
AttachFiles: "Attach Files",
|
|
762
|
+
ReadMessageHistory: "Read Message History",
|
|
763
|
+
MentionEveryone: "Mention Everyone",
|
|
764
|
+
UseExternalEmojis: "Use External Emojis",
|
|
765
|
+
ViewGuildInsights: "View Server Insights",
|
|
766
|
+
Connect: "Connect",
|
|
767
|
+
Speak: "Speak",
|
|
768
|
+
MuteMembers: "Mute Members",
|
|
769
|
+
DeafenMembers: "Deafen Members",
|
|
770
|
+
MoveMembers: "Move Members",
|
|
771
|
+
UseVAD: "Use Voice Activity",
|
|
772
|
+
ChangeNickname: "Change Nickname",
|
|
773
|
+
ManageNicknames: "Manage Nicknames",
|
|
774
|
+
ManageRoles: "Manage Roles",
|
|
775
|
+
ManageWebhooks: "Manage Webhooks",
|
|
776
|
+
ManageGuildExpressions: "Manage Expressions",
|
|
777
|
+
UseApplicationCommands: "Use Application Commands",
|
|
778
|
+
RequestToSpeak: "Request to Speak",
|
|
779
|
+
ManageEvents: "Manage Events",
|
|
780
|
+
ManageThreads: "Manage Threads",
|
|
781
|
+
CreatePublicThreads: "Create Public Threads",
|
|
782
|
+
CreatePrivateThreads: "Create Private Threads",
|
|
783
|
+
UseExternalStickers: "Use External Stickers",
|
|
784
|
+
SendMessagesInThreads: "Send Messages in Threads",
|
|
785
|
+
UseEmbeddedActivities: "Use Activities",
|
|
786
|
+
ModerateMembers: "Timeout Members"
|
|
787
|
+
};
|
|
788
|
+
function formatPermissions(permissions) {
|
|
789
|
+
const names = new PermissionsBitField(permissions).toArray();
|
|
790
|
+
if (names.length === 0) return "none";
|
|
791
|
+
return names.map((flag) => PERMISSION_LABELS[flag] ?? flag).join(", ");
|
|
792
|
+
}
|
|
793
|
+
var DiscordErrorCode = {
|
|
794
|
+
/** A referenced channel no longer exists or is invisible to the bot. */
|
|
795
|
+
UnknownChannel: RESTJSONErrorCodes.UnknownChannel,
|
|
796
|
+
// 10003
|
|
797
|
+
/** The targeted guild is gone or the bot was removed from it. */
|
|
798
|
+
UnknownGuild: RESTJSONErrorCodes.UnknownGuild,
|
|
799
|
+
// 10004
|
|
800
|
+
/** The referenced member is not in the guild. */
|
|
801
|
+
UnknownMember: RESTJSONErrorCodes.UnknownMember,
|
|
802
|
+
// 10007
|
|
803
|
+
/** The message was deleted (or never existed) before the action ran. */
|
|
804
|
+
UnknownMessage: RESTJSONErrorCodes.UnknownMessage,
|
|
805
|
+
// 10008
|
|
806
|
+
/** The user could not be resolved. */
|
|
807
|
+
UnknownUser: RESTJSONErrorCodes.UnknownUser,
|
|
808
|
+
// 10013
|
|
809
|
+
/** The interaction token expired (the classic 3-second-window failure). */
|
|
810
|
+
UnknownInteraction: RESTJSONErrorCodes.UnknownInteraction,
|
|
811
|
+
// 10062
|
|
812
|
+
/** The bot lacks access to the resource entirely (not just one permission). */
|
|
813
|
+
MissingAccess: RESTJSONErrorCodes.MissingAccess,
|
|
814
|
+
// 50001
|
|
815
|
+
/** Action attempted on a DM channel that does not support it. */
|
|
816
|
+
CannotExecuteActionOnDMChannel: RESTJSONErrorCodes.CannotExecuteActionOnDMChannel,
|
|
817
|
+
// 50003
|
|
818
|
+
/** The target user has DMs closed or blocked the bot. */
|
|
819
|
+
CannotSendMessagesToThisUser: RESTJSONErrorCodes.CannotSendMessagesToThisUser,
|
|
820
|
+
// 50007
|
|
821
|
+
/** The bot is missing one or more permissions required for the action. */
|
|
822
|
+
MissingPermissions: RESTJSONErrorCodes.MissingPermissions,
|
|
823
|
+
// 50013
|
|
824
|
+
/** The request body failed Discord's validation. */
|
|
825
|
+
InvalidFormBodyOrContentType: RESTJSONErrorCodes.InvalidFormBodyOrContentType,
|
|
826
|
+
// 50035
|
|
827
|
+
/** The interaction was already acknowledged elsewhere. */
|
|
828
|
+
InteractionHasAlreadyBeenAcknowledged: RESTJSONErrorCodes.InteractionHasAlreadyBeenAcknowledged,
|
|
829
|
+
// 40060
|
|
830
|
+
/** The bot reached the maximum number of guilds it may join. */
|
|
831
|
+
MaximumNumberOfGuildsReached: RESTJSONErrorCodes.MaximumNumberOfGuildsReached,
|
|
832
|
+
// 30001
|
|
833
|
+
/** Too many active reactions / pins / etc. of this kind. */
|
|
834
|
+
MaximumNumberOfReactionsReached: RESTJSONErrorCodes.MaximumNumberOfReactionsReached
|
|
835
|
+
// 30010
|
|
836
|
+
};
|
|
837
|
+
function isDiscordError(error, code) {
|
|
838
|
+
if (!(error instanceof DiscordAPIError)) return false;
|
|
839
|
+
if (code === void 0) return true;
|
|
840
|
+
const codes = Array.isArray(code) ? code : [code];
|
|
841
|
+
return codes.includes(error.code);
|
|
842
|
+
}
|
|
843
|
+
function isHTTPError(error) {
|
|
844
|
+
return error instanceof HTTPError;
|
|
845
|
+
}
|
|
846
|
+
function isRateLimitError(error) {
|
|
847
|
+
return error instanceof DiscordAPIError && error.status === 429;
|
|
848
|
+
}
|
|
849
|
+
var FRIENDLY = {
|
|
850
|
+
[DiscordErrorCode.UnknownChannel]: "That channel no longer exists.",
|
|
851
|
+
[DiscordErrorCode.UnknownMessage]: "That message no longer exists.",
|
|
852
|
+
[DiscordErrorCode.UnknownMember]: "That member isn't in this server.",
|
|
853
|
+
[DiscordErrorCode.UnknownUser]: "I couldn't find that user.",
|
|
854
|
+
[DiscordErrorCode.UnknownInteraction]: "This took too long and expired \u2014 please run it again.",
|
|
855
|
+
[DiscordErrorCode.InteractionHasAlreadyBeenAcknowledged]: "This took too long and expired \u2014 please run it again.",
|
|
856
|
+
[DiscordErrorCode.MissingAccess]: "I don't have access to do that here.",
|
|
857
|
+
[DiscordErrorCode.CannotSendMessagesToThisUser]: "I can't DM that user \u2014 they may have DMs disabled.",
|
|
858
|
+
[DiscordErrorCode.CannotExecuteActionOnDMChannel]: "That can't be done in a DM.",
|
|
859
|
+
[DiscordErrorCode.MissingPermissions]: "I'm missing the permissions needed to do that."
|
|
860
|
+
};
|
|
861
|
+
function explainDiscordError(error) {
|
|
862
|
+
if (isRateLimitError(error)) {
|
|
863
|
+
return "I'm being rate-limited right now \u2014 please try again in a moment.";
|
|
864
|
+
}
|
|
865
|
+
if (!(error instanceof DiscordAPIError)) return null;
|
|
866
|
+
if (typeof error.code === "number") {
|
|
867
|
+
const known = FRIENDLY[error.code];
|
|
868
|
+
if (known !== void 0) return known;
|
|
869
|
+
}
|
|
870
|
+
return null;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// src/shutdown.ts
|
|
874
|
+
function gracefulShutdown(client, options = {}) {
|
|
875
|
+
const signals = options.signals ?? ["SIGINT", "SIGTERM"];
|
|
876
|
+
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
877
|
+
const exit = options.exit ?? true;
|
|
878
|
+
let shuttingDown = false;
|
|
879
|
+
const handler = (signal) => {
|
|
880
|
+
if (shuttingDown) return;
|
|
881
|
+
shuttingDown = true;
|
|
882
|
+
options.logger?.info?.(`received ${signal}, shutting down`);
|
|
883
|
+
const force = setTimeout(() => {
|
|
884
|
+
options.logger?.error?.("shutdown timed out \u2014 forcing exit");
|
|
885
|
+
if (exit) process.exit(1);
|
|
886
|
+
}, timeoutMs);
|
|
887
|
+
if (typeof force.unref === "function") force.unref();
|
|
888
|
+
void (async () => {
|
|
889
|
+
try {
|
|
890
|
+
await options.onShutdown?.(signal);
|
|
891
|
+
await client.destroy();
|
|
892
|
+
clearTimeout(force);
|
|
893
|
+
options.logger?.info?.("shutdown complete");
|
|
894
|
+
if (exit) process.exit(0);
|
|
895
|
+
} catch (error) {
|
|
896
|
+
clearTimeout(force);
|
|
897
|
+
options.logger?.error?.("shutdown failed", error);
|
|
898
|
+
if (exit) process.exit(1);
|
|
899
|
+
}
|
|
900
|
+
})();
|
|
901
|
+
};
|
|
902
|
+
for (const signal of signals) process.on(signal, handler);
|
|
903
|
+
return () => {
|
|
904
|
+
for (const signal of signals) process.off(signal, handler);
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// src/collectors.ts
|
|
909
|
+
async function awaitMessage(channel, options = {}) {
|
|
910
|
+
const { filter, time = 6e4 } = options;
|
|
911
|
+
try {
|
|
912
|
+
const collected = await channel.awaitMessages({
|
|
913
|
+
filter,
|
|
914
|
+
max: 1,
|
|
915
|
+
time,
|
|
916
|
+
errors: ["time"]
|
|
917
|
+
});
|
|
918
|
+
return collected.first() ?? null;
|
|
919
|
+
} catch {
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
async function awaitComponent(message, options = {}) {
|
|
924
|
+
const { filter, time = 6e4, componentType } = options;
|
|
925
|
+
try {
|
|
926
|
+
return await message.awaitMessageComponent({
|
|
927
|
+
time,
|
|
928
|
+
filter: (interaction) => (componentType === void 0 || interaction.componentType === componentType) && (filter?.(interaction) ?? true)
|
|
929
|
+
});
|
|
930
|
+
} catch {
|
|
931
|
+
return null;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
function resolveModalCustomId(modal2) {
|
|
935
|
+
const m = modal2;
|
|
936
|
+
return m.data?.custom_id ?? m.custom_id ?? m.customId;
|
|
937
|
+
}
|
|
938
|
+
async function showAndAwaitModal(interaction, modal2, options = {}) {
|
|
939
|
+
const customId = resolveModalCustomId(modal2);
|
|
940
|
+
await interaction.showModal(modal2);
|
|
941
|
+
const { time = 12e4, filter } = options;
|
|
942
|
+
try {
|
|
943
|
+
return await interaction.awaitModalSubmit({
|
|
944
|
+
time,
|
|
945
|
+
filter: (submitted) => submitted.user.id === interaction.user.id && (customId === void 0 || submitted.customId === customId) && (filter?.(submitted) ?? true)
|
|
946
|
+
});
|
|
947
|
+
} catch {
|
|
948
|
+
return null;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
var DEFAULT_AUTO_DEFER_DELAY_MS = 2e3;
|
|
952
|
+
function normalizeAutoDefer(input) {
|
|
953
|
+
if (input === void 0 || input === false) return void 0;
|
|
954
|
+
if (input === true) return { ephemeral: false, delayMs: DEFAULT_AUTO_DEFER_DELAY_MS };
|
|
955
|
+
return {
|
|
956
|
+
ephemeral: input.ephemeral ?? false,
|
|
957
|
+
delayMs: input.delayMs ?? DEFAULT_AUTO_DEFER_DELAY_MS
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
function armAutoDefer(interaction, config) {
|
|
961
|
+
const timer = setTimeout(() => {
|
|
962
|
+
if (!interaction.replied && !interaction.deferred) {
|
|
963
|
+
void interaction.deferReply(config.ephemeral ? { flags: MessageFlags.Ephemeral } : {}).catch(() => void 0);
|
|
964
|
+
}
|
|
965
|
+
}, config.delayMs);
|
|
966
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
967
|
+
return () => clearTimeout(timer);
|
|
968
|
+
}
|
|
521
969
|
function withEphemeralFlag(flags) {
|
|
522
970
|
if (flags == null) return MessageFlags.Ephemeral;
|
|
523
971
|
if (typeof flags === "number" || typeof flags === "bigint") {
|
|
@@ -614,6 +1062,38 @@ var BaseContext = class {
|
|
|
614
1062
|
await this.reply(input);
|
|
615
1063
|
}
|
|
616
1064
|
}
|
|
1065
|
+
/** The bot's resolved permissions in the current channel. */
|
|
1066
|
+
get botPermissions() {
|
|
1067
|
+
return this.interaction.appPermissions;
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Permission flag names the BOT is missing in the current channel — empty when
|
|
1071
|
+
* it has them all. Zero-fetch: reads the permissions Discord attached to the
|
|
1072
|
+
* interaction. Use before an action that needs elevated permissions.
|
|
1073
|
+
*/
|
|
1074
|
+
botMissing(required) {
|
|
1075
|
+
return this.interaction.appPermissions.missing(required);
|
|
1076
|
+
}
|
|
1077
|
+
/** Permission flag names the invoking USER is missing in the current channel. */
|
|
1078
|
+
userMissing(required) {
|
|
1079
|
+
const held = this.interaction.memberPermissions;
|
|
1080
|
+
if (held === null) return new PermissionsBitField(required).toArray();
|
|
1081
|
+
return held.missing(required);
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Wait for the next message in this channel from `userId` (defaults to the
|
|
1085
|
+
* invoking user), resolving to it or `null` on timeout. The "type your answer"
|
|
1086
|
+
* flow without hand-rolling a collector.
|
|
1087
|
+
*/
|
|
1088
|
+
awaitMessageFrom(userId = this.user.id, options = {}) {
|
|
1089
|
+
const channel = this.interaction.channel;
|
|
1090
|
+
if (channel === null || !("awaitMessages" in channel)) return Promise.resolve(null);
|
|
1091
|
+
const extra = options.filter;
|
|
1092
|
+
return awaitMessage(channel, {
|
|
1093
|
+
time: options.time,
|
|
1094
|
+
filter: (message) => message.author.id === userId && (extra?.(message) ?? true)
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
617
1097
|
/** Get the configured {@link Embeds} factory — `client.embeds` or the default. */
|
|
618
1098
|
getEmbeds() {
|
|
619
1099
|
return this.interaction.client.embeds ?? defaultEmbeds;
|
|
@@ -778,6 +1258,7 @@ function userCommand(config) {
|
|
|
778
1258
|
name: config.name,
|
|
779
1259
|
cooldown,
|
|
780
1260
|
guards: config.guards,
|
|
1261
|
+
autoDefer: normalizeAutoDefer(config.autoDefer),
|
|
781
1262
|
toJSON: () => baseJSON(config, ApplicationCommandType.User),
|
|
782
1263
|
execute: async (interaction) => {
|
|
783
1264
|
await config.run(new UserContextMenuContext(interaction));
|
|
@@ -791,6 +1272,7 @@ function messageCommand(config) {
|
|
|
791
1272
|
name: config.name,
|
|
792
1273
|
cooldown,
|
|
793
1274
|
guards: config.guards,
|
|
1275
|
+
autoDefer: normalizeAutoDefer(config.autoDefer),
|
|
794
1276
|
toJSON: () => baseJSON(config, ApplicationCommandType.Message),
|
|
795
1277
|
execute: async (interaction) => {
|
|
796
1278
|
await config.run(new MessageContextMenuContext(interaction));
|
|
@@ -805,6 +1287,7 @@ var ContextMenuRegistry = class {
|
|
|
805
1287
|
defaultCooldown;
|
|
806
1288
|
defaultGuards = [];
|
|
807
1289
|
onUsage;
|
|
1290
|
+
defaultAutoDefer;
|
|
808
1291
|
/** Register one or more context-menu commands. */
|
|
809
1292
|
add(...commands) {
|
|
810
1293
|
for (const command2 of commands) {
|
|
@@ -838,6 +1321,11 @@ var ContextMenuRegistry = class {
|
|
|
838
1321
|
this.defaultGuards = guards;
|
|
839
1322
|
return this;
|
|
840
1323
|
}
|
|
1324
|
+
/** Default auto-defer applied to menus that don't set their own. */
|
|
1325
|
+
setAutoDefer(config) {
|
|
1326
|
+
this.defaultAutoDefer = config;
|
|
1327
|
+
return this;
|
|
1328
|
+
}
|
|
841
1329
|
setUsageHook(hook) {
|
|
842
1330
|
this.onUsage = hook;
|
|
843
1331
|
return this;
|
|
@@ -881,6 +1369,8 @@ var ContextMenuRegistry = class {
|
|
|
881
1369
|
return;
|
|
882
1370
|
}
|
|
883
1371
|
}
|
|
1372
|
+
const autoDefer = command2.autoDefer ?? this.defaultAutoDefer;
|
|
1373
|
+
const cancelAutoDefer = autoDefer !== void 0 ? armAutoDefer(interaction, autoDefer) : void 0;
|
|
884
1374
|
const start = Date.now();
|
|
885
1375
|
try {
|
|
886
1376
|
if (command2.kind === "userMenu") {
|
|
@@ -916,15 +1406,19 @@ var ContextMenuRegistry = class {
|
|
|
916
1406
|
timestamp: /* @__PURE__ */ new Date()
|
|
917
1407
|
});
|
|
918
1408
|
interaction.client.emit("error", err);
|
|
1409
|
+
const content = explainDiscordError(err) ?? "Something went wrong.";
|
|
919
1410
|
try {
|
|
920
|
-
if (
|
|
921
|
-
await interaction.
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1411
|
+
if (interaction.deferred) {
|
|
1412
|
+
await interaction.editReply({ content });
|
|
1413
|
+
} else if (interaction.replied) {
|
|
1414
|
+
await interaction.followUp({ content, flags: MessageFlags.Ephemeral });
|
|
1415
|
+
} else {
|
|
1416
|
+
await interaction.reply({ content, flags: MessageFlags.Ephemeral });
|
|
925
1417
|
}
|
|
926
1418
|
} catch {
|
|
927
1419
|
}
|
|
1420
|
+
} finally {
|
|
1421
|
+
cancelAutoDefer?.();
|
|
928
1422
|
}
|
|
929
1423
|
}
|
|
930
1424
|
};
|
|
@@ -1860,7 +2354,8 @@ function resolveOptions(input) {
|
|
|
1860
2354
|
prefixes: typeof prefix === "string" ? [prefix] : [...prefix],
|
|
1861
2355
|
mention: options.mention ?? true,
|
|
1862
2356
|
ignoreBots: options.ignoreBots ?? true,
|
|
1863
|
-
caseInsensitive: options.caseInsensitive ?? true
|
|
2357
|
+
caseInsensitive: options.caseInsensitive ?? true,
|
|
2358
|
+
dynamic: options.dynamic
|
|
1864
2359
|
};
|
|
1865
2360
|
}
|
|
1866
2361
|
function actorFromMessage(message) {
|
|
@@ -1944,8 +2439,8 @@ var PrefixRegistry = class {
|
|
|
1944
2439
|
return [...this.commands.values()];
|
|
1945
2440
|
}
|
|
1946
2441
|
/** Strip a matching prefix (or bot mention) from `content`, or return `null`. */
|
|
1947
|
-
stripPrefix(content, botId) {
|
|
1948
|
-
for (const prefix of
|
|
2442
|
+
stripPrefix(content, botId, prefixes) {
|
|
2443
|
+
for (const prefix of prefixes) {
|
|
1949
2444
|
if (prefix.length > 0 && content.startsWith(prefix)) return content.slice(prefix.length);
|
|
1950
2445
|
}
|
|
1951
2446
|
if (this.options.mention && botId !== void 0) {
|
|
@@ -1954,14 +2449,24 @@ var PrefixRegistry = class {
|
|
|
1954
2449
|
}
|
|
1955
2450
|
return null;
|
|
1956
2451
|
}
|
|
2452
|
+
/** Resolve the effective prefixes for a message: static plus any dynamic ones. */
|
|
2453
|
+
async resolvePrefixes(message) {
|
|
2454
|
+
if (this.options.dynamic === void 0) return this.options.prefixes;
|
|
2455
|
+
const extra = await this.options.dynamic(message);
|
|
2456
|
+
if (extra === null || extra === void 0) return this.options.prefixes;
|
|
2457
|
+
const list = typeof extra === "string" ? [extra] : extra;
|
|
2458
|
+
return [...this.options.prefixes, ...list];
|
|
2459
|
+
}
|
|
1957
2460
|
/**
|
|
1958
2461
|
* Parse and dispatch a message. Returns `true` when a command ran (or was
|
|
1959
2462
|
* blocked by a cooldown), `false` when the message was not a prefix command.
|
|
1960
2463
|
*/
|
|
1961
2464
|
async handle(message) {
|
|
1962
|
-
|
|
2465
|
+
const hasMatcher = this.options.prefixes.length > 0 || this.options.mention || this.options.dynamic !== void 0;
|
|
2466
|
+
if (!hasMatcher) return false;
|
|
1963
2467
|
if (this.options.ignoreBots && message.author.bot) return false;
|
|
1964
|
-
const
|
|
2468
|
+
const prefixes = await this.resolvePrefixes(message);
|
|
2469
|
+
const stripped = this.stripPrefix(message.content, message.client.user?.id, prefixes);
|
|
1965
2470
|
if (stripped === null) return false;
|
|
1966
2471
|
const trimmed = stripped.trimStart();
|
|
1967
2472
|
const match = /^(\S+)\s*([\s\S]*)$/.exec(trimmed);
|
|
@@ -2309,6 +2814,13 @@ var CommandContext = class extends BaseContext {
|
|
|
2309
2814
|
async showModal(modal2) {
|
|
2310
2815
|
await this.interaction.showModal(modal2);
|
|
2311
2816
|
}
|
|
2817
|
+
/**
|
|
2818
|
+
* Show a modal and wait for the user to submit it, resolving to the submission
|
|
2819
|
+
* or `null` if they dismiss it / it times out. Scoped to this user and modal.
|
|
2820
|
+
*/
|
|
2821
|
+
awaitModal(modal2, options) {
|
|
2822
|
+
return showAndAwaitModal(this.interaction, modal2, options);
|
|
2823
|
+
}
|
|
2312
2824
|
};
|
|
2313
2825
|
var AutocompleteContext = class {
|
|
2314
2826
|
constructor(interaction) {
|
|
@@ -2363,6 +2875,8 @@ var SlashCommand = class {
|
|
|
2363
2875
|
cooldown;
|
|
2364
2876
|
/** Resolved guard list for this command, if any. */
|
|
2365
2877
|
guards;
|
|
2878
|
+
/** Resolved auto-defer configuration for this command, if any. */
|
|
2879
|
+
autoDefer;
|
|
2366
2880
|
/** @internal */
|
|
2367
2881
|
constructor(spec) {
|
|
2368
2882
|
this.name = spec.name;
|
|
@@ -2372,6 +2886,7 @@ var SlashCommand = class {
|
|
|
2372
2886
|
this.autocompleter = spec.autocompleter;
|
|
2373
2887
|
this.cooldown = spec.cooldown;
|
|
2374
2888
|
this.guards = spec.guards;
|
|
2889
|
+
this.autoDefer = spec.autoDefer;
|
|
2375
2890
|
}
|
|
2376
2891
|
/** Serialise to the discord REST chat-input command payload. */
|
|
2377
2892
|
toJSON() {
|
|
@@ -2451,7 +2966,8 @@ function command(config) {
|
|
|
2451
2966
|
executor,
|
|
2452
2967
|
autocompleter: makeAutocompleter(options),
|
|
2453
2968
|
cooldown: config.cooldown !== void 0 ? normalizeCooldown(config.cooldown) : void 0,
|
|
2454
|
-
guards: config.guards
|
|
2969
|
+
guards: config.guards,
|
|
2970
|
+
autoDefer: normalizeAutoDefer(config.autoDefer)
|
|
2455
2971
|
});
|
|
2456
2972
|
}
|
|
2457
2973
|
function subcommand(config) {
|
|
@@ -2524,7 +3040,8 @@ function commandGroup(config) {
|
|
|
2524
3040
|
executor,
|
|
2525
3041
|
autocompleter,
|
|
2526
3042
|
cooldown: config.cooldown !== void 0 ? normalizeCooldown(config.cooldown) : void 0,
|
|
2527
|
-
guards: config.guards
|
|
3043
|
+
guards: config.guards,
|
|
3044
|
+
autoDefer: normalizeAutoDefer(config.autoDefer)
|
|
2528
3045
|
});
|
|
2529
3046
|
}
|
|
2530
3047
|
var CommandRegistry = class {
|
|
@@ -2535,6 +3052,7 @@ var CommandRegistry = class {
|
|
|
2535
3052
|
defaultCooldown;
|
|
2536
3053
|
defaultGuards = [];
|
|
2537
3054
|
onUsage;
|
|
3055
|
+
defaultAutoDefer;
|
|
2538
3056
|
/** Register one or more commands. Later registrations override by name. */
|
|
2539
3057
|
add(...commands) {
|
|
2540
3058
|
for (const command2 of commands) this.commands.set(command2.name, command2);
|
|
@@ -2581,6 +3099,11 @@ var CommandRegistry = class {
|
|
|
2581
3099
|
this.defaultGuards = guards;
|
|
2582
3100
|
return this;
|
|
2583
3101
|
}
|
|
3102
|
+
/** Default auto-defer applied to commands that don't set their own. */
|
|
3103
|
+
setAutoDefer(config) {
|
|
3104
|
+
this.defaultAutoDefer = config;
|
|
3105
|
+
return this;
|
|
3106
|
+
}
|
|
2584
3107
|
/** Attach a hook called after each successful command execution. */
|
|
2585
3108
|
setUsageHook(hook) {
|
|
2586
3109
|
this.onUsage = hook;
|
|
@@ -2620,6 +3143,8 @@ var CommandRegistry = class {
|
|
|
2620
3143
|
return;
|
|
2621
3144
|
}
|
|
2622
3145
|
}
|
|
3146
|
+
const autoDefer = command2.autoDefer ?? this.defaultAutoDefer;
|
|
3147
|
+
const cancelAutoDefer = autoDefer !== void 0 ? armAutoDefer(interaction, autoDefer) : void 0;
|
|
2623
3148
|
const start = Date.now();
|
|
2624
3149
|
try {
|
|
2625
3150
|
await command2.execute(interaction);
|
|
@@ -2653,6 +3178,8 @@ var CommandRegistry = class {
|
|
|
2653
3178
|
} else {
|
|
2654
3179
|
await this.defaultErrorReply(err, interaction);
|
|
2655
3180
|
}
|
|
3181
|
+
} finally {
|
|
3182
|
+
cancelAutoDefer?.();
|
|
2656
3183
|
}
|
|
2657
3184
|
}
|
|
2658
3185
|
/** Dispatch an autocomplete interaction to its command. */
|
|
@@ -2685,7 +3212,7 @@ var CommandRegistry = class {
|
|
|
2685
3212
|
}
|
|
2686
3213
|
async defaultErrorReply(error, interaction) {
|
|
2687
3214
|
interaction.client.emit("error", error);
|
|
2688
|
-
const content = "Something went wrong while running that command.";
|
|
3215
|
+
const content = explainDiscordError(error) ?? "Something went wrong while running that command.";
|
|
2689
3216
|
try {
|
|
2690
3217
|
if (interaction.deferred) {
|
|
2691
3218
|
await interaction.editReply({ content });
|
|
@@ -2901,6 +3428,13 @@ var MessageComponentContext = class extends BaseContext {
|
|
|
2901
3428
|
async showModal(modal2) {
|
|
2902
3429
|
await this.interaction.showModal(modal2);
|
|
2903
3430
|
}
|
|
3431
|
+
/**
|
|
3432
|
+
* Show a modal and wait for the user to submit it, resolving to the submission
|
|
3433
|
+
* or `null` if they dismiss it / it times out. Scoped to this user and modal.
|
|
3434
|
+
*/
|
|
3435
|
+
awaitModal(modal2, options) {
|
|
3436
|
+
return showAndAwaitModal(this.interaction, modal2, options);
|
|
3437
|
+
}
|
|
2904
3438
|
};
|
|
2905
3439
|
var ButtonContext = class extends MessageComponentContext {
|
|
2906
3440
|
};
|
|
@@ -3112,8 +3646,13 @@ var ComponentRegistry = class {
|
|
|
3112
3646
|
await this.errorHandler(err, interaction);
|
|
3113
3647
|
} else {
|
|
3114
3648
|
interaction.client.emit("error", err);
|
|
3115
|
-
|
|
3116
|
-
|
|
3649
|
+
const content = explainDiscordError(err) ?? "Something went wrong.";
|
|
3650
|
+
if (interaction.deferred) {
|
|
3651
|
+
await interaction.editReply({ content }).catch(() => void 0);
|
|
3652
|
+
} else if (interaction.replied) {
|
|
3653
|
+
await interaction.followUp({ content, flags: MessageFlags.Ephemeral }).catch(() => void 0);
|
|
3654
|
+
} else {
|
|
3655
|
+
await interaction.reply({ content, flags: MessageFlags.Ephemeral }).catch(() => void 0);
|
|
3117
3656
|
}
|
|
3118
3657
|
}
|
|
3119
3658
|
}
|
|
@@ -3415,17 +3954,20 @@ var SpearClient = class extends Client {
|
|
|
3415
3954
|
contextMenus = new ContextMenuRegistry();
|
|
3416
3955
|
envConfig;
|
|
3417
3956
|
constructor(options = {}) {
|
|
3418
|
-
const { intents, logger, dotenv, cooldown, prefix, usage, embeds, guards, ...rest } = options;
|
|
3957
|
+
const { intents, logger, dotenv, cooldown, prefix, usage, embeds, guards, autoDefer, ...rest } = options;
|
|
3419
3958
|
super({ ...rest, intents: intents ?? Intents.default });
|
|
3420
3959
|
this.embeds = embeds instanceof Embeds ? embeds : new Embeds(embeds);
|
|
3421
3960
|
this.envConfig = dotenv === false ? false : dotenv === void 0 || dotenv === true ? {} : dotenv;
|
|
3422
3961
|
this.logger = logger instanceof Logger ? logger : new Logger(logger);
|
|
3423
3962
|
const defaultCooldown = cooldown !== void 0 ? normalizeCooldown(cooldown) : void 0;
|
|
3963
|
+
const defaultAutoDefer = normalizeAutoDefer(autoDefer);
|
|
3424
3964
|
this.commands.setLogger(this.logger.child("commands"));
|
|
3425
3965
|
this.commands.setCooldowns(this.cooldowns, defaultCooldown);
|
|
3966
|
+
this.commands.setAutoDefer(defaultAutoDefer);
|
|
3426
3967
|
this.components.setLogger(this.logger.child("components"));
|
|
3427
3968
|
this.contextMenus.setLogger(this.logger.child("contextMenus"));
|
|
3428
3969
|
this.contextMenus.setCooldowns(this.cooldowns, defaultCooldown);
|
|
3970
|
+
this.contextMenus.setAutoDefer(defaultAutoDefer);
|
|
3429
3971
|
this.prefix.setLogger(this.logger.child("prefix"));
|
|
3430
3972
|
this.prefix.setCooldowns(this.cooldowns, defaultCooldown);
|
|
3431
3973
|
if (prefix !== void 0) this.prefix.setOptions(prefix);
|
|
@@ -3559,6 +4101,21 @@ var SpearClient = class extends Client {
|
|
|
3559
4101
|
this.scheduler.stop();
|
|
3560
4102
|
await super.destroy();
|
|
3561
4103
|
}
|
|
4104
|
+
/**
|
|
4105
|
+
* Close the bot cleanly on `SIGINT`/`SIGTERM`: run an optional hook, then
|
|
4106
|
+
* `destroy()` (stopping the scheduler and gateway), then exit. Returns a
|
|
4107
|
+
* disposer that removes the signal handlers. Logs progress via `client.logger`.
|
|
4108
|
+
*/
|
|
4109
|
+
enableGracefulShutdown(options = {}) {
|
|
4110
|
+
const log = this.logger.child("shutdown");
|
|
4111
|
+
return gracefulShutdown(this, {
|
|
4112
|
+
logger: {
|
|
4113
|
+
info: (message) => log.info(message),
|
|
4114
|
+
error: (message, meta) => log.error(message, { error: toError(meta) })
|
|
4115
|
+
},
|
|
4116
|
+
...options
|
|
4117
|
+
});
|
|
4118
|
+
}
|
|
3562
4119
|
async route(interaction) {
|
|
3563
4120
|
if (interaction.isChatInputCommand()) {
|
|
3564
4121
|
await this.commands.handle(interaction);
|
|
@@ -3619,6 +4176,6 @@ function definePlugin(plugin) {
|
|
|
3619
4176
|
return plugin;
|
|
3620
4177
|
}
|
|
3621
4178
|
|
|
3622
|
-
export { AutocompleteContext, BaseContext, ButtonContext, ChannelSelectContext, CommandContext, CommandRegistry, ComponentRegistry, ContextMenuRegistry, CooldownManager, CronExpression, DEFAULT_EMBED_COLORS, DEFAULT_EMBED_ICONS, Embeds, EventRegistry, Intents, JsonFileUsageStore, KeyedLock, Logger, MAX_CUSTOM_ID_LENGTH, MemoryCache, MemoryUsageStore, MentionableSelectContext, MessageComponentContext, MessageContextMenuContext, ModalContext, PrefixArgsBuilder, PrefixContext, PrefixRegistry, RoleSelectContext, SlashCommand, SpearClient, StringSelectContext, TaskScheduler, UsageTracker, UserContextMenuContext, UserSelectContext, asEphemeral, buildCustomId, buildPaginatorPage, button, channelSelect, collectModules, command, commandGroup, compilePattern, confirm, consoleSink, createCache, cron, defaultEmbeds, definePlugin, denied, discordTimestamp, dmOnly, effectiveDuration, env, event, fetchChannel, fetchGuild, fetchMember, fetchMessage, fetchRole, fetchUser, formatCooldownMessage, formatDuration, formatUsage, guard, guildOnly, jsonlSink, linkButton, loadConfig, loadConfigAsync, loadEnv, loadInto, lookup, lookupOptional, mentionableSelect, messageCommand, modal, normalizeCooldown, normalizeReply, option, optionsHaveAutocomplete, paginate, paramsFromValues, parseCustomId, parseDuration, parseEnv, prefixArgs, prefixCommand, readOption, relativeTimestamp, requireAllRoles, requireAnyRole, requireBotPermissions, requireOwner, requireUserPermissions, roleSelect, row, runGuards, safeFetch, safeTry, stringSelect, subcommand, subcommandGroup, task, textInput, toAPIOption, toError, userCommand, userSelect, webhookSink, withSafeTimeout };
|
|
4179
|
+
export { AutocompleteContext, BaseContext, ButtonContext, ChannelSelectContext, CommandContext, CommandRegistry, ComponentRegistry, ContextMenuRegistry, CooldownManager, CronExpression, DEFAULT_AUTO_DEFER_DELAY_MS, DEFAULT_EMBED_COLORS, DEFAULT_EMBED_ICONS, DiscordErrorCode, Embeds, EventRegistry, Intents, JsonFileUsageStore, JsonStore, KeyedLock, Logger, MAX_CUSTOM_ID_LENGTH, MESSAGE_CHARACTER_LIMIT, MemoryCache, MemoryStore, MemoryUsageStore, MentionableSelectContext, MessageComponentContext, MessageContextMenuContext, ModalContext, PrefixArgsBuilder, PrefixContext, PrefixRegistry, RoleSelectContext, SlashCommand, SpearClient, StringSelectContext, TaskScheduler, UsageTracker, UserContextMenuContext, UserSelectContext, armAutoDefer, asEphemeral, awaitComponent, awaitMessage, botMissingPermissions, buildCustomId, buildPaginatorPage, button, canActOn, channelSelect, chunkMessage, collectModules, command, commandGroup, compareRoles, compilePattern, confirm, consoleSink, createCache, createSettings, cron, defaultEmbeds, definePlugin, denied, discordTimestamp, dmOnly, effectiveDuration, env, event, explainDiscordError, fetchChannel, fetchGuild, fetchMember, fetchMessage, fetchRole, fetchUser, formatCooldownMessage, formatDuration, formatPermissions, formatUsage, gracefulShutdown, guard, guildOnly, hasPermissions, isDiscordError, isHTTPError, isRateLimitError, jsonlSink, linkButton, loadConfig, loadConfigAsync, loadEnv, loadInto, lookup, lookupOptional, mentionableSelect, messageCommand, missingPermissions, modal, moderationCheck, namespaced, normalizeAutoDefer, normalizeCooldown, normalizeReply, option, optionsHaveAutocomplete, paginate, paramsFromValues, parseCustomId, parseDuration, parseEnv, prefixArgs, prefixCommand, readOption, relativeTimestamp, requireAllRoles, requireAnyRole, requireBotPermissions, requireOwner, requireUserPermissions, roleSelect, row, runGuards, safeFetch, safeTry, showAndAwaitModal, stringSelect, subcommand, subcommandGroup, task, textInput, toAPIOption, toError, truncate, userCommand, userSelect, webhookSink, withSafeTimeout };
|
|
3623
4180
|
//# sourceMappingURL=index.js.map
|
|
3624
4181
|
//# sourceMappingURL=index.js.map
|