mirror of
https://github.com/samuelthomas2774/nxapi.git
synced 2026-03-21 18:04:10 -05:00
Add a command to monitor SplatNet 3 records
This commit is contained in:
parent
a33a36c4f5
commit
8f6ae71061
14
package-lock.json
generated
14
package-lock.json
generated
|
|
@ -22,7 +22,7 @@
|
|||
"node-notifier": "^10.0.1",
|
||||
"node-persist": "^3.1.0",
|
||||
"read": "^1.0.7",
|
||||
"splatnet3-types": "^0.1.20221021222422",
|
||||
"splatnet3-types": "^0.1.20221026095800",
|
||||
"supports-color": "^8.1.1",
|
||||
"tslib": "^2.4.0",
|
||||
"uuid": "^8.3.2",
|
||||
|
|
@ -4363,9 +4363,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/splatnet3-types": {
|
||||
"version": "0.1.20221021222422",
|
||||
"resolved": "https://registry.npmjs.org/splatnet3-types/-/splatnet3-types-0.1.20221021222422.tgz",
|
||||
"integrity": "sha512-eAzmc5x4/tVeFGXs0yASlw/fCte4YTV/BmeimkJvH83Y38mOV7J9fghcaKJyDMMvRTcp0hVr3v3feOCzh90o+g=="
|
||||
"version": "0.1.20221026095800",
|
||||
"resolved": "https://registry.npmjs.org/splatnet3-types/-/splatnet3-types-0.1.20221026095800.tgz",
|
||||
"integrity": "sha512-2PEFKno7ZYfMqLxZbEU9UCRZV9YV+a3j9P8otn189qPHlURfwTQC5uoQ5s5x2+IEkacYmo2P9ex+RZWzy1q6kA=="
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.1.2",
|
||||
|
|
@ -8394,9 +8394,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"splatnet3-types": {
|
||||
"version": "0.1.20221021222422",
|
||||
"resolved": "https://registry.npmjs.org/splatnet3-types/-/splatnet3-types-0.1.20221021222422.tgz",
|
||||
"integrity": "sha512-eAzmc5x4/tVeFGXs0yASlw/fCte4YTV/BmeimkJvH83Y38mOV7J9fghcaKJyDMMvRTcp0hVr3v3feOCzh90o+g=="
|
||||
"version": "0.1.20221026095800",
|
||||
"resolved": "https://registry.npmjs.org/splatnet3-types/-/splatnet3-types-0.1.20221026095800.tgz",
|
||||
"integrity": "sha512-2PEFKno7ZYfMqLxZbEU9UCRZV9YV+a3j9P8otn189qPHlURfwTQC5uoQ5s5x2+IEkacYmo2P9ex+RZWzy1q6kA=="
|
||||
},
|
||||
"sprintf-js": {
|
||||
"version": "1.1.2",
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@
|
|||
"node-notifier": "^10.0.1",
|
||||
"node-persist": "^3.1.0",
|
||||
"read": "^1.0.7",
|
||||
"splatnet3-types": "^0.1.20221021222422",
|
||||
"splatnet3-types": "^0.1.20221026095800",
|
||||
"supports-color": "^8.1.1",
|
||||
"tslib": "^2.4.0",
|
||||
"uuid": "^8.3.2",
|
||||
|
|
|
|||
|
|
@ -266,30 +266,54 @@ export default class SplatNet3Api {
|
|||
|
||||
/** / -> /challenge -> /challenge/{id} */
|
||||
async getChallengeJourney(id: string) {
|
||||
return this.persistedQuery(RequestId.JourneyQuery, {
|
||||
const result = await this.persistedQuery(RequestId.JourneyQuery, {
|
||||
id,
|
||||
});
|
||||
|
||||
if (!result.data.journey) {
|
||||
throw new ErrorResponse('[splatnet3] Journey not found', result[ResponseSymbol], result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** / -> /challenge -> /challenge/{id} -> pull-to-refresh */
|
||||
async getChallengeJourneyRefetch(id: string) {
|
||||
return this.persistedQuery(RequestId.JourneyRefetchQuery, {
|
||||
const result = await this.persistedQuery(RequestId.JourneyRefetchQuery, {
|
||||
id,
|
||||
});
|
||||
|
||||
if (!result.data.journey) {
|
||||
throw new ErrorResponse('[splatnet3] Journey not found', result[ResponseSymbol], result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** / -> /challenge -> /challenge/{id} -> /challenge/{id}/*s */
|
||||
async getChallengeJourneyChallenges(id: string) {
|
||||
return this.persistedQuery(RequestId.JourneyChallengeDetailQuery, {
|
||||
const result = await this.persistedQuery(RequestId.JourneyChallengeDetailQuery, {
|
||||
journeyId: id,
|
||||
});
|
||||
|
||||
if (!result.data.journey) {
|
||||
throw new ErrorResponse('[splatnet3] Journey not found', result[ResponseSymbol], result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** / -> /challenge -> /challenge/{id} -> /challenge/{id}/* -> pull-to-refresh */
|
||||
async getChallengeJourneyChallengesRefetch(id: string) {
|
||||
return this.persistedQuery(RequestId.JourneyChallengeDetailRefetchQuery, {
|
||||
const result = await this.persistedQuery(RequestId.JourneyChallengeDetailRefetchQuery, {
|
||||
journeyId: id,
|
||||
});
|
||||
|
||||
if (!result.data.journey) {
|
||||
throw new ErrorResponse('[splatnet3] Journey not found', result[ResponseSymbol], result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** / -> /challenge -> /challenge/{id} -> /challenge/{id}/* -> support */
|
||||
|
|
@ -315,30 +339,54 @@ export default class SplatNet3Api {
|
|||
|
||||
/** / -> /fest_record/{id} */
|
||||
async getFestDetail(id: string) {
|
||||
return this.persistedQuery(RequestId.DetailFestRecordDetailQuery, {
|
||||
const result = await this.persistedQuery(RequestId.DetailFestRecordDetailQuery, {
|
||||
festId: id,
|
||||
});
|
||||
|
||||
if (!result.data.fest) {
|
||||
throw new ErrorResponse('[splatnet3] Fest not found', result[ResponseSymbol], result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** / -> /fest_record -> /fest_record/{id} -> pull-to-refresh */
|
||||
async getFestDetailRefetch(id: string) {
|
||||
return this.persistedQuery(RequestId.DetailFestRefethQuery, {
|
||||
const result = await this.persistedQuery(RequestId.DetailFestRefethQuery, {
|
||||
festId: id,
|
||||
});
|
||||
|
||||
if (!result.data.fest) {
|
||||
throw new ErrorResponse('[splatnet3] Fest not found', result[ResponseSymbol], result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** / -> /fest_record -> /fest_record/{id} - not closed -> /fest_record/voting_status/{id} */
|
||||
async getFestVotingStatus(id: string) {
|
||||
return this.persistedQuery(RequestId.DetailVotingStatusQuery, {
|
||||
const result = await this.persistedQuery(RequestId.DetailVotingStatusQuery, {
|
||||
festId: id,
|
||||
});
|
||||
|
||||
if (!result.data.fest) {
|
||||
throw new ErrorResponse('[splatnet3] Fest not found', result[ResponseSymbol], result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** / -> /fest_record -> /fest_record/{id} - not closed -> /fest_record/voting_status/{id} -> pull-to-refresh */
|
||||
async getFestVotingStatusRefetch(id: string) {
|
||||
return this.persistedQuery(RequestId.DetailFestVotingStatusRefethQuery, {
|
||||
const result = await this.persistedQuery(RequestId.DetailFestVotingStatusRefethQuery, {
|
||||
festId: id,
|
||||
});
|
||||
|
||||
if (!result.data.fest) {
|
||||
throw new ErrorResponse('[splatnet3] Fest not found', result[ResponseSymbol], result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** / -> /fest_record -> /fest_record/{id} - not closed -> /fest_record/voting_status/{id} - not voted in game */
|
||||
|
|
@ -350,9 +398,15 @@ export default class SplatNet3Api {
|
|||
|
||||
/** / -> /fest_record -> /fest_record/{id} - closed -> /fest_record/ranking/{id} */
|
||||
async getFestRanking(id: string) {
|
||||
return this.persistedQuery(RequestId.DetailRankingQuery, {
|
||||
const result = await this.persistedQuery(RequestId.DetailRankingQuery, {
|
||||
festId: id,
|
||||
});
|
||||
|
||||
if (!result.data.fest) {
|
||||
throw new ErrorResponse('[splatnet3] Fest not found', result[ResponseSymbol], result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
//
|
||||
|
|
@ -371,9 +425,15 @@ export default class SplatNet3Api {
|
|||
|
||||
/** / -> /gesotown -> /gesotown/{id} */
|
||||
async getSaleGearDetail(id: string) {
|
||||
return this.persistedQuery(RequestId.SaleGearDetailQuery, {
|
||||
const result = await this.persistedQuery(RequestId.SaleGearDetailQuery, {
|
||||
saleGearId: id,
|
||||
});
|
||||
|
||||
if (!result.data.saleGear) {
|
||||
throw new ErrorResponse('[splatnet3] Sale gear not found', result[ResponseSymbol], result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** / -> /gesotown -> /gesotown/{id} -> order */
|
||||
|
|
@ -402,18 +462,24 @@ export default class SplatNet3Api {
|
|||
|
||||
/** / -> /my_outfits -> /my_outfits/{id} */
|
||||
async getMyOutfitDetail(id: string) {
|
||||
return this.persistedQuery(RequestId.MyOutfitDetailQuery, {
|
||||
const result = await this.persistedQuery(RequestId.MyOutfitDetailQuery, {
|
||||
myOutfitId: id,
|
||||
});
|
||||
|
||||
if (!result.data.myOutfit) {
|
||||
throw new ErrorResponse('[splatnet3] My outfit not found', result[ResponseSymbol], result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** / -> /my_outfits -> /my_outfits/{id / create} */
|
||||
async getEquipmentFilters(id: string) {
|
||||
async getEquipmentFilters() {
|
||||
return this.persistedQuery(RequestId.MyOutfitCommonDataFilteringConditionQuery, {});
|
||||
}
|
||||
|
||||
/** / -> /my_outfits -> /my_outfits/{id / create} */
|
||||
async getEquipment(id: string) {
|
||||
async getEquipment() {
|
||||
return this.persistedQuery(RequestId.MyOutfitCommonDataEquipmentsQuery, {});
|
||||
}
|
||||
|
||||
|
|
@ -459,9 +525,15 @@ export default class SplatNet3Api {
|
|||
async getReplaySearchResult(code: string) {
|
||||
if (!REPLAY_CODE_REGEX.test(code)) throw new Error('Invalid replay code');
|
||||
|
||||
return this.persistedQuery(RequestId.DownloadSearchReplayQuery, {
|
||||
const result = await this.persistedQuery(RequestId.DownloadSearchReplayQuery, {
|
||||
code,
|
||||
});
|
||||
|
||||
if (!result.data.replay) {
|
||||
throw new ErrorResponse('[splatnet3] Replay not found', result[ResponseSymbol], result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** / -> /replay -> enter code -> download */
|
||||
|
|
@ -487,33 +559,73 @@ export default class SplatNet3Api {
|
|||
return this.persistedQuery(RequestId.LatestBattleHistoriesQuery, {});
|
||||
}
|
||||
|
||||
/** / -> /history -> /history/latest -> pull-to-refresh */
|
||||
async getLatestBattleHistoriesRefetch() {
|
||||
return this.persistedQuery(RequestId.LatestBattleHistoriesRefetchQuery, {
|
||||
fetchCurrentPlayer: true,
|
||||
});
|
||||
}
|
||||
|
||||
/** / -> /history */
|
||||
async getRegularBattleHistories() {
|
||||
return this.persistedQuery(RequestId.RegularBattleHistoriesQuery, {});
|
||||
}
|
||||
|
||||
/** / -> /history -> /history/regular -> pull-to-refresh */
|
||||
async getRegularBattleHistoriesRefetch() {
|
||||
return this.persistedQuery(RequestId.RegularBattleHistoriesRefetchQuery, {
|
||||
fetchCurrentPlayer: true,
|
||||
});
|
||||
}
|
||||
|
||||
/** / -> /history */
|
||||
async getBankaraBattleHistories() {
|
||||
return this.persistedQuery(RequestId.BankaraBattleHistoriesQuery, {});
|
||||
}
|
||||
|
||||
/** / -> /history -> /history/bankara -> pull-to-refresh */
|
||||
async getBankaraBattleHistoriesRefetch() {
|
||||
return this.persistedQuery(RequestId.BankaraBattleHistoriesRefetchQuery, {
|
||||
fetchCurrentPlayer: true,
|
||||
});
|
||||
}
|
||||
|
||||
/** / -> /history */
|
||||
async getPrivateBattleHistories() {
|
||||
return this.persistedQuery(RequestId.PrivateBattleHistoriesQuery, {});
|
||||
}
|
||||
|
||||
/** / -> /history -> /history/private -> pull-to-refresh */
|
||||
async getPrivateBattleHistoriesRefetch() {
|
||||
return this.persistedQuery(RequestId.PrivateBattleHistoriesRefetchQuery, {
|
||||
fetchCurrentPlayer: true,
|
||||
});
|
||||
}
|
||||
|
||||
/** / -> /history -> /history/detail/{id} */
|
||||
async getBattleHistoryDetail(id: string) {
|
||||
return this.persistedQuery(RequestId.VsHistoryDetailQuery, {
|
||||
const result = await this.persistedQuery(RequestId.VsHistoryDetailQuery, {
|
||||
vsResultId: id,
|
||||
});
|
||||
|
||||
if (!result.data.vsHistoryDetail) {
|
||||
throw new ErrorResponse('[splatnet3] Battle history not found', result[ResponseSymbol], result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** / -> /history -> /history/detail/{id} -> pull-to-refresh */
|
||||
async getBattleHistoryDetailPagerRefetch(id: string) {
|
||||
return this.persistedQuery(RequestId.VsHistoryDetailPagerRefetchQuery, {
|
||||
const result = await this.persistedQuery(RequestId.VsHistoryDetailPagerRefetchQuery, {
|
||||
vsResultId: id,
|
||||
});
|
||||
|
||||
if (!result.data.vsHistoryDetail) {
|
||||
throw new ErrorResponse('[splatnet3] Battle history not found', result[ResponseSymbol], result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** / -> /history -> /history/detail/* -> latest */
|
||||
|
|
@ -548,16 +660,28 @@ export default class SplatNet3Api {
|
|||
|
||||
/** / -> /coop -> /coop/{id} */
|
||||
async getCoopHistoryDetail(id: string) {
|
||||
return this.persistedQuery(RequestId.CoopHistoryDetailQuery, {
|
||||
const result = await this.persistedQuery(RequestId.CoopHistoryDetailQuery, {
|
||||
coopHistoryDetailId: id,
|
||||
});
|
||||
|
||||
if (!result.data.coopHistoryDetail) {
|
||||
throw new ErrorResponse('[splatnet3] Co-op history not found', result[ResponseSymbol], result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** / -> /coop -> /coop/{id} -> pull-to-refresh */
|
||||
async getCoopHistoryDetailRefetch(id: string) {
|
||||
return this.persistedQuery(RequestId.CoopHistoryDetailRefetchQuery, {
|
||||
const result = await this.persistedQuery(RequestId.CoopHistoryDetailRefetchQuery, {
|
||||
id,
|
||||
});
|
||||
|
||||
if (!result.data.node) {
|
||||
throw new ErrorResponse('[splatnet3] Co-op history not found', result[ResponseSymbol], result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** / -> /coop -> /coop/* -> latest */
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export async function dumpAlbumPhotos(
|
|||
refresh: PhotoAlbumResult | boolean = false
|
||||
) {
|
||||
debug('Fetching photo album items');
|
||||
console.warn('Fetching photo album items');
|
||||
if (typeof refresh !== 'object') console.warn('Fetching photo album items');
|
||||
|
||||
const results = refresh ?
|
||||
await splatnet.getPhotoAlbumRefetch() :
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@ import * as path from 'node:path';
|
|||
import * as fs from 'node:fs/promises';
|
||||
import createDebug from 'debug';
|
||||
import mkdirp from 'mkdirp';
|
||||
import { RequestId } from 'splatnet3-types/splatnet3';
|
||||
import { BankaraBattleHistoriesRefetchResult, CoopHistoryResult, LatestBattleHistoriesRefetchResult, LatestBattleHistoriesResult, PrivateBattleHistoriesRefetchResult, RegularBattleHistoriesRefetchResult, RequestId } from 'splatnet3-types/splatnet3';
|
||||
import type { Arguments as ParentArguments } from '../splatnet3.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getBulletToken } from '../../common/auth/splatnet3.js';
|
||||
import SplatNet3Api from '../../api/splatnet3.js';
|
||||
import { ResponseSymbol } from '../../api/util.js';
|
||||
import { dumpCatalogRecords, dumpHistoryRecords } from './dump-records.js';
|
||||
|
||||
const debug = createDebug('cli:splatnet3:dump-results');
|
||||
|
||||
|
|
@ -31,12 +32,27 @@ export function builder(yargs: Argv<ParentArguments>) {
|
|||
}).option('coop', {
|
||||
describe: 'Include coop (Salmon Run) results',
|
||||
type: 'boolean',
|
||||
}).option('include-history', {
|
||||
describe: 'Include history records',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
}).option('include-catalog', {
|
||||
describe: 'Include catalog records',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
});
|
||||
}
|
||||
|
||||
type Arguments = YargsArguments<ReturnType<typeof builder>>;
|
||||
|
||||
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
const _all = [argv.battles, argv.coop];
|
||||
const _default = !_all.find(f => f === true);
|
||||
|
||||
if (!_all.some(f => f ?? _default)) {
|
||||
throw new Error('Enable one of --battles or --coop');
|
||||
}
|
||||
|
||||
const storage = await initStorage(argv.dataPath);
|
||||
|
||||
const usernsid = argv.user ?? await storage.getItem('SelectedUser');
|
||||
|
|
@ -48,28 +64,34 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
|||
|
||||
await mkdirp(directory);
|
||||
|
||||
const _default = typeof argv.battles !== 'boolean' && typeof argv.coop !== 'boolean';
|
||||
|
||||
if (argv.battles ?? _default) {
|
||||
await dumpResults(splatnet, directory);
|
||||
}
|
||||
if (argv.coop ?? _default) {
|
||||
await dumpCoopResults(splatnet, directory);
|
||||
}
|
||||
|
||||
if (argv.includeHistory) {
|
||||
await dumpHistoryRecords(splatnet, directory);
|
||||
}
|
||||
if (argv.includeCatalog) {
|
||||
await dumpCatalogRecords(splatnet, directory);
|
||||
}
|
||||
}
|
||||
|
||||
export async function dumpResults(
|
||||
splatnet: SplatNet3Api, directory: string
|
||||
splatnet: SplatNet3Api, directory: string,
|
||||
refresh: LatestBattleHistoriesResult | boolean = false
|
||||
) {
|
||||
debug('Fetching battle results');
|
||||
console.warn('Fetching battle results');
|
||||
|
||||
const [player, battles, battles_regular, battles_anarchy, battles_private] = await Promise.all([
|
||||
splatnet.getBattleHistoryCurrentPlayer(),
|
||||
splatnet.getLatestBattleHistories(),
|
||||
splatnet.getRegularBattleHistories(),
|
||||
splatnet.getBankaraBattleHistories(),
|
||||
splatnet.getPrivateBattleHistories(),
|
||||
refresh ? null : splatnet.getBattleHistoryCurrentPlayer(),
|
||||
refresh ? splatnet.getLatestBattleHistoriesRefetch() : splatnet.getLatestBattleHistories(),
|
||||
refresh ? splatnet.getRegularBattleHistoriesRefetch() : splatnet.getRegularBattleHistories(),
|
||||
refresh ? splatnet.getBankaraBattleHistoriesRefetch() : splatnet.getBankaraBattleHistories(),
|
||||
refresh ? splatnet.getPrivateBattleHistoriesRefetch() : splatnet.getPrivateBattleHistories(),
|
||||
]);
|
||||
|
||||
const filename = 'splatnet3-results-summary-' + Date.now() + '.json';
|
||||
|
|
@ -77,40 +99,49 @@ export async function dumpResults(
|
|||
|
||||
debug('Writing %s', filename);
|
||||
await fs.writeFile(file, JSON.stringify({
|
||||
player: {
|
||||
player: player ? {
|
||||
result: player.data.currentPlayer,
|
||||
query: RequestId.BattleHistoryCurrentPlayerQuery,
|
||||
be_version: player[ResponseSymbol].headers.get('x-be-version'),
|
||||
},
|
||||
} : undefined,
|
||||
latestBattleHistories: {
|
||||
result: battles.data.latestBattleHistories,
|
||||
fest: battles.data.currentFest,
|
||||
query: RequestId.LatestBattleHistoriesQuery,
|
||||
player: 'currentPlayer' in battles.data ?
|
||||
(battles.data as LatestBattleHistoriesRefetchResult).currentPlayer : undefined,
|
||||
query: refresh ? RequestId.LatestBattleHistoriesRefetchQuery : RequestId.LatestBattleHistoriesQuery,
|
||||
be_version: battles[ResponseSymbol].headers.get('x-be-version'),
|
||||
},
|
||||
regularBattleHistories: {
|
||||
result: battles_regular.data.regularBattleHistories,
|
||||
query: RequestId.RegularBattleHistoriesQuery,
|
||||
player: 'currentPlayer' in battles_regular.data ?
|
||||
(battles_regular.data as RegularBattleHistoriesRefetchResult).currentPlayer : undefined,
|
||||
query: refresh ? RequestId.RegularBattleHistoriesRefetchQuery : RequestId.RegularBattleHistoriesQuery,
|
||||
be_version: battles_regular[ResponseSymbol].headers.get('x-be-version'),
|
||||
},
|
||||
bankaraBattleHistories: {
|
||||
result: battles_anarchy.data.bankaraBattleHistories,
|
||||
query: RequestId.BankaraBattleHistoriesQuery,
|
||||
player: 'currentPlayer' in battles_anarchy.data ?
|
||||
(battles_anarchy.data as BankaraBattleHistoriesRefetchResult).currentPlayer : undefined,
|
||||
query: refresh ? RequestId.BankaraBattleHistoriesRefetchQuery : RequestId.BankaraBattleHistoriesQuery,
|
||||
be_version: battles_anarchy[ResponseSymbol].headers.get('x-be-version'),
|
||||
},
|
||||
privateBattleHistories: {
|
||||
result: battles_private.data.privateBattleHistories,
|
||||
query: RequestId.PrivateBattleHistoriesQuery,
|
||||
player: 'currentPlayer' in battles_private.data ?
|
||||
(battles_private.data as PrivateBattleHistoriesRefetchResult).currentPlayer : undefined,
|
||||
query: refresh ? RequestId.PrivateBattleHistoriesRefetchQuery : RequestId.PrivateBattleHistoriesQuery,
|
||||
be_version: battles_private[ResponseSymbol].headers.get('x-be-version'),
|
||||
},
|
||||
app_version: splatnet.version,
|
||||
}, null, 4) + '\n', 'utf-8');
|
||||
|
||||
const downloaded = [];
|
||||
const skipped = [];
|
||||
|
||||
// Reverse battle history order so oldest records are downloaded first
|
||||
for (const group of battles.data.latestBattleHistories.historyGroups.nodes.reverse()) {
|
||||
for (const item of group.historyDetails.nodes.reverse()) {
|
||||
for (const group of [...battles.data.latestBattleHistories.historyGroups.nodes].reverse()) {
|
||||
for (const item of [...group.historyDetails.nodes].reverse()) {
|
||||
const id_str = Buffer.from(item.id, 'base64').toString() || item.id;
|
||||
const match = id_str.match(/^VsHistoryDetail-(u-[0-9a-z]{20}):([A-Z]+):((\d{8,}T\d{6})_([0-9a-f-]{36}))$/);
|
||||
const id = match ? match[1] + '-' + match[3] : id_str;
|
||||
|
|
@ -134,6 +165,8 @@ export async function dumpResults(
|
|||
app_version: splatnet.version,
|
||||
be_version: result[ResponseSymbol].headers.get('x-be-version'),
|
||||
}, null, 4) + '\n', 'utf-8');
|
||||
|
||||
downloaded.push(item.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -142,12 +175,27 @@ export async function dumpResults(
|
|||
if (skipped.length === 1) debug('Skipped battle result %s, file already exists', skipped[0]);
|
||||
else debug('Skipped %d battle results, files already exist', skipped.length);
|
||||
}
|
||||
|
||||
return {
|
||||
player,
|
||||
battles,
|
||||
battles_regular,
|
||||
battles_anarchy,
|
||||
battles_private,
|
||||
downloaded,
|
||||
};
|
||||
}
|
||||
|
||||
export async function dumpCoopResults(splatnet: SplatNet3Api, directory: string) {
|
||||
export async function dumpCoopResults(
|
||||
splatnet: SplatNet3Api, directory: string,
|
||||
refresh: CoopHistoryResult | boolean = false
|
||||
) {
|
||||
debug('Fetching coop results');
|
||||
console.warn('Fetching coop results');
|
||||
const results = await splatnet.getCoopHistory();
|
||||
|
||||
const results = refresh ?
|
||||
await splatnet.getCoopHistoryRefetch() :
|
||||
await splatnet.getCoopHistory();
|
||||
|
||||
const filename = 'splatnet3-coop-summary-' + Date.now() + '.json';
|
||||
const file = path.join(directory, filename);
|
||||
|
|
@ -155,16 +203,17 @@ export async function dumpCoopResults(splatnet: SplatNet3Api, directory: string)
|
|||
debug('Writing %s', filename);
|
||||
await fs.writeFile(file, JSON.stringify({
|
||||
result: results.data.coopResult,
|
||||
query: RequestId.CoopHistoryQuery,
|
||||
query: refresh ? RequestId.RefetchableCoopHistory_CoopResultQuery : RequestId.CoopHistoryQuery,
|
||||
app_version: splatnet.version,
|
||||
be_version: results[ResponseSymbol].headers.get('x-be-version'),
|
||||
}, null, 4) + '\n', 'utf-8');
|
||||
|
||||
const downloaded = [];
|
||||
const skipped = [];
|
||||
|
||||
// Reverse coop history order so oldest records are downloaded first
|
||||
for (const group of results.data.coopResult.historyGroups.nodes.reverse()) {
|
||||
for (const item of group.historyDetails.nodes.reverse()) {
|
||||
for (const group of [...results.data.coopResult.historyGroups.nodes].reverse()) {
|
||||
for (const item of [...group.historyDetails.nodes].reverse()) {
|
||||
const id_str = Buffer.from(item.id, 'base64').toString() || item.id;
|
||||
const match = id_str.match(/^CoopHistoryDetail-(u-[0-9a-z]{20}):((\d{8,}T\d{6})_([0-9a-f-]{36}))$/);
|
||||
const id = match ? match[1] + '-' + match[2] : id_str;
|
||||
|
|
@ -187,6 +236,8 @@ export async function dumpCoopResults(splatnet: SplatNet3Api, directory: string)
|
|||
app_version: splatnet.version,
|
||||
be_version: result[ResponseSymbol].headers.get('x-be-version'),
|
||||
}, null, 4) + '\n', 'utf-8');
|
||||
|
||||
downloaded.push(item.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -195,4 +246,9 @@ export async function dumpCoopResults(splatnet: SplatNet3Api, directory: string)
|
|||
if (skipped.length === 1) debug('Skipped co-op result %s, file already exist', skipped[0]);
|
||||
else debug('Skipped %d co-op results, files already exist', skipped.length);
|
||||
}
|
||||
|
||||
return {
|
||||
results,
|
||||
downloaded,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,3 +9,4 @@ export * as dumpRecords from './dump-records.js';
|
|||
export * as dumpFests from './dump-fests.js';
|
||||
export * as dumpAlbum from './dump-album.js';
|
||||
export * as dumpResults from './dump-results.js';
|
||||
export * as monitor from './monitor.js';
|
||||
|
|
|
|||
233
src/cli/splatnet3/monitor.ts
Normal file
233
src/cli/splatnet3/monitor.ts
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
import * as path from 'node:path';
|
||||
import createDebug from 'debug';
|
||||
import mkdirp from 'mkdirp';
|
||||
import type { Arguments as ParentArguments } from '../splatnet3.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getBulletToken } from '../../common/auth/splatnet3.js';
|
||||
import { dumpCatalogRecords, dumpHistoryRecords, dumpStageStats, dumpWeaponStats } from './dump-records.js';
|
||||
import { dumpCoopResults, dumpResults } from './dump-results.js';
|
||||
import { dumpAlbumPhotos } from './dump-album.js';
|
||||
import SplatNet3Api from '../../api/splatnet3.js';
|
||||
|
||||
const debug = createDebug('cli:splatnet3:monitor');
|
||||
|
||||
export const command = 'monitor [directory]';
|
||||
export const desc = 'Monitor SplatNet 3 for new battle and coop results and photo album items';
|
||||
|
||||
export function builder(yargs: Argv<ParentArguments>) {
|
||||
return yargs.positional('directory', {
|
||||
describe: 'Directory to write record data to',
|
||||
type: 'string',
|
||||
}).option('user', {
|
||||
describe: 'Nintendo Account ID',
|
||||
type: 'string',
|
||||
}).option('token', {
|
||||
describe: 'Nintendo Account session token',
|
||||
type: 'string',
|
||||
}).option('battles', {
|
||||
describe: 'Include regular/ranked/private/festival battle results',
|
||||
type: 'boolean',
|
||||
}).option('coop', {
|
||||
describe: 'Include coop (Salmon Run) results',
|
||||
type: 'boolean',
|
||||
}).option('album', {
|
||||
describe: 'Include photo album items',
|
||||
type: 'boolean',
|
||||
}).option('include-history', {
|
||||
describe: 'Include history records',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
}).option('include-catalog', {
|
||||
describe: 'Include catalog records',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
}).option('include-stage', {
|
||||
describe: 'Include stage stats',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
}).option('include-weapon', {
|
||||
describe: 'Include weapon stats',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
}).option('update-interval', {
|
||||
describe: 'Update interval in seconds',
|
||||
type: 'number',
|
||||
// 15 minutes
|
||||
default: 15 * 60,
|
||||
});
|
||||
}
|
||||
|
||||
type Arguments = YargsArguments<ReturnType<typeof builder>>;
|
||||
|
||||
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
const _all = [argv.battles, argv.coop, argv.album];
|
||||
const _default = !_all.find(f => f === true);
|
||||
|
||||
if (!_all.some(f => f ?? _default)) {
|
||||
throw new Error('Enable one of --battles, --coop or --album');
|
||||
}
|
||||
|
||||
const storage = await initStorage(argv.dataPath);
|
||||
|
||||
const usernsid = argv.user ?? await storage.getItem('SelectedUser');
|
||||
const token: string = argv.token ||
|
||||
await storage.getItem('NintendoAccountToken.' + usernsid);
|
||||
const {splatnet} = await getBulletToken(storage, token, argv.zncProxyUrl, argv.autoUpdateSession);
|
||||
|
||||
const directory = argv.directory ?? path.join(argv.dataPath, 'splatnet3');
|
||||
|
||||
await mkdirp(directory);
|
||||
|
||||
let vs: (ReturnType<typeof dumpResults> extends Promise<infer T> ? T : never) | null = null;
|
||||
let coop: (ReturnType<typeof dumpCoopResults> extends Promise<infer T> ? T : never) | null = null;
|
||||
let album: (ReturnType<typeof dumpAlbumPhotos> extends Promise<infer T> ? T : never) | null = null;
|
||||
|
||||
if (argv.battles ?? _default) {
|
||||
vs = await dumpResults(splatnet, directory);
|
||||
}
|
||||
if (argv.coop ?? _default) {
|
||||
coop = await dumpCoopResults(splatnet, directory);
|
||||
}
|
||||
if (argv.album ?? _default) {
|
||||
album = await dumpAlbumPhotos(splatnet, directory);
|
||||
}
|
||||
|
||||
if (argv.includeHistory) {
|
||||
await dumpHistoryRecords(splatnet, directory);
|
||||
}
|
||||
if (argv.includeCatalog) {
|
||||
await dumpCatalogRecords(splatnet, directory);
|
||||
}
|
||||
if (argv.includeStage) {
|
||||
await dumpStageStats(splatnet, directory);
|
||||
}
|
||||
if (argv.includeWeapon) {
|
||||
await dumpWeaponStats(splatnet, directory);
|
||||
}
|
||||
|
||||
console.warn('Monitoring for new data');
|
||||
|
||||
if (vs) {
|
||||
const latest_id = vs.battles.data.latestBattleHistories.historyGroups.nodes[0].historyDetails.nodes[0].id;
|
||||
|
||||
// If we already had the latest battle result, fetch it again now to match the behavour of Nintendo's app
|
||||
if (!vs.downloaded.includes(latest_id)) {
|
||||
const id_str = Buffer.from(latest_id, 'base64').toString() || latest_id;
|
||||
const match = id_str.match(/^VsHistoryDetail-(u-[0-9a-z]{20}):([A-Z]+):((\d{8,}T\d{6})_([0-9a-f-]{36}))$/);
|
||||
const id = match ? match[1] + '-' + match[3] : id_str;
|
||||
|
||||
debug('Fetching latest battle result %s', id);
|
||||
const result = await splatnet.getBattleHistoryDetail(latest_id);
|
||||
const pager = await splatnet.getBattleHistoryDetailPagerRefetch(latest_id);
|
||||
}
|
||||
}
|
||||
|
||||
if (coop) {
|
||||
const latest_id = coop.results.data.coopResult.historyGroups.nodes[0].historyDetails.nodes[0].id;
|
||||
|
||||
// If we already had the latest coop result, fetch it again now to match the behavour of Nintendo's app
|
||||
if (!coop.downloaded.includes(latest_id)) {
|
||||
const id_str = Buffer.from(latest_id, 'base64').toString() || latest_id;
|
||||
const match = id_str.match(/^CoopHistoryDetail-(u-[0-9a-z]{20}):((\d{8,}T\d{6})_([0-9a-f-]{36}))$/);
|
||||
const id = match ? match[1] + '-' + match[2] : id_str;
|
||||
|
||||
debug('Fetching latest coop result %s', id);
|
||||
const result = await splatnet.getCoopHistoryDetail(latest_id);
|
||||
}
|
||||
}
|
||||
|
||||
let updating = false;
|
||||
let should_exit = false;
|
||||
let sleep_timeout: NodeJS.Timeout | null = null;
|
||||
let sleep_resolve: ((value: void) => void) | null = null;
|
||||
|
||||
const exit = () => {
|
||||
if (updating) {
|
||||
console.warn('Waiting for the current update to complete before exiting');
|
||||
}
|
||||
|
||||
should_exit = true;
|
||||
sleep_resolve?.call(null);
|
||||
clearTimeout(sleep_timeout!);
|
||||
process.removeListener('SIGINT' as any, exit);
|
||||
process.removeListener('SIGTERM' as any, exit);
|
||||
};
|
||||
process.on('SIGINT', exit);
|
||||
process.on('SIGTERM', exit);
|
||||
|
||||
try {
|
||||
await new Promise(rs => sleep_timeout = setTimeout(sleep_resolve = rs, argv.updateInterval * 1000));
|
||||
|
||||
while (!should_exit) {
|
||||
updating = true;
|
||||
[vs, coop, album] = await update(argv, splatnet, directory, vs, coop, album);
|
||||
updating = false;
|
||||
|
||||
if (should_exit) continue;
|
||||
await new Promise(rs => sleep_timeout = setTimeout(sleep_resolve = rs, argv.updateInterval * 1000));
|
||||
}
|
||||
} finally {
|
||||
process.removeListener('SIGINT' as any, exit);
|
||||
process.removeListener('SIGTERM' as any, exit);
|
||||
}
|
||||
}
|
||||
|
||||
async function update(
|
||||
argv: ArgumentsCamelCase<Arguments>,
|
||||
splatnet: SplatNet3Api,
|
||||
directory: string,
|
||||
vs: (ReturnType<typeof dumpResults> extends Promise<infer T> ? T : never) | null,
|
||||
coop: (ReturnType<typeof dumpCoopResults> extends Promise<infer T> ? T : never) | null,
|
||||
album: (ReturnType<typeof dumpAlbumPhotos> extends Promise<infer T> ? T : never) | null,
|
||||
) {
|
||||
debug('Checking for new data');
|
||||
|
||||
let updated_vs = false;
|
||||
let updated_coop = false;
|
||||
|
||||
if (vs) {
|
||||
const latest_id = vs.battles.data.latestBattleHistories.historyGroups.nodes[0].historyDetails.nodes[0].id;
|
||||
|
||||
const pager = await splatnet.getBattleHistoryDetailPagerRefetch(latest_id);
|
||||
|
||||
if (pager.data.vsHistoryDetail.nextHistoryDetail) {
|
||||
// New battle results available
|
||||
debug('New battle result', pager.data.vsHistoryDetail.nextHistoryDetail);
|
||||
vs = await dumpResults(splatnet, directory, vs.battles.data);
|
||||
updated_vs = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (coop) {
|
||||
const latest_id = coop.results.data.coopResult.historyGroups.nodes[0].historyDetails.nodes[0].id;
|
||||
|
||||
const pager = await splatnet.getCoopHistoryDetailRefetch(latest_id);
|
||||
|
||||
if (pager.data.node.nextHistoryDetail) {
|
||||
// New coop results available
|
||||
debug('New coop result', pager.data.node.nextHistoryDetail);
|
||||
coop = await dumpCoopResults(splatnet, directory, coop.results.data);
|
||||
updated_coop = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (album) {
|
||||
await dumpAlbumPhotos(splatnet, directory, album);
|
||||
}
|
||||
|
||||
if (argv.includeHistory && (updated_vs || updated_coop)) {
|
||||
await dumpHistoryRecords(splatnet, directory, true);
|
||||
}
|
||||
if (argv.includeCatalog && (updated_vs || updated_coop)) {
|
||||
await dumpCatalogRecords(splatnet, directory, true);
|
||||
}
|
||||
if (argv.includeStage && updated_vs) {
|
||||
await dumpStageStats(splatnet, directory, true);
|
||||
}
|
||||
if (argv.includeWeapon && updated_vs) {
|
||||
await dumpWeaponStats(splatnet, directory, true);
|
||||
}
|
||||
|
||||
return [vs, coop, album] as const;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user