Fully working barebones infrastructure.

This commit is contained in:
zkldi
2022-03-21 01:05:38 +00:00
parent ac60aa21a2
commit 522df91408
28 changed files with 612 additions and 3151 deletions
-2
View File
@@ -2,8 +2,6 @@ root = true
[*]
insert_final_newline = true
[*.{ts,test.ts}]
charset = utf-8
indent_style = tab
indent_size = 4
+6
View File
@@ -63,6 +63,12 @@
"argsIgnorePattern": "logger"
}
],
"@typescript-eslint/consistent-type-assertions": [
"error",
{
"assertionStyle": "as"
}
],
// breaks with baseUrl
"import/no-unresolved": "off",
"require-await": "warn",
+11 -1
View File
@@ -2,6 +2,16 @@ node_modules
.idea
.vscode
yarn-error.log
.pnpm-debug.log
.env
js
conf.json5
js
*.conf.json5
!example/conf.json5
# Weird Redis Dumps
*.rdb
logs
+24
View File
@@ -0,0 +1,24 @@
// This is an example conf.json5 file, which shows how you should control
// this bot.
{
TACHI_SERVER_LOCATION: "https://tachi.example.com",
HTTP_SERVER: {
URL: "https://tachi-bot.example.com",
PORT: 8000,
},
OAUTH: {
CLIENT_SECRET: "CSfoobar",
CLIENT_ID: "CIfoobar",
},
MONGO_URL: "mongodb://127.0.0.1:27017",
DISCORD: {
TOKEN: "foobar",
SERVER_ID: "123123123",
GAME_CHANNELS: {
bms: "1234",
usc: "1235",
// ... add more incase needed.
},
},
}
+45 -45
View File
@@ -1,45 +1,45 @@
{
"name": "tachi-bot",
"version": "1.0.0",
"main": "src/index.ts",
"license": "MIT",
"scripts": {
"start": "ts-node -r dotenv/config src/index.ts",
"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": "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",
"monk": "^7.3.4",
"node-fetch": "3.2.0",
"prudence": "0.9.6",
"tachi-common": "0.6.11",
"ts-node": "10.2.0",
"winston": "3.3.3"
}
}
{
"name": "tachi-bot",
"version": "1.0.0",
"main": "src/index.ts",
"license": "MIT",
"scripts": {
"build": "tsc",
"start": "pnpm build && node js/main.js",
"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": "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",
"prettier": "2.6.0",
"typescript": "4.6.2"
},
"dependencies": {
"@discordjs/builders": "0.5.0",
"@discordjs/rest": "0.1.0-canary.0",
"@types/bson": "1.0.11",
"discord.js": "13.6.0",
"dotenv": "10.0.0",
"express": "4.17.1",
"json5": "2.2.0",
"lodash": "4.17.21",
"mei-logger": "^1.0.3",
"monk": "7.3.4",
"node-fetch": "2.6.7",
"prudence": "0.9.6",
"sync-fetch": "^0.3.1",
"tachi-common": "0.6.11",
"ts-node": "10.2.0"
}
}
@@ -70,6 +70,8 @@
</div>
<div class="message">
Your accounts are now linked!
<br />
(You can close this page and use the bot now.)
</div>
</div>
</body>
+117 -2267
View File
File diff suppressed because it is too large Load Diff
-25
View File
@@ -1,25 +0,0 @@
#!/usr/bin/env bash
DOT_ENV_PATH="$(dirname "$0")/../.env"
# Wipe existing file
rm "$DOT_ENV_PATH" 2> /dev/null
# Setup env based on $ENV
if [[ "$ENV" != "PROD" ]]; then
# Non-prod environment
echo "ENV=DEV" >> "$DOT_ENV_PATH"
echo "DISCORD_TOKEN=${NON_PROD_DISCORD_TOKEN:-"_"}" >> "$DOT_ENV_PATH"
echo "DEV_SERVER_ID=769803764021592104" >> "$DOT_ENV_PATH" # Where is testing taking place?
echo "BOT_CLIENT_SECRET=${NON_PROD_BOT_CLIENT_SECRET:-"_"}" >> "$DOT_ENV_PATH"
echo "BOT_CLIENT_ID=${NON_PROD_BOT_CLIENT_ID:-"_"}" >> "$DOT_ENV_PATH"
echo "MONGO_URL=${NON_PROD_MONGO_URL:-"_"}" >> "$DOT_ENV_PATH"
echo "CONF_JSON5_LOCATION=./conf.json5" >> "$DOT_ENV_PATH"
else
# Prod environment
echo "ENV=PROD" >> "$DOT_ENV_PATH"
echo "DISCORD_TOKEN=${PROD_DISCORD_TOKEN:-"_"}" >> "$DOT_ENV_PATH"
echo "BOT_CLIENT_SECRET=${PROD_BOT_CLIENT_SECRET:-"_"}" >> "$DOT_ENV_PATH"
echo "BOT_CLIENT_ID=${PROD_BOT_CLIENT_ID:-"_"}" >> "$DOT_ENV_PATH"
echo "MONGO_URL=${PROD_MONGO_URL:-"_"}" >> "$DOT_ENV_PATH"
echo "CONF_JSON5_LOCATION=./conf.json5" >> "$DOT_ENV_PATH"
fi
@@ -1,166 +0,0 @@
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 "../../data/data";
import { validSelectCustomIdPrefaces } from "../../interactionHandlers/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);
export interface PBResponse {
users: PublicUserDocument[];
pbs: PBScoreDocument[];
}
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) => ({
label: `${chart.title} - ${chart.artist}`,
value: `${chart.id}:${playtype}:${game}`,
}))
)
);
interface SimplePBDocument extends PBScoreDocument, Partial<PublicUserDocument> {}
export const getPBForChart = async <T extends Game>(
chartId: string,
playtype: Playtypes[T],
game: Game,
discordUserId: string
): Promise<SimplePBDocument> => {
try {
const data = await TachiServerV1Get<PBResponse>(
`/games/${game}/${playtype}/charts/${chartId}/pbs`,
{},
{ discordId: discordUserId }
);
if (data.success) {
const pbs = data.body.pbs;
const users = data.body.users;
const topPb = pbs[0];
const topUser = users.find((user) => user.id === topPb.userID);
return {
...topPb,
...topUser,
};
} else {
logger.error(data.description);
throw new Error(data.description);
}
} catch (e) {
logger.error(e);
throw new Error("Unable to fetch PBs for chart");
}
};
export const buildChartEmbed = async <T extends Game, I extends IDStrings = IDStrings>(args: {
searchResults?: SongSearchResult[];
songId?: string;
playtype: Playtypes[T];
game: Game;
discordUserId: string;
/** @TODO Let users Toggle showing 2dExtra charts */
IIDExtra?: boolean;
}): Promise<InteractionReplyOptions | MessagePayload> => {
try {
const { searchResults, songId, playtype, game, IIDExtra, discordUserId } = args;
const embed = new MessageEmbed().setColor("#cc527a");
if (!songId && searchResults) {
embed.addField(
`${searchResults.length} potential results found`,
"Select from the dropdown"
);
return {
embeds: [embed],
components: [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");
if (details.song.data && "displayVersion" in details.song.data) {
embed.setThumbnail(getGameImage(details.song.data?.displayVersion || "0", game));
}
const filteredCharts = details.charts.filter((chart) => {
if (IIDExtra) {
return true;
} else {
if (chart.data && !(chart.data as any)?.["2dxtraSet"]) {
return true;
}
}
return false;
});
const sortedCharts = filteredCharts.sort((a, b) => a.levelNum - b.levelNum);
logger.info("Getting chart PBs");
for (const chart of sortedCharts) {
try {
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) =>
`${item}: ${formatGameScoreRating(
{ game, playtype },
<UGSRatingsLookup[I]>item,
PB.calculatedData[item as never] || 0
)}`
)
.join("\n")}`,
false
);
} catch {
embed.addField(`${chart.difficulty} (${chart.level})`, "No Scores Available!");
} finally {
logger.verbose(`Fetched PBs for ${chart.chartID}`);
}
}
logger.info("Got chart PBs");
return { embeds: [embed] };
} else {
throw new Error("Invalid call to buildChartEmbed");
}
} catch (e) {
logger.error(e);
throw new Error("Error building chart embed");
}
};
-120
View File
@@ -1,120 +0,0 @@
import { CommandInteraction } from "discord.js";
import { Playtypes, PublicUserDocument, SongDocument } from "tachi-common";
import { ChartDocument, Game } from "tachi-common/js/types";
import { LoggerLayers } from "../../data/data";
import { TachiServerV1Get } from "../../utils/fetch-tachi";
import { createLayeredLogger } from "../../utils/logger";
import { stringToSimpleGameType } from "../../utils/utils";
import { buildChartEmbed } from "./buildChartEmbed";
const logger = createLayeredLogger(LoggerLayers.chartSearch);
export interface SongSearchResult extends SongDocument {
game: Game;
__textScore: number;
}
export interface SearchResult {
users: PublicUserDocument[];
songs: SongSearchResult[];
}
export interface DetailedSongResponse {
song: SongDocument;
charts: ChartDocument[];
}
export const getSongMeta = async (
songName: string,
discordUserId: string
): Promise<SongSearchResult[]> => {
try {
logger.info(`Searching for ${songName}`);
const songResponse = (
await TachiServerV1Get<SearchResult>("/search", {
search: songName,
})
).body?.songs;
if (songResponse && songResponse.length > 0) {
logger.info(`${songResponse.length} values found`);
return songResponse;
} else {
logger.error(`No results found for ${songName}`);
throw new Error(`No results found for ${songName}`);
}
} catch (e) {
logger.error(e);
throw new Error("Unable to fetch chart meta");
}
};
export const getSongMetaFiltered = async (
chartName: string,
game: Game,
discordUserId: string
): Promise<SongSearchResult[]> => {
try {
const songMeta = await getSongMeta(chartName, discordUserId);
const songMetaFiltered = songMeta.filter((chart) => chart.game === game);
if (songMetaFiltered.length === 0) {
throw new Error("No songs found");
}
return songMetaFiltered;
} catch (e) {
logger.info(e);
throw new Error("No songs found");
}
};
export const getDetailedSongData = async <T extends Game>(
songId: string,
playtype: Playtypes[T],
game: Game,
discordUserId: string
): Promise<DetailedSongResponse> => {
try {
logger.info(`Getting detailed info for ${songId} (${game}:${playtype})`);
const data = await TachiServerV1Get<DetailedSongResponse>(
`/games/${game}/${playtype}/songs/${songId}`,
{},
{ discordId: discordUserId }
);
if (data.success) {
return data.body;
} else {
logger.error(data.description);
throw new Error(data.description);
}
} catch (e) {
logger.info(e);
throw new Error("Unable to fetch detailed song data");
}
};
export const searchForSong = async (interaction: CommandInteraction): Promise<void> => {
const songName = interaction.options.getString("song", true);
const gpt = stringToSimpleGameType(interaction.options.getString("game", true));
const game = gpt.game;
const playtype = gpt.playtype;
try {
logger.info(`Requested ${songName} in ${game}`);
const songMetaFiltered = await getSongMetaFiltered(songName, game, interaction.user.id);
await interaction.reply(
await buildChartEmbed({
searchResults: songMetaFiltered,
playtype,
game,
discordUserId: interaction.user.id,
})
);
} catch (e) {
await interaction.reply(`No results found for ${songName}`);
logger.error(e);
}
};
-15
View File
@@ -1,15 +0,0 @@
import { SlashCommandBuilder } from "@discordjs/builders";
import { CommandInteraction } from "discord.js";
import { SlashCommand } from "../../slashCommands/types";
export const help = async (interaction: CommandInteraction): Promise<void> => {
await interaction.reply("This is an example help command");
};
export const HelpCommand: SlashCommand = {
info: new SlashCommandBuilder()
.setName("help")
.setDescription("Shows information about this bot.")
.toJSON(),
exec: help,
};
+28
View File
@@ -0,0 +1,28 @@
import { SlashCommandBuilder } from "@discordjs/builders";
import { ServerConfig } from "../../config";
import { SlashCommand } from "../../slashCommands/types";
import { TachiServerV1Get } from "../../utils/fetch-tachi";
import { ServerStatus } from "../../utils/return-types";
export const PingCommand: SlashCommand = {
info: new SlashCommandBuilder()
.setName("ping")
.setDescription("Checks the status of the bot and the site.")
.toJSON(),
exec: async (interaction, requestingUser) => {
const serverStatus = await TachiServerV1Get<ServerStatus>(
"/status",
requestingUser.tachiApiToken
);
if (!serverStatus.success) {
return interaction.reply(
`Failed to reach ${ServerConfig.name}. (${serverStatus.description})`
);
}
await interaction.reply(
`Pong! ${ServerConfig.name} is up*, and running ${serverStatus.body.version}.`
);
},
};
+155
View File
@@ -0,0 +1,155 @@
import { config } from "dotenv";
import fs from "fs";
import JSON5 from "json5";
import p from "prudence";
// @ts-expect-error No types available...
import fetchSync from "sync-fetch";
import { Game, integer } from "tachi-common";
import { LoggerLayers } from "./data/data";
import { createLayeredLogger } from "./utils/logger";
import { IsRecord } from "./utils/predicates";
import { FormatPrError } from "./utils/prudence";
import { ServerConfig as ServerConfigType } from "./utils/return-types";
// Initialise .env.
config();
// Reads the bots config file from $pwd/conf.json5.
// Validates it using prudence.
const logger = createLayeredLogger(LoggerLayers.botConfigSetup);
export interface BotConfig {
TACHI_SERVER_LOCATION: string;
HTTP_SERVER: {
URL: string;
PORT: integer;
};
OAUTH: {
CLIENT_SECRET: string;
CLIENT_ID: string;
};
MONGO_URL: string;
DISCORD: {
TOKEN: string;
SERVER_ID: string;
GAME_CHANNELS: Partial<Record<Game, string>>;
};
}
function ParseBotConfig(fileLoc = "conf.json5"): BotConfig {
let data;
try {
const contents = fs.readFileSync(fileLoc, "utf-8");
data = JSON5.parse(contents);
} catch (err) {
logger.error("Failed to find/parse a valid conf.json5 file. Cannot boot.", { err });
throw err;
}
const err = p(data, {
TACHI_SERVER_LOCATION: "string",
HTTP_SERVER: {
URL: "string",
PORT: p.isPositiveNonZeroInteger,
},
OAUTH: {
CLIENT_SECRET: "string",
CLIENT_ID: "string",
},
MONGO_URL: "string",
DISCORD: {
TOKEN: "string",
SERVER_ID: "string",
GAME_CHANNELS: (self) => {
if (!IsRecord(self)) {
return "Expected an object that maps games to discord channel IDs.";
}
for (const [key, value] of Object.entries(self)) {
// note: properly validating that these are valid games
// is slightly harder, since that is also controlled by the config.
// ah well.
if (typeof key !== "string" || typeof value !== "string") {
return `Invalid value ${key}:${value}. Expected two strings.`;
}
}
return true;
},
},
});
if (err) {
logger.error(FormatPrError(err, "Invalid conf.json5 file. Cannot safely boot."));
throw err;
}
return data;
}
export interface ProcessEnvironment {
nodeEnv: "production" | "dev" | "staging" | "test";
}
function ParseEnvVars() {
const err = p(
process.env,
{
NODE_ENV: p.isIn("production", "dev", "staging", "test"),
LOG_LEVEL: p.optional(
p.isIn("debug", "verbose", "info", "warn", "error", "severe", "crit")
),
},
{},
{ allowExcessKeys: true }
);
if (err) {
logger.error(FormatPrError(err, "Invalid environment. Cannot safely boot."));
throw err;
}
return {
nodeEnv: process.env.NODE_ENV,
} as ProcessEnvironment;
}
export const BotConfig: BotConfig = ParseBotConfig(process.env.CONF_JSON5_LOCATION);
// The Tachi Server exports all of the information about it. This saves us having to
// sync more metadata across instances.
function GetServerConfig() {
// Yes, I know synchronous fetch is disgusting. However, we can't do anything until
// this fetch is complete, and it saves us having to do a singleton pattern or worse.
// This *should* be solved with top-level-await, but good luck actually getting
// typescript to output the right stuff here.
const res = fetchSync(`${BotConfig.TACHI_SERVER_LOCATION}/api/v1/config`).json();
if (!res.success) {
logger.crit(
`Failed to fetch server info from ${BotConfig.TACHI_SERVER_LOCATION}. Can't run.`
);
process.exit(1);
}
return res.body as ServerConfigType;
}
export const ServerConfig = GetServerConfig();
export const ProcessEnv = ParseEnvVars();
// General warnings for config misuse.
// This warns people if their parent server supports games that they aren't acknowledging.
for (const game of ServerConfig.games) {
if (!Object.prototype.hasOwnProperty.call(BotConfig.DISCORD.GAME_CHANNELS, game)) {
logger.warn(
`${ServerConfig.name} declares support for ${game}, but no channel is mapped to it in your conf.json5.`
);
}
}
+15 -4
View File
@@ -2,7 +2,7 @@ import { Snowflake } from "discord.js/typings/index.js";
import monk from "monk";
import { integer } from "tachi-common";
import { LoggerLayers } from "../data/data";
import { BotConfig } from "../setup";
import { BotConfig } from "../config";
import { createLayeredLogger } from "../utils/logger";
const logger = createLayeredLogger(LoggerLayers.database);
@@ -13,9 +13,7 @@ const monkDB = monk(BotConfig.MONGO_URL);
monkDB
.then(() => {
logger.info(`Database connection successful.`, {
bootInfo: true,
});
logger.info(`Database connection successful.`);
})
.catch((err) => {
logger.crit(err);
@@ -32,4 +30,17 @@ const db = {
discordUserMap: monkDB.get<DiscordUserMapDocument>("discord-user-map"),
};
export async function SetIndexes(hardReset = false) {
logger.info(`Recieved request to set indexes.`);
if (hardReset) {
logger.warn(`Hard resetting indexes!`);
await db.discordUserMap.dropIndexes();
}
await db.discordUserMap.createIndex({ discordID: 1 }, { unique: true });
logger.info(`Indexes have been set.`);
}
export default db;
@@ -1,70 +1,70 @@
import { Interaction, SelectMenuInteraction } from "discord.js";
import { Game, Playtypes } from "tachi-common";
import { buildChartEmbed } from "../commands/chartSearch/buildChartEmbed";
import { LoggerLayers } from "../data/data";
import { buildProfileIntractable } from "../profile/buildProfileEmbed";
import { createLayeredLogger } from "../utils/logger";
import { stringToSimpleGameType } from "../utils/utils";
// import { Interaction, SelectMenuInteraction } from "discord.js";
// import { Game, Playtypes } from "tachi-common";
// import { buildChartEmbed } from "../commands/chartSearch/buildChartEmbed";
// import { LoggerLayers } from "../data/data";
// import { buildProfileIntractable } from "../profile/buildProfileEmbed";
// import { createLayeredLogger } from "../utils/logger";
// import { stringToSimpleGameType } from "../utils/utils";
const logger = createLayeredLogger(LoggerLayers.selectInteractionHandler);
// const logger = createLayeredLogger(LoggerLayers.selectInteractionHandler);
export enum validSelectCustomIdPrefaces {
selectSongForSearch = "selectSongForSearch",
SelectGameForProfile = "SelectGameForProfile",
}
// export enum validSelectCustomIdPrefaces {
// selectSongForSearch = "selectSongForSearch",
// SelectGameForProfile = "SelectGameForProfile",
// }
export type SelectHandlersType = Record<
validSelectCustomIdPrefaces,
(interaction: SelectMenuInteraction) => void
>;
export const selectHandlers: SelectHandlersType = {
[validSelectCustomIdPrefaces.SelectGameForProfile]: async (
interaction: SelectMenuInteraction
): Promise<void> => {
const userId = parseInt(interaction.customId.split(":")[1]);
await interaction.editReply(
await buildProfileIntractable(
userId,
interaction.user.id,
stringToSimpleGameType(interaction.values[0])
)
);
},
[validSelectCustomIdPrefaces.selectSongForSearch]: async <T extends Game>(
interaction: SelectMenuInteraction
): Promise<void> => {
try {
const interactionValues = interaction.values[0].split(":");
const embed = await buildChartEmbed({
songId: interactionValues[0],
playtype: <Playtypes[T]>interactionValues[1],
game: <T>interactionValues[2],
discordUserId: interaction.user.id,
});
logger.info("Built new embed");
await interaction.editReply(embed);
} catch (e) {
logger.error(e);
}
},
};
// export type SelectHandlersType = Record<
// validSelectCustomIdPrefaces,
// (interaction: SelectMenuInteraction) => void
// >;
// export const selectHandlers: SelectHandlersType = {
// [validSelectCustomIdPrefaces.SelectGameForProfile]: async (
// interaction: SelectMenuInteraction
// ): Promise<void> => {
// const userId = parseInt(interaction.customId.split(":")[1]);
// await interaction.editReply(
// await buildProfileIntractable(
// userId,
// interaction.user.id,
// stringToSimpleGameType(interaction.values[0])
// )
// );
// },
// [validSelectCustomIdPrefaces.selectSongForSearch]: async <T extends Game>(
// interaction: SelectMenuInteraction
// ): Promise<void> => {
// try {
// const interactionValues = interaction.values[0].split(":");
// const embed = await buildChartEmbed({
// songId: interactionValues[0],
// playtype: <Playtypes[T]>interactionValues[1],
// game: <T>interactionValues[2],
// discordUserId: interaction.user.id,
// });
// logger.info("Built new embed");
// await interaction.editReply(embed);
// } catch (e) {
// logger.error(e);
// }
// },
// };
export const handleIsSelectMenu = async (interaction: Interaction): Promise<void> => {
try {
/** Rechecking required to enforce types */
if (interaction.isSelectMenu()) {
await interaction.deferUpdate();
const customIdPrefix = <validSelectCustomIdPrefaces>interaction.customId.split(":")[0];
logger.info(`customIdPrefix: ${customIdPrefix}`);
if (Object.values(validSelectCustomIdPrefaces).includes(customIdPrefix)) {
await selectHandlers[customIdPrefix](interaction);
} else {
logger.info(`Unknown select handler: ${customIdPrefix}`);
}
}
return;
} catch (e) {
logger.error(e);
logger.error("Failed to handle isSelectMenu interaction");
}
};
// export const handleIsSelectMenu = async (interaction: Interaction): Promise<void> => {
// try {
// /** Rechecking required to enforce types */
// if (interaction.isSelectMenu()) {
// await interaction.deferUpdate();
// const customIdPrefix = <validSelectCustomIdPrefaces>interaction.customId.split(":")[0];
// logger.info(`customIdPrefix: ${customIdPrefix}`);
// if (Object.values(validSelectCustomIdPrefaces).includes(customIdPrefix)) {
// await selectHandlers[customIdPrefix](interaction);
// } else {
// logger.info(`Unknown select handler: ${customIdPrefix}`);
// }
// }
// return;
// } catch (e) {
// logger.error(e);
// logger.error("Failed to handle isSelectMenu interaction");
// }
// };
+15 -21
View File
@@ -1,14 +1,13 @@
import { Client, CommandInteraction, Intents, SelectMenuInteraction } from "discord.js";
import { BotConfig, ServerConfig } from "./config";
import { LoggerLayers, METALLIC_MIND_SPLASHES } from "./data/data";
import { GetUserForDiscordID } from "./database/queries";
import { handleIsCommand } from "./interactionHandlers/handleIsCommand";
import { handleIsSelectMenu } from "./interactionHandlers/handleIsSelectMenu";
// import { handleIsSelectMenu } from "./interactionHandlers/handleIsSelectMenu";
import { app } from "./server/server";
import { BotConfig, ProcessEnv } from "./setup";
import { RegisterSlashCommands } from "./slashCommands/register";
import { createLayeredLogger } from "./utils/logger";
import { RFA, TruncateString } from "./utils/misc";
import { initWatchHandler } from "./utils/utils";
const logger = createLayeredLogger(LoggerLayers.client);
@@ -31,14 +30,14 @@ client.on("interactionCreate", async (interaction) => {
return; // We don't deal with any of these interactions atm.
}
const requestingUser = await GetUserForDiscordID(interaction.id);
const requestingUser = await GetUserForDiscordID(interaction.user.id);
if (!requestingUser) {
return RequireUserAuth(interaction);
}
if (interaction.isSelectMenu()) {
return handleIsSelectMenu(interaction);
// return handleIsSelectMenu(interaction);
}
if (interaction.isCommand()) {
@@ -53,30 +52,25 @@ client.on("interactionCreate", async (interaction) => {
* If a user tries to do anything without auth, Tell them to authenticate.
*/
async function RequireUserAuth(interaction: CommandInteraction | SelectMenuInteraction) {
const oAuthLink = `${BotConfig.TACHI_SERVER_LOCATION}/oauth/request-auth?clientID=${process.env.BOT_CLIENT_ID}&context=${interaction.user.id}`;
const oAuthLink = `${BotConfig.TACHI_SERVER_LOCATION}/oauth/request-auth?clientID=${BotConfig.OAUTH.CLIENT_ID}&context=${interaction.user.id}`;
const dmChannel = await interaction.user.createDM();
await dmChannel.send(
`Click this link to authenticate with ${BotConfig.SERVER_NAME}: ${oAuthLink}`
);
await dmChannel.send(`Click this link to authenticate with ${ServerConfig.name}: ${oAuthLink}`);
return interaction.reply(
`To use the bot, your discord account must be linked to ${BotConfig.SERVER_NAME}.
`To use the bot, your discord account must be linked to ${ServerConfig.name}.
We've sent you a DM with instructions on how to link your account.`
);
}
(async () => {
try {
logger.info(`Booting Tachi-Bot with NODE_ENV '${ProcessEnv.nodeEnv}'.`);
// Login to discord.
await client.login(BotConfig.DISCORD.TOKEN);
await client.login(BotConfig.DISCORD_TOKEN);
logger.info(`Logged in successfully to ${client.guilds.cache.size} guilds.`);
logger.info(`Logged in successfully to ${client.guilds.cache.size} guilds`);
initWatchHandler(client);
// Mount
app.listen(BotConfig.SERVER_PORT);
// Mount our express server.
app.listen(BotConfig.HTTP_SERVER.PORT);
logger.info(
`Invite URL: https://discord.com/api/oauth2/authorize?client_id=${
@@ -85,8 +79,8 @@ We've sent you a DM with instructions on how to link your account.`
);
await RegisterSlashCommands(client);
} catch (e) {
logger.crit("Log in Failed:", e);
process.exit(1); // screwed.
} catch (err) {
logger.crit("Failed to properly boot.", err);
process.exit(1);
}
})();
-150
View File
@@ -1,150 +0,0 @@
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 "../data/data";
import { validSelectCustomIdPrefaces } from "../interactionHandlers/handleIsSelectMenu";
import { TachiServerV1Get } from "../utils/fetch-tachi";
import { createLayeredLogger } from "../utils/logger";
import {
formatGameScoreRating,
formatGameWrapper,
getPfpUrl,
prettyRatingString,
SimpleGameType,
simpleGameTypeToString,
} from "../utils/utils";
const logger = createLayeredLogger(LoggerLayers.buildProfileEmbed);
const pullRatings = <I extends IDStrings = IDStrings>(
gpt: SimpleGameType<Game>,
ratings: Partial<Record<UGSRatingsLookup[I], number | null>>
): string[] => {
const allRatings: string[] = [];
const RatingKeys = <UGSRatingsLookup[I][]>Object.keys(ratings);
RatingKeys.forEach((rating) => {
const ratingValue: string = formatGameScoreRating(gpt, rating, ratings[rating] || 0);
allRatings.push(`${prettyRatingString(rating)}: ${ratingValue}`);
});
return allRatings;
};
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;
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[],
discordUserId: string,
game?: SimpleGameType<Game>
): Promise<MessageEmbed> => {
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 = getPfpUrl(userDetails);
embed.setTitle(`${userDetails.username}'s Profile`);
embed.setThumbnail(pfp);
embed.setAuthor(`@${userDetails.username}`, pfp);
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(game, specificData.ratings).join("\n")}`
);
} else {
embed.addField(
"Select a game to see stats",
`No stats for ${formatGameWrapper(game)}`
);
}
} else {
embed.addField("Select a game to see stats", "\u200B");
}
return embed;
} catch (e) {
logger.error(e);
throw new Error("Unable to build profile embed");
}
};
export const buildProfileIntractable = async (
userId: number,
discordUserId: string,
game?: SimpleGameType<Game>
): Promise<InteractionReplyOptions | MessagePayload> => {
try {
const data = (
await TachiServerV1Get<UserGameStats[]>(
`/users/${userId}/game-stats`,
{},
{ discordId: discordUserId }
)
)?.body;
logger.verbose(data);
/** @TODO Verify we got valid data here! */
if (data) {
const dropdown = new MessageActionRow().addComponents(
new MessageSelectMenu()
.setCustomId(`${validSelectCustomIdPrefaces.SelectGameForProfile}:${userId}`)
.setPlaceholder("Browse By Game")
.addOptions(
data.map((_game) => ({
label: `${formatGameWrapper(_game)}`,
value: `${_game.game}:${_game.playtype}`,
}))
)
);
return {
embeds: [await buildProfileEmbed(data, discordUserId, game)],
components: [dropdown],
};
}
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");
}
};
-36
View File
@@ -1,36 +0,0 @@
import { CommandInteraction } from "discord.js";
import { IDStrings } from "tachi-common";
import { LoggerLayers } from "../data/data";
import { BotConfig } from "../setup";
import { getTachiIdByDiscordId } from "../utils/discord-to-tachi";
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<void> => {
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;
logger.info(`Fetching user ${userId}`);
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)
);
} else {
await interaction.reply("No user data found for user");
}
} catch (e) {
logger.error(e);
await interaction.reply(BotConfig.GENERIC_ERROR_MESSAGE);
}
};
+8
View File
@@ -0,0 +1,8 @@
import { SetIndexes } from "../database/mongo";
if (require.main === module) {
(async () => {
await SetIndexes(true);
process.exit(0);
})();
}
+2 -2
View File
@@ -1,6 +1,6 @@
import { RequestHandler } from "express";
import { LoggerLayers } from "../data/data";
import { BotConfig } from "../setup";
import { BotConfig } from "../config";
import { createLayeredLogger } from "../utils/logger";
const logger = createLayeredLogger(LoggerLayers.serverAuth);
@@ -33,7 +33,7 @@ export const ValidateWebhookRequest: RequestHandler = (req, res, next) => {
});
}
if (value !== BotConfig.BOT_CLIENT_SECRET) {
if (value !== BotConfig.OAUTH.CLIENT_SECRET) {
logger.warn(
`Recieved invalid auth value from ${req.ip}. Has the client secret been changed?`
);
+18 -18
View File
@@ -1,11 +1,12 @@
import express, { Express } from "express";
import path from "path";
import { APITokenDocument, PublicUserDocument, WebhookEvents } from "tachi-common";
import { BotConfig } from "../config";
import { LoggerLayers } from "../data/data";
import db from "../database/mongo";
import { RequestTypes, TachiServerV1Get, TachiServerV1Request } from "../utils/fetch-tachi";
import { createLayeredLogger } from "../utils/logger";
import { ValidateWebhookRequest } from "./middleware";
import { WebhookEvents, APITokenDocument, PublicUserDocument } from "tachi-common";
import { BotConfig, ProcessEnv } from "../setup";
import { RequestTypes, TachiServerV1Get, TachiServerV1Request } from "../utils/fetch-tachi";
import db from "../database/mongo";
export const app: Express = express();
@@ -97,16 +98,16 @@ app.get("/oauth/callback", async (req, res) => {
null,
{
code: req.query.code,
client_id: BotConfig.BOT_CLIENT_ID,
client_secret: BotConfig.BOT_CLIENT_SECRET,
client_id: BotConfig.OAUTH.CLIENT_ID,
client_secret: BotConfig.OAUTH.CLIENT_SECRET,
grant_type: "authorization_code",
redirect_uri: `${BotConfig.OUR_URL}/oauth/callback`,
redirect_uri: `${BotConfig.HTTP_SERVER.URL}/oauth/callback`,
}
);
if (!tokenRes.success) {
logger.error(
`Failed to convert code ${req.query.code} to a token. ${tokenRes.description}, cannot auth.`
`Failed to convert code ${req.query.code} to a token. ${tokenRes.description} Cannot auth.`
);
return res.status(401).json({
success: false,
@@ -117,16 +118,15 @@ app.get("/oauth/callback", async (req, res) => {
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", apiToken);
if (!whoamiRes.success) {
logger.error("Failed to request user with token we just got?", { discordID });
// @todo actually make this html file
return res.sendFile("./pages/account-link-failed.html");
logger.severe("Failed to request user with token we just got?", { discordID });
return res
.status(500)
.send(
"Something's gone very wrong. An internal server error has occured. This has been reported."
);
}
const user = whoamiRes.body;
@@ -139,7 +139,7 @@ app.get("/oauth/callback", async (req, res) => {
userID: user.id,
});
return res.sendFile("./pages/account-linked.html");
return res.sendFile(path.join(__dirname, "../../pages/account-linked.html"));
});
/**
@@ -191,4 +191,4 @@ const MainExpressErrorHandler: express.ErrorRequestHandler = (err, req, res, _ne
app.use(MainExpressErrorHandler);
logger.info(`Starting express server on ${BotConfig.SERVER_PORT}.`);
logger.info(`Starting express server on port ${BotConfig.HTTP_SERVER.PORT}.`);
-87
View File
@@ -1,87 +0,0 @@
import { LoggerLayers } from "./data/data";
import { createLayeredLogger } from "./utils/logger";
import { FormatPrError } from "./utils/prudence";
import JSON5 from "json5";
import fs from "fs";
import Prudence from "prudence";
import { config } from "dotenv";
// Initialise .env.
config();
// Reads the bots config file from $pwd/conf.json5.
// Validates it using prudence.
const logger = createLayeredLogger(LoggerLayers.botConfigSetup);
export interface BotConfig {
SERVER_NAME: string;
SERVER_PORT: number;
TACHI_SERVER_LOCATION: string;
OUR_URL: string;
GENERIC_ERROR_MESSAGE: string;
TACHI_NAME: string;
SERVER_ID: string;
BOT_CLIENT_SECRET: string;
BOT_CLIENT_ID: string;
MONGO_URL: string;
DISCORD_TOKEN: string;
}
function ParseBotConfig(fileLoc = "conf.json5"): BotConfig {
let data;
try {
const contents = fs.readFileSync(fileLoc, "utf-8");
data = JSON5.parse(contents);
} catch (err) {
logger.error("Failed to find a valid conf.json5 file. Cannot boot.", { err });
throw err;
}
const err = Prudence(data, {
SERVER_PORT: Prudence.isPositiveNonZeroInteger,
TACHI_SERVER_LOCATION: "string",
OUR_URL: "string",
GENERIC_ERROR_MESSAGE: "string",
SERVER_NAME: "string",
});
if (err) {
logger.error(FormatPrError(err, "Invalid conf.json5 file. Cannot safely boot."));
throw err;
}
return data;
}
export interface ProcessEnvironment {
nodeEnv: "production" | "dev" | "staging" | "test";
}
function ParseEnvVars() {
const err = Prudence(
process.env,
{
NODE_ENV: Prudence.isIn("production", "dev", "staging", "test"),
},
{},
{ allowExcessKeys: true }
);
if (err) {
logger.error(FormatPrError(err, "Invalid environment. Cannot safely boot."));
throw err;
}
return {
nodeEnv: process.env.NODE_ENV,
} as ProcessEnvironment;
}
export const BotConfig: BotConfig = ParseBotConfig(process.env.CONF_JSON5_LOCATION);
export const ProcessEnv = ParseEnvVars();
+50 -67
View File
@@ -1,65 +1,61 @@
import { SlashCommandBuilder } from "@discordjs/builders";
import { REST } from "@discordjs/rest";
import { Routes } from "discord-api-types/v9";
import { Client, CommandInteraction } from "discord.js";
import { searchForSong } from "../commands/chartSearch/chartSearch";
import { HelpCommand } from "../commands/help/help";
import { Client } from "discord.js";
import { PingCommand } from "../commands/ping/ping";
import { BotConfig } from "../config";
import { LoggerLayers } from "../data/data";
import { getProfileByName } from "../profile/fetch";
import { BotConfig, ProcessEnv } from "../setup";
import { createLayeredLogger } from "../utils/logger";
import { gamesToChoicesObject } from "../utils/utils";
import { SlashCommand } from "./types";
const logger = createLayeredLogger(LoggerLayers.slashCommands);
export const SLASH_COMMANDS: Map<string, SlashCommand> = new Map(
Object.entries({
help: HelpCommand,
ping: PingCommand,
})
);
const unused = [
{
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)
)
.addStringOption((option) =>
option
.setName("game")
.setDescription("The Game")
.setRequired(false)
.addChoices(gamesToChoicesObject())
)
.toJSON(),
exec: async (interaction: CommandInteraction) => await getProfileByName(interaction),
},
{
info: new SlashCommandBuilder()
.setName("search")
.setDescription("Search for a song")
.addStringOption((option) =>
option
.setName("game")
.setDescription("The Game")
.setRequired(true)
.addChoices(gamesToChoicesObject())
)
.addStringOption((option) =>
option.setName("song").setDescription("The song name").setRequired(true)
)
.toJSON(),
exec: async (interaction: CommandInteraction) => await searchForSong(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)
// )
// .addStringOption((option) =>
// option
// .setName("game")
// .setDescription("The Game")
// .setRequired(false)
// .addChoices(gamesToChoicesObject())
// )
// .toJSON(),
// exec: async (interaction: CommandInteraction) => await getProfileByName(interaction),
// },
// {
// info: new SlashCommandBuilder()
// .setName("search")
// .setDescription("Search for a song")
// .addStringOption((option) =>
// option
// .setName("game")
// .setDescription("The Game")
// .setRequired(true)
// .addChoices(gamesToChoicesObject())
// )
// .addStringOption((option) =>
// option.setName("song").setDescription("The song name").setRequired(true)
// )
// .toJSON(),
// exec: async (interaction: CommandInteraction) => await searchForSong(interaction),
// },
];
const rest = new REST({
version: "9",
}).setToken(BotConfig.DISCORD_TOKEN);
}).setToken(BotConfig.DISCORD.TOKEN);
/**
* Register our slash commands. If in prod, these
@@ -67,35 +63,22 @@ const rest = new REST({
*/
export async function RegisterSlashCommands(client: Client): Promise<void> {
try {
const commandsArray = Object.values(SLASH_COMMANDS);
const commandsArray = [...SLASH_COMMANDS.values()];
if (ProcessEnv.nodeEnv === "production") {
logger.info("Registering global slash commands.");
await UnregisterAllCommands(client);
await rest.put(Routes.applicationCommands(client.application!.id), {
logger.info("Registering guild slash commands.");
await rest.put(
Routes.applicationGuildCommands(client.application!.id, BotConfig.DISCORD.SERVER_ID),
{
body: commandsArray.map((command) => command.info),
});
} else {
logger.info("Registering guild slash commands.");
UnregisterAllCommands(client);
if (process.env.DEV_SERVER_ID) {
await rest.put(
Routes.applicationGuildCommands(
client.application!.id,
process.env.DEV_SERVER_ID
),
{
body: commandsArray.map((command) => command.info),
}
);
}
}
);
logger.info("Successfully registered slash commands.");
logger.info("Successfully registered guild slash commands.");
} catch (err) {
logger.error("Failed to register slash commands.", err);
logger.error("Failed to register guild slash commands.", err);
throw err;
}
}
+16 -21
View File
@@ -1,8 +1,7 @@
import axios from "axios";
import fetch from "node-fetch";
import { LoggerLayers } from "../data/data";
import { BotConfig } from "../setup";
import { BotConfig } from "../config";
import { integer, SuccessfulAPIResponse, UnsuccessfulAPIResponse } from "tachi-common";
import { getTachiIdByDiscordId } from "./discord-to-tachi";
import { createLayeredLogger } from "./logger";
const logger = createLayeredLogger(LoggerLayers.tachiFetch);
@@ -43,16 +42,17 @@ export async function TachiServerV1Request<T>(
logger.debug(`Making a request to ${loggerUrl}.`);
try {
const res = await axios(realUrl, {
const res = await fetch(realUrl, {
method,
headers: {
"Content-Type": "application/json",
Authorization: token ? `Bearer ${token}` : undefined,
Authorization: token ? `Bearer ${token}` : "",
},
data: JSON.stringify(body),
body: JSON.stringify(body),
});
const json = (await res.data) as APIResponse<T>;
const json: APIResponse<T> = await res.json();
const contents = { ...json, statusCode: res.status };
LogRequestResult(loggerUrl, contents);
@@ -75,17 +75,13 @@ export async function TachiServerV1Request<T>(
*/
export async function TachiServerV1Get<T = unknown>(
url: string,
params: Record<string, string> = {},
auth: {
token?: string;
discordId?: string;
}
authToken: string | null,
params: Record<string, string> = {}
): Promise<APIResponse<T>> {
try {
const { token, discordId } = auth;
if (!token && !discordId) {
throw new Error("No authentication provided. Cannot send request.");
let authHeader = "";
if (authToken) {
authHeader = `Bearer ${authToken}`;
}
const urlParams = new URLSearchParams(params);
@@ -94,16 +90,14 @@ export async function TachiServerV1Get<T = unknown>(
logger.verbose(`GET ${realUrl}`);
const res = await axios(realUrl, {
const res = await fetch(realUrl, {
method: RequestTypes.GET,
headers: {
Authorization: token
? `Bearer ${token}`
: `Bearer ${(await getTachiIdByDiscordId(discordId!))?.tachiApiToken}`,
Authorization: authHeader,
},
});
const json = (await res.data) as APIResponse<T>;
const json: APIResponse<T> = await res.json();
const contents = { ...json, statusCode: res.status };
LogRequestResult(`GET ${realUrl}`, contents);
@@ -121,6 +115,7 @@ export async function TachiServerV1Get<T = unknown>(
*/
export function PrependTachiUrl(url: string, version: "1" = "1"): string {
if (url[0] !== "/") {
// eslint-disable-next-line no-param-reassign
url = `/${url}`;
}
+14 -17
View File
@@ -1,21 +1,18 @@
import { createLogger, transports, format, Logger } from "winston";
import { CreateLogger, MeiLogger } from "mei-logger";
import { LoggerLayers } from "../data/data";
const { combine, timestamp, label, printf, align, colorize } = format;
const loggerFormat = printf(
({ level, message, label, timestamp }) => `${timestamp} [${label}] ${level} ${message}`
);
const logger = CreateLogger(`tachi-bot`);
export const createLayeredLogger = (layer: LoggerLayers): Logger =>
createLogger({
transports: [new transports.Console()],
format: combine(
label({
label: layer,
}),
colorize(),
timestamp(),
align(),
loggerFormat
),
export default logger;
export function createLayeredLogger(layerName: LoggerLayers) {
const lg = logger.child({
context: [layerName],
});
lg.defaultMeta = Object.assign({}, lg.defaultMeta ?? {}, {
context: [layerName],
});
return lg as MeiLogger;
}
+16
View File
@@ -0,0 +1,16 @@
import { APIPermissions, Game, ImportTypes, integer } from "tachi-common";
export interface ServerConfig {
games: Game[];
importTypes: ImportTypes[];
name: string;
type: "ktchi" | "btchi" | "omni";
}
export interface ServerStatus {
serverTime: number;
startTime: number;
version: string;
whoami: integer | null;
permissions: APIPermissions[];
}
+3 -20
View File
@@ -17,8 +17,8 @@ export const simpleGameTypeToString = <T extends Game>(gpt: SimpleGameType<T>):
`${gpt.game}:${gpt.playtype}` as IDStrings;
export const stringToSimpleGameType = (gptString: string): SimpleGameType<Game> => ({
game: <Game>gptString.split(":")[0],
playtype: <Playtypes[never]>gptString.split(":")[1],
game: gptString.split(":")[0] as Game,
playtype: gptString.split(":")[1] as Playtypes[Game],
});
export const formatGameWrapper = (gpt: SimpleGameType<Game>): string =>
@@ -74,7 +74,7 @@ export const gamesToChoicesObject = (): [name: string, value: string][] =>
/** @description excludes play-types from options */
export const gamesToGenericChoicesObject = (): [name: string, value: string][] =>
simpleGameIdentifierStrings.map((identifier) => [
formatGameWrapperSimple(<Game>identifier),
formatGameWrapperSimple(identifier as Game),
identifier,
]);
@@ -90,20 +90,3 @@ export const formatGameScoreRating = <I extends IDStrings = IDStrings>(
}
return value.toFixed(2);
};
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(""),
(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("")
]();
}
}
);
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"module": "commonjs",
"module": "CommonJS",
"noImplicitAny": true,
"strict": true,
"target": "ES2020",