From 342b9c968440c825a639ea351cbb8580e0190ab1 Mon Sep 17 00:00:00 2001 From: Pfych Date: Sat, 25 Sep 2021 14:32:41 +1000 Subject: [PATCH 1/8] [user-can] Basic chart search implementation --- bot/src/chartSearch/buildChartEmbed.ts | 61 +++++++++++ bot/src/chartSearch/chartSearch.ts | 103 ++++++++++++++++++ bot/src/config.ts | 4 +- .../command/handleIsCommand.ts | 2 +- .../selectMenu/handleIsSelectMenu.ts | 39 ++++++- bot/src/profile/buildProfileEmbed.ts | 3 +- bot/src/slashCommands/register.ts | 14 ++- bot/src/utils/utils.ts | 29 +++++ 8 files changed, 245 insertions(+), 10 deletions(-) create mode 100644 bot/src/chartSearch/buildChartEmbed.ts create mode 100644 bot/src/chartSearch/chartSearch.ts diff --git a/bot/src/chartSearch/buildChartEmbed.ts b/bot/src/chartSearch/buildChartEmbed.ts new file mode 100644 index 000000000..2e3310e70 --- /dev/null +++ b/bot/src/chartSearch/buildChartEmbed.ts @@ -0,0 +1,61 @@ +import { InteractionReplyOptions, MessageActionRow, MessageEmbed, MessagePayload, MessageSelectMenu } from "discord.js"; +import { Game, Playtypes } from "tachi-common"; +import { LoggerLayers } from "../config"; +import { validSelectCustomIdPrefaces } from "../interactionHandlers/selectMenu/handleIsSelectMenu"; +import { createLayeredLogger } from "../utils/logger"; +import { getGameImage } from "../utils/utils"; +import { getDetailedChartData, SongSearchResult } from "./chartSearch"; + +const logger = createLayeredLogger(LoggerLayers.buildChartEmbed); + +export const buildChartsSelect = (charts: SongSearchResult[], playtype: Playtypes[T], game: Game) => { + return new MessageActionRow().addComponents( + new MessageSelectMenu() + .setCustomId(validSelectCustomIdPrefaces.SelectChartForSearch) + .setPlaceholder("Select Chart") + .addOptions( + charts.map((chart) => { + return { + label: `${chart.title} - ${chart.artist}`, + value: `${chart.id}:${playtype}:${game}` + }; + }) + ) + ); +}; + +export const buildChartEmbed = async (args: { + searchResults?: SongSearchResult[]; + songId?: string; + playtype: Playtypes[T]; + game: Game; +}): Promise => { + try { + const { searchResults, songId, playtype, game } = args; + + const embed = new MessageEmbed().setColor("#cc527a"); + + if (!songId && searchResults) { + embed.addField(`${searchResults.length} potential results found`, "Select from the dropdown"); + return { embeds: [embed], components: [buildChartsSelect(searchResults, playtype, game)] }; + } else if (songId) { + const details = await getDetailedChartData(songId, playtype, game); + embed.addField(details.song.title || "Chart", details.song.artist || "Artist"); + embed.setThumbnail(getGameImage(details.song.firstVersion || "0", game)); + + const sortedCharts = details.charts.sort((a, b) => a.levelNum - b.levelNum); + + sortedCharts.forEach((chart) => { + embed.addField(`${chart.difficulty} (${chart.level})`, "Parse relevant data here", true); + }); + + return { embeds: [embed] }; + } else { + throw new Error("Invalid call to buildChartEmbed"); + } + } catch (e) { + logger.error(e); + + throw new Error("Error building chart embed"); + } +}; diff --git a/bot/src/chartSearch/chartSearch.ts b/bot/src/chartSearch/chartSearch.ts new file mode 100644 index 000000000..5ac2a80f0 --- /dev/null +++ b/bot/src/chartSearch/chartSearch.ts @@ -0,0 +1,103 @@ +import { CommandInteraction } from "discord.js"; +import { Playtypes, PublicUserDocument, SongDocument } from "tachi-common"; +import { ChartDocument, Game } from "tachi-common/js/types"; +import { LoggerLayers } from "../config"; +import { TachiServerV1Get } from "../utils/fetch-tachi"; +import { createLayeredLogger } from "../utils/logger"; +import { stringToSimpleGameType } from "../utils/utils"; +import { buildChartEmbed } from "./buildChartEmbed"; + +const logger = createLayeredLogger(LoggerLayers.chartSearch); + +export interface SongSearchResult extends SongDocument { + game: Game; + __textScore: number; +} + +export interface SearchResult { + users: PublicUserDocument[]; + songs: SongSearchResult[]; +} + +export interface DetailedSongResponse { + song: Partial; + charts: ChartDocument[]; +} + +export const getChartMeta = async (chartName: string): Promise => { + try { + logger.info(`Searching for ${chartName}`); + const chartResponse = ( + await TachiServerV1Get("/search", { + search: chartName + }) + ).body?.songs; + + if (chartResponse && chartResponse.length > 0) { + logger.info(`${chartResponse.length} values found`); + return chartResponse; + } else { + logger.error(`No results found for ${chartName}`); + throw new Error(`No results found for ${chartName}`); + } + } catch (e) { + logger.error(e); + throw new Error("Unable to fetch chart meta"); + } +}; + +export const getChartMetaFiltered = async (chartName: string, game: Game): Promise => { + try { + const chartMeta = await getChartMeta(chartName); + const chartMetaFiltered = chartMeta.filter((chart) => chart.game === game); + if (chartMetaFiltered.length === 0) { + logger.info("No charts found"); + + throw new Error("No charts found"); + } + + return chartMetaFiltered; + } catch (e) { + logger.info(e); + + throw new Error("No charts found"); + } +}; + +export const getDetailedChartData = async ( + chartId: string, + playtype: Playtypes[T], + game: Game +): Promise => { + try { + logger.info(`Getting detailed info for ${chartId} (${game}:${playtype})`); + const data = await TachiServerV1Get(`/games/${game}/${playtype}/songs/${chartId}`); + if (data.success) { + return data.body; + } else { + logger.error(data.description); + throw new Error(data.description); + } + } catch (e) { + logger.info(e); + throw new Error("Unable to fetch detailed chart data"); + } +}; + +export const searchForChart = async (interaction: CommandInteraction): Promise => { + const chartName = interaction.options.getString("chart", true); + const gpt = stringToSimpleGameType(interaction.options.getString("game", true)); + const game = gpt.game; + const playtype = gpt.playtype; + + try { + logger.info(`Requested ${chartName} in ${game}`); + + const chartMetaFiltered = await getChartMetaFiltered(chartName, game); + + await interaction.reply(await buildChartEmbed({ searchResults: chartMetaFiltered, playtype, game })); + } catch (e) { + await interaction.reply(`No results found for ${chartName}`); + logger.error(e); + } +}; diff --git a/bot/src/config.ts b/bot/src/config.ts index 86bf3e1c4..c723a142d 100644 --- a/bot/src/config.ts +++ b/bot/src/config.ts @@ -10,5 +10,7 @@ export enum LoggerLayers { tachiFetch = "tachiFetch", profile = "profile", selectInteractionHandler = "selectInteractionHandler", - buildProfileEmbed = "buildProfileEmbed" + buildProfileEmbed = "buildProfileEmbed", + chartSearch = "chartSearch", + buildChartEmbed = "buildChartEmbed" } diff --git a/bot/src/interactionHandlers/command/handleIsCommand.ts b/bot/src/interactionHandlers/command/handleIsCommand.ts index a812a0d0f..1d03afb38 100644 --- a/bot/src/interactionHandlers/command/handleIsCommand.ts +++ b/bot/src/interactionHandlers/command/handleIsCommand.ts @@ -3,7 +3,7 @@ import { LoggerLayers } from "../../config"; import { SlashCommand, slashCommands } from "../../slashCommands/register"; import { createLayeredLogger } from "../../utils/logger"; -const logger = createLayeredLogger(LoggerLayers.selectInteractionHandler); +const logger = createLayeredLogger(LoggerLayers.slashCommands); export const handleIsCommand = async (interaction: Interaction): Promise => { try { diff --git a/bot/src/interactionHandlers/selectMenu/handleIsSelectMenu.ts b/bot/src/interactionHandlers/selectMenu/handleIsSelectMenu.ts index 77fee9e9d..826bece89 100644 --- a/bot/src/interactionHandlers/selectMenu/handleIsSelectMenu.ts +++ b/bot/src/interactionHandlers/selectMenu/handleIsSelectMenu.ts @@ -1,4 +1,6 @@ -import { Interaction } from "discord.js"; +import { Interaction, SelectMenuInteraction } from "discord.js"; +import { Game, Playtypes } from "tachi-common"; +import { buildChartEmbed } from "../../chartSearch/buildChartEmbed"; import { LoggerLayers } from "../../config"; import { buildProfileIntractable } from "../../profile/buildProfileEmbed"; import { createLayeredLogger } from "../../utils/logger"; @@ -6,18 +8,43 @@ import { stringToSimpleGameType } from "../../utils/utils"; const logger = createLayeredLogger(LoggerLayers.selectInteractionHandler); +export enum validSelectCustomIdPrefaces { + SelectChartForSearch = "SelectChartForSearch", + SelectGameForProfile = "SelectGameForProfile" +} + +export type SelectHandlersType = Record void>; +export const selectHandlers: SelectHandlersType = { + [validSelectCustomIdPrefaces.SelectGameForProfile]: async (interaction: SelectMenuInteraction): Promise => { + const userId = interaction.customId.split(":")[1]; + await interaction.update(await buildProfileIntractable(userId, stringToSimpleGameType(interaction.values[0]))); + }, + [validSelectCustomIdPrefaces.SelectChartForSearch]: async ( + interaction: SelectMenuInteraction + ): Promise => { + const interactionValues = interaction.values[0].split(":"); + await interaction.update( + await buildChartEmbed({ + songId: interactionValues[0], + playtype: interactionValues[1], + game: interactionValues[2] + }) + ); + } +}; + export const handleIsSelectMenu = async (interaction: Interaction): Promise => { try { /** Rechecking required to enforce types */ if (interaction.isSelectMenu()) { - /** @TODO Split this out when we add more dropdowns */ - const userId = interaction.customId.split(":")[1]; - await interaction.update( - await buildProfileIntractable(userId, stringToSimpleGameType(interaction.values[0])) - ); + const customIdPrefix = interaction.customId.split(":")[0]; + if (Object.values(validSelectCustomIdPrefaces).includes(customIdPrefix)) { + await selectHandlers[customIdPrefix](interaction); + } } return; } catch (e) { + logger.error(e); logger.error("Failed to handle isSelectMenu interaction"); } }; diff --git a/bot/src/profile/buildProfileEmbed.ts b/bot/src/profile/buildProfileEmbed.ts index 23dcd21b2..6bd5851bc 100644 --- a/bot/src/profile/buildProfileEmbed.ts +++ b/bot/src/profile/buildProfileEmbed.ts @@ -2,6 +2,7 @@ import { InteractionReplyOptions, MessageActionRow, MessageEmbed, MessagePayload import { Game, UserGameStats, IDStrings, PublicUserDocument, UGSRatingsLookup } from "tachi-common"; import { find } from "lodash"; import { LoggerLayers } from "../config"; +import { validSelectCustomIdPrefaces } from "../interactionHandlers/selectMenu/handleIsSelectMenu"; import { TachiServerV1Get } from "../utils/fetch-tachi"; import { createLayeredLogger } from "../utils/logger"; import { @@ -97,7 +98,7 @@ export const buildProfileIntractable = async ( if (data) { const dropdown = new MessageActionRow().addComponents( new MessageSelectMenu() - .setCustomId(`SelectGameForProfile:${userId}`) + .setCustomId(`${validSelectCustomIdPrefaces.SelectGameForProfile}:${userId}`) .setPlaceholder("Browse By Game") .addOptions( data.map((_game) => { diff --git a/bot/src/slashCommands/register.ts b/bot/src/slashCommands/register.ts index 79a91017f..7d9744578 100644 --- a/bot/src/slashCommands/register.ts +++ b/bot/src/slashCommands/register.ts @@ -3,12 +3,13 @@ import { REST } from "@discordjs/rest"; import { APIApplicationCommandOption } from "discord-api-types"; import { Routes } from "discord-api-types/v9"; import { Client, CommandInteraction } from "discord.js"; +import { searchForChart } from "../chartSearch/chartSearch"; import { getProfileByName } from "../profile/fetch"; import { ProcessEnv } from "../setup"; import { LoggerLayers } from "../config"; import { help } from "../help/help"; import { createLayeredLogger } from "../utils/logger"; -import { gamesToChoicesObject } from "../utils/utils"; +import { gamesToChoicesObject, gamesToGenericChoicesObject } from "../utils/utils"; const logger = createLayeredLogger(LoggerLayers.slashCommands); @@ -37,6 +38,17 @@ export const slashCommands: SlashCommand[] = [ ) .toJSON(), exec: async (interaction: CommandInteraction) => await getProfileByName(interaction) + }, + { + info: new SlashCommandBuilder() + .setName("search") + .setDescription("Search for a song") + .addStringOption((option) => + option.setName("game").setDescription("The Game").setRequired(true).addChoices(gamesToChoicesObject()) + ) + .addStringOption((option) => option.setName("chart").setDescription("The chart name").setRequired(true)) + .toJSON(), + exec: async (interaction: CommandInteraction) => await searchForChart(interaction) } ]; diff --git a/bot/src/utils/utils.ts b/bot/src/utils/utils.ts index 16df3eafd..48042a4aa 100644 --- a/bot/src/utils/utils.ts +++ b/bot/src/utils/utils.ts @@ -1,4 +1,5 @@ import { FormatGame, Game, Playtypes, IDStrings, UGSRatingsLookup, GetGamePTConfig } from "tachi-common"; +import { GetGameConfig } from "tachi-common/js/config/config"; import { ProcessEnv } from "../setup"; import { PrependTachiUrl } from "./fetch-tachi"; @@ -15,6 +16,7 @@ export const stringToSimpleGameType = (gptString: string): SimpleGameType }; export const formatGameWrapper = (gpt: SimpleGameType): string => FormatGame(gpt.game, gpt.playtype); +export const formatGameWrapperSimple = (gpt: Game): string => GetGameConfig(gpt).name; export const capitalise = (s: string): string => (s && s[0].toUpperCase() + s.slice(1)) || ""; @@ -29,6 +31,13 @@ export const getPfpUrl = (userId: number): string => { return PrependTachiUrl(`/users/${userId}/pfp`); }; +export const getGameImage = (gameId: string, game: Game): string => { + if (ProcessEnv.ENV !== "prod") { + return `https://kamaitachi.xyz/static/images/gameicons/${game}/${gameId}.png`; + } + return PrependTachiUrl(`/users/${gameId}/pfp`); +}; + export const gameIdentifierStrings = [ "iidx:SP", "iidx:DP", @@ -44,12 +53,32 @@ export const gameIdentifierStrings = [ "gitadora:Gita", "gitadora:Dora" ]; + +export const simpleGameIdentifierStrings = [ + "iidx", + "museca", + "maimai", + "sdvx", + "ddr", + "bms", + "chunithm", + "gitadora", + "usc" +]; + export const gamesToChoicesObject = (): [name: string, value: string][] => { return gameIdentifierStrings.map((identifier) => { return [formatGameWrapper(stringToSimpleGameType(identifier)), identifier]; }); }; +/** @description excludes play-types from options */ +export const gamesToGenericChoicesObject = (): [name: string, value: string][] => { + return simpleGameIdentifierStrings.map((identifier) => { + return [formatGameWrapperSimple(identifier), identifier]; + }); +}; + export const formatGameScoreRating = ( gpt: SimpleGameType, ratingName: UGSRatingsLookup[I], From 50545284ed80fc4c791320c47b9d159e11e32321 Mon Sep 17 00:00:00 2001 From: Pfych Date: Sat, 25 Sep 2021 14:43:46 +1000 Subject: [PATCH 2/8] Refactor chart > song --- bot/src/chartSearch/buildChartEmbed.ts | 17 +++--- bot/src/chartSearch/chartSearch.ts | 56 +++++++++---------- .../selectMenu/handleIsSelectMenu.ts | 4 +- bot/src/slashCommands/register.ts | 8 +-- 4 files changed, 43 insertions(+), 42 deletions(-) diff --git a/bot/src/chartSearch/buildChartEmbed.ts b/bot/src/chartSearch/buildChartEmbed.ts index 2e3310e70..376018820 100644 --- a/bot/src/chartSearch/buildChartEmbed.ts +++ b/bot/src/chartSearch/buildChartEmbed.ts @@ -2,19 +2,20 @@ import { InteractionReplyOptions, MessageActionRow, MessageEmbed, MessagePayload import { Game, Playtypes } from "tachi-common"; import { LoggerLayers } from "../config"; import { validSelectCustomIdPrefaces } from "../interactionHandlers/selectMenu/handleIsSelectMenu"; +import { TachiServerV1Get } from "../utils/fetch-tachi"; import { createLayeredLogger } from "../utils/logger"; import { getGameImage } from "../utils/utils"; -import { getDetailedChartData, SongSearchResult } from "./chartSearch"; +import { getDetailedSongData, SongSearchResult } from "./chartSearch"; const logger = createLayeredLogger(LoggerLayers.buildChartEmbed); -export const buildChartsSelect = (charts: SongSearchResult[], playtype: Playtypes[T], game: Game) => { +export const buildSongSelect = (songs: SongSearchResult[], playtype: Playtypes[T], game: Game) => { return new MessageActionRow().addComponents( new MessageSelectMenu() - .setCustomId(validSelectCustomIdPrefaces.SelectChartForSearch) - .setPlaceholder("Select Chart") + .setCustomId(validSelectCustomIdPrefaces.selectSongForSearch) + .setPlaceholder("Select Song") .addOptions( - charts.map((chart) => { + songs.map((chart) => { return { label: `${chart.title} - ${chart.artist}`, value: `${chart.id}:${playtype}:${game}` @@ -37,10 +38,10 @@ export const buildChartEmbed = async (args: { if (!songId && searchResults) { embed.addField(`${searchResults.length} potential results found`, "Select from the dropdown"); - return { embeds: [embed], components: [buildChartsSelect(searchResults, playtype, game)] }; + return { embeds: [embed], components: [buildSongSelect(searchResults, playtype, game)] }; } else if (songId) { - const details = await getDetailedChartData(songId, playtype, game); - embed.addField(details.song.title || "Chart", details.song.artist || "Artist"); + const details = await getDetailedSongData(songId, playtype, game); + embed.addField(details.song.title || "Song", details.song.artist || "Artist"); embed.setThumbnail(getGameImage(details.song.firstVersion || "0", game)); const sortedCharts = details.charts.sort((a, b) => a.levelNum - b.levelNum); diff --git a/bot/src/chartSearch/chartSearch.ts b/bot/src/chartSearch/chartSearch.ts index 5ac2a80f0..9e92a65b4 100644 --- a/bot/src/chartSearch/chartSearch.ts +++ b/bot/src/chartSearch/chartSearch.ts @@ -24,21 +24,21 @@ export interface DetailedSongResponse { charts: ChartDocument[]; } -export const getChartMeta = async (chartName: string): Promise => { +export const getSongMeta = async (songName: string): Promise => { try { - logger.info(`Searching for ${chartName}`); - const chartResponse = ( + logger.info(`Searching for ${songName}`); + const songResponse = ( await TachiServerV1Get("/search", { - search: chartName + search: songName }) ).body?.songs; - if (chartResponse && chartResponse.length > 0) { - logger.info(`${chartResponse.length} values found`); - return chartResponse; + if (songResponse && songResponse.length > 0) { + logger.info(`${songResponse.length} values found`); + return songResponse; } else { - logger.error(`No results found for ${chartName}`); - throw new Error(`No results found for ${chartName}`); + logger.error(`No results found for ${songName}`); + throw new Error(`No results found for ${songName}`); } } catch (e) { logger.error(e); @@ -46,32 +46,32 @@ export const getChartMeta = async (chartName: string): Promise => { +export const getSongMetaFiltered = async (chartName: string, game: Game): Promise => { try { - const chartMeta = await getChartMeta(chartName); - const chartMetaFiltered = chartMeta.filter((chart) => chart.game === game); - if (chartMetaFiltered.length === 0) { - logger.info("No charts found"); + const songMeta = await getSongMeta(chartName); + const songMetaFiltered = songMeta.filter((chart) => chart.game === game); + if (songMetaFiltered.length === 0) { + logger.info("No songs found"); - throw new Error("No charts found"); + throw new Error("No songs found"); } - return chartMetaFiltered; + return songMetaFiltered; } catch (e) { logger.info(e); - throw new Error("No charts found"); + throw new Error("No songs found"); } }; -export const getDetailedChartData = async ( - chartId: string, +export const getDetailedSongData = async ( + songId: string, playtype: Playtypes[T], game: Game ): Promise => { try { - logger.info(`Getting detailed info for ${chartId} (${game}:${playtype})`); - const data = await TachiServerV1Get(`/games/${game}/${playtype}/songs/${chartId}`); + logger.info(`Getting detailed info for ${songId} (${game}:${playtype})`); + const data = await TachiServerV1Get(`/games/${game}/${playtype}/songs/${songId}`); if (data.success) { return data.body; } else { @@ -80,24 +80,24 @@ export const getDetailedChartData = async ( } } catch (e) { logger.info(e); - throw new Error("Unable to fetch detailed chart data"); + throw new Error("Unable to fetch detailed song data"); } }; -export const searchForChart = async (interaction: CommandInteraction): Promise => { - const chartName = interaction.options.getString("chart", true); +export const searchForSong = async (interaction: CommandInteraction): Promise => { + const songName = interaction.options.getString("song", true); const gpt = stringToSimpleGameType(interaction.options.getString("game", true)); const game = gpt.game; const playtype = gpt.playtype; try { - logger.info(`Requested ${chartName} in ${game}`); + logger.info(`Requested ${songName} in ${game}`); - const chartMetaFiltered = await getChartMetaFiltered(chartName, game); + const songMetaFiltered = await getSongMetaFiltered(songName, game); - await interaction.reply(await buildChartEmbed({ searchResults: chartMetaFiltered, playtype, game })); + await interaction.reply(await buildChartEmbed({ searchResults: songMetaFiltered, playtype, game })); } catch (e) { - await interaction.reply(`No results found for ${chartName}`); + await interaction.reply(`No results found for ${songName}`); logger.error(e); } }; diff --git a/bot/src/interactionHandlers/selectMenu/handleIsSelectMenu.ts b/bot/src/interactionHandlers/selectMenu/handleIsSelectMenu.ts index 826bece89..bb7b0d87d 100644 --- a/bot/src/interactionHandlers/selectMenu/handleIsSelectMenu.ts +++ b/bot/src/interactionHandlers/selectMenu/handleIsSelectMenu.ts @@ -9,7 +9,7 @@ import { stringToSimpleGameType } from "../../utils/utils"; const logger = createLayeredLogger(LoggerLayers.selectInteractionHandler); export enum validSelectCustomIdPrefaces { - SelectChartForSearch = "SelectChartForSearch", + selectSongForSearch = "selectSongForSearch", SelectGameForProfile = "SelectGameForProfile" } @@ -19,7 +19,7 @@ export const selectHandlers: SelectHandlersType = { const userId = interaction.customId.split(":")[1]; await interaction.update(await buildProfileIntractable(userId, stringToSimpleGameType(interaction.values[0]))); }, - [validSelectCustomIdPrefaces.SelectChartForSearch]: async ( + [validSelectCustomIdPrefaces.selectSongForSearch]: async ( interaction: SelectMenuInteraction ): Promise => { const interactionValues = interaction.values[0].split(":"); diff --git a/bot/src/slashCommands/register.ts b/bot/src/slashCommands/register.ts index 7d9744578..b76029d2b 100644 --- a/bot/src/slashCommands/register.ts +++ b/bot/src/slashCommands/register.ts @@ -3,13 +3,13 @@ import { REST } from "@discordjs/rest"; import { APIApplicationCommandOption } from "discord-api-types"; import { Routes } from "discord-api-types/v9"; import { Client, CommandInteraction } from "discord.js"; -import { searchForChart } from "../chartSearch/chartSearch"; +import { searchForSong } from "../chartSearch/chartSearch"; import { getProfileByName } from "../profile/fetch"; import { ProcessEnv } from "../setup"; import { LoggerLayers } from "../config"; import { help } from "../help/help"; import { createLayeredLogger } from "../utils/logger"; -import { gamesToChoicesObject, gamesToGenericChoicesObject } from "../utils/utils"; +import { gamesToChoicesObject } from "../utils/utils"; const logger = createLayeredLogger(LoggerLayers.slashCommands); @@ -46,9 +46,9 @@ export const slashCommands: SlashCommand[] = [ .addStringOption((option) => option.setName("game").setDescription("The Game").setRequired(true).addChoices(gamesToChoicesObject()) ) - .addStringOption((option) => option.setName("chart").setDescription("The chart name").setRequired(true)) + .addStringOption((option) => option.setName("song").setDescription("The song name").setRequired(true)) .toJSON(), - exec: async (interaction: CommandInteraction) => await searchForChart(interaction) + exec: async (interaction: CommandInteraction) => await searchForSong(interaction) } ]; From d428eddec6fc7a2a39abfcba8d9215939642479c Mon Sep 17 00:00:00 2001 From: Pfych Date: Sat, 25 Sep 2021 15:20:51 +1000 Subject: [PATCH 3/8] Added top score details --- bot/src/chartSearch/buildChartEmbed.ts | 74 +++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/bot/src/chartSearch/buildChartEmbed.ts b/bot/src/chartSearch/buildChartEmbed.ts index 376018820..31461b4a4 100644 --- a/bot/src/chartSearch/buildChartEmbed.ts +++ b/bot/src/chartSearch/buildChartEmbed.ts @@ -1,14 +1,20 @@ import { InteractionReplyOptions, MessageActionRow, MessageEmbed, MessagePayload, MessageSelectMenu } from "discord.js"; -import { Game, Playtypes } from "tachi-common"; +import { Game, IDStrings, Playtypes, PublicUserDocument, UGSRatingsLookup } from "tachi-common"; +import { PBScoreDocument } from "tachi-common/js/types"; import { LoggerLayers } from "../config"; import { validSelectCustomIdPrefaces } from "../interactionHandlers/selectMenu/handleIsSelectMenu"; import { TachiServerV1Get } from "../utils/fetch-tachi"; import { createLayeredLogger } from "../utils/logger"; -import { getGameImage } from "../utils/utils"; +import { formatGameScoreRating, getGameImage } from "../utils/utils"; import { getDetailedSongData, SongSearchResult } from "./chartSearch"; const logger = createLayeredLogger(LoggerLayers.buildChartEmbed); +export interface PBResponse { + users: PublicUserDocument[]; + pbs: PBScoreDocument[]; +} + export const buildSongSelect = (songs: SongSearchResult[], playtype: Playtypes[T], game: Game) => { return new MessageActionRow().addComponents( new MessageSelectMenu() @@ -25,7 +31,42 @@ export const buildSongSelect = (songs: SongSearchResult[], playt ); }; -export const buildChartEmbed = async (args: { +interface SimplePBDocument + extends Pick, + Pick, + Partial {} + +export const getPBForChart = async ( + chartId: string, + playtype: Playtypes[T], + game: Game +): Promise => { + try { + const data = await TachiServerV1Get(`/games/${game}/${playtype}/charts/${chartId}/pbs`); + if (data.success) { + const pbs = data.body.pbs; + const users = data.body.users; + + const topPb = pbs[0]; + const topUser = users.find((user) => user.id === topPb.userID); + + return { + ...topUser, + scoreData: topPb.scoreData, + calculatedData: topPb.calculatedData + }; + } else { + logger.error(data.description); + throw new Error(data.description); + } + } catch (e) { + logger.info(e); + + throw new Error("Unable to fetch PBs for chart"); + } +}; + +export const buildChartEmbed = async (args: { searchResults?: SongSearchResult[]; songId?: string; playtype: Playtypes[T]; @@ -46,9 +87,30 @@ export const buildChartEmbed = async (args: { const sortedCharts = details.charts.sort((a, b) => a.levelNum - b.levelNum); - sortedCharts.forEach((chart) => { - embed.addField(`${chart.difficulty} (${chart.level})`, "Parse relevant data here", true); - }); + for (const chart of sortedCharts) { + try { + const PB = await getPBForChart(chart.chartID, chart.playtype, game); + embed.addField( + `${chart.difficulty} (${chart.level})`, + `Server Top: **[${PB.username}](https://kamaitachi.xyz/dashboard/profiles/${ + PB.id + }/games/${game}?playtype=${playtype})**\n${PB.scoreData.percent.toFixed(2)}% [${ + PB.scoreData.score + }]\n${PB.scoreData.grade} [${PB.scoreData.lamp}]\n${Object.keys(PB.calculatedData) + .map((item) => { + return `${item}: ${formatGameScoreRating( + { game, playtype }, + item, + PB.calculatedData[item as never] || 0 + )}`; + }) + .join("\n")}`, + false + ); + } catch { + embed.addField(`${chart.difficulty} (${chart.level})`, "No Scores Available!"); + } + } return { embeds: [embed] }; } else { From 76df51f59fec093c19bd7686fc87391cdb3ce6ed Mon Sep 17 00:00:00 2001 From: Pfych Date: Sat, 25 Sep 2021 15:42:45 +1000 Subject: [PATCH 4/8] Less repeated logging --- bot/src/chartSearch/buildChartEmbed.ts | 2 +- bot/src/chartSearch/chartSearch.ts | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/bot/src/chartSearch/buildChartEmbed.ts b/bot/src/chartSearch/buildChartEmbed.ts index 31461b4a4..bf942d535 100644 --- a/bot/src/chartSearch/buildChartEmbed.ts +++ b/bot/src/chartSearch/buildChartEmbed.ts @@ -60,7 +60,7 @@ export const getPBForChart = async ( throw new Error(data.description); } } catch (e) { - logger.info(e); + logger.error(e); throw new Error("Unable to fetch PBs for chart"); } diff --git a/bot/src/chartSearch/chartSearch.ts b/bot/src/chartSearch/chartSearch.ts index 9e92a65b4..c6f6fc747 100644 --- a/bot/src/chartSearch/chartSearch.ts +++ b/bot/src/chartSearch/chartSearch.ts @@ -51,8 +51,6 @@ export const getSongMetaFiltered = async (chartName: string, game: Game): Promis const songMeta = await getSongMeta(chartName); const songMetaFiltered = songMeta.filter((chart) => chart.game === game); if (songMetaFiltered.length === 0) { - logger.info("No songs found"); - throw new Error("No songs found"); } From 6d1f2f9f1bf86bbcb5f3ba3f8ea1ff74ddf6601c Mon Sep 17 00:00:00 2001 From: Pfych Date: Sat, 2 Oct 2021 10:34:24 +1000 Subject: [PATCH 5/8] Fix SimplePBDocument interface --- bot/src/chartSearch/buildChartEmbed.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/bot/src/chartSearch/buildChartEmbed.ts b/bot/src/chartSearch/buildChartEmbed.ts index bf942d535..ac88bdf14 100644 --- a/bot/src/chartSearch/buildChartEmbed.ts +++ b/bot/src/chartSearch/buildChartEmbed.ts @@ -31,10 +31,7 @@ export const buildSongSelect = (songs: SongSearchResult[], playt ); }; -interface SimplePBDocument - extends Pick, - Pick, - Partial {} +interface SimplePBDocument extends PBScoreDocument, Partial {} export const getPBForChart = async ( chartId: string, @@ -51,9 +48,8 @@ export const getPBForChart = async ( const topUser = users.find((user) => user.id === topPb.userID); return { - ...topUser, - scoreData: topPb.scoreData, - calculatedData: topPb.calculatedData + ...topPb, + ...topUser }; } else { logger.error(data.description); From cecbdd43ccf21cfb18cacef5943b3b63b2ee2423 Mon Sep 17 00:00:00 2001 From: Pfych Date: Sat, 2 Oct 2021 10:34:37 +1000 Subject: [PATCH 6/8] Add prod cdn todo --- bot/src/utils/utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/src/utils/utils.ts b/bot/src/utils/utils.ts index 48042a4aa..2aa0c591c 100644 --- a/bot/src/utils/utils.ts +++ b/bot/src/utils/utils.ts @@ -35,6 +35,7 @@ export const getGameImage = (gameId: string, game: Game): string => { if (ProcessEnv.ENV !== "prod") { return `https://kamaitachi.xyz/static/images/gameicons/${game}/${gameId}.png`; } + /** @TODO Prod CDN for game images */ return PrependTachiUrl(`/users/${gameId}/pfp`); }; From 2d3f75ff297ee05d637abdac5fd072398ebd91f8 Mon Sep 17 00:00:00 2001 From: Pfych Date: Sat, 2 Oct 2021 10:54:48 +1000 Subject: [PATCH 7/8] Updated buildChartEmbed to filter 2dextra out --- bot/src/chartSearch/buildChartEmbed.ts | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/bot/src/chartSearch/buildChartEmbed.ts b/bot/src/chartSearch/buildChartEmbed.ts index ac88bdf14..489f763f0 100644 --- a/bot/src/chartSearch/buildChartEmbed.ts +++ b/bot/src/chartSearch/buildChartEmbed.ts @@ -67,10 +67,11 @@ export const buildChartEmbed = async => { try { - const { searchResults, songId, playtype, game } = args; - + const { searchResults, songId, playtype, game, IIDExtra } = args; const embed = new MessageEmbed().setColor("#cc527a"); if (!songId && searchResults) { @@ -81,18 +82,29 @@ export const buildChartEmbed = async a.levelNum - b.levelNum); + const filteredCharts = details.charts.filter((chart) => { + if (IIDExtra) { + return true; + } else { + if (chart.data && !(chart.data as any)?.["2dxtraSet"]) { + return true; + } + } + + return false; + }); + const sortedCharts = filteredCharts.sort((a, b) => a.levelNum - b.levelNum); for (const chart of sortedCharts) { try { const PB = await getPBForChart(chart.chartID, chart.playtype, game); embed.addField( `${chart.difficulty} (${chart.level})`, - `Server Top: **[${PB.username}](https://kamaitachi.xyz/dashboard/profiles/${ + `Server Top: **[${PB.username}](https://kamaitachi.xyz/dashboard/users/${ PB.id - }/games/${game}?playtype=${playtype})**\n${PB.scoreData.percent.toFixed(2)}% [${ - PB.scoreData.score - }]\n${PB.scoreData.grade} [${PB.scoreData.lamp}]\n${Object.keys(PB.calculatedData) + }/games/${game}/${playtype})**\n${PB.scoreData.percent.toFixed(2)}% [${PB.scoreData.score}]\n${ + PB.scoreData.grade + } [${PB.scoreData.lamp}]\n${Object.keys(PB.calculatedData) .map((item) => { return `${item}: ${formatGameScoreRating( { game, playtype }, From 0d8528a1ad3d2c4b2c0cba03844bb6d1f601425e Mon Sep 17 00:00:00 2001 From: Pfych Date: Sat, 2 Oct 2021 10:56:52 +1000 Subject: [PATCH 8/8] Fixed imports --- bot/src/utils/utils.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot/src/utils/utils.ts b/bot/src/utils/utils.ts index 2aa0c591c..faf207471 100644 --- a/bot/src/utils/utils.ts +++ b/bot/src/utils/utils.ts @@ -1,5 +1,4 @@ -import { FormatGame, Game, Playtypes, IDStrings, UGSRatingsLookup, GetGamePTConfig } from "tachi-common"; -import { GetGameConfig } from "tachi-common/js/config/config"; +import { FormatGame, Game, Playtypes, IDStrings, UGSRatingsLookup, GetGamePTConfig, GetGameConfig } from "tachi-common"; import { ProcessEnv } from "../setup"; import { PrependTachiUrl } from "./fetch-tachi";