Compare commits

...

11 Commits

12 changed files with 848 additions and 517 deletions

View File

@ -1,3 +1,18 @@
## 0.4.1
feat: add support for Challenges
([#72](https://github.com/spacemeowx2/s3si.ts/issues/72))
## 0.4.0
feat: update `callImink`
feat: update VersionData
## 0.3.6
feat: update `WEB_VIEW_VERSION` and query hashes for 4.0.0
## 0.3.5 ## 0.3.5
fix: wrong ability keys in some languages fix: wrong ability keys in some languages

View File

@ -11,34 +11,34 @@
"lint": "eslint --max-warnings=0 src" "lint": "eslint --max-warnings=0 src"
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "^1.2.0", "@tauri-apps/api": "^1.3.0",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"daisyui": "^2.51.3", "daisyui": "^2.52.0",
"i18next": "^22.4.10", "i18next": "^22.5.0",
"i18next-browser-languagedetector": "^7.0.1", "i18next-browser-languagedetector": "^7.0.2",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-i18next": "^12.2.0", "react-i18next": "^12.3.1",
"react-icons": "^4.8.0", "react-icons": "^4.9.0",
"react-router-dom": "^6.8.2", "react-router-dom": "^6.11.2",
"react-use": "^17.4.0" "react-use": "^17.4.0"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^1.2.3", "@tauri-apps/cli": "^1.3.1",
"@types/node": "^18.14.5", "@types/node": "^20.2.5",
"@types/react": "^18.0.15", "@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0.6",
"@vitejs/plugin-react": "^3.1.0", "@vitejs/plugin-react": "^4.0.0",
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.14",
"eslint": "^8.35.0", "eslint": "^8.41.0",
"eslint-config-react-app": "^7.0.1", "eslint-config-react-app": "^7.0.1",
"i18next-http-backend": "^2.1.1", "i18next-http-backend": "^2.2.1",
"postcss": "^8.4.21", "postcss": "^8.4.24",
"tailwindcss": "^3.2.7", "tailwindcss": "^3.3.2",
"typescript": "^4.9.5", "typescript": "^5.0.4",
"vite": "^4.1.4", "vite": "^4.3.9",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
"vite-tsconfig-paths": "^4.0.5" "vite-tsconfig-paths": "^4.2.0"
}, },
"eslintConfig": { "eslintConfig": {
"extends": "react-app" "extends": "react-app"

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@
}, },
"package": { "package": {
"productName": "s3si-ts", "productName": "s3si-ts",
"version": "0.3.5" "version": "0.4.1"
}, },
"tauri": { "tauri": {
"allowlist": { "allowlist": {

View File

@ -9,22 +9,19 @@ if (import.meta.main) {
"x86_64-apple-darwin", "x86_64-apple-darwin",
"aarch64-apple-darwin", "aarch64-apple-darwin",
]; ];
const rustInfo = new TextDecoder().decode( const rustInfo = await (new Deno.Command("rustc", {
await Deno.run({ args: ["-Vv"],
cmd: ["rustc", "-Vv"], })).output();
stdout: "piped", const target =
}).output(), /host: (\S+)/g.exec(new TextDecoder().decode(rustInfo.stdout))?.[1] ?? "?";
);
const target = /host: (\S+)/g.exec(rustInfo)?.[1] ?? "?";
if (!TARGETS.includes(target)) { if (!TARGETS.includes(target)) {
console.error(`Unsupported target: ${target}`); console.error(`Unsupported target: ${target}`);
Deno.exit(1); Deno.exit(1);
} }
const p = Deno.run({ const p = new Deno.Command("deno", {
cmd: [ args: [
"deno",
"compile", "compile",
"--target", "--target",
target, target,
@ -35,7 +32,7 @@ if (import.meta.main) {
], ],
cwd: __dirname, cwd: __dirname,
}); });
const status = await p.status(); const status = await p.output();
if (!status.success) { if (!status.success) {
console.error( console.error(
"Failed to run deno compile for target", "Failed to run deno compile for target",
@ -50,18 +47,21 @@ if (import.meta.main) {
Deno.build.os === "windows" ? ".exe" : "" Deno.build.os === "windows" ? ".exe" : ""
}`; }`;
console.log("Test the binary"); console.log("Test the binary");
const s3si = Deno.run({ const s3si = new Deno.Command(binPath, {
cmd: [binPath],
stdin: "piped", stdin: "piped",
stdout: "piped", stdout: "piped",
}); }).spawn();
await s3si.stdin?.write( const s3siWriter = s3si.stdin.getWriter();
await s3siWriter.write(
new TextEncoder().encode( new TextEncoder().encode(
'{"jsonrpc":"2.0","method":"hello","params":[],"id":1}\n', '{"jsonrpc":"2.0","method":"hello","params":[],"id":1}\n',
), ),
); );
s3si.stdin?.close();
const output = new TextDecoder().decode(await s3si.output()); const output = new TextDecoder().decode(
(await s3si.stdout.getReader().read()).value,
);
await s3siWriter.close();
assertEquals( assertEquals(
output, output,

View File

@ -30,6 +30,62 @@ function getConst(content: string, name: string): string {
return JSON.parse(match[1]); return JSON.parse(match[1]);
} }
function replaceEnum(
content: string,
name: string,
pairs: Record<string, string>,
): string {
const regex = new RegExp(`export enum ${name} {([\\s\\S^}]+?)}`);
const body = Object.entries(pairs).map(([key, value]) =>
` ${key} = "${value}"`
).join(",\n");
return content.replace(regex, `export enum ${name} {\n${body}\n}`);
}
function getEnumKeys(content: string, name: string): string[] {
const regex = new RegExp(`export enum ${name} {([\\s\\S^}]+?)}`);
const match = regex.exec(content);
if (!match) {
throw new Error(`Cannot find ${name}`);
}
const body = match[1];
// extract keys from `key = "value"`
const keys: string[] = [];
const keyRE = /\s*(\w+)\s*=/g;
while (true) {
const match = keyRE.exec(body);
if (!match) {
break;
}
keys.push(match[1]);
}
return keys;
}
function getQueryHash(js: string, query: string): string {
const regex = new RegExp(
`params:\\{id:"([^"]*?)",metadata:{},name:"${query}"`,
);
const match = regex.exec(js);
if (!match) {
throw new Error(`Cannot find ${query}`);
}
if (match[0].length > 500) {
throw new Error(`Match too large ${match[0].length}`);
}
return match[1];
}
async function printError<T>(p: Promise<T>): Promise<T | undefined> { async function printError<T>(p: Promise<T>): Promise<T | undefined> {
try { try {
return await p; return await p;
@ -39,7 +95,7 @@ async function printError<T>(p: Promise<T>): Promise<T | undefined> {
} }
} }
async function getWebViewVer(): Promise<string> { async function getMainJSBody(): 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];
@ -50,9 +106,16 @@ async function getWebViewVer(): Promise<string> {
const mainJSBody = await (await fetch(SPLATNET3_URL + mainJS)).text(); const mainJSBody = await (await fetch(SPLATNET3_URL + mainJS)).text();
const revision = /"([0-9a-f]{40})"/.exec(mainJSBody)?.[1]; return mainJSBody;
}
const mainJSBody = await getMainJSBody();
// deno-lint-ignore require-await
async function getWebViewVer(js: string): Promise<string> {
const revision = /"([0-9a-f]{40})"/.exec(js)?.[1];
const version = /revision_info_not_set.*?=("|`)(\d+\.\d+\.\d+)-/.exec( const version = /revision_info_not_set.*?=("|`)(\d+\.\d+\.\d+)-/.exec(
mainJSBody, js,
) )
?.[2]; ?.[2];
@ -83,7 +146,7 @@ const oldValues = {
}; };
const newValues: Record<string, string | undefined> = {}; const newValues: Record<string, string | undefined> = {};
newValues.WEB_VIEW_VERSION = await printError(getWebViewVer()); newValues.WEB_VIEW_VERSION = await printError(getWebViewVer(mainJSBody));
newValues.NSOAPP_VERSION = await printError(getNSOVer()); newValues.NSOAPP_VERSION = await printError(getNSOVer());
for (const [key, value] of Object.entries(newValues)) { for (const [key, value] of Object.entries(newValues)) {
@ -91,8 +154,27 @@ for (const [key, value] of Object.entries(newValues)) {
content = replaceConst(content, key, value); content = replaceConst(content, key, value);
} }
} }
await Deno.writeTextFile(CONSTANT_PATH, content);
console.log("Done"); console.log("const updated");
console.log("Old:", oldValues); console.log("Old:", oldValues);
console.log("New:", newValues); console.log("New:", newValues);
const keys = getEnumKeys(content, "Queries");
const pairs = Object.fromEntries(
keys.map((key) => [key, getQueryHash(mainJSBody, key)]),
);
content = replaceEnum(content, "Queries", pairs);
console.log("query updated");
await Deno.writeTextFile(CONSTANT_PATH, content);
const command = new Deno.Command(Deno.execPath(), {
args: ["fmt", "./src/constant.ts"],
cwd: ROOT_DIR,
stdin: "inherit",
stdout: "inherit",
});
const { code } = command.outputSync();
if (code !== 0) {
Deno.exit(code);
}

View File

@ -26,6 +26,10 @@ Deno.test("getSeason", () => {
assertEquals(season3?.id, "season202303"); assertEquals(season3?.id, "season202303");
const season4 = getSeason(new Date("2023-06-01T00:00:00+00:00"));
assertEquals(season4?.id, "season202306");
const nonExist = getSeason(new Date("2022-06-09T00:00:00+00:00")); const nonExist = getSeason(new Date("2022-06-09T00:00:00+00:00"));
assertEquals(nonExist, undefined); assertEquals(nonExist, undefined);

View File

@ -24,6 +24,12 @@ export const SEASONS: Season[] = [
start: new Date("2023-03-01T00:00:00+00:00"), start: new Date("2023-03-01T00:00:00+00:00"),
end: new Date("2023-06-01T00:00:00+00:00"), end: new Date("2023-06-01T00:00:00+00:00"),
}, },
{
id: "season202306",
name: "Sizzle Season 2023",
start: new Date("2023-06-01T00:00:00+00:00"),
end: new Date("2023-09-01T00:00:00+00:00"),
},
]; ];
export const getSeason = (date: Date): Season | undefined => { export const getSeason = (date: Date): Season | undefined => {

View File

@ -2,10 +2,26 @@ import type { StatInkPostBody, VsHistoryDetail } from "./types.ts";
export const AGENT_NAME = "splashcat / s3si.ts"; export const AGENT_NAME = "splashcat / s3si.ts";
export const AGENT_VERSION = "1.1.1"; export const AGENT_VERSION = "1.1.1";
export const S3SI_VERSION = "0.3.5"; export const S3SI_VERSION = "0.4.1";
export const COMBINED_VERSION = `${AGENT_VERSION}/${S3SI_VERSION}`; export const COMBINED_VERSION = `${AGENT_VERSION}/${S3SI_VERSION}`;
export const NSOAPP_VERSION = "2.5.1"; export const NSOAPP_VERSION = "2.5.1";
export const WEB_VIEW_VERSION = "3.0.0-0742bda0"; export const WEB_VIEW_VERSION = "4.0.0-d5178440";
export enum Queries {
HomeQuery = "7dcc64ea27a08e70919893a0d3f70871",
LatestBattleHistoriesQuery = "0d90c7576f1916469b2ae69f64292c02",
RegularBattleHistoriesQuery = "3baef04b095ad8975ea679d722bc17de",
BankaraBattleHistoriesQuery = "0438ea6978ae8bd77c5d1250f4f84803",
XBattleHistoriesQuery = "6796e3cd5dc3ebd51864dc709d899fc5",
PrivateBattleHistoriesQuery = "8e5ae78b194264a6c230e262d069bd28",
VsHistoryDetailQuery = "9ee0099fbe3d8db2a838a75cf42856dd",
CoopHistoryQuery = "91b917becd2fa415890f5b47e15ffb15",
CoopHistoryDetailQuery = "379f0d9b78b531be53044bcac031b34b",
myOutfitCommonDataFilteringConditionQuery =
"d02ab22c9dccc440076055c8baa0fa7a",
myOutfitCommonDataEquipmentsQuery = "d29cd0c2b5e6bac90dd5b817914832f8",
HistoryRecordQuery = "d9246baf077b2a29b5f7aac321810a77",
ConfigureAnalyticsQuery = "f8ae00773cc412a50dd41a6d9a159ddd",
}
export const S3SI_LINK = "https://forgejo.catgirlin.space/catgirl/s3si.ts"; export const S3SI_LINK = "https://forgejo.catgirlin.space/catgirl/s3si.ts";
export const USERAGENT = `${AGENT_NAME}/(${COMBINED_VERSION}) (${S3SI_LINK})`; export const USERAGENT = `${AGENT_NAME}/(${COMBINED_VERSION}) (${S3SI_LINK})`;

View File

@ -330,6 +330,8 @@ export class StatInkExporter implements GameExporter {
} }
} else if (vsMode === "X_MATCH") { } else if (vsMode === "X_MATCH") {
return "xmatch"; return "xmatch";
} else if (vsMode === "LEAGUE") {
return "event";
} }
throw new TypeError(`Unknown vsMode ${vsMode}`); throw new TypeError(`Unknown vsMode ${vsMode}`);
@ -421,6 +423,7 @@ export class StatInkExporter implements GameExporter {
myTeam, myTeam,
otherTeams, otherTeams,
bankaraMatch, bankaraMatch,
leagueMatch,
festMatch, festMatch,
playedTime, playedTime,
} = vsDetail; } = vsDetail;
@ -563,6 +566,10 @@ export class StatInkExporter implements GameExporter {
result.rank_after_s_plus = result.rank_before_s_plus; result.rank_after_s_plus = result.rank_before_s_plus;
} }
} }
if (leagueMatch) {
result.event = leagueMatch.leagueMatchEvent?.id;
result.event_power = leagueMatch.myLeaguePower;
}
if (challengeProgress) { if (challengeProgress) {
result.challenge_win = challengeProgress.winCount; result.challenge_win = challengeProgress.winCount;

View File

@ -175,13 +175,14 @@ export async function getGToken(
}, },
); );
const uiRespJson = await uiResp.json(); const uiRespJson = await uiResp.json();
const { nickname, birthday, language, country } = uiRespJson; const { nickname, birthday, language, country, id: userId } = uiRespJson;
const getIdToken2 = async (idToken: string) => { const getIdToken2 = async (idToken: string) => {
const { f, request_id: requestId, timestamp } = await callImink({ const { f, request_id: requestId, timestamp } = await callImink({
fApi, fApi,
step: 1, step: 1,
idToken, idToken,
userId,
env, env,
}); });
const resp = await fetch.post( const resp = await fetch.post(
@ -210,23 +211,28 @@ export async function getGToken(
); );
const respJson = await resp.json(); const respJson = await resp.json();
const idToken2 = respJson?.result?.webApiServerCredential?.accessToken; const idToken2: string = respJson?.result?.webApiServerCredential
?.accessToken;
const coralUserId: number = respJson?.result?.user?.id;
if (!idToken2) { if (!idToken2 || !coralUserId) {
throw new APIError({ throw new APIError({
response: resp, response: resp,
json: respJson, json: respJson,
message: "No idToken2 found", message:
`No idToken2 or coralUserId found. Please try again later. ('${idToken2}', '${coralUserId}')`,
}); });
} }
return idToken2 as string; return [idToken2, coralUserId] as const;
}; };
const getGToken = async (idToken: string) => { const getGToken = async (idToken: string, coralUserId: number) => {
const { f, request_id: requestId, timestamp } = await callImink({ const { f, request_id: requestId, timestamp } = await callImink({
step: 2, step: 2,
idToken, idToken,
fApi, fApi,
userId,
coralUserId,
env, env,
}); });
const resp = await fetch.post( const resp = await fetch.post(
@ -266,8 +272,8 @@ export async function getGToken(
return webServiceToken as string; return webServiceToken as string;
}; };
const idToken2 = await retry(() => getIdToken2(idToken)); const [idToken2, coralUserId] = await retry(() => getIdToken2(idToken));
const webServiceToken = await retry(() => getGToken(idToken2)); const webServiceToken = await retry(() => getGToken(idToken2, coralUserId));
return { return {
webServiceToken, webServiceToken,
@ -403,13 +409,16 @@ type IminkResponse = {
timestamp: number; timestamp: number;
}; };
async function callImink( async function callImink(
{ fApi, step, idToken, env }: { params: {
fApi: string; fApi: string;
step: number; step: number;
idToken: string; idToken: string;
userId: string;
coralUserId?: number;
env: Env; env: Env;
}, },
): Promise<IminkResponse> { ): Promise<IminkResponse> {
const { fApi, step, idToken, userId, coralUserId, env } = params;
const { post } = env.newFetcher(); const { post } = env.newFetcher();
const resp = await post({ const resp = await post({
url: fApi, url: fApi,
@ -420,6 +429,8 @@ async function callImink(
body: JSON.stringify({ body: JSON.stringify({
"token": idToken, "token": idToken,
"hash_method": step, "hash_method": step,
"na_id": userId,
"coral_user_id": coralUserId,
}), }),
}); });

View File

@ -1,23 +1,8 @@
import { splatNet3Types } from "../deps.ts"; import { splatNet3Types } from "../deps.ts";
import { RankState } from "./state.ts"; import { RankState } from "./state.ts";
import { Queries } from "./constant.ts";
export { Queries };
export enum Queries {
HomeQuery = "22e2fa8294168003c21b00c333c35384",
LatestBattleHistoriesQuery = "0176a47218d830ee447e10af4a287b3f",
RegularBattleHistoriesQuery = "3baef04b095ad8975ea679d722bc17de",
BankaraBattleHistoriesQuery = "0438ea6978ae8bd77c5d1250f4f84803",
XBattleHistoriesQuery = "6796e3cd5dc3ebd51864dc709d899fc5",
PrivateBattleHistoriesQuery = "8e5ae78b194264a6c230e262d069bd28",
VsHistoryDetailQuery = "291295ad311b99a6288fc95a5c4cb2d2",
CoopHistoryQuery = "91b917becd2fa415890f5b47e15ffb15",
CoopHistoryDetailQuery = "379f0d9b78b531be53044bcac031b34b",
myOutfitCommonDataFilteringConditionQuery =
"d02ab22c9dccc440076055c8baa0fa7a",
myOutfitCommonDataEquipmentsQuery = "d29cd0c2b5e6bac90dd5b817914832f8",
HistoryRecordQuery = "f09da9d24d888797fdfb2f060dbdf4ed",
ConfigureAnalyticsQuery = "f8ae00773cc412a50dd41a6d9a159ddd",
StageRecordQuery = "f08a932d533845dde86e674e03bbb7d3",
}
export type VarsMap = { export type VarsMap = {
[Queries.HomeQuery]: []; [Queries.HomeQuery]: [];
[Queries.LatestBattleHistoriesQuery]: []; [Queries.LatestBattleHistoriesQuery]: [];
@ -221,7 +206,13 @@ export type CoopInfo = {
}; };
}; };
export type Game = VsInfo | CoopInfo; export type Game = VsInfo | CoopInfo;
export type VsMode = "REGULAR" | "BANKARA" | "PRIVATE" | "FEST" | "X_MATCH"; export type VsMode =
| "REGULAR"
| "BANKARA"
| "PRIVATE"
| "FEST"
| "X_MATCH"
| "LEAGUE";
export type VsHistoryDetail = { export type VsHistoryDetail = {
id: string; id: string;
vsRule: { vsRule: {
@ -252,6 +243,13 @@ export type VsHistoryDetail = {
contribution: number; contribution: number;
myFestPower: number | null; myFestPower: number | null;
} | null; } | null;
leagueMatch: {
leagueMatchEvent: {
"name": string;
"id": string;
} | null;
myLeaguePower: number | null;
} | null;
myTeam: VsTeam; myTeam: VsTeam;
otherTeams: VsTeam[]; otherTeams: VsTeam[];
@ -770,12 +768,13 @@ export type StatInkPostBody = {
| "xmatch" | "xmatch"
| "splatfest_challenge" | "splatfest_challenge"
| "splatfest_open" | "splatfest_open"
| "private"; | "private"
| "event";
rule: "nawabari" | "area" | "hoko" | "yagura" | "asari" | "tricolor"; rule: "nawabari" | "area" | "hoko" | "yagura" | "asari" | "tricolor";
stage: string; stage: string;
weapon: string; weapon: string;
result: "win" | "lose" | "draw" | "exempted_lose"; result: "win" | "lose" | "draw" | "exempted_lose";
knockout?: "yes" | "no"; // for TW, set null or not sending knockout?: "yes" | "no" | null; // for TW, set null or not sending
rank_in_team: number; // position in scoreboard rank_in_team: number; // position in scoreboard
kill?: number; kill?: number;
assist?: number; assist?: number;
@ -827,6 +826,8 @@ export type StatInkPostBody = {
clout_before?: number; // Splatfest Clout, before the battle clout_before?: number; // Splatfest Clout, before the battle
clout_after?: number; // Splatfest Clout, after the battle clout_after?: number; // Splatfest Clout, after the battle
clout_change?: number; // Splatfest Clout, equals to clout_after - clout_before if you know them clout_change?: number; // Splatfest Clout, equals to clout_after - clout_before if you know them
event?: string;
event_power?: number | null;
cash_before?: number; cash_before?: number;
cash_after?: number; cash_after?: number;
our_team_players: StatInkPlayer[]; our_team_players: StatInkPlayer[];