Add commands to download SplatNet 2 data

This commit is contained in:
Samuel Elliott
2022-03-21 20:38:52 +00:00
parent 0515a781f7
commit 86fb372722
17 changed files with 1587 additions and 93 deletions

681
src/api/splatnet2-types.ts Normal file
View File

@@ -0,0 +1,681 @@
export interface WebServiceError {
code: string;
message: string;
}
/** GET /records */
export interface Records {
challenges: Challenges;
festivals: unknown[];
records: {
recent_disconnect_count: number;
recent_win_count: number;
stage_stats: unknown;
update_time: number;
recent_lose_count: number;
league_stats: {
team: LeagueStats;
pair: LeagueStats;
};
fes_results: unknown;
unique_id: string;
weapon_stats: Record<string | number, WeaponStats>;
win_count: number;
lose_count: number;
total_paint_point_octa: number;
player: Player;
start_time: number;
};
}
interface Challenges {
next_challenge: Challenge;
archived_challenges_octa: OctaChallenge[];
total_paint_point: number;
rewards: ChallengeReward[];
total_paint_point_octa: number;
rewards_octa: ChallengeReward[];
archived_challenges: Challenge[];
next_challenge_octa: OctaChallenge;
}
interface Challenge {
image: string;
key: string;
name: string;
paint_points: number;
}
interface OctaChallenge {
key: string;
paint_points: number;
url: null;
url_message: null;
name: string;
is_last: boolean;
image: string;
}
interface ChallengeReward {
id: string;
images: {
thumbnail: string;
url: string;
}[];
paint_points: number;
}
interface LeagueStats {
gold_count: number;
bronze_count: number;
silver_count: number;
no_medal_count: number;
}
interface WeaponStats {
win_meter: number;
total_paint_point: number;
last_use_time: number;
lose_count: number;
weapon: Weapon;
win_count: number;
max_win_meter: number;
}
interface Weapon {
name: string;
id: string;
thumbnail: string;
sub: SubWeapon;
special: SpecialWeapon;
image: string;
}
interface SubWeapon {
image_b: string;
name: string;
id: string;
image_a: string;
}
interface SpecialWeapon {
image_a: string;
id: string;
name: string;
image_b: string;
}
interface Player {
clothes: Gear & {kind: 'clothes'};
star_rank: number;
head_skills: Skills;
nickname: string;
shoes_skills: Skills;
weapon: Weapon;
player_rank: number;
max_league_point_team: number;
shoes: Gear & {kind: 'shoes'};
clothes_skills: Skills;
udemae_tower: Rank;
udemae_clam: Rank;
udemae_rainmaker: Rank;
player_type: PlayerType;
head: Gear & {kind: 'head'};
max_league_point_pair: number;
principal_id: string;
udemae_zones: Rank;
}
interface Gear {
name: string;
brand: Brand;
id: string;
rarity: number;
image: string;
kind: 'clothes' | 'shoes' | 'head';
thumbnail: string;
}
interface Brand {
image: string;
frequent_skill?: Skill;
name: string;
id: string;
}
interface Skills {
subs: (Skill | null)[];
main: Skill;
}
interface Skill {
name: string;
id: string;
image: string;
}
interface Rank {
name: null;
s_plus_number: null;
is_x: boolean;
number: number;
is_number_reached: boolean;
}
interface PlayerType {
species: PlayerSpecies;
style: PlayerStyle;
}
enum PlayerSpecies {
INKLING = 'inklings',
OCTOLING = 'octolings',
}
enum PlayerStyle {
GIRL = 'girl',
BOY = 'boy',
}
/** GET /data/stages */
export interface Stages {
stages: Stage[];
}
interface Stage {
image: string;
name: string;
id: string;
}
/** GET /festivals/active */
export interface ActiveFestivals {
festivals: unknown[];
}
/** GET /timeline */
export interface Timeline {
onlineshop: {
importance: number;
merchandise: ShopMerchandise;
};
udemae: {
importance: number;
};
coop: {
importance: number;
schedule: CoopSchedule;
reward_gear: CoopRewardGear;
};
stats: {
importance: number;
recents: MatchResults[];
};
challenge: {
next_challenge: Challenge;
total_paint_point: number;
importance: number;
last_archived_challenge: Challenge;
};
schedule: {
schedules: {
gachi: ScheduleItem[];
league: ScheduleItem[];
regular: ScheduleItem[];
};
importance: number;
};
weapon_availability: {
availabilities: unknown[];
importance: number;
};
fes_winners: {
importance: number;
};
fes_event_match_result: {
importance: number;
};
unique_id: string;
download_contents: {
is_available: boolean;
importance: number;
};
}
interface ShopMerchandise {
skill: Skill;
end_time: number;
gear: Gear;
id: string;
price: number;
kind: string;
}
interface CoopSchedule {
weapons: CoopWeapon[];
stage: {
image: string;
name: string;
};
start_time: number;
end_time: number;
}
type CoopWeapon = CoopStandardWeapon | CoopSpecialWeapon;
interface CoopStandardWeapon {
weapon: Omit<Weapon, 'sub' | 'special'>;
id: string;
}
interface CoopSpecialWeapon {
id: string;
coop_special_weapon: {
image: string;
name: string;
};
}
interface CoopRewardGear {
available_time: number;
gear: Gear;
}
/** GET /nickname_and_icon?id={...} */
export interface NicknameAndIcons {
nickname_and_icons: NicknameAndIcon[];
}
export interface NicknameAndIcon {
thumbnail_url: string;
nickname: string;
nsa_id: string;
}
/** GET /schedules */
export interface Schedules {
regular: ScheduleItem[];
league: ScheduleItem[];
gachi: ScheduleItem[];
}
interface ScheduleItem {
start_time: number;
end_time: number;
stage_b: Stage;
rule: Rule;
id: number;
game_mode: GameMode;
stage_a: Stage;
}
/** GET /records/hero */
export interface HeroRecords {
stage_infos: StageInfo[];
summary: {
weapon_cleared_info: Record<string, boolean>;
honor: {
code: string;
name: string;
};
clear_rate: number;
};
weapon_map: Record<string, HeroWeapon>;
}
interface StageInfo {
clear_weapons: Record<string, StageCleared>;
stage: HeroStage;
}
interface StageCleared {
clear_time: number;
weapon_category: string;
weapon_level: number;
}
interface HeroStage {
id: string;
is_boss: boolean;
area: string;
}
interface HeroWeapon {
category: string;
image: string;
}
/** GET /x_power_ranking/{season}/summary */
export interface XPowerRankingSummary {
clam_blitz: XPowerRankingRecords;
rainmaker: XPowerRankingRecords;
tower_control: XPowerRankingRecords;
splat_zones: XPowerRankingRecords;
}
interface XPowerRankingRecords {
weapon_ranking: null;
season_id: string;
top_rankings_count: number;
top_rankings: XPowerRankingRecordsRanking[];
status: string;
rule: Rule;
start_time: number;
end_time: number;
my_ranking: null;
}
interface XPowerRankingRecordsRanking {
name: string;
principal_id: string;
weapon: Weapon;
rank: number;
unique_id: string;
x_power: number;
rank_change: null;
cheater: boolean;
}
/** GET /festivals/pasts */
export interface PastFestivals {
festivals: Festival[];
results: FestivalResults[];
}
interface Festival {
colors: {
middle: InkColour;
bravo: InkColour;
alpha: InkColour;
};
festival_id: number;
names: {
bravo_short: string;
bravo_long: string;
alpha_short: string;
alpha_long: string;
};
images: {
bravo: string;
alpha: string;
panel: string;
};
times: {
start: number;
announce: number;
end: number;
result: number;
};
special_stage: {
name: string;
id: string;
image: string;
};
}
interface InkColour {
g: number;
a: number;
r: number;
b: number;
css_rgb: string;
}
type FestivalResults = FestivalResults1 | FestivalResults2;
interface FestivalResults2 {
contribution_alpha: FestivalResultsContribution;
contribution_bravo: FestivalResultsContribution;
rates: {
regular: FestivalResultsRates;
challenge: FestivalResultsRates;
vote: FestivalResultsRates;
},
festival_version: 2;
summary: {
regular: number;
challenge: number;
total: number;
vote: number;
};
festival_id: number;
}
interface FestivalResultsContribution {
regular: number;
challenge: number;
}
interface FestivalResultsRates {
alpha: number;
bravo: number;
}
interface FestivalResults1 {
festival_id: number;
summary: {
team: number;
vote: number;
total: number;
solo: number;
},
festival_version: 1;
rates: {
team: FestivalResultsRates;
solo: FestivalResultsRates;
vote: FestivalResultsRates;
};
}
/** GET /league_match_ranking/{league}/ALL */
export interface LeagueMatchRankings {
start_time: number;
league_type: {
name: string;
key: string;
};
league_ranking_region: {
code: string;
id: number;
};
rankings: LeagueMatchRanking[];
league_id: string;
}
interface LeagueMatchRanking {
tag_members: LeagueTagMember[];
point: number;
tag_id: string;
cheater: boolean;
rank: number;
}
interface LeagueTagMember {
weapon: Weapon;
unique_id: string;
principal_id: string;
}
/** GET /results */
export interface Results {
results: MatchResults[];
unique_id: string;
summary: {
kill_count_average: number;
victory_count: number;
count: number;
defeat_count: number;
special_count_average: number;
victory_rate: number;
death_count_average: number;
assist_count_average: number;
};
}
interface MatchResults {
battle_number: string;
my_team_percentage: number;
type: string;
start_time: number;
win_meter: number;
player_result: PlayerResult;
rule: Rule;
star_rank: number;
stage: Stage;
other_team_percentage: number;
other_team_result: TeamResult;
weapon_paint_point: number;
player_rank: number;
game_mode: GameMode;
my_team_result: TeamResult;
}
interface PlayerResult {
kill_count: number;
death_count: number;
player: Omit<Player, 'max_league_point_team' | 'udemae_tower' | 'udemae_clam' | 'udemae_rainmaker' | 'max_league_point_pair' | 'udemae_zones'>;
special_count: number;
assist_count: number;
sort_score: number;
game_paint_point: number;
}
interface Rule {
multiline_name: string;
name: string;
key: string;
}
interface GameMode {
key: string;
name: string;
}
interface TeamResult {
key: 'victory' | 'defeat';
name: string;
}
/** GET /results/1 */
export interface Result extends MatchResults {
other_team_members: PlayerResult[];
my_team_members: PlayerResult[];
}
/** GET /coop_results */
export interface CoopResults {
results: CoopResultsResult[];
summary: {
stats: CoopSummaryResult[];
card: CoopSummaryCard;
};
reward_gear: Gear;
}
interface CoopResultsResult {
grade_point: number;
job_score: number;
my_result: CoopPlayerResult;
grade: CoopGrade;
job_rate: number;
job_id: number;
start_time: number;
boss_counts: Record<string, CoopBossCount>;
end_time: number;
danger_rate: number;
play_time: number;
player_type: PlayerType;
job_result: CoopJobResult;
schedule: CoopSchedule;
wave_details: CoopWave[];
kuma_point: number;
grade_point_delta: number;
}
interface CoopPlayerResult {
special: SpecialWeapon;
name: string;
help_count: number;
golden_ikura_num: number;
dead_count: number;
pid: string;
boss_kill_counts: Record<string, CoopBossCount>;
weapon_list: CoopWeapon[];
ikura_num: number;
special_counts: number[];
player_type: PlayerType;
}
interface CoopGrade {
id: string;
long_name: string;
short_name: string;
name: string;
}
interface CoopBossCount {
boss: CoopBoss;
count: number;
}
interface CoopBoss {
name: string;
key: string;
}
type CoopJobResult = CoopJobResultSuccess | CoopJobResultFailure;
interface CoopJobResultSuccess {
is_clear: true;
failure_reason: null;
failure_wave: null;
}
interface CoopJobResultFailure {
failure_wave: number;
failure_reason: string;
is_clear: false;
}
interface CoopWave {
ikura_num: number;
event_type: CoopWaveType;
golden_ikura_num: number;
golden_ikura_pop_num: number;
water_level: CoopWaveWaterLevel;
quota_num: number;
}
interface CoopWaveType {
name: string;
key: string;
}
interface CoopWaveWaterLevel {
key: string;
name: string;
}
interface CoopSummaryResult {
schedule: CoopSchedule;
my_ikura_total: number;
kuma_point_total: number;
clear_num: number;
end_time: number;
job_num: number;
team_golden_ikura_total: number;
failure_counts: number[];
grade: {
id: string;
name: string;
};
team_ikura_total: number;
help_total: number;
start_time: number;
dead_total: number;
grade_point: number;
my_golden_ikura_total: number;
}
interface CoopSummaryCard {
kuma_point_total: number;
golden_ikura_total: number;
help_total: number;
ikura_total: number;
kuma_point: number;
job_num: number;
}
/** GET /coop_results/1 */
export interface CoopResult extends CoopResultsResult {
other_results: CoopPlayerResult[];
}
/** GET /coop_schedules */
export interface CoopSchedules {
details: CoopSchedule[];
schedules: CoopScheduleTimes[];
}
interface CoopScheduleTimes {
start_time: number;
end_time: number;
}
/** GET /onlineshop/merchandises */
export interface ShopMerchandises {
merchandises: ShopMerchandise[];
ordered_info: null;
}
/** POST /share/profile, POST /share/results/summary, POST /share/results/1 */
export interface ShareResponse {
url: string;
hashtags: string[];
text: string;
}

