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);