feat: add list command to retrieve and paginate user files
This commit is contained in:
parent
5fd014b677
commit
a5457c6cc6
6 changed files with 153 additions and 17 deletions
|
@ -1,7 +1,9 @@
|
||||||
|
import list from "./list";
|
||||||
import register from "./register";
|
import register from "./register";
|
||||||
import user from "./user";
|
import user from "./user";
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
user,
|
user,
|
||||||
register
|
register,
|
||||||
|
list
|
||||||
]
|
]
|
107
cmd/list.ts
Normal file
107
cmd/list.ts
Normal 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;
|
18
cmd/user.ts
18
cmd/user.ts
|
@ -1,6 +1,7 @@
|
||||||
// commands/overview.ts
|
// commands/overview.ts
|
||||||
import { ChatInputCommandInteraction, EmbedBuilder, MessageFlags, SlashCommandBuilder } from "discord.js";
|
import { ChatInputCommandInteraction, MessageFlags, SlashCommandBuilder } from "discord.js";
|
||||||
import { db } from "../db/db";
|
import { db } from "../db/db";
|
||||||
|
import { createEmbed } from "../lib/embed";
|
||||||
import { formatSize } from "../lib/files";
|
import { formatSize } from "../lib/files";
|
||||||
import { StereoFile } from "../types/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 files = await db.all<StereoFile[]>`SELECT * FROM files WHERE owner = ${user.id}`;
|
||||||
const totalSize = files.reduce((a, b) => a + b.size, 0);
|
const totalSize = files.reduce((a, b) => a + b.size, 0);
|
||||||
|
|
||||||
const embed = new EmbedBuilder()
|
const embed = createEmbed(user)
|
||||||
.setColor(0xff264e)
|
|
||||||
.setAuthor({
|
|
||||||
name: `${user.globalName || "user"} on stereo`,
|
|
||||||
iconURL: user.avatarURL({ size: 512 }) || ""
|
|
||||||
})
|
|
||||||
.setDescription("Here's your overview:")
|
.setDescription("Here's your overview:")
|
||||||
.addFields(
|
.addFields(
|
||||||
{ name: "Uploads", value: `${files.length} files`, inline: true },
|
{ name: "Uploads", value: `${files.length} files`, inline: true },
|
||||||
{ name: "Uploaded", value: `${formatSize(totalSize)} / 15 GB`, inline: true },
|
{ name: "Uploaded", value: `${formatSize(totalSize)} / 15 GB`, inline: true },
|
||||||
{ name: "Plan", value: `Free`, inline: true }
|
{ name: "Plan", value: `Free`, inline: true }
|
||||||
)
|
)
|
||||||
.setFooter({ text: "powered by stereo" })
|
|
||||||
.setTimestamp();
|
|
||||||
|
|
||||||
await interaction.reply({ embeds: [embed] });
|
await interaction.reply({ embeds: [embed] });
|
||||||
}
|
}
|
||||||
|
|
17
index.ts
17
index.ts
|
@ -3,7 +3,7 @@ import commands from "./cmd";
|
||||||
import { db } from "./db/db";
|
import { db } from "./db/db";
|
||||||
import { StereoFile } from "./types/files";
|
import { StereoFile } from "./types/files";
|
||||||
|
|
||||||
const client = new Client({
|
const bot = new Client({
|
||||||
intents: [
|
intents: [
|
||||||
GatewayIntentBits.Guilds,
|
GatewayIntentBits.Guilds,
|
||||||
GatewayIntentBits.GuildMessages,
|
GatewayIntentBits.GuildMessages,
|
||||||
|
@ -14,7 +14,7 @@ const client = new Client({
|
||||||
const rest = new REST({ version: "10" })
|
const rest = new REST({ version: "10" })
|
||||||
.setToken(process.env.TOKEN);
|
.setToken(process.env.TOKEN);
|
||||||
|
|
||||||
client.once(Events.ClientReady, async (c) => {
|
bot.once(Events.ClientReady, async (c) => {
|
||||||
await rest.put(
|
await rest.put(
|
||||||
Routes.applicationCommands(process.env.CLIENT_ID),
|
Routes.applicationCommands(process.env.CLIENT_ID),
|
||||||
{ body: commands.map(([data, _]) => data) }
|
{ body: commands.map(([data, _]) => data) }
|
||||||
|
@ -29,7 +29,7 @@ client.once(Events.ClientReady, async (c) => {
|
||||||
console.log(c.user.tag);
|
console.log(c.user.tag);
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on(Events.InteractionCreate, async interaction => {
|
bot.on(Events.InteractionCreate, async interaction => {
|
||||||
if (!interaction.isChatInputCommand()) return;
|
if (!interaction.isChatInputCommand()) return;
|
||||||
const cmd = commands.find(([data]) => data.name === interaction.commandName);
|
const cmd = commands.find(([data]) => data.name === interaction.commandName);
|
||||||
if (!cmd) return;
|
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)
|
console.error("Failed to login:", err)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
});
|
});
|
12
lib/b64.ts
Normal file
12
lib/b64.ts
Normal file
|
@ -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 = <T>(str: string): T => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(Buffer.from(str, "base64url").toString());
|
||||||
|
} catch {
|
||||||
|
throw new Error("invalid base64-encoded object string: " + str);
|
||||||
|
}
|
||||||
|
}
|
12
lib/embed.ts
Normal file
12
lib/embed.ts
Normal file
|
@ -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()
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue