big commit without a message

This commit is contained in:
Gleb Koval 2021-07-18 21:13:00 +01:00
parent 051d5b3c52
commit d96fb38aa1
No known key found for this signature in database
GPG Key ID: 120F2F6DA9D995FB
29 changed files with 919 additions and 124 deletions

143
package-lock.json generated
View File

@ -11,8 +11,12 @@
"bcrypt": "^5.0.1",
"compression": "^1.7.1",
"cookie": "^0.4.0",
"echarts": "^5.1.2",
"hat": "^0.0.3",
"json-bigint": "^1.0.0",
"nbt-ts": "^1.3.3",
"node-fetch": "^2.6.1",
"node-gzip": "^1.1.2",
"polka": "^0.5.2",
"sirv": "^1.0.0"
},
@ -34,7 +38,9 @@
"@types/compression": "^1.7.0",
"@types/cookie": "^0.4.0",
"@types/hat": "^0.0.1",
"@types/json-bigint": "^1.0.1",
"@types/node": "^14.11.1",
"@types/node-gzip": "^1.1.0",
"@types/polka": "^0.5.1",
"rollup": "^2.3.4",
"rollup-plugin-svelte": "^7.0.0",
@ -1864,6 +1870,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/json-bigint": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/json-bigint/-/json-bigint-1.0.1.tgz",
"integrity": "sha512-zpchZLNsNuzJHi6v64UBoFWAvQlPhch7XAi36FkH6tL1bbbmimIF+cS7vwkzY4u5RaSWMoflQfu+TshMPPw8uw==",
"dev": true
},
"node_modules/@types/mime": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
@ -1875,6 +1887,15 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.4.tgz",
"integrity": "sha512-8kQ3+wKGRNN0ghtEn7EGps/B8CzuBz1nXZEIGGLP2GnwbqYn4dbTs7k+VKLTq1HvZLRCIDtN3Snx1Ege8B7L5A=="
},
"node_modules/@types/node-gzip": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@types/node-gzip/-/node-gzip-1.1.0.tgz",
"integrity": "sha512-j7cGb6HIOZbDx3sqe9/9VAPeSvyt143yu5k35gzRXE3mxEgK6BOZ6BAiJ3ToXBcJqLzL9Cr53dav21jlp3f9gw==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/polka": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/@types/polka/-/polka-0.5.2.tgz",
@ -2095,6 +2116,14 @@
"node": ">= 10.0.0"
}
},
"node_modules/bignumber.js": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz",
"integrity": "sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA==",
"engines": {
"node": "*"
}
},
"node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@ -2490,6 +2519,20 @@
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
},
"node_modules/echarts": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.1.2.tgz",
"integrity": "sha512-okUhO4sw22vwZp+rTPNjd/bvTdpug4K4sHNHyrV8NdAncIX9/AarlolFqtJCAYKGFYhUBNjIWu1EznFrSWTFxg==",
"dependencies": {
"tslib": "2.0.3",
"zrender": "5.1.1"
}
},
"node_modules/echarts/node_modules/tslib": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ=="
},
"node_modules/electron-to-chromium": {
"version": "1.3.768",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.768.tgz",
@ -2947,6 +2990,14 @@
"node": ">=4"
}
},
"node_modules/json-bigint": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
"dependencies": {
"bignumber.js": "^9.0.0"
}
},
"node_modules/json5": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
@ -3169,6 +3220,11 @@
"node": ">=8.3.0"
}
},
"node_modules/nbt-ts": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/nbt-ts/-/nbt-ts-1.3.3.tgz",
"integrity": "sha512-xmEVWDJzO7YdA2YJHqAkfiOKlKECXe/hfNB10t3W7aDJsCXTjXyRbhP5HYvCwrMefGk8p6arQqeMO2V6djyfxQ=="
},
"node_modules/negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
@ -3199,6 +3255,11 @@
"node": "4.x || >=6.0.0"
}
},
"node_modules/node-gzip": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/node-gzip/-/node-gzip-1.1.2.tgz",
"integrity": "sha512-ZB6zWpfZHGtxZnPMrJSKHVPrRjURoUzaDbLFj3VO70mpLTW5np96vXyHwft4Id0o+PYIzgDkBUjIzaNHhQ8srw=="
},
"node_modules/node-releases": {
"version": "1.1.73",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.73.tgz",
@ -4196,6 +4257,19 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/zrender": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.1.1.tgz",
"integrity": "sha512-oeWlmUZPQdS9f5hK4pV21tHPqA3wgQ7CkKkw7l0CCBgWlJ/FP+lRgLFtUBW6yam4JX8y9CdHJo1o587VVrbcoQ==",
"dependencies": {
"tslib": "2.0.3"
}
},
"node_modules/zrender/node_modules/tslib": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ=="
}
},
"dependencies": {
@ -5481,6 +5555,12 @@
"integrity": "sha512-GQoFDN07Knft7pvik72m/ddy8wmwMykzJEPZLoT7Wp3PHZsttdAbQ51qKf1DLWAdU6Ac31xon1Ji3g0hqQv6sw==",
"dev": true
},
"@types/json-bigint": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/json-bigint/-/json-bigint-1.0.1.tgz",
"integrity": "sha512-zpchZLNsNuzJHi6v64UBoFWAvQlPhch7XAi36FkH6tL1bbbmimIF+cS7vwkzY4u5RaSWMoflQfu+TshMPPw8uw==",
"dev": true
},
"@types/mime": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
@ -5492,6 +5572,15 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.4.tgz",
"integrity": "sha512-8kQ3+wKGRNN0ghtEn7EGps/B8CzuBz1nXZEIGGLP2GnwbqYn4dbTs7k+VKLTq1HvZLRCIDtN3Snx1Ege8B7L5A=="
},
"@types/node-gzip": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@types/node-gzip/-/node-gzip-1.1.0.tgz",
"integrity": "sha512-j7cGb6HIOZbDx3sqe9/9VAPeSvyt143yu5k35gzRXE3mxEgK6BOZ6BAiJ3ToXBcJqLzL9Cr53dav21jlp3f9gw==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/polka": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/@types/polka/-/polka-0.5.2.tgz",
@ -5681,6 +5770,11 @@
"node-addon-api": "^3.1.0"
}
},
"bignumber.js": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz",
"integrity": "sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA=="
},
"binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@ -5988,6 +6082,22 @@
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
},
"echarts": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.1.2.tgz",
"integrity": "sha512-okUhO4sw22vwZp+rTPNjd/bvTdpug4K4sHNHyrV8NdAncIX9/AarlolFqtJCAYKGFYhUBNjIWu1EznFrSWTFxg==",
"requires": {
"tslib": "2.0.3",
"zrender": "5.1.1"
},
"dependencies": {
"tslib": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ=="
}
}
},
"electron-to-chromium": {
"version": "1.3.768",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.768.tgz",
@ -6335,6 +6445,14 @@
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
"dev": true
},
"json-bigint": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
"requires": {
"bignumber.js": "^9.0.0"
}
},
"json5": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
@ -6497,6 +6615,11 @@
"resolved": "https://registry.npmjs.org/multi-part-lite/-/multi-part-lite-1.0.0.tgz",
"integrity": "sha512-KxIRbBZZ45hoKX1ROD/19wJr0ql1bef1rE8Y1PCwD3PuNXV42pp7Wo8lEHYuAajoT4vfAFcd3rPjlkyEEyt1nw=="
},
"nbt-ts": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/nbt-ts/-/nbt-ts-1.3.3.tgz",
"integrity": "sha512-xmEVWDJzO7YdA2YJHqAkfiOKlKECXe/hfNB10t3W7aDJsCXTjXyRbhP5HYvCwrMefGk8p6arQqeMO2V6djyfxQ=="
},
"negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
@ -6521,6 +6644,11 @@
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
},
"node-gzip": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/node-gzip/-/node-gzip-1.1.2.tgz",
"integrity": "sha512-ZB6zWpfZHGtxZnPMrJSKHVPrRjURoUzaDbLFj3VO70mpLTW5np96vXyHwft4Id0o+PYIzgDkBUjIzaNHhQ8srw=="
},
"node-releases": {
"version": "1.1.73",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.73.tgz",
@ -7259,6 +7387,21 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"zrender": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.1.1.tgz",
"integrity": "sha512-oeWlmUZPQdS9f5hK4pV21tHPqA3wgQ7CkKkw7l0CCBgWlJ/FP+lRgLFtUBW6yam4JX8y9CdHJo1o587VVrbcoQ==",
"requires": {
"tslib": "2.0.3"
},
"dependencies": {
"tslib": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ=="
}
}
}
}
}

