[WIP] Added boilerplate for profile view

This commit is contained in:
Pfych
2021-09-15 19:40:53 +10:00
parent 5c746bbab3
commit d6118de14e
10 changed files with 179 additions and 48 deletions
+1
View File
@@ -3,3 +3,4 @@ node_modules
.vscode
yarn-error.log
.env
conf.json5
+3 -1
View File
@@ -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",
+2 -1
View File
@@ -8,5 +8,6 @@ export enum LoggerLayers {
server = "server",
serverAuth = "serverAuth",
botConfigSetup = "botConfigSetup",
tachiFetch = "tachiFetch"
tachiFetch = "tachiFetch",
profile = "profile"
}
+21 -4
View File
@@ -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<UserGameStats[]>(`/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);
+65
View File
@@ -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] };
};
+28
View File
@@ -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<void> => {
try {
const userId = interaction.options.getString("user", false) || "TODO FALLBACK TO SELF";
logger.info(`Fetching user ${userId}`);
const userData = (await TachiServerV1Get<UserGameStats[]>(`/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);
}
};
+13 -7
View File
@@ -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();
+9
View File
@@ -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)
}
];
+19 -9
View File
@@ -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<T> = (SuccessfulAPIResponse<T> | UnsuccessfulAPIResponse) & { statusCode: integer };
export type APIResponse<T> = (SuccessfulAPIResponse<T> | (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<T>(method: Exclude<RequestTypes, RequestTypes.GET>, url: string, body: unknown = {}): Promise<APIResponse<T>> {
export async function TachiServerV1Request<T>(
method: Exclude<RequestTypes, RequestTypes.GET>,
url: string,
body: unknown = {}
): Promise<APIResponse<T>> {
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<T>;
const contents = { ...json, statusCode: res.status };
LogRequestResult(loggerUrl, contents);
@@ -60,14 +67,17 @@ export async function TachiServerV1Request<T>(method: Exclude<RequestTypes, Requ
* @param params - Any URL params for this request.
* @param T - A generic that asserts the type of the response contents. Defaults to unknown.
*/
export async function TachiServerV1Get<T = unknown>(url: string, params: Record<string, string> = {}): Promise<APIResponse<T>> {
export async function TachiServerV1Get<T = unknown>(
url: string,
params: Record<string, string> = {}
): Promise<APIResponse<T>> {
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<T>;
const contents = { ...json, statusCode: res.status };
LogRequestResult(`GET ${realUrl}`, contents);
+18 -26
View File
@@ -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"