I can't be bothered to even explain what happene here
This commit is contained in:
@@ -1,8 +1,22 @@
|
||||
import { Context, Next } from "@oak/oak";
|
||||
// @ts-types = "npm:@types/express"
|
||||
import express from "express";
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
|
||||
const log = new Logging('APIUtils');
|
||||
|
||||
interface AppRouter {
|
||||
path: string,
|
||||
router: express.Router
|
||||
}
|
||||
|
||||
export function createRouter(path: string) {
|
||||
const router: AppRouter = {
|
||||
path: path,
|
||||
router: express.Router()
|
||||
}
|
||||
return router;
|
||||
}
|
||||
|
||||
export function generateRandomString(length: number) {
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let randomString = '';
|
||||
@@ -18,12 +32,11 @@ export function generateRandomString(length: number) {
|
||||
const instanceId = generateRandomString(128);
|
||||
|
||||
export function checkQueryTypes<T>(typeDef: T) {
|
||||
return (ctx: Context, nxt: Next) => {
|
||||
return (rq: express.Request, rs: express.Response, nxt: express.NextFunction) => {
|
||||
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."));
|
||||
if (typeof rq.query[key] !== typeof (typeDef)[key]) {
|
||||
rs.statusCode = 400;
|
||||
rs.json(genericResponseFormat(true, "One or more query parameters were invalid or not found."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -31,13 +44,12 @@ export function checkQueryTypes<T>(typeDef: T) {
|
||||
};
|
||||
}
|
||||
export function checkBodyTypes<T>(typeDef: T) {
|
||||
return async (ctx: Context, nxt: Next) => {
|
||||
return (rq: express.Request, rs: express.Response, nxt: express.NextFunction) => {
|
||||
for (const key in typeDef) {
|
||||
if (typeof (await ctx.request.body.json())[key] !== typeof (typeDef)[key]) {
|
||||
if (typeof rq.body[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."));
|
||||
rs.statusCode = 400;
|
||||
rs.json(genericResponseFormat(true, "One or more body values were invalid or not found."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -49,9 +61,8 @@ export function genericResponseFormat(failure: boolean, msg: string | null = nul
|
||||
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 });
|
||||
return (_rq: express.Request, rs: express.Response) => {
|
||||
rs.json({ failed: failure, instance: instanceId, message: msg, data: data });
|
||||
};
|
||||
}
|
||||
type RecNetResponse = {
|
||||
@@ -60,29 +71,35 @@ type RecNetResponse = {
|
||||
};
|
||||
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);
|
||||
return (_rq: express.Request, rs: express.Response) => {
|
||||
rs.json(msg);
|
||||
}
|
||||
}
|
||||
|
||||
export async function logBody(ctx: Context, nxt: Next) {
|
||||
export function logBody(rq: express.Request, _rs: express.Response, nxt: express.NextFunction) {
|
||||
nxt();
|
||||
log.d(`Request body: ${JSON.stringify(await ctx.request.body.text())}`);
|
||||
log.d(`Request body: ${JSON.stringify(rq.body)}`);
|
||||
}
|
||||
|
||||
export function emptyArrayResponse(ctx: Context) {
|
||||
setContentType(ctx, 'application/json');
|
||||
ctx.response.body = JSON.stringify([]);
|
||||
export function emptyArrayResponse(_rq: express.Request, rs: express.Response) {
|
||||
rs.json([]);
|
||||
}
|
||||
|
||||
export function setJSONBody(ctx: Context, obj: object) {
|
||||
ctx.response.type = 'json';
|
||||
ctx.response.body = JSON.stringify(obj);
|
||||
export function getSrcIpDefault(rq: express.Request) {
|
||||
const cfIp = rq.header('cf-connecting-ip');
|
||||
if (cfIp !== undefined) return cfIp;
|
||||
|
||||
const xrIp = rq.header('x-real-ip');
|
||||
if (xrIp !== undefined) return xrIp;
|
||||
|
||||
const ip = typeof rq.ip === 'undefined' ? '(unknown source)' : rq.ip;
|
||||
return ip;
|
||||
}
|
||||
|
||||
export function setContentType(ctx: Context, type: string) {
|
||||
ctx.response.headers.set('Content-Type', type);
|
||||
export function statusResponse(code: number) {
|
||||
return (_rq: express.Request, rs: express.Response) => {
|
||||
rs.sendStatus(code);
|
||||
}
|
||||
}
|
||||
|
||||
export * as APIUtils from "./apiutils.ts"
|
||||
@@ -1,6 +1,5 @@
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import * as fs from "node:fs";
|
||||
import process from "node:process";
|
||||
|
||||
const log = new Logging("Config");
|
||||
|
||||
@@ -14,13 +13,17 @@ type RedisConfiguration = {
|
||||
|
||||
type WebConfiguration = {
|
||||
port: number,
|
||||
host: string
|
||||
host: string,
|
||||
nameserverHost: string,
|
||||
secureNameserverHost: boolean
|
||||
}
|
||||
|
||||
type PublicConfiguration = {
|
||||
serverName: string,
|
||||
owner: string,
|
||||
motd: string
|
||||
motd: string,
|
||||
levelScale: number,
|
||||
maxLevels: number
|
||||
}
|
||||
|
||||
type LoggingConfiguration = {
|
||||
@@ -34,15 +37,20 @@ type DiscordConfiguration = {
|
||||
guildId: string
|
||||
}
|
||||
|
||||
type GalvanicConfiguration = {
|
||||
type SecretConfiguration = {
|
||||
authSecret: string
|
||||
}
|
||||
|
||||
export type GalvanicConfiguration = {
|
||||
redis: RedisConfiguration,
|
||||
web: WebConfiguration,
|
||||
public: PublicConfiguration,
|
||||
logging: LoggingConfiguration,
|
||||
discord: DiscordConfiguration
|
||||
discord: DiscordConfiguration,
|
||||
secrets: SecretConfiguration
|
||||
}
|
||||
|
||||
const defaultConfig: GalvanicConfiguration = {
|
||||
export const defaultConfig: GalvanicConfiguration = {
|
||||
redis: {
|
||||
host: "127.0.0.1",
|
||||
port: 6379,
|
||||
@@ -52,12 +60,16 @@ const defaultConfig: GalvanicConfiguration = {
|
||||
},
|
||||
web: {
|
||||
port: 3000,
|
||||
host: "127.0.0.1"
|
||||
host: "127.0.0.1",
|
||||
nameserverHost: "127.0.0.1:3000",
|
||||
secureNameserverHost: false
|
||||
},
|
||||
public: {
|
||||
serverName: "Galvanic Corrosion",
|
||||
owner: "John Doe",
|
||||
motd: "The narwhal bacons at midnight"
|
||||
motd: "The narwhal bacons at midnight",
|
||||
levelScale: 1,
|
||||
maxLevels: 30
|
||||
},
|
||||
logging: {
|
||||
debug: false,
|
||||
@@ -67,6 +79,9 @@ const defaultConfig: GalvanicConfiguration = {
|
||||
token: "replace-me",
|
||||
guildId: "replace-me",
|
||||
clientId: "replace-me"
|
||||
},
|
||||
secrets: {
|
||||
authSecret: "CHANGE-ME-PLEASE"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,19 +92,7 @@ try {
|
||||
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;
|
||||
}
|
||||
Deno.exit(1);
|
||||
}
|
||||
|
||||
/** Does the configuration file exist on the disk? */
|
||||
|
||||
48
src/data/auth.ts
Normal file
48
src/data/auth.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { encode, decode } from "@gz/jwt";
|
||||
import { Config, GalvanicConfiguration } from "../config.ts";
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
|
||||
const log = new Logging("Auth");
|
||||
|
||||
const config = Config.getConfig() as GalvanicConfiguration;
|
||||
|
||||
type TokenFormat = {
|
||||
iss: string;
|
||||
sub: number;
|
||||
nbf: number;
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
|
||||
export class GameAuthContext {
|
||||
|
||||
valid: boolean | null = null;
|
||||
|
||||
#rawToken: string
|
||||
|
||||
playerId: number | null = null;
|
||||
|
||||
constructor(token: string) {
|
||||
|
||||
this.#rawToken = token;
|
||||
|
||||
}
|
||||
|
||||
async decode() {
|
||||
try {
|
||||
const decoded = await decode(this.#rawToken, config.secrets.authSecret) as TokenFormat;
|
||||
this.playerId = decoded.sub || null;
|
||||
const now = Math.round(Date.now() / 1000);
|
||||
|
||||
this.valid = true;
|
||||
if (decoded.exp < now) this.valid = false;
|
||||
if (decoded.nbf > now) this.valid = false;
|
||||
} catch (e) {
|
||||
this.valid = false;
|
||||
log.w(`Token decode failed: ${(e as Error).stack}`);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export * as Authentication from "./auth.ts";
|
||||
65
src/data/config.ts
Normal file
65
src/data/config.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Config } from "../config.ts";
|
||||
import { Objectives } from "./objectives.ts";
|
||||
|
||||
export type Config = {
|
||||
Key: string,
|
||||
Value: string
|
||||
}
|
||||
export type LevelProgressionItem = {
|
||||
Level: number,
|
||||
RequiredXp: number
|
||||
}
|
||||
export type PublicConfig = {
|
||||
MessageOfTheDay: string,
|
||||
CdnBaseUri: string,
|
||||
MatchmakingParams: {
|
||||
PreferFullRoomsFrequency: number,
|
||||
PreferEmptyRoomsFrequency: number
|
||||
},
|
||||
ServerMaintenance: {
|
||||
StartsInMinutes: number
|
||||
},
|
||||
LevelProgressionMaps: LevelProgressionItem[],
|
||||
DailyObjectives: Objectives.Objective[][],
|
||||
ConfigTable: Config[],
|
||||
PhotonConfig: {
|
||||
CrcCheckEnabled: boolean,
|
||||
EnableServerTracingAfterDisconnect: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export function getConfig() {
|
||||
const c = Config.getConfig();
|
||||
if (typeof c == 'undefined') return null;
|
||||
const config = c as Config.GalvanicConfiguration;
|
||||
|
||||
function generateLevelProgressionMap() {
|
||||
const m: LevelProgressionItem[] = [];
|
||||
for (let i = 0; i < config.public.maxLevels + 1; i++)
|
||||
m.push({Level: i, RequiredXp: Math.round(i * config.public.levelScale * 20)});
|
||||
return m;
|
||||
}
|
||||
|
||||
const conf: PublicConfig = {
|
||||
MessageOfTheDay: config.public.motd,
|
||||
CdnBaseUri: `${config.web.secureNameserverHost ? 'https' : 'http'}://${c.web.nameserverHost}/{0}`,
|
||||
MatchmakingParams: {
|
||||
PreferFullRoomsFrequency: 1,
|
||||
PreferEmptyRoomsFrequency: 0
|
||||
},
|
||||
ServerMaintenance: {
|
||||
StartsInMinutes: 0
|
||||
},
|
||||
LevelProgressionMaps: generateLevelProgressionMap(),
|
||||
DailyObjectives: [],
|
||||
ConfigTable: [],
|
||||
PhotonConfig: {
|
||||
CrcCheckEnabled: false,
|
||||
EnableServerTracingAfterDisconnect: false
|
||||
}
|
||||
}
|
||||
|
||||
return conf;
|
||||
}
|
||||
|
||||
export * as GameConfigs from "./config.ts";
|
||||
17
src/data/content/avatar.ts
Normal file
17
src/data/content/avatar.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
enum AvatarItemType {
|
||||
None = -1,
|
||||
Hat,
|
||||
BackHead,
|
||||
Hair,
|
||||
Eye = 10,
|
||||
Mouth = 20,
|
||||
Neck = 100,
|
||||
Shirt,
|
||||
Belt,
|
||||
Pocket,
|
||||
TeamJersey,
|
||||
Wrist = 200,
|
||||
Glove,
|
||||
Watch,
|
||||
TeamWrist
|
||||
}
|
||||
61
src/data/content/comsumable.ts
Normal file
61
src/data/content/comsumable.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
export enum Consumable {
|
||||
ASSORTED_DONUTS,
|
||||
SUPREME_PIZZA,
|
||||
ROOT_BEER,
|
||||
CHOCOLATE_FROSTED_DONUTS,
|
||||
CHEESE_PIZZA,
|
||||
PEPPERONI_PIZZA,
|
||||
GLAZED_DONUTS
|
||||
}
|
||||
|
||||
const ids = [
|
||||
"ZuvkidodzkuOfGLDnTOFyg",
|
||||
"wUCIKdJSvEmiQHYMyx4X4w",
|
||||
"JfnVXFmilU6ysv-VbTAe3A",
|
||||
"mMCGPgK3tki5S_15q2Z81A",
|
||||
"5hIAZ9wg5EyG1cILf4FS2A",
|
||||
"mq23W-RSP0G8iGNLdrcpUw",
|
||||
"7OZ5AE3uuUyqa0P-2W1ptg"
|
||||
] as const;
|
||||
|
||||
export class ConsumableSelection {
|
||||
|
||||
type: Consumable;
|
||||
|
||||
guid: string;
|
||||
|
||||
constructor(type: Consumable) {
|
||||
|
||||
this.type = type;
|
||||
this.guid = ids[type];
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class ConsumableBuilder {
|
||||
|
||||
Id: number;
|
||||
|
||||
ConsumableItemDesc: string;
|
||||
|
||||
CreatedAt: string;
|
||||
|
||||
Count: number;
|
||||
|
||||
UnlockedLevel: number;
|
||||
|
||||
IsActive: boolean;
|
||||
|
||||
constructor(selection: ConsumableSelection, id: number, createdAt: Date, count: number, active: boolean) {
|
||||
|
||||
this.Id = id;
|
||||
this.ConsumableItemDesc = selection.guid;
|
||||
this.CreatedAt = createdAt.toUTCString();
|
||||
this.Count = count;
|
||||
this.UnlockedLevel = 0; // All players have access to every consumable - avatars and equipment are different
|
||||
this.IsActive = active;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
96
src/data/objectives.ts
Normal file
96
src/data/objectives.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
export enum ObjectiveType {
|
||||
Default = -1,
|
||||
FirstSessionOfDay = 1,
|
||||
AddAFriend,
|
||||
PartyUp,
|
||||
AllOtherChallenges,
|
||||
LevelUp,
|
||||
CheerAPlayer,
|
||||
PointedAtPlayer,
|
||||
CheerARoom,
|
||||
SubscribeToPlayer,
|
||||
DailyObjective1,
|
||||
DailyObjective2,
|
||||
DailyObjective3,
|
||||
AllDailyObjectives,
|
||||
CompleteAnyDaily,
|
||||
CompleteAnyWeekly,
|
||||
OOBE_GoToLockerRoom = 20,
|
||||
OOBE_GoToActivity,
|
||||
OOBE_FinishActivity,
|
||||
NUX_PunchcardObjective = 25,
|
||||
NUX_AllPunchcardObjectives,
|
||||
GoToRecCenter = 30,
|
||||
FinishActivity,
|
||||
VisitACustomRoom,
|
||||
CreateACustomRoom,
|
||||
ScoreBasketInRecCenter = 35,
|
||||
UploadPhotoToRecNet,
|
||||
UpdatePlayerBio,
|
||||
SaveOutfitSlot,
|
||||
PurchaseClothingItem,
|
||||
PurchaseNonClothingItem,
|
||||
CharadesGames = 100,
|
||||
CharadesWinsPerformer,
|
||||
CharadesWinsGuesser,
|
||||
DiscGolfWins = 200,
|
||||
DiscGolfGames,
|
||||
DiscGolfHolesUnderPar,
|
||||
DodgeballWins = 300,
|
||||
DodgeballGames,
|
||||
DodgeballHits,
|
||||
PaddleballGames = 400,
|
||||
PaddleballWins,
|
||||
PaddleballScores,
|
||||
PaintballAnyModeGames = 500,
|
||||
PaintballAnyModeWins,
|
||||
PaintballAnyModeHits,
|
||||
PaintballCTFWins = 600,
|
||||
PaintballCTFGames,
|
||||
PaintballCTFHits,
|
||||
PaintballFlagCaptures,
|
||||
PaintballTeamBattleWins = 700,
|
||||
PaintballTeamBattleGames,
|
||||
PaintballTeamBattleHits,
|
||||
PaintballFreeForAllWins = 710,
|
||||
PaintballFreeForAllGames,
|
||||
PaintballFreeForAllHits,
|
||||
SoccerWins = 800,
|
||||
SoccerGames,
|
||||
SoccerGoals,
|
||||
QuestGames = 1000,
|
||||
QuestWins,
|
||||
QuestPlayerRevives,
|
||||
QuestEnemyKills,
|
||||
QuestGames_Goblin1 = 1010,
|
||||
QuestWins_Goblin1,
|
||||
QuestPlayerRevives_Goblin1,
|
||||
QuestEnemyKills_Goblin1,
|
||||
QuestGames_Goblin2 = 1020,
|
||||
QuestWins_Goblin2,
|
||||
QuestPlayerRevives_Goblin2,
|
||||
QuestEnemyKills_Goblin2,
|
||||
QuestGames_Scifi1 = 1030,
|
||||
QuestWins_Scifi1,
|
||||
QuestPlayerRevives_Scifi1,
|
||||
QuestEnemyKills_Scifi1,
|
||||
QuestGames_Pirate1 = 1040,
|
||||
QuestWins_Pirate1,
|
||||
QuestPlayerRevives_Pirate1,
|
||||
QuestEnemyKills_Pirate1,
|
||||
ArenaGames = 2000,
|
||||
ArenaWins,
|
||||
ArenaPlayerRevives,
|
||||
ArenaHeroTags,
|
||||
ArenaBotTags,
|
||||
RecRoyaleGames = 3000,
|
||||
RecRoyaleWins,
|
||||
RecRoyaleTags
|
||||
}
|
||||
|
||||
export type Objective = {
|
||||
type: ObjectiveType,
|
||||
score: number
|
||||
}
|
||||
|
||||
export * as Objectives from "./objectives.ts";
|
||||
12
src/data/users.ts
Normal file
12
src/data/users.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
interface UserInitOptions {
|
||||
username: string,
|
||||
password: string,
|
||||
}
|
||||
|
||||
export class User {
|
||||
|
||||
static init() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
39
src/db.ts
39
src/db.ts
@@ -1,14 +1,13 @@
|
||||
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);
|
||||
Deno.exit(1);
|
||||
}
|
||||
|
||||
let shuttingDown = false;
|
||||
@@ -16,18 +15,19 @@ Deno.addSignalListener('SIGINT', () => {
|
||||
if (shuttingDown) return;
|
||||
shuttingDown = true;
|
||||
log.n('Disconnecting from Redis');
|
||||
if (typeof Database !== 'undefined') Database.quit();
|
||||
Database.quit();
|
||||
});
|
||||
|
||||
export let Database: Redis | undefined;
|
||||
export const 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,
|
||||
lazyConnect: true
|
||||
});
|
||||
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
|
||||
});
|
||||
Database.connect();
|
||||
log.i(`Connected to Redis`);
|
||||
}
|
||||
|
||||
@@ -35,13 +35,18 @@ export function buildKey(...args: string[]) {
|
||||
return args.join(':');
|
||||
}
|
||||
export const KeyGroups = {
|
||||
Config: {
|
||||
Root: "config",
|
||||
Dynamic: "dynamic"
|
||||
},
|
||||
Accounts: {
|
||||
Ids: "account-ids",
|
||||
Usernames: "account-usernames",
|
||||
DisplayNames: "account-displaynames",
|
||||
XP: "account-scores",
|
||||
Developers: "account-developers",
|
||||
ProfileImages: "account-images"
|
||||
Root: "accounts",
|
||||
Ids: "ids",
|
||||
Usernames: "usernames",
|
||||
DisplayNames: "displaynames",
|
||||
XP: "scores",
|
||||
Developers: "developers",
|
||||
ProfileImages: "images"
|
||||
}
|
||||
}
|
||||
export * as Redis from "./db.ts";
|
||||
@@ -1,14 +1,13 @@
|
||||
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);
|
||||
Deno.exit(1);
|
||||
}
|
||||
|
||||
export const client = new discord.Client({ intents: [discord.GatewayIntentBits.Guilds, discord.GatewayIntentBits.GuildPresences] });
|
||||
|
||||
48
src/dynamicconfig.ts
Normal file
48
src/dynamicconfig.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { KeyGroups, Redis } from "./db.ts";
|
||||
|
||||
export enum ResultType {
|
||||
Found,
|
||||
NotFound
|
||||
}
|
||||
|
||||
interface ConfigResult {
|
||||
Status: ResultType,
|
||||
Data: string | null
|
||||
}
|
||||
interface ConfigMResult {
|
||||
Status: ResultType,
|
||||
Data: (string | null)[] | null
|
||||
}
|
||||
|
||||
/** Get a dyamic config. */
|
||||
export async function getConfig(key: string) {
|
||||
const res: ConfigResult = {
|
||||
Status: ResultType.Found,
|
||||
Data: null
|
||||
}
|
||||
|
||||
const data = await Redis.Database.get(`${KeyGroups.Config.Root}:${KeyGroups.Config.Dynamic}:${key}`);
|
||||
if (data == null) res.Status = ResultType.NotFound;
|
||||
else res.Data = data;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function mgetConfig(...keys: string[]) {
|
||||
const res: ConfigMResult = {
|
||||
Status: ResultType.Found,
|
||||
Data: null
|
||||
}
|
||||
|
||||
const data = await Redis.Database.mget(...keys);
|
||||
res.Data = data;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/** Set a dynamic config. */
|
||||
export async function setConfig(key: string, value: string) {
|
||||
await Redis.Database.set(`${KeyGroups.Config.Root}:${KeyGroups.Config.Dynamic}:${key}`, value);
|
||||
}
|
||||
|
||||
export * as DynamicConfig from "./dynamicconfig.ts";
|
||||
66
src/main.ts
66
src/main.ts
@@ -1,11 +1,11 @@
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import * as Log from "@proxnet/undead-logging";
|
||||
import * as Config from "./config.ts";
|
||||
import { Application, Router } from "@oak/oak";
|
||||
// @ts-types = 'npm:@types/express'
|
||||
import express from "express";
|
||||
import { Redis } from "./db.ts";
|
||||
import { Discord } from "./discord.ts";
|
||||
import { APIUtils } from "./apiutils.ts";
|
||||
|
||||
const log = new Logging("Main");
|
||||
const log = new Log.default("Main");
|
||||
|
||||
log.i(`Starting Galvanic Corrosion..`);
|
||||
|
||||
@@ -15,16 +15,44 @@ if (typeof config == 'undefined') {
|
||||
log.e('Cannot start: Configuration is undefined');
|
||||
Deno.exit(1);
|
||||
}
|
||||
if (config.secrets.authSecret == Config.defaultConfig.secrets.authSecret) {
|
||||
log.e(`Cannot start: Auth secret is default. Please change 'secrets.authSecret' in 'config.json'`);
|
||||
Deno.exit(1);
|
||||
}
|
||||
|
||||
Log.MessageTypeVisibility.Network = config.logging.network;
|
||||
Log.MessageTypeVisibility.Debug = config.logging.debug;
|
||||
|
||||
const port = config.web.port;
|
||||
const host = config.web.host;
|
||||
|
||||
log.i(`Starting HTTP server on http://${host}:${port}`);
|
||||
|
||||
const abortController = new AbortController();
|
||||
const app = new Application();
|
||||
const app = express();
|
||||
|
||||
app.use(new Router().all('/', APIUtils.genericResponse(false, `${config?.public.serverName} - ${config?.public.motd}`)).routes());
|
||||
app.disable('etag');
|
||||
app.disable('x-powered-by');
|
||||
|
||||
app.use((rq: express.Request, rs: express.Response, nxt: express.NextFunction) => {
|
||||
rs.locals.auth = null;
|
||||
log.n(`${APIUtils.getSrcIpDefault(rq)} ${rq.method} ${rq.originalUrl}`);
|
||||
nxt();
|
||||
});
|
||||
|
||||
app.use('/', APIUtils.genericResponse(false, `${config?.public.serverName} - ${config?.public.motd}`));
|
||||
|
||||
// content routes
|
||||
const nameserverRouter = await import('./routes/nameserver.ts');
|
||||
const apiRouter = await import('./routes/api.ts');
|
||||
|
||||
app.use(nameserverRouter.route.path, nameserverRouter.route.router);
|
||||
app.use(apiRouter.route.path, apiRouter.route.router);
|
||||
|
||||
app.use((rq: express.Request, rs: express.Response) => {
|
||||
log.e(`${APIUtils.getSrcIpDefault(rq)} 404 ${rq.method} ${rq.url.toString()}`);
|
||||
rs.statusCode = 404;
|
||||
rs.json(APIUtils.genericResponseFormat(true, 'Endpoint not found. Check your syntax and/or method.'));
|
||||
});
|
||||
|
||||
try {
|
||||
log.i(`Connecting to Redis..`);
|
||||
@@ -35,18 +63,22 @@ try {
|
||||
}
|
||||
|
||||
try {
|
||||
app.listen({port: port, hostname: host, signal: abortController.signal });
|
||||
const http = app.listen(config.web.port, config.web.host, () => {
|
||||
log.n(`Listening on http://${config.web.host}`);
|
||||
|
||||
let shuttingDown = false;
|
||||
Deno.addSignalListener('SIGINT', () => {
|
||||
if (shuttingDown) return;
|
||||
shuttingDown = true;
|
||||
log.i(`Shutting down`);
|
||||
|
||||
http.close();
|
||||
http.removeAllListeners();
|
||||
});
|
||||
});
|
||||
} 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();
|
||||
});
|
||||
//Discord.login(); do not use for now
|
||||
8
src/routes/api.ts
Normal file
8
src/routes/api.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { route as VersionCheckRoute } from "./api/versioncheck.ts";
|
||||
import { route as ConfigRoute } from "./api/config.ts";
|
||||
import { APIUtils } from "../apiutils.ts";
|
||||
|
||||
export const route = APIUtils.createRouter('/api');
|
||||
|
||||
route.router.use(VersionCheckRoute.router);
|
||||
route.router.use(ConfigRoute.router);
|
||||
10
src/routes/api/config.ts
Normal file
10
src/routes/api/config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { APIUtils } from "../../apiutils.ts";
|
||||
import { GameConfigs } from "../../data/config.ts";
|
||||
|
||||
export const route = APIUtils.createRouter('/config');
|
||||
|
||||
route.router.get('/v2', (rq, rs) => {
|
||||
const config = GameConfigs.getConfig();
|
||||
if (config == null) rs.sendStatus(500);
|
||||
else rs.json(config);
|
||||
});
|
||||
24
src/routes/api/versioncheck.ts
Normal file
24
src/routes/api/versioncheck.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { APIUtils } from "../../apiutils.ts";
|
||||
|
||||
export const route = APIUtils.createRouter('/versioncheck');
|
||||
|
||||
const validVersion = '20191120';
|
||||
|
||||
type ValidVersionResponse = {
|
||||
ValidVersion: boolean
|
||||
}
|
||||
|
||||
route.router.get('/v3', (rq, rs) => {
|
||||
const requestedVer = rq.query['v'];
|
||||
if (typeof requestedVer !== 'string' || requestedVer !== validVersion) {
|
||||
const res: ValidVersionResponse = {
|
||||
ValidVersion: false
|
||||
}
|
||||
rs.json(res);
|
||||
} else {
|
||||
const res: ValidVersionResponse = {
|
||||
ValidVersion: true
|
||||
}
|
||||
rs.json(res);
|
||||
}
|
||||
});
|
||||
40
src/routes/nameserver.ts
Normal file
40
src/routes/nameserver.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { APIUtils } from "../apiutils.ts";
|
||||
import { Config } from "../config.ts";
|
||||
|
||||
const config = Config.getConfig() as Config.GalvanicConfiguration;
|
||||
const protocol = config.web.secureNameserverHost ? 'https' : 'http';
|
||||
|
||||
export const route = APIUtils.createRouter('/ns');
|
||||
|
||||
type NameserverHosts = {
|
||||
Auth: string,
|
||||
API: string,
|
||||
WWW: string,
|
||||
Notifications: string,
|
||||
Images: string,
|
||||
CDN: string,
|
||||
Commerce: string,
|
||||
Matchmaking: string,
|
||||
Storage: string,
|
||||
Chat: string,
|
||||
Leaderboard: string
|
||||
}
|
||||
|
||||
const path = `${protocol}://${config.web.nameserverHost}`;
|
||||
const nameserver: NameserverHosts = {
|
||||
Auth: path,
|
||||
API: path,
|
||||
WWW: path,
|
||||
Notifications: path,
|
||||
Images: path,
|
||||
CDN: path,
|
||||
Commerce: path,
|
||||
Matchmaking: path,
|
||||
Storage: path,
|
||||
Chat: path,
|
||||
Leaderboard: path
|
||||
}
|
||||
|
||||
route.router.get('*', (_rq, rs) => {
|
||||
rs.json(nameserver);
|
||||
});
|
||||
9
src/types/express.ts
Normal file
9
src/types/express.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Authentication } from "../data/auth.ts";
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Locals {
|
||||
auth: Authentication.GameAuthContext | null
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user