mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-07 06:18:03 +03:00
chart_pb support
This commit is contained in:
@@ -1,70 +1,54 @@
|
||||
// import { Interaction, SelectMenuInteraction } from "discord.js";
|
||||
// import { Game, Playtypes } from "tachi-common";
|
||||
// import { buildChartEmbed } from "../commands/chartSearch/buildChartEmbed";
|
||||
// import { LoggerLayers } from "../data/data";
|
||||
// import { buildProfileIntractable } from "../profile/buildProfileEmbed";
|
||||
// import { createLayeredLogger } from "../utils/logger";
|
||||
// import { stringToSimpleGameType } from "../utils/utils";
|
||||
import { SelectMenuInteraction } from "discord.js";
|
||||
import { Game, Playtype } from "tachi-common";
|
||||
import { LoggerLayers } from "../data/data";
|
||||
import { DiscordUserMapDocument } from "../database/documents";
|
||||
import { GetUserAndTokenForDiscordID } from "../database/queries";
|
||||
import { GetUserInfo } from "../utils/apiRequests";
|
||||
import { CreateChartScoresEmbed } from "../utils/embeds";
|
||||
import { CreateLayeredLogger } from "../utils/logger";
|
||||
|
||||
// const logger = createLayeredLogger(LoggerLayers.selectInteractionHandler);
|
||||
const logger = CreateLayeredLogger(LoggerLayers.slashCommands);
|
||||
|
||||
// export enum validSelectCustomIdPrefaces {
|
||||
// selectSongForSearch = "selectSongForSearch",
|
||||
// SelectGameForProfile = "SelectGameForProfile",
|
||||
// }
|
||||
/**
|
||||
* Handles incoming command requests by resolving the interaction to the command
|
||||
* it refers to, and calling it.
|
||||
*
|
||||
* @param interaction - The interaction the user made. This contains things like what
|
||||
* command they called and with what arguments.
|
||||
* @param requestingUser - The user who interacted with this command.
|
||||
*/
|
||||
export async function handleIsSelectMenu(
|
||||
interaction: SelectMenuInteraction,
|
||||
requestingUser: DiscordUserMapDocument
|
||||
) {
|
||||
try {
|
||||
if (interaction.user.id !== requestingUser.discordID) {
|
||||
await interaction.reply({
|
||||
ephemeral: true,
|
||||
content: "You can't interact with other peoples select boxes, get your own!",
|
||||
});
|
||||
}
|
||||
|
||||
// export type SelectHandlersType = Record<
|
||||
// validSelectCustomIdPrefaces,
|
||||
// (interaction: SelectMenuInteraction) => void
|
||||
// >;
|
||||
// export const selectHandlers: SelectHandlersType = {
|
||||
// [validSelectCustomIdPrefaces.SelectGameForProfile]: async (
|
||||
// interaction: SelectMenuInteraction
|
||||
// ): Promise<void> => {
|
||||
// const userId = parseInt(interaction.customId.split(":")[1]);
|
||||
// await interaction.editReply(
|
||||
// await buildProfileIntractable(
|
||||
// userId,
|
||||
// interaction.user.id,
|
||||
// stringToSimpleGameType(interaction.values[0])
|
||||
// )
|
||||
// );
|
||||
// },
|
||||
// [validSelectCustomIdPrefaces.selectSongForSearch]: async <T extends Game>(
|
||||
// interaction: SelectMenuInteraction
|
||||
// ): Promise<void> => {
|
||||
// try {
|
||||
// const interactionValues = interaction.values[0].split(":");
|
||||
// const embed = await buildChartEmbed({
|
||||
// songId: interactionValues[0],
|
||||
// playtype: <Playtypes[T]>interactionValues[1],
|
||||
// game: <T>interactionValues[2],
|
||||
// discordUserId: interaction.user.id,
|
||||
// });
|
||||
// logger.info("Built new embed");
|
||||
// await interaction.editReply(embed);
|
||||
// } catch (e) {
|
||||
// logger.error(e);
|
||||
// }
|
||||
// },
|
||||
// };
|
||||
if (interaction.customId.startsWith("chart-select")) {
|
||||
// horrendous hackery, but we only have 100 chars to store selector metadata.
|
||||
const [, game, playtype, userID] = interaction.customId.match(
|
||||
/!(.*):(.*):(.*)$/u
|
||||
) as unknown as [string, Game, Playtype, string];
|
||||
|
||||
// export const handleIsSelectMenu = async (interaction: Interaction): Promise<void> => {
|
||||
// try {
|
||||
// /** Rechecking required to enforce types */
|
||||
// if (interaction.isSelectMenu()) {
|
||||
// await interaction.deferUpdate();
|
||||
// const customIdPrefix = <validSelectCustomIdPrefaces>interaction.customId.split(":")[0];
|
||||
// logger.info(`customIdPrefix: ${customIdPrefix}`);
|
||||
// if (Object.values(validSelectCustomIdPrefaces).includes(customIdPrefix)) {
|
||||
// await selectHandlers[customIdPrefix](interaction);
|
||||
// } else {
|
||||
// logger.info(`Unknown select handler: ${customIdPrefix}`);
|
||||
// }
|
||||
// }
|
||||
// return;
|
||||
// } catch (e) {
|
||||
// logger.error(e);
|
||||
// logger.error("Failed to handle isSelectMenu interaction");
|
||||
// }
|
||||
// };
|
||||
const userDoc = await GetUserInfo(userID);
|
||||
|
||||
const embed = await CreateChartScoresEmbed(
|
||||
userDoc,
|
||||
game,
|
||||
playtype,
|
||||
interaction.values[0]
|
||||
);
|
||||
|
||||
await interaction.update({ embeds: [embed] });
|
||||
} else {
|
||||
logger.warn(`Unknown selector triggered: ${interaction.customId}.`);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("Failed to handle isSelectMenu interaction", { error: e });
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -3,6 +3,7 @@ import { BotConfig, ServerConfig } from "./config";
|
||||
import { LoggerLayers, METALLIC_MIND_SPLASHES } from "./data/data";
|
||||
import { GetUserAndTokenForDiscordID } from "./database/queries";
|
||||
import { handleIsCommand } from "./interactionHandlers/handleIsCommand";
|
||||
import { handleIsSelectMenu } from "./interactionHandlers/handleIsSelectMenu";
|
||||
// import { handleIsSelectMenu } from "./interactionHandlers/handleIsSelectMenu";
|
||||
import { app } from "./server/server";
|
||||
import { RegisterSlashCommands } from "./slashCommands/register";
|
||||
@@ -38,7 +39,7 @@ client.on("interactionCreate", async (interaction) => {
|
||||
}
|
||||
|
||||
if (interaction.isSelectMenu()) {
|
||||
// return handleIsSelectMenu(interaction);
|
||||
return handleIsSelectMenu(interaction, requestingUser);
|
||||
}
|
||||
|
||||
if (interaction.isCommand()) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import profile from "./commands/profile";
|
||||
import quote from "./commands/quote";
|
||||
import sync from "./commands/sync";
|
||||
import whois from "./commands/whois";
|
||||
import chart_pb from "./commands/chart_pb";
|
||||
import { SlashCommand } from "./types";
|
||||
|
||||
export const SLASH_COMMANDS: Map<string, SlashCommand> = new Map(
|
||||
@@ -14,6 +15,7 @@ export const SLASH_COMMANDS: Map<string, SlashCommand> = new Map(
|
||||
faq,
|
||||
whois,
|
||||
profile,
|
||||
chart_pb,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { SlashCommandBuilder } from "@discordjs/builders";
|
||||
import { MessageActionRow, MessageSelectMenu, Util } from "discord.js";
|
||||
import { FormatChart, integer, SongDocument } from "tachi-common";
|
||||
import { GetGPTAndUser } from "../../utils/argParsers";
|
||||
import { CreateChartScoresEmbed } from "../../utils/embeds";
|
||||
import { TachiServerV1Get } from "../../utils/fetchTachi";
|
||||
import { GPTOptions, MakeRequired, OtherUserOption } from "../../utils/options";
|
||||
import { ChartQueryReturns } from "../../utils/returnTypes";
|
||||
import { SlashCommand } from "../types";
|
||||
|
||||
const command: SlashCommand = {
|
||||
info: new SlashCommandBuilder()
|
||||
.setName("chart_pb")
|
||||
.setDescription("Retrieve your personal best on a given chart.")
|
||||
.addStringOption(MakeRequired(GPTOptions))
|
||||
.addStringOption((str) =>
|
||||
str
|
||||
.setName("song_name")
|
||||
.setDescription("The name of the song to fetch info on.")
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(OtherUserOption)
|
||||
.toJSON(),
|
||||
exec: async (interaction, requestingUser) => {
|
||||
const gptUserInfo = await GetGPTAndUser(interaction, requestingUser);
|
||||
|
||||
if (gptUserInfo.error !== null) {
|
||||
return gptUserInfo.error;
|
||||
}
|
||||
|
||||
const search = interaction.options.getString("song_name", true);
|
||||
|
||||
const { userDoc, game, playtype } = gptUserInfo.content;
|
||||
|
||||
const chartsRes = await TachiServerV1Get<ChartQueryReturns>(
|
||||
`/games/${game}/${playtype}/charts`,
|
||||
requestingUser.tachiApiToken,
|
||||
{ search }
|
||||
);
|
||||
|
||||
if (!chartsRes.success) {
|
||||
return `Failed to search charts for '${Util.escapeMarkdown(search)}'.`;
|
||||
}
|
||||
|
||||
if (chartsRes.body.charts.length === 0) {
|
||||
return `Found no charts for the query '${Util.escapeMarkdown(search)}'.`;
|
||||
}
|
||||
|
||||
// discord caps selects at 25 ... :(
|
||||
if (chartsRes.body.charts.length > 25) {
|
||||
chartsRes.body.charts = chartsRes.body.charts.slice(0, 25);
|
||||
}
|
||||
|
||||
const firstChart = chartsRes.body.charts[0];
|
||||
|
||||
const songMap = new Map<integer, SongDocument>();
|
||||
|
||||
for (const song of chartsRes.body.songs) {
|
||||
songMap.set(song.id, song);
|
||||
}
|
||||
|
||||
const firstSong = songMap.get(firstChart.songID);
|
||||
|
||||
if (!firstSong) {
|
||||
throw new Error(`No firstSong for equivalent firstChart ${firstChart.chartID}?`);
|
||||
}
|
||||
|
||||
const embed = await CreateChartScoresEmbed(userDoc, game, playtype, firstChart.chartID);
|
||||
|
||||
const select = new MessageActionRow().addComponents(
|
||||
new MessageSelectMenu()
|
||||
.setCustomId(`chart-select!${game}:${playtype}:${userDoc.id}`)
|
||||
.setPlaceholder("Select a different chart.")
|
||||
.addOptions(
|
||||
chartsRes.body.charts.map((chart) => ({
|
||||
label: FormatChart(game, songMap.get(chart.songID)!, chart),
|
||||
value: chart.chartID,
|
||||
}))
|
||||
)
|
||||
);
|
||||
|
||||
return { embeds: [embed], components: [select] };
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
@@ -1,8 +1,13 @@
|
||||
import { APIApplicationCommandOption } from "discord-api-types";
|
||||
import { CommandInteraction, MessageEmbed, MessagePayload } from "discord.js";
|
||||
import {
|
||||
CommandInteraction,
|
||||
InteractionReplyOptions,
|
||||
MessageEmbed,
|
||||
MessagePayload,
|
||||
} from "discord.js";
|
||||
import { DiscordUserMapDocument } from "../database/documents";
|
||||
|
||||
export type Emittable = MessagePayload | string | MessageEmbed;
|
||||
export type Emittable = MessagePayload | string | MessageEmbed | InteractionReplyOptions;
|
||||
|
||||
export type Command = (
|
||||
interaction: CommandInteraction,
|
||||
|
||||
@@ -6,8 +6,12 @@ import {
|
||||
Playtype,
|
||||
PublicUserDocument,
|
||||
integer,
|
||||
ChartDocument,
|
||||
SongDocument,
|
||||
PBScoreDocument,
|
||||
} from "tachi-common";
|
||||
import { LoggerLayers } from "../data/data";
|
||||
import db from "../database/mongo";
|
||||
import { RequestTypes, TachiServerV1Get, TachiServerV1Request } from "./fetchTachi";
|
||||
import { CreateLayeredLogger } from "./logger";
|
||||
import { Sleep } from "./misc";
|
||||
@@ -51,6 +55,35 @@ export async function GetGoalWithID(goalID: string, game: Game, playtype: Playty
|
||||
return res.body;
|
||||
}
|
||||
|
||||
export async function GetChartInfoForUser(
|
||||
userID: integer | string,
|
||||
chartID: string,
|
||||
game: Game,
|
||||
playtype: Playtype
|
||||
) {
|
||||
const res = await TachiServerV1Get<{ song: SongDocument; chart: ChartDocument }>(
|
||||
`/games/${game}/${playtype}/charts/${chartID}`,
|
||||
null
|
||||
);
|
||||
|
||||
if (!res.success) {
|
||||
throw new Error(`Failed to fetch song/chart with chartID ${chartID}.`);
|
||||
}
|
||||
|
||||
const pbRes = await TachiServerV1Get<{ chart: ChartDocument; pb: PBScoreDocument }>(
|
||||
`/users/${userID}/games/${game}/${playtype}/pbs/${chartID}`,
|
||||
null
|
||||
);
|
||||
|
||||
const pb = pbRes.success ? pbRes.body.pb : null;
|
||||
|
||||
if (pb === null && pbRes.statusCode !== 404) {
|
||||
throw new Error(`Failed to fetch score info for userID ${userID} on chart ${chartID}.`);
|
||||
}
|
||||
|
||||
return { song: res.body.song, chart: res.body.chart, pb };
|
||||
}
|
||||
|
||||
export async function PerformScoreImport(
|
||||
url: string,
|
||||
authToken: string,
|
||||
|
||||
+64
-1
@@ -1,11 +1,22 @@
|
||||
import { MessageEmbed } from "discord.js";
|
||||
import { FormatGame, GetGamePTConfig, ImportDocument, PublicUserDocument } from "tachi-common";
|
||||
import {
|
||||
FormatChart,
|
||||
FormatGame,
|
||||
Game,
|
||||
GetGamePTConfig,
|
||||
ImportDocument,
|
||||
Playtype,
|
||||
PublicUserDocument,
|
||||
} from "tachi-common";
|
||||
import { BotConfig, ServerConfig } from "../config";
|
||||
import { GetChartInfoForUser } from "./apiRequests";
|
||||
import { PrependTachiUrl } from "./fetchTachi";
|
||||
import {
|
||||
CreateChartLink,
|
||||
Entries,
|
||||
FormatDate,
|
||||
FormatProfileRating,
|
||||
FormatScoreRating,
|
||||
MillisToSince,
|
||||
Pluralise,
|
||||
UppercaseFirst,
|
||||
@@ -98,3 +109,55 @@ export function CreateGameProfileEmbed(userDoc: PublicUserDocument, ugptStats: U
|
||||
`${BotConfig.TACHI_SERVER_LOCATION}/dashboard/users/${userDoc.username}/games/${game}/${playtype}`
|
||||
);
|
||||
}
|
||||
|
||||
export async function CreateChartScoresEmbed(
|
||||
userDoc: PublicUserDocument,
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
chartID: string
|
||||
) {
|
||||
const { song, chart, pb } = await GetChartInfoForUser(userDoc.id, chartID, game, playtype);
|
||||
|
||||
const embed = CreateEmbed()
|
||||
.setTitle(`${userDoc.username}: ${FormatChart(game, song, chart)}`)
|
||||
.setThumbnail(PrependTachiUrl(`/users/${userDoc.id}/pfp`))
|
||||
.setURL(CreateChartLink(chart, game));
|
||||
|
||||
if (pb === null) {
|
||||
embed.setDescription("You have not played this chart.");
|
||||
} else {
|
||||
embed
|
||||
.addField(
|
||||
"Score",
|
||||
`${pb.scoreData.score} (${pb.scoreData.grade}, ${pb.scoreData.percent.toFixed(
|
||||
2
|
||||
)}%)`,
|
||||
true
|
||||
)
|
||||
.addField("Lamp", pb.scoreData.lamp, true)
|
||||
.addField(
|
||||
"Ratings",
|
||||
Entries(pb.calculatedData)
|
||||
.map(
|
||||
([key, value]) =>
|
||||
`${UppercaseFirst(key)}: ${FormatScoreRating(
|
||||
game,
|
||||
playtype,
|
||||
key,
|
||||
value
|
||||
)}`
|
||||
)
|
||||
.join("\n")
|
||||
)
|
||||
.addField(
|
||||
"Last Raised",
|
||||
pb.timeAchieved
|
||||
? `${FormatDate(pb.timeAchieved)} (${MillisToSince(pb.timeAchieved)})`
|
||||
: "N/A",
|
||||
true
|
||||
)
|
||||
.addField("Ranking", `#**${pb.rankingData.rank}**/${pb.rankingData.outOf}`);
|
||||
}
|
||||
|
||||
return embed;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
Playtype,
|
||||
UGSRatingsLookup,
|
||||
integer,
|
||||
ChartDocument,
|
||||
ScoreCalculatedDataLookup,
|
||||
} from "tachi-common";
|
||||
import { GameClassSets } from "tachi-common/js/game-classes";
|
||||
import { BotConfig } from "../config";
|
||||
@@ -67,6 +69,25 @@ export function FormatProfileRating(
|
||||
return formatter(value);
|
||||
}
|
||||
|
||||
export function FormatScoreRating(
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
rating: ScoreCalculatedDataLookup[IDStrings],
|
||||
value: number | null | undefined
|
||||
) {
|
||||
if (value === null || value === undefined) {
|
||||
return "No Data.";
|
||||
}
|
||||
|
||||
const formatter = GetGamePTConfig(game, playtype).scoreRatingAlgFormatters[rating];
|
||||
|
||||
if (!formatter) {
|
||||
return value.toFixed(2);
|
||||
}
|
||||
|
||||
return formatter(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Typesafe asserted version of Object.entries.
|
||||
*/
|
||||
@@ -127,6 +148,9 @@ export function FormatClass(
|
||||
return gptConfig.classHumanisedFormat[classSet][classValue].display;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a game, return the discord channel it's associated with.
|
||||
*/
|
||||
export function GetGameChannel(client: Client, game: Game) {
|
||||
const gameChannelID = BotConfig.DISCORD.GAME_CHANNELS[game];
|
||||
|
||||
@@ -150,3 +174,16 @@ export function GetGameChannel(client: Client, game: Game) {
|
||||
|
||||
return channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a chart and a game, return a link to the site for that chart.
|
||||
*/
|
||||
export function CreateChartLink(chart: ChartDocument, game: Game) {
|
||||
if (chart.isPrimary) {
|
||||
return `${BotConfig.TACHI_SERVER_LOCATION}/dashboard/games/${game}/${
|
||||
chart.playtype
|
||||
}/songs/${chart.songID}/${encodeURIComponent(chart.difficulty)}`;
|
||||
}
|
||||
|
||||
return `${BotConfig.TACHI_SERVER_LOCATION}/dashboard/games/${game}/${chart.playtype}/songs/${chart.songID}/${chart.chartID}`;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
UGSRatingsLookup,
|
||||
UserGameStats,
|
||||
integer,
|
||||
ChartDocument,
|
||||
SongDocument,
|
||||
} from "tachi-common";
|
||||
|
||||
export interface ServerConfig {
|
||||
@@ -50,3 +52,8 @@ export interface UGPTStats<I extends IDStrings = IDStrings> {
|
||||
totalScores: integer;
|
||||
rankingData: Record<UGSRatingsLookup[I], { ranking: integer; outOf: integer }>;
|
||||
}
|
||||
|
||||
export interface ChartQueryReturns {
|
||||
charts: (ChartDocument & { __playcount: integer })[];
|
||||
songs: SongDocument[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user