Add commands to fetch tokens for splatnet2statink, s3s and s3si.ts

This commit is contained in:
Samuel Elliott
2025-08-17 13:06:30 +01:00
parent 34e399932a
commit 5ba004f9ab
5 changed files with 298 additions and 1 deletions

View File

@@ -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',

View File

@@ -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';

View File

@@ -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 <config>';
export const desc = 'Update a splatnet2statink config file with valid tokens for SplatNet 2';
export function builder(yargs: Argv<ParentArguments>) {
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<ReturnType<typeof builder>>;
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
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: '',
};

View File

@@ -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 <config>';
export const desc = 'Update a s3s config file with valid tokens for SplatNet 3';
export function builder(yargs: Argv<ParentArguments>) {
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<ReturnType<typeof builder>>;
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
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: '',
};

View File

@@ -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 <profile>';
export const desc = 'Update a s3si.ts profile with valid tokens for SplatNet 3';
export function builder(yargs: Argv<ParentArguments>) {
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<ReturnType<typeof builder>>;
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
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,
};