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 { getBulletToken, getGToken, loginManually } from "./iksm.ts";
import { MultiProgressBar, Mutex } from "../deps.ts"; import { MultiProgressBar, Mutex } from "../deps.ts";
import { DEFAULT_STATE, FileStateBackend, State, StateBackend } from "./state.ts";
import { import {
checkToken, DEFAULT_STATE,
FileStateBackend,
State,
StateBackend,
} from "./state.ts";
import {
getBankaraBattleHistories, getBankaraBattleHistories,
getBattleDetail, getBattleDetail,
getBattleList, getBattleList,
isTokenExpired,
} from "./splatnet3.ts"; } from "./splatnet3.ts";
import { import {
BattleExporter, BattleExporter,
@ -17,7 +22,14 @@ import {
import { Cache, FileCache, MemoryCache } from "./cache.ts"; import { Cache, FileCache, MemoryCache } from "./cache.ts";
import { StatInkExporter } from "./exporters/stat.ink.ts"; import { StatInkExporter } from "./exporters/stat.ink.ts";
import { FileExporter } from "./exporters/file.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 = { export type Opts = {
profilePath: string; profilePath: string;
@ -159,9 +171,19 @@ type Progress = {
export class App { export class App {
state: State = DEFAULT_STATE; state: State = DEFAULT_STATE;
stateBackend: StateBackend; stateBackend: StateBackend;
recoveryToken: RecoverableError = {
name: "Refetch Token",
is: isTokenExpired,
recovery: async () => {
console.log("Token expired, refetch tokens.");
await this.fetchToken();
},
};
constructor(public opts: Opts) { constructor(public opts: Opts) {
this.stateBackend = opts.stateBackend ?? new FileStateBackend(opts.profilePath); this.stateBackend = opts.stateBackend ??
new FileStateBackend(opts.profilePath);
} }
async writeState(newState: State) { async writeState(newState: State) {
this.state = newState; this.state = newState;
@ -212,7 +234,10 @@ export class App {
return out; return out;
} }
async exportOnce() { exportOnce() {
return retryRecoverableError(() => this._exportOnce(), this.recoveryToken);
}
async _exportOnce() {
const bar = !this.opts.noProgress const bar = !this.opts.noProgress
? new MultiProgressBar({ ? new MultiProgressBar({
title: "Export battles", title: "Export battles",
@ -220,6 +245,7 @@ export class App {
}) })
: undefined; : undefined;
try {
const exporters = await this.getExporters(); const exporters = await this.getExporters();
const fetcher = new BattleFetcher({ const fetcher = new BattleFetcher({
@ -272,6 +298,9 @@ export class App {
.join(", ") .join(", ")
}`, }`,
); );
} finally {
bar?.end();
}
} }
async monitor() { async monitor() {
while (true) { while (true) {
@ -295,25 +324,12 @@ export class App {
} }
bar?.end(); bar?.end();
} }
async run() { async fetchToken() {
await this.readState(); const sessionToken = this.state.loginState?.sessionToken;
if (!this.state.loginState?.sessionToken) { if (!sessionToken) {
const sessionToken = await loginManually(); throw new Error("Session token is not set.");
await this.writeState({
...this.state,
loginState: {
...this.state.loginState,
sessionToken,
},
});
} }
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({ const { webServiceToken, userCountry, userLang } = await getGToken({
fApi: this.state.fGen, fApi: this.state.fGen,
@ -338,6 +354,20 @@ export class App {
userCountry: this.state.userCountry ?? userCountry, 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) { if (this.opts.monitor) {
await this.monitor(); await this.monitor();

View File

@ -1,7 +1,7 @@
import type { StatInkPostBody, VsHistoryDetail } from "./types.ts"; import type { StatInkPostBody, VsHistoryDetail } from "./types.ts";
export const AGENT_NAME = "s3si.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 NSOAPP_VERSION = "2.3.1";
export const WEB_VIEW_VERSION = "1.0.0-216d0219"; export const WEB_VIEW_VERSION = "1.0.0-216d0219";
export const S3SI_LINK = "https://github.com/spacemeowx2/s3si.ts" 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; 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) { export async function checkToken(state: State) {
if ( if (
!state.loginState?.sessionToken || !state.loginState?.bulletToken || !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 { try {
await p; return await p;
} catch (e) { } catch (e) {
if (e instanceof APIError) { if (e instanceof APIError) {
console.error( console.error(
@ -116,3 +116,35 @@ export function parseVsHistoryDetailId(id: string) {
export const delay = (ms: number) => export const delay = (ms: number) =>
new Promise<void>((resolve) => setTimeout(resolve, ms)); 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;
}
}
}