chart_pb support

This commit is contained in:
zkldi
2022-03-22 01:44:07 +00:00
parent 0670462d3a
commit 52e8ecf564
9 changed files with 288 additions and 70 deletions
@@ -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
View File
@@ -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()) {
+2
View File
@@ -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;
+7 -2
View File
@@ -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,
+33
View File
@@ -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
View File
@@ -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;
}
+37
View File
@@ -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}`;
}
+7
View File
@@ -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[];
}