feat: add HomeQuery type definition
parent
584f7406ee
commit
daca9cfb1c
|
|
@ -0,0 +1,14 @@
|
||||||
|
export class APIError extends Error {
|
||||||
|
response: Response;
|
||||||
|
json: unknown;
|
||||||
|
constructor(
|
||||||
|
{ response, message }: {
|
||||||
|
response: Response;
|
||||||
|
json?: unknown;
|
||||||
|
message?: string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.response = response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
export const DEFAULT_APP_USER_AGENT =
|
||||||
|
"Mozilla/5.0 (Linux; Android 11; Pixel 5) " +
|
||||||
|
"AppleWebKit/537.36 (KHTML, like Gecko) " +
|
||||||
|
"Chrome/94.0.4606.61 Mobile Safari/537.36";
|
||||||
|
export const SPLATNET3_URL = "https://api.lp1.av5ja.srv.nintendo.net";
|
||||||
|
export const SPLATNET3_ENDPOINT =
|
||||||
|
"https://api.lp1.av5ja.srv.nintendo.net/api/graphql";
|
||||||
32
iksm.ts
32
iksm.ts
|
|
@ -1,21 +1,8 @@
|
||||||
import { CookieJar, wrapFetch } from "./deps.ts";
|
import { CookieJar, wrapFetch } from "./deps.ts";
|
||||||
import { readline, retry, urlBase64Encode } from "./utils.ts";
|
import { cache, readline, retry, urlBase64Encode } from "./utils.ts";
|
||||||
import { NSOAPP_VERSION, USERAGENT } from "./version.ts";
|
import { NSOAPP_VERSION, USERAGENT } from "./version.ts";
|
||||||
|
import { DEFAULT_APP_USER_AGENT, SPLATNET3_URL } from "./constant.ts";
|
||||||
export class APIError extends Error {
|
import { APIError } from "./APIError.ts";
|
||||||
response: Response;
|
|
||||||
json: unknown;
|
|
||||||
constructor(
|
|
||||||
{ response, message }: {
|
|
||||||
response: Response;
|
|
||||||
json?: unknown;
|
|
||||||
message?: string;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
super(message);
|
|
||||||
this.response = response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loginManually(): Promise<string> {
|
export async function loginManually(): Promise<string> {
|
||||||
const cookieJar = new CookieJar();
|
const cookieJar = new CookieJar();
|
||||||
|
|
@ -240,9 +227,7 @@ export async function getGToken(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const SPLATNET3_URL = "https://api.lp1.av5ja.srv.nintendo.net";
|
async function _getWebViewVer(): Promise<string> {
|
||||||
|
|
||||||
async function getWebViewVer(): Promise<string> {
|
|
||||||
const splatnet3Home = await (await fetch(SPLATNET3_URL)).text();
|
const splatnet3Home = await (await fetch(SPLATNET3_URL)).text();
|
||||||
|
|
||||||
const mainJS = /src="(\/.*?\.js)"/.exec(splatnet3Home)?.[1];
|
const mainJS = /src="(\/.*?\.js)"/.exec(splatnet3Home)?.[1];
|
||||||
|
|
@ -261,12 +246,11 @@ async function getWebViewVer(): Promise<string> {
|
||||||
throw new Error("No version and revision found");
|
throw new Error("No version and revision found");
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${version}-${revision.substring(0, 8)}`;
|
const ver = `${version}-${revision.substring(0, 8)}`;
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_APP_USER_AGENT = "Mozilla/5.0 (Linux; Android 11; Pixel 5) " +
|
return ver;
|
||||||
"AppleWebKit/537.36 (KHTML, like Gecko) " +
|
}
|
||||||
"Chrome/94.0.4606.61 Mobile Safari/537.36";
|
export const getWebViewVer = cache(_getWebViewVer);
|
||||||
|
|
||||||
export async function getBulletToken(
|
export async function getBulletToken(
|
||||||
{
|
{
|
||||||
|
|
|
||||||
23
s3si.ts
23
s3si.ts
|
|
@ -1,6 +1,8 @@
|
||||||
import { APIError, getBulletToken, getGToken, loginManually } from "./iksm.ts";
|
import { getBulletToken, getGToken, loginManually } from "./iksm.ts";
|
||||||
|
import { APIError } from "./APIError.ts";
|
||||||
import { flags } from "./deps.ts";
|
import { flags } from "./deps.ts";
|
||||||
import { DEFAULT_STATE, State } from "./state.ts";
|
import { DEFAULT_STATE, State } from "./state.ts";
|
||||||
|
import { checkToken } from "./splatnet3.ts";
|
||||||
|
|
||||||
type Opts = {
|
type Opts = {
|
||||||
configPath: string;
|
configPath: string;
|
||||||
|
|
@ -63,7 +65,9 @@ Options:
|
||||||
}
|
}
|
||||||
const sessionToken = this.state.loginState.sessionToken!;
|
const sessionToken = this.state.loginState.sessionToken!;
|
||||||
|
|
||||||
if (!this.state.loginState?.gToken) {
|
if (
|
||||||
|
!this.state.loginState?.gToken || !this.state.loginState.bulletToken
|
||||||
|
) {
|
||||||
const { webServiceToken, userCountry, userLang } = await getGToken({
|
const { webServiceToken, userCountry, userLang } = await getGToken({
|
||||||
fApi: this.state.fGen,
|
fApi: this.state.fGen,
|
||||||
sessionToken,
|
sessionToken,
|
||||||
|
|
@ -76,14 +80,21 @@ Options:
|
||||||
appUserAgent: this.state.appUserAgent,
|
appUserAgent: this.state.appUserAgent,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.state.loginState = {
|
this.state = {
|
||||||
...this.state.loginState,
|
...this.state,
|
||||||
gToken: webServiceToken,
|
loginState: {
|
||||||
bulletToken,
|
...this.state.loginState,
|
||||||
|
gToken: webServiceToken,
|
||||||
|
bulletToken,
|
||||||
|
},
|
||||||
|
userLang,
|
||||||
|
userCountry,
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.writeState();
|
await this.writeState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await checkToken(this.state);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof APIError) {
|
if (e instanceof APIError) {
|
||||||
console.error(`APIError: ${e.message}`, e.response, e.json);
|
console.error(`APIError: ${e.message}`, e.response, e.json);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { getWebViewVer } from "./iksm.ts";
|
||||||
|
import { State } from "./state.ts";
|
||||||
|
import { DEFAULT_APP_USER_AGENT, SPLATNET3_ENDPOINT } from "./constant.ts";
|
||||||
|
import { APIError } from "./APIError.ts";
|
||||||
|
|
||||||
|
enum Queries {
|
||||||
|
HomeQuery = "dba47124d5ec3090c97ba17db5d2f4b3",
|
||||||
|
LatestBattleHistoriesQuery = "7d8b560e31617e981cf7c8aa1ca13a00",
|
||||||
|
RegularBattleHistoriesQuery = "f6e7e0277e03ff14edfef3b41f70cd33",
|
||||||
|
BankaraBattleHistoriesQuery = "c1553ac75de0a3ea497cdbafaa93e95b",
|
||||||
|
PrivateBattleHistoriesQuery = "38e0529de8bc77189504d26c7a14e0b8",
|
||||||
|
VsHistoryDetailQuery = "2b085984f729cd51938fc069ceef784a",
|
||||||
|
CoopHistoryQuery = "817618ce39bcf5570f52a97d73301b30",
|
||||||
|
CoopHistoryDetailQuery = "f3799a033f0a7ad4b1b396f9a3bafb1e",
|
||||||
|
}
|
||||||
|
type VarsMap = {
|
||||||
|
[Queries.HomeQuery]: Record<never, never>;
|
||||||
|
[Queries.LatestBattleHistoriesQuery]: Record<never, never>;
|
||||||
|
[Queries.RegularBattleHistoriesQuery]: Record<never, never>;
|
||||||
|
[Queries.BankaraBattleHistoriesQuery]: Record<never, never>;
|
||||||
|
[Queries.PrivateBattleHistoriesQuery]: Record<never, never>;
|
||||||
|
[Queries.VsHistoryDetailQuery]: {
|
||||||
|
vsResultId: string;
|
||||||
|
};
|
||||||
|
[Queries.CoopHistoryQuery]: Record<never, never>;
|
||||||
|
[Queries.CoopHistoryDetailQuery]: {
|
||||||
|
coopHistoryDetailId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type Image = {
|
||||||
|
url: string;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
};
|
||||||
|
type RespMap = {
|
||||||
|
[Queries.HomeQuery]: {
|
||||||
|
currentPlayer: {
|
||||||
|
weapon: {
|
||||||
|
image: Image;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
banners: { image: Image; message: string; jumpTo: string }[];
|
||||||
|
friends: {
|
||||||
|
nodes: {
|
||||||
|
id: number;
|
||||||
|
nickname: string;
|
||||||
|
userIcon: Image;
|
||||||
|
}[];
|
||||||
|
totalCount: number;
|
||||||
|
};
|
||||||
|
footerMessages: unknown[];
|
||||||
|
};
|
||||||
|
[Queries.LatestBattleHistoriesQuery]: Record<never, never>;
|
||||||
|
[Queries.RegularBattleHistoriesQuery]: Record<never, never>;
|
||||||
|
[Queries.BankaraBattleHistoriesQuery]: Record<never, never>;
|
||||||
|
[Queries.PrivateBattleHistoriesQuery]: Record<never, never>;
|
||||||
|
[Queries.VsHistoryDetailQuery]: Record<never, never>;
|
||||||
|
[Queries.CoopHistoryQuery]: Record<never, never>;
|
||||||
|
[Queries.CoopHistoryDetailQuery]: Record<never, never>;
|
||||||
|
};
|
||||||
|
type GraphQLResponse<T> = {
|
||||||
|
data: T;
|
||||||
|
} | {
|
||||||
|
errors: unknown[];
|
||||||
|
};
|
||||||
|
|
||||||
|
async function request<Q extends Queries>(
|
||||||
|
state: State,
|
||||||
|
query: Q,
|
||||||
|
variables: VarsMap[Q],
|
||||||
|
): Promise<RespMap[Q]> {
|
||||||
|
const body = {
|
||||||
|
extensions: {
|
||||||
|
persistedQuery: {
|
||||||
|
sha256Hash: query,
|
||||||
|
version: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
variables,
|
||||||
|
};
|
||||||
|
const resp = await fetch(SPLATNET3_ENDPOINT, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${state.loginState?.bulletToken}`,
|
||||||
|
"Accept-Language": state.userLang ?? "en-US",
|
||||||
|
"User-Agent": state.appUserAgent ?? DEFAULT_APP_USER_AGENT,
|
||||||
|
"X-Web-View-Ver": await getWebViewVer(),
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "*/*",
|
||||||
|
"Origin": "https://api.lp1.av5ja.srv.nintendo.net",
|
||||||
|
"X-Requested-With": "com.nintendo.znca",
|
||||||
|
"Referer":
|
||||||
|
`https://api.lp1.av5ja.srv.nintendo.net/?lang=${state.userLang}&na_country=${state.userCountry}&na_lang=${state.userLang}`,
|
||||||
|
"Accept-Encoding": "gzip, deflate",
|
||||||
|
"Cookie": `_gtoken: ${state.loginState?.gToken}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
if (resp.status !== 200) {
|
||||||
|
throw new APIError({
|
||||||
|
response: resp,
|
||||||
|
message: "Splatnet3 request failed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const json: GraphQLResponse<RespMap[Q]> = await resp.json();
|
||||||
|
if ("errors" in json) {
|
||||||
|
throw new APIError({
|
||||||
|
response: resp,
|
||||||
|
json,
|
||||||
|
message: "Splatnet3 request failed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return json.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkToken(state: State) {
|
||||||
|
if (
|
||||||
|
!state.loginState?.sessionToken || !state.loginState?.bulletToken ||
|
||||||
|
!state.loginState?.gToken
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await request(state, Queries.HomeQuery, {});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
2
state.ts
2
state.ts
|
|
@ -7,6 +7,8 @@ export type State = {
|
||||||
loginState?: LoginState;
|
loginState?: LoginState;
|
||||||
fGen: string;
|
fGen: string;
|
||||||
appUserAgent?: string;
|
appUserAgent?: string;
|
||||||
|
userLang?: string;
|
||||||
|
userCountry?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_STATE: State = {
|
export const DEFAULT_STATE: State = {
|
||||||
|
|
|
||||||
19
utils.ts
19
utils.ts
|
|
@ -40,3 +40,22 @@ export async function retry<F extends () => Promise<unknown>>(
|
||||||
}
|
}
|
||||||
throw lastError;
|
throw lastError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GLOBAL_CACHE: Record<string, { ts: number; value: unknown }> = {};
|
||||||
|
export function cache<F extends () => Promise<unknown>>(
|
||||||
|
f: F,
|
||||||
|
{ key = f.name, expireIn = 3600 }: { key?: string; expireIn?: number } = {},
|
||||||
|
): () => Promise<PromiseReturnType<F>> {
|
||||||
|
return async () => {
|
||||||
|
const cached = GLOBAL_CACHE[key];
|
||||||
|
if (cached && cached.ts + expireIn * 1000 > Date.now()) {
|
||||||
|
return cached.value as PromiseReturnType<F>;
|
||||||
|
}
|
||||||
|
const value = await f();
|
||||||
|
GLOBAL_CACHE[key] = {
|
||||||
|
ts: Date.now(),
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
return value as PromiseReturnType<F>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue