From 49d6b51de65572f2392556d67db73c159db0023a Mon Sep 17 00:00:00 2001 From: grngxd <36968271+grngxd@users.noreply.github.com> Date: Thu, 31 Jul 2025 19:25:31 +0100 Subject: [PATCH 1/4] refactor: reorganize project structure and improve user command handling --- cmd/index.ts | 7 +++++++ cmd/register.ts | 18 ++++++++++++++++++ cmd/user.ts | 27 +++++++++++++++++++++------ db.ts => db/db.ts | 0 index.ts | 38 ++++++++++++++++++-------------------- lib.ts => lib/files.ts | 0 types.ts => types/files.ts | 0 7 files changed, 64 insertions(+), 26 deletions(-) create mode 100644 cmd/index.ts create mode 100644 cmd/register.ts rename db.ts => db/db.ts (100%) rename lib.ts => lib/files.ts (100%) rename types.ts => types/files.ts (100%) diff --git a/cmd/index.ts b/cmd/index.ts new file mode 100644 index 0000000..a703f5d --- /dev/null +++ b/cmd/index.ts @@ -0,0 +1,7 @@ +import register from "./register"; +import user from "./user"; + +export default [ + user, + register +] \ No newline at end of file diff --git a/cmd/register.ts b/cmd/register.ts new file mode 100644 index 0000000..a72d90c --- /dev/null +++ b/cmd/register.ts @@ -0,0 +1,18 @@ +import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js"; +import { db } from "../db/db"; + +export const data = new SlashCommandBuilder() + .setName("register") + .setDescription("create your stereo account"); + +export const execute = async (interaction: ChatInputCommandInteraction) => { + const existingUser = await db.get<{ id: string }>`SELECT id FROM users WHERE id = ${interaction.user.id}`; + if (existingUser) { + await interaction.reply({ content: "You already have a stereo account.", ephemeral: true }); + return; + } + + await interaction.reply({ content: "Your stereo account has been created!", ephemeral: true }); +} + +export default [data, execute] as const; \ No newline at end of file diff --git a/cmd/user.ts b/cmd/user.ts index 5714dcc..20f0a63 100644 --- a/cmd/user.ts +++ b/cmd/user.ts @@ -1,16 +1,26 @@ // commands/overview.ts import { ChatInputCommandInteraction, EmbedBuilder, MessageFlags, SlashCommandBuilder } from "discord.js"; -import { db } from "../db"; -import { formatSize } from "../lib"; -import { StereoFile } from "../types"; +import { db } from "../db/db"; +import { formatSize } from "../lib/files"; +import { StereoFile } from "../types/files"; export const data = new SlashCommandBuilder() - .setName("user") - .addUserOption(option => option.setName("user").setDescription("user to look up").setRequired(false)) - .setDescription("get information about a user (or yourself) on stereo"); + .setName("user") + .addUserOption(option => option.setName("user").setDescription("user to look up").setRequired(false)) + .setDescription("get information about a user (or yourself) on stereo"); export const execute = async (interaction: ChatInputCommandInteraction) => { const user = interaction.options.getUser("user") || interaction.user; + if (user.id === interaction.client.user?.id) { + await interaction.reply({ content: "hey! stop that!!!!", flags: MessageFlags.Ephemeral }); + return; + } + + if (user.bot) { + await interaction.reply({ content: "bots can't have stereo accounts...", flags: MessageFlags.Ephemeral }); + return; + } + if (!user) { await interaction.reply({ content: "couldn't find user", flags: MessageFlags.Ephemeral }); return; @@ -18,6 +28,11 @@ export const execute = async (interaction: ChatInputCommandInteraction) => { const id = await db.get<{ id: string }>`SELECT id FROM users WHERE id = ${user.id}`; if (!id) { + if (user.id === interaction.user.id) { + await interaction.reply({ content: "you don't have a stereo account yet, use the `/register` command to make one", flags: MessageFlags.Ephemeral }); + return; + } + await interaction.reply({ content: "this user doesn't have a stereo account", flags: MessageFlags.Ephemeral }); return; } diff --git a/db.ts b/db/db.ts similarity index 100% rename from db.ts rename to db/db.ts diff --git a/index.ts b/index.ts index 5822beb..d66a3df 100644 --- a/index.ts +++ b/index.ts @@ -1,22 +1,23 @@ import { ActivityType, Client, Events, GatewayIntentBits, REST, Routes } from "discord.js"; -import user from "./cmd/user"; -import { db } from "./db"; -import { StereoFile } from "./types"; +import commands from "./cmd"; +import { db } from "./db/db"; +import { StereoFile } from "./types/files"; const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, - ] - }); -const rest = new REST({ version: "10" }).setToken(process.env.TOKEN); +}); + +const rest = new REST({ version: "10" }) + .setToken(process.env.TOKEN); client.once(Events.ClientReady, async (c) => { await rest.put( Routes.applicationCommands(process.env.CLIENT_ID), - { body: commands.map(([data]) => data) } + { body: commands.map(([data, _]) => data) } ); const files = (await db.all`SELECT * FROM files`).length @@ -28,20 +29,17 @@ client.once(Events.ClientReady, async (c) => { console.log(c.user.tag); }); -const commands = [ - user -] - client.on(Events.InteractionCreate, async interaction => { - if (!interaction.isChatInputCommand()) return; - const cmd = commands.find(([data]) => data.name === interaction.commandName); - if (!cmd) return; - try { - await cmd[1](interaction); - } catch (err) { - console.error(err); - await interaction.reply({ content: "there was an error executing this command", ephemeral: true }); - } + if (!interaction.isChatInputCommand()) return; + const cmd = commands.find(([data]) => data.name === interaction.commandName); + if (!cmd) return; + + try { + await cmd[1](interaction); + } catch (err) { + console.error(err); + await interaction.reply({ content: "there was an error executing this command", ephemeral: true }); + } }); client.login(process.env.TOKEN).catch((err) => { diff --git a/lib.ts b/lib/files.ts similarity index 100% rename from lib.ts rename to lib/files.ts diff --git a/types.ts b/types/files.ts similarity index 100% rename from types.ts rename to types/files.ts From 5fd014b67711aa8b3a3edb4cd44cbd994a5b2832 Mon Sep 17 00:00:00 2001 From: grngxd <36968271+grngxd@users.noreply.github.com> Date: Thu, 31 Jul 2025 19:39:56 +0100 Subject: [PATCH 2/4] register cmd --- cmd/register.ts | 13 ++++++++----- globals.d.ts | 5 ++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/cmd/register.ts b/cmd/register.ts index a72d90c..6612a69 100644 --- a/cmd/register.ts +++ b/cmd/register.ts @@ -1,4 +1,4 @@ -import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js"; +import { ChatInputCommandInteraction, MessageFlags, SlashCommandBuilder } from "discord.js"; import { db } from "../db/db"; export const data = new SlashCommandBuilder() @@ -8,11 +8,14 @@ export const data = new SlashCommandBuilder() export const execute = async (interaction: ChatInputCommandInteraction) => { const existingUser = await db.get<{ id: string }>`SELECT id FROM users WHERE id = ${interaction.user.id}`; if (existingUser) { - await interaction.reply({ content: "You already have a stereo account.", ephemeral: true }); + await interaction.reply({ content: "you already have a stereo account", flags: MessageFlags.Ephemeral }); return; - } - - await interaction.reply({ content: "Your stereo account has been created!", ephemeral: true }); + } + + await interaction.reply({ + content: `visit [this link](${process.env.API + "/auth/login"}) to create your stereo account`, + flags: MessageFlags.Ephemeral + }) } export default [data, execute] as const; \ No newline at end of file diff --git a/globals.d.ts b/globals.d.ts index b9d593c..06cbe52 100644 --- a/globals.d.ts +++ b/globals.d.ts @@ -4,8 +4,11 @@ declare module "bun" { TOKEN: string; CLIENT_ID: string; - // stereo + // db DB_URL: string; DB_TYPE: "sqlite" | "postgres"; + + // web + API: string; } } \ No newline at end of file From a5457c6cc6a96b22e983501c9c60c127617df954 Mon Sep 17 00:00:00 2001 From: grngxd <36968271+grngxd@users.noreply.github.com> Date: Thu, 31 Jul 2025 20:44:22 +0100 Subject: [PATCH 3/4] feat: add list command to retrieve and paginate user files --- cmd/index.ts | 4 +- cmd/list.ts | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++ cmd/user.ts | 18 +++------ index.ts | 17 ++++++-- lib/b64.ts | 12 ++++++ lib/embed.ts | 12 ++++++ 6 files changed, 153 insertions(+), 17 deletions(-) create mode 100644 cmd/list.ts create mode 100644 lib/b64.ts create mode 100644 lib/embed.ts diff --git a/cmd/index.ts b/cmd/index.ts index a703f5d..27d9814 100644 --- a/cmd/index.ts +++ b/cmd/index.ts @@ -1,7 +1,9 @@ +import list from "./list"; import register from "./register"; import user from "./user"; export default [ user, - register + register, + list ] \ No newline at end of file diff --git a/cmd/list.ts b/cmd/list.ts new file mode 100644 index 0000000..619bb26 --- /dev/null +++ b/cmd/list.ts @@ -0,0 +1,107 @@ +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CacheType, ChatInputCommandInteraction, Events, Interaction, InteractionReplyOptions, MessageFlags, SlashCommandBuilder, User } from "discord.js"; +import { db } from "../db/db"; +import { b64decode, b64encode } from "../lib/b64"; +import { createEmbed } from "../lib/embed"; +import { formatSize } from "../lib/files"; +import { StereoFile } from "../types/files"; + +export const data = new SlashCommandBuilder() + .setName("list") + .setDescription("list all of your files") + .addIntegerOption(option => + option.setName("page") + .setDescription("page number to view") + .setRequired(false) + .setMinValue(1) + ); + +export async function renderList(user: User, userId: string, page: number): Promise { + const amount = 2; + + const { count } = await db.get<{ count: number }>` + SELECT COUNT(*) as count FROM files WHERE owner = ${userId} + `; + const totalPages = Math.max(1, Math.ceil(count / amount)); + const pages = Math.max(1, Math.min(page, totalPages)); + + const files = await db.all` + SELECT * FROM files WHERE owner = ${userId} + ORDER BY created_at DESC + LIMIT ${amount} OFFSET ${(pages - 1) * amount} + `; + + if (files.length === 0) { + return { content: "you havn't uploaded any files yet, visit the website to get started!", flags: MessageFlags.Ephemeral }; + } + + const embed = createEmbed(user) + .setTitle("your files") + .addFields(...files.map(file => ({ + name: file.name, + value: `${formatSize(file.size)}\n${new Date(file.created_at).toLocaleString()}\n[open in browser](${process.env.API}/${file.id})`, + inline: true + }))) + .setFooter({ text: `page ${pages} of ${totalPages}` }); + + const row = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId(b64encode({ dir: "prev", user: userId, page: pages })) + .setLabel('<') + .setStyle(ButtonStyle.Danger) + .setDisabled(pages === 1), + new ButtonBuilder() + .setCustomId(b64encode({ dir: "next", user: userId, page: pages })) + .setLabel('>') + .setStyle(ButtonStyle.Danger) + .setDisabled(pages === totalPages) + ); + + return { embeds: [embed], components: [row] }; +} + +export const execute = async (interaction: ChatInputCommandInteraction) => { + const page = interaction.options.getInteger("page") || 1; + const userId = interaction.user.id; + const reply = await renderList(interaction.user, userId, page); + await interaction.reply(reply); +}; + +export const on: { + [event in Events]?: (interaction: Interaction) => Promise; +} = { + [Events.InteractionCreate]: async (interaction: Interaction) => { + if (!interaction.isButton()) return; + // old, cursed code :vomit: + + // const match = interaction.customId.match(/^s\.list\.(prev|next)\.(\d+)$/); + // if (!match) return; + + // let page = parseInt(match[2] || "1", 10); + // page = match[1] === "next" ? page + 1 : page - 1; + + // const dirtyReply = await renderList(interaction.user, interaction.user.id, page); + // const { flags, ...reply } = dirtyReply; + // await interaction.update({ ...reply }); + + // new, clean code :sunglasses: + + const data = b64decode<{ + dir: "next" | "prev"; + user: string; + page: number; + }>(interaction.customId); + + if (interaction.user.id !== data.user) { + await interaction.reply({ content: "hey, stop that!!!", flags: MessageFlags.Ephemeral }); + return; + } + + const page = data.dir === "next" ? data.page + 1 : data.page - 1; + const dirty = await renderList(interaction.user, interaction.user.id, page); + const { flags, ...r } = dirty; + await interaction.update({ ...r }); + } +} + +export default [data, execute, on] as const; \ No newline at end of file diff --git a/cmd/user.ts b/cmd/user.ts index 20f0a63..8c747d5 100644 --- a/cmd/user.ts +++ b/cmd/user.ts @@ -1,6 +1,7 @@ // commands/overview.ts -import { ChatInputCommandInteraction, EmbedBuilder, MessageFlags, SlashCommandBuilder } from "discord.js"; +import { ChatInputCommandInteraction, MessageFlags, SlashCommandBuilder } from "discord.js"; import { db } from "../db/db"; +import { createEmbed } from "../lib/embed"; import { formatSize } from "../lib/files"; import { StereoFile } from "../types/files"; @@ -40,20 +41,13 @@ export const execute = async (interaction: ChatInputCommandInteraction) => { const files = await db.all`SELECT * FROM files WHERE owner = ${user.id}`; const totalSize = files.reduce((a, b) => a + b.size, 0); - const embed = new EmbedBuilder() - .setColor(0xff264e) - .setAuthor({ - name: `${user.globalName || "user"} on stereo`, - iconURL: user.avatarURL({ size: 512 }) || "" - }) + const embed = createEmbed(user) .setDescription("Here's your overview:") .addFields( - { name: "Uploads", value: `${files.length} files`, inline: true }, - { name: "Uploaded", value: `${formatSize(totalSize)} / 15 GB`, inline: true }, - { name: "Plan", value: `Free`, inline: true } + { name: "Uploads", value: `${files.length} files`, inline: true }, + { name: "Uploaded", value: `${formatSize(totalSize)} / 15 GB`, inline: true }, + { name: "Plan", value: `Free`, inline: true } ) - .setFooter({ text: "powered by stereo" }) - .setTimestamp(); await interaction.reply({ embeds: [embed] }); } diff --git a/index.ts b/index.ts index d66a3df..8145569 100644 --- a/index.ts +++ b/index.ts @@ -3,7 +3,7 @@ import commands from "./cmd"; import { db } from "./db/db"; import { StereoFile } from "./types/files"; -const client = new Client({ +const bot = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, @@ -14,7 +14,7 @@ const client = new Client({ const rest = new REST({ version: "10" }) .setToken(process.env.TOKEN); -client.once(Events.ClientReady, async (c) => { +bot.once(Events.ClientReady, async (c) => { await rest.put( Routes.applicationCommands(process.env.CLIENT_ID), { body: commands.map(([data, _]) => data) } @@ -29,7 +29,7 @@ client.once(Events.ClientReady, async (c) => { console.log(c.user.tag); }); -client.on(Events.InteractionCreate, async interaction => { +bot.on(Events.InteractionCreate, async interaction => { if (!interaction.isChatInputCommand()) return; const cmd = commands.find(([data]) => data.name === interaction.commandName); if (!cmd) return; @@ -42,7 +42,16 @@ client.on(Events.InteractionCreate, async interaction => { } }); -client.login(process.env.TOKEN).catch((err) => { +for (const [_, __, on] of commands) { + if (!on) continue; + + for (const [event, handler] of Object.entries(on)) { + if (!Object.values(Events).includes(event as Events)) { console.warn(`Unknown event: ${event}`); continue; } + bot.on(event, handler); + } +} + +bot.login(process.env.TOKEN).catch((err) => { console.error("Failed to login:", err) process.exit(1) }); \ No newline at end of file diff --git a/lib/b64.ts b/lib/b64.ts new file mode 100644 index 0000000..485cff1 --- /dev/null +++ b/lib/b64.ts @@ -0,0 +1,12 @@ +// i actually love this +export const b64encode = (obj: object): string => { + return Buffer.from(JSON.stringify(obj)).toString("base64url"); +} + +export const b64decode = (str: string): T => { + try { + return JSON.parse(Buffer.from(str, "base64url").toString()); + } catch { + throw new Error("invalid base64-encoded object string: " + str); + } +} \ No newline at end of file diff --git a/lib/embed.ts b/lib/embed.ts new file mode 100644 index 0000000..3abbe00 --- /dev/null +++ b/lib/embed.ts @@ -0,0 +1,12 @@ +import { EmbedBuilder, User } from "discord.js"; + +export const createEmbed = (user: User) => { + return new EmbedBuilder() + .setColor(0xff264e) + .setAuthor({ + name: `${user.globalName || "user"} on stereo`, + iconURL: user.avatarURL({ size: 512 }) || "" + }) + .setFooter({ text: "powered by stereo" }) + .setTimestamp() +} \ No newline at end of file From b6e24c517bb6f62c0635920bef903d5c29952750 Mon Sep 17 00:00:00 2001 From: grngxd <36968271+grngxd@users.noreply.github.com> Date: Thu, 31 Jul 2025 21:04:01 +0100 Subject: [PATCH 4/4] use messagepack to make it even smaller!!!! --- bun.lock | 14 ++++++++++++++ cmd/list.ts | 12 ++++++------ lib/b64.ts | 6 ++++-- package.json | 6 ++++-- 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/bun.lock b/bun.lock index 829da38..e4c11a1 100644 --- a/bun.lock +++ b/bun.lock @@ -6,9 +6,11 @@ "dependencies": { "discord.js": "^14.21.0", "ky": "^1.8.2", + "msgpack-lite": "^0.1.26", }, "devDependencies": { "@types/bun": "latest", + "@types/msgpack-lite": "^0.1.11", }, "peerDependencies": { "typescript": "^5", @@ -36,6 +38,8 @@ "@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="], + "@types/msgpack-lite": ["@types/msgpack-lite@0.1.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-cdCZS/gw+jIN22I4SUZUFf1ZZfVv5JM1//Br/MuZcI373sxiy3eSSoiyLu0oz+BPatTbGGGBO5jrcvd0siCdTQ=="], + "@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="], "@types/react": ["@types/react@19.1.9", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA=="], @@ -52,8 +56,16 @@ "discord.js": ["discord.js@14.21.0", "", { "dependencies": { "@discordjs/builders": "^1.11.2", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.1", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.1", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.1", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-U5w41cEmcnSfwKYlLv5RJjB8Joa+QJyRwIJz5i/eg+v2Qvv6EYpCRhN9I2Rlf0900LuqSDg8edakUATrDZQncQ=="], + "event-lite": ["event-lite@0.1.3", "", {}, "sha512-8qz9nOz5VeD2z96elrEKD2U433+L3DWdUdDkOINLGOJvx1GsMBbMn0aCeu28y8/e85A6mCigBiFlYMnTBEGlSw=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "int64-buffer": ["int64-buffer@0.1.10", "", {}, "sha512-v7cSY1J8ydZ0GyjUHqF+1bshJ6cnEVLo9EnjB8p+4HDRPZc9N5jjmvUV7NvEsqQOKyH0pmIBFWXVQbiS0+OBbA=="], + + "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + "ky": ["ky@1.8.2", "", {}, "sha512-XybQJ3d4Ea1kI27DoelE5ZCT3bSJlibYTtQuMsyzKox3TMyayw1asgQdl54WroAm+fIA3ZCr8zXW2RpR7qWVpA=="], "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], @@ -62,6 +74,8 @@ "magic-bytes.js": ["magic-bytes.js@1.12.1", "", {}, "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA=="], + "msgpack-lite": ["msgpack-lite@0.1.26", "", { "dependencies": { "event-lite": "^0.1.1", "ieee754": "^1.1.8", "int64-buffer": "^0.1.9", "isarray": "^1.0.0" }, "bin": { "msgpack": "./bin/msgpack" } }, "sha512-SZ2IxeqZ1oRFGo0xFGbvBJWMp3yLIY9rlIJyxy8CGrwZn1f0ZK4r6jV/AM1r0FZMDUkWkglOk/eeKIL9g77Nxw=="], + "ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], diff --git a/cmd/list.ts b/cmd/list.ts index 619bb26..466ee92 100644 --- a/cmd/list.ts +++ b/cmd/list.ts @@ -46,12 +46,12 @@ export async function renderList(user: User, userId: string, page: number): Prom const row = new ActionRowBuilder() .addComponents( new ButtonBuilder() - .setCustomId(b64encode({ dir: "prev", user: userId, page: pages })) + .setCustomId(b64encode({ direction: -1, user: userId, pages: pages })) .setLabel('<') .setStyle(ButtonStyle.Danger) .setDisabled(pages === 1), new ButtonBuilder() - .setCustomId(b64encode({ dir: "next", user: userId, page: pages })) + .setCustomId(b64encode({ direction: 1, user: userId, pages: pages })) .setLabel('>') .setStyle(ButtonStyle.Danger) .setDisabled(pages === totalPages) @@ -84,12 +84,12 @@ export const on: { // const { flags, ...reply } = dirtyReply; // await interaction.update({ ...reply }); - // new, clean code :sunglasses: + // new, clean code :sunglasses: (minified since discord limits ids to 100 chars) const data = b64decode<{ - dir: "next" | "prev"; + direction: -1 | 1; user: string; - page: number; + pages: number; }>(interaction.customId); if (interaction.user.id !== data.user) { @@ -97,7 +97,7 @@ export const on: { return; } - const page = data.dir === "next" ? data.page + 1 : data.page - 1; + const page = data.pages + data.direction; const dirty = await renderList(interaction.user, interaction.user.id, page); const { flags, ...r } = dirty; await interaction.update({ ...r }); diff --git a/lib/b64.ts b/lib/b64.ts index 485cff1..7f73ead 100644 --- a/lib/b64.ts +++ b/lib/b64.ts @@ -1,11 +1,13 @@ +import m from "msgpack-lite"; + // i actually love this export const b64encode = (obj: object): string => { - return Buffer.from(JSON.stringify(obj)).toString("base64url"); + return Buffer.from(m.encode(obj)).toString("base64url"); } export const b64decode = (str: string): T => { try { - return JSON.parse(Buffer.from(str, "base64url").toString()); + return m.decode(Buffer.from(str, "base64url")); } catch { throw new Error("invalid base64-encoded object string: " + str); } diff --git a/package.json b/package.json index 00a538b..76dd15c 100644 --- a/package.json +++ b/package.json @@ -4,13 +4,15 @@ "type": "module", "private": true, "devDependencies": { - "@types/bun": "latest" + "@types/bun": "latest", + "@types/msgpack-lite": "^0.1.11" }, "peerDependencies": { "typescript": "^5" }, "dependencies": { "discord.js": "^14.21.0", - "ky": "^1.8.2" + "ky": "^1.8.2", + "msgpack-lite": "^0.1.26" } }