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"; export async function* readLines(readable: ReadableStream) { const decoder = new TextDecoder(); let buffer = ""; for await (const chunk of readable) { buffer += decoder.decode(chunk, { stream: true }); 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; } } const stdinLines = readLines(Deno.stdin.readable); export async function readline( { skipEmpty = true }: { skipEmpty?: boolean } = {}, ) { while (true) { const result = await stdinLines.next(); if (result.done) { throw new Error("EOF"); } const line = result.value; if (!skipEmpty || line !== "") { return line; } } } export function urlBase64Encode(data: ArrayBuffer) { return base64.encodeBase64(data) .replaceAll("+", "-") .replaceAll("/", "_") .replaceAll("=", ""); } export function urlBase64Decode(data: string) { return base64.encodeBase64( data .replaceAll("_", "/") .replaceAll("-", "+"), ); } type PromiseReturnType = T extends () => Promise ? R : never; export async function retry Promise>( f: F, times = 2, ): Promise> { let lastError; for (let i = 0; i < times; i++) { try { return await f() as PromiseReturnType; } catch (e) { lastError = e; } } throw lastError; } const GLOBAL_CACHE: Record = {}; export function cache Promise>( f: F, { key = f.name, expireIn = 3600 }: { key?: string; expireIn?: number } = {}, ): () => Promise> { return async () => { const cached = GLOBAL_CACHE[key]; if (cached && cached.ts + expireIn * 1000 > Date.now()) { return cached.value as PromiseReturnType; } const value = await f(); GLOBAL_CACHE[key] = { ts: Date.now(), value, }; return value as PromiseReturnType; }; } export async function showError(env: Env, p: Promise): Promise { try { 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; } } /** * @param id id of VsHistoryDetail or CoopHistoryDetail * @returns */ export function gameId( id: string, ): Promise { 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") { return uuid.v5.generate(COOP_NAMESPACE, base64.decodeBase64(id)); } else { throw new Error("Unknown type"); } } export function s3siGameId(id: string) { const fullId = base64.decodeBase64(id); const tsUuid = fullId.slice(fullId.length - 52, fullId.length); return uuid.v5.generate(S3SI_NAMESPACE, tsUuid); } /** * https://github.com/spacemeowx2/s3si.ts/issues/45 * * @param id id of CoopHistoryDetail * @returns uuid used in stat.ink */ export function s3sCoopGameId(id: string) { const fullId = base64.decodeBase64(id); const tsUuid = fullId.slice(fullId.length - 52, fullId.length); return uuid.v5.generate(COOP_NAMESPACE, tsUuid); } /** * @param id VsHistoryDetail id or CoopHistoryDetail id */ export function parseHistoryDetailId(id: string) { const plainText = new TextDecoder().decode(base64.decodeBase64(id)); 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)!; return { type: "VsHistoryDetail", uid, listType, timestamp, uuid, } as const; } else if (coopRE.test(plainText)) { const [, uid, timestamp, uuid] = plainText.match(coopRE)!; return { type: "CoopHistoryDetail", uid, timestamp, uuid, } as const; } else { throw new Error(`Invalid ID: ${plainText}`); } } export const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); /** * Decode ID and get number after '-' */ export function b64Number(id: string): number { const text = new TextDecoder().decode(base64.decodeBase64(id)); const [_, num] = text.split("-"); return parseInt(num); } export function nonNullable(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; } } 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); };