feat: add list command to retrieve and paginate user files

This commit is contained in:
grngxd 2025-07-31 20:44:22 +01:00
parent 5fd014b677
commit a5457c6cc6
6 changed files with 153 additions and 17 deletions

View file

@ -1,7 +1,9 @@
import list from "./list";
import register from "./register";
import user from "./user";
export default [
user,
register
register,
list
]

107
cmd/list.ts Normal file
View file

@ -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<InteractionReplyOptions> {
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<StereoFile[]>`
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<ButtonBuilder>()
.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<CacheType>) => Promise<void>;
} = {
[Events.InteractionCreate]: async (interaction: Interaction<CacheType>) => {
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;

View file

@ -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<StereoFile[]>`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] });
}