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,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<string, Progress> = {};
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<string, Progress> = {};
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<string, number> = Object.fromEntries(
exporters.map((e) => [e.name, 0]),
);
};
const stats: Record<string, number> = 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();

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;
}
}
}