Base utilities, add README, Redis, Discord integration, db keygroup planning

This commit is contained in:
2024-11-18 07:31:25 -05:00
parent ab5907355d
commit d982567c7b
10 changed files with 640 additions and 346 deletions

88
src/apiutils.ts Normal file
View File

@@ -0,0 +1,88 @@
import { Context, Next } from "@oak/oak";
import Logging from "@proxnet/undead-logging";
const log = new Logging('APIUtils');
export function generateRandomString(length: number) {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let randomString = '';
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * characters.length);
randomString += characters.charAt(randomIndex);
}
return randomString;
}
const instanceId = generateRandomString(128);
export function checkQueryTypes<T>(typeDef: T) {
return (ctx: Context, nxt: Next) => {
for (const key in typeDef) {
if (typeof Object.fromEntries(ctx.request.url.searchParams)[key] !== typeof (typeDef)[key]) {
ctx.response.status = 400;
setContentType(ctx, 'application/json');
ctx.response.body = JSON.stringify(genericResponseFormat(true, "One or more query parameters were invalid or not found."));
return;
}
}
nxt();
};
}
export function checkBodyTypes<T>(typeDef: T) {
return async (ctx: Context, nxt: Next) => {
for (const key in typeDef) {
if (typeof (await ctx.request.body.json())[key] !== typeof (typeDef)[key]) {
log.e(`Body check for key '${key}' failed.`);
ctx.response.status = 400;
setContentType(ctx, 'application/json');
ctx.response.body = JSON.stringify(genericResponseFormat(true, "One or more body values were invalid or not found."));
return;
}
}
nxt();
};
}
export function genericResponseFormat(failure: boolean, msg: string | null = null, data = null) {
return { failed: failure, instance: instanceId, message: msg, data: data };
}
export function genericResponse(failure: boolean, msg: string | null = null, data = null) {
return (ctx: Context) => {
setContentType(ctx, 'application/json');
ctx.response.body = JSON.stringify({ failed: failure, instance: instanceId, message: msg, data: data });
};
}
type RecNetResponse = {
Success: boolean,
Message: string
};
export function RecNetResponse(success: boolean, message: string) {
const msg: RecNetResponse = { Success: success, Message: message };
return (ctx: Context) => {
setContentType(ctx, 'application/json');
ctx.response.body = JSON.stringify(msg);
}
}
export async function logBody(ctx: Context, nxt: Next) {
nxt();
log.d(`Request body: ${JSON.stringify(await ctx.request.body.text())}`);
}
export function emptyArrayResponse(ctx: Context) {
setContentType(ctx, 'application/json');
ctx.response.body = JSON.stringify([]);
}
export function setJSONBody(ctx: Context, obj: object) {
ctx.response.type = 'json';
ctx.response.body = JSON.stringify(obj);
}
export function setContentType(ctx: Context, type: string) {
ctx.response.headers.set('Content-Type', type);
}
export * as APIUtils from "./apiutils.ts"

110
src/config.ts Normal file
View File

@@ -0,0 +1,110 @@
import Logging from "@proxnet/undead-logging";
import * as fs from "node:fs";
import process from "node:process";
const log = new Logging("Config");
type RedisConfiguration = {
host: string,
port: number,
username: string,
password: string,
db: number
}
type WebConfiguration = {
port: number,
host: string
}
type PublicConfiguration = {
serverName: string,
owner: string,
motd: string
}
type LoggingConfiguration = {
debug: boolean,
network: boolean
}
type DiscordConfiguration = {
token: string,
clientId: string,
guildId: string
}
type GalvanicConfiguration = {
redis: RedisConfiguration,
web: WebConfiguration,
public: PublicConfiguration,
logging: LoggingConfiguration,
discord: DiscordConfiguration
}
const defaultConfig: GalvanicConfiguration = {
redis: {
host: "127.0.0.1",
port: 6379,
username: "",
password: "",
db: 0
},
web: {
port: 3000,
host: "127.0.0.1"
},
public: {
serverName: "Galvanic Corrosion",
owner: "John Doe",
motd: "The narwhal bacons at midnight"
},
logging: {
debug: false,
network: false
},
discord: {
token: "replace-me",
guildId: "replace-me",
clientId: "replace-me"
}
}
/** The current configuration. Read and parsed only during startup. */
let config: GalvanicConfiguration | undefined;
try {
if (!configurationExists()) generateDefaultConfig();
config = JSON.parse(fs.readFileSync('./config.json').toString());
} catch (err) {
log.e(`Could not get config: ${err}`);
process.exit(1);
}
/**
* Looks for a certain file in the current directory that shouldn't exist on the first run.
* Returns `false` when GC has ran at least once
*/
export function firstRun() {
if (!fs.existsSync('./firstrun')) return true;
else {
fs.writeFile('./firstrun', "", () => {});
return false;
}
}
/** Does the configuration file exist on the disk? */
export function configurationExists() {
return fs.existsSync('./config.json');
}
/** Place the default configuration in the current directory. */
export function generateDefaultConfig() {
fs.writeFileSync('./config.json', JSON.stringify(defaultConfig, undefined, ' '));
}
/** Get current server configuration */
export function getConfig() {
return config;
}
export * as Config from './config.ts';

