Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions apps/discord-bot/src/commands/base.hypixel-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,20 @@
*/

import {
AbstractCommandListener,
ApiService,
Command,
CommandContext,
ComponentRoute,
LocalizationString,
LocalizeFunction,
Page,
paginate,
PlayerArgument,
SubPage,
createCustomId,
} from "@statsify/discord";
import { CacheLevel } from "@statsify/api-client";
import { Container } from "typedi";
import { GamesWithBackgrounds, mapBackground } from "#constants";
import { HistoricalTimeData } from "#components";
Expand Down Expand Up @@ -47,6 +51,19 @@ export interface ProfileData<T extends GamesWithBackgrounds, K = never> {

export type ModeEmoji = LocalizationString | false | undefined;

/** Page buttons re-run the command: `p:<user id>:<command>:<player uuid>:<page>` */
export const PROFILE_ROUTE = "p";

export function profileRoute(listener: AbstractCommandListener): ComponentRoute {
return (interaction, { ownerId, values: [command, uuid] }) =>
listener.rerunCommand(interaction, ownerId, [command], { player: uuid });
}

export function getProfileCustomId(context: CommandContext, uuid: string) {
return (page: string) =>
createCustomId(PROFILE_ROUTE, context.getInteraction().getUserId(), context.getCommandName(), uuid, page);
}

export interface BaseHypixelCommand<T extends GamesWithBackgrounds, K = never> {
getPreProfileData?(player: Player): K | Promise<K>;
filterModes?(player: Player, modes: GameModeWithSubModes<T>[]): GameModeWithSubModes<T>[];
Expand All @@ -69,8 +86,13 @@ export abstract class BaseHypixelCommand<T extends GamesWithBackgrounds, K = nev

public async run(context: CommandContext) {
const user = context.getUser();
const interaction = context.getInteraction();

const player = await this.apiService.getPlayer(context.option("player"), user);
const player = await this.apiService.getPlayer(
context.option("player"),
user,
interaction.isCommandInteraction() ? undefined : CacheLevel.CACHE_ONLY
);

const [logo, skin, badge] = await Promise.all([
getLogo(user),
Expand Down Expand Up @@ -157,7 +179,7 @@ export abstract class BaseHypixelCommand<T extends GamesWithBackgrounds, K = nev
return { ...pageInput, subPages };
});

return paginate(context, pages);
return paginate(context, pages, getProfileCustomId(context, player.uuid));
}

public abstract getProfile(
Expand Down
3 changes: 3 additions & 0 deletions apps/discord-bot/src/lib/command.listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
Interaction,
} from "@statsify/discord";
import { Container } from "typedi";
import { PROFILE_ROUTE, profileRoute } from "#commands/base.hypixel-command";
import { STATUS_COLORS } from "@statsify/logger";
import { User, UserTier } from "@statsify/schemas";
import { config, formatTime } from "@statsify/util";
Expand Down Expand Up @@ -49,6 +50,8 @@ export class CommandListener extends AbstractCommandListener {

this.apiService = Container.get(ApiService);
this.cooldowns = new Map();

this.addRoute(PROFILE_ROUTE, profileRoute(this));
}

protected async onCommand(interaction: Interaction): Promise<void> {
Expand Down
5 changes: 5 additions & 0 deletions packages/discord/src/command/command.context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ export class CommandContext {
return this.user;
}

/** The name of the command or subcommand being run, without its parents */
public getCommandName(): string {
return this.data.name;
}

public setUser(user: User | null) {
this.user = user;
if (user?.locale) this.interaction.setLocale(user?.locale);
Expand Down
9 changes: 7 additions & 2 deletions packages/discord/src/services/api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,19 @@ export class ApiService extends StatsifyApiService {
*
* @param tag Username, UUID, or Discord ID, or nothing. If nothing is provided it will attempt to fall back on the provided user.
* @param user User to use if no tag is provided.
* @param cache Defaults to live stats for Gold users and recently cached stats for everyone else
* @returns a Player
*/
public async getPlayer(tag: string, user: User | null = null) {
public async getPlayer(
tag: string,
user: User | null = null,
cache = User.isGold(user) ? CacheLevel.LIVE : CacheLevel.CACHE
) {
const [formattedTag, type] = this.parseTag(tag);
const input = await this.resolveTag(formattedTag, type, user);

return super
.getCachedPlayer(input, User.isGold(user) ? CacheLevel.LIVE : CacheLevel.CACHE)
.getCachedPlayer(input, cache)
.catch((err) => {
if (!err.response || !err.response.data) throw this.unknownError();
const error = err.response.data as PlayerNotFoundException;
Expand Down
52 changes: 46 additions & 6 deletions packages/discord/src/services/paginate.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
SelectMenuBuilder,
SelectMenuOptionBuilder,
} from "#messages";
import { Interaction } from "#interaction";
import { Interaction, parseCustomId } from "#interaction";
import type {
AbstractCommandListener,
CommandContext,
Expand Down Expand Up @@ -53,12 +53,13 @@ type PageId = `${number}|${number}`;
*
* @param context The context of the command
* @param pages The array of pages to paginate against
* @param timeout When to stop the pagination (ms), defaults to 300000
* @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 paginate(
context: CommandContext,
pages: Page[],
timeout = 300_000,
getCustomId?: (page: string) => string,
) {
const cache = new Map<PageId, Message>();
const t = context.t();
Expand Down Expand Up @@ -86,6 +87,25 @@ export async function paginate(
// If there is only one page with no sub pages, return the message immediately without pagination
if (pages.length === 1 && "generator" in pages[0]) return getMessage(0, 0);

if (getCustomId) {
let [index, subIndex] = getRequestedPage(context.getInteraction());
if (index >= pages.length) [index, subIndex] = [0, 0];

const page = pages[index];
if ("subPages" in page && subIndex >= page.subPages.length) subIndex = 0;

const message = await getMessage(index, subIndex);

message.components = [
new PageController(pages, index, getCustomId).getActionRow(),
"subPages" in page && page.subPages.length > 1 ?
new PageController(page.subPages, subIndex, getCustomId, index).getActionRow() :
undefined,
].filter((row) => row !== undefined);

return message;
}

const userId = context.getInteraction().getUserId();
const listener = context.getListener();

Expand Down Expand Up @@ -164,7 +184,7 @@ export async function paginate(
return context.reply({ components: [] });
}

setTimeout(onTimeout, timeout);
setTimeout(onTimeout, 300_000);

const message = await getMessage(currentIndex, currentSubIndex);
message.components = [
Expand Down Expand Up @@ -267,6 +287,17 @@ export async function scrollingPagination(
return message;
}

function getRequestedPage(interaction: Interaction): [index: number, subIndex: number] {
if (!interaction.isMessageComponentInteraction()) return [0, 0];

const target = interaction.isSelectMenuInteraction() ?
interaction.getData().values[0] :
parseCustomId(interaction.getCustomId()).values.at(-1)!;

const [index, subIndex = 0] = target.split(".").map(Number);
return [index, subIndex];
}

async function contentToMessage(
content: PaginateInteractionContent,
): Promise<Message> {
Expand All @@ -291,14 +322,22 @@ async function contentToMessage(
class PageController {
#menu: SelectMenuBuilder | ButtonBuilder[];

public constructor(pages: PageInput[], selected: number) {
public constructor(
pages: PageInput[],
selected: number,
getCustomId?: (page: string) => string,
parentIndex?: number
) {
const target = (index: number) => (parentIndex === undefined ? `${index}` : `${parentIndex}.${index}`);

if (pages.length > 5) {
const menu = new SelectMenuBuilder();
if (getCustomId) menu.customId(getCustomId(parentIndex === undefined ? "menu" : "submenu"));

for (const [index, page] of pages.entries()) {
const option = new SelectMenuOptionBuilder()
.label(page.label)
.value(`${index}`);
.value(target(index));
if (page.emoji) option.emoji(page.emoji);
menu.option(option);
}
Expand All @@ -310,6 +349,7 @@ class PageController {
const button = new ButtonBuilder()
.label(page.label)
.style(ButtonStyle.Secondary);
if (getCustomId) button.customId(getCustomId(target(index)));
if (page.emoji) button.emoji(page.emoji);
if (index === selected) button.style(ButtonStyle.Primary);
return button;
Expand Down
Loading