333
src/api/splatnet2.ts Normal file
View File

@@ -0,0 +1,333 @@
import fetch from 'node-fetch';
import createDebug from 'debug';
import { v4 as uuidgen } from 'uuid';
import { WebServiceToken } from './znc-types.js';
import { NintendoAccountUser } from './na.js';
import { ErrorResponse } from './util.js';
import ZncApi from './znc.js';
import { ActiveFestivals, CoopResult, CoopResults, CoopSchedules, HeroRecords, NicknameAndIcons, PastFestivals, Records, Result, Results, Schedules, ShareResponse, ShopMerchandises, Stages, Timeline, WebServiceError, XPowerRankingSummary } from './splatnet2-types.js';
const debug = createDebug('api:splatnet2');
export const SPLATNET2_WEBSERVICE_ID = '5741031244955648';
export const SPLATNET2_WEBSERVICE_URL = 'https://app.splatoon2.nintendo.net/';
export const SPLATNET2_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 SPLATNET2_URL = SPLATNET2_WEBSERVICE_URL + 'api';
const XPOWERRANKING_SEASON = /^(\d{2})(\d{2})01T00_(\d{2})(\d{2})01T00$/;
const LEAGUE_ID = /^(\d{2})(\d{2})(\d{2})(\d{2})(T|P)$/;
export interface SavedIksmSessionToken {
webserviceToken: WebServiceToken;
url: string;
cookies: string;
iksm_session: string;
expires_at: number;
useragent: string;
}
export default class SplatNet2Api {
constructor(
public iksm_session: string,
public useragent: string
) {}
async fetch<T = unknown>(url: string, method = 'GET', body?: string | FormData, headers?: object) {
const response = await fetch(SPLATNET2_URL + url, {
method: method,
headers: Object.assign({
'Upgrade-Insecure-Requests': '1',
'User-Agent': this.useragent,
'Cookie': 'iksm_session=' + encodeURIComponent(this.iksm_session),
'dnt': '1',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-GB,en-US;q=0.8',
'X-Requested-With': 'com.nintendo.znca',
}, headers),
body: body,
});
debug('fetch %s %s, response %s', method, url, response.status);
if (response.status !== 200) {
const data = response.headers.get('Content-Type')?.match(/\bapplication\/json\b/i) ?
await response.json() : await response.text();
throw new ErrorResponse('[splatnet2] Unknown error', response, data);
}
const data = await response.json() as T | WebServiceError;
if ('code' in data) {
throw new ErrorResponse('[splatnet2] ' + data.message, response, data);
}
return data;
}
async getRecords() {
return this.fetch<Records>('/records');
}
async getStages() {
return this.fetch<Stages>('/data/stages');
}
async getActiveFestivals() {
return this.fetch<ActiveFestivals>('/festivals/active');
}
async getTimeline() {
return this.fetch<Timeline>('/timeline');
}
async getUserNicknameAndIcon(ids: string[]) {
return this.fetch<NicknameAndIcons>('/nickname_and_icon?' +
ids.map(id => 'id=' + encodeURIComponent(id)).join('&'));
}
async getSchedules() {
return this.fetch<Schedules>('/schedules');
}
async getHeroRecords() {
return this.fetch<HeroRecords>('/records/hero');
}
async getXPowerRankingSummary(season: string | Date) {
if (season instanceof Date) {
season = toSeasonId(season.getUTCFullYear(), season.getUTCMonth() + 1);
}
let match = season.match(/^(\d+)-(\d{2})$/);
if (match) {
season = toSeasonId(parseInt(match[1]), parseInt(match[2]));
}
if (!season.match(XPOWERRANKING_SEASON)) {
throw new Error('Invalid season ID');
}
return this.fetch<XPowerRankingSummary>('/x_power_ranking/' + season + '/summary');
}
async getXPowerRankingLeaderboard(season: string | Date, rule: XPowerRankingRule, page: number = 1) {
if (season instanceof Date) {
season = toSeasonId(season.getUTCFullYear(), season.getUTCMonth() + 1);
}
let match = season.match(/^(\d+)-(\d{2})$/);
if (match) {
season = toSeasonId(parseInt(match[1]), parseInt(match[2]));
}
if (!season.match(XPOWERRANKING_SEASON)) {
throw new Error('Invalid season ID');
}
return this.fetch<unknown>('/x_power_ranking/' + season + '/' + rule + '?page=' + page);
}
async getPastFestivals() {
return this.fetch<PastFestivals>('/festivals/pasts');
}
async getLeagueMatchRanking(id: string, region: LeagueRegion) {
if (!id.match(LEAGUE_ID)) {
throw new Error('Invalid league ID');
}
return this.fetch<PastFestivals>('/league_match_ranking/' + id + '/' + region);
}
async getResults() {
return this.fetch<Results>('/results');
}
async getResult(id: string | number) {
return this.fetch<Result>('/results/' + id);
}
async getCoopResults() {
return this.fetch<CoopResults>('/coop_results');
}
async getCoopResult(id: number) {
return this.fetch<CoopResult>('/coop_results/' + id);
}
async getCoopSchedules() {
return this.fetch<CoopSchedules>('/coop_schedules');
}
async getShopMerchandises() {
return this.fetch<ShopMerchandises>('/onlineshop/merchandises');
}
async shareProfile(stage: string, colour: ShareColour) {
const boundary = uuidgen();
const data = `--${boundary}
Content-Disposition: form-data; name="stage"
${stage}
--${boundary}
Content-Disposition: form-data; name="color"
${colour}
--${boundary}--
`.replace(/\r?\n/g, '\r\n');
return this.fetch<ShareResponse>('/share/profile', 'POST', data, {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Referer': 'https://app.splatoon2.nintendo.net/home',
'X-Requested-With': 'XMLHttpRequest',
'Origin': 'https://app.splatoon2.nintendo.net',
});
}
async shareResultsSummary() {
return this.fetch<ShareResponse>('/share/results/summary', 'POST', '', {
'Referer': 'https://app.splatoon2.nintendo.net/results',
'X-Requested-With': 'XMLHttpRequest',
'Origin': 'https://app.splatoon2.nintendo.net',
});
}
async shareResult(id: string | number) {
return this.fetch<ShareResponse>('/share/results/' + id, 'POST', '', {
'Referer': 'https://app.splatoon2.nintendo.net/results/' + id,
'X-Requested-With': 'XMLHttpRequest',
'Origin': 'https://app.splatoon2.nintendo.net',
});
}
static async createWithZnc(nso: ZncApi, user: NintendoAccountUser) {
const data = await this.loginWithZnc(nso, user);
return {
splatnet: new this(data.iksm_session, data.useragent),
data,
};
}
static async loginWithZnc(nso: ZncApi, user: NintendoAccountUser) {
const webserviceToken = await nso.getWebServiceToken(SPLATNET2_WEBSERVICE_ID);
return this.loginWithWebServiceToken(webserviceToken.result, user);
}
static async loginWithWebServiceToken(webserviceToken: WebServiceToken, user: NintendoAccountUser): Promise<SavedIksmSessionToken> {
const url = new URL(SPLATNET2_WEBSERVICE_URL);
url.search = new URLSearchParams({
lang: user.language,
na_country: user.country,
na_lang: user.language,
}).toString();
const response = await fetch(url.toString(), {
headers: {
'Upgrade-Insecure-Requests': '1',
'User-Agent': SPLATNET2_WEBSERVICE_USERAGENT,
'x-gamewebtoken': webserviceToken.accessToken,
'dnt': '1',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-GB,en-US;q=0.8',
'X-Requested-With': 'com.nintendo.znca',
},
});
debug('fetch %s %s, response %s', 'GET', url, response.status);
if (response.status !== 200) {
throw new ErrorResponse('Unknown error', response);
}
const cookies = response.headers.get('Set-Cookie');
const match = cookies?.match(/\biksm_session=([^;]*)(;(\s*((?!expires)[a-z]+=([^;]*));?)*(\s*(expires=([^;]*));?)?|$)/i);
if (!match) {
throw new ErrorResponse('Response didn\'t include iksm_session cookie', response);
}
const iksm_session = decodeURIComponent(match[1]);
// Nintendo sets the expires field to an invalid timestamp - browsers don't care but Data.parse does
const expires = decodeURIComponent(match[8] || '')
.replace(/(\b)(\d{1,2})-([a-z]{3})-(\d{4})(\b)/gi, '$1$2 $3 $4$5');
debug('iksm_session %s, expires %s', iksm_session, expires);
const expires_at = expires ? Date.parse(expires) : Date.now() + 24 * 60 * 60 * 1000;
return {
webserviceToken,
url: url.toString(),
cookies: cookies!,
iksm_session,
expires_at,
useragent: SPLATNET2_WEBSERVICE_USERAGENT,
};
}
}
export function toSeasonId(year: number, month: number) {
const nextyear = month === 12 ? year + 1 : year;
const nextmonth = month === 12 ? 1 : month + 1;
if (year < 2000) throw new Error('Invalid season ID');
if (nextyear >= 2100) throw new Error('Invalid season ID');
return ('' + (year - 2000)).padStart(2, '0') +
('' + month).padStart(2, '0') +
'01T00_' +
('' + (nextyear - 2000)).padStart(2, '0') +
('' + nextmonth).padStart(2, '0') +
'01T00';
}
export enum XPowerRankingRule {
SPLAT_ZONES = 'splat_zones',
TOWER_CONTROL = 'tower_control',
RAINMAKER = 'rainmaker',
CLAM_BLITZ = 'clam_blitz',
}
export function toLeagueId(date: Date, type: LeagueType) {
const year = date.getUTCFullYear();
const month = date.getUTCMonth() + 1;
const day = date.getUTCDate();
const hour = Math.floor(date.getUTCHours() / 2) * 2;
if (year < 2000) throw new Error('Invalid league ID');
if (year >= 2100) throw new Error('Invalid league ID');
return ('' + (year - 2000)).padStart(2, '0') +
('' + month).padStart(2, '0') +
('' + day).padStart(2, '0') +
('' + hour).padStart(2, '0') +
type;
}
export enum LeagueType {
TEAM = 'T',
PAIR = 'P',
}
export enum LeagueRegion {
ALL_REGIONS = 'ALL',
JAPAN = 'JP',
NA_AU_NZ = 'US',
EUROPE = 'EU',
}
export enum ShareColour {
PINK = 'pink',
GREEN = 'green',
YELLOW = 'yellow',
PURPLE = 'purple',
BLUE = 'blue',
SUN_YELLOW = 'sun-yellow',
}

View File

@@ -73,6 +73,7 @@ export interface Game {
imageUri: string;
shopUri: string;
totalPlayTime: number;
/** 0 if never played before */
firstPlayedAt: number;
sysDescription: string;
}

View File

@@ -72,17 +72,17 @@ export default class ZncApi {
return this.fetch<CurrentUser>('/v3/User/ShowSelf', 'POST', '{"parameter":{}}');
}
async getWebServiceToken(id: string, nintendoAccountToken: string) {
async getWebServiceToken(id: string) {
const uuid = uuidgen();
const timestamp = '' + Math.floor(Date.now() / 1000);
const data = process.env.ZNCA_API_URL ?
await genfc(process.env.ZNCA_API_URL + '/f', nintendoAccountToken, timestamp, uuid, FlapgIid.APP) :
await flapg(nintendoAccountToken, timestamp, uuid, FlapgIid.APP);
await genfc(process.env.ZNCA_API_URL + '/f', this.token, timestamp, uuid, FlapgIid.APP) :
await flapg(this.token, timestamp, uuid, FlapgIid.APP);
const req = {
id,
registrationToken: nintendoAccountToken,
registrationToken: this.token,
f: data.f,
requestId: uuid,
timestamp,