diff --git a/src/lib/hypixel.ts b/src/lib/hypixel.ts index 98013c1..30e89a0 100644 --- a/src/lib/hypixel.ts +++ b/src/lib/hypixel.ts @@ -30,3 +30,92 @@ export async function getAPIKeyInformation(key: string): Promise< _response: response }; } + +/** + * Get Skyblock profiles by player + * @param key API key + * @param player The player's UUID + * @returns A response with the player's profiles + */ +export async function getProfilesByPlayer( + key: string, + player: string +): Promise< + HypixelResponse<{ + profiles?: { + profile_id: string; + members: any; + community_upgrades: any | null; + cute_name: string; + banking: any | null; + game_mode: string | null; + }[]; + }> +> { + let response = await request( + `/skyblock/profiles?uuid=${player}`, + authorize(key) + ); + return { + ...(await response.json()), + _response: response + }; +} + +/** + * Get Skyblock profile by its UUID + * @param key API key + * @param profile The profile's UUID + * @returns A response with the profile data + */ +export async function getProfileByUUID( + key: string, + profile: string +): Promise< + HypixelResponse<{ + profile?: { + profile_id: string; + members: any; + community_upgrades: any | null; + cute_name: string; + banking: any | null; + game_mode: string | null; + }; + }> +> { + let response = await request( + `/skyblock/profile?profile=${profile}`, + authorize(key) + ); + return { + ...(await response.json()), + _response: response + }; +} + +/** + * Get a player's current online status + * @param key API key + * @param player Player to get status of + * @returns A response with player status data + */ +export async function getOnlineStatus( + key: string, + player: string +): Promise< + HypixelResponse<{ + uuid?: string; + session?: { + online: boolean; + gameType: string; + mode: string; + map: string; + }; + }> +> { + let response = await request(`/status?uuid=${player}`, authorize(key)); + return { + ...(await response.json()), + _response: response + }; +} diff --git a/src/lib/permissions.ts b/src/lib/permissions.ts new file mode 100644 index 0000000..0b567d0 --- /dev/null +++ b/src/lib/permissions.ts @@ -0,0 +1,17 @@ +export enum Permissions { + ALL = "*" +} + +/** + * Check permissions + * @param required The permission required + * @param has The permissions to check against + * @returns Whether the required permissions are fulfilled + */ +export function checkPermissions(required: string[], has: string[]): boolean { + if (has.includes("*")) return true; + + return !required.some(req => { + if (!has.includes(req)) return true; + }); +} diff --git a/src/lib/util.ts b/src/lib/util.ts index 07ea89c..c859bf0 100644 --- a/src/lib/util.ts +++ b/src/lib/util.ts @@ -1,5 +1,3 @@ -import { join } from "path"; - /** * Get a fetch function * @returns A fetch function @@ -11,6 +9,12 @@ export function getFetch(): typeof fetch { return fetch; } +/** + * Create a fetch-like function with defaults + * @param baseURL Base URL of all requests + * @param defaultInit Default RequestInit options + * @returns A fetch-like function + */ export function requestWithDefaults( baseURL: string, defaultInit?: RequestInit @@ -20,7 +24,7 @@ export function requestWithDefaults( overrideInit?: RequestInit ): Promise { let response = await getFetch()( - join(baseURL, endpoint), + new URL(endpoint, baseURL).href, defaultInit ? deepMerge(defaultInit, overrideInit) : overrideInit ); if (response.ok) { diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 0000000..f16c046 --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,16 @@ +import { getOnlineStatus, getProfileByUUID } from "./lib/hypixel"; + +export async function log(key: string, profile: string) { + let r = await getProfileByUUID(key, profile); + if (!r.success || !r.profile) throw new Error("No success from Hypixel"); + + let d = r.profile; + for (const member in d.members) { + let onlineStatus = await getOnlineStatus(key, member); + if (onlineStatus.success && onlineStatus.session) { + d.members[member].online_status = onlineStatus.session; + } + } + + return d; +} diff --git a/src/server.ts b/src/server.ts index 636bcbb..df5dc44 100644 --- a/src/server.ts +++ b/src/server.ts @@ -3,8 +3,88 @@ import polka from "polka"; import compression from "compression"; import * as sapper from "@sapper/server"; -const { PORT, NODE_ENV } = process.env; +import arangojs, { Database, aql } from "arangojs"; +import { hash } from "bcrypt"; +import { log } from "./logger"; + +const { PORT, NODE_ENV, DB_USERNAME, DB_PASSWORD, DB_URL, DB_NAME, API_KEY } = + process.env; const dev = NODE_ENV === "development"; +const db = arangojs({ + auth: { + username: DB_USERNAME || "root", + password: DB_PASSWORD + }, + url: DB_URL || "http://127.0.0.1:8529", + databaseName: DB_NAME || "sbdatatracker" +}); +const saltRounds = 10; +const delay = 3 * 60 * 1000; + +const intervalFunc = async (db: Database) => { + const cnf = db.collection("config"); + const users = db.collection("users"); + if (!(await cnf.exists())) { + await cnf.create(); + } + if (!(await users.exists())) { + await users.create(); + await users.save({ + username: DB_USERNAME, + password: await hash(DB_PASSWORD || "SBDataTracker", saltRounds), + forceChangePassword: true, + permissions: ["*"] + }); + } + + const cursor = await db.query( + aql` + FOR doc IN ${cnf} + FILTER doc.type == "tracker" + RETURN doc + `, + { + 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 () => { + if (++counter >= (cursor.count as number)) clearInterval(interval); + + const { _key, profile } = await cursor.next(); + if (profile) { + const col = db.collection(`c${profile}`); + if (!(await col.exists())) col.create(); + + await col.save(await log(API_KEY || "", profile)); + } else { + console.warn( + `Configuration entry '${_key}' with type 'tracker' has no "profile" value` + ); + } + }; + if ((cursor.count as number) > 1) + interval = setInterval( + iterate, + delay / ((cursor.count as number) - 1) + ); + setTimeout(iterate); + } +}; + +setInterval(intervalFunc, delay, db); +setTimeout(intervalFunc, 0, db); polka({ onError: err => {