sbdatatracker/src/server.ts

175 lines
4.0 KiB
TypeScript

import sirv from "sirv";
import polka from "polka";
import compression from "compression";
import * as sapper from "@sapper/server";
import arangojs, { Database, aql } from "arangojs";
import { hash } from "bcrypt";
import { log } from "./logger";
import { Permissions } from "./lib/permissions";
import { stringify } from "json-bigint";
// HACK : BigInt support on JSON.stringify
JSON.stringify = stringify;
export let profileNameCache: {
[player: string]: {
[profile: string]: string;
};
} = {};
export const {
PORT,
NODE_ENV,
DB_USERNAME,
DB_PASSWORD,
DB_URL,
DB_NAME,
API_KEY
} = process.env;
const dev = NODE_ENV === "development";
export const db = arangojs({
auth: {
username: DB_USERNAME || "root",
password: DB_PASSWORD
},
url: DB_URL || "http://127.0.0.1:8529",
databaseName: DB_NAME || "sbdatatracker"
});
export const saltRounds = 9;
export const accessTokenExpire = 1000 * 60 * 20;
export const refreshTokenExpire = 1000 * 60 * 60 * 24 * 7;
const delay = 3 * 60 * 1000;
const intervalFunc = async (db: Database) => {
const cnf = db.collection("config");
const users = db.collection("users");
const sessions = db.collection("sessions");
if (!(await cnf.exists())) {
await cnf.create({
schema: {
rule: {
properties: {
type: { type: "string" },
data: { type: "object" }
},
required: ["type", "data"]
}
}
});
}
if (!(await users.exists())) {
await users.create({
schema: {
rule: {
properties: {
username: { type: "string" },
password: { type: "string" },
permissions: {
type: "array",
items: { type: "string" }
}
},
required: ["username", "permissions"]
}
}
});
await users.save({
username: DB_USERNAME || "root",
password: await hash(DB_PASSWORD || "SBDataTracker", saltRounds),
permissions: [Permissions.ALL]
});
await users.save({
username: "_guest",
permissions: [Permissions.VIEW_PROFILES]
});
}
if (!(await sessions.exists())) {
await sessions.create({
schema: {
rule: {
properties: {
token: { type: "string" },
exp: { type: "number" },
type: { type: "string" },
user: { type: "string" }
},
required: ["token", "exp", "type", "user"]
}
}
});
}
// Delete expired sessions
const now = Date.now();
await db.query(aql`
FOR doc in ${sessions}
FILTER doc.exp <= ${now}
REMOVE doc in ${sessions}
`);
const cursor = await db.query(
aql`
FOR doc IN ${cnf}
FILTER doc.type == "tracker"
RETURN { _key: doc._key }
`,
{
count: true
}
);
/*
Instead of running a loop on all trackers at once,
they are spread out evenly across the delay which
lowers the chance of exceeding the Hypixel rate-limit
when there are a lot of trackers.
TODO : Add a hard-stop if the rate-limit is about
to be exceeded.
*/
if (cursor.hasNext) {
let counter = 0;
let interval: NodeJS.Timeout;
const iterate = async () => {
try {
if (++counter >= cursor.count!) clearInterval(interval);
const { _key: profile } = await cursor.next();
if (profile) {
const col = db.collection(`c${profile}`);
if (!(await col.exists())) col.create();
console.log(`${new Date()}\tLogging '${profile}'`);
await col.save(await log(API_KEY || "", profile));
} else {
console.warn(
`Configuration entry '${profile}' with type 'tracker' has no "profile" value`
);
}
} catch (e) {
console.error(e);
}
};
// Separated into setInterval and a setTimeout so when the script is started,
// immediately at least one profile is logged. This is useful for debugging
// since you don't have to wait for up to 3 minutes to pass.
if (cursor.count! > 1)
interval = setInterval(iterate, delay / (cursor.count! - 1));
setTimeout(iterate);
}
};
setInterval(intervalFunc, delay, db);
setTimeout(intervalFunc, 0, db);
polka({
onError: err => {
if (err) console.log("Error", err);
}
})
.use(
compression({ threshold: 0 }),
sirv("static", { dev }),
sapper.middleware()
)
.listen(PORT);