Compare commits

...

11 Commits

Author SHA1 Message Date
Rosalina edda191f56
Merge remote-tracking branch 'upstream/main' into dumb-splashcat-thing 2023-06-14 22:20:48 -04:00
spacemeowx2 a67bb4814d chore: update `NSOAPP_VERSION` 2023-06-13 21:47:39 +08:00
spacemeowx2 6e5c2e05f3 chore: update README 2023-06-13 01:42:39 +08:00
spacemeowx2 40cfd13e6c feat: send Anarchy (Open) Power 2023-06-12 17:05:46 +08:00
spacemeowx2 6d044a15ae chore: bump version(0.4.3) 2023-06-06 22:40:52 +08:00
spacemeowx2 63ea9347da feat: implement auto list-method 2023-06-06 22:40:52 +08:00
spacemeowx2 a5f35c78c9 style: remove one line 2023-06-06 22:40:52 +08:00
spacemeowx2 91f528a3be feat: add fetch from all modes 2023-06-06 22:40:52 +08:00
spacemeowx2 8a96cb321c feat: add list-method opt and its query 2023-06-06 22:40:52 +08:00
spacemeowx2 0517bda98d fix: don't print token (oops) 2023-06-05 19:15:27 +08:00
spacemeowx2 cabfa8f8c0 fix: `coral_user_id` is string 2023-06-05 17:06:42 +08:00
12 changed files with 209 additions and 42 deletions

View File

