From 5ba004f9ab0e9467909aaef30b28462d0064628d Mon Sep 17 00:00:00 2001 From: Samuel Elliott Date: Sun, 17 Aug 2025 13:06:30 +0100 Subject: [PATCH] Add commands to fetch tokens for splatnet2statink, s3s and s3si.ts --- src/api/splatnet3.ts | 2 +- src/cli/util/commands.ts | 3 + src/cli/util/update-s2s-token.ts | 90 +++++++++++++++++++++++ src/cli/util/update-s3s-token.ts | 90 +++++++++++++++++++++++ src/cli/util/update-s3si-token.ts | 114 ++++++++++++++++++++++++++++++ 5 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 src/cli/util/update-s2s-token.ts create mode 100644 src/cli/util/update-s3s-token.ts create mode 100644 src/cli/util/update-s3si-token.ts diff --git a/src/api/splatnet3.ts b/src/api/splatnet3.ts index 3179dd6..7dd5086 100644 --- a/src/api/splatnet3.ts +++ b/src/api/splatnet3.ts @@ -17,7 +17,7 @@ export const SPLATNET3_WEBSERVICE_ID = 4834290508791808; export const SPLATNET3_WEBSERVICE_URL = 'https://api.lp1.av5ja.srv.nintendo.net'; export const SPLATNET3_WEBSERVICE_USERAGENT = 'Mozilla/5.0 (Linux; Android 8.0.0) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.125 Mobile Safari/537.36'; -const languages = [ +export const languages = [ 'de-DE', 'en-GB', 'en-US', 'es-ES', 'es-MX', 'fr-CA', 'fr-FR', 'it-IT', 'ja-JP', 'ko-KR', 'nl-NL', 'ru-RU', 'zh-CN', 'zh-TW', diff --git a/src/cli/util/commands.ts b/src/cli/util/commands.ts index 61eb081..08d378e 100644 --- a/src/cli/util/commands.ts +++ b/src/cli/util/commands.ts @@ -10,3 +10,6 @@ export * as presenceEmbedServer from './presence-embed-server.js'; export * as logArchive from './log-archive.js'; export * as decryptLogArchive from './decrypt-log-archive.js'; export * as status from './status.js'; +export * as updateS2sToken from './update-s2s-token.js'; +export * as updateS3sToken from './update-s3s-token.js'; +export * as updateS3siToken from './update-s3si-token.js'; diff --git a/src/cli/util/update-s2s-token.ts b/src/cli/util/update-s2s-token.ts new file mode 100644 index 0000000..58fd8aa --- /dev/null +++ b/src/cli/util/update-s2s-token.ts @@ -0,0 +1,90 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import type { Arguments as ParentArguments } from './index.js'; +import createDebug from '../../util/debug.js'; +import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js'; +import { initStorage } from '../../util/storage.js'; +import { getIksmToken } from '../../common/auth/splatnet2.js'; + +const debug = createDebug('cli:util:update-s2s-token'); +debug.enabled = true; + +export const command = 'update-s2s-token '; +export const desc = 'Update a splatnet2statink config file with valid tokens for SplatNet 2'; + +export function builder(yargs: Argv) { + return yargs.positional('config', { + describe: 'splatnet2statink config file path', + type: 'string', + }).option('znc-proxy-url', { + describe: 'URL of Nintendo Switch Online app API proxy server to use', + type: 'string', + default: process.env.ZNC_PROXY_URL, + }).option('auto-update-session', { + describe: 'Automatically obtain and refresh the iksm_session cookie', + type: 'boolean', + default: true, + }).option('user', { + describe: 'Nintendo Account ID', + type: 'string', + }).option('token', { + describe: 'Nintendo Account session token', + type: 'string', + }); +} + +type Arguments = YargsArguments>; + +export async function handler(argv: ArgumentsCamelCase) { + if (!argv.config) { + console.warn('Path to splatnet2statink config file not set.'); + process.exit(1); + } + + const config_json = await readFile(argv.config, 'utf-8').catch(err => { + if (err.code === 'ENOENT') return null; + throw err; + }); + + if (!config_json) { + debug('splatnet2statink config file does not exist, will create new config file'); + } + + const config = config_json ? JSON.parse(config_json) as Splatnet2statinkConfig : null; + + 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, data} = await getIksmToken(storage, token, argv.zncProxyUrl, argv.autoUpdateSession); + + const updated_config: Splatnet2statinkConfig = { + ...default_config, + ...config, + + cookie: data.iksm_session, + user_lang: data.language, + session_token: 'skip', + app_unique_id: data.user_id, + }; + + debug('writing splatnet2statink config file'); + await writeFile(argv.config, JSON.stringify(updated_config, null, 4) + '\n', 'utf-8'); +} + +interface Splatnet2statinkConfig { + api_key: string; + cookie: string; + user_lang: string; + session_token: string; + + app_timezone_offset?: string; + app_unique_id?: string; + app_user_agent?: string; +} + +const default_config: Splatnet2statinkConfig = { + api_key: '', + cookie: '', + user_lang: '', + session_token: '', +}; diff --git a/src/cli/util/update-s3s-token.ts b/src/cli/util/update-s3s-token.ts new file mode 100644 index 0000000..dc1c62e --- /dev/null +++ b/src/cli/util/update-s3s-token.ts @@ -0,0 +1,90 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import type { Arguments as ParentArguments } from './index.js'; +import createDebug from '../../util/debug.js'; +import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js'; +import { initStorage } from '../../util/storage.js'; +import { getBulletToken } from '../../common/auth/splatnet3.js'; + +const debug = createDebug('cli:util:update-s3s-token'); +debug.enabled = true; + +export const command = 'update-s3s-token '; +export const desc = 'Update a s3s config file with valid tokens for SplatNet 3'; + +export function builder(yargs: Argv) { + return yargs.positional('config', { + describe: 's3s config file path', + type: 'string', + }).option('znc-proxy-url', { + describe: 'URL of Nintendo Switch Online app API proxy server to use', + type: 'string', + default: process.env.ZNC_PROXY_URL, + }).option('auto-update-session', { + describe: 'Automatically obtain and refresh the SplatNet 3 access token', + type: 'boolean', + default: true, + }).option('user', { + describe: 'Nintendo Account ID', + type: 'string', + }).option('token', { + describe: 'Nintendo Account session token', + type: 'string', + }); +} + +type Arguments = YargsArguments>; + +export async function handler(argv: ArgumentsCamelCase) { + if (!argv.config) { + console.warn('Path to s3s config file not set.'); + process.exit(1); + } + + const config_json = await readFile(argv.config, 'utf-8').catch(err => { + if (err.code === 'ENOENT') return null; + throw err; + }); + + if (!config_json) { + debug('s3s config file does not exist, will create new config file'); + } + + const config = config_json ? JSON.parse(config_json) as S3sConfig : null; + + 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, data} = await getBulletToken(storage, token, argv.zncProxyUrl, argv.autoUpdateSession); + + const updated_config: S3sConfig = { + ...default_config, + ...config, + + acc_loc: data.language + '|' + data.country, + gtoken: data.webserviceToken.accessToken, + bullettoken: data.bullet_token.bulletToken, + session_token: 'skip', + }; + + debug('writing s3s config file'); + await writeFile(argv.config, JSON.stringify(updated_config, null, 4) + '\n', 'utf-8'); +} + +interface S3sConfig { + api_key: string; + acc_loc: string; + gtoken: string; + bullettoken: string; + session_token: string; + f_gen: string; +} + +const default_config: S3sConfig = { + api_key: '', + acc_loc: '', + gtoken: '', + bullettoken: '', + session_token: '', + f_gen: '', +}; diff --git a/src/cli/util/update-s3si-token.ts b/src/cli/util/update-s3si-token.ts new file mode 100644 index 0000000..6a598db --- /dev/null +++ b/src/cli/util/update-s3si-token.ts @@ -0,0 +1,114 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import type { Arguments as ParentArguments } from './index.js'; +import createDebug from '../../util/debug.js'; +import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js'; +import { initStorage } from '../../util/storage.js'; +import { getBulletToken } from '../../common/auth/splatnet3.js'; +import { languages as splatnet3_languages } from '../../api/splatnet3.js'; + +const debug = createDebug('cli:util:update-s3si-token'); +debug.enabled = true; + +export const command = 'update-s3si-token '; +export const desc = 'Update a s3si.ts profile with valid tokens for SplatNet 3'; + +export function builder(yargs: Argv) { + return yargs.positional('profile', { + describe: 's3si.ts profile path', + type: 'string', + }).option('znc-proxy-url', { + describe: 'URL of Nintendo Switch Online app API proxy server to use', + type: 'string', + default: process.env.ZNC_PROXY_URL, + }).option('auto-update-session', { + describe: 'Automatically obtain and refresh the SplatNet 3 access token', + type: 'boolean', + default: true, + }).option('user', { + describe: 'Nintendo Account ID', + type: 'string', + }).option('token', { + describe: 'Nintendo Account session token', + type: 'string', + }).option('language', { + describe: 'SplatNet 3 language - by default this command will set the language to your Nintendo Account language, use this to force a different language, usually `en-US` when exporting to Splashcat', + type: 'string', + choices: splatnet3_languages, + }); +} + +type Arguments = YargsArguments>; + +export async function handler(argv: ArgumentsCamelCase) { + if (!argv.profile) { + console.warn('Path to s3si.ts profile not set.'); + process.exit(1); + } + + const profile_json = await readFile(argv.profile, 'utf-8').catch(err => { + if (err.code === 'ENOENT') return null; + throw err; + }); + + if (!profile_json) { + debug('s3si.ts profile does not exist, will create new profile'); + } + + const profile = profile_json ? JSON.parse(profile_json) as ProfileState : null; + + 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, data} = await getBulletToken(storage, token, argv.zncProxyUrl, argv.autoUpdateSession); + + const updated_profile: ProfileState = { + ...default_profile, + ...profile, + + loginState: { + sessionToken: 'null', + gToken: data.webserviceToken.accessToken, + bulletToken: data.bullet_token.bulletToken, + }, + + userLang: argv.language ?? data.language, + userCountry: data.country, + }; + + debug('writing s3si.ts profile'); + await writeFile(argv.profile, JSON.stringify(updated_profile, null, 2) + '\n', 'utf-8'); +} + +interface ProfileState { + loginState?: ProfileLoginState; + fGen: string; + appUserAgent?: string; + userLang?: string; + userCountry?: string; + rankState?: ProfileRankState; + cacheDir: string; + + statInkApiKey?: string; + fileExportPath: string; + monitorInterval: number; + splashcatApiKey?: string; +} +interface ProfileLoginState { + sessionToken?: string; + gToken?: string; + bulletToken?: string; +} +interface ProfileRankState { + gameId: string; + timestamp?: number; + rank: string; + rankPoint: number; +} + +const default_profile: ProfileState = { + cacheDir: './cache', + fGen: 'https://api.imink.app/f', + fileExportPath: './export', + monitorInterval: 500, +};