mirror of
https://github.com/samuelthomas2774/nxapi.git
synced 2026-10-22 01:21:33 -05:00
Add commands to download SplatNet 2 data
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
export * as users from './users.js';
|
||||
export * as nso from './nso.js';
|
||||
export * as splatnet2 from './splatnet2.js';
|
||||
export * as pctl from './pctl.js';
|
||||
export * as androidZncaApiServerFrida from './android-znca-api-server-frida.js';
|
||||
|
||||
@@ -231,7 +231,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
|
||||
app.get('/api/znc/webservice/:id/token', nsoAuth, async (req, res) => {
|
||||
try {
|
||||
const response = await req.znc!.getWebServiceToken(req.params.id, req.zncAuth!.credential.accessToken);
|
||||
const response = await req.znc!.getWebServiceToken(req.params.id);
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({
|
||||
|
||||
@@ -175,88 +175,6 @@ class ZncDiscordPresence extends ZncNotifications {
|
||||
(online && 'name' in presence.game) ||
|
||||
(this.argv.showConsoleOnline && presence?.state === PresenceState.INACTIVE);
|
||||
|
||||
if (show_presence) {
|
||||
const fc = this.argv.friendCode === '' || this.argv.friendCode === '-' ? friendcode : this.forceFriendCode;
|
||||
const discordpresence = 'name' in presence.game ?
|
||||
getDiscordPresence(presence.game, fc) :
|
||||
getInactiveDiscordPresence(fc);
|
||||
|
||||
if (this.rpc && this.rpc.id !== discordpresence.id) {
|
||||
const client = this.rpc.client;
|
||||
this.rpc = null;
|
||||
await client.destroy();
|
||||
}
|
||||
|
||||
if (!this.rpc) {
|
||||
const client = new DiscordRPC.Client({transport: 'ipc'});
|
||||
let attempts = 0;
|
||||
let connected = false;
|
||||
|
||||
while (attempts < 10) {
|
||||
if (attempts === 0) debugDiscord('RPC connecting');
|
||||
else debugDiscord('RPC connecting, attempt %d', attempts + 1);
|
||||
|
||||
try {
|
||||
await client.connect(discordpresence.id);
|
||||
debugDiscord('RPC connected');
|
||||
connected = true;
|
||||
break;
|
||||
} catch (err) {}
|
||||
|
||||
attempts++;
|
||||
await new Promise(rs => setTimeout(rs, 5000));
|
||||
}
|
||||
|
||||
if (!connected) throw new Error('Failed to connect to Discord');
|
||||
|
||||
// @ts-expect-error
|
||||
client.transport.on('close', async () => {
|
||||
if (this.rpc?.client !== client) return;
|
||||
|
||||
console.warn('[discordrpc] RPC client disconnected, attempting to reconnect');
|
||||
debugDiscord('RPC client disconnected');
|
||||
let attempts = 0;
|
||||
let connected = false;
|
||||
|
||||
while (attempts < 10) {
|
||||
if (this.rpc?.client !== client) return;
|
||||
|
||||
debugDiscord('RPC reconnecting, attempt %d', attempts + 1);
|
||||
try {
|
||||
await client.connect(discordpresence.id);
|
||||
console.warn('[discordrpc] RPC reconnected');
|
||||
debugDiscord('RPC reconnected');
|
||||
connected = true;
|
||||
break;
|
||||
} catch (err) {}
|
||||
|
||||
attempts++;
|
||||
await new Promise(rs => setTimeout(rs, 5000));
|
||||
}
|
||||
|
||||
if (!connected) throw new Error('Failed to reconnect to Discord');
|
||||
|
||||
throw new Error('Discord disconnected');
|
||||
});
|
||||
|
||||
this.rpc = {client, id: discordpresence.id};
|
||||
}
|
||||
|
||||
if (discordpresence.title) {
|
||||
if (discordpresence.title !== this.title?.id) {
|
||||
this.title = {id: discordpresence.title, since: Date.now()};
|
||||
}
|
||||
|
||||
if (discordpresence.showTimestamp) {
|
||||
discordpresence.activity.startTimestamp = this.title.since;
|
||||
}
|
||||
} else {
|
||||
this.title = null;
|
||||
}
|
||||
|
||||
this.rpc.client.setActivity(discordpresence.activity);
|
||||
}
|
||||
|
||||
if (!presence || !show_presence) {
|
||||
if (this.rpc) {
|
||||
const client = this.rpc.client;
|
||||
@@ -265,7 +183,86 @@ class ZncDiscordPresence extends ZncNotifications {
|
||||
}
|
||||
|
||||
this.title = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const fc = this.argv.friendCode === '' || this.argv.friendCode === '-' ? friendcode : this.forceFriendCode;
|
||||
const discordpresence = 'name' in presence.game ?
|
||||
getDiscordPresence(presence.game, fc) :
|
||||
getInactiveDiscordPresence(fc);
|
||||
|
||||
if (this.rpc && this.rpc.id !== discordpresence.id) {
|
||||
const client = this.rpc.client;
|
||||
this.rpc = null;
|
||||
await client.destroy();
|
||||
}
|
||||
|
||||
if (!this.rpc) {
|
||||
const client = new DiscordRPC.Client({transport: 'ipc'});
|
||||
let attempts = 0;
|
||||
let connected = false;
|
||||
|
||||
while (attempts < 10) {
|
||||
if (attempts === 0) debugDiscord('RPC connecting');
|
||||
else debugDiscord('RPC connecting, attempt %d', attempts + 1);
|
||||
|
||||
try {
|
||||
await client.connect(discordpresence.id);
|
||||
debugDiscord('RPC connected');
|
||||
connected = true;
|
||||
break;
|
||||
} catch (err) {}
|
||||
|
||||
attempts++;
|
||||
await new Promise(rs => setTimeout(rs, 5000));
|
||||
}
|
||||
|
||||
if (!connected) throw new Error('Failed to connect to Discord');
|
||||
|
||||
// @ts-expect-error
|
||||
client.transport.on('close', async () => {
|
||||
if (this.rpc?.client !== client) return;
|
||||
|
||||
debugDiscord('RPC client disconnected, attempting to reconnect');
|
||||
let attempts = 0;
|
||||
let connected = false;
|
||||
|
||||
while (attempts < 10) {
|
||||
if (this.rpc?.client !== client) return;
|
||||
|
||||
debugDiscord('RPC reconnecting, attempt %d', attempts + 1);
|
||||
try {
|
||||
await client.connect(discordpresence.id);
|
||||
debugDiscord('RPC reconnected');
|
||||
connected = true;
|
||||
break;
|
||||
} catch (err) {}
|
||||
|
||||
attempts++;
|
||||
await new Promise(rs => setTimeout(rs, 5000));
|
||||
}
|
||||
|
||||
if (!connected) throw new Error('Failed to reconnect to Discord');
|
||||
|
||||
throw new Error('Discord disconnected');
|
||||
});
|
||||
|
||||
this.rpc = {client, id: discordpresence.id};
|
||||
}
|
||||
|
||||
if (discordpresence.title) {
|
||||
if (discordpresence.title !== this.title?.id) {
|
||||
this.title = {id: discordpresence.title, since: Date.now()};
|
||||
}
|
||||
|
||||
if (discordpresence.showTimestamp) {
|
||||
discordpresence.activity.startTimestamp = this.title.since;
|
||||
}
|
||||
} else {
|
||||
this.title = null;
|
||||
}
|
||||
|
||||
this.rpc.client.setActivity(discordpresence.activity);
|
||||
}
|
||||
|
||||
async update() {
|
||||
|
||||
@@ -48,7 +48,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
throw new Error('Invalid web service');
|
||||
}
|
||||
|
||||
const webserviceToken = await nso.getWebServiceToken(argv.id, data.credential.accessToken);
|
||||
const webserviceToken = await nso.getWebServiceToken(argv.id);
|
||||
|
||||
// https://app.splatoon2.nintendo.net/?lang=en-GB&na_country=GB&na_lang=en-GB
|
||||
const url = new URL(webservice.uri);
|
||||
|
||||
@@ -43,11 +43,19 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
|
||||
const devices = await moon.getDevices();
|
||||
|
||||
for (const device of argv.device ?? devices.items.map(d => d.deviceId)) {
|
||||
console.warn('Downloading summaries for device %s', device);
|
||||
for (const id of argv.device ?? []) {
|
||||
if (!devices.items.find(d => d.deviceId === id)) {
|
||||
console.warn('Device %s does not exist or is not linked to the authenticated user');
|
||||
}
|
||||
}
|
||||
|
||||
await dumpMonthlySummariesForDevice(moon, argv.directory, '' + device);
|
||||
await dumpDailySummariesForDevice(moon, argv.directory, '' + device);
|
||||
for (const device of devices.items) {
|
||||
if (argv.device && !argv.device.includes(device.deviceId)) continue;
|
||||
|
||||
console.warn('Downloading summaries for device %s (%s)', device.label, device.deviceId);
|
||||
|
||||
await dumpMonthlySummariesForDevice(moon, argv.directory, device.deviceId);
|
||||
await dumpDailySummariesForDevice(moon, argv.directory, device.deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
27
src/cli/splatnet2.ts
Normal file
27
src/cli/splatnet2.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../cli.js';
|
||||
import { Argv, YargsArguments } from '../util.js';
|
||||
import * as commands from './splatnet2/index.js';
|
||||
|
||||
const debug = createDebug('cli:splatnet2');
|
||||
|
||||
export const command = 'splatnet2 <command>';
|
||||
export const desc = 'SplatNet 2';
|
||||
|
||||
export function builder(yargs: Argv<ParentArguments>) {
|
||||
for (const command of Object.values(commands)) {
|
||||
// @ts-expect-error
|
||||
yargs.command(command);
|
||||
}
|
||||
|
||||
return yargs.option('znc-proxy-url', {
|
||||
describe: 'URL of Nintendo Switch Online app API proxy server to use',
|
||||
type: 'string',
|
||||
}).option('auto-update-iksm-session', {
|
||||
describe: 'Automatically obtain and refresh the iksm_session cookie',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
});
|
||||
}
|
||||
|
||||
export type Arguments = YargsArguments<ReturnType<typeof builder>>;
|
||||
143
src/cli/splatnet2/dump-records.ts
Normal file
143
src/cli/splatnet2/dump-records.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import createDebug from 'debug';
|
||||
import mkdirp from 'mkdirp';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import type { Arguments as ParentArguments } from '../splatnet2.js';
|
||||
import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js';
|
||||
import { getIksmToken } from './util.js';
|
||||
import SplatNet2Api, { ShareColour } from '../../api/splatnet2.js';
|
||||
import { NicknameAndIcon } from '../../api/splatnet2-types.js';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
const debug = createDebug('cli:splatnet2:dump-records');
|
||||
|
||||
export const command = 'dump-records <directory>';
|
||||
export const desc = 'Download all other records';
|
||||
|
||||
export function builder(yargs: Argv<ParentArguments>) {
|
||||
return yargs.positional('directory', {
|
||||
describe: 'Directory to write record data to',
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
}).option('user', {
|
||||
describe: 'Nintendo Account ID',
|
||||
type: 'string',
|
||||
}).option('token', {
|
||||
describe: 'Nintendo Account session token',
|
||||
type: 'string',
|
||||
}).option('user-records', {
|
||||
describe: 'Include user records',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
}).option('profile-image', {
|
||||
describe: 'Include profile image',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
}).option('favourite-stage', {
|
||||
describe: 'Favourite stage to include on profile image',
|
||||
type: 'string',
|
||||
}).option('favourite-colour', {
|
||||
describe: 'Favourite colour to include on profile image',
|
||||
type: 'string',
|
||||
}).option('hero-records', {
|
||||
describe: 'Include hero (Octo Canyon) records',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
}).option('timeline', {
|
||||
describe: 'Include timeline records',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
});
|
||||
}
|
||||
|
||||
type Arguments = YargsArguments<ReturnType<typeof builder>>;
|
||||
|
||||
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
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 getIksmToken(storage, token, argv.zncProxyUrl, argv.autoUpdateIksmSession);
|
||||
|
||||
await mkdirp(argv.directory);
|
||||
|
||||
const [records, stages, activefestivals, timeline] = await Promise.all([
|
||||
splatnet.getRecords(),
|
||||
splatnet.getStages(),
|
||||
splatnet.getActiveFestivals(),
|
||||
splatnet.getTimeline(),
|
||||
]);
|
||||
const nickname_and_icons = await splatnet.getUserNicknameAndIcon([records.records.player.principal_id]);
|
||||
|
||||
if (argv.userRecords) {
|
||||
const filename = 'splatnet2-records-' + records.records.unique_id + '-' + Date.now() + '.json';
|
||||
const file = path.join(argv.directory, filename);
|
||||
|
||||
debug('Writing records %s', filename);
|
||||
await fs.writeFile(file, JSON.stringify(records, null, 4) + '\n', 'utf-8');
|
||||
|
||||
const ni_filename = 'splatnet2-ni-' + records.records.unique_id + '-' + Date.now() + '.json';
|
||||
const ni_file = path.join(argv.directory, ni_filename);
|
||||
|
||||
debug('Writing records %s', ni_filename);
|
||||
await fs.writeFile(ni_file, JSON.stringify(nickname_and_icons.nickname_and_icons[0], null, 4) + '\n', 'utf-8');
|
||||
}
|
||||
|
||||
if (argv.profileImage) {
|
||||
const filename = 'splatnet2-profile-' + records.records.unique_id + '-' + Date.now() + '.json';
|
||||
const file = path.join(argv.directory, filename);
|
||||
const image_filename = 'splatnet2-profile-' + records.records.unique_id + '-' + Date.now() + '.png';
|
||||
const image_file = path.join(argv.directory, image_filename);
|
||||
|
||||
const stage = argv.favouriteStage ?
|
||||
stages.stages.find(s => s.id === argv.favouriteStage ||
|
||||
s.name.toLowerCase() === argv.favouriteStage?.toLowerCase()) :
|
||||
stages.stages[0];
|
||||
|
||||
if (!stage) {
|
||||
debug('Invalid favourite stage "%s"', argv.favouriteStage);
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
if (!Object.values(ShareColour).includes(argv.favouriteColour)) {
|
||||
argv.favouriteColour = ShareColour.PINK;
|
||||
}
|
||||
|
||||
debug('Fetching profile image URL');
|
||||
const share = await splatnet.shareProfile(stage?.id ?? stages.stages[0].id, argv.favouriteColour as ShareColour);
|
||||
|
||||
debug('Fetching profile image');
|
||||
const image_response = await fetch(share.url);
|
||||
const image = await image_response.buffer();
|
||||
|
||||
debug('Writing profile image data %s', filename);
|
||||
await fs.writeFile(file, JSON.stringify({
|
||||
share,
|
||||
stage: stage ?? stages.stages[0],
|
||||
colour: argv.favouriteColour,
|
||||
}, null, 4) + '\n', 'utf-8');
|
||||
|
||||
debug('Writing profile image %s', filename);
|
||||
await fs.writeFile(image_file, image);
|
||||
}
|
||||
|
||||
if (argv.heroRecords) {
|
||||
debug('Fetching hero records');
|
||||
const hero = await splatnet.getHeroRecords();
|
||||
|
||||
const filename = 'splatnet2-hero-' + records.records.unique_id + '-' + Date.now() + '.json';
|
||||
const file = path.join(argv.directory, filename);
|
||||
|
||||
debug('Writing hero records %s', filename);
|
||||
await fs.writeFile(file, JSON.stringify(hero, null, 4) + '\n', 'utf-8');
|
||||
}
|
||||
|
||||
if (argv.timeline) {
|
||||
const filename = 'splatnet2-timeline-' + records.records.unique_id + '-' + Date.now() + '.json';
|
||||
const file = path.join(argv.directory, filename);
|
||||
|
||||
debug('Writing timeline %s', filename);
|
||||
await fs.writeFile(file, JSON.stringify(records, null, 4) + '\n', 'utf-8');
|
||||
}
|
||||
}
|
||||
194
src/cli/splatnet2/dump-results.ts
Normal file
194
src/cli/splatnet2/dump-results.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import createDebug from 'debug';
|
||||
import mkdirp from 'mkdirp';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import type { Arguments as ParentArguments } from '../splatnet2.js';
|
||||
import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js';
|
||||
import { getIksmToken } from './util.js';
|
||||
import SplatNet2Api from '../../api/splatnet2.js';
|
||||
import { NicknameAndIcon } from '../../api/splatnet2-types.js';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
const debug = createDebug('cli:splatnet2:dump-results');
|
||||
|
||||
export const command = 'dump-results <directory>';
|
||||
export const desc = 'Download all battle and coop results';
|
||||
|
||||
export function builder(yargs: Argv<ParentArguments>) {
|
||||
return yargs.positional('directory', {
|
||||
describe: 'Directory to write record data to',
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
}).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',
|
||||
default: true,
|
||||
}).option('battle-summary-image', {
|
||||
describe: 'Include regular/ranked/private/festival battle summary image',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
}).option('battle-images', {
|
||||
describe: 'Include regular/ranked/private/festival battle result images',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
}).option('coop', {
|
||||
describe: 'Include coop (Salmon Run) results',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
});
|
||||
}
|
||||
|
||||
type Arguments = YargsArguments<ReturnType<typeof builder>>;
|
||||
|
||||
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
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 getIksmToken(storage, token, argv.zncProxyUrl, argv.autoUpdateIksmSession);
|
||||
|
||||
await mkdirp(argv.directory);
|
||||
|
||||
const records = argv.coop ? await splatnet.getRecords() : null;
|
||||
|
||||
if (argv.battles) {
|
||||
await dumpResults(splatnet, argv.directory, argv.battleImages, argv.battleSummaryImage);
|
||||
}
|
||||
if (argv.coop) {
|
||||
await dumpCoopResults(splatnet, argv.directory, records!.records.unique_id);
|
||||
}
|
||||
}
|
||||
|
||||
async function dumpResults(splatnet: SplatNet2Api, directory: string, images = false, summary_image = images) {
|
||||
debug('Fetching battle results');
|
||||
const results = await splatnet.getResults();
|
||||
|
||||
const summary_filename = 'splatnet2-results-summary-' + results.unique_id + '-' + Date.now() + '.json';
|
||||
const summary_file = path.join(directory, summary_filename);
|
||||
|
||||
debug('Writing summary %s', summary_filename);
|
||||
await fs.writeFile(summary_file, JSON.stringify(results, null, 4) + '\n', 'utf-8');
|
||||
|
||||
if (summary_image) {
|
||||
const filename = 'splatnet2-results-summary-image-' + results.unique_id + '-' + Date.now() + '.json';
|
||||
const file = path.join(directory, filename);
|
||||
const image_filename = 'splatnet2-results-summary-image-' + results.unique_id + '-' + Date.now() + '.png';
|
||||
const image_file = path.join(directory, image_filename);
|
||||
|
||||
debug('Fetching battle results summary image URL');
|
||||
const share = await splatnet.shareResultsSummary();
|
||||
|
||||
debug('Fetching battle results summary image');
|
||||
const image_response = await fetch(share.url);
|
||||
const image = await image_response.buffer();
|
||||
|
||||
debug('Writing battle results summary image data %s', filename);
|
||||
await fs.writeFile(file, JSON.stringify({
|
||||
share,
|
||||
}, null, 4) + '\n', 'utf-8');
|
||||
|
||||
debug('Writing battle results summary image %s', filename);
|
||||
await fs.writeFile(image_file, image);
|
||||
}
|
||||
|
||||
for (const item of results.results) {
|
||||
const filename = 'splatnet2-result-' + results.unique_id + '-' + item.battle_number + '-' + item.type + '.json';
|
||||
const file = path.join(directory, filename);
|
||||
|
||||
try {
|
||||
await fs.stat(file);
|
||||
debug('Skipping battle result %d, file already exists', item.battle_number);
|
||||
} catch (err) {
|
||||
debug('Fetching battle result %d', item.battle_number);
|
||||
const result = await splatnet.getResult(item.battle_number);
|
||||
|
||||
const nickname_and_icons: NicknameAndIcon[] = [];
|
||||
|
||||
for (const playerresult of [result.player_result, ...result.my_team_members, ...result.other_team_members]) {
|
||||
const nickname_and_icon = await splatnet.getUserNicknameAndIcon([playerresult.player.principal_id]);
|
||||
nickname_and_icons.push(...nickname_and_icon.nickname_and_icons);
|
||||
}
|
||||
|
||||
debug('Writing %s', filename);
|
||||
await fs.writeFile(file, JSON.stringify({
|
||||
result,
|
||||
nickname_and_icons,
|
||||
}, null, 4) + '\n', 'utf-8');
|
||||
}
|
||||
|
||||
if (images) {
|
||||
const filename = 'splatnet2-result-image-' + results.unique_id + '-' +
|
||||
item.battle_number + '-' + item.type + '.json';
|
||||
const file = path.join(directory, filename);
|
||||
const image_filename = 'splatnet2-result-image-' + results.unique_id + '-' +
|
||||
item.battle_number + '-' + item.type + '.png';
|
||||
const image_file = path.join(directory, image_filename);
|
||||
|
||||
try {
|
||||
await fs.stat(file);
|
||||
await fs.stat(image_file);
|
||||
debug('Skipping battle result image %d, file already exists', item.battle_number);
|
||||
} catch (err) {
|
||||
debug('Fetching battle results summary image URL');
|
||||
const share = await splatnet.shareResult(item.battle_number);
|
||||
|
||||
debug('Fetching battle results summary image');
|
||||
const image_response = await fetch(share.url);
|
||||
const image = await image_response.buffer();
|
||||
|
||||
debug('Writing battle results summary image data %s', filename);
|
||||
await fs.writeFile(file, JSON.stringify({
|
||||
share,
|
||||
}, null, 4) + '\n', 'utf-8');
|
||||
|
||||
debug('Writing battle results summary image %s', filename);
|
||||
await fs.writeFile(image_file, image);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function dumpCoopResults(splatnet: SplatNet2Api, directory: string, user_id: string) {
|
||||
debug('Fetching coop results');
|
||||
const results = await splatnet.getCoopResults();
|
||||
|
||||
const summary_filename = 'splatnet2-coop-summary-' + user_id + '-' + Date.now() + '.json';
|
||||
const summary_file = path.join(directory, summary_filename);
|
||||
|
||||
debug('Writing summary %s', summary_filename);
|
||||
await fs.writeFile(summary_file, JSON.stringify(results, null, 4) + '\n', 'utf-8');
|
||||
|
||||
for (const item of results.results) {
|
||||
const filename = 'splatnet2-coop-result-' + user_id + '-' + item.job_id + '.json';
|
||||
const file = path.join(directory, filename);
|
||||
|
||||
try {
|
||||
await fs.stat(file);
|
||||
debug('Skipping coop result %d, file already exists', item.job_id);
|
||||
continue;
|
||||
} catch (err) {}
|
||||
|
||||
debug('Fetching coop result %d', item.job_id);
|
||||
const result = await splatnet.getCoopResult(item.job_id);
|
||||
|
||||
const nickname_and_icons: NicknameAndIcon[] = [];
|
||||
|
||||
for (const playerresult of [result.my_result, ...result.other_results]) {
|
||||
const nickname_and_icon = await splatnet.getUserNicknameAndIcon([playerresult.pid]);
|
||||
nickname_and_icons.push(...nickname_and_icon.nickname_and_icons);
|
||||
}
|
||||
|
||||
debug('Writing %s', filename);
|
||||
await fs.writeFile(file, JSON.stringify({
|
||||
result,
|
||||
nickname_and_icons,
|
||||
}, null, 4) + '\n', 'utf-8');
|
||||
}
|
||||
}
|
||||
3
src/cli/splatnet2/index.ts
Normal file
3
src/cli/splatnet2/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * as user from './user.js';
|
||||
export * as dumpResults from './dump-results.js';
|
||||
export * as dumpRecords from './dump-records.js';
|
||||
45
src/cli/splatnet2/user.ts
Normal file
45
src/cli/splatnet2/user.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../splatnet2.js';
|
||||
import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js';
|
||||
import { getIksmToken } from './util.js';
|
||||
|
||||
const debug = createDebug('cli:splatnet2:user');
|
||||
|
||||
export const command = 'user';
|
||||
export const desc = 'Get the authenticated Nintendo Account\'s player record';
|
||||
|
||||
export function builder(yargs: Argv<ParentArguments>) {
|
||||
return yargs.option('user', {
|
||||
describe: 'Nintendo Account ID',
|
||||
type: 'string',
|
||||
}).option('token', {
|
||||
describe: 'Nintendo Account session token',
|
||||
type: 'string',
|
||||
});
|
||||
}
|
||||
|
||||
type Arguments = YargsArguments<ReturnType<typeof builder>>;
|
||||
|
||||
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
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 getIksmToken(storage, token, argv.zncProxyUrl, argv.autoUpdateIksmSession);
|
||||
|
||||
const [records, stages, activefestivals, timeline] = await Promise.all([
|
||||
splatnet.getRecords(),
|
||||
splatnet.getStages(),
|
||||
splatnet.getActiveFestivals(),
|
||||
splatnet.getTimeline(),
|
||||
]);
|
||||
const nickname_and_icons = await splatnet.getUserNicknameAndIcon([records.records.player.principal_id]);
|
||||
|
||||
console.log('Player %s (Splatoon 2 ID %s, NSA ID %s) level %d',
|
||||
records.records.player.nickname,
|
||||
records.records.unique_id,
|
||||
records.records.player.principal_id,
|
||||
records.records.player.player_rank,
|
||||
records.records.player.player_type);
|
||||
}
|
||||
42
src/cli/splatnet2/util.ts
Normal file
42
src/cli/splatnet2/util.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import createDebug from 'debug';
|
||||
import persist from 'node-persist';
|
||||
import { getToken } from '../../util.js';
|
||||
import SplatNet2Api, { SavedIksmSessionToken } from '../../api/splatnet2.js';
|
||||
|
||||
const debug = createDebug('cli:splatnet2');
|
||||
|
||||
export async function getIksmToken(storage: persist.LocalStorage, token: string, proxy_url?: string, allow_fetch_token = false) {
|
||||
if (!token) {
|
||||
console.error('No token set. Set a Nintendo Account session token using the `--token` option or by running `nxapi nso token`.');
|
||||
throw new Error('Invalid token');
|
||||
}
|
||||
|
||||
const existingToken: SavedIksmSessionToken | undefined = await storage.getItem('IksmToken.' + token);
|
||||
|
||||
if (!existingToken || existingToken.expires_at <= Date.now()) {
|
||||
if (!allow_fetch_token) {
|
||||
throw new Error('No valid iksm_session cookie');
|
||||
}
|
||||
|
||||
console.warn('Authenticating to SplatNet 2');
|
||||
debug('Authenticating to SplatNet 2');
|
||||
|
||||
const {nso, data} = await getToken(storage, token, proxy_url);
|
||||
|
||||
const existingToken = await SplatNet2Api.loginWithZnc(nso, data.user);
|
||||
|
||||
await storage.setItem('IksmToken.' + token, existingToken);
|
||||
|
||||
return {
|
||||
splatnet: new SplatNet2Api(existingToken.iksm_session, existingToken.useragent),
|
||||
data: existingToken,
|
||||
};
|
||||
}
|
||||
|
||||
debug('Using existing token');
|
||||
|
||||
return {
|
||||
splatnet: new SplatNet2Api(existingToken.iksm_session, existingToken.useragent),
|
||||
data: existingToken,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user