47
src/db.ts Normal file
View File

@@ -0,0 +1,47 @@
import { Redis } from "ioredis";
import * as Config from "./config.ts";
import Logging from "@proxnet/undead-logging";
import process from "node:process";
const log = new Logging("RedisDB");
const config = Config.getConfig();
if (typeof config == 'undefined') {
log.e(`Cannot start: Redis configuration failed`);
process.exit(1);
}
let shuttingDown = false;
Deno.addSignalListener('SIGINT', () => {
if (shuttingDown) return;
shuttingDown = true;
log.n('Disconnecting from Redis');
if (typeof Database !== 'undefined') Database.quit();
});
export let Database: Redis | undefined;
export function connectToRedis() {
Database = new Redis({
port: config?.redis.port,
host: config?.redis.host,
username: config?.redis.username == "" ? undefined : config?.redis.username,
password: config?.redis.password == "" ? undefined : config?.redis.password,
db: config?.redis.db
});
log.i(`Connected to Redis`);
}
export function buildKey(...args: string[]) {
return args.join(':');
}
export const KeyGroups = {
Accounts: {
Ids: "account-ids",
Usernames: "account-usernames",
DisplayNames: "account-displaynames",
XP: "account-scores",
Developers: "account-developers",
ProfileImages: "account-images"
}
}
export * as Redis from "./db.ts";

34
src/discord.ts Normal file
View File

@@ -0,0 +1,34 @@
import * as discord from "discord.js";
import { Config } from "./config.ts";
import Logging from "@proxnet/undead-logging";
import process from "node:process";
const log = new Logging("Discord");
const config = Config.getConfig();
if (typeof config == 'undefined') {
log.e(`Cannot start: Discord configuration is unavailable`);
process.exit(1);
}
export const client = new discord.Client({ intents: [discord.GatewayIntentBits.Guilds, discord.GatewayIntentBits.GuildPresences] });
client.once(discord.Events.ClientReady, client => {
log.i(`Logged in to Discord as "${client.user.tag}"`);
client.user?.setActivity(config?.public.motd, { type: discord.ActivityType.Custom });
});
let shuttingDown = false;
Deno.addSignalListener('SIGINT', () => {
if (shuttingDown) return;
shuttingDown = true;
log.n('Disconnecting from Discord');
client.destroy();
});
export function login() {
log.i(`Creating Discord connection..`);
client.login(config?.discord.token);
}
export * as Discord from "./discord.ts";

View File

@@ -1,22 +1,52 @@
import Logging from "log-like-a-zombie";
import express from "express";
import Logging from "@proxnet/undead-logging";
import * as Config from "./config.ts";
import { Application, Router } from "@oak/oak";
import { Redis } from "./db.ts";
import { Discord } from "./discord.ts";
import { APIUtils } from "./apiutils.ts";
const log = new Logging("Main");
const port = 3000;
const address = "127.0.0.1";
log.i(`Starting Galvanic Corrosion..`);
log.i(`Starting HTTP server on http://${address}:${port}`);
const config = Config.getConfig();
const app = express();
app.disable('etag');
app.disable('x-powered-by');
if (typeof config == 'undefined') {
log.e('Cannot start: Configuration is undefined');
Deno.exit(1);
}
app.use((rq: express.Request, rs: express.Response) => {
log.n(`${rq.ip} ${rq.method} ${rq.originalUrl}`);
rs.sendStatus(200);
});
const port = config.web.port;
const host = config.web.host;
app.listen(port, address, () => {
log.i(`Listening on http://${address}:${port}`);
log.i(`Starting HTTP server on http://${host}:${port}`);
const abortController = new AbortController();
const app = new Application();
app.use(new Router().all('/', APIUtils.genericResponse(false, `${config?.public.serverName} - ${config?.public.motd}`)).routes());
try {
log.i(`Connecting to Redis..`);
Redis.connectToRedis();
} catch (err) {
log.e(`Cannot start: Redis could not be initialized. ${err}`);
Deno.exit(1);
}
try {
app.listen({port: port, hostname: host, signal: abortController.signal });
} catch (err) {
log.e(`Cannot start: Network could not be initalized. ${err}`);
Deno.exit(1);
}
Discord.login();
let shuttingDown = false;
Deno.addSignalListener('SIGINT', () => {
if (shuttingDown) return;
shuttingDown = true;
log.n(`Shutting down`);
abortController.abort();
});