@ -1,3 +1,16 @@
## 0.4.4
feat: send Anarchy (Open) Power
## 0.4.3
feat: add `list-method` option
([#73](https://github.com/spacemeowx2/s3si.ts/issues/73))
## 0.4.2
fix: `coral_user_id` is string
## 0.4.1
feat: add support for Challenges

View File

@ -20,12 +20,16 @@ Options:
--exporter <exporter>, -e Exporter list to use (default: stat.ink)
Multiple exporters can be separated by commas
(e.g. "stat.ink,file")
--list-method When set to "latest", the latest 50 matches will be obtained.
When set to "all", matches of all modes will be obtained with a maximum of 250 matches (5 modes x 50 matches).
When set to "auto", the latest 50 matches will be obtained. If 50 matches have not been uploaded yet, matches will be obtained from the list of all modes.
"auto" is the default setting.
--no-progress, -n Disable progress bar
--monitor, -m Monitor mode
--skip-mode <mode>, -s Skip mode (default: null)
("vs", "coop")
--with-summary Include summary in the output
--help Show this help message and exit`,
--help Show this help message and exit
```
3. If it's your first time running this, follow the instructions to login to

View File

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

View File

@ -4,7 +4,7 @@ import { flags } from "./deps.ts";
const parseArgs = (args: string[]) => {
const parsed = flags.parse(args, {
string: ["profilePath", "exporter", "skipMode"],
string: ["profilePath", "exporter", "skipMode", "listMethod"],
boolean: ["help", "noProgress", "monitor", "withSummary"],
alias: {
"help": "h",
@ -15,6 +15,7 @@ const parseArgs = (args: string[]) => {
"skipMode": ["s", "skip-mode"],
"withSummary": "with-summary",
"withStages": "with-stages",
"listMethod": "list-method",
},
});
return parsed;
@ -30,6 +31,10 @@ Options:
--exporter <exporter>, -e Exporter list to use (default: stat.ink)
Multiple exporters can be separated by commas
(e.g. "stat.ink,file,mongodb")
--list-method When set to "latest", the latest 50 matches will be obtained.
When set to "all", matches of all modes will be obtained with a maximum of 250 matches (5 modes x 50 matches).
When set to "auto", the latest 50 matches will be obtained. If 50 matches have not been uploaded yet, matches will be obtained from the list of all modes.
"auto" is the default setting.
--no-progress, -n Disable progress bar
--monitor, -m Monitor mode
--skip-mode <mode>, -s Skip mode (default: null)

View File

@ -5,7 +5,7 @@ import {
HistoryGroups,
RankParam,
} from "./types.ts";
import { gameId, parseHistoryDetailId } from "./utils.ts";
import { battleTime, gameId } from "./utils.ts";
import { getSeason } from "./VersionData.ts";
const splusParams = () => {
@ -193,17 +193,6 @@ function addRank(
};
}
const battleTime = (id: string) => {
const { timestamp } = parseHistoryDetailId(id);
const dateStr = timestamp.replace(
/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})/,
"$1-$2-$3T$4:$5:$6Z",
);
return new Date(dateStr);
};
type FlattenItem = {
id: string;
gameId: string;

View File

@ -1,8 +1,8 @@
import { loginManually } from "./iksm.ts";
import { MultiProgressBar } from "../deps.ts";
import { MultiProgressBar, Mutex } from "../deps.ts";
import { FileStateBackend, Profile, StateBackend } from "./state.ts";
import { Splatnet3 } from "./splatnet3.ts";
import { BattleListType, Game, GameExporter } from "./types.ts";
import { BattleListType, Game, GameExporter, ListMethod } from "./types.ts";
import { Cache, FileCache } from "./cache.ts";
import { StatInkExporter } from "./exporters/stat.ink.ts";
import { FileExporter } from "./exporters/file.ts";
@ -20,6 +20,7 @@ export type Opts = {
withSummary: boolean;
withStages: boolean;
skipMode?: string;
listMethod?: string;
cache?: Cache;
stateBackend?: StateBackend;
env: Env;
@ -32,6 +33,7 @@ export const DEFAULT_OPTS: Opts = {
monitor: false,
withSummary: false,
withStages: true,
listMethod: "latest",
env: DEFAULT_ENV,
};
@ -56,6 +58,103 @@ class StepProgress {
}
}
interface GameListFetcher {
/**
* Return not exported game list.
* [0] is the latest game.
* @param exporter GameExporter
*/
fetch(exporter: GameExporter): Promise<string[]>;
}
class BattleListFetcher implements GameListFetcher {
protected listMethod: ListMethod;
protected allBattleList?: string[];
protected latestBattleList?: string[];
protected allLock = new Mutex();
protected latestLock = new Mutex();
constructor(
listMethod: string,
protected splatnet: Splatnet3,
) {
if (listMethod === "all") {
this.listMethod = "all";
} else if (listMethod === "latest") {
this.listMethod = "latest";
} else {
this.listMethod = "auto";
}
}
protected getAllBattleList() {
return this.allLock.use(async () => {
if (!this.allBattleList) {
this.allBattleList = await this.splatnet.getAllBattleList();
}
return this.allBattleList;
});
}
protected getLatestBattleList() {
return this.latestLock.use(async () => {
if (!this.latestBattleList) {
this.latestBattleList = await this.splatnet.getBattleList();
}
return this.latestBattleList;
});
}
private async innerFetch(exporter: GameExporter) {
if (this.listMethod === "latest") {
return await exporter.notExported({
type: "VsInfo",
list: await this.getLatestBattleList(),
});
}
if (this.listMethod === "all") {
return await exporter.notExported({
type: "VsInfo",
list: await this.getAllBattleList(),
});
}
if (this.listMethod === "auto") {
const latestList = await exporter.notExported({
type: "VsInfo",
list: await this.getLatestBattleList(),
});
if (latestList.length === 50) {
return await exporter.notExported({
type: "VsInfo",
list: await this.getAllBattleList(),
});
}
return latestList;
}
throw new TypeError(`Unknown listMethod: ${this.listMethod}`);
}
async fetch(exporter: GameExporter) {
return [...await this.innerFetch(exporter)].reverse();
}
}
class CoopListFetcher implements GameListFetcher {
constructor(
protected splatnet: Splatnet3,
) {}
async fetch(exporter: GameExporter) {
return [
...await exporter.notExported({
type: "CoopInfo",
list: await this.splatnet.getBattleList(BattleListType.Coop),
}),
].reverse();
}
}
function progress({ total, currentUrl, done }: StepProgress): Progress {
return {
total,
@ -76,6 +175,12 @@ export class App {
env: opts.env,
});
this.env = opts.env;
if (
opts.listMethod && !["all", "auto", "latest"].includes(opts.listMethod)
) {
throw new TypeError(`Unknown listMethod: ${opts.listMethod}`);
}
}
getSkipMode(): ("vs" | "coop")[] {
@ -193,8 +298,10 @@ export class App {
if (skipMode.includes("vs") || exporters.length === 0) {
this.env.logger.log("Skip exporting VS games.");
} else {
this.env.logger.log("Fetching battle list...");
const gameList = await splatnet.getBattleList();
const gameListFetcher = new BattleListFetcher(
this.opts.listMethod ?? "auto",
splatnet,
);
const { redraw, endBar } = this.exporterProgress("Export vs games");
const fetcher = new GameFetcher({
@ -213,7 +320,7 @@ export class App {
type: "VsInfo",
fetcher,
exporter: e,
gameList,
gameListFetcher,
stepProgress: stats[e.name],
onStep: () => {
redraw(e.name, progress(stats[e.name]));
@ -247,10 +354,7 @@ export class App {
if (skipMode.includes("coop") || exporters.length === 0) {
this.env.logger.log("Skip exporting coop games.");
} else {
this.env.logger.log("Fetching coop battle list...");
const coopBattleList = await splatnet.getBattleList(
BattleListType.Coop,
);
const gameListFetcher = new CoopListFetcher(splatnet);
const { redraw, endBar } = this.exporterProgress("Export coop games");
const fetcher = new GameFetcher({
@ -267,7 +371,7 @@ export class App {
type: "CoopInfo",
fetcher,
exporter: e,
gameList: coopBattleList,
gameListFetcher,
stepProgress: stats[e.name],
onStep: () => {
redraw(e.name, progress(stats[e.name]));
@ -401,30 +505,24 @@ export class App {
* @param gameList ID list of games, sorted by date, newest first
* @param onStep Callback function called when a game is exported
*/
async exportGameList({
private async exportGameList({
type,
fetcher,
exporter,
gameList,
gameListFetcher,
stepProgress,
onStep,
}: {
type: Game["type"];
exporter: GameExporter;
fetcher: GameFetcher;
gameList: string[];
gameListFetcher: GameListFetcher;
stepProgress: StepProgress;
onStep: () => void;
}): Promise<StepProgress> {
onStep?.();
const workQueue = [
...await exporter.notExported({
type,
list: gameList,
}),
]
.reverse();
const workQueue = await gameListFetcher.fetch(exporter);
const step = async (id: string) => {
const detail = await fetcher.fetch(type, id);

View File

@ -2,9 +2,9 @@ import type { StatInkPostBody, VsHistoryDetail } from "./types.ts";
export const AGENT_NAME = "splashcat / s3si.ts";
export const AGENT_VERSION = "1.1.1";
export const S3SI_VERSION = "0.4.1";
export const S3SI_VERSION = "0.4.4";
export const COMBINED_VERSION = `${AGENT_VERSION}/${S3SI_VERSION}`;
export const NSOAPP_VERSION = "2.5.1";
export const NSOAPP_VERSION = "2.5.2";
export const WEB_VIEW_VERSION = "4.0.0-d5178440";
export enum Queries {
HomeQuery = "7dcc64ea27a08e70919893a0d3f70871",
@ -12,6 +12,7 @@ export enum Queries {
RegularBattleHistoriesQuery = "3baef04b095ad8975ea679d722bc17de",
BankaraBattleHistoriesQuery = "0438ea6978ae8bd77c5d1250f4f84803",
XBattleHistoriesQuery = "6796e3cd5dc3ebd51864dc709d899fc5",
EventBattleHistoriesQuery = "9744fcf676441873c7c8a51285b6aa4d",
PrivateBattleHistoriesQuery = "8e5ae78b194264a6c230e262d069bd28",
VsHistoryDetailQuery = "9ee0099fbe3d8db2a838a75cf42856dd",
CoopHistoryQuery = "91b917becd2fa415890f5b47e15ffb15",

View File

@ -588,6 +588,8 @@ export class StatInkExporter implements GameExporter {
}
}
result.bankara_power_after = vsDetail.bankaraMatch?.bankaraPower?.power;
if (rankBeforeState && rankState) {
result.rank_before_exp = rankBeforeState.rankPoint;
result.rank_after_exp = rankState.rankPoint;

View File

@ -213,20 +213,20 @@ export async function getGToken(
const idToken2: string = respJson?.result?.webApiServerCredential
?.accessToken;
const coralUserId: number = respJson?.result?.user?.id;
const coralUserId: string = respJson?.result?.user?.id?.toString();
if (!idToken2 || !coralUserId) {
throw new APIError({
response: resp,
json: respJson,
message:
`No idToken2 or coralUserId found. Please try again later. ('${idToken2}', '${coralUserId}')`,
`No idToken2 or coralUserId found. Please try again later. (${idToken2.length}, ${coralUserId.length})`,
});
}
return [idToken2, coralUserId] as const;
};
const getGToken = async (idToken: string, coralUserId: number) => {
const getGToken = async (idToken: string, coralUserId: string) => {
const { f, request_id: requestId, timestamp } = await callImink({
step: 2,
idToken,
@ -414,7 +414,7 @@ async function callImink(
step: number;
idToken: string;
userId: string;
coralUserId?: number;
coralUserId?: string;
env: Env;
},
): Promise<IminkResponse> {

View File

@ -15,7 +15,7 @@ import {
} from "./types.ts";
import { DEFAULT_ENV, Env } from "./env.ts";
import { getBulletToken, getGToken } from "./iksm.ts";
import { parseHistoryDetailId } from "./utils.ts";
import { battleTime, parseHistoryDetailId } from "./utils.ts";
export class Splatnet3 {
protected profile: Profile;
@ -137,6 +137,12 @@ export class Splatnet3 {
[BattleListType.Bankara]: () =>
this.request(Queries.BankaraBattleHistoriesQuery)
.then((r) => getIdsFromGroups(r.bankaraBattleHistories)),
[BattleListType.XBattle]: () =>
this.request(Queries.XBattleHistoriesQuery)
.then((r) => getIdsFromGroups(r.xBattleHistories)),
[BattleListType.Event]: () =>
this.request(Queries.EventBattleHistoriesQuery)
.then((r) => getIdsFromGroups(r.eventBattleHistories)),
[BattleListType.Private]: () =>
this.request(Queries.PrivateBattleHistoriesQuery)
.then((r) => getIdsFromGroups(r.privateBattleHistories)),
@ -168,6 +174,29 @@ export class Splatnet3 {
return await this.BATTLE_LIST_TYPE_MAP[battleListType]();
}
// Get all id from all battle list, sort by time, [0] is the latest
async getAllBattleList() {
const ALL_TYPE: BattleListType[] = [
BattleListType.Regular,
BattleListType.Bankara,
BattleListType.XBattle,
BattleListType.Event,
BattleListType.Private,
];
const ids: string[] = [];
for (const type of ALL_TYPE) {
ids.push(...await this.getBattleList(type));
}
const timeMap = new Map<string, Date>(
ids.map((id) => [id, battleTime(id)] as const),
);
return ids.sort((a, b) =>
timeMap.get(b)!.getTime() - timeMap.get(a)!.getTime()
);
}
getBattleDetail(
id: string,
) {

View File

@ -9,6 +9,7 @@ export type VarsMap = {
[Queries.RegularBattleHistoriesQuery]: [];
[Queries.BankaraBattleHistoriesQuery]: [];
[Queries.XBattleHistoriesQuery]: [];
[Queries.EventBattleHistoriesQuery]: [];
[Queries.PrivateBattleHistoriesQuery]: [];
[Queries.VsHistoryDetailQuery]: [{
vsResultId: string;
@ -244,6 +245,9 @@ export type VsHistoryDetail = {
bankaraMatch: {
earnedUdemaePoint: null | number;
mode: "OPEN" | "CHALLENGE";
bankaraPower?: null | {
power?: null | number;
};
} | null;
festMatch: {
dragonMatchType: "NORMAL" | "DECUPLE" | "DRAGON" | "DOUBLE_DRAGON";
@ -427,6 +431,11 @@ export type RespMap = {
};
[Queries.BankaraBattleHistoriesQuery]: BankaraBattleHistories;
[Queries.XBattleHistoriesQuery]: XBattleHistories;
[Queries.EventBattleHistoriesQuery]: {
eventBattleHistories: {
historyGroups: HistoryGroups<BattleListNode>;
};
};
[Queries.PrivateBattleHistoriesQuery]: {
privateBattleHistories: {
historyGroups: HistoryGroups<BattleListNode>;
@ -614,10 +623,14 @@ export enum BattleListType {
Latest,
Regular,
Bankara,
Event,
XBattle,
Private,
Coop,
}
export type ListMethod = "latest" | "all" | "auto";
export type StatInkUuidList = {
status: number;
code: number;
@ -822,6 +835,8 @@ export type StatInkPostBody = {
challenge_lose?: number;
x_power_before?: number | null;
x_power_after?: number | null;
bankara_power_before?: number | null;
bankara_power_after?: number | null;
fest_power?: number; // Splatfest Power (Pro)
fest_dragon?:
| "10x"

View File

@ -188,3 +188,14 @@ export function urlSimplify(url: string): { pathname: string } | string {
return url;
}
}
export const battleTime = (id: string) => {
const { timestamp } = parseHistoryDetailId(id);
const dateStr = timestamp.replace(
/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})/,
"$1-$2-$3T$4:$5:$6Z",
);
return new Date(dateStr);
};