mirror of
https://github.com/samuelthomas2774/nxapi.git
synced 2026-09-07 02:45:09 -05:00
Initial commit
This commit is contained in:
80
src/api/f.ts
Normal file
80
src/api/f.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import fetch from 'node-fetch';
|
||||
import createDebug from 'debug';
|
||||
import { ErrorResponse } from './util.js';
|
||||
|
||||
const debugS2s = createDebug('api:s2s');
|
||||
const debugFlapg = createDebug('api:flapg');
|
||||
|
||||
export async function getLoginHash(token: string, timestamp: string | number) {
|
||||
debugS2s('Getting login hash');
|
||||
|
||||
const response = await fetch('https://elifessler.com/s2s/api/gen2', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'User-Agent': 'discord-switch-presence/1.0.0',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
naIdToken: token,
|
||||
timestamp: '' + timestamp,
|
||||
}).toString(),
|
||||
});
|
||||
|
||||
const data = await response.json() as LoginHashApiResponse | LoginHashApiError;
|
||||
|
||||
if ('error' in data) {
|
||||
throw new ErrorResponse('[s2s] ' + data.error, response, data);
|
||||
}
|
||||
|
||||
debugS2s('Got login hash "%s"', data.hash, data);
|
||||
|
||||
return data.hash;
|
||||
}
|
||||
|
||||
export async function flapg(token: string, timestamp: string | number, guid: string, iid: FlapgIid) {
|
||||
const hash = await getLoginHash(token, timestamp);
|
||||
|
||||
debugFlapg('Getting f parameter', {
|
||||
token, timestamp, guid, iid,
|
||||
});
|
||||
|
||||
const response = await fetch('https://flapg.com/ika2/api/login?public', {
|
||||
headers: {
|
||||
'x-token': token,
|
||||
'x-time': '' + timestamp,
|
||||
'x-guid': guid,
|
||||
'x-hash': hash,
|
||||
'x-ver': '3',
|
||||
'x-iid': iid,
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json() as FlapgApiResponse;
|
||||
|
||||
debugFlapg('Got f parameter "%s"', data.result.f);
|
||||
|
||||
return data.result;
|
||||
}
|
||||
|
||||
export interface LoginHashApiResponse {
|
||||
hash: string;
|
||||
}
|
||||
export interface LoginHashApiError {
|
||||
error: string;
|
||||
}
|
||||
|
||||
export enum FlapgIid {
|
||||
/** Nintendo Switch Online app token */
|
||||
NSO = 'nso',
|
||||
/** Web service token */
|
||||
APP = 'app',
|
||||
}
|
||||
|
||||
export interface FlapgApiResponse {
|
||||
result: {
|
||||
f: string;
|
||||
p1: string;
|
||||
p2: string;
|
||||
p3: string;
|
||||
};
|
||||
}
|
||||
138
src/api/na.ts
Normal file
138
src/api/na.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import fetch from 'node-fetch';
|
||||
import createDebug from 'debug';
|
||||
import { ErrorResponse } from './util.js';
|
||||
|
||||
const debug = createDebug('api:na');
|
||||
|
||||
export async function getNintendoAccountToken(token: string) {
|
||||
debug('Getting Nintendo Account token');
|
||||
|
||||
const response = await fetch('https://accounts.nintendo.com/connect/1.0.0/api/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 8.0.0)',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
client_id: '71b963c1b7b6d119',
|
||||
session_token: token,
|
||||
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer-session-token',
|
||||
}),
|
||||
});
|
||||
|
||||
const nintendoAccountToken = await response.json() as NintendoAccountToken | NintendoAccountError;
|
||||
|
||||
if ('errorCode' in nintendoAccountToken) {
|
||||
throw new ErrorResponse('[na] + ' + nintendoAccountToken.detail, response, nintendoAccountToken);
|
||||
}
|
||||
|
||||
debug('Got Nintendo Account token', nintendoAccountToken);
|
||||
|
||||
return nintendoAccountToken;
|
||||
}
|
||||
|
||||
export async function getNintendoAccountUser(token: NintendoAccountToken) {
|
||||
debug('Getting Nintendo Account user info');
|
||||
|
||||
const response = await fetch('https://api.accounts.nintendo.com/2.0.0/users/me', {
|
||||
headers: {
|
||||
'Accept-Language': 'en-GB',
|
||||
'User-Agent': 'NASDKAPI; Android',
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': 'Bearer ' + token.access_token!,
|
||||
},
|
||||
});
|
||||
|
||||
const user = await response.json() as NintendoAccountUser | NintendoAccountError;
|
||||
|
||||
if ('errorCode' in user) {
|
||||
throw new ErrorResponse('[na] + ' + user.detail, response, user);
|
||||
}
|
||||
|
||||
debug('Got Nintendo Account user info', user);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
export interface NintendoAccountToken {
|
||||
scope: ['openid', 'user', 'user.birthday', 'user.mii', 'user.screenName'];
|
||||
token_type: 'Bearer';
|
||||
id_token: string;
|
||||
access_token?: string;
|
||||
expires_in: 900;
|
||||
}
|
||||
|
||||
export interface NintendoAccountUser {
|
||||
emailOptedIn: boolean;
|
||||
language: string;
|
||||
country: string;
|
||||
timezone: {
|
||||
name: string;
|
||||
id: string;
|
||||
utcOffsetSeconds: number;
|
||||
utcOffset: string;
|
||||
};
|
||||
region: null;
|
||||
nickname: string;
|
||||
clientFriendsOptedIn: boolean;
|
||||
mii: {
|
||||
favoriteColor: string;
|
||||
id: string;
|
||||
updatedAt: number;
|
||||
coreData: {
|
||||
'4': string;
|
||||
};
|
||||
clientId: '1cfe3a55ed8924d9';
|
||||
imageUriTemplate: string;
|
||||
storeData: {
|
||||
'3': string;
|
||||
};
|
||||
imageOrigin: string;
|
||||
etag: string;
|
||||
type: 'profile';
|
||||
};
|
||||
isChild: boolean;
|
||||
eachEmailOptedIn: {
|
||||
survey: {
|
||||
updatedAt: number;
|
||||
optedIn: boolean;
|
||||
};
|
||||
deals: {
|
||||
updatedAt: number;
|
||||
optedIn: boolean;
|
||||
};
|
||||
};
|
||||
updatedAt: number;
|
||||
candidateMiis: unknown[];
|
||||
id: string;
|
||||
createdAt: number;
|
||||
emailVerified: boolean;
|
||||
analyticsPermissions: {
|
||||
internalAnalysis: {
|
||||
updatedAt: number;
|
||||
permitted: boolean;
|
||||
};
|
||||
targetMarketing: {
|
||||
updatedAt: number;
|
||||
permitted: boolean;
|
||||
};
|
||||
};
|
||||
emailOptedInUpdatedAt: number;
|
||||
birthday: string;
|
||||
screenName: string;
|
||||
gender: string;
|
||||
analyticsOptedInUpdatedAt: number;
|
||||
analyticsOptedIn: boolean;
|
||||
clientFriendsOptedInUpdatedAt: number;
|
||||
}
|
||||
|
||||
export interface NintendoAccountError {
|
||||
errorCode: string;
|
||||
detail: string;
|
||||
instance: string;
|
||||
title: string;
|
||||
status: number;
|
||||
type: string;
|
||||
}
|
||||
11
src/api/util.ts
Normal file
11
src/api/util.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Response } from 'node-fetch';
|
||||
|
||||
export class ErrorResponse<T = unknown> extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly response: Response,
|
||||
readonly data: T = undefined as any
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
135
src/api/znc-types.ts
Normal file
135
src/api/znc-types.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
|
||||
export interface ZncSuccessResponse<T = unknown> {
|
||||
status: 0;
|
||||
result: T;
|
||||
correlationId: string;
|
||||
}
|
||||
|
||||
export interface ZncErrorResponse {
|
||||
status: number;
|
||||
errorMessage: string;
|
||||
correlationId: string;
|
||||
}
|
||||
|
||||
export type ZncResponse<T = unknown> = ZncSuccessResponse<T> | ZncErrorResponse;
|
||||
|
||||
export interface AccountLogin {
|
||||
user: CurrentUser;
|
||||
webApiServerCredential: {
|
||||
accessToken: string;
|
||||
expiresIn: number;
|
||||
};
|
||||
firebaseCredential: {
|
||||
accessToken: string;
|
||||
expiresIn: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Announcement {
|
||||
announcementId: number;
|
||||
priority: number;
|
||||
forceDisplayEndDate: number;
|
||||
distributionDate: number;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface Friends {
|
||||
friends: Friend[];
|
||||
}
|
||||
|
||||
export interface Friend {
|
||||
id: number;
|
||||
nsaId: string;
|
||||
imageUri: string;
|
||||
name: string;
|
||||
isFriend: boolean;
|
||||
isFavoriteFriend: boolean;
|
||||
isServiceUser: boolean;
|
||||
friendCreatedAt: number;
|
||||
presence: Presence;
|
||||
}
|
||||
|
||||
export interface Presence {
|
||||
state: PresenceState;
|
||||
updatedAt: number;
|
||||
logoutAt: number;
|
||||
game: Game | {};
|
||||
}
|
||||
|
||||
export enum PresenceState {
|
||||
/** Offline */
|
||||
OFFLINE = 'OFFLINE',
|
||||
/** A console linked to this account is online, but the user isn't selected in an application */
|
||||
INACTIVE = 'INACTIVE',
|
||||
/** The user is selected in an application */
|
||||
ONLINE = 'ONLINE',
|
||||
/** The user is selected in an application (and I assume playing online?) */
|
||||
PLAYING = 'PLAYING',
|
||||
}
|
||||
|
||||
export interface Game {
|
||||
name: string;
|
||||
imageUri: string;
|
||||
shopUri: string;
|
||||
totalPlayTime: number;
|
||||
firstPlayedAt: number;
|
||||
sysDescription: string;
|
||||
}
|
||||
|
||||
export interface WebService {
|
||||
id: number;
|
||||
uri: string;
|
||||
customAttributes: WebServiceAttribute[];
|
||||
whiteList: string[];
|
||||
name: string;
|
||||
imageUri: string;
|
||||
}
|
||||
|
||||
export interface WebServiceAttribute {
|
||||
attrValue: string;
|
||||
attrKey: string;
|
||||
}
|
||||
|
||||
export interface ActiveEvent {
|
||||
// ??
|
||||
}
|
||||
|
||||
export interface CurrentUser {
|
||||
id: number;
|
||||
nsaId: string;
|
||||
imageUri: string;
|
||||
name: string;
|
||||
supportId: string;
|
||||
isChildRestricted: boolean;
|
||||
etag: string;
|
||||
links: {
|
||||
nintendoAccount: {
|
||||
membership: {
|
||||
// active: {
|
||||
// active: boolean;
|
||||
// };
|
||||
active: boolean;
|
||||
};
|
||||
};
|
||||
friendCode: {
|
||||
regenerable: boolean;
|
||||
regenerableAt: number;
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
permissions: {
|
||||
presence: PresencePermissions;
|
||||
};
|
||||
presence: Presence;
|
||||
}
|
||||
export enum PresencePermissions {
|
||||
FRIENDS = 'FRIENDS',
|
||||
FAVORITE_FRIENDS = 'FAVORITE_FRIENDS',
|
||||
SELF = 'SELF',
|
||||
}
|
||||
|
||||
export interface WebServiceToken {
|
||||
accessToken: string;
|
||||
expiresIn: number;
|
||||
}
|
||||
164
src/api/znc.ts
Normal file
164
src/api/znc.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import fetch from 'node-fetch';
|
||||
import { v4 as uuidgen } from 'uuid';
|
||||
import createDebug from 'debug';
|
||||
import { flapg, FlapgIid } from './f.js';
|
||||
import { AccountLogin, ActiveEvent, Announcement, CurrentUser, Friends, WebService, WebServiceToken, ZncResponse } from './znc-types.js';
|
||||
import { getNintendoAccountToken, getNintendoAccountUser } from './na.js';
|
||||
import { ErrorResponse } from './util.js';
|
||||
|
||||
const debug = createDebug('api:znc');
|
||||
|
||||
const ZNCA_PLATFORM = 'Android';
|
||||
const ZNCA_PLATFORM_VERSION = '8.0.0';
|
||||
const ZNCA_VERSION = '2.0.0';
|
||||
const ZNCA_USER_AGENT = `com.nintendo.znca/${ZNCA_VERSION}(${ZNCA_PLATFORM}/${ZNCA_PLATFORM_VERSION})`;
|
||||
|
||||
const ZNC_URL = 'https://api-lp1.znc.srv.nintendo.net';
|
||||
|
||||
export default class ZncApi {
|
||||
constructor(
|
||||
private token: string
|
||||
) {}
|
||||
|
||||
protected async fetch<T = unknown>(url: string, method = 'GET', body?: string, headers?: object) {
|
||||
const response = await fetch(ZNC_URL + url, {
|
||||
method: method,
|
||||
headers: Object.assign({
|
||||
'X-Platform': ZNCA_PLATFORM,
|
||||
'X-ProductVersion': ZNCA_VERSION,
|
||||
'Authorization': 'Bearer ' + this.token,
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'User-Agent': ZNCA_USER_AGENT,
|
||||
}, headers),
|
||||
body: body,
|
||||
});
|
||||
|
||||
const data = await response.json() as ZncResponse<T>;
|
||||
|
||||
if ('errorMessage' in data) {
|
||||
throw new ErrorResponse('[znc] ' + data.errorMessage, response, data);
|
||||
}
|
||||
if (data.status !== 0) {
|
||||
throw new ErrorResponse('[znc] Unknown error', response, data);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async getAnnouncements() {
|
||||
return this.fetch<Announcement[]>('/v1/Announcement/List', 'POST', '{"parameter":{}}');
|
||||
}
|
||||
|
||||
async getFriendList() {
|
||||
return this.fetch<Friends>('/v3/Friend/List', 'POST', '{"parameter":{}}');
|
||||
}
|
||||
|
||||
async getWebServices() {
|
||||
const uuid = uuidgen();
|
||||
|
||||
return this.fetch<WebService[]>('/v1/Game/ListWebServices', 'POST', JSON.stringify({
|
||||
requestId: uuid,
|
||||
}));
|
||||
}
|
||||
|
||||
async getActiveEvent() {
|
||||
return this.fetch<ActiveEvent>('/v1/Event/GetActiveEvent', 'POST', '{"parameter":{}}');
|
||||
}
|
||||
|
||||
async getCurrentUser() {
|
||||
return this.fetch<CurrentUser>('/v3/User/ShowSelf', 'POST', '{"parameter":{}}');
|
||||
}
|
||||
|
||||
async getWebServiceToken(id: string, nintendoAccountToken: string) {
|
||||
const uuid = uuidgen();
|
||||
const timestamp = '' + Math.floor(Date.now() / 1000);
|
||||
|
||||
const data = await flapg(nintendoAccountToken, timestamp, uuid, FlapgIid.APP);
|
||||
|
||||
const req = {
|
||||
id,
|
||||
registrationToken: nintendoAccountToken,
|
||||
f: data.f,
|
||||
requestId: data.p3,
|
||||
timestamp: data.p2,
|
||||
};
|
||||
|
||||
return this.fetch<WebServiceToken>('/v2/Game/GetWebServiceToken', 'POST', JSON.stringify({
|
||||
parameter: req,
|
||||
}));
|
||||
}
|
||||
|
||||
static async createWithSessionToken(token: string) {
|
||||
const data = await this.loginWithSessionToken(token);
|
||||
|
||||
return {
|
||||
nso: new this(data.credential.accessToken),
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
async renewToken(token: string) {
|
||||
const data = await ZncApi.loginWithSessionToken(token);
|
||||
|
||||
this.token = data.credential.accessToken;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static async loginWithSessionToken(token: string) {
|
||||
const uuid = uuidgen();
|
||||
const timestamp = '' + Math.floor(Date.now() / 1000);
|
||||
|
||||
// Nintendo Account token
|
||||
const nintendoAccountToken = await getNintendoAccountToken(token);
|
||||
|
||||
// Nintendo Account user data
|
||||
const user = await getNintendoAccountUser(nintendoAccountToken);
|
||||
|
||||
const flapgdata = await flapg(nintendoAccountToken.id_token, timestamp, uuid, FlapgIid.NSO);
|
||||
|
||||
debug('Getting Nintendo Switch Online app token');
|
||||
|
||||
const response = await fetch(ZNC_URL + '/v3/Account/Login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Platform': ZNCA_PLATFORM,
|
||||
'X-ProductVersion': ZNCA_VERSION,
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'User-Agent': ZNCA_USER_AGENT,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
parameter: {
|
||||
naIdToken: flapgdata.p1,
|
||||
naBirthday: user.birthday,
|
||||
naCountry: user.country,
|
||||
language: user.language,
|
||||
timestamp: flapgdata.p2,
|
||||
requestId: flapgdata.p3,
|
||||
f: flapgdata.f,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json() as ZncResponse<AccountLogin>;
|
||||
|
||||
if ('errorMessage' in data) {
|
||||
throw new ErrorResponse('[znc] ' + data.errorMessage, response, data);
|
||||
}
|
||||
if (data.status !== 0) {
|
||||
throw new ErrorResponse('[znc] Unknown error', response, data);
|
||||
}
|
||||
|
||||
debug('Got Nintendo Switch Online app token', data);
|
||||
|
||||
return {
|
||||
uuid,
|
||||
timestamp,
|
||||
nintendoAccountToken,
|
||||
user,
|
||||
flapg: flapgdata,
|
||||
nsoAccount: data.result,
|
||||
credential: data.result.webApiServerCredential,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user