From 3e2eede47cc2bd0dd6902959becd4773c5dfa1e3 Mon Sep 17 00:00:00 2001 From: spacemeowx2 Date: Mon, 24 Oct 2022 20:46:21 +0800 Subject: [PATCH] feat: refetch token when 401 close #5 --- src/app.ts | 190 +++++++++++++++++++++++++++-------------------- src/constant.ts | 2 +- src/splatnet3.ts | 8 ++ src/utils.ts | 36 ++++++++- 4 files changed, 153 insertions(+), 83 deletions(-) diff --git a/src/app.ts b/src/app.ts index eb4dd70..5733617 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,11 +1,16 @@ import { getBulletToken, getGToken, loginManually } from "./iksm.ts"; import { MultiProgressBar, Mutex } from "../deps.ts"; -import { DEFAULT_STATE, FileStateBackend, State, StateBackend } from "./state.ts"; import { - checkToken, + DEFAULT_STATE, + FileStateBackend, + State, + StateBackend, +} from "./state.ts"; +import { getBankaraBattleHistories, getBattleDetail, getBattleList, + isTokenExpired, } from "./splatnet3.ts"; import { BattleExporter, @@ -17,7 +22,14 @@ import { import { Cache, FileCache, MemoryCache } from "./cache.ts"; import { StatInkExporter } from "./exporters/stat.ink.ts"; import { FileExporter } from "./exporters/file.ts"; -import { battleId, delay, readline, showError } from "./utils.ts"; +import { + battleId, + delay, + readline, + RecoverableError, + retryRecoverableError, + showError, +} from "./utils.ts"; export type Opts = { profilePath: string; @@ -159,9 +171,19 @@ type Progress = { export class App { state: State = DEFAULT_STATE; stateBackend: StateBackend; + recoveryToken: RecoverableError = { + name: "Refetch Token", + is: isTokenExpired, + recovery: async () => { + console.log("Token expired, refetch tokens."); + + await this.fetchToken(); + }, + }; constructor(public opts: Opts) { - this.stateBackend = opts.stateBackend ?? new FileStateBackend(opts.profilePath); + this.stateBackend = opts.stateBackend ?? + new FileStateBackend(opts.profilePath); } async writeState(newState: State) { this.state = newState; @@ -212,7 +234,10 @@ export class App { return out; } - async exportOnce() { + exportOnce() { + return retryRecoverableError(() => this._exportOnce(), this.recoveryToken); + } + async _exportOnce() { const bar = !this.opts.noProgress ? new MultiProgressBar({ title: "Export battles", @@ -220,58 +245,62 @@ export class App { }) : undefined; - const exporters = await this.getExporters(); + try { + const exporters = await this.getExporters(); - const fetcher = new BattleFetcher({ - cache: this.opts.cache ?? new FileCache(this.state.cacheDir), - state: this.state, - }); - console.log("Fetching battle list..."); - const battleList = await getBattleList(this.state); + const fetcher = new BattleFetcher({ + cache: this.opts.cache ?? new FileCache(this.state.cacheDir), + state: this.state, + }); + console.log("Fetching battle list..."); + const battleList = await getBattleList(this.state); - const allProgress: Record = {}; - const redraw = (name: string, progress: Progress) => { - allProgress[name] = progress; - bar?.render( - Object.entries(allProgress).map(([name, progress]) => ({ - completed: progress.current, - total: progress.total, - text: name, - })), + const allProgress: Record = {}; + const redraw = (name: string, progress: Progress) => { + allProgress[name] = progress; + bar?.render( + Object.entries(allProgress).map(([name, progress]) => ({ + completed: progress.current, + total: progress.total, + text: name, + })), + ); + }; + const stats: Record = Object.fromEntries( + exporters.map((e) => [e.name, 0]), ); - }; - const stats: Record = Object.fromEntries( - exporters.map((e) => [e.name, 0]), - ); - await Promise.all( - exporters.map((e) => - showError( - this.exportBattleList({ - fetcher, - exporter: e, - battleList, - onStep: (progress) => redraw(e.name, progress), - }) - .then((count) => { - stats[e.name] = count; - }), - ) - .catch((err) => { - console.error(`\nFailed to export to ${e.name}:`, err); - }) - ), - ); + await Promise.all( + exporters.map((e) => + showError( + this.exportBattleList({ + fetcher, + exporter: e, + battleList, + onStep: (progress) => redraw(e.name, progress), + }) + .then((count) => { + stats[e.name] = count; + }), + ) + .catch((err) => { + console.error(`\nFailed to export to ${e.name}:`, err); + }) + ), + ); - bar?.end(); + bar?.end(); - console.log( - `Exported ${ - Object.entries(stats) - .map(([name, count]) => `${name}: ${count}`) - .join(", ") - }`, - ); + console.log( + `Exported ${ + Object.entries(stats) + .map(([name, count]) => `${name}: ${count}`) + .join(", ") + }`, + ); + } finally { + bar?.end(); + } } async monitor() { while (true) { @@ -295,6 +324,36 @@ export class App { } bar?.end(); } + async fetchToken() { + const sessionToken = this.state.loginState?.sessionToken; + + if (!sessionToken) { + throw new Error("Session token is not set."); + } + + const { webServiceToken, userCountry, userLang } = await getGToken({ + fApi: this.state.fGen, + sessionToken, + }); + + const bulletToken = await getBulletToken({ + webServiceToken, + userLang, + userCountry, + appUserAgent: this.state.appUserAgent, + }); + + await this.writeState({ + ...this.state, + loginState: { + ...this.state.loginState, + gToken: webServiceToken, + bulletToken, + }, + userLang: this.state.userLang ?? userLang, + userCountry: this.state.userCountry ?? userCountry, + }); + } async run() { await this.readState(); @@ -309,35 +368,6 @@ export class App { }, }); } - const sessionToken = this.state.loginState!.sessionToken!; - - console.log("Checking token..."); - if (!await checkToken(this.state)) { - console.log("Token expired, refetch tokens."); - - const { webServiceToken, userCountry, userLang } = await getGToken({ - fApi: this.state.fGen, - sessionToken, - }); - - const bulletToken = await getBulletToken({ - webServiceToken, - userLang, - userCountry, - appUserAgent: this.state.appUserAgent, - }); - - await this.writeState({ - ...this.state, - loginState: { - ...this.state.loginState, - gToken: webServiceToken, - bulletToken, - }, - userLang: this.state.userLang ?? userLang, - userCountry: this.state.userCountry ?? userCountry, - }); - } if (this.opts.monitor) { await this.monitor(); diff --git a/src/constant.ts b/src/constant.ts index 35fad0b..be2c209 100644 --- a/src/constant.ts +++ b/src/constant.ts @@ -1,7 +1,7 @@ import type { StatInkPostBody, VsHistoryDetail } from "./types.ts"; export const AGENT_NAME = "s3si.ts"; -export const S3SI_VERSION = "0.1.6"; +export const S3SI_VERSION = "0.1.7"; export const NSOAPP_VERSION = "2.3.1"; export const WEB_VIEW_VERSION = "1.0.0-216d0219"; export const S3SI_LINK = "https://github.com/spacemeowx2/s3si.ts" diff --git a/src/splatnet3.ts b/src/splatnet3.ts index a783148..509ce41 100644 --- a/src/splatnet3.ts +++ b/src/splatnet3.ts @@ -66,6 +66,14 @@ async function request( return json.data; } +export const isTokenExpired = (e: unknown) => { + if (e instanceof APIError) { + return e.response.status === 401; + } else { + return false; + } +}; + export async function checkToken(state: State) { if ( !state.loginState?.sessionToken || !state.loginState?.bulletToken || diff --git a/src/utils.ts b/src/utils.ts index d35e438..2fe4c1a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -63,9 +63,9 @@ export function cache Promise>( }; } -export async function showError(p: Promise) { +export async function showError(p: Promise): Promise { try { - await p; + return await p; } catch (e) { if (e instanceof APIError) { console.error( @@ -116,3 +116,35 @@ export function parseVsHistoryDetailId(id: string) { export const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export type RecoverableError = { + name: string; + is: (err: unknown) => boolean; + recovery: () => Promise; + retryTimes?: number; + delayTime?: number; +}; +export async function retryRecoverableError Promise>( + f: F, + ...errors: RecoverableError[] +): Promise> { + const retryTimes: Record = Object.fromEntries( + errors.map(({ name, retryTimes }) => [name, retryTimes ?? 1]), + ); + while (true) { + try { + return await f() as PromiseReturnType; + } catch (e) { + const error = errors.find((error) => error.is(e)); + if (error) { + if (retryTimes[error.name] > 0) { + retryTimes[error.name]--; + await error.recovery(); + await delay(error.delayTime ?? 1000); + continue; + } + } + throw e; + } + } +}