mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-01 19:38:13 +03:00
183 lines
4.6 KiB
TypeScript
183 lines
4.6 KiB
TypeScript
import { IsRecord } from "./utils/predicates";
|
|
import { FormatPrError } from "./utils/prudence";
|
|
import { config } from "dotenv";
|
|
import JSON5 from "json5";
|
|
// eslint-disable-next-line import/order
|
|
import { p } from "prudence";
|
|
|
|
// @ts-expect-error No types available...
|
|
import fetchSync from "sync-fetch";
|
|
import fs from "fs";
|
|
import type { Game, integer, TachiServerCoreConfig } from "tachi-common";
|
|
|
|
// Initialise .env.
|
|
config();
|
|
|
|
// Reads the bots config file from $pwd/conf.json5.
|
|
// Validates it using prudence.
|
|
|
|
// the real logger tries to bind to discord, and is dependent on the options
|
|
// below.
|
|
const logger = console;
|
|
|
|
export interface BotConfig {
|
|
TACHI_SERVER_LOCATION: string;
|
|
HTTP_SERVER: {
|
|
URL: string;
|
|
};
|
|
OAUTH: {
|
|
CLIENT_SECRET: string;
|
|
CLIENT_ID: string;
|
|
};
|
|
DISCORD: {
|
|
TOKEN: string;
|
|
SERVER_ID: string;
|
|
GAME_CHANNELS: Partial<Record<Game, string>>;
|
|
ADMIN_USERS: Array<string>;
|
|
LIMBO_CHANNEL?: string;
|
|
APPROVED_ROLE?: 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",
|
|
},
|
|
OAUTH: {
|
|
CLIENT_SECRET: "string",
|
|
CLIENT_ID: "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;
|
|
},
|
|
|
|
// A list of users that are allowed to do powerful stuff.
|
|
ADMIN_USERS: ["string"],
|
|
|
|
// when users are authenticated with the bot, this role will be assigned
|
|
// to them.
|
|
// if this is not set, no role will be assigned.
|
|
APPROVED_ROLE: "*string",
|
|
|
|
LIMBO_CHANNEL: "*string",
|
|
},
|
|
});
|
|
|
|
if (err) {
|
|
logger.error(FormatPrError(err, "Invalid conf.json5 file. Cannot safely boot."));
|
|
|
|
throw err;
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
export interface ProcessEnvironment {
|
|
nodeEnv: "dev" | "production" | "staging" | "test";
|
|
mongoUrl: string;
|
|
port: integer;
|
|
seqUrl?: string;
|
|
seqApiKey?: string;
|
|
}
|
|
|
|
function ParseEnvVars() {
|
|
const err = p(
|
|
process.env,
|
|
{
|
|
NODE_ENV: p.isIn("production", "dev", "staging", "test"),
|
|
|
|
// mei implicitly reads this.
|
|
LOG_LEVEL: p.optional(
|
|
p.isIn("debug", "verbose", "info", "warn", "error", "severe", "crit")
|
|
),
|
|
MONGO_URL: "string",
|
|
PORT: (self) =>
|
|
p.isPositiveInteger(Number(self)) === true ||
|
|
"Should be a string representing a whole integer port.",
|
|
SEQ_URL: "*string",
|
|
SEQ_API_KEY: "*string",
|
|
},
|
|
{},
|
|
{ allowExcessKeys: true }
|
|
);
|
|
|
|
if (err) {
|
|
logger.error(FormatPrError(err, "Invalid environment. Cannot safely boot."));
|
|
|
|
throw err;
|
|
}
|
|
|
|
return {
|
|
nodeEnv: process.env.NODE_ENV,
|
|
mongoUrl: process.env.MONGO_URL,
|
|
port: Number(process.env.PORT),
|
|
seqUrl: process.env.SEQ_URL,
|
|
seqApiKey: process.env.SEQ_API_KEY,
|
|
} 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.error(
|
|
`Failed to fetch server info from ${BotConfig.TACHI_SERVER_LOCATION}. Can't run.`
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
return res.body as TachiServerCoreConfig;
|
|
}
|
|
|
|
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.`
|
|
);
|
|
}
|
|
}
|