From b21a7d666697dc608206c22980daa93c8b419216 Mon Sep 17 00:00:00 2001 From: Cody <36463298+ugcodrr@users.noreply.github.com> Date: Sun, 1 Nov 2026 20:55:59 -0500 Subject: [PATCH 1/7] feat(discord-bot): keep guild top and rankings buttons working after restarts --- .../src/commands/base.hypixel-command.ts | 15 +- .../commands/guild/guild-top.subcommand.tsx | 163 ++++++------------ .../src/commands/guild/guild.route.ts | 17 ++ .../commands/rankings/rankings.command.tsx | 14 +- apps/discord-bot/src/lib/command.listener.ts | 2 + packages/api-client/src/api.service.ts | 3 +- .../discord/src/command/command.context.ts | 6 + packages/discord/src/services/api.service.ts | 7 +- .../discord/src/services/paginate.service.ts | 35 +++- 9 files changed, 127 insertions(+), 135 deletions(-) create mode 100644 apps/discord-bot/src/commands/guild/guild.route.ts diff --git a/apps/discord-bot/src/commands/base.hypixel-command.ts b/apps/discord-bot/src/commands/base.hypixel-command.ts index 083edbecc..d8cf95657 100644 --- a/apps/discord-bot/src/commands/base.hypixel-command.ts +++ b/apps/discord-bot/src/commands/base.hypixel-command.ts @@ -20,7 +20,6 @@ import { SubPage, createCustomId, } from "@statsify/discord"; -import { CacheLevel } from "@statsify/api-client"; import { Container } from "typedi"; import { GamesWithBackgrounds, mapBackground } from "#constants"; import { HistoricalTimeData } from "#components"; @@ -51,12 +50,15 @@ export interface ProfileData { export type ModeEmoji = LocalizationString | false | undefined; -/** Page buttons re-run the command: `p::::` */ +/** + * Page buttons on player commands re-run the command for the same player: + * `p::::` + */ export const PROFILE_ROUTE = "p"; export function profileRoute(listener: AbstractCommandListener): ComponentRoute { return (interaction, { ownerId, values: [command, uuid] }) => - listener.rerunCommand(interaction, ownerId, [command], { player: uuid }); + listener.rerunCommand(interaction, ownerId, command.split("."), { player: uuid }); } export interface BaseHypixelCommand { @@ -83,12 +85,7 @@ export abstract class BaseHypixelCommand; type GuildTopKey = "daily" | "weekly" | "monthly" | number; - -interface GuildTopPageState { - modeIndex: number; - page: number; -} +type GuildTopMode = [key: GuildTopKey, title: string]; export class GuildTopSubCommand extends GuildLeaderboardSubCommand { @SubCommand({ description: (t) => t("commands.guild-top"), args: GuildArgument }) public async top(context: CommandContext) { - const userId = context.getInteraction().getUserId(); + const interaction = context.getInteraction(); const user = context.getUser(); const t = context.t(); @@ -61,12 +59,7 @@ export class GuildTopSubCommand extends GuildLeaderboardSubCommand { t, }; - const theme = getTheme(user); - - const up = new ButtonBuilder().emoji(t("emojis:up")).style(ButtonStyle.Success); - const down = new ButtonBuilder().emoji(t("emojis:down")).style(ButtonStyle.Danger); - - const modes: [GuildTopKey, string][] = [ + const modes: GuildTopMode[] = [ ["daily", "Today"], ["weekly", "This Week"], ["monthly", "This Month"], @@ -78,98 +71,50 @@ export class GuildTopSubCommand extends GuildLeaderboardSubCommand { [6, "6 days ago"], ]; - const dropdown = new SelectMenuBuilder(); + const pageCount = Math.ceil(guild.members.length / GUILD_TOP_PAGE_SIZE); + const [modeIndex, page] = this.getRequestedPage(interaction, modes, pageCount); + const [key, title] = modes[modeIndex]; + + const customId = (...values: string[]) => + createCustomId(GUILD_ROUTE, interaction.getUserId(), context.getCommandName(), guild.id, ...values); + + const dropdown = new SelectMenuBuilder().customId(customId("menu", `${page}`)); for (const [index, [key, title]] of modes.entries()) { dropdown.option( new SelectMenuOptionBuilder() .label(title) .value(`${key}`) - .default(index === 0) + .default(index === modeIndex) ); } const components = [new ActionRowBuilder().component(dropdown)]; - const pageCount = Math.ceil(guild.members.length / GUILD_TOP_PAGE_SIZE); if (pageCount > 1) - components.push(new ActionRowBuilder().component(up).component(down)); - - let page = 0; - let modeIndex = 0; - - const cache = new Map(); - const listener = CommandListener.getInstance(); - - const changePage = - (fn: (interaction: Interaction) => Partial) => - async (interaction: Interaction) => { - if (user?.locale) interaction.setLocale(user.locale); - - const state = fn(interaction); - - const mode = modes[state.modeIndex ?? modeIndex]; - - const message = await this.getGuildTopPageMessage( - cache, - components, - props, - mode[1], - mode[0], - state.page ?? page, - theme - ); - - if (interaction.getUserId() === userId) { - if (state.page !== undefined) page = state.page; - if (state.modeIndex !== undefined) modeIndex = state.modeIndex; - - dropdown.activeOption(modeIndex); - - context.reply(message); - } else { - interaction.sendFollowup({ ...message, components: [], ephemeral: true }); - } - }; - - listener.addHook( - up.getCustomId(), - changePage(() => ({ page: page - 1 < 0 ? pageCount - 1 : page - 1 })) - ); - - listener.addHook( - down.getCustomId(), - changePage(() => ({ page: page + 1 > pageCount - 1 ? 0 : page + 1 })) - ); - - listener.addHook( - dropdown.getCustomId(), - changePage((interaction) => { - const value = interaction.getData().values[0]; - return { modeIndex: modes.findIndex(([key]) => `${key}` === value) }; - }) - ); - - setTimeout(() => { - context.reply({ components: [] }); - cache.clear(); + components.push( + new ActionRowBuilder() + .component( + new ButtonBuilder() + .emoji(t("emojis:up")) + .style(ButtonStyle.Success) + .customId(customId(`${key}`, `prev.${page}`)) + ) + .component( + new ButtonBuilder() + .emoji(t("emojis:down")) + .style(ButtonStyle.Danger) + .customId(customId(`${key}`, `next.${page}`)) + ) + ); - listener.removeHook(up.getCustomId()); - listener.removeHook(down.getCustomId()); - listener.removeHook(dropdown.getCustomId()); - }, 300_000); + const image = await this.generateGuildTopPage(props, title, key, page, getTheme(user)); - const message = await this.getGuildTopPageMessage( - cache, + return { + files: [{ name: "guild-top.png", data: image, type: "image/png" }], + attachments: [], components, - props, - modes[modeIndex][1], - modes[modeIndex][0], - page, - theme - ); - - return message; + }; } protected getGuild(context: CommandContext) { @@ -177,34 +122,24 @@ export class GuildTopSubCommand extends GuildLeaderboardSubCommand { const query = context.option("query"); const type = context.option("type"); - return this.apiService.getGuild(query, type, user); + return this.apiService.getGuild(query, type, user, context.getCacheLevel()); } - private async getGuildTopPageMessage( - cache: Map, - components: ActionRowBuilder[], - props: BaseGuildTopProfileProps, - title: string, - key: GuildTopKey, - page: number, - theme?: Theme - ): Promise { - const cacheKey = `${key}-${page}`; + /** Today's first page for the command, otherwise the mode and page a click asks for */ + private getRequestedPage( + interaction: Interaction, + modes: GuildTopMode[], + pageCount: number + ): [modeIndex: number, page: number] { + if (!interaction.isMessageComponentInteraction()) return [0, 0]; - let image: Buffer; + const [, , key, target] = parseCustomId(interaction.getCustomId()).values; + const findMode = (key: string) => modes.findIndex(([mode]) => `${mode}` === key); - if (cache.has(cacheKey)) { - image = cache.get(cacheKey)!; - } else { - image = await this.generateGuildTopPage(props, title, key, page, theme); - cache.set(cacheKey, image); - } + if (interaction.isSelectMenuInteraction()) + return [findMode(interaction.getData().values[0]), Number(target)]; - return { - files: [{ name: "guild-top.png", data: image, type: "image/png" }], - attachments: [], - components, - }; + return [findMode(key), getScrolledPage(target, pageCount)]; } private async generateGuildTopPage( diff --git a/apps/discord-bot/src/commands/guild/guild.route.ts b/apps/discord-bot/src/commands/guild/guild.route.ts new file mode 100644 index 000000000..ed00832a8 --- /dev/null +++ b/apps/discord-bot/src/commands/guild/guild.route.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) Statsify + * + * This source code is licensed under the GNU GPL v3 license found in the + * LICENSE file in the root directory of this source tree. + * https://github.com/Statsify/statsify/blob/main/LICENSE + */ + +import type { AbstractCommandListener, ComponentRoute } from "@statsify/discord"; + +/** Buttons on /guild re-run the subcommand for the same guild: `g::::...` */ +export const GUILD_ROUTE = "g"; + +export function guildRoute(listener: AbstractCommandListener): ComponentRoute { + return (interaction, { ownerId, values: [subcommand, guildId] }) => + listener.rerunCommand(interaction, ownerId, ["guild", subcommand], { query: guildId }); +} diff --git a/apps/discord-bot/src/commands/rankings/rankings.command.tsx b/apps/discord-bot/src/commands/rankings/rankings.command.tsx index 15966236e..ae2061b10 100644 --- a/apps/discord-bot/src/commands/rankings/rankings.command.tsx +++ b/apps/discord-bot/src/commands/rankings/rankings.command.tsx @@ -50,9 +50,11 @@ import { PlayerArgument, SubCommand, type SubCommandOptions, + createCustomId, } from "@statsify/discord"; import { ButtonStyle } from "discord-api-types/v10"; import { type GamesWithBackgrounds, mapBackground } from "#constants"; +import { PROFILE_ROUTE } from "#commands/base.hypixel-command"; import { RankingsProfile } from "./rankings.profile.js"; import { arrayGroup } from "@statsify/util"; import { games } from "./games.js"; @@ -309,7 +311,7 @@ export class RankingsCommand { const user = context.getUser(); const t = context.t(); - const player = await this.apiService.getPlayer(context.option("player"), user); + const player = await this.apiService.getPlayer(context.option("player"), user, context.getCacheLevel()); const isGameNotAll = game !== "all"; @@ -365,7 +367,15 @@ export class RankingsCommand { ), new ButtonBuilder().emoji(t("emojis:up")).style(ButtonStyle.Success), new ButtonBuilder().emoji(t("emojis:down")).style(ButtonStyle.Danger), - true + true, + (page) => + createCustomId( + PROFILE_ROUTE, + context.getInteraction().getUserId(), + `rankings.${context.getCommandName()}`, + player.uuid, + page + ) ); } } diff --git a/apps/discord-bot/src/lib/command.listener.ts b/apps/discord-bot/src/lib/command.listener.ts index 7b9fbe789..a3dc64005 100644 --- a/apps/discord-bot/src/lib/command.listener.ts +++ b/apps/discord-bot/src/lib/command.listener.ts @@ -17,6 +17,7 @@ import { Interaction, } from "@statsify/discord"; import { Container } from "typedi"; +import { GUILD_ROUTE, guildRoute } from "#commands/guild/guild.route"; import { LEADERBOARD_ROUTE, leaderboardRoute } from "#commands/leaderboards/leaderboard.route"; import { PROFILE_ROUTE, profileRoute } from "#commands/base.hypixel-command"; import { STATUS_COLORS } from "@statsify/logger"; @@ -54,6 +55,7 @@ export class CommandListener extends AbstractCommandListener { this.addRoute(PROFILE_ROUTE, profileRoute(this)); this.addRoute(LEADERBOARD_ROUTE, leaderboardRoute(this)); + this.addRoute(GUILD_ROUTE, guildRoute(this)); } protected async onCommand(interaction: Interaction): Promise { diff --git a/packages/api-client/src/api.service.ts b/packages/api-client/src/api.service.ts index f43108367..c6582052c 100644 --- a/packages/api-client/src/api.service.ts +++ b/packages/api-client/src/api.service.ts @@ -113,10 +113,11 @@ export class ApiService { ); } - public getGuild(tag: string, type: GuildQuery) { + public getCachedGuild(tag: string, type: GuildQuery, cache: CacheLevel) { return this.requestKey("/guild", "guild", { guild: tag, type, + cache, }); } diff --git a/packages/discord/src/command/command.context.ts b/packages/discord/src/command/command.context.ts index 9f5ee927e..4fe20b078 100644 --- a/packages/discord/src/command/command.context.ts +++ b/packages/discord/src/command/command.context.ts @@ -10,6 +10,7 @@ import { type APIApplicationCommandInteractionDataBasicOption, ApplicationCommandOptionType, } from "discord-api-types/v10"; +import { CacheLevel } from "@statsify/api-client"; import { User } from "@statsify/schemas"; import { noop } from "@statsify/util"; import type { AbstractCommandListener } from "./abstract-command.listener.js"; @@ -38,6 +39,11 @@ export class CommandContext { return this.data.name; } + /** A button re-runs its command, so a click reads cached stats and never asks Hypixel */ + public getCacheLevel() { + return this.interaction.isCommandInteraction() ? undefined : CacheLevel.CACHE_ONLY; + } + public setUser(user: User | null) { this.user = user; if (user?.locale) this.interaction.setLocale(user?.locale); diff --git a/packages/discord/src/services/api.service.ts b/packages/discord/src/services/api.service.ts index 0f8892d95..d431a3335 100644 --- a/packages/discord/src/services/api.service.ts +++ b/packages/discord/src/services/api.service.ts @@ -125,10 +125,11 @@ export class ApiService extends StatsifyApiService { }); } - public override async getGuild( + public async getGuild( tag: string, type?: GuildQuery, - user: User | null = null + user: User | null = null, + cache = CacheLevel.CACHE ) { let input: string; let playerType: PlayerTag; @@ -155,7 +156,7 @@ export class ApiService extends StatsifyApiService { input = tag; } - return super.getGuild(input, resolvedType).catch((err) => { + return super.getCachedGuild(input, resolvedType, cache).catch((err) => { if (!err.response || !err.response.data) throw this.unknownError(); const error = err.response.data as GuildNotFoundException | PlayerNotFoundException; diff --git a/packages/discord/src/services/paginate.service.ts b/packages/discord/src/services/paginate.service.ts index 5fa6d8d91..1bd8574c3 100644 --- a/packages/discord/src/services/paginate.service.ts +++ b/packages/discord/src/services/paginate.service.ts @@ -199,8 +199,7 @@ export async function paginate( * @param forwardButton The button to use for forward pagination * @param backwardButton The button to use for back pagination * @param invertButtons Whether to invert the buttons (backward becomes forward, forward becomes backward) - * @param startingIndex The starting page - * @param timeout When to stop the pagination (ms), defaults to 300000 + * @param customId Makes the buttons keep working after a restart, as in paginate */ export async function scrollingPagination( context: CommandContext, @@ -208,10 +207,9 @@ export async function scrollingPagination( forwardButton?: ButtonBuilder, backwardButton?: ButtonBuilder, invertButtons = false, - startingIndex = 0, - timeout = 300_000, + customId?: (page: string) => string, ) { - let currentIndex = startingIndex; + let currentIndex = 0; const cache = new Map(); const t = context.t(); @@ -236,6 +234,22 @@ export async function scrollingPagination( if (invertButtons) controller.reverse(); + if (customId) { + const interaction = context.getInteraction(); + + const index = interaction.isMessageComponentInteraction() ? + getScrolledPage(parseCustomId(interaction.getCustomId()).values.at(-1)!, pages.length) : + 0; + + controller[0].customId(customId(`prev.${index}`)); + controller[1].customId(customId(`next.${index}`)); + + const message = await getMessage(index); + message.components = [new ActionRowBuilder(controller)]; + + return message; + } + const listener = context.getListener(); for (const [index, component] of controller.entries()) { @@ -276,7 +290,7 @@ export async function scrollingPagination( cache.clear(); } - setTimeout(onTimeout, timeout); + setTimeout(onTimeout, 300_000); const message = await getMessage(currentIndex); message.components = [new ActionRowBuilder(controller)]; @@ -284,6 +298,15 @@ export async function scrollingPagination( return message; } +/** + * The page a scroll button asks for, wrapping around. Its custom id ends with `prev.` or + * `next.` for the page it's on, since with two pages both buttons would ask for the same one + */ +export function getScrolledPage(target: string, pageCount: number) { + const [direction, page] = target.split("."); + return (Number(page) + (direction === "prev" ? pageCount - 1 : 1)) % pageCount; +} + /** The page a click asks for: the select menu's value, or the end of the button's custom id */ function getRequestedPage(interaction: Interaction): [index: number, subIndex: number] { if (!interaction.isMessageComponentInteraction()) return [0, 0]; From 3b5ea3c6102e32990787fa6ecbcacd7385f29d1e Mon Sep 17 00:00:00 2001 From: Cody <36463298+ugcodrr@users.noreply.github.com> Date: Sun, 1 Nov 2026 22:08:50 -0500 Subject: [PATCH 2/7] refactor(discord-bot): pass guild top keys and pages to createCustomId as they are --- .../src/commands/guild/guild-top.subcommand.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/discord-bot/src/commands/guild/guild-top.subcommand.tsx b/apps/discord-bot/src/commands/guild/guild-top.subcommand.tsx index 892756fdf..dc4523b3a 100644 --- a/apps/discord-bot/src/commands/guild/guild-top.subcommand.tsx +++ b/apps/discord-bot/src/commands/guild/guild-top.subcommand.tsx @@ -75,10 +75,10 @@ export class GuildTopSubCommand extends GuildLeaderboardSubCommand { const [modeIndex, page] = this.getRequestedPage(interaction, modes, pageCount); const [key, title] = modes[modeIndex]; - const customId = (...values: string[]) => + const customId = (...values: (string | number)[]) => createCustomId(GUILD_ROUTE, interaction.getUserId(), context.getCommandName(), guild.id, ...values); - const dropdown = new SelectMenuBuilder().customId(customId("menu", `${page}`)); + const dropdown = new SelectMenuBuilder().customId(customId("menu", page)); for (const [index, [key, title]] of modes.entries()) { dropdown.option( @@ -98,13 +98,13 @@ export class GuildTopSubCommand extends GuildLeaderboardSubCommand { new ButtonBuilder() .emoji(t("emojis:up")) .style(ButtonStyle.Success) - .customId(customId(`${key}`, `prev.${page}`)) + .customId(customId(key, `prev.${page}`)) ) .component( new ButtonBuilder() .emoji(t("emojis:down")) .style(ButtonStyle.Danger) - .customId(customId(`${key}`, `next.${page}`)) + .customId(customId(key, `next.${page}`)) ) ); From a46cb05c12cba631991b7a197525ea79a7a85100 Mon Sep 17 00:00:00 2001 From: Cody <36463298+ugcodrr@users.noreply.github.com> Date: Sun, 1 Nov 2026 22:09:00 -0500 Subject: [PATCH 3/7] refactor(discord): read the user id after scrollingPagination's custom id return --- packages/discord/src/services/paginate.service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/discord/src/services/paginate.service.ts b/packages/discord/src/services/paginate.service.ts index 1bd8574c3..e76dd13e3 100644 --- a/packages/discord/src/services/paginate.service.ts +++ b/packages/discord/src/services/paginate.service.ts @@ -225,8 +225,6 @@ export async function scrollingPagination( if (pages.length === 1) return getMessage(currentIndex); - const userId = context.getInteraction().getUserId(); - const controller = [ backwardButton ?? new ButtonBuilder().emoji(t("emojis:backward")), forwardButton ?? new ButtonBuilder().emoji(t("emojis:forward")), @@ -250,6 +248,7 @@ export async function scrollingPagination( return message; } + const userId = context.getInteraction().getUserId(); const listener = context.getListener(); for (const [index, component] of controller.entries()) { From d79e7895333a5fc1054c52a38322b9ca848bf6d9 Mon Sep 17 00:00:00 2001 From: Cody <36463298+ugcodrr@users.noreply.github.com> Date: Thu, 5 Nov 2026 11:59:53 -0500 Subject: [PATCH 4/7] refactor(discord): name the scrolling pagination custom id builder getCustomId --- .../src/commands/guild/guild-top.subcommand.tsx | 8 ++++---- packages/discord/src/services/paginate.service.ts | 11 ++++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/discord-bot/src/commands/guild/guild-top.subcommand.tsx b/apps/discord-bot/src/commands/guild/guild-top.subcommand.tsx index dc4523b3a..8d03fac7e 100644 --- a/apps/discord-bot/src/commands/guild/guild-top.subcommand.tsx +++ b/apps/discord-bot/src/commands/guild/guild-top.subcommand.tsx @@ -75,10 +75,10 @@ export class GuildTopSubCommand extends GuildLeaderboardSubCommand { const [modeIndex, page] = this.getRequestedPage(interaction, modes, pageCount); const [key, title] = modes[modeIndex]; - const customId = (...values: (string | number)[]) => + const getCustomId = (...values: (string | number)[]) => createCustomId(GUILD_ROUTE, interaction.getUserId(), context.getCommandName(), guild.id, ...values); - const dropdown = new SelectMenuBuilder().customId(customId("menu", page)); + const dropdown = new SelectMenuBuilder().customId(getCustomId("menu", page)); for (const [index, [key, title]] of modes.entries()) { dropdown.option( @@ -98,13 +98,13 @@ export class GuildTopSubCommand extends GuildLeaderboardSubCommand { new ButtonBuilder() .emoji(t("emojis:up")) .style(ButtonStyle.Success) - .customId(customId(key, `prev.${page}`)) + .customId(getCustomId(key, `prev.${page}`)) ) .component( new ButtonBuilder() .emoji(t("emojis:down")) .style(ButtonStyle.Danger) - .customId(customId(key, `next.${page}`)) + .customId(getCustomId(key, `next.${page}`)) ) ); diff --git a/packages/discord/src/services/paginate.service.ts b/packages/discord/src/services/paginate.service.ts index abb78aea1..a445f1869 100644 --- a/packages/discord/src/services/paginate.service.ts +++ b/packages/discord/src/services/paginate.service.ts @@ -202,7 +202,8 @@ export async function paginate( * @param forwardButton The button to use for forward pagination * @param backwardButton The button to use for back pagination * @param invertButtons Whether to invert the buttons (backward becomes forward, forward becomes backward) - * @param customId Makes the buttons keep working after a restart, as in paginate + * @param getCustomId Returns a custom id ending with `page`, whose route re-runs the command. + * Without it, the buttons stop after 5 minutes */ export async function scrollingPagination( context: CommandContext, @@ -210,7 +211,7 @@ export async function scrollingPagination( forwardButton?: ButtonBuilder, backwardButton?: ButtonBuilder, invertButtons = false, - customId?: (page: string) => string, + getCustomId?: (page: string) => string, ) { let currentIndex = 0; const cache = new Map(); @@ -235,15 +236,15 @@ export async function scrollingPagination( if (invertButtons) controller.reverse(); - if (customId) { + if (getCustomId) { const interaction = context.getInteraction(); const index = interaction.isMessageComponentInteraction() ? getScrolledPage(parseCustomId(interaction.getCustomId()).values.at(-1)!, pages.length) : 0; - controller[0].customId(customId(`prev.${index}`)); - controller[1].customId(customId(`next.${index}`)); + controller[0].customId(getCustomId(`prev.${index}`)); + controller[1].customId(getCustomId(`next.${index}`)); const message = await getMessage(index); message.components = [new ActionRowBuilder(controller)]; From 6186063b2d5d8936de26327b2033363ec2bdbb39 Mon Sep 17 00:00:00 2001 From: Cody <36463298+ugcodrr@users.noreply.github.com> Date: Thu, 5 Nov 2026 12:00:04 -0500 Subject: [PATCH 5/7] docs(discord-bot): cut comments that restate the guild top and scroll code --- apps/discord-bot/src/commands/base.hypixel-command.ts | 5 +---- apps/discord-bot/src/commands/guild/guild-top.subcommand.tsx | 1 - apps/discord-bot/src/commands/guild/guild.route.ts | 2 +- packages/discord/src/services/paginate.service.ts | 5 +---- 4 files changed, 3 insertions(+), 10 deletions(-) diff --git a/apps/discord-bot/src/commands/base.hypixel-command.ts b/apps/discord-bot/src/commands/base.hypixel-command.ts index d8cf95657..d0b948441 100644 --- a/apps/discord-bot/src/commands/base.hypixel-command.ts +++ b/apps/discord-bot/src/commands/base.hypixel-command.ts @@ -50,10 +50,7 @@ export interface ProfileData { export type ModeEmoji = LocalizationString | false | undefined; -/** - * Page buttons on player commands re-run the command for the same player: - * `p::::` - */ +/** Page buttons re-run the command: `p::::` */ export const PROFILE_ROUTE = "p"; export function profileRoute(listener: AbstractCommandListener): ComponentRoute { diff --git a/apps/discord-bot/src/commands/guild/guild-top.subcommand.tsx b/apps/discord-bot/src/commands/guild/guild-top.subcommand.tsx index 8d03fac7e..e57887830 100644 --- a/apps/discord-bot/src/commands/guild/guild-top.subcommand.tsx +++ b/apps/discord-bot/src/commands/guild/guild-top.subcommand.tsx @@ -125,7 +125,6 @@ export class GuildTopSubCommand extends GuildLeaderboardSubCommand { return this.apiService.getGuild(query, type, user, context.getCacheLevel()); } - /** Today's first page for the command, otherwise the mode and page a click asks for */ private getRequestedPage( interaction: Interaction, modes: GuildTopMode[], diff --git a/apps/discord-bot/src/commands/guild/guild.route.ts b/apps/discord-bot/src/commands/guild/guild.route.ts index ed00832a8..94e1e9247 100644 --- a/apps/discord-bot/src/commands/guild/guild.route.ts +++ b/apps/discord-bot/src/commands/guild/guild.route.ts @@ -8,7 +8,7 @@ import type { AbstractCommandListener, ComponentRoute } from "@statsify/discord"; -/** Buttons on /guild re-run the subcommand for the same guild: `g::::...` */ +/** Guild buttons re-run `/guild `: `g::::...` */ export const GUILD_ROUTE = "g"; export function guildRoute(listener: AbstractCommandListener): ComponentRoute { diff --git a/packages/discord/src/services/paginate.service.ts b/packages/discord/src/services/paginate.service.ts index a445f1869..192e28cfb 100644 --- a/packages/discord/src/services/paginate.service.ts +++ b/packages/discord/src/services/paginate.service.ts @@ -301,10 +301,7 @@ export async function scrollingPagination( return message; } -/** - * The page a scroll button asks for, wrapping around. Its custom id ends with `prev.` or - * `next.` for the page it's on, since with two pages both buttons would ask for the same one - */ +/** Ids hold the current page, since with two pages both buttons would target the same one */ export function getScrolledPage(target: string, pageCount: number) { const [direction, page] = target.split("."); return (Number(page) + (direction === "prev" ? pageCount - 1 : 1)) % pageCount; From af500b741d2f14783011488cef898b5f844773fb Mon Sep 17 00:00:00 2001 From: Cody <36463298+ugcodrr@users.noreply.github.com> Date: Thu, 5 Nov 2026 12:10:33 -0500 Subject: [PATCH 6/7] refactor(discord-bot): build rankings page custom ids with getProfileCustomId --- .../discord-bot/src/commands/base.hypixel-command.ts | 4 ++-- .../src/commands/rankings/rankings.command.tsx | 12 ++---------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/apps/discord-bot/src/commands/base.hypixel-command.ts b/apps/discord-bot/src/commands/base.hypixel-command.ts index 00374e17c..46bf61733 100644 --- a/apps/discord-bot/src/commands/base.hypixel-command.ts +++ b/apps/discord-bot/src/commands/base.hypixel-command.ts @@ -58,9 +58,9 @@ export function profileRoute(listener: AbstractCommandListener): ComponentRoute listener.rerunCommand(interaction, ownerId, command.split("."), { player: uuid }); } -export function getProfileCustomId(context: CommandContext, uuid: string) { +export function getProfileCustomId(context: CommandContext, uuid: string, command = context.getCommandName()) { return (page: string) => - createCustomId(PROFILE_ROUTE, context.getInteraction().getUserId(), context.getCommandName(), uuid, page); + createCustomId(PROFILE_ROUTE, context.getInteraction().getUserId(), command, uuid, page); } export interface BaseHypixelCommand { diff --git a/apps/discord-bot/src/commands/rankings/rankings.command.tsx b/apps/discord-bot/src/commands/rankings/rankings.command.tsx index ae2061b10..28449e2f3 100644 --- a/apps/discord-bot/src/commands/rankings/rankings.command.tsx +++ b/apps/discord-bot/src/commands/rankings/rankings.command.tsx @@ -50,11 +50,10 @@ import { PlayerArgument, SubCommand, type SubCommandOptions, - createCustomId, } from "@statsify/discord"; import { ButtonStyle } from "discord-api-types/v10"; import { type GamesWithBackgrounds, mapBackground } from "#constants"; -import { PROFILE_ROUTE } from "#commands/base.hypixel-command"; +import { getProfileCustomId } from "#commands/base.hypixel-command"; import { RankingsProfile } from "./rankings.profile.js"; import { arrayGroup } from "@statsify/util"; import { games } from "./games.js"; @@ -368,14 +367,7 @@ export class RankingsCommand { new ButtonBuilder().emoji(t("emojis:up")).style(ButtonStyle.Success), new ButtonBuilder().emoji(t("emojis:down")).style(ButtonStyle.Danger), true, - (page) => - createCustomId( - PROFILE_ROUTE, - context.getInteraction().getUserId(), - `rankings.${context.getCommandName()}`, - player.uuid, - page - ) + getProfileCustomId(context, player.uuid, `rankings.${context.getCommandName()}`) ); } } From f42a1373e0ab6ea3b39e86a4aa227ecaf321b31a Mon Sep 17 00:00:00 2001 From: Cody <36463298+ugcodrr@users.noreply.github.com> Date: Thu, 5 Nov 2026 12:11:13 -0500 Subject: [PATCH 7/7] fix(discord-bot): drop the unused interaction in BaseHypixelCommand.run --- apps/discord-bot/src/commands/base.hypixel-command.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/discord-bot/src/commands/base.hypixel-command.ts b/apps/discord-bot/src/commands/base.hypixel-command.ts index 46bf61733..796dbc1b5 100644 --- a/apps/discord-bot/src/commands/base.hypixel-command.ts +++ b/apps/discord-bot/src/commands/base.hypixel-command.ts @@ -85,7 +85,6 @@ export abstract class BaseHypixelCommand