2022-11-17 06:19:11 -05:00
|
|
|
import { Profile } from "./state.ts";
|
2022-10-20 23:48:45 -04:00
|
|
|
import {
|
|
|
|
|
DEFAULT_APP_USER_AGENT,
|
|
|
|
|
SPLATNET3_ENDPOINT,
|
|
|
|
|
WEB_VIEW_VERSION,
|
|
|
|
|
} from "./constant.ts";
|
2022-10-20 09:45:59 -04:00
|
|
|
import { APIError } from "./APIError.ts";
|
|
|
|
|
import {
|
|
|
|
|
BattleListType,
|
|
|
|
|
GraphQLResponse,
|
|
|
|
|
Queries,
|
|
|
|
|
RespMap,
|
2022-11-26 10:01:42 -05:00
|
|
|
Summary,
|
2022-10-20 09:45:59 -04:00
|
|
|
VarsMap,
|
|
|
|
|
} from "./types.ts";
|
2022-11-17 06:19:11 -05:00
|
|
|
import { DEFAULT_ENV, Env } from "./env.ts";
|
|
|
|
|
import { getBulletToken, getGToken } from "./iksm.ts";
|
2022-11-26 10:15:40 -05:00
|
|
|
import { parseHistoryDetailId } from "./utils.ts";
|
2022-10-20 09:45:59 -04:00
|
|
|
|
2022-11-17 06:19:11 -05:00
|
|
|
export class Splatnet3 {
|
|
|
|
|
protected profile: Profile;
|
|
|
|
|
protected env: Env;
|
|
|
|
|
|
|
|
|
|
constructor({ profile, env = DEFAULT_ENV }: { profile: Profile; env?: Env }) {
|
|
|
|
|
this.profile = profile;
|
|
|
|
|
this.env = env;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected async request<Q extends Queries>(
|
|
|
|
|
query: Q,
|
|
|
|
|
...rest: VarsMap[Q]
|
|
|
|
|
): Promise<RespMap[Q]> {
|
|
|
|
|
const doRequest = async () => {
|
|
|
|
|
const state = this.profile.state;
|
|
|
|
|
const variables = rest?.[0] ?? {};
|
|
|
|
|
const body = {
|
|
|
|
|
extensions: {
|
|
|
|
|
persistedQuery: {
|
|
|
|
|
sha256Hash: query,
|
|
|
|
|
version: 1,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
variables,
|
|
|
|
|
};
|
2022-11-17 14:25:04 -05:00
|
|
|
const { post } = this.env.newFetcher();
|
|
|
|
|
const resp = await post({
|
|
|
|
|
url: SPLATNET3_ENDPOINT,
|
2022-11-17 06:19:11 -05:00
|
|
|
headers: {
|
|
|
|
|
"Authorization": `Bearer ${state.loginState?.bulletToken}`,
|
|
|
|
|
"Accept-Language": state.userLang ?? "en-US",
|
|
|
|
|
"User-Agent": state.appUserAgent ?? DEFAULT_APP_USER_AGENT,
|
|
|
|
|
"X-Web-View-Ver": WEB_VIEW_VERSION,
|
|
|
|
|
"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(${json.errors?.[0].message})`,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return json.data;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
return await doRequest();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (isTokenExpired(e)) {
|
|
|
|
|
await this.fetchToken();
|
|
|
|
|
return await doRequest();
|
|
|
|
|
}
|
|
|
|
|
throw e;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fetchToken() {
|
|
|
|
|
const state = this.profile.state;
|
|
|
|
|
const sessionToken = state.loginState?.sessionToken;
|
|
|
|
|
|
|
|
|
|
if (!sessionToken) {
|
|
|
|
|
throw new Error("Session token is not set.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { webServiceToken, userCountry, userLang } = await getGToken({
|
|
|
|
|
fApi: state.fGen,
|
|
|
|
|
sessionToken,
|
2022-11-17 14:25:04 -05:00
|
|
|
env: this.env,
|
2022-11-17 06:19:11 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const bulletToken = await getBulletToken({
|
|
|
|
|
webServiceToken,
|
|
|
|
|
userLang,
|
|
|
|
|
userCountry,
|
|
|
|
|
appUserAgent: state.appUserAgent,
|
2022-11-17 14:25:04 -05:00
|
|
|
env: this.env,
|
2022-11-17 06:19:11 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await this.profile.writeState({
|
|
|
|
|
...state,
|
|
|
|
|
loginState: {
|
|
|
|
|
...state.loginState,
|
|
|
|
|
gToken: webServiceToken,
|
|
|
|
|
bulletToken,
|
2022-10-20 09:45:59 -04:00
|
|
|
},
|
2022-11-17 06:19:11 -05:00
|
|
|
userLang: state.userLang ?? userLang,
|
|
|
|
|
userCountry: state.userCountry ?? userCountry,
|
2022-10-20 09:45:59 -04:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-17 06:19:11 -05:00
|
|
|
protected BATTLE_LIST_TYPE_MAP: Record<
|
|
|
|
|
BattleListType,
|
|
|
|
|
() => Promise<string[]>
|
|
|
|
|
> = {
|
|
|
|
|
[BattleListType.Latest]: () =>
|
|
|
|
|
this.request(Queries.LatestBattleHistoriesQuery)
|
|
|
|
|
.then((r) => getIdsFromGroups(r.latestBattleHistories)),
|
|
|
|
|
[BattleListType.Regular]: () =>
|
|
|
|
|
this.request(Queries.RegularBattleHistoriesQuery)
|
|
|
|
|
.then((r) => getIdsFromGroups(r.regularBattleHistories)),
|
|
|
|
|
[BattleListType.Bankara]: () =>
|
|
|
|
|
this.request(Queries.BankaraBattleHistoriesQuery)
|
|
|
|
|
.then((r) => getIdsFromGroups(r.bankaraBattleHistories)),
|
|
|
|
|
[BattleListType.Private]: () =>
|
|
|
|
|
this.request(Queries.PrivateBattleHistoriesQuery)
|
|
|
|
|
.then((r) => getIdsFromGroups(r.privateBattleHistories)),
|
|
|
|
|
[BattleListType.Coop]: () =>
|
|
|
|
|
this.request(Queries.CoopHistoryQuery)
|
|
|
|
|
.then((r) => getIdsFromGroups(r.coopResult)),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
async checkToken() {
|
|
|
|
|
const state = this.profile.state;
|
|
|
|
|
if (
|
|
|
|
|
!state.loginState?.sessionToken || !state.loginState?.bulletToken ||
|
|
|
|
|
!state.loginState?.gToken
|
|
|
|
|
) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2023-03-09 09:08:42 -05:00
|
|
|
await this.request(Queries.ConfigureAnalyticsQuery);
|
2022-11-17 06:19:11 -05:00
|
|
|
return true;
|
|
|
|
|
} catch (_e) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2022-10-20 09:45:59 -04:00
|
|
|
}
|
|
|
|
|
|
2022-11-17 06:19:11 -05:00
|
|
|
async getBattleList(
|
|
|
|
|
battleListType: BattleListType = BattleListType.Latest,
|
|
|
|
|
) {
|
|
|
|
|
return await this.BATTLE_LIST_TYPE_MAP[battleListType]();
|
2022-10-24 08:46:21 -04:00
|
|
|
}
|
|
|
|
|
|
2022-11-17 06:19:11 -05:00
|
|
|
getBattleDetail(
|
|
|
|
|
id: string,
|
2022-10-20 09:45:59 -04:00
|
|
|
) {
|
2022-11-17 06:19:11 -05:00
|
|
|
return this.request(
|
|
|
|
|
Queries.VsHistoryDetailQuery,
|
|
|
|
|
{
|
|
|
|
|
vsResultId: id,
|
|
|
|
|
},
|
|
|
|
|
);
|
2022-10-20 09:45:59 -04:00
|
|
|
}
|
|
|
|
|
|
2022-11-17 06:19:11 -05:00
|
|
|
getCoopDetail(
|
|
|
|
|
id: string,
|
|
|
|
|
) {
|
|
|
|
|
return this.request(
|
|
|
|
|
Queries.CoopHistoryDetailQuery,
|
|
|
|
|
{
|
|
|
|
|
coopHistoryDetailId: id,
|
|
|
|
|
},
|
|
|
|
|
);
|
2022-10-20 09:45:59 -04:00
|
|
|
}
|
|
|
|
|
|
2022-11-17 06:19:11 -05:00
|
|
|
async getBankaraBattleHistories() {
|
|
|
|
|
const resp = await this.request(Queries.BankaraBattleHistoriesQuery);
|
2022-10-20 09:45:59 -04:00
|
|
|
|
2022-11-17 06:19:11 -05:00
|
|
|
return resp;
|
|
|
|
|
}
|
2022-10-20 09:45:59 -04:00
|
|
|
|
2022-12-01 02:52:04 -05:00
|
|
|
async getXBattleHistories() {
|
|
|
|
|
return await this.request(Queries.XBattleHistoriesQuery);
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-17 06:19:11 -05:00
|
|
|
async getCoopHistories() {
|
|
|
|
|
const resp = await this.request(Queries.CoopHistoryQuery);
|
2022-10-20 22:47:56 -04:00
|
|
|
|
2022-11-17 06:19:11 -05:00
|
|
|
return resp;
|
|
|
|
|
}
|
2022-10-24 14:45:36 -04:00
|
|
|
|
2022-11-17 06:19:11 -05:00
|
|
|
async getGearPower() {
|
|
|
|
|
const resp = await this.request(
|
|
|
|
|
Queries.myOutfitCommonDataFilteringConditionQuery,
|
|
|
|
|
);
|
2022-10-27 07:38:29 -04:00
|
|
|
|
2022-11-17 06:19:11 -05:00
|
|
|
return resp;
|
|
|
|
|
}
|
2022-10-24 14:45:36 -04:00
|
|
|
|
2022-11-17 06:19:11 -05:00
|
|
|
async getLatestBattleHistoriesQuery() {
|
|
|
|
|
const resp = await this.request(
|
|
|
|
|
Queries.LatestBattleHistoriesQuery,
|
|
|
|
|
);
|
2022-10-24 14:45:36 -04:00
|
|
|
|
2022-11-17 06:19:11 -05:00
|
|
|
return resp;
|
|
|
|
|
}
|
2022-11-05 00:57:29 -04:00
|
|
|
|
2022-11-17 06:19:11 -05:00
|
|
|
async getGears() {
|
|
|
|
|
const resp = await this.request(
|
|
|
|
|
Queries.myOutfitCommonDataEquipmentsQuery,
|
|
|
|
|
);
|
2022-11-05 00:57:29 -04:00
|
|
|
|
2022-11-17 06:19:11 -05:00
|
|
|
return resp;
|
|
|
|
|
}
|
2022-11-26 10:01:42 -05:00
|
|
|
|
|
|
|
|
async getSummary(): Promise<Summary> {
|
|
|
|
|
const ConfigureAnalyticsQuery = await this.request(
|
|
|
|
|
Queries.ConfigureAnalyticsQuery,
|
|
|
|
|
);
|
|
|
|
|
const HistoryRecordQuery = await this.request(Queries.HistoryRecordQuery);
|
|
|
|
|
const CoopHistoryQuery = await this.request(Queries.CoopHistoryQuery);
|
2022-11-26 10:15:40 -05:00
|
|
|
const getFirstBattleId = async () => {
|
|
|
|
|
const latest = await this.request(Queries.LatestBattleHistoriesQuery);
|
|
|
|
|
const id = latest?.latestBattleHistories?.historyGroups?.nodes?.[0]
|
|
|
|
|
?.historyDetails?.nodes?.[0]?.id;
|
|
|
|
|
return id;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const id = CoopHistoryQuery?.coopResult?.historyGroups?.nodes?.[0]
|
|
|
|
|
?.historyDetails?.nodes?.[0]?.id ?? await getFirstBattleId();
|
|
|
|
|
if (!id) {
|
|
|
|
|
throw new Error("No battle id found");
|
|
|
|
|
}
|
|
|
|
|
const { uid } = parseHistoryDetailId(id);
|
|
|
|
|
|
2022-11-26 10:01:42 -05:00
|
|
|
return {
|
2022-11-26 10:15:40 -05:00
|
|
|
uid,
|
2022-11-26 10:01:42 -05:00
|
|
|
ConfigureAnalyticsQuery,
|
|
|
|
|
HistoryRecordQuery,
|
|
|
|
|
CoopHistoryQuery,
|
|
|
|
|
};
|
|
|
|
|
}
|
2022-11-05 00:57:29 -04:00
|
|
|
}
|
2022-11-14 14:29:31 -05:00
|
|
|
|
2022-11-17 06:19:11 -05:00
|
|
|
function getIdsFromGroups<T extends { id: string }>(
|
2022-11-25 06:07:39 -05:00
|
|
|
{ historyGroups }: {
|
|
|
|
|
historyGroups: {
|
|
|
|
|
nodes: {
|
|
|
|
|
historyDetails: {
|
|
|
|
|
nodes: T[];
|
|
|
|
|
};
|
|
|
|
|
}[];
|
|
|
|
|
};
|
|
|
|
|
},
|
2022-11-17 06:19:11 -05:00
|
|
|
) {
|
|
|
|
|
return historyGroups.nodes.flatMap((i) => i.historyDetails.nodes).map((i) =>
|
|
|
|
|
i.id
|
2022-11-14 14:29:31 -05:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-17 06:19:11 -05:00
|
|
|
export function isTokenExpired(e: unknown) {
|
|
|
|
|
if (e instanceof APIError) {
|
|
|
|
|
return e.response.status === 401;
|
|
|
|
|
} else {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2022-11-14 14:29:31 -05:00
|
|
|
}
|