Base utilities, add README, Redis, Discord integration, db keygroup planning
This commit is contained in:
88
src/apiutils.ts
Normal file
88
src/apiutils.ts
Normal 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
110
src/config.ts
Normal 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
47
src/db.ts
Normal 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
34
src/discord.ts
Normal 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";
|
||||
58
src/main.ts
58
src/main.ts
@@ -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();
|
||||
});
|
||||
Reference in New Issue
Block a user