feat: refetch token when 401 close #5
parent
390b7ce279
commit
3e2eede47c
190
src/app.ts
190
src/app.ts
|
|
@ -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,58 +245,62 @@ export class App {
|
||||||
})
|
})
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const exporters = await this.getExporters();
|
try {
|
||||||
|
const exporters = await this.getExporters();
|
||||||
|
|
||||||
const fetcher = new BattleFetcher({
|
const fetcher = new BattleFetcher({
|
||||||
cache: this.opts.cache ?? new FileCache(this.state.cacheDir),
|
cache: this.opts.cache ?? new FileCache(this.state.cacheDir),
|
||||||
state: this.state,
|
state: this.state,
|
||||||
});
|
});
|
||||||
console.log("Fetching battle list...");
|
console.log("Fetching battle list...");
|
||||||
const battleList = await getBattleList(this.state);
|
const battleList = await getBattleList(this.state);
|
||||||
|
|
||||||
const allProgress: Record<string, Progress> = {};
|
const allProgress: Record<string, Progress> = {};
|
||||||
const redraw = (name: string, progress: Progress) => {
|
const redraw = (name: string, progress: Progress) => {
|
||||||
allProgress[name] = progress;
|
allProgress[name] = progress;
|
||||||
bar?.render(
|
bar?.render(
|
||||||
Object.entries(allProgress).map(([name, progress]) => ({
|
Object.entries(allProgress).map(([name, progress]) => ({
|
||||||
completed: progress.current,
|
completed: progress.current,
|
||||||
total: progress.total,
|
total: progress.total,
|
||||||
text: name,
|
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(
|
await Promise.all(
|
||||||
exporters.map((e) =>
|
exporters.map((e) =>
|
||||||
showError(
|
showError(
|
||||||
this.exportBattleList({
|
this.exportBattleList({
|
||||||
fetcher,
|
fetcher,
|
||||||
exporter: e,
|
exporter: e,
|
||||||
battleList,
|
battleList,
|
||||||
onStep: (progress) => redraw(e.name, progress),
|
onStep: (progress) => redraw(e.name, progress),
|
||||||
})
|
})
|
||||||
.then((count) => {
|
.then((count) => {
|
||||||
stats[e.name] = count;
|
stats[e.name] = count;
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(`\nFailed to export to ${e.name}:`, err);
|
console.error(`\nFailed to export to ${e.name}:`, err);
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
bar?.end();
|
bar?.end();
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Exported ${
|
`Exported ${
|
||||||
Object.entries(stats)
|
Object.entries(stats)
|
||||||
.map(([name, count]) => `${name}: ${count}`)
|
.map(([name, count]) => `${name}: ${count}`)
|
||||||
.join(", ")
|
.join(", ")
|
||||||
}`,
|
}`,
|
||||||
);
|
);
|
||||||
|
} finally {
|
||||||
|
bar?.end();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
async monitor() {
|
async monitor() {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|
@ -295,6 +324,36 @@ export class App {
|
||||||
}
|
}
|
||||||
bar?.end();
|
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() {
|
async run() {
|
||||||
await this.readState();
|
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) {
|
if (this.opts.monitor) {
|
||||||
await this.monitor();
|
await this.monitor();
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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 ||
|
||||||
|
|
|
||||||
36
src/utils.ts
36
src/utils.ts
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue