s3si.ts/src/utils.ts

226 lines
5.4 KiB
TypeScript
Raw Normal View History

import { APIError } from "./APIError.ts";
import {
BATTLE_NAMESPACE,
COOP_NAMESPACE,
S3SI_NAMESPACE,
} from "./constant.ts";
import { base64, uuid } from "../deps.ts";
import { Env } from "./env.ts";
2022-11-18 07:02:15 -05:00
2024-01-30 02:38:39 -05:00
export async function* readLines(readable: ReadableStream<Uint8Array>) {
2024-01-30 01:30:56 -05:00
const decoder = new TextDecoder();
let buffer = "";
2024-01-30 02:38:39 -05:00
for await (const chunk of readable) {
buffer += decoder.decode(chunk, { stream: true });
2024-01-30 01:30:56 -05:00
let lineEndIndex;
while ((lineEndIndex = buffer.indexOf("\n")) !== -1) {
const line = buffer.slice(0, lineEndIndex).trim();
buffer = buffer.slice(lineEndIndex + 1);
yield line;
}
}
if (buffer.length > 0) {
yield buffer;
}
}
2024-01-30 02:38:39 -05:00
const stdinLines = readLines(Deno.stdin.readable);
2022-11-18 07:02:15 -05:00
export async function readline(
{ skipEmpty = true }: { skipEmpty?: boolean } = {},
) {
2024-01-22 02:13:05 -05:00
while (true) {
const result = await stdinLines.next();
if (result.done) {
throw new Error("EOF");
}
const line = result.value;
2022-11-18 07:02:15 -05:00
if (!skipEmpty || line !== "") {
return line;
}
}
}
2022-10-18 08:08:26 -04:00
export function urlBase64Encode(data: ArrayBuffer) {
2024-01-30 01:30:56 -05:00
return base64.encodeBase64(data)
.replaceAll("+", "-")
.replaceAll("/", "_")
2022-10-18 08:08:26 -04:00
.replaceAll("=", "");
}
export function urlBase64Decode(data: string) {
2024-01-30 01:30:56 -05:00
return base64.encodeBase64(
2022-10-18 08:08:26 -04:00
data
.replaceAll("_", "/")
.replaceAll("-", "+"),
2022-10-18 08:08:26 -04:00
);
}
2022-10-18 09:16:51 -04:00
2022-10-18 20:36:58 -04:00
type PromiseReturnType<T> = T extends () => Promise<infer R> ? R : never;
export async function retry<F extends () => Promise<unknown>>(
f: F,
times = 2,
): Promise<PromiseReturnType<F>> {
let lastError;
for (let i = 0; i < times; i++) {
try {
return await f() as PromiseReturnType<F>;
} catch (e) {
lastError = e;
}
}
throw lastError;
}
2022-10-19 04:56:18 -04:00
const GLOBAL_CACHE: Record<string, { ts: number; value: unknown }> = {};
export function cache<F extends () => Promise<unknown>>(
f: F,
{ key = f.name, expireIn = 3600 }: { key?: string; expireIn?: number } = {},
): () => Promise<PromiseReturnType<F>> {
return async () => {
const cached = GLOBAL_CACHE[key];
if (cached && cached.ts + expireIn * 1000 > Date.now()) {
return cached.value as PromiseReturnType<F>;
}
const value = await f();
GLOBAL_CACHE[key] = {
ts: Date.now(),
value,
};
return value as PromiseReturnType<F>;
};
}
export async function showError<T>(env: Env, p: Promise<T>): Promise<T> {
try {
2022-10-24 08:46:21 -04:00
return await p;
} catch (e) {
if (e instanceof APIError) {
env.logger.error(
`\n\nAPIError: ${e.message}`,
"\nResponse: ",
e.response,
"\nBody: ",
e.json,
);
} else {
env.logger.error(e);
}
throw e;
}
}
2022-10-20 22:47:56 -04:00
2022-10-22 06:40:20 -04:00
/**
2022-10-24 14:45:36 -04:00
* @param id id of VsHistoryDetail or CoopHistoryDetail
2022-10-22 06:40:20 -04:00
* @returns
*/
2022-10-24 14:45:36 -04:00
export function gameId(
2022-10-20 22:47:56 -04:00
id: string,
): Promise<string> {
const parsed = parseHistoryDetailId(id);
if (parsed.type === "VsHistoryDetail") {
const content = new TextEncoder().encode(
`${parsed.timestamp}_${parsed.uuid}`,
);
return uuid.v5.generate(BATTLE_NAMESPACE, content);
} else if (parsed.type === "CoopHistoryDetail") {
2024-01-30 01:30:56 -05:00
return uuid.v5.generate(COOP_NAMESPACE, base64.decodeBase64(id));
} else {
throw new Error("Unknown type");
}
}
export function s3siGameId(id: string) {
2024-01-30 01:30:56 -05:00
const fullId = base64.decodeBase64(id);
2022-10-20 22:47:56 -04:00
const tsUuid = fullId.slice(fullId.length - 52, fullId.length);
return uuid.v5.generate(S3SI_NAMESPACE, tsUuid);
2022-10-20 22:47:56 -04:00
}
2022-10-22 06:40:20 -04:00
/**
* https://github.com/spacemeowx2/s3si.ts/issues/45
*
* @param id id of CoopHistoryDetail
* @returns uuid used in stat.ink
*/
export function s3sCoopGameId(id: string) {
2024-01-30 01:30:56 -05:00
const fullId = base64.decodeBase64(id);
const tsUuid = fullId.slice(fullId.length - 52, fullId.length);
return uuid.v5.generate(COOP_NAMESPACE, tsUuid);
}
2022-10-24 14:45:36 -04:00
/**
* @param id VsHistoryDetail id or CoopHistoryDetail id
*/
export function parseHistoryDetailId(id: string) {
2024-01-30 01:30:56 -05:00
const plainText = new TextDecoder().decode(base64.decodeBase64(id));
2022-10-22 06:40:20 -04:00
2022-10-24 14:45:36 -04:00
const vsRE =
/VsHistoryDetail-([a-z0-9-]+):(\w+):(\d{8}T\d{6})_([0-9a-f-]{36})/;
const coopRE = /CoopHistoryDetail-([a-z0-9-]+):(\d{8}T\d{6})_([0-9a-f-]{36})/;
if (vsRE.test(plainText)) {
const [, uid, listType, timestamp, uuid] = plainText.match(vsRE)!;
2022-10-22 06:40:20 -04:00
2022-10-24 14:45:36 -04:00
return {
type: "VsHistoryDetail",
uid,
listType,
timestamp,
uuid,
} as const;
2022-10-24 14:45:36 -04:00
} else if (coopRE.test(plainText)) {
const [, uid, timestamp, uuid] = plainText.match(coopRE)!;
2022-10-22 06:40:20 -04:00
2022-10-24 14:45:36 -04:00
return {
type: "CoopHistoryDetail",
uid,
timestamp,
uuid,
} as const;
2022-10-24 14:45:36 -04:00
} else {
throw new Error(`Invalid ID: ${plainText}`);
}
2022-10-22 06:40:20 -04:00
}
2022-10-22 18:52:08 -04:00
export const delay = (ms: number) =>
new Promise<void>((resolve) => setTimeout(resolve, ms));
/**
* Decode ID and get number after '-'
*/
export function b64Number(id: string): number {
2024-01-30 01:30:56 -05:00
const text = new TextDecoder().decode(base64.decodeBase64(id));
const [_, num] = text.split("-");
return parseInt(num);
}
2022-11-25 07:20:47 -05:00
export function nonNullable<T>(v: T | null | undefined): v is T {
return v !== null && v !== undefined;
}
/**
* Only preserve the pathname of the URL
* @param url A url
*/
export function urlSimplify(url: string): { pathname: string } | string {
try {
const { pathname } = new URL(url);
return { pathname };
} catch (_e) {
return url;
}
}
2023-06-06 07:37:52 -04:00
export const battleTime = (id: string) => {
const { timestamp } = parseHistoryDetailId(id);
const dateStr = timestamp.replace(
/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})/,
"$1-$2-$3T$4:$5:$6Z",
);
return new Date(dateStr);
};