From d6118de14ed9cf3f0058bef9b55e8e0407f4ec00 Mon Sep 17 00:00:00 2001 From: Pfych Date: Wed, 15 Sep 2021 19:40:53 +1000 Subject: [PATCH 01/17] [WIP] Added boilerplate for profile view --- bot/.gitignore | 1 + bot/package.json | 4 +- bot/src/config.ts | 3 +- bot/src/index.ts | 25 +++++++++-- bot/src/profile/buildProfileEmbed.ts | 65 ++++++++++++++++++++++++++++ bot/src/profile/fetch.ts | 28 ++++++++++++ bot/src/setup.ts | 20 ++++++--- bot/src/slashCommands/register.ts | 9 ++++ bot/src/utils/fetch-tachi.ts | 28 ++++++++---- bot/yarn.lock | 44 ++++++++----------- 10 files changed, 179 insertions(+), 48 deletions(-) create mode 100644 bot/src/profile/buildProfileEmbed.ts create mode 100644 bot/src/profile/fetch.ts diff --git a/bot/.gitignore b/bot/.gitignore index fc8f7df0d..5e83af438 100644 --- a/bot/.gitignore +++ b/bot/.gitignore @@ -3,3 +3,4 @@ node_modules .vscode yarn-error.log .env +conf.json5 diff --git a/bot/package.json b/bot/package.json index d53eb745d..279d635cc 100644 --- a/bot/package.json +++ b/bot/package.json @@ -12,6 +12,7 @@ "@types/express-serve-static-core": "^4.17.24", "@types/jest": "^27.0.1", "@types/json5": "^2.2.0", + "@types/lodash": "^4.14.172", "@types/node": "^16.7.10", "@typescript-eslint/eslint-plugin": "^4.29.1", "@typescript-eslint/parser": "^4.29.1", @@ -24,12 +25,13 @@ "dependencies": { "@discordjs/builders": "^0.5.0", "@discordjs/rest": "^0.1.0-canary.0", + "axios": "^0.21.4", "deezer-api-ts": "^1.2.1", "discord.js": "^13.1.0", "dotenv": "^10.0.0", "express": "^4.17.1", "json5": "^2.2.0", - "node-fetch": "^3.0.0", + "lodash": "^4.17.21", "prudence": "^0.9.6", "songlink-api": "^1.0.0", "tachi-common": "^0.2.1", diff --git a/bot/src/config.ts b/bot/src/config.ts index a4e67a8a9..03865f7bb 100644 --- a/bot/src/config.ts +++ b/bot/src/config.ts @@ -8,5 +8,6 @@ export enum LoggerLayers { server = "server", serverAuth = "serverAuth", botConfigSetup = "botConfigSetup", - tachiFetch = "tachiFetch" + tachiFetch = "tachiFetch", + profile = "profile" } diff --git a/bot/src/index.ts b/bot/src/index.ts index c172ad4d1..10a1df701 100644 --- a/bot/src/index.ts +++ b/bot/src/index.ts @@ -1,10 +1,12 @@ import { Client, Intents } from "discord.js"; +import { UserGameStats } from "tachi-common"; +import { buildProfileIntractable } from "./profile/buildProfileEmbed"; import { ProcessEnv } from "./setup"; import { LoggerLayers } from "./config"; import { registerSlashCommands, slashCommands, SlashCommand } from "./slashCommands/register"; +import { TachiServerV1Get } from "./utils/fetch-tachi"; import { createLayeredLogger } from "./utils/logger"; - const logger = createLayeredLogger(LoggerLayers.client); const client = new Client({ @@ -12,9 +14,22 @@ const client = new Client({ }); client.on("interactionCreate", async (interaction) => { - if (!interaction.isCommand()) return; - try { + if (interaction.isSelectMenu()) { + /** @TODO Move this to its own handler! + * We only have one intractable but for now this works */ + const userId = interaction.customId.split(":")[1]; + const userData = (await TachiServerV1Get(`/users/${userId}/game-stats`))?.body; + if (userData) { + await interaction.update(buildProfileIntractable(userData, userId, interaction.values[0])); + } else { + throw new Error("Failed to re-fetch user data! This is a bad sign!"); + } + return; + } + + if (!interaction.isCommand()) return; + const command = slashCommands.find((command: SlashCommand) => { return command.info.name === interaction.commandName; }); @@ -38,7 +53,9 @@ client.on("interactionCreate", async (interaction) => { await client.login(ProcessEnv.DISCORD_TOKEN); logger.info(`Logged in successfully to ${client.guilds.cache.size} guilds`); logger.info( - `Invite URL: https://discord.com/api/oauth2/authorize?client_id=${client.application!.id}&permissions=8&scope=applications.commands%20bot` + `Invite URL: https://discord.com/api/oauth2/authorize?client_id=${ + client.application!.id + }&permissions=8&scope=applications.commands%20bot` ); await registerSlashCommands(client); diff --git a/bot/src/profile/buildProfileEmbed.ts b/bot/src/profile/buildProfileEmbed.ts new file mode 100644 index 000000000..68c836b39 --- /dev/null +++ b/bot/src/profile/buildProfileEmbed.ts @@ -0,0 +1,65 @@ +import { InteractionReplyOptions, MessageActionRow, MessageEmbed, MessagePayload, MessageSelectMenu } from "discord.js"; +import { FormatGame, Game, Playtypes, UserGameStats } from "tachi-common"; +import { IDStrings, UGSRatingsLookup } from "tachi-common/js/types"; +import { find } from "lodash"; + +export const buildProfileEmbed = (data: UserGameStats[], game?: string): MessageEmbed => { + const embed = new MessageEmbed().setColor("#cc527a"); + + /** @TODO FETCH USERS ACTUAL DETAILS */ + embed.addField("Foo", "Foo"); + embed.addField("Bar", "Bar"); + embed.addField("Baz", "Baz"); + embed.setThumbnail("https://cdn.mos.cms.futurecdn.net/mrArzwHcNuQbRwbEmuiwdJ.jpg"); + + if (game) { + const specificData: UserGameStats | undefined = find(data, (item) => game === `${item.game}:${item.playtype}`); + if (!specificData) { + throw new Error(`Could not get ratings for requested game: ${game}`); + } else { + const ratings: string[] = []; + Object.keys(specificData.ratings).map((rating) => { + /** @TODO MAKE THIS ITS OWN FUNCTION THIS IS FILTHY */ + ratings.push( + `${rating}: ${ + Math.round( + (specificData.ratings[rating as UGSRatingsLookup[IDStrings]] || 0 + Number.EPSILON) * 100 + ) / 100 + }` + ); + }); + + /** @TODO DONT SPLIT THE STRING HERE HOLY SHIT */ + embed.addField( + `Stats for: ${FormatGame(game.split(":")[0] as Game, game.split(":")[1] as Playtypes[never])}`, + `${ratings.join("\n")}` + ); + } + } else { + embed.addField("Select a game to see stats", "\u200B"); + } + + return embed; +}; + +export const buildProfileIntractable = ( + data: UserGameStats[], + userId: string, + game?: string +): InteractionReplyOptions | MessagePayload => { + const dropdown = new MessageActionRow().addComponents( + new MessageSelectMenu() + .setCustomId(`SelectGameForProfile:${userId}`) + .setPlaceholder("Browse By Game") + .addOptions( + data.map((game) => { + return { + label: `${FormatGame(game.game, game.playtype)}`, + value: `${game.game}:${game.playtype}` + }; + }) + ) + ); + + return { embeds: [buildProfileEmbed(data, game)], components: [dropdown] }; +}; diff --git a/bot/src/profile/fetch.ts b/bot/src/profile/fetch.ts new file mode 100644 index 000000000..c10c23779 --- /dev/null +++ b/bot/src/profile/fetch.ts @@ -0,0 +1,28 @@ +import { CommandInteraction } from "discord.js"; +import { LoggerLayers } from "../config"; +import { BotConfig } from "../setup"; +import { TachiServerV1Get } from "../utils/fetch-tachi"; +import { createLayeredLogger } from "../utils/logger"; +import { UserGameStats } from "tachi-common"; +import { buildProfileIntractable } from "./buildProfileEmbed"; + +const logger = createLayeredLogger(LoggerLayers.profile); + +export const getProfileByName = async (interaction: CommandInteraction): Promise => { + try { + const userId = interaction.options.getString("user", false) || "TODO FALLBACK TO SELF"; + logger.info(`Fetching user ${userId}`); + + const userData = (await TachiServerV1Get(`/users/${userId}/game-stats`))?.body; + logger.verbose(userData); + + if (userData) { + await interaction.reply(buildProfileIntractable(userData, userId)); + } else { + await interaction.reply("No user data found for user"); + } + } catch (e) { + logger.error(e); + await interaction.reply(BotConfig.GENERIC_ERROR_MESSAGE); + } +}; diff --git a/bot/src/setup.ts b/bot/src/setup.ts index d07a31bbd..2754f8724 100644 --- a/bot/src/setup.ts +++ b/bot/src/setup.ts @@ -18,6 +18,7 @@ export interface BotConfig { SERVER_PORT: number; TACHI_SERVER_LOCATION: string; OUR_URL: string; + GENERIC_ERROR_MESSAGE: string; } function ParseBotConfig(fileLoc = "conf.json5"): BotConfig { @@ -36,6 +37,7 @@ function ParseBotConfig(fileLoc = "conf.json5"): BotConfig { SERVER_PORT: Prudence.isPositiveNonZeroInteger, TACHI_SERVER_LOCATION: "string", OUR_URL: "string", + GENERIC_ERROR_MESSAGE: "string" }); if (err) { @@ -55,12 +57,17 @@ export interface ProcessEnvironment { } function ParseEnvVars() { - const err = Prudence(process.env, { - ENV: "string", - DISCORD_TOKEN: "string", - BOT_CLIENT_SECRET: "string", - BOT_CLIENT_ID: "string" - }, {}, { allowExcessKeys: true }); + const err = Prudence( + process.env, + { + ENV: "string", + DISCORD_TOKEN: "string", + BOT_CLIENT_SECRET: "string", + BOT_CLIENT_ID: "string" + }, + {}, + { allowExcessKeys: true } + ); if (err) { logger.error(FormatPrError(err, "Invalid environment. Cannot safely boot.")); @@ -73,5 +80,4 @@ function ParseEnvVars() { export const BotConfig: BotConfig = ParseBotConfig(process.env.CONF_JSON5_LOCATION); - export const ProcessEnv = ParseEnvVars(); diff --git a/bot/src/slashCommands/register.ts b/bot/src/slashCommands/register.ts index cf341743c..073fae8d1 100644 --- a/bot/src/slashCommands/register.ts +++ b/bot/src/slashCommands/register.ts @@ -3,6 +3,7 @@ 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 { getProfileByName } from "../profile/fetch"; import { ProcessEnv } from "../setup"; import { LoggerLayers } from "../config"; import { help } from "../help/help"; @@ -23,6 +24,14 @@ export const slashCommands: SlashCommand[] = [ { info: new SlashCommandBuilder().setName("help").setDescription("Shows information about this bot").toJSON(), exec: async (interaction: CommandInteraction) => await help(interaction) + }, + { + info: new SlashCommandBuilder() + .setName("profile") + .setDescription("Displays a Kamaitachi Profile") + .addStringOption((option) => option.setName("user").setDescription("The users id").setRequired(false)) + .toJSON(), + exec: async (interaction: CommandInteraction) => await getProfileByName(interaction) } ]; diff --git a/bot/src/utils/fetch-tachi.ts b/bot/src/utils/fetch-tachi.ts index 951c07c88..f29641e3c 100644 --- a/bot/src/utils/fetch-tachi.ts +++ b/bot/src/utils/fetch-tachi.ts @@ -1,3 +1,4 @@ +import axios from "axios"; import { LoggerLayers } from "../config"; import { BotConfig } from "../setup"; import { integer, SuccessfulAPIResponse, UnsuccessfulAPIResponse } from "tachi-common"; @@ -5,14 +6,16 @@ import { createLayeredLogger } from "./logger"; const logger = createLayeredLogger(LoggerLayers.tachiFetch); -export type APIResponse = (SuccessfulAPIResponse | UnsuccessfulAPIResponse) & { statusCode: integer }; +export type APIResponse = (SuccessfulAPIResponse | (UnsuccessfulAPIResponse & { body: null })) & { + statusCode: integer; +}; export enum RequestTypes { GET = "GET", POST = "POST", PATCH = "PATCH", PUT = "PUT", - DELETE = "DELETE", + DELETE = "DELETE" // HEAD, OPTIONS not used by tachi-server anywhere. } @@ -24,22 +27,26 @@ export enum RequestTypes { * @param body - Optionally, provide some content for the request body. * @param T - A generic that asserts the type of the response contents. Defaults to unknown. */ -export async function TachiServerV1Request(method: Exclude, url: string, body: unknown = {}): Promise> { +export async function TachiServerV1Request( + method: Exclude, + url: string, + body: unknown = {} +): Promise> { const realUrl = PrependTachiUrl(url, "1"); const loggerUrl = `${method} ${realUrl}`; logger.debug(`Making a request to ${loggerUrl}.`); try { - const res = await fetch(realUrl, { + const res = await axios(realUrl, { method, headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body) + data: JSON.stringify(body) }); - const json = await res.json(); + const json = (await res.data) as APIResponse; const contents = { ...json, statusCode: res.status }; LogRequestResult(loggerUrl, contents); @@ -60,14 +67,17 @@ export async function TachiServerV1Request(method: Exclude(url: string, params: Record = {}): Promise> { +export async function TachiServerV1Get( + url: string, + params: Record = {} +): Promise> { try { const urlParams = new URLSearchParams(params); const realUrl = `${PrependTachiUrl(url, "1")}?${urlParams.toString()}`; - const res = await fetch(realUrl); - const json = await res.json(); + const res = await axios(realUrl, { method: RequestTypes.GET }); + const json = (await res.data) as APIResponse; const contents = { ...json, statusCode: res.status }; LogRequestResult(`GET ${realUrl}`, contents); diff --git a/bot/yarn.lock b/bot/yarn.lock index 4b82bfaeb..d7afb1e68 100644 --- a/bot/yarn.lock +++ b/bot/yarn.lock @@ -862,6 +862,11 @@ dependencies: "@types/node" "*" +"@types/lodash@^4.14.172": + version "4.14.172" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.172.tgz#aad774c28e7bfd7a67de25408e03ee5a8c3d028a" + integrity sha512-/BHF5HAx3em7/KkzVKm3LrsD6HZAXuXO1AJZQ3cRRBZj4oHZDviWPYu0aEplAqDFNHZPW6d3G7KN+ONcCCC7pw== + "@types/mime@^1": version "1.3.2" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" @@ -1282,6 +1287,13 @@ axios@^0.19.0: dependencies: follow-redirects "1.5.10" +axios@^0.21.4: + version "0.21.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" + integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== + dependencies: + follow-redirects "^1.14.0" + babel-jest@^27.1.0: version "27.1.0" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.1.0.tgz#e96ca04554fd32274439869e2b6d24de9d91bc4e" @@ -1846,11 +1858,6 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -data-uri-to-buffer@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz#594b8973938c5bc2c33046535785341abc4f3636" - integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og== - data-urls@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" @@ -2397,13 +2404,6 @@ fecha@^4.2.0: resolved "https://registry.npmjs.org/fecha/-/fecha-4.2.1.tgz" integrity sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q== -fetch-blob@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.1.2.tgz#6bc438675f3851ecea51758ac91f6a1cd1bacabd" - integrity sha512-hunJbvy/6OLjCD0uuhLdp0mMPzP/yd2ssd1t2FCJsaA7wkWhpbp9xfuNVpv7Ll4jFhzp6T4LAupSiV9uOeg0VQ== - dependencies: - web-streams-polyfill "^3.0.3" - file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" @@ -2478,6 +2478,11 @@ follow-redirects@1.5.10: dependencies: debug "=3.1.0" +follow-redirects@^1.14.0: + version "1.14.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.3.tgz#6ada78118d8d24caee595595accdc0ac6abd022e" + integrity sha512-3MkHxknWMUtb23apkgz/83fDoe+y+qr0TdgacGIA7bew+QLBo3vdgEN2xEsuXNivpFy4CyDhBBZnNZOtalmenw== + foreground-child@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-2.0.0.tgz#71b32800c9f15aa8f2f83f4a6bd9bff35d861a53" @@ -3692,7 +3697,7 @@ lodash.truncate@^4.4.2: resolved "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz" integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= -lodash@^4.7.0: +lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -3953,14 +3958,6 @@ node-fetch@^2.6.1: resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== -node-fetch@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.0.0.tgz#79da7146a520036f2c5f644e4a26095f17e411ea" - integrity sha512-bKMI+C7/T/SPU1lKnbQbwxptpCrG9ashG+VkytmXCPZyuM9jB6VU+hY0oi4lC8LxTtAeWdckNCTa3nrGsAdA3Q== - dependencies: - data-uri-to-buffer "^3.0.1" - fetch-blob "^3.1.2" - node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -5365,11 +5362,6 @@ walker@^1.0.7: dependencies: makeerror "1.0.x" -web-streams-polyfill@^3.0.3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.1.0.tgz#86f983b4f44745502b0d8563d9ef3afc609d4465" - integrity sha512-wO9r1YnYe7kFBLHyyVEhV1H8VRWoNiNnuP+v/HUUmSTaRF8F93Kmd3JMrETx0f11GXxRek6OcL2QtjFIdc5WYw== - webidl-conversions@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" From 68d22b1d77df4a582b7847bc7f8703c0567e8ce8 Mon Sep 17 00:00:00 2001 From: Pfych Date: Sat, 18 Sep 2021 13:02:10 +1000 Subject: [PATCH 02/17] Move interaction handlers to their own files --- bot/src/config.ts | 3 +- bot/src/index.ts | 29 ++++--------------- .../command/handleIsCommand.ts | 24 +++++++++++++++ .../selectMenu/handleIsSelectMenu.ts | 26 +++++++++++++++++ 4 files changed, 58 insertions(+), 24 deletions(-) create mode 100644 bot/src/interactionHandlers/command/handleIsCommand.ts create mode 100644 bot/src/interactionHandlers/selectMenu/handleIsSelectMenu.ts diff --git a/bot/src/config.ts b/bot/src/config.ts index 03865f7bb..85fc38e06 100644 --- a/bot/src/config.ts +++ b/bot/src/config.ts @@ -9,5 +9,6 @@ export enum LoggerLayers { serverAuth = "serverAuth", botConfigSetup = "botConfigSetup", tachiFetch = "tachiFetch", - profile = "profile" + profile = "profile", + selectInteractionHandler = "selectInteractionHandler" } diff --git a/bot/src/index.ts b/bot/src/index.ts index 10a1df701..9764c2965 100644 --- a/bot/src/index.ts +++ b/bot/src/index.ts @@ -1,10 +1,9 @@ import { Client, Intents } from "discord.js"; -import { UserGameStats } from "tachi-common"; -import { buildProfileIntractable } from "./profile/buildProfileEmbed"; +import { handleIsCommand } from "./interactionHandlers/command/handleIsCommand"; +import { handleIsSelectMenu } from "./interactionHandlers/selectMenu/handleIsSelectMenu"; import { ProcessEnv } from "./setup"; import { LoggerLayers } from "./config"; -import { registerSlashCommands, slashCommands, SlashCommand } from "./slashCommands/register"; -import { TachiServerV1Get } from "./utils/fetch-tachi"; +import { registerSlashCommands } from "./slashCommands/register"; import { createLayeredLogger } from "./utils/logger"; const logger = createLayeredLogger(LoggerLayers.client); @@ -16,27 +15,11 @@ const client = new Client({ client.on("interactionCreate", async (interaction) => { try { if (interaction.isSelectMenu()) { - /** @TODO Move this to its own handler! - * We only have one intractable but for now this works */ - const userId = interaction.customId.split(":")[1]; - const userData = (await TachiServerV1Get(`/users/${userId}/game-stats`))?.body; - if (userData) { - await interaction.update(buildProfileIntractable(userData, userId, interaction.values[0])); - } else { - throw new Error("Failed to re-fetch user data! This is a bad sign!"); - } - return; + return await handleIsSelectMenu(interaction); } - if (!interaction.isCommand()) return; - - const command = slashCommands.find((command: SlashCommand) => { - return command.info.name === interaction.commandName; - }); - - if (command && command.exec) { - logger.info(`Running ${command.info.name} interaction`); - command.exec(interaction); + if (interaction.isCommand()) { + return await handleIsCommand(interaction); } } catch (e) { logger.error("Failed to run interaction"); diff --git a/bot/src/interactionHandlers/command/handleIsCommand.ts b/bot/src/interactionHandlers/command/handleIsCommand.ts new file mode 100644 index 000000000..a812a0d0f --- /dev/null +++ b/bot/src/interactionHandlers/command/handleIsCommand.ts @@ -0,0 +1,24 @@ +import { Interaction } from "discord.js"; +import { LoggerLayers } from "../../config"; +import { SlashCommand, slashCommands } from "../../slashCommands/register"; +import { createLayeredLogger } from "../../utils/logger"; + +const logger = createLayeredLogger(LoggerLayers.selectInteractionHandler); + +export const handleIsCommand = async (interaction: Interaction): Promise => { + try { + /** Rechecking required to enforce types */ + if (interaction.isCommand()) { + const command = slashCommands.find((command: SlashCommand) => { + return command.info.name === interaction.commandName; + }); + + if (command && command.exec) { + logger.info(`Running ${command.info.name} interaction`); + command.exec(interaction); + } + } + } catch (e) { + logger.error("Failed to handle isCommand interaction"); + } +}; diff --git a/bot/src/interactionHandlers/selectMenu/handleIsSelectMenu.ts b/bot/src/interactionHandlers/selectMenu/handleIsSelectMenu.ts new file mode 100644 index 000000000..08201f8d9 --- /dev/null +++ b/bot/src/interactionHandlers/selectMenu/handleIsSelectMenu.ts @@ -0,0 +1,26 @@ +import { Interaction } from "discord.js"; +import { UserGameStats } from "tachi-common"; +import { LoggerLayers } from "../../config"; +import { buildProfileIntractable } from "../../profile/buildProfileEmbed"; +import { TachiServerV1Get } from "../../utils/fetch-tachi"; +import { createLayeredLogger } from "../../utils/logger"; + +const logger = createLayeredLogger(LoggerLayers.selectInteractionHandler); + +export const handleIsSelectMenu = async (interaction: Interaction): Promise => { + try { + /** Rechecking required to enforce types */ + if (interaction.isSelectMenu()) { + const userId = interaction.customId.split(":")[1]; + const userData = (await TachiServerV1Get(`/users/${userId}/game-stats`))?.body; + if (userData) { + await interaction.update(buildProfileIntractable(userData, userId, interaction.values[0])); + } else { + throw new Error("Failed to re-fetch user data! This is a bad sign!"); + } + } + return; + } catch (e) { + logger.error("Failed to handle isSelectMenu interaction"); + } +}; From bc8a4fa143e3e7da394ed57f34ec203f98951211 Mon Sep 17 00:00:00 2001 From: Pfych Date: Sat, 18 Sep 2021 13:49:43 +1000 Subject: [PATCH 03/17] Tidied select code --- bot/.eslintrc.json | 5 +- bot/src/config.ts | 4 +- .../selectMenu/handleIsSelectMenu.ts | 12 +-- bot/src/profile/buildProfileEmbed.ts | 98 +++++++++++-------- bot/src/profile/fetch.ts | 9 +- bot/src/utils/utils.ts | 35 +++++++ 6 files changed, 105 insertions(+), 58 deletions(-) create mode 100644 bot/src/utils/utils.ts diff --git a/bot/.eslintrc.json b/bot/.eslintrc.json index 5cb359029..56c038c78 100644 --- a/bot/.eslintrc.json +++ b/bot/.eslintrc.json @@ -18,7 +18,10 @@ "rules": { "indent": [ "error", - "tab" + "tab", + { + "SwitchCase": 1 + } ], "linebreak-style": [ "error", diff --git a/bot/src/config.ts b/bot/src/config.ts index 85fc38e06..86bf3e1c4 100644 --- a/bot/src/config.ts +++ b/bot/src/config.ts @@ -1,5 +1,4 @@ /** @TODO Potentially re-work this to only run on PROD env, handy for DEV env! **/ -/** @deprecated replace with database call & cache */ export const allowedChannels = ["876321394005254214"]; export enum LoggerLayers { @@ -10,5 +9,6 @@ export enum LoggerLayers { botConfigSetup = "botConfigSetup", tachiFetch = "tachiFetch", profile = "profile", - selectInteractionHandler = "selectInteractionHandler" + selectInteractionHandler = "selectInteractionHandler", + buildProfileEmbed = "buildProfileEmbed" } diff --git a/bot/src/interactionHandlers/selectMenu/handleIsSelectMenu.ts b/bot/src/interactionHandlers/selectMenu/handleIsSelectMenu.ts index 08201f8d9..0f35e745b 100644 --- a/bot/src/interactionHandlers/selectMenu/handleIsSelectMenu.ts +++ b/bot/src/interactionHandlers/selectMenu/handleIsSelectMenu.ts @@ -1,9 +1,8 @@ import { Interaction } from "discord.js"; -import { UserGameStats } from "tachi-common"; import { LoggerLayers } from "../../config"; import { buildProfileIntractable } from "../../profile/buildProfileEmbed"; -import { TachiServerV1Get } from "../../utils/fetch-tachi"; import { createLayeredLogger } from "../../utils/logger"; +import { stringToSimpleGameType } from "../../utils/utils"; const logger = createLayeredLogger(LoggerLayers.selectInteractionHandler); @@ -12,12 +11,9 @@ export const handleIsSelectMenu = async (interaction: Interaction): Promise(`/users/${userId}/game-stats`))?.body; - if (userData) { - await interaction.update(buildProfileIntractable(userData, userId, interaction.values[0])); - } else { - throw new Error("Failed to re-fetch user data! This is a bad sign!"); - } + await interaction.update( + await buildProfileIntractable(userId, stringToSimpleGameType(interaction.values[0])) + ); } return; } catch (e) { diff --git a/bot/src/profile/buildProfileEmbed.ts b/bot/src/profile/buildProfileEmbed.ts index 68c836b39..9eadf151f 100644 --- a/bot/src/profile/buildProfileEmbed.ts +++ b/bot/src/profile/buildProfileEmbed.ts @@ -1,9 +1,29 @@ import { InteractionReplyOptions, MessageActionRow, MessageEmbed, MessagePayload, MessageSelectMenu } from "discord.js"; -import { FormatGame, Game, Playtypes, UserGameStats } from "tachi-common"; +import { Game, UserGameStats } from "tachi-common"; import { IDStrings, UGSRatingsLookup } from "tachi-common/js/types"; import { find } from "lodash"; +import { LoggerLayers } from "../config"; +import { TachiServerV1Get } from "../utils/fetch-tachi"; +import { createLayeredLogger } from "../utils/logger"; +import { formatGameWrapper, prettyRatingString, SimpleGameType, simpleGameTypeToString } from "../utils/utils"; -export const buildProfileEmbed = (data: UserGameStats[], game?: string): MessageEmbed => { +const logger = createLayeredLogger(LoggerLayers.buildProfileEmbed); + +const pullRatings = ( + ratings: Partial> +): string[] => { + const allRatings: string[] = []; + const RatingKeys = Object.keys(ratings); + + RatingKeys.map((rating) => { + const ratingValue: number = ratings[rating] || 0; + allRatings.push(`${prettyRatingString(rating)}: ${Math.round((ratingValue + Number.EPSILON) * 100) / 100}`); + }); + + return allRatings; +}; + +export const buildProfileEmbed = (data: UserGameStats[], game?: SimpleGameType): MessageEmbed => { const embed = new MessageEmbed().setColor("#cc527a"); /** @TODO FETCH USERS ACTUAL DETAILS */ @@ -13,27 +33,14 @@ export const buildProfileEmbed = (data: UserGameStats[], game?: string): Message embed.setThumbnail("https://cdn.mos.cms.futurecdn.net/mrArzwHcNuQbRwbEmuiwdJ.jpg"); if (game) { - const specificData: UserGameStats | undefined = find(data, (item) => game === `${item.game}:${item.playtype}`); - if (!specificData) { - throw new Error(`Could not get ratings for requested game: ${game}`); + const specificData: UserGameStats | undefined = find( + data, + (item) => simpleGameTypeToString(game) === `${item.game}:${item.playtype}` + ); + if (specificData) { + embed.addField(`Stats for: ${formatGameWrapper(game)}`, `${pullRatings(specificData.ratings).join("\n")}`); } else { - const ratings: string[] = []; - Object.keys(specificData.ratings).map((rating) => { - /** @TODO MAKE THIS ITS OWN FUNCTION THIS IS FILTHY */ - ratings.push( - `${rating}: ${ - Math.round( - (specificData.ratings[rating as UGSRatingsLookup[IDStrings]] || 0 + Number.EPSILON) * 100 - ) / 100 - }` - ); - }); - - /** @TODO DONT SPLIT THE STRING HERE HOLY SHIT */ - embed.addField( - `Stats for: ${FormatGame(game.split(":")[0] as Game, game.split(":")[1] as Playtypes[never])}`, - `${ratings.join("\n")}` - ); + throw new Error(`Could not get ratings for requested game: ${game}`); } } else { embed.addField("Select a game to see stats", "\u200B"); @@ -42,24 +49,35 @@ export const buildProfileEmbed = (data: UserGameStats[], game?: string): Message return embed; }; -export const buildProfileIntractable = ( - data: UserGameStats[], +export const buildProfileIntractable = async ( userId: string, - game?: string -): InteractionReplyOptions | MessagePayload => { - const dropdown = new MessageActionRow().addComponents( - new MessageSelectMenu() - .setCustomId(`SelectGameForProfile:${userId}`) - .setPlaceholder("Browse By Game") - .addOptions( - data.map((game) => { - return { - label: `${FormatGame(game.game, game.playtype)}`, - value: `${game.game}:${game.playtype}` - }; - }) - ) - ); + game?: SimpleGameType +): Promise => { + try { + const data = (await TachiServerV1Get(`/users/${userId}/game-stats`))?.body; + logger.verbose(data); - return { embeds: [buildProfileEmbed(data, game)], components: [dropdown] }; + if (data) { + const dropdown = new MessageActionRow().addComponents( + new MessageSelectMenu() + .setCustomId(`SelectGameForProfile:${userId}`) + .setPlaceholder("Browse By Game") + .addOptions( + data.map((game) => { + return { + label: `${formatGameWrapper(game)}`, + value: `${game.game}:${game.playtype}` + }; + }) + ) + ); + + return { embeds: [buildProfileEmbed(data, game)], components: [dropdown] }; + } else { + throw new Error(`No data found for user ${userId}`); + } + } catch (e) { + logger.error("Unable to build profile interactable"); + throw new Error("Unable to build profile interactable"); + } }; diff --git a/bot/src/profile/fetch.ts b/bot/src/profile/fetch.ts index c10c23779..64de84302 100644 --- a/bot/src/profile/fetch.ts +++ b/bot/src/profile/fetch.ts @@ -1,9 +1,7 @@ import { CommandInteraction } from "discord.js"; import { LoggerLayers } from "../config"; import { BotConfig } from "../setup"; -import { TachiServerV1Get } from "../utils/fetch-tachi"; import { createLayeredLogger } from "../utils/logger"; -import { UserGameStats } from "tachi-common"; import { buildProfileIntractable } from "./buildProfileEmbed"; const logger = createLayeredLogger(LoggerLayers.profile); @@ -13,11 +11,8 @@ export const getProfileByName = async (interaction: CommandInteraction): Promise const userId = interaction.options.getString("user", false) || "TODO FALLBACK TO SELF"; logger.info(`Fetching user ${userId}`); - const userData = (await TachiServerV1Get(`/users/${userId}/game-stats`))?.body; - logger.verbose(userData); - - if (userData) { - await interaction.reply(buildProfileIntractable(userData, userId)); + if (userId) { + await interaction.reply(await buildProfileIntractable(userId)); } else { await interaction.reply("No user data found for user"); } diff --git a/bot/src/utils/utils.ts b/bot/src/utils/utils.ts new file mode 100644 index 000000000..b9e0b70e3 --- /dev/null +++ b/bot/src/utils/utils.ts @@ -0,0 +1,35 @@ +import { FormatGame, Game, Playtypes } from "tachi-common"; +import { IDStrings, UGSRatingsLookup } from "tachi-common/js/types"; + +export interface SimpleGameType { + game: T; + playtype: Playtypes[T]; +} + +export const simpleGameTypeToString = (game: SimpleGameType): IDStrings => + `${game.game}:${game.playtype}`; + +export const stringToSimpleGameType = (game: string): SimpleGameType => { + return { game: game.split(":")[0], playtype: game.split(":")[1] }; +}; + +export const formatGameWrapper = (game: SimpleGameType): string => FormatGame(game.game, game.playtype); + +export const prettyRatingString = (rating: UGSRatingsLookup[I]): string => { + switch (rating) { + case "ktRating": + return "Kamai Rating"; + case "ktLampRating": + return "Kamai Lamp Rating"; + case "sieglinde": + return "???"; + case "VF6": + return "Volforce"; + case "BPI": + return "BPI"; + case "MFCP": + return "???"; + default: + return "Skill"; + } +}; From 7a7ed4194006d898ff67df7ebe5e918b075c41bc Mon Sep 17 00:00:00 2001 From: Pfych Date: Sat, 18 Sep 2021 14:08:41 +1000 Subject: [PATCH 04/17] Fetch users actual details --- bot/src/profile/buildProfileEmbed.ts | 42 ++++++++++++++++++++++------ bot/src/utils/utils.ts | 13 +++++++-- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/bot/src/profile/buildProfileEmbed.ts b/bot/src/profile/buildProfileEmbed.ts index 9eadf151f..1877aeac0 100644 --- a/bot/src/profile/buildProfileEmbed.ts +++ b/bot/src/profile/buildProfileEmbed.ts @@ -1,11 +1,17 @@ import { InteractionReplyOptions, MessageActionRow, MessageEmbed, MessagePayload, MessageSelectMenu } from "discord.js"; import { Game, UserGameStats } from "tachi-common"; -import { IDStrings, UGSRatingsLookup } from "tachi-common/js/types"; +import { IDStrings, PublicUserDocument, UGSRatingsLookup } from "tachi-common/js/types"; import { find } from "lodash"; import { LoggerLayers } from "../config"; import { TachiServerV1Get } from "../utils/fetch-tachi"; import { createLayeredLogger } from "../utils/logger"; -import { formatGameWrapper, prettyRatingString, SimpleGameType, simpleGameTypeToString } from "../utils/utils"; +import { + formatGameWrapper, + getPfpUrl, + prettyRatingString, + SimpleGameType, + simpleGameTypeToString +} from "../utils/utils"; const logger = createLayeredLogger(LoggerLayers.buildProfileEmbed); @@ -23,14 +29,32 @@ const pullRatings = ( return allRatings; }; -export const buildProfileEmbed = (data: UserGameStats[], game?: SimpleGameType): MessageEmbed => { +export const fetchUserDetails = async (userId: number): Promise => { + try { + logger.info(`Fetching public user document for ${userId}`); + const data = (await TachiServerV1Get(`/users/${userId}`))?.body; + if (data) { + return data; + } else { + throw new Error(`Could not find public user document for ${userId}`); + } + } catch (e) { + logger.error("Unable to fetch user details"); + throw new Error("Unable to fetch user details"); + } +}; + +export const buildProfileEmbed = async (data: UserGameStats[], game?: SimpleGameType): Promise => { const embed = new MessageEmbed().setColor("#cc527a"); - /** @TODO FETCH USERS ACTUAL DETAILS */ - embed.addField("Foo", "Foo"); - embed.addField("Bar", "Bar"); - embed.addField("Baz", "Baz"); - embed.setThumbnail("https://cdn.mos.cms.futurecdn.net/mrArzwHcNuQbRwbEmuiwdJ.jpg"); + const userId = data[0].userID; + const userDetails = await fetchUserDetails(userId); + const pfp = userDetails.customPfp + ? getPfpUrl(userId) + : "https://cdn.mos.cms.futurecdn.net/mrArzwHcNuQbRwbEmuiwdJ.jpg"; + embed.setTitle(`${userDetails.username}'s Profile`); + embed.setThumbnail(pfp); + embed.setAuthor(`@${userDetails.username}`, pfp); if (game) { const specificData: UserGameStats | undefined = find( @@ -72,7 +96,7 @@ export const buildProfileIntractable = async ( ) ); - return { embeds: [buildProfileEmbed(data, game)], components: [dropdown] }; + return { embeds: [await buildProfileEmbed(data, game)], components: [dropdown] }; } else { throw new Error(`No data found for user ${userId}`); } diff --git a/bot/src/utils/utils.ts b/bot/src/utils/utils.ts index b9e0b70e3..db80da880 100644 --- a/bot/src/utils/utils.ts +++ b/bot/src/utils/utils.ts @@ -1,5 +1,7 @@ import { FormatGame, Game, Playtypes } from "tachi-common"; import { IDStrings, UGSRatingsLookup } from "tachi-common/js/types"; +import { ProcessEnv } from "../setup"; +import { PrependTachiUrl } from "./fetch-tachi"; export interface SimpleGameType { game: T; @@ -26,10 +28,17 @@ export const prettyRatingString = (rating: UGSR case "VF6": return "Volforce"; case "BPI": - return "BPI"; + return "Beat Power Index"; case "MFCP": - return "???"; + return "MFCs"; default: return "Skill"; } }; + +export const getPfpUrl = (userId: number): string => { + if (ProcessEnv.ENV !== "prod") { + return `https://kamaitachi.xyz/static/images/users/${userId}-pfp.png`; + } + return PrependTachiUrl(`/users/${userId}/pfp`); +}; From 5a721d7545538d4e5824c64bf3e65df299566fcf Mon Sep 17 00:00:00 2001 From: Pfych Date: Sat, 18 Sep 2021 15:21:19 +1000 Subject: [PATCH 05/17] Optional game param --- bot/src/profile/buildProfileEmbed.ts | 60 +++++++++++++++++----------- bot/src/profile/fetch.ts | 8 +++- bot/src/slashCommands/register.ts | 8 ++++ bot/src/utils/utils.ts | 23 +++++++++++ 4 files changed, 74 insertions(+), 25 deletions(-) diff --git a/bot/src/profile/buildProfileEmbed.ts b/bot/src/profile/buildProfileEmbed.ts index 1877aeac0..eacc4e7b6 100644 --- a/bot/src/profile/buildProfileEmbed.ts +++ b/bot/src/profile/buildProfileEmbed.ts @@ -45,32 +45,43 @@ export const fetchUserDetails = async (userId: number): Promise): Promise => { - const embed = new MessageEmbed().setColor("#cc527a"); + try { + logger.info(`Building profile embed${game ? `for ${formatGameWrapper(game)}` : ""}`); + const embed = new MessageEmbed().setColor("#cc527a"); - const userId = data[0].userID; - const userDetails = await fetchUserDetails(userId); - const pfp = userDetails.customPfp - ? getPfpUrl(userId) - : "https://cdn.mos.cms.futurecdn.net/mrArzwHcNuQbRwbEmuiwdJ.jpg"; - embed.setTitle(`${userDetails.username}'s Profile`); - embed.setThumbnail(pfp); - embed.setAuthor(`@${userDetails.username}`, pfp); + const userId = data[0].userID; + const userDetails = await fetchUserDetails(userId); + const pfp = userDetails.customPfp + ? getPfpUrl(userId) + : "https://cdn.mos.cms.futurecdn.net/mrArzwHcNuQbRwbEmuiwdJ.jpg"; + embed.setTitle(`${userDetails.username}'s Profile`); + embed.setThumbnail(pfp); + embed.setAuthor(`@${userDetails.username}`, pfp); - if (game) { - const specificData: UserGameStats | undefined = find( - data, - (item) => simpleGameTypeToString(game) === `${item.game}:${item.playtype}` - ); - if (specificData) { - embed.addField(`Stats for: ${formatGameWrapper(game)}`, `${pullRatings(specificData.ratings).join("\n")}`); + logger.info(`Embed is for ${userDetails.username}`); + + if (game) { + const specificData: UserGameStats | undefined = find( + data, + (item) => simpleGameTypeToString(game) === `${item.game}:${item.playtype}` + ); + if (specificData) { + embed.addField( + `Stats for: ${formatGameWrapper(game)}`, + `${pullRatings(specificData.ratings).join("\n")}` + ); + } else { + embed.addField("Select a game to see stats", `No stats for ${formatGameWrapper(game)}`); + } } else { - throw new Error(`Could not get ratings for requested game: ${game}`); + embed.addField("Select a game to see stats", "\u200B"); } - } else { - embed.addField("Select a game to see stats", "\u200B"); - } - return embed; + return embed; + } catch (e) { + logger.error(e); + throw new Error("Unable to build profile embed"); + } }; export const buildProfileIntractable = async ( @@ -81,16 +92,17 @@ export const buildProfileIntractable = async ( const data = (await TachiServerV1Get(`/users/${userId}/game-stats`))?.body; logger.verbose(data); + /** @TODO Verify we got valid data here! */ if (data) { const dropdown = new MessageActionRow().addComponents( new MessageSelectMenu() .setCustomId(`SelectGameForProfile:${userId}`) .setPlaceholder("Browse By Game") .addOptions( - data.map((game) => { + data.map((_game) => { return { - label: `${formatGameWrapper(game)}`, - value: `${game.game}:${game.playtype}` + label: `${formatGameWrapper(_game)}`, + value: `${_game.game}:${_game.playtype}` }; }) ) diff --git a/bot/src/profile/fetch.ts b/bot/src/profile/fetch.ts index 64de84302..42893ac62 100644 --- a/bot/src/profile/fetch.ts +++ b/bot/src/profile/fetch.ts @@ -1,18 +1,24 @@ import { CommandInteraction } from "discord.js"; +import { IDStrings } from "tachi-common/js/types"; import { LoggerLayers } from "../config"; import { BotConfig } from "../setup"; import { createLayeredLogger } from "../utils/logger"; +import { stringToSimpleGameType } from "../utils/utils"; import { buildProfileIntractable } from "./buildProfileEmbed"; const logger = createLayeredLogger(LoggerLayers.profile); export const getProfileByName = async (interaction: CommandInteraction): Promise => { try { + /** @TODO this can be an @User, userId or userName!!! Handle this at some point */ const userId = interaction.options.getString("user", false) || "TODO FALLBACK TO SELF"; logger.info(`Fetching user ${userId}`); + const optionalGameOption = interaction.options.getString("game", false); + const game = optionalGameOption ? stringToSimpleGameType(optionalGameOption) : undefined; + if (userId) { - await interaction.reply(await buildProfileIntractable(userId)); + await interaction.reply(await buildProfileIntractable(userId, game)); } else { await interaction.reply("No user data found for user"); } diff --git a/bot/src/slashCommands/register.ts b/bot/src/slashCommands/register.ts index 073fae8d1..d8122543f 100644 --- a/bot/src/slashCommands/register.ts +++ b/bot/src/slashCommands/register.ts @@ -8,6 +8,7 @@ import { ProcessEnv } from "../setup"; import { LoggerLayers } from "../config"; import { help } from "../help/help"; import { createLayeredLogger } from "../utils/logger"; +import { gamesToChoicesObject } from "../utils/utils"; const logger = createLayeredLogger(LoggerLayers.slashCommands); @@ -30,6 +31,13 @@ export const slashCommands: SlashCommand[] = [ .setName("profile") .setDescription("Displays a Kamaitachi Profile") .addStringOption((option) => option.setName("user").setDescription("The users id").setRequired(false)) + .addStringOption((option) => + option + .setName("game") + .setDescription("The game id") + .setRequired(false) + .addChoices(gamesToChoicesObject()) + ) .toJSON(), exec: async (interaction: CommandInteraction) => await getProfileByName(interaction) } diff --git a/bot/src/utils/utils.ts b/bot/src/utils/utils.ts index db80da880..6a0af67a7 100644 --- a/bot/src/utils/utils.ts +++ b/bot/src/utils/utils.ts @@ -1,3 +1,4 @@ +import { APIApplicationCommandOptionChoice } from "discord-api-types"; import { FormatGame, Game, Playtypes } from "tachi-common"; import { IDStrings, UGSRatingsLookup } from "tachi-common/js/types"; import { ProcessEnv } from "../setup"; @@ -42,3 +43,25 @@ export const getPfpUrl = (userId: number): string => { } return PrependTachiUrl(`/users/${userId}/pfp`); }; + +/** @TODO @zkldi is there somewhere this exists already? */ +export const gameIdentifierStrings = [ + "iidx:SP", + "iidx:DP", + "sdvx:Single", + "usc:Single", + "ddr:SP", + "ddr:DP", + "maimai:Single", + "museca:Single", + "bms:7K", + "bms:14K", + "chunithm:Single", + "gitadora:Gita", + "gitadora:Dora" +]; +export const gamesToChoicesObject = (): [name: string, value: string][] => { + return gameIdentifierStrings.map((identifier) => { + return [formatGameWrapper(stringToSimpleGameType(identifier)), identifier]; + }); +}; From bd7af1182da287838b0cc2fd1735d9af6654a60b Mon Sep 17 00:00:00 2001 From: Pfych Date: Sat, 18 Sep 2021 15:26:56 +1000 Subject: [PATCH 06/17] Added extra TODO --- bot/src/interactionHandlers/selectMenu/handleIsSelectMenu.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/src/interactionHandlers/selectMenu/handleIsSelectMenu.ts b/bot/src/interactionHandlers/selectMenu/handleIsSelectMenu.ts index 0f35e745b..77fee9e9d 100644 --- a/bot/src/interactionHandlers/selectMenu/handleIsSelectMenu.ts +++ b/bot/src/interactionHandlers/selectMenu/handleIsSelectMenu.ts @@ -10,6 +10,7 @@ export const handleIsSelectMenu = async (interaction: Interaction): Promise Date: Sat, 18 Sep 2021 15:33:20 +1000 Subject: [PATCH 07/17] Silence return type error --- bot/src/profile/buildProfileEmbed.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/src/profile/buildProfileEmbed.ts b/bot/src/profile/buildProfileEmbed.ts index eacc4e7b6..5021fa24e 100644 --- a/bot/src/profile/buildProfileEmbed.ts +++ b/bot/src/profile/buildProfileEmbed.ts @@ -109,9 +109,9 @@ export const buildProfileIntractable = async ( ); return { embeds: [await buildProfileEmbed(data, game)], components: [dropdown] }; - } else { - throw new Error(`No data found for user ${userId}`); } + + throw new Error(`No data found for user ${userId}`); } catch (e) { logger.error("Unable to build profile interactable"); throw new Error("Unable to build profile interactable"); From a0a9b71b24f4dc37a34d3051f8dc430fe8e88a57 Mon Sep 17 00:00:00 2001 From: Pfych Date: Sun, 19 Sep 2021 13:05:54 +1000 Subject: [PATCH 08/17] Change pretty rating string to just capitalise existing value --- bot/src/utils/utils.ts | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/bot/src/utils/utils.ts b/bot/src/utils/utils.ts index 6a0af67a7..46153666d 100644 --- a/bot/src/utils/utils.ts +++ b/bot/src/utils/utils.ts @@ -18,23 +18,10 @@ export const stringToSimpleGameType = (game: string): SimpleGameType => { export const formatGameWrapper = (game: SimpleGameType): string => FormatGame(game.game, game.playtype); +export const capitalise = (s: string): string => (s && s[0].toUpperCase() + s.slice(1)) || ""; + export const prettyRatingString = (rating: UGSRatingsLookup[I]): string => { - switch (rating) { - case "ktRating": - return "Kamai Rating"; - case "ktLampRating": - return "Kamai Lamp Rating"; - case "sieglinde": - return "???"; - case "VF6": - return "Volforce"; - case "BPI": - return "Beat Power Index"; - case "MFCP": - return "MFCs"; - default: - return "Skill"; - } + return capitalise(rating); }; export const getPfpUrl = (userId: number): string => { From 48bcb6f83bdc82a03187cb489b0b5390097ef800 Mon Sep 17 00:00:00 2001 From: Pfych Date: Sun, 19 Sep 2021 13:06:43 +1000 Subject: [PATCH 09/17] [user-can] Fix incorrect imports --- bot/src/utils/utils.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bot/src/utils/utils.ts b/bot/src/utils/utils.ts index 46153666d..322fec0bb 100644 --- a/bot/src/utils/utils.ts +++ b/bot/src/utils/utils.ts @@ -1,6 +1,4 @@ -import { APIApplicationCommandOptionChoice } from "discord-api-types"; -import { FormatGame, Game, Playtypes } from "tachi-common"; -import { IDStrings, UGSRatingsLookup } from "tachi-common/js/types"; +import { FormatGame, Game, Playtypes, IDStrings, UGSRatingsLookup } from "tachi-common"; import { ProcessEnv } from "../setup"; import { PrependTachiUrl } from "./fetch-tachi"; From bf0c8cb14e34823d161d70cac66041afb6cad3ee Mon Sep 17 00:00:00 2001 From: Pfych Date: Sun, 19 Sep 2021 13:08:36 +1000 Subject: [PATCH 10/17] use gpt instead of game for gamePlayType --- bot/src/utils/utils.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bot/src/utils/utils.ts b/bot/src/utils/utils.ts index 322fec0bb..9f64b5074 100644 --- a/bot/src/utils/utils.ts +++ b/bot/src/utils/utils.ts @@ -7,14 +7,14 @@ export interface SimpleGameType { playtype: Playtypes[T]; } -export const simpleGameTypeToString = (game: SimpleGameType): IDStrings => - `${game.game}:${game.playtype}`; +export const simpleGameTypeToString = (gpt: SimpleGameType): IDStrings => + `${gpt.game}:${gpt.playtype}`; -export const stringToSimpleGameType = (game: string): SimpleGameType => { - return { game: game.split(":")[0], playtype: game.split(":")[1] }; +export const stringToSimpleGameType = (gptString: string): SimpleGameType => { + return { game: gptString.split(":")[0], playtype: gptString.split(":")[1] }; }; -export const formatGameWrapper = (game: SimpleGameType): string => FormatGame(game.game, game.playtype); +export const formatGameWrapper = (gpt: SimpleGameType): string => FormatGame(gpt.game, gpt.playtype); export const capitalise = (s: string): string => (s && s[0].toUpperCase() + s.slice(1)) || ""; From d330072a4bc628adda72f6eca8c72912b70aaa3b Mon Sep 17 00:00:00 2001 From: Pfych Date: Sun, 19 Sep 2021 14:02:46 +1000 Subject: [PATCH 11/17] Swapped score rounding to use tachi-common service --- bot/src/profile/buildProfileEmbed.ts | 14 ++++++++------ bot/src/profile/fetch.ts | 2 +- bot/src/utils/utils.ts | 17 ++++++++++++++++- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/bot/src/profile/buildProfileEmbed.ts b/bot/src/profile/buildProfileEmbed.ts index 5021fa24e..aeacd2e73 100644 --- a/bot/src/profile/buildProfileEmbed.ts +++ b/bot/src/profile/buildProfileEmbed.ts @@ -1,11 +1,12 @@ import { InteractionReplyOptions, MessageActionRow, MessageEmbed, MessagePayload, MessageSelectMenu } from "discord.js"; -import { Game, UserGameStats } from "tachi-common"; -import { IDStrings, PublicUserDocument, UGSRatingsLookup } from "tachi-common/js/types"; +import { Game, UserGameStats, IDStrings, PublicUserDocument, UGSRatingsLookup } from "tachi-common"; import { find } from "lodash"; +import { ScoreCalculatedDataLookup } from "tachi-common/js/types"; import { LoggerLayers } from "../config"; import { TachiServerV1Get } from "../utils/fetch-tachi"; import { createLayeredLogger } from "../utils/logger"; import { + formatGameScoreRating, formatGameWrapper, getPfpUrl, prettyRatingString, @@ -16,14 +17,15 @@ import { const logger = createLayeredLogger(LoggerLayers.buildProfileEmbed); const pullRatings = ( + gpt: SimpleGameType, ratings: Partial> ): string[] => { const allRatings: string[] = []; const RatingKeys = Object.keys(ratings); - RatingKeys.map((rating) => { - const ratingValue: number = ratings[rating] || 0; - allRatings.push(`${prettyRatingString(rating)}: ${Math.round((ratingValue + Number.EPSILON) * 100) / 100}`); + RatingKeys.forEach((rating) => { + const ratingValue: string = formatGameScoreRating(gpt, rating, ratings[rating] || 0); + allRatings.push(`${prettyRatingString(rating)}: ${ratingValue}`); }); return allRatings; @@ -68,7 +70,7 @@ export const buildProfileEmbed = async (data: UserGameStats[], game?: SimpleGame if (specificData) { embed.addField( `Stats for: ${formatGameWrapper(game)}`, - `${pullRatings(specificData.ratings).join("\n")}` + `${pullRatings(game, specificData.ratings).join("\n")}` ); } else { embed.addField("Select a game to see stats", `No stats for ${formatGameWrapper(game)}`); diff --git a/bot/src/profile/fetch.ts b/bot/src/profile/fetch.ts index 42893ac62..665a91c90 100644 --- a/bot/src/profile/fetch.ts +++ b/bot/src/profile/fetch.ts @@ -1,5 +1,5 @@ import { CommandInteraction } from "discord.js"; -import { IDStrings } from "tachi-common/js/types"; +import { IDStrings } from "tachi-common"; import { LoggerLayers } from "../config"; import { BotConfig } from "../setup"; import { createLayeredLogger } from "../utils/logger"; diff --git a/bot/src/utils/utils.ts b/bot/src/utils/utils.ts index 9f64b5074..cdd57c7e0 100644 --- a/bot/src/utils/utils.ts +++ b/bot/src/utils/utils.ts @@ -1,4 +1,5 @@ -import { FormatGame, Game, Playtypes, IDStrings, UGSRatingsLookup } from "tachi-common"; +import { FormatGame, Game, Playtypes, IDStrings, UGSRatingsLookup, GetGamePTConfig } from "tachi-common"; +import { ScoreCalculatedDataLookup } from "tachi-common/js/types"; import { ProcessEnv } from "../setup"; import { PrependTachiUrl } from "./fetch-tachi"; @@ -50,3 +51,17 @@ export const gamesToChoicesObject = (): [name: string, value: string][] => { return [formatGameWrapper(stringToSimpleGameType(identifier)), identifier]; }); }; + +export const formatGameScoreRating = ( + gpt: SimpleGameType, + ratingName: UGSRatingsLookup[I], + value: number +): string => { + const gptConfig = GetGamePTConfig(gpt.game, gpt.playtype); + /** @TODO UGSRatingsLookup should be able to cast to ScoreCalculatedDataLookup straight up */ + const val = gptConfig.scoreRatingAlgFormatters[ratingName as unknown as ScoreCalculatedDataLookup[I]]; + if (val) { + return val(value); + } + return value.toFixed(2); +}; From 5262a5db63b2f9f58588029b6beb28c7fd556c85 Mon Sep 17 00:00:00 2001 From: Pfych Date: Sun, 19 Sep 2021 14:03:37 +1000 Subject: [PATCH 12/17] Remove optional chain from data fetch --- bot/src/profile/buildProfileEmbed.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/src/profile/buildProfileEmbed.ts b/bot/src/profile/buildProfileEmbed.ts index aeacd2e73..7db836be4 100644 --- a/bot/src/profile/buildProfileEmbed.ts +++ b/bot/src/profile/buildProfileEmbed.ts @@ -34,7 +34,7 @@ const pullRatings = ( export const fetchUserDetails = async (userId: number): Promise => { try { logger.info(`Fetching public user document for ${userId}`); - const data = (await TachiServerV1Get(`/users/${userId}`))?.body; + const data = (await TachiServerV1Get(`/users/${userId}`)).body; if (data) { return data; } else { From 285ff5c699235980f66fc724344e1da5470d51c5 Mon Sep 17 00:00:00 2001 From: Pfych Date: Sun, 19 Sep 2021 14:05:38 +1000 Subject: [PATCH 13/17] [user-can] Remove double space --- bot/src/profile/buildProfileEmbed.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/src/profile/buildProfileEmbed.ts b/bot/src/profile/buildProfileEmbed.ts index 7db836be4..e856665aa 100644 --- a/bot/src/profile/buildProfileEmbed.ts +++ b/bot/src/profile/buildProfileEmbed.ts @@ -41,8 +41,8 @@ export const fetchUserDetails = async (userId: number): Promise Date: Sun, 19 Sep 2021 14:06:32 +1000 Subject: [PATCH 14/17] Change command description --- bot/src/slashCommands/register.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/bot/src/slashCommands/register.ts b/bot/src/slashCommands/register.ts index d8122543f..a5338ce22 100644 --- a/bot/src/slashCommands/register.ts +++ b/bot/src/slashCommands/register.ts @@ -32,11 +32,7 @@ export const slashCommands: SlashCommand[] = [ .setDescription("Displays a Kamaitachi Profile") .addStringOption((option) => option.setName("user").setDescription("The users id").setRequired(false)) .addStringOption((option) => - option - .setName("game") - .setDescription("The game id") - .setRequired(false) - .addChoices(gamesToChoicesObject()) + option.setName("game").setDescription("The Game").setRequired(false).addChoices(gamesToChoicesObject()) ) .toJSON(), exec: async (interaction: CommandInteraction) => await getProfileByName(interaction) From a7bc69fa59a96bf85090a75f8aa163e684cb9cc0 Mon Sep 17 00:00:00 2001 From: Pfych Date: Sun, 19 Sep 2021 14:08:44 +1000 Subject: [PATCH 15/17] Make user not optional for now --- bot/src/slashCommands/register.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/src/slashCommands/register.ts b/bot/src/slashCommands/register.ts index a5338ce22..79a91017f 100644 --- a/bot/src/slashCommands/register.ts +++ b/bot/src/slashCommands/register.ts @@ -30,7 +30,8 @@ export const slashCommands: SlashCommand[] = [ info: new SlashCommandBuilder() .setName("profile") .setDescription("Displays a Kamaitachi Profile") - .addStringOption((option) => option.setName("user").setDescription("The users id").setRequired(false)) + /** @TODO Make this optional once we have a fallback */ + .addStringOption((option) => option.setName("user").setDescription("The users id").setRequired(true)) .addStringOption((option) => option.setName("game").setDescription("The Game").setRequired(false).addChoices(gamesToChoicesObject()) ) From 332c838344f8c93fceea11da75adaf656fd83847 Mon Sep 17 00:00:00 2001 From: Pfych Date: Sat, 25 Sep 2021 12:20:38 +1000 Subject: [PATCH 16/17] Use correct profile formatter --- bot/src/profile/buildProfileEmbed.ts | 1 - bot/src/utils/utils.ts | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/bot/src/profile/buildProfileEmbed.ts b/bot/src/profile/buildProfileEmbed.ts index e856665aa..23dcd21b2 100644 --- a/bot/src/profile/buildProfileEmbed.ts +++ b/bot/src/profile/buildProfileEmbed.ts @@ -1,7 +1,6 @@ import { InteractionReplyOptions, MessageActionRow, MessageEmbed, MessagePayload, MessageSelectMenu } from "discord.js"; import { Game, UserGameStats, IDStrings, PublicUserDocument, UGSRatingsLookup } from "tachi-common"; import { find } from "lodash"; -import { ScoreCalculatedDataLookup } from "tachi-common/js/types"; import { LoggerLayers } from "../config"; import { TachiServerV1Get } from "../utils/fetch-tachi"; import { createLayeredLogger } from "../utils/logger"; diff --git a/bot/src/utils/utils.ts b/bot/src/utils/utils.ts index cdd57c7e0..3ab725258 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 { ScoreCalculatedDataLookup } from "tachi-common/js/types"; import { ProcessEnv } from "../setup"; import { PrependTachiUrl } from "./fetch-tachi"; @@ -59,7 +58,7 @@ export const formatGameScoreRating = ( ): string => { const gptConfig = GetGamePTConfig(gpt.game, gpt.playtype); /** @TODO UGSRatingsLookup should be able to cast to ScoreCalculatedDataLookup straight up */ - const val = gptConfig.scoreRatingAlgFormatters[ratingName as unknown as ScoreCalculatedDataLookup[I]]; + const val = gptConfig.profileRatingAlgFormatters[ratingName]; if (val) { return val(value); } From 1b6ea9364339c012992a7161d5245bd9b3c1e19b Mon Sep 17 00:00:00 2001 From: Pfych Date: Sat, 25 Sep 2021 12:30:00 +1000 Subject: [PATCH 17/17] [user-can] Remove old comments --- bot/src/utils/utils.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/bot/src/utils/utils.ts b/bot/src/utils/utils.ts index 3ab725258..16df3eafd 100644 --- a/bot/src/utils/utils.ts +++ b/bot/src/utils/utils.ts @@ -29,7 +29,6 @@ export const getPfpUrl = (userId: number): string => { return PrependTachiUrl(`/users/${userId}/pfp`); }; -/** @TODO @zkldi is there somewhere this exists already? */ export const gameIdentifierStrings = [ "iidx:SP", "iidx:DP", @@ -57,7 +56,6 @@ export const formatGameScoreRating = ( value: number ): string => { const gptConfig = GetGamePTConfig(gpt.game, gpt.playtype); - /** @TODO UGSRatingsLookup should be able to cast to ScoreCalculatedDataLookup straight up */ const val = gptConfig.profileRatingAlgFormatters[ratingName]; if (val) { return val(value);