refac: use purrkit instead of sapphire
vi did:web:vt3e.cat
Fri, 12 Jun 2026 23:40:35 +0100
8 files changed,
39 insertions(+),
227 deletions(-)
M
bun.lock
→
bun.lock
@@ -5,7 +5,7 @@ "workspaces": {
"": { "name": "utils", "dependencies": { - "@sapphire/framework": "^5.5.0", + "@purrkit/core": "^1.0.1", "@sapphire/result": "^2.8.0", "discord.js": "^14.26.4", "pino": "^10.3.1",@@ -114,31 +114,15 @@ "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.66.0", "", { "os": "win32", "cpu": "x64" }, "sha512-/DbBvw8UFBhja6PqudUjV4UtfsJr0Oa7jUjWVKB0g86lj/VwnPrkngn0sFql3c9RDA0O16dh7ozsXb6GjNAzBQ=="],
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], - "@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="], - - "@sapphire/discord-utilities": ["@sapphire/discord-utilities@4.0.0", "", { "dependencies": { "discord-api-types": "^0.38.30" } }, "sha512-QAvrKNHgswz+ZX48WqSYpRiRzQcugNXXB1C3fR1qbpTJGd7Ckr2OWyFK88TyOksi3U2isrk8sMriTcAgaIe7Qg=="], - - "@sapphire/discord.js-utilities": ["@sapphire/discord.js-utilities@7.3.3", "", { "dependencies": { "@sapphire/discord-utilities": "^3.5.0", "@sapphire/duration": "^1.2.0", "@sapphire/utilities": "^3.18.2", "tslib": "^2.8.1" } }, "sha512-WDj+zjWgNCUSvzYDD0wY3TVeTUseHq0Nhk0wVWxSDjY8z2gFEVcpY7wF8/fbTDWP44LUG5sUQ4haIrIj2OjmkQ=="], - - "@sapphire/duration": ["@sapphire/duration@1.2.0", "", {}, "sha512-LxjOAFXz81WmrI8XX9YaVcAZDjQj/1p78lZCvkAWZB1nphOwz/D0dU3CBejmhOWx5dO5CszTkLJMNR0xuCK+Zg=="], - - "@sapphire/framework": ["@sapphire/framework@5.5.0", "", { "dependencies": { "@discordjs/builders": "^1.13.0", "@sapphire/discord-utilities": "^4.0.0", "@sapphire/discord.js-utilities": "^7.3.3", "@sapphire/lexure": "^1.1.12", "@sapphire/pieces": "^4.4.1", "@sapphire/ratelimits": "^2.4.11", "@sapphire/result": "^2.8.0", "@sapphire/stopwatch": "^1.5.4", "@sapphire/utilities": "^3.18.2" } }, "sha512-jC+zS8yR/MtJaTFEQutBBWj7Scuvny4d0adtQ3s0OCFWe3rUqNfENomBG3P4tsQ/kRF7L3WCqucyLV8ywG24hw=="], - - "@sapphire/lexure": ["@sapphire/lexure@1.1.12", "", { "dependencies": { "@sapphire/result": "^2.8.0" } }, "sha512-F7Z3QzRnAZGunRl24/qQMhzRogZU/foumu2EBBunRnQi/o/DLTCwdAbLgJATyPlvJa8N6FrJq0JJwvzM/vXoXg=="], + "@purrkit/core": ["@purrkit/core@1.0.1", "", { "peerDependencies": { "discord.js": "^14.26.4", "typescript": "^5" } }, "sha512-UzOgkHuCe/+tGwf4l/zp0xre4VJ9ZbGqvtuErd/abp2gFOTKksxE4VRAALzea4kY9QQeGRXQibZQBfazl0ww+A=="], - "@sapphire/pieces": ["@sapphire/pieces@4.4.1", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@sapphire/utilities": "^3.18.2", "tslib": "^2.8.1" } }, "sha512-2v49P++RzHGb23bdjSa9u7flkdvhrq94IUO9PneY538TILN5QiMxfaXVK+pDR2YR7jpQYBy9APwtFDes2Svykg=="], - - "@sapphire/ratelimits": ["@sapphire/ratelimits@2.4.11", "", {}, "sha512-O6FNA/P0wxU4Ve9gxL948CoZw7+sSpujyUR2CLyLLCNuNvuFGFxPCJVl5crFVLXMIyBIrc2qk+/H9bsqsyQK1Q=="], + "@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="], "@sapphire/result": ["@sapphire/result@2.8.0", "", {}, "sha512-693yWouX+hR9uJm1Jgq0uSSjbSD3UrblMaxiuGbHPjSwzLCSZTcm0h3kvdVhq3o/yl4+oeAWW3hiaJ0TELuRJQ=="], "@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="], "@sapphire/snowflake": ["@sapphire/snowflake@3.5.3", "", {}, "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ=="], - - "@sapphire/stopwatch": ["@sapphire/stopwatch@1.5.4", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-IVI48D2yAz411bSttXyTkBH0p2vhrXoqWLn5loDDSAAEUGkM1r5KNCX2027ifQ8svdoMkUfIGjFueR+satLeWw=="], - - "@sapphire/utilities": ["@sapphire/utilities@3.18.2", "", {}, "sha512-QGLdC9+pT74Zd7aaObqn0EUfq40c4dyTL65pFnkM6WO1QYN7Yg/s4CdH+CXmx0Zcu6wcfCWILSftXPMosJHP5A=="], "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],@@ -239,10 +223,6 @@
"@discordjs/rest/@sapphire/snowflake": ["@sapphire/snowflake@3.5.5", "", {}, "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ=="], "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], - - "@sapphire/discord.js-utilities/@sapphire/discord-utilities": ["@sapphire/discord-utilities@3.5.0", "", { "dependencies": { "discord-api-types": "^0.38.1" } }, "sha512-H4SY5KTVDZrqA5QG7ob6etwqhdOb3TRSY2wv56f0tiobUdIr0irlrYvdmr8Kg/FRxWU+aiHDIISWGG5vBuxOGw=="], - - "@sapphire/pieces/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], "pino/real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], }
M
package.json
→
package.json
@@ -9,7 +9,7 @@ "fmt": "bunx oxfmt fmt src tests",
"lint": "bunx oxlint lint src tests" }, "dependencies": { - "@sapphire/framework": "^5.5.0", + "@purrkit/core": "^1.0.1", "@sapphire/result": "^2.8.0", "discord.js": "^14.26.4", "pino": "^10.3.1",
M
src/commands/ban.ts
→
src/commands/ban.ts
@@ -1,9 +1,11 @@
-import { Command, container, Result } from "@sapphire/framework"; +import { Result } from "@sapphire/result"; import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; import { parseIncludedGames, parseDuration, createReactive } from "@/utils"; import config, { type Game } from "@/config"; import { ban, getUser } from "@/roblox"; +import { kitten } from "@/discord"; +import { option } from "@purrkit/core"; const { games: GAMES } = config; type State = {@@ -51,62 +53,20 @@
return row; } -export class BanCommand extends Command { - public constructor(context: Command.LoaderContext, options: Command.Options) { - super(context, { ...options }); - } - - public override registerApplicationCommands(registry: Command.Registry) { - registry.registerChatInputCommand((builder) => - builder - .setName("ban") - .setDescription("ban a player") - .addStringOption((option) => - option.setName("player").setDescription("the ID of the player to ban").setRequired(true), - ) - .addStringOption((option) => - option.setName("reason").setDescription("the reason for the ban").setRequired(true), - ) - .addStringOption((option) => - option - .setName("private-reason") - .setDescription("the private reason for the ban") - .setRequired(true), - ) - .addStringOption((option) => - option - .setName("length") - .setDescription("length of the ban; defaults to permanent") - .setRequired(false), - ) - .addBooleanOption((option) => - option - .setName("ban-alts") - .setDescription("whether to ban alts of the player") - .setRequired(false), - ) - .addStringOption((option) => - option - .setName("games") - .setDescription("the games to ban them from; defaults to all") - .setRequired(false), - ), - ); - } - - public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { +export const banCommand = kitten.command("ban", { + description: "ban a user from the specified games", + options: { + player: option.string("ID of the player to ban", { required: true }), + reason: option.string("reason for the ban", { required: true }), + "private-reason": option.string("private reason for the ban", { required: true }), + length: option.string("length of the ban; defaults to permanent"), + "ban-alts": option.boolean("whether to ban alts of the user; defaults to true"), + games: option.string("games to ban them from; defaults to all"), + }, + run: async (interaction, options) => { await interaction.deferReply({ withResponse: true }); - const options = { - player: interaction.options.getString("player", true), - reason: interaction.options.getString("reason", true), - privateReason: interaction.options.getString("private-reason", true), - length: interaction.options.getString("length", false), - banAlts: interaction.options.getBoolean("ban-alts", false), - includedGames: interaction.options.getString("games", false), - }; - - const gamesResult = Result.from(() => parseIncludedGames(options.includedGames)); + const gamesResult = Result.from(() => parseIncludedGames(options.games)); if (gamesResult.isErr()) { const err = gamesResult.unwrapErr(); const message = err instanceof Error ? err.message : "failed to parse included games";@@ -137,7 +97,7 @@ );
const details = [ `> **Reason:** ${options.reason}`, - `> **Private Reason:** ${options.privateReason}`, + `> **Private Reason:** ${options["private-reason"]}`, ].join("\n"); const state = createReactive<State>(@@ -159,9 +119,9 @@ );
const banOptions = { duration, - excludeAltAccounts: !options.banAlts, + excludeAltAccounts: !!options["ban-alts"], displayReason: options.reason, - privateReason: options.privateReason, + privateReason: options["private-reason"], }; await Promise.all(@@ -175,11 +135,5 @@ }),
); state.prefix = "✅ Banned"; - } -} - -container.stores.loadPiece({ - piece: BanCommand, - name: "ban", - store: "commands", + }, });
M
src/discord/client.ts
→
src/discord/client.ts
@@ -1,20 +1,9 @@
-import { LogLevel, SapphireClient } from "@sapphire/framework"; - -import { PinoSapphireLogger } from "@/logger"; -import "@/commands"; -import config from "@/config"; +import { Client } from "discord.js"; -export const client = new SapphireClient({ +export const client = new Client({ intents: [], - baseUserDirectory: null, allowedMentions: { parse: [], repliedUser: true, - }, - logger: { - instance: new PinoSapphireLogger( - { level: "trace" }, - config.isProduction ? LogLevel.Info : LogLevel.Debug, - ), }, });
M
src/discord/index.ts
→
src/discord/index.ts
@@ -1,2 +1,7 @@
+import { Kitten } from "@purrkit/core"; +import { client } from "./client"; + export * from "./rest"; export * from "./client"; + +export const kitten = new Kitten(client);
M
src/index.ts
→
src/index.ts
@@ -1,8 +1,12 @@
-import { client } from "@/discord"; +import { Events } from "discord.js"; +import { client, kitten } from "@/discord"; + +import * as commands from "@/commands"; import config from "@/config"; -async function main() { - await client.login(config.discord.token); -} +client.on(Events.ClientReady, async () => { + kitten.register({ commands: [commands.banCommand] }); + await kitten.sync(); +}); -await main(); +client.login(config.discord.token);
D
src/logger.ts
@@ -1,113 +0,0 @@
-import { type ILogger, LogLevel } from "@sapphire/framework"; -import pino, { type Logger as PinoInstance } from "pino"; -import config from "@/config"; - -export class PinoSapphireLogger implements ILogger { - private pino: PinoInstance; - private minLevel: LogLevel; - - constructor(options?: pino.LoggerOptions, minLevel: LogLevel = LogLevel.Info) { - this.minLevel = minLevel; - - this.pino = pino({ - transport: config.isProduction - ? undefined - : { target: "pino-pretty", options: { colorize: true } }, - base: { - pid: false, - }, - level: "trace", - ...options, - }); - } - - public has(level: LogLevel): boolean { - return level >= this.minLevel && level !== LogLevel.None; - } - - public write(level: LogLevel, ...values: readonly unknown[]): void { - if (!this.has(level)) return; - - const { name, msg, obj } = this.extractModuleAndPayload(values); - const payload = { name, ...obj }; - - switch (level) { - case LogLevel.Trace: - this.pino.trace(payload, msg); - break; - case LogLevel.Debug: - this.pino.debug(payload, msg); - break; - case LogLevel.Info: - this.pino.info(payload, msg); - break; - case LogLevel.Warn: - this.pino.warn(payload, msg); - break; - case LogLevel.Error: - this.pino.error(payload, msg); - break; - case LogLevel.Fatal: - this.pino.fatal(payload, msg); - break; - default: - break; - } - } - - public trace(...values: readonly unknown[]): void { - this.write(LogLevel.Trace, ...values); - } - public debug(...values: readonly unknown[]): void { - this.write(LogLevel.Debug, ...values); - } - public info(...values: readonly unknown[]): void { - this.write(LogLevel.Info, ...values); - } - public warn(...values: readonly unknown[]): void { - this.write(LogLevel.Warn, ...values); - } - public error(...values: readonly unknown[]): void { - this.write(LogLevel.Error, ...values); - } - public fatal(...values: readonly unknown[]): void { - this.write(LogLevel.Fatal, ...values); - } - - private extractModuleAndPayload(values: readonly unknown[]): { - name: string; - msg: string; - obj?: Record<string, unknown>; - } { - let name = "sapphire"; - let msg = ""; - let err: Error | undefined = undefined; - const extra: unknown[] = []; - const args = [...values]; - - if (args.length > 0 && typeof args[0] === "string") { - const firstArg = args[0]; - const colonIndex = firstArg.indexOf(":"); - - if (colonIndex !== -1) { - name = firstArg.substring(0, colonIndex).trim(); - const remainingMessage = firstArg.substring(colonIndex + 1).trim(); - - if (remainingMessage) args[0] = remainingMessage; - else args.shift(); - } - } - - for (const val of args) { - if (val instanceof Error) err = val; - else if (typeof val === "object" && val !== null) extra.push(val); - else msg += (msg ? " " : "") + String(val); - } - - const obj: Record<string, any> = {}; - if (err) obj.err = err; - if (extra.length > 0) obj.extra = extra; - - return { name, msg, obj }; - } -}
M
src/roblox/index.ts
→
src/roblox/index.ts
@@ -1,4 +1,3 @@
-import { container } from "@sapphire/framework"; import config from "@/config"; import type { BanResponse, FriendsResponse, RestrictionsResponse, RobloxUser } from "./types";@@ -82,11 +81,6 @@ displayReason: string;
privateReason: string; }, ): Promise<BanResponse> { - container.logger.debug(`banning ${userId} in universe ${universeId}`, { - name: "ban", - ...options, - }); - const makeRequest = async () => { const res = await fetch( `https://apis.roblox.com/user/cloud/v2/universes/${universeId}/user-restrictions/${userId}`,@@ -110,7 +104,6 @@ },
); const data = (await res.json()) as Record<string, unknown>; - container.logger.debug("ban response", { ...data }); if (res.status === 403) { const csrfHeader = res.headers.get("x-csrf-token");