feat: refetch token when 401 close #5

main
spacemeowx2 2022-10-24 20:46:21 +08:00
parent 390b7ce279
commit 3e2eede47c
4 changed files with 153 additions and 83 deletions

View File

@ -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,6 +245,7 @@ export class App {
})
: undefined;
try {
const exporters = await this.getExporters();
const fetcher = new BattleFetcher({
@ -272,6 +298,9 @@ export class App {
.join(", ")
}`,
);
} finally {
bar?.end();
}
}
async monitor() {
while (true) {
@ -295,25 +324,12 @@ export class App {
}
bar?.end();
}
async run() {
await this.readState();
async fetchToken() {
const sessionToken = this.state.loginState?.sessionToken;
if (!this.state.loginState?.sessionToken) {
const sessionToken = await loginManually();
await this.writeState({
...this.state,
loginState: {
...this.state.loginState,
sessionToken,
},
});
if (!sessionToken) {
throw new Error("Session token is not set.");
}
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,
@ -338,6 +354,20 @@ export class App {
userCountry: this.state.userCountry ?? userCountry,
});
}
async run() {
await this.readState();
if (!this.state.loginState?.sessionToken) {
const sessionToken = await loginManually();
await this.writeState({
...this.state,
loginState: {
...this.state.loginState,
sessionToken,
},
});
}
if (this.opts.monitor) {
await this.monitor();

View File

@ -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"

View File

@ -66,6 +66,14 @@ async function request<Q extends Queries>(
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 ||

View File

@ -63,9 +63,9 @@ export function cache<F extends () => Promise<unknown>>(
};
}
export async function showError(p: Promise<void>) {
export async function showError<T>(p: Promise<T>): Promise<T> {
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<void>((resolve) => setTimeout(resolve, ms));
export type RecoverableError = {
name: string;
is: (err: unknown) => boolean;
recovery: () => Promise<void>;
retryTimes?: number;
delayTime?: number;
};
export async function retryRecoverableError<F extends () => Promise<unknown>>(
f: F,
...errors: RecoverableError[]
): Promise<PromiseReturnType<F>> {
const retryTimes: Record<string, number> = Object.fromEntries(
errors.map(({ name, retryTimes }) => [name, retryTimes ?? 1]),
);
while (true) {
try {
return await f() as PromiseReturnType<F>;
} 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;
}
}
}