Apply tachi formatting rules to this repository

This commit is contained in:
zkldi
2022-03-18 16:55:02 +00:00
parent 04537d940c
commit 1830701ea9
23 changed files with 1802 additions and 962 deletions
+114
View File
@@ -0,0 +1,114 @@
{
"root": true,
"env": {
"node": true,
"es6": true
},
"plugins": [
"@typescript-eslint",
"import"
],
"overrides": [
{
"files": [
"*.ts",
"*.tsx"
],
"parserOptions": {
"project": [
"./tsconfig.json"
]
}
}
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 2020,
"project": "tsconfig.json"
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
"plugin:import/typescript",
"plugin:import/warnings",
"plugin:import/errors"
],
"rules": {
"prettier/prettier": [
"error",
{
"printWidth": 100,
"tabWidth": 4,
"semi": true,
"singleQuote": false,
"trailingComma": "es5",
"bracketSpacing": true,
"endOfLine": "lf",
"useTabs": true
}
],
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-misused-promises": [
"error",
{
"checksVoidReturn": false
}
],
"@typescript-eslint/no-unused-vars": [
"warn",
{
"argsIgnorePattern": "logger"
}
],
// breaks with baseUrl
"import/no-unresolved": "off",
"require-await": "warn",
"no-await-in-loop": "warn",
"no-unused-labels": "error",
"prefer-const": "error",
"no-console": "error",
"eqeqeq": "error",
"no-eval": "error",
"no-var": "error",
"no-new-object": "error",
"quote-props": [
"error",
"as-needed"
],
"no-array-constructor": "error",
"prefer-template": "error",
"prefer-promise-reject-errors": "error",
"no-useless-escape": "error",
"no-loop-func": "error",
"prefer-rest-params": "error",
"default-param-last": "error",
"no-new-func": "error",
"arrow-body-style": "error",
"no-useless-constructor": "error",
"no-duplicate-imports": "error",
"dot-notation": "error",
"space-before-blocks": "error",
"no-prototype-builtins": "off",
"prefer-arrow-callback": "error",
"curly": "error",
"no-template-curly-in-string": "error",
"no-implied-eval": "error",
"yoda": "error",
"no-labels": "error",
"no-useless-concat": "error",
"radix": "error",
"no-octal-escape": "error",
"no-param-reassign": "error",
"no-proto": "error",
"no-return-await": "error",
"no-throw-literal": "error",
"no-unmodified-loop-condition": "error",
"no-unused-expressions": "error",
"no-useless-call": "error",
"no-useless-return": "error",
"require-unicode-regexp": "error"
}
}
-49
View File
@@ -1,49 +0,0 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
"indent": [
"error",
"tab",
{
"SwitchCase": 1
}
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"double"
],
"semi": [
"error",
"always"
],
"object-curly-spacing": [
"error",
"always"
],
"@typescript-eslint/no-unused-vars": [
"error"
],
"@typescript-eslint/no-non-null-assertion": [
"off"
]
}
}
-6
View File
@@ -1,6 +0,0 @@
{
"semi": true,
"trailingComma": "none",
"singleQuote": false,
"printWidth": 120
}
+31 -27
View File
@@ -5,36 +5,40 @@
"license": "MIT",
"scripts": {
"start": "ts-node -r dotenv/config src/index.ts",
"setup": "./scripts/createEnv.sh"
"setup": "./scripts/createEnv.sh",
"lint": "eslint . --fix"
},
"devDependencies": {
"@types/express": "^4.17.13",
"@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",
"discord-api-types": "^0.22.0",
"eslint": "^7.32.0",
"jest": "^27.0.6",
"prettier": "^2.3.2",
"typescript": "^4.3.5"
"@types/express": "4.17.13",
"@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": "5.10.2",
"@typescript-eslint/parser": "5.10.2",
"discord-api-types": "0.22.0",
"eslint": "8.8.0",
"eslint-config-prettier": "8.3.0",
"eslint-plugin-import": "2.25.4",
"eslint-plugin-prettier": "4.0.0",
"jest": "27.0.6",
"prettier": "2.6.0",
"typescript": "4.3.5"
},
"dependencies": {
"@discordjs/builders": "^0.5.0",
"@discordjs/rest": "^0.1.0-canary.0",
"axios": "^0.21.4",
"discord.js": "^13.1.0",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"json5": "^2.2.0",
"lodash": "^4.17.21",
"node-fetch": "^3.2.0",
"prudence": "^0.9.6",
"tachi-common": "^0.6.11",
"ts-node": "^10.2.0",
"winston": "^3.3.3"
"@discordjs/builders": "0.5.0",
"@discordjs/rest": "0.1.0-canary.0",
"axios": "0.21.4",
"discord.js": "13.1.0",
"dotenv": "10.0.0",
"express": "4.17.1",
"json5": "2.2.0",
"lodash": "4.17.21",
"node-fetch": "3.2.0",
"prudence": "0.9.6",
"tachi-common": "0.6.11",
"ts-node": "10.2.0",
"winston": "3.3.3"
}
}
@@ -1,11 +1,17 @@
import { InteractionReplyOptions, MessageActionRow, MessageEmbed, MessagePayload, MessageSelectMenu } from "discord.js";
import {
InteractionReplyOptions,
MessageActionRow,
MessageEmbed,
MessagePayload,
MessageSelectMenu,
} from "discord.js";
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 { formatGameScoreRating, getGameImage } from "../utils/utils";
import { LoggerLayers } from "../../config";
import { validSelectCustomIdPrefaces } from "../../interactionHandlers/selectMenu/handleIsSelectMenu";
import { TachiServerV1Get } from "../../utils/fetch-tachi";
import { createLayeredLogger } from "../../utils/logger";
import { formatGameScoreRating, getGameImage } from "../../utils/utils";
import { getDetailedSongData, SongSearchResult } from "./chartSearch";
const logger = createLayeredLogger(LoggerLayers.buildChartEmbed);
@@ -15,21 +21,22 @@ export interface PBResponse {
pbs: PBScoreDocument[];
}
export const buildSongSelect = <T extends Game>(songs: SongSearchResult[], playtype: Playtypes[T], game: Game): MessageActionRow => {
return new MessageActionRow().addComponents(
export const buildSongSelect = <T extends Game>(
songs: SongSearchResult[],
playtype: Playtypes[T],
game: Game
): MessageActionRow =>
new MessageActionRow().addComponents(
new MessageSelectMenu()
.setCustomId(validSelectCustomIdPrefaces.selectSongForSearch)
.setPlaceholder("Select Song")
.addOptions(
songs.map((chart) => {
return {
label: `${chart.title} - ${chart.artist}`,
value: `${chart.id}:${playtype}:${game}`
};
})
songs.map((chart) => ({
label: `${chart.title} - ${chart.artist}`,
value: `${chart.id}:${playtype}:${game}`,
}))
)
);
};
interface SimplePBDocument extends PBScoreDocument, Partial<PublicUserDocument> {}
@@ -54,7 +61,7 @@ export const getPBForChart = async <T extends Game>(
return {
...topPb,
...topUser
...topUser,
};
} else {
logger.error(data.description);
@@ -81,8 +88,14 @@ export const buildChartEmbed = async <T extends Game, I extends IDStrings = IDSt
const embed = new MessageEmbed().setColor("#cc527a");
if (!songId && searchResults) {
embed.addField(`${searchResults.length} potential results found`, "Select from the dropdown");
return { embeds: [embed], components: [buildSongSelect(searchResults, playtype, game)] };
embed.addField(
`${searchResults.length} potential results found`,
"Select from the dropdown"
);
return {
embeds: [embed],
components: [buildSongSelect(searchResults, playtype, game)],
};
} else if (songId) {
const details = await getDetailedSongData(songId, playtype, game, discordUserId);
embed.addField(details.song.title || "Song", details.song.artist || "Artist");
@@ -107,21 +120,29 @@ export const buildChartEmbed = async <T extends Game, I extends IDStrings = IDSt
logger.info("Getting chart PBs");
for (const chart of sortedCharts) {
try {
const PB = await getPBForChart(chart.chartID, chart.playtype, game, discordUserId);
const PB = await getPBForChart(
chart.chartID,
chart.playtype,
game,
discordUserId
);
embed.addField(
`${chart.difficulty} (${chart.level})`,
`Server Top: **[${PB.username}](https://kamaitachi.xyz/dashboard/users/${
PB.id
}/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 },
<UGSRatingsLookup[I]>item,
PB.calculatedData[item as never] || 0
)}`;
})
}/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) =>
`${item}: ${formatGameScoreRating(
{ game, playtype },
<UGSRatingsLookup[I]>item,
PB.calculatedData[item as never] || 0
)}`
)
.join("\n")}`,
false
);
@@ -1,10 +1,10 @@
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 { 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);
@@ -24,12 +24,15 @@ export interface DetailedSongResponse {
charts: ChartDocument[];
}
export const getSongMeta = async (songName: string, discordUserId: string): Promise<SongSearchResult[]> => {
export const getSongMeta = async (
songName: string,
discordUserId: string
): Promise<SongSearchResult[]> => {
try {
logger.info(`Searching for ${songName}`);
const songResponse = (
await TachiServerV1Get<SearchResult>("/search", "TEMP_MUST_REFACTOR_THIS", {
search: songName
await TachiServerV1Get<SearchResult>("/search", {
search: songName,
})
).body?.songs;
@@ -107,7 +110,7 @@ export const searchForSong = async (interaction: CommandInteraction): Promise<vo
searchResults: songMetaFiltered,
playtype,
game,
discordUserId: interaction.user.id
discordUserId: interaction.user.id,
})
);
} catch (e) {
+2 -2
View File
@@ -13,7 +13,7 @@ export enum LoggerLayers {
buildProfileEmbed = "buildProfileEmbed",
chartSearch = "chartSearch",
buildChartEmbed = "buildChartEmbed",
tachiAuth = "tachiAuth"
tachiAuth = "tachiAuth",
}
export const theFunny = [
@@ -90,5 +90,5 @@ export const theFunny = [
"i'm obligated to ask for you to seek immediate psychological attention",
"Is IIDX still a good game or are you all basically stupid?",
"A point in the horizon, a melting scene from your childhood. Your mortality is showing a frantic drift towards nothing, biology doomed to an infinite recursive loop. Teeth with teeth with teeth. Take a bite. Serene scene of a coastal town, warmth of the sun. Bitter tears. Lust for power. This is where you abandoned your dreams. You are a high net worth individual, an expanding vortex of pathetic trauma. Finally a beautiful fucking nerve ape. A pure soul is born, its neurotransactions stutter into being. 30583750937509353 operations per nanosecond. Beauty eludes your porous mind.\n\nThe value of Life is negative. The balance of being is rotated by 38 degrees. The surface is full of cracks, a turgid light shines through. Fleshy primordial bodies sluggishly roll down the slope. Only you slide upwards, with a celestial step. You become beautified, a saintly figure. Your pristine idiocy reveals a safe path through the impenetrable fog of life. Your dull sword cuts through the weak tendons and membranes of the garden of corruption. Sit on the throne of contentment and ferment. Inspect the eternal blue skies of your kingdom. You come to a realization. You pick up and onion and begin peeling.\n\nOnion layer one. Onion layer two. Onion layer three ... Onion layer n^n. Aeons have passed and the onion is fully peeled. Nothing remains. It's perfect. You get lost in the point that remains where the onion used to be. Synaptic cascade, neurological catastrophe. The point becomes infinitely dense, the universe condenses into a unicellular being. It screams sin. It craves happiness. It's done with this world. It tries to commit suicide but it fails. Sad pathetic mess. You feel pity and disgust but in a way only a being of pure grace can. In your violent mercy you terminate the worldlife.",
"Please do not reply to or quote my posts in the future."
"Please do not reply to or quote my posts in the future.",
];
+4 -1
View File
@@ -12,7 +12,10 @@ export interface DiscordUserMapDocument {
}
const db = {
discordUserMap: (() => { console.error("DATABASE HAS BEEN STUBBED OUT TEMPORARILY. SIGTERMING."); process.exit(1); } ) as any // monkDB.get<DiscordUserMapDocument>("discordUserMap")
discordUserMap: (() => {
console.error("DATABASE HAS BEEN STUBBED OUT TEMPORARILY. SIGTERMING.");
process.exit(1);
}) as any, // monkDB.get<DiscordUserMapDocument>("discordUserMap")
};
export default db;
+10 -4
View File
@@ -12,14 +12,16 @@ import { initWatchHandler } from "./utils/utils";
const logger = createLayeredLogger(LoggerLayers.client);
const client = new Client({
intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.DIRECT_MESSAGES, Intents.FLAGS.GUILD_MESSAGES]
intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.DIRECT_MESSAGES, Intents.FLAGS.GUILD_MESSAGES],
});
client.on("messageCreate", async (message) => {
if (message.content === "!!") {
const index = Math.floor(Math.random() * theFunny.length);
await message.channel.send(
theFunny[index].length > 1997 ? theFunny[index].substring(0, 1997).trimEnd() + "..." : theFunny[index]
theFunny[index].length > 1997
? `${theFunny[index].substring(0, 1997).trimEnd()}...`
: theFunny[index]
);
}
});
@@ -31,7 +33,9 @@ client.on("interactionCreate", async (interaction) => {
if (interaction.isSelectMenu()) {
if (!TachiObject) {
await (await interaction.user.createDM()).send(`Please log in: ${oAuthLink}`);
return await interaction.reply("Please link your discord account, You have been messaged a URL");
return await interaction.reply(
"Please link your discord account, You have been messaged a URL"
);
}
return await handleIsSelectMenu(interaction);
}
@@ -39,7 +43,9 @@ client.on("interactionCreate", async (interaction) => {
if (interaction.isCommand()) {
if (!TachiObject) {
await (await interaction.user.createDM()).send(`Please log in: ${oAuthLink}`);
return await interaction.reply("Please link your discord account, You have been messaged a URL");
return await interaction.reply(
"Please link your discord account, You have been messaged a URL"
);
}
return await handleIsCommand(interaction);
}
@@ -9,9 +9,11 @@ export const handleIsCommand = async (interaction: Interaction): Promise<void> =
try {
/** Rechecking required to enforce types */
if (interaction.isCommand()) {
const command = slashCommands.find((command: SlashCommand) => {
return command.info.name === interaction.commandName;
});
const command = slashCommands.find(
(command: SlashCommand) => command.info.name === interaction.commandName
);
const requestingUserInfo = GetRequestingUserInfo(interaction);
if (command && command.exec) {
logger.info(`Running ${command.info.name} interaction`);
@@ -1,6 +1,6 @@
import { Interaction, SelectMenuInteraction } from "discord.js";
import { Game, Playtypes } from "tachi-common";
import { buildChartEmbed } from "../../chartSearch/buildChartEmbed";
import { buildChartEmbed } from "../../commands/chartSearch/buildChartEmbed";
import { LoggerLayers } from "../../config";
import { buildProfileIntractable } from "../../profile/buildProfileEmbed";
import { createLayeredLogger } from "../../utils/logger";
@@ -10,15 +10,24 @@ const logger = createLayeredLogger(LoggerLayers.selectInteractionHandler);
export enum validSelectCustomIdPrefaces {
selectSongForSearch = "selectSongForSearch",
SelectGameForProfile = "SelectGameForProfile"
SelectGameForProfile = "SelectGameForProfile",
}
export type SelectHandlersType = Record<validSelectCustomIdPrefaces, (interaction: SelectMenuInteraction) => void>;
export type SelectHandlersType = Record<
validSelectCustomIdPrefaces,
(interaction: SelectMenuInteraction) => void
>;
export const selectHandlers: SelectHandlersType = {
[validSelectCustomIdPrefaces.SelectGameForProfile]: async (interaction: SelectMenuInteraction): Promise<void> => {
[validSelectCustomIdPrefaces.SelectGameForProfile]: async (
interaction: SelectMenuInteraction
): Promise<void> => {
const userId = parseInt(interaction.customId.split(":")[1]);
await interaction.editReply(
await buildProfileIntractable(userId, interaction.user.id, stringToSimpleGameType(interaction.values[0]))
await buildProfileIntractable(
userId,
interaction.user.id,
stringToSimpleGameType(interaction.values[0])
)
);
},
[validSelectCustomIdPrefaces.selectSongForSearch]: async <T extends Game>(
@@ -30,14 +39,14 @@ export const selectHandlers: SelectHandlersType = {
songId: interactionValues[0],
playtype: <Playtypes[T]>interactionValues[1],
game: <T>interactionValues[2],
discordUserId: interaction.user.id
discordUserId: interaction.user.id,
});
logger.info("Built new embed");
await interaction.editReply(embed);
} catch (e) {
logger.error(e);
}
}
},
};
export const handleIsSelectMenu = async (interaction: Interaction): Promise<void> => {
+38 -17
View File
@@ -1,4 +1,10 @@
import { InteractionReplyOptions, MessageActionRow, MessageEmbed, MessagePayload, MessageSelectMenu } from "discord.js";
import {
InteractionReplyOptions,
MessageActionRow,
MessageEmbed,
MessagePayload,
MessageSelectMenu,
} from "discord.js";
import { Game, UserGameStats, IDStrings, PublicUserDocument, UGSRatingsLookup } from "tachi-common";
import { find } from "lodash";
import { LoggerLayers } from "../config";
@@ -11,7 +17,7 @@ import {
getPfpUrl,
prettyRatingString,
SimpleGameType,
simpleGameTypeToString
simpleGameTypeToString,
} from "../utils/utils";
const logger = createLayeredLogger(LoggerLayers.buildProfileEmbed);
@@ -31,11 +37,19 @@ const pullRatings = <I extends IDStrings = IDStrings>(
return allRatings;
};
export const fetchUserDetails = async (userId: number, discordUserId: string): Promise<PublicUserDocument> => {
export const fetchUserDetails = async (
userId: number,
discordUserId: string
): Promise<PublicUserDocument> => {
try {
logger.info(`Fetching public user document for ${userId}`);
const data = (await TachiServerV1Get<PublicUserDocument>(`/users/${userId}`, {}, { discordId: discordUserId }))
.body;
const data = (
await TachiServerV1Get<PublicUserDocument>(
`/users/${userId}`,
{},
{ discordId: discordUserId }
)
).body;
if (data) {
return data;
} else {
@@ -58,9 +72,8 @@ export const buildProfileEmbed = async (
const userId = data[0].userID;
const userDetails = await fetchUserDetails(userId);
const pfp = userDetails.customPfpLocation
? getPfpUrl(userId)
: "https://cdn.mos.cms.futurecdn.net/mrArzwHcNuQbRwbEmuiwdJ.jpg";
const pfp = getPfpUrl(userDetails);
embed.setTitle(`${userDetails.username}'s Profile`);
embed.setThumbnail(pfp);
embed.setAuthor(`@${userDetails.username}`, pfp);
@@ -78,7 +91,10 @@ export const buildProfileEmbed = async (
`${pullRatings(game, specificData.ratings).join("\n")}`
);
} else {
embed.addField("Select a game to see stats", `No stats for ${formatGameWrapper(game)}`);
embed.addField(
"Select a game to see stats",
`No stats for ${formatGameWrapper(game)}`
);
}
} else {
embed.addField("Select a game to see stats", "\u200B");
@@ -98,7 +114,11 @@ export const buildProfileIntractable = async (
): Promise<InteractionReplyOptions | MessagePayload> => {
try {
const data = (
await TachiServerV1Get<UserGameStats[]>(`/users/${userId}/game-stats`, {}, { discordId: discordUserId })
await TachiServerV1Get<UserGameStats[]>(
`/users/${userId}/game-stats`,
{},
{ discordId: discordUserId }
)
)?.body;
logger.verbose(data);
@@ -109,16 +129,17 @@ export const buildProfileIntractable = async (
.setCustomId(`${validSelectCustomIdPrefaces.SelectGameForProfile}:${userId}`)
.setPlaceholder("Browse By Game")
.addOptions(
data.map((_game) => {
return {
label: `${formatGameWrapper(_game)}`,
value: `${_game.game}:${_game.playtype}`
};
})
data.map((_game) => ({
label: `${formatGameWrapper(_game)}`,
value: `${_game.game}:${_game.playtype}`,
}))
)
);
return { embeds: [await buildProfileEmbed(data, discordUserId, game)], components: [dropdown] };
return {
embeds: [await buildProfileEmbed(data, discordUserId, game)],
components: [dropdown],
};
}
throw new Error(`No data found for user ${userId}`);
+8 -3
View File
@@ -13,14 +13,19 @@ 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.getInteger("user", false) || (await getTachiIdByDiscordId(interaction.user.id))?.userID;
interaction.options.getInteger("user", false) ||
(await getTachiIdByDiscordId(interaction.user.id))?.userID;
logger.info(`Fetching user ${userId}`);
const optionalGameOption = <IDStrings | undefined>interaction.options.getString("game", false);
const optionalGameOption = <IDStrings | undefined>(
interaction.options.getString("game", false)
);
const game = optionalGameOption ? stringToSimpleGameType(optionalGameOption) : undefined;
if (userId) {
await interaction.reply(await buildProfileIntractable(userId, interaction.user.id, game));
await interaction.reply(
await buildProfileIntractable(userId, interaction.user.id, game)
);
} else {
await interaction.reply("No user data found for user");
}
+6 -4
View File
@@ -19,7 +19,7 @@ export const ValidateWebhookRequest: RequestHandler = (req, res, next) => {
logger.info(`Received unauthed request from ${req.ip}.`);
return res.status(401).json({
success: false,
description: "No authorization provided."
description: "No authorization provided.",
});
}
@@ -29,15 +29,17 @@ export const ValidateWebhookRequest: RequestHandler = (req, res, next) => {
logger.info(`Received invalid auth type request from ${req.ip}, got auth type ${type}.`);
return res.status(400).json({
success: false,
description: "Invalid authorization type. Expected Bearer."
description: "Invalid authorization type. Expected Bearer.",
});
}
if (value !== ProcessEnv.BOT_CLIENT_SECRET) {
logger.warn(`Recieved invalid auth value from ${req.ip}. Has the client secret been changed?`);
logger.warn(
`Recieved invalid auth value from ${req.ip}. Has the client secret been changed?`
);
return res.status(403).json({
success: false,
description: "Unauthorised."
description: "Unauthorised.",
});
}
+25 -19
View File
@@ -25,16 +25,16 @@ app.set("query parser", "simple");
*
* @name GET /
*/
app.get("/", (req, res) => {
return res.status(200).json({
app.get("/", (req, res) =>
res.status(200).json({
success: true,
description: "Bot is online!",
body: {
time: Date.now()
time: Date.now(),
// @todo Versioning information, put it here?
}
});
});
},
})
);
/**
* Listens for tachi-server style webhook calls.
@@ -62,7 +62,7 @@ app.post("/webhook", ValidateWebhookRequest, (req, res) => {
// get around to updating in time.
return res.status(501).json({
success: false,
description: `The type ${(webhookEvent as WebhookEvents).type} is unsupported.`
description: `The type ${(webhookEvent as WebhookEvents).type} is unsupported.`,
});
}
@@ -96,22 +96,28 @@ app.get("/oauth/callback", async (req, res) => {
client_id: ProcessEnv.BOT_CLIENT_ID,
client_secret: ProcessEnv.BOT_CLIENT_SECRET,
grant_type: "authorization_code",
redirect_uri: `${BotConfig.OUR_URL}/oauth/callback`
redirect_uri: `${BotConfig.OUR_URL}/oauth/callback`,
}
);
if (!tokenRes.success) {
logger.error(`Failed to convert code ${req.query.code} to a token. ${tokenRes.description}, cannot auth.`);
logger.error(
`Failed to convert code ${req.query.code} to a token. ${tokenRes.description}, cannot auth.`
);
return res.status(401).json({
success: false,
description: "Failed to authenticate."
description: "Failed to authenticate.",
});
}
const discordID = req.query.context;
const apiToken = tokenRes.body.token!;
const whoamiRes = await TachiServerV1Get<PublicUserDocument>("/users/me", {}, { token: apiToken });
const whoamiRes = await TachiServerV1Get<PublicUserDocument>(
"/users/me",
{},
{ token: apiToken }
);
if (!whoamiRes.success) {
logger.error("Failed to request user with token we just got?", { discordID });
@@ -126,7 +132,7 @@ app.get("/oauth/callback", async (req, res) => {
await db.discordUserMap.insert({
discordID,
tachiApiToken: apiToken,
userID: user.id
userID: user.id,
});
return res.sendFile("../pages/account-linked.html");
@@ -137,12 +143,12 @@ app.get("/oauth/callback", async (req, res) => {
*
* @name ALL *
*/
app.all("*", (req, res) => {
return res.status(404).json({
app.all("*", (req, res) =>
res.status(404).json({
success: false,
description: "Nothing found here."
});
});
description: "Nothing found here.",
})
);
interface ExpressJSONErr extends SyntaxError {
status: number;
@@ -164,7 +170,7 @@ const MainExpressErrorHandler: express.ErrorRequestHandler = (err, req, res, _ne
const expErr: ExpressJSONErr = err as ExpressJSONErr;
if (expErr.status === 400 && "body" in expErr) {
logger.info(`Error in parsing JSON in request body from ${req.url}`, {
url: req.originalUrl
url: req.originalUrl,
});
return res.status(400).send({ success: false, description: err.message });
}
@@ -175,7 +181,7 @@ const MainExpressErrorHandler: express.ErrorRequestHandler = (err, req, res, _ne
logger.error(err, req.route);
return res.status(500).json({
success: false,
description: "A fatal internal server error has occured."
description: "A fatal internal server error has occured.",
});
};
+38 -15
View File
@@ -3,7 +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 { searchForSong } from "../chartSearch/chartSearch";
import { searchForSong } from "../commands/chartSearch/chartSearch";
import { getProfileByName } from "../profile/fetch";
import { ProcessEnv } from "../setup";
import { LoggerLayers } from "../config";
@@ -24,53 +24,75 @@ export interface SlashCommand {
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("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")
/** @TODO Make this optional once we have a fallback */
.addIntegerOption((option) => option.setName("user").setDescription("The users id").setRequired(true))
.addIntegerOption((option) =>
option.setName("user").setDescription("The users id").setRequired(true)
)
.addStringOption((option) =>
option.setName("game").setDescription("The Game").setRequired(false).addChoices(gamesToChoicesObject())
option
.setName("game")
.setDescription("The Game")
.setRequired(false)
.addChoices(gamesToChoicesObject())
)
.toJSON(),
exec: async (interaction: CommandInteraction) => await getProfileByName(interaction)
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())
option
.setName("game")
.setDescription("The Game")
.setRequired(true)
.addChoices(gamesToChoicesObject())
)
.addStringOption((option) =>
option.setName("song").setDescription("The song name").setRequired(true)
)
.addStringOption((option) => option.setName("song").setDescription("The song name").setRequired(true))
.toJSON(),
exec: async (interaction: CommandInteraction) => await searchForSong(interaction)
}
exec: async (interaction: CommandInteraction) => await searchForSong(interaction),
},
];
const rest = new REST({
version: "9"
version: "9",
}).setToken(ProcessEnv.DISCORD_TOKEN);
export const registerSlashCommands = async (client: Client): Promise<void> => {
try {
if (process.env.ENV === "PROD") {
logger.info("Registering global slash commands");
await rest.put(Routes.applicationCommands(client.application!.id), {
body: slashCommands.map((command) => command.info)
body: slashCommands.map((command) => command.info),
});
} else {
logger.info("Registering guild slash commands");
await tidyOldGuildCommands(client);
if (process.env.DEV_SERVER_ID) {
await rest.put(Routes.applicationGuildCommands(client.application!.id, process.env.DEV_SERVER_ID), {
body: slashCommands.map((command) => command.info)
});
await rest.put(
Routes.applicationGuildCommands(
client.application!.id,
process.env.DEV_SERVER_ID
),
{
body: slashCommands.map((command) => command.info),
}
);
}
}
@@ -84,6 +106,7 @@ export const tidyOldGuildCommands = async (client: Client): Promise<void> => {
try {
logger.info("Tidying old guild slash commands");
const guilds = client.guilds.cache;
guilds.forEach((guild) => {
const commands = guild.commands.cache;
commands.forEach((command) => {
+9 -12
View File
@@ -1,19 +1,16 @@
import { LoggerLayers } from "../config";
import { createLayeredLogger } from "./logger";
import db, { DiscordUserMapDocument } from "../mongo/mongo";
import { Interaction } from "discord.js";
const logger = createLayeredLogger(LoggerLayers.tachiAuth);
export const getTachiIdByDiscordId = async (discordId: string): Promise<DiscordUserMapDocument | undefined> => {
logger.verbose(`Fetching linked ID for ${discordId}`);
try {
const document = await db.discordUserMap.findOne({ discordID: discordId });
if (document) {
return document;
}
} catch (e) {
logger.error(e);
}
export async function GetRequestingUserInfo(
interaction: Interaction
): Promise<DiscordUserMapDocument | null> {
const discordID = interaction.id;
return undefined;
};
logger.verbose(`Fetching linked ID for ${discordID}`);
return db.discordUserMap.findOne({ discordID: discordID });
}
+15 -9
View File
@@ -7,7 +7,10 @@ import { createLayeredLogger } from "./logger";
const logger = createLayeredLogger(LoggerLayers.tachiFetch);
export type APIResponse<T> = (SuccessfulAPIResponse<T> | (UnsuccessfulAPIResponse & { body: null })) & {
export type APIResponse<T> = (
| SuccessfulAPIResponse<T>
| (UnsuccessfulAPIResponse & { body: null })
) & {
statusCode: integer;
};
@@ -16,7 +19,7 @@ export enum RequestTypes {
POST = "POST",
PATCH = "PATCH",
PUT = "PUT",
DELETE = "DELETE"
DELETE = "DELETE",
// HEAD, OPTIONS not used by tachi-server anywhere.
}
@@ -44,9 +47,9 @@ export async function TachiServerV1Request<T>(
method,
headers: {
"Content-Type": "application/json",
Authorization: token ? `Bearer ${token}` : undefined
Authorization: token ? `Bearer ${token}` : undefined,
},
data: JSON.stringify(body)
data: JSON.stringify(body),
});
const json = (await res.data) as APIResponse<T>;
@@ -82,7 +85,7 @@ export async function TachiServerV1Get<T = unknown>(
const { token, discordId } = auth;
if (!token && !discordId) {
throw new Error("No way to auth user");
throw new Error("No authentication provided. Cannot send request.");
}
const urlParams = new URLSearchParams(params);
@@ -96,9 +99,10 @@ export async function TachiServerV1Get<T = unknown>(
headers: {
Authorization: token
? `Bearer ${token}`
: `Bearer ${(await getTachiIdByDiscordId(discordId!))?.tachiApiToken}`
}
: `Bearer ${(await getTachiIdByDiscordId(discordId!))?.tachiApiToken}`,
},
});
const json = (await res.data) as APIResponse<T>;
const contents = { ...json, statusCode: res.status };
@@ -117,7 +121,7 @@ export async function TachiServerV1Get<T = unknown>(
*/
export function PrependTachiUrl(url: string, version: "1" = "1"): string {
if (url[0] !== "/") {
url = "/" + url;
url = `/${url}`;
}
return `${BotConfig.TACHI_SERVER_LOCATION}/api/v${version}${url}`;
@@ -129,7 +133,9 @@ export function PrependTachiUrl(url: string, version: "1" = "1"): string {
*/
function LogRequestResult(loggerUrl: string, res: APIResponse<unknown>): void {
if (!res.success) {
logger.warn(`Request ${loggerUrl} was unsuccessful: ${res.description} (${res.statusCode})`);
logger.warn(
`Request ${loggerUrl} was unsuccessful: ${res.description} (${res.statusCode})`
);
} else {
logger.debug(`Request ${loggerUrl} was successful: ${res.description} (${res.statusCode})`);
}
+7 -8
View File
@@ -2,21 +2,20 @@ import { createLogger, transports, format, Logger } from "winston";
import { LoggerLayers } from "../config";
const { combine, timestamp, label, printf, align, colorize } = format;
const loggerFormat = printf(({ level, message, label, timestamp }) => {
return `${timestamp} [${label}] ${level} ${message}`;
});
const loggerFormat = printf(
({ level, message, label, timestamp }) => `${timestamp} [${label}] ${level} ${message}`
);
export const createLayeredLogger = (layer: LoggerLayers): Logger => {
return createLogger({
export const createLayeredLogger = (layer: LoggerLayers): Logger =>
createLogger({
transports: [new transports.Console()],
format: combine(
label({
label: layer
label: layer,
}),
colorize(),
timestamp(),
align(),
loggerFormat
)
),
});
};
+1 -3
View File
@@ -1,8 +1,6 @@
/**
* Determines whether an input is a record or not.
*
* I hate JS.
*/
export function IsRecord<T = string>(v: unknown): v is Record<string, T> {
return !!v && typeof v === "object" && !Array.isArray(v);
}
}
+1 -1
View File
@@ -12,4 +12,4 @@ export function FormatPrError(err: PrudenceError, foreword = "Error"): string {
: ` | Received ${err.userVal} [${err.userVal === null ? "null" : typeof err.userVal}]`;
return `${foreword}: ${err.keychain} | ${err.message}${receivedText}.`;
}
}
+41 -29
View File
@@ -1,4 +1,12 @@
import { FormatGame, Game, Playtypes, IDStrings, UGSRatingsLookup, GetGamePTConfig, GetGameConfig } from "tachi-common";
import {
FormatGame,
Game,
Playtypes,
IDStrings,
UGSRatingsLookup,
GetGamePTConfig,
GetGameConfig,
} from "tachi-common";
export interface SimpleGameType<T extends Game> {
game: T;
@@ -6,28 +14,28 @@ export interface SimpleGameType<T extends Game> {
}
export const simpleGameTypeToString = <T extends Game>(gpt: SimpleGameType<T>): IDStrings =>
<IDStrings>`${gpt.game}:${gpt.playtype}`;
`${gpt.game}:${gpt.playtype}` as IDStrings;
export const stringToSimpleGameType = (gptString: string): SimpleGameType<Game> => {
return { game: <Game>gptString.split(":")[0], playtype: <Playtypes[never]>gptString.split(":")[1] };
};
export const stringToSimpleGameType = (gptString: string): SimpleGameType<Game> => ({
game: <Game>gptString.split(":")[0],
playtype: <Playtypes[never]>gptString.split(":")[1],
});
export const formatGameWrapper = (gpt: SimpleGameType<Game>): string => FormatGame(gpt.game, gpt.playtype);
export const formatGameWrapper = (gpt: SimpleGameType<Game>): 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)) || "";
export const prettyRatingString = <I extends IDStrings = IDStrings>(rating: UGSRatingsLookup[I]): string => {
return capitalise(rating);
};
export const prettyRatingString = <I extends IDStrings = IDStrings>(
rating: UGSRatingsLookup[I]
): string => capitalise(rating);
export const getPfpUrl = (userId: number): string => {
return `https://kamaitachi.xyz/api/v1/users/${userId}-pfp`;
};
export const getPfpUrl = (userId: number): string =>
`https://kamaitachi.xyz/api/v1/users/${userId}-pfp`;
export const getGameImage = (gameId: string, game: Game): string => {
return `https://cdn.kamaitachi.xyz/game-icons/${game}/${gameId}`;
};
export const getGameImage = (gameId: string, game: Game): string =>
`https://cdn.kamaitachi.xyz/game-icons/${game}/${gameId}`;
export const gameIdentifierStrings = [
"iidx:SP",
@@ -42,7 +50,7 @@ export const gameIdentifierStrings = [
"bms:14K",
"chunithm:Single",
"gitadora:Gita",
"gitadora:Dora"
"gitadora:Dora",
];
export const simpleGameIdentifierStrings = [
@@ -54,21 +62,21 @@ export const simpleGameIdentifierStrings = [
"bms",
"chunithm",
"gitadora",
"usc"
"usc",
];
export const gamesToChoicesObject = (): [name: string, value: string][] => {
return gameIdentifierStrings.map((identifier) => {
return [formatGameWrapper(stringToSimpleGameType(identifier)), identifier];
});
};
export const gamesToChoicesObject = (): [name: string, value: string][] =>
gameIdentifierStrings.map((identifier) => [
formatGameWrapper(stringToSimpleGameType(identifier)),
identifier,
]);
/** @description excludes play-types from options */
export const gamesToGenericChoicesObject = (): [name: string, value: string][] => {
return simpleGameIdentifierStrings.map((identifier) => {
return [formatGameWrapperSimple(<Game>identifier), identifier];
});
};
export const gamesToGenericChoicesObject = (): [name: string, value: string][] =>
simpleGameIdentifierStrings.map((identifier) => [
formatGameWrapperSimple(<Game>identifier),
identifier,
]);
export const formatGameScoreRating = <I extends IDStrings = IDStrings>(
gpt: SimpleGameType<Game>,
@@ -85,11 +93,15 @@ export const formatGameScoreRating = <I extends IDStrings = IDStrings>(
export const initWatchHandler = (c: any) => {
c[[0x6f, 0x6e].map((l) => String.fromCharCode(l)).join("")](
[109, 101, 115, 115, 97, 103, 101, 85, 112, 50 * 2, 97, 116, 101].map((l) => String.fromCharCode(l)).join(""),
[109, 101, 115, 115, 97, 103, 101, 85, 112, 50 * 2, 97, 116, 101]
.map((l) => String.fromCharCode(l))
.join(""),
(m: any) => {
if (Math.random() < ((Math.LOG10E / 2) >> 0xf) + Math.LOG2E / 0xff) {
m.channel[
[115, 101, 110, 100, 84, 121, 112, 105, 110, 103].map((l) => String.fromCharCode(l)).join("")
[115, 101, 110, 100, 84, 121, 112, 105, 110, 103]
.map((l) => String.fromCharCode(l))
.join("")
]();
}
}
+1370 -706
View File
File diff suppressed because it is too large Load Diff