View File

@ -14,8 +14,12 @@
"bcrypt": "^5.0.1",
"compression": "^1.7.1",
"cookie": "^0.4.0",
"echarts": "^5.1.2",
"hat": "^0.0.3",
"json-bigint": "^1.0.0",
"nbt-ts": "^1.3.3",
"node-fetch": "^2.6.1",
"node-gzip": "^1.1.2",
"polka": "^0.5.2",
"sirv": "^1.0.0"
},
@ -37,7 +41,9 @@
"@types/compression": "^1.7.0",
"@types/cookie": "^0.4.0",
"@types/hat": "^0.0.1",
"@types/json-bigint": "^1.0.1",
"@types/node": "^14.11.1",
"@types/node-gzip": "^1.1.0",
"@types/polka": "^0.5.1",
"rollup": "^2.3.4",
"rollup-plugin-svelte": "^7.0.0",

View File

@ -1,4 +1,5 @@
<script lang="ts">
export let hrefPrefix = "";
import { login } from "./stores";
import { getGraphs } from "../lib/api";
@ -15,14 +16,10 @@
</span>
{:then graphs}
{#each graphs as graph}
<a class="item" href="./graph/{graph.graph}">
<a class="item" href="{hrefPrefix}/graph/{graph.graph}">
<h2>{graph.data.name} <span>#{graph.graph}</span></h2>
<h3>
Value:
{#each graph.data.value as key}
<span>{key}</span>
{/each}
</h3>
<p>{graph.data.style}</p>
<code>{graph.data.value}</code>
</a>
{/each}
<!-- svelte-ignore a11y-missing-content -->
@ -36,6 +33,7 @@
<style lang="scss">
@use "../styles/GridList";
@use "../styles/GridItems";
a.item {
text-decoration: none;
color: hsl(30, 20%, 90%);
@ -51,16 +49,7 @@
font-weight: 100;
font-size: 0.75em;
}
h3 span {
font-weight: 400;
}
h3 span::after {
content: " > ";
font-size: 0.6em;
color: hsl(30, 5%, 60%);
vertical-align: middle;
}
h3 span:nth-last-child(1)::after {
content: "";
.add {
@include GridItems.add;
}
</style>

View File

@ -1,21 +1,48 @@
<script lang="ts">
export let segment: string | undefined;
import { postLogout } from "../lib/api";
import { getPermissions, postLogout } from "../lib/api";
import { checkPermissions, Permissions } from "../lib/permissions";
import { extraNav, login } from "./stores";
let profiles = false;
let users = false;
$: if ($login !== undefined) {
getPermissions($login?.token).then(permissions => {
if (checkPermissions([Permissions.VIEW_PROFILES], permissions)) {
profiles = true;
}
if (checkPermissions([Permissions.VIEW_USERS], permissions)) {
users = true;
}
});
}
</script>
<nav>
<a href="/" class:active={segment === undefined || segment === "profile"}>
<a
href="/profiles"
class:active={segment === "profiles" || segment === "profile"}
class:invisible={!profiles}
>
<h3>Profiles</h3>
</a>
<span class="sep" />
<a
href="/graphs"
class:active={segment === "graphs" || segment === "graph"}
class:invisible={!profiles}
>
<h3>Graphs</h3>
</a>
<span class="sep" />
<a
href="/users"
class:active={segment === "users" || segment === "user"}
class:invisible={!users}
>
<h3>Users</h3>
</a>
<span class="sep" />
{#if $extraNav !== undefined}
<span class="extra">
<h3>
@ -128,4 +155,8 @@
}
}
}
.invisible,
.invisible + .sep {
display: none;
}
</style>

View File

@ -49,3 +49,28 @@ function createExtraNav() {
}
export const extraNav = createExtraNav();
function createCache() {
const { subscribe, set, update } = writable<{
[key: string]: string;
}>({});
return {
subscribe,
set: (key: string, value: string) =>
update(current => ({
...current,
[key]: value
})),
unset: (key: string) =>
update(current => {
delete current[key];
return current;
}),
clear: () => set({}),
update
};
}
export const playerCache = createCache();
export const profileCache = createCache();

View File

@ -55,11 +55,33 @@ export async function getProfiles(
/**
* Get a list of skyblock profiles by player
* @param token Authorization token
* @param player Player's UUID
* @returns A list of profiles
*/
export async function getPlayerProfiles(player: string): Promise<Profile[]> {
let response = await request(`/api/profiles/${player}`);
export async function getPlayerProfiles(
token: string,
player: string
): Promise<Profile[]> {
let response = await request(`/api/profiles/${player}`, {
headers: { Authorization: token }
});
return response.json();
}
/**
* Get a palayer's profile cute name
* @param player Player's UUID
* @param profile Profile's UUID
* @returns Profile's cute name
*/
export async function getProfileCuteName(
player: string,
profile: string
): Promise<string> {
let response = await request(
`/api/profiles/${player}/${profile}/cute-name`
);
return response.json();
}
@ -119,3 +141,57 @@ export async function putProfiles(
);
return response.json();
}
/**
* Get permissions of authenticated user
* @param token Authorization token
* @returns Array of permissions
*/
export async function getPermissions(token?: string): Promise<string[]> {
let response = await request(
"/api/permissions",
token ? { headers: { Authorization: token } } : {}
);
return response.json();
}
export async function putGraph(
token: string | undefined,
type: string,
name: string,
style: string,
value: string,
xAxisName: string = "Date & time",
xAxisType: string = "time",
yAxisName: string = "",
yAxisType: string = "value"
): Promise<void> {
let searchParams = new URLSearchParams({
type,
name,
style,
value,
x_axis_name: xAxisName,
x_axis_type: xAxisType,
y_axis_name: yAxisName,
y_axis_type: yAxisType
});
await request("/api/graphs?" + searchParams.toString(), {
method: "PUT",
...(token ? { headers: { Authorization: token } } : {})
});
return;
}
export async function getGraph(
token: string | undefined,
graph: string,
profile: string,
member: string
): Promise<[number, number][]> {
let response = await request(
`/api/graphs/${graph}/${profile}/${member}`,
token ? { headers: { Authorization: token } } : {}
);
return response.json();
}

View File

@ -3,6 +3,7 @@ import type { Document } from "arangojs/documents";
import { compare } from "bcrypt";
import type { IncomingMessage, ServerResponse } from "http";
import type { Session, User } from "../routes/api/_types";
import type { Profile } from "./hypixel";
import { checkPermissions } from "./permissions";
/**

View File

@ -6,7 +6,38 @@ export type HypixelResponse<T> = {
} & T;
export const ENDPOINT = "https://api.hypixel.net/";
const request = requestWithDefaults(ENDPOINT);
// Request function with rate limit handling
const request: ReturnType<typeof requestWithDefaults> = (() => {
async function req(
endpoint: string,
init?: RequestInit
): Promise<Response> {
try {
let response = await requestWithDefaults(ENDPOINT)(endpoint, init);
console.log(
`Left: ${response.headers.get(
"ratelimit-remaining"
)}\tReset: ${response.headers.get("ratelimit-reset")}s`
);
return response;
} catch (e) {
let response: Response = e.response;
if (response.status === 429) {
const reset = response.headers.get("ratelimit-reset");
if (reset) {
console.log("Rate limited, waiting for", reset, "seconds");
await new Promise(resolve =>
setTimeout(resolve, (2 + parseInt(reset, 10)) * 1000)
);
return req(endpoint, init);
}
}
throw e;
}
}
return req;
})();
/**
* Get Hypixel API key information
@ -38,6 +69,7 @@ export type Profile = {
cute_name: string;
banking: any | null;
game_mode: string | null;
timestamp: number;
};
/**
* Get Skyblock profiles by player

View File

@ -1,11 +1,12 @@
export enum Permissions {
ALL = "*",
VIEW = "view",
VIEW_PROFILES = "view_profiles",
ADD_PROFILE = "add_profile",
REMOVE_PROFILE = "remove_profile",
ADD_GRAPH = "add_graph",
MANUAL_GRAPH = "manual_graph", // separate because allows for remote code execution
REMOVE_GRAPH = "remove_graph",
VIEW_USERS = "view_users",

View File

@ -1,16 +1,59 @@
import { getOnlineStatus, getProfileByUUID } from "./lib/hypixel";
import nbt from "nbt-ts";
import gzip from "node-gzip";
import { getOnlineStatus, getProfileByUUID, Profile } 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;
let d: Profile = r.profile as any;
d.timestamp = Date.now();
for (const member in d.members) {
let onlineStatus = await getOnlineStatus(key, member);
if (onlineStatus.success && onlineStatus.session) {
d.members[member].online_status = onlineStatus.session;
d.members[member].online_status.timestamp = Date.now();
}
const inventories = [
"inv_armor",
"quiver",
"talisman_bag",
"backpack_icons",
"fishing_bag",
"ender_chest_contents",
"wardrobe_contents",
"potion_bag",
"personal_vault_contents",
"inv_contents",
"candy_inventory_contents"
];
if (d.members[member].backpack_contents) {
for (const backpack in d.members[member].backpack_contents) {
await decodeInventory(
d.members[member].backpack_contents[backpack]
);
}
}
if (d.members[member].backpack_icons) {
for (const icon in d.members[member].backpack_icons) {
await decodeInventory(d.members[member].backpack_icons[icon]);
}
}
for (const idx in inventories) {
const inv = inventories[idx];
await decodeInventory(d.members[member][inv]);
}
}
return d;
}
async function decodeInventory(inventory: any) {
if (inventory?.data) {
inventory.data = nbt.decode(
await gzip.ungzip(Buffer.from(inventory.data, "base64"))
);
}
}

View File

@ -40,7 +40,9 @@
padding-top: $navbar-padding;
min-height: calc(100% - #{$navbar-padding} - 0.75em);
}
:global(*) {
:global(button),
:global(input),
:global(textarea) {
font-family: "Fira Code", monospace;
}
</style>

View File

@ -25,10 +25,26 @@ export type Tracker = Config<
}
>;
export type Graph = Config<
"graph",
{
// Graphs
export type Axis = {
name?: string;
type?: string;
};
export type BaseGraph<TS = string, T = any> = Config<
TS,
T & {
name: string;
value: (string | number)[];
style: string;
x_axis?: Axis;
y_axis?: Axis;
}
>;
export type Graph = BaseGraph<
"manual_graph",
{
name: string;
value: string;
}
>;

View File

@ -2,18 +2,20 @@ import { aql } from "arangojs";
import type { Document } from "arangojs/documents";
import type { IncomingMessage, ServerResponse } from "http";
import { authorizeRequest } from "../../lib/butil";
import { Permissions } from "../../lib/permissions";
import { checkPermissions, Permissions } from "../../lib/permissions";
import { db } from "../../server";
import type { Graph } from "./_types";
export async function get(req: IncomingMessage, res: ServerResponse) {
const [_, exit] = await authorizeRequest(req, res, db, [Permissions.VIEW]);
const [_, exit] = await authorizeRequest(req, res, db, [
Permissions.VIEW_PROFILES
]);
if (exit) return;
let graphs: (Graph & { graph: string })[] = [];
const cursor = await db.query(aql`
FOR doc IN config
FILTER doc.type == "graph"
FILTER CONTAINS(doc.type, "graph")
RETURN doc
`);
while (cursor.hasNext) {
@ -27,3 +29,93 @@ export async function get(req: IncomingMessage, res: ServerResponse) {
res.writeHead(200);
res.end(JSON.stringify(graphs));
}
export async function put(req: IncomingMessage, res: ServerResponse) {
const [user, exit] = await authorizeRequest(req, res, db, [
Permissions.ADD_GRAPH
]);
if (exit) return;
const url = new URL(req.url!, "https://example.com/");
if (!url.searchParams.get("name")) {
res.writeHead(400);
res.end(
JSON.stringify({
message: "name query parameter should be set"
})
);
return;
}
if (!url.searchParams.get("style")) {
res.writeHead(400);
res.end(
JSON.stringify({
message: "style query parameter should be set"
})
);
return;
}
if (!url.searchParams.get("type")) {
res.writeHead(400);
res.end(
JSON.stringify({
message: "type query parameter should be set"
})
);
return;
}
switch (url.searchParams.get("type")) {
case "manual_graph":
if (
!checkPermissions([Permissions.MANUAL_GRAPH], user!.permissions)
) {
res.writeHead(403);
res.end(
JSON.stringify({
message: "You are unauthroized to make manual graphs"
})
);
return;
}
if (!url.searchParams.get("value")) {
res.writeHead(400);
res.end(
JSON.stringify({
message: "value query parameter should be set"
})
);
return;
}
await db.collection("config").save({
type: "manual_graph",
data: {
name: url.searchParams.get("name"),
style: url.searchParams.get("style"),
value: url.searchParams.get("value"),
x_axis: {
name:
url.searchParams.get("x_axis_name") ||
"Date & time",
type: url.searchParams.get("x_axis_type") || "time"
},
y_axis: {
name: url.searchParams.get("y_axis_name") || undefined,
type: url.searchParams.get("y_axis_type") || "value"
}
}
} as Graph);
res.writeHead(204);
res.end();
break;
default:
res.writeHead(400);
res.end(
JSON.stringify({
message: "Invalid graph type"
})
);
}
}

View File

@ -0,0 +1,54 @@
import type { IncomingMessage, ServerResponse } from "http";
import { authorizeRequest } from "../../../../../lib/butil";
import { Permissions } from "../../../../../lib/permissions";
import { db } from "../../../../../server";
import type { Graph } from "../../../_types";
export async function get(
req: IncomingMessage & {
params: { graph: string; profile: string; player: string };
},
res: ServerResponse
) {
const [_, exit] = await authorizeRequest(req, res, db, [
Permissions.VIEW_PROFILES
]);
if (exit) return;
const cnf = db.collection("config");
const profile = db.collection("c" + req.params.profile);
if (!(await profile.exists())) {
res.writeHead(404);
res.end(
JSON.stringify({
message: "Profile not found"
})
);
}
let graph: Graph = await cnf.document(req.params.graph);
switch (graph.type) {
case "manual_graph":
let data = await (
await db.query({
query: `FOR profile IN ${profile.name}
LET member = profile.members[@member_uuid]
${graph.data.value}`,
bindVars: {
member_uuid: req.params.player
}
})
).all();
res.writeHead(200);
res.end(JSON.stringify(data));
return;
default:
res.writeHead(404);
res.end(
JSON.stringify({
message: "Not a graph ID"
})
);
return;
}
}

View File

@ -0,0 +1,11 @@
import type { IncomingMessage, ServerResponse } from "http";
import { authorizeRequest } from "../../lib/butil";
import { db } from "../../server";
export async function get(req: IncomingMessage, res: ServerResponse) {
const [user, exit] = await authorizeRequest(req, res, db, []);
if (exit) return;
res.writeHead(200);
res.end(JSON.stringify(user!.permissions));
}

View File

@ -8,7 +8,9 @@ import { db, API_KEY } from "../../server";
import type { Tracker } from "./_types";
export async function get(req: IncomingMessage, res: ServerResponse) {
const [_, exit] = await authorizeRequest(req, res, db, [Permissions.VIEW]);
const [_, exit] = await authorizeRequest(req, res, db, [
Permissions.VIEW_PROFILES
]);
if (exit) return;
let profiles: (Tracker & { profile: string })[] = [];

View File

@ -1,11 +1,18 @@
import type { IncomingMessage, ServerResponse } from "http";
import { authorizeRequest } from "../../../lib/butil";
import { Permissions } from "../../../lib/permissions";
import { getProfilesByPlayer } from "../../../lib/hypixel";
import { API_KEY } from "../../../server";
import { API_KEY, db } from "../../../server";
export async function get(
req: IncomingMessage & { params: { player: string } },
res: ServerResponse
) {
const [_, exit] = await authorizeRequest(req, res, db, [
Permissions.ADD_PROFILE
]);
if (exit) return;
let response = await getProfilesByPlayer(API_KEY || "", req.params.player);
if (response.success && response.profiles !== undefined) {
res.writeHead(200);

View File

@ -0,0 +1,42 @@
import type { IncomingMessage, ServerResponse } from "http";
import { getProfilesByPlayer } from "../../../../../lib/hypixel";
import { API_KEY, profileNameCache } from "../../../../../server";
export async function get(
req: IncomingMessage & { params: { player: string; profile: string } },
res: ServerResponse
) {
if (profileNameCache[req.params.player]?.[req.params.profile]) {
res.writeHead(200);
res.end(
JSON.stringify(
profileNameCache[req.params.player]?.[req.params.profile]
)
);
return;
}
let response = await getProfilesByPlayer(API_KEY || "", req.params.player);
if (response.success && response.profiles !== undefined) {
if (profileNameCache[req.params.player] === undefined) {
profileNameCache[req.params.player] = {};
}
response.profiles.forEach(
p =>
(profileNameCache[req.params.player][p.profile_id] =
p.cute_name)
);
res.writeHead(200);
res.end(
response.profiles.find(p => p.profile_id === req.params.profile)
?.cute_name
);
} else {
res.writeHead(response._response.status);
res.end(
JSON.stringify({
message: "Unsuccessful Hypixel API response"
})
);
}
}

View File

@ -0,0 +1,97 @@
<script lang="ts">
import { goto } from "@sapper/app";
import { login, extraNav } from "../../components/stores";
import { putGraph } from "../../lib/api";
$extraNav; // important for subscription to store to be activated
extraNav.set([["Add", () => (type = "")]]);
let type = "";
let name = "";
let style = "";
let value = "";
let status = "";
let xAxisName = "Date & time";
let yAxisName = "";
let xAxisType = "time";
let yAxisType = "value";
$: if (type !== "") {
extraNav.set([
["Add", () => (type = "")],
{
manual_graph: "Manual Graph"
}[type]!
]);
} else {
extraNav.set([["Add", () => (type = "")]]);
}
</script>
<svelte:head>
<title>Add Graph - Skyblock Data Tracker</title>
</svelte:head>
<div id="list">
{#if type === ""}
<button class="item" on:click={() => (type = "manual_graph")}>
<h1>Manual Graph</h1>
</button>
{:else if type === "manual_graph"}
<form
class="item"
on:submit={async event => {
event.preventDefault();
try {
await putGraph($login?.token, type, name, style, value);
goto("/graphs");
} catch (e) {
if (
e.response?.status === 401 ||
e.response?.status === 403
) {
alert("Unauthorised");
} else {
alert("An error occured while adding the profile");
}
status = "Error";
}
}}
>
<h1>Manual Graph</h1>
<p id="status">{status}</p>
<input type="text" placeholder="Name" bind:value={name} />
<input type="text" placeholder="Style" bind:value={style} />
<textarea rows="16" placeholder="AQL" bind:value />
<h2>X Axis</h2>
<p>
Name: <input type="text" bind:value={xAxisName} />
</p>
<p>
Type: <input type="text" bind:value={xAxisType} />
</p>
<h2>Y Axis</h2>
<p>
Name: <input type="text" bind:value={yAxisName} />
</p>
<p>
Type: <input type="text" bind:value={yAxisType} />
</p>
<input type="submit" value="Add" />
</form>
{/if}
</div>
<style lang="scss">
@use "../../styles/GridList";
@use "../../styles/CodeForm";
.item {
color: hsl(30, 20%, 90%);
font-size: 1.17em;
}
button.item:hover {
cursor: pointer;
}
</style>

View File

@ -1,70 +1,6 @@
<script lang="ts">
import { login } from "../components/stores";
import { getProfiles, getUsername } from "../lib/api";
import { goto } from "@sapper/app";
import { onMount } from "svelte";
let profilesPromise: ReturnType<typeof getProfiles> = new Promise(() => {});
$: if ($login !== undefined) {
profilesPromise = getProfiles($login?.token);
}
onMount(() => goto("/profiles"));
</script>
<svelte:head>
<title>Skyblock Data Tracker</title>
</svelte:head>
<div id="list">
{#await profilesPromise}
<div class="item">
<h2>Fetching profiles...</h2>
</div>
{:then profiles}
{#each profiles as profile}
<button type="button" class="item">
{#each profile.data.members as member, idx}
{#await getUsername(member)}
<a href="/profile/{profile.profile}/member/{member}"
>...</a
>
{:then username}
<a href="/profile/{profile.profile}/member/{member}">
{username}
{#if idx === 0 && profile.data.name}
@ {profile.data.name}
{/if}
</a>
{:catch error}
<p style="color: red">{error.message}</p>
{/await}
{/each}
</button>
{/each}
<!-- svelte-ignore a11y-missing-content -->
<a class="item add" href="/profile/new" />
{:catch error}
<div class="item">
<h2 style="color: red">{error.message}</h2>
</div>
{/await}
</div>
<style lang="scss">
@use "../styles/GridList";
.item {
font-size: 1.17em;
a {
display: block;
margin: 1em 0;
color: hsl(30, 20%, 90%);
text-decoration: none;
font-family: "Minecraft";
&:hover {
text-decoration: underline;
}
}
a:nth-child(1) {
color: hsl(60, 100%, 50%);
font-size: 2em;
}
}
</style>

View File

@ -12,4 +12,26 @@
<script lang="ts">
export let profile: string;
export let member: string;
import Graphs from "../../../../components/Graphs.svelte";
import { goto } from "@sapper/app";
import { extraNav, login } from "../../../../components/stores";
import { getPlayerProfiles, getUsername } from "../../../../lib/api";
$extraNav;
$: if ($login !== undefined) {
(async () => {
let username = await getUsername(member);
let profiles = await getPlayerProfiles(member);
extraNav.set([
[
username +
" @ " +
profiles.find(p => p.profile_id === profile)?.cute_name,
() => goto(`/profile/${profile}/member/${member}`)
]
]);
})();
}
</script>
<Graphs hrefPrefix="/profile/{profile}/member/{member}" />

View File

@ -0,0 +1,75 @@
<script context="module" lang="ts">
import type common from "@sapper/common";
export async function preload(page: common.Page) {
return {
profile: page.params.profile,
member: page.params.member,
graph: page.params.graph
};
}
</script>
<script lang="ts">
export let profile: string;
export let member: string;
export let graph: string;
import { onMount } from "svelte";
import { init } from "echarts";
import { getGraph, getGraphs } from "../../../../../../lib/api";
import { login } from "../../../../../../components/stores";
onMount(async () => {
let graphs = await getGraphs($login?.token);
let g = graphs.find(g => g.graph === graph);
let data = await getGraph($login?.token, graph, profile, member);
let c = document.querySelector("div#list");
if (c && g) {
init(c as HTMLElement, "dark").setOption({
title: {
text: g.data.name
},
tooltip: {
trigger: "axis"
},
xAxis: g.data.x_axis || {
name: "Date & time",
type: "time"
},
yAxis: g.data.y_axis || {
type: "value"
},
series: [
{
name: g.data.name,
type: g.data.style,
data: data
}
],
dataZoom: [
{
id: "dataZoomX",
type: "slider",
filterMode: "filter"
},
{
id: "dataZoomY",
type: "slider",
filterMode: "empty"
}
]
});
}
});
</script>
<div id="list">
<div class="item"><h1>Loading...</h1></div>
</div>
<style lang="scss">
@use "../../../../../../styles/GridList";
</style>

View File

@ -85,7 +85,7 @@
);
}
}
goto("/#");
goto("/profiles");
}}
>
{#await getUsername(uuid)}

View File

@ -0,0 +1,75 @@
<script lang="ts">
import { login } from "../components/stores";
import { getProfiles, getUsername } from "../lib/api";
let profilesPromise: ReturnType<typeof getProfiles> = new Promise(() => {});
$: if ($login !== undefined) {
profilesPromise = getProfiles($login?.token);
}
</script>
<svelte:head>
<title>Skyblock Data Tracker</title>
</svelte:head>
<div id="list">
{#await profilesPromise}
<div class="item">
<h2>Fetching profiles...</h2>
</div>
{:then profiles}
{#each profiles as profile}
<button type="button" class="item">
{#each profile.data.members as member, idx}
{#await getUsername(member)}
<a href="/profile/{profile.profile}/member/{member}"
>...</a
>
{:then username}
<a href="/profile/{profile.profile}/member/{member}">
{username}
{#if idx === 0 && profile.data.name}
@ {profile.data.name}
{/if}
</a>
{:catch error}
<p style="color: red">{error.message}</p>
{/await}
{/each}
</button>
{/each}
<!-- svelte-ignore a11y-missing-content -->
<a class="item add" href="/profile/new" />
{:catch error}
<div class="item">
<h2 style="color: red">{error.message}</h2>
</div>
{/await}
</div>
<style lang="scss">
@use "../styles/GridList";
@use "../styles/GridItems";
.item {
font-size: 1.17em;
a {
display: block;
margin: 1em 0;
color: hsl(30, 20%, 90%);
text-decoration: none;
font-family: "Minecraft";
&:hover {
text-decoration: underline;
}
}
a:nth-child(1) {
color: hsl(60, 100%, 50%);
font-size: 2em;
}
}
.add {
@include GridItems.add;
}
</style>

View File

@ -7,6 +7,16 @@ 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,
@ -71,7 +81,7 @@ const intervalFunc = async (db: Database) => {
});
await users.save({
username: "_guest",
permissions: [Permissions.VIEW]
permissions: [Permissions.VIEW_PROFILES]
});
}
if (!(await sessions.exists())) {

4
src/styles/CodeForm.scss Normal file
View File

@ -0,0 +1,4 @@
@use "./Form";
textarea {
@include Form.input;
}

View File

@ -7,7 +7,7 @@ form {
color: red;
}
}
input {
@mixin input {
background: none;
padding: 0.75em;
margin-bottom: 1em;
@ -22,6 +22,9 @@ input {
border-color: black;
}
}
input {
@include input;
}
input[type="submit"] {
margin: 0;
width: 100%;

18
src/styles/GridItems.scss Normal file
View File

@ -0,0 +1,18 @@
@mixin add {
position: relative;
display: block;
&::before,
&::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: min(12em, 30%);
height: 0.3em;
background-color: hsl(30, 20%, 90%);
}
&::before {
transform: translate(-50%, -50%) rotateZ(90deg);
}
}

View File

@ -22,21 +22,3 @@ div#list {
margin-bottom: 0;
}
}
.add {
position: relative;
display: block;
&::before,
&::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: min(12em, 30%);
height: 0.3em;
background-color: hsl(30, 20%, 90%);
}
&::before {
transform: translate(-50%, -50%) rotateZ(90deg);
}
}