mirror of
https://github.com/samuelthomas2774/nxapi.git
synced 2026-09-07 02:45:09 -05:00
Add parental controls
This commit is contained in:
276
src/api/moon-types.ts
Normal file
276
src/api/moon-types.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
export interface MoonError {
|
||||
type: string; // "https://moon.nintendo.com/errors/v1/401/invalid_token"
|
||||
status: number; // 401
|
||||
errorCode: string; // "invalid_token"
|
||||
title: string; // "UnauthorizedException"
|
||||
detail: string; // ""
|
||||
instance: string;
|
||||
}
|
||||
|
||||
/** GET /v1/users/{nintendoAccountId} */
|
||||
export interface User {
|
||||
nintendoAccountId: string;
|
||||
nickname: string;
|
||||
country: string; // "GB"
|
||||
language: string; // "en-GB"
|
||||
miiUri: {
|
||||
extraLarge: string;
|
||||
large: string;
|
||||
medium: string;
|
||||
small: string;
|
||||
extraSmall: string;
|
||||
};
|
||||
analyticsOptedIn: boolean;
|
||||
acceptedNotification: {
|
||||
all: boolean;
|
||||
};
|
||||
notices: unknown[];
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
/** GET /v1/users/{nintendoAccountId}/smart_devices */
|
||||
export interface SmartDevices {
|
||||
count: number;
|
||||
items: SmartDevice[];
|
||||
}
|
||||
|
||||
export interface SmartDevice {
|
||||
/** UUID v4, for iOS devices this is uppercase hex, for Android devices this is lowercase hex?? */
|
||||
id: string;
|
||||
nintendoAccountId: string;
|
||||
bundleId: string;
|
||||
os: SmartDeviceOS;
|
||||
osVersion: string;
|
||||
modelName: string;
|
||||
timeZone: string;
|
||||
appVersion: {
|
||||
displayedVersion: string; // "1.15.1", "1.16.0"
|
||||
internalVersion: number; // 305, 247
|
||||
};
|
||||
osLanguage: string;
|
||||
appLanguage: string;
|
||||
notificationToken: string | null;
|
||||
updateRequired: boolean;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
export enum SmartDeviceOS {
|
||||
IOS = 'IOS',
|
||||
ANDROID = 'ANDROID',
|
||||
}
|
||||
|
||||
/** GET /v1/users/{nintendoAccountId}/devices */
|
||||
export interface Devices {
|
||||
count: number;
|
||||
items: PairedDevice[];
|
||||
}
|
||||
|
||||
export interface PairedDevice {
|
||||
deviceId: string;
|
||||
device: Device;
|
||||
nintendoAccountId: string;
|
||||
label: string;
|
||||
parentalControlSettingState: {
|
||||
deviceId: string;
|
||||
targetEtag: string;
|
||||
synchronizationStatus: 'SYNCHRONIZED';
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
};
|
||||
hasNewMonthlySummary: boolean;
|
||||
hasFirstDailySummary: boolean;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
export interface Device {
|
||||
id: string;
|
||||
notificationToken: string;
|
||||
timeZone: string;
|
||||
language: string;
|
||||
region: string; // "EUROPE"
|
||||
serialNumber: string;
|
||||
firmwareVersion: {
|
||||
displayedVersion: string; // "13.2.1"
|
||||
internalVersion: number; // 852481
|
||||
};
|
||||
links: {
|
||||
pairingCode: {
|
||||
code: string;
|
||||
createdAt: number;
|
||||
expiresAt: number;
|
||||
};
|
||||
};
|
||||
activated: boolean;
|
||||
synchronizedUnlockCode: string;
|
||||
synchronizedParentalControlSetting: {
|
||||
synchronizedEtag: string;
|
||||
synchronizedAt: number;
|
||||
};
|
||||
lastOnlineCheckedAt: number;
|
||||
alarmSetting: {
|
||||
visibility: 'VISIBLE';
|
||||
invisibleUntil: number;
|
||||
};
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
/** GET /v1/devices/{deviceId}/daily_summaries */
|
||||
export interface DailySummaries {
|
||||
count: number;
|
||||
items: DailySummary[];
|
||||
updatedRecently: boolean;
|
||||
}
|
||||
|
||||
export interface DailySummary {
|
||||
deviceId: string;
|
||||
date: string; // "2022-03-14"
|
||||
result: DailySummaryResult;
|
||||
playingTime: number;
|
||||
exceededTime: null;
|
||||
disabledTime: number;
|
||||
miscTime: number;
|
||||
importantInfos: ImportantInfo[];
|
||||
notices: unknown[];
|
||||
observations: Observation[];
|
||||
playedApps: PlayedTitle[];
|
||||
anonymousPlayer: AnonymousPlayer | null;
|
||||
devicePlayers: DevicePlayer[];
|
||||
timeZoneUtcOffsetSeconds: number;
|
||||
lastPlayedAt: number | null;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
export enum DailySummaryResult {
|
||||
CALCULATING = 'CALCULATING',
|
||||
ACHIEVED = 'ACHIEVED',
|
||||
}
|
||||
export enum ImportantInfo {
|
||||
DID_WRONG_UNLOCK_CODE = 'DID_WRONG_UNLOCK_CODE',
|
||||
}
|
||||
|
||||
export interface Title {
|
||||
applicationId: string;
|
||||
title: string;
|
||||
imageUri: {
|
||||
extraSmall: string;
|
||||
small: string;
|
||||
medium: string;
|
||||
large: string;
|
||||
extraLarge: string;
|
||||
};
|
||||
hasUgc: boolean;
|
||||
shopUri: string;
|
||||
firstPlayDate: string | null;
|
||||
}
|
||||
|
||||
export interface ObservationTitleDownloaded {
|
||||
type: 'DID_APP_DOWNLOAD_START';
|
||||
applications: ObservationTitleDownloadedTitle[];
|
||||
}
|
||||
export interface ObservationTitleDownloadedTitle extends Title {
|
||||
firstPlayDate: null;
|
||||
}
|
||||
|
||||
export type Observation = ObservationTitleDownloaded;
|
||||
|
||||
export interface PlayedTitle extends Title {
|
||||
firstPlayDate: string;
|
||||
}
|
||||
|
||||
export interface AnonymousPlayer {
|
||||
playingTime: number;
|
||||
playedApps: DevicePlayerTitle[];
|
||||
}
|
||||
|
||||
export interface DevicePlayer extends AnonymousPlayer {
|
||||
playerId: string;
|
||||
nickname: string;
|
||||
imageUri: string;
|
||||
}
|
||||
|
||||
export interface DevicePlayerTitle {
|
||||
applicationId: string;
|
||||
firstPlayDate: string;
|
||||
playingTime: number;
|
||||
}
|
||||
|
||||
/** GET /v1/devices/{deviceId}/monthly_summaries */
|
||||
export interface MonthlySummaries {
|
||||
count: number;
|
||||
indexes: string[]; // ["2022-02", ...]
|
||||
items: MonthlySummariesMonthlySummary[];
|
||||
}
|
||||
export interface MonthlySummariesMonthlySummary {
|
||||
deviceId: string;
|
||||
month: string; // "2022-02"
|
||||
}
|
||||
|
||||
/** GET /v1/devices/{deviceId}/monthly_summaries/{month} */
|
||||
export interface MonthlySummary {
|
||||
deviceId: string;
|
||||
month: string; // "2022-02"
|
||||
dailySummaries: Record<string, MonthlySummaryDailySummary>;
|
||||
playingDays: number;
|
||||
playedApps: MonthlySummaryPlayedTitle[];
|
||||
insights: MonthlySummaryInsights;
|
||||
devicePlayers: MonthlySummaryDevicePlayer[];
|
||||
includedMajorVersions: number[];
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
export interface MonthlySummaryDevicePlayer {
|
||||
playerId: string;
|
||||
nickname: string;
|
||||
imageUri: string;
|
||||
dailySummaries: Record<string, MonthlySummaryDailySummary>;
|
||||
insights: MonthlySummaryInsights;
|
||||
}
|
||||
export interface MonthlySummaryDailySummary {
|
||||
date: string;
|
||||
result: 'ACHIEVED';
|
||||
playingTime: number;
|
||||
}
|
||||
export interface MonthlySummaryPlayedTitle extends PlayedTitle {
|
||||
playingDays: number;
|
||||
position: MonthlySummaryTitleRankingPosition;
|
||||
}
|
||||
export interface MonthlySummaryInsights {
|
||||
thisMonth: {
|
||||
averagePlayingTime: number;
|
||||
playingDays: number;
|
||||
playingTime: number;
|
||||
};
|
||||
previousMonth: {
|
||||
averagePlayingTime: number;
|
||||
playingDays: number;
|
||||
playingTime: number;
|
||||
} | null;
|
||||
rankings: {
|
||||
byDay: MonthlySummaryTitleRanking[];
|
||||
byTime: MonthlySummaryTitleRanking[];
|
||||
};
|
||||
}
|
||||
export interface MonthlySummaryTitleRanking {
|
||||
applicationId: string;
|
||||
units: number;
|
||||
position: MonthlySummaryTitleRankingPosition;
|
||||
ratio: number;
|
||||
}
|
||||
export enum MonthlySummaryTitleRankingPosition {
|
||||
UP = 'UP',
|
||||
STAY = 'STAY',
|
||||
DOWN = 'DOWN',
|
||||
NEW = 'NEW',
|
||||
}
|
||||
|
||||
/** GET /v1/devices/{deviceId}/parental_control_setting_state */
|
||||
export interface ParentalControlSettingState {
|
||||
deviceId: string;
|
||||
targetEtag: string;
|
||||
synchronizationStatus: 'SYNCHRONIZED';
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
108
src/api/moon.ts
Normal file
108
src/api/moon.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import fetch from 'node-fetch';
|
||||
import createDebug from 'debug';
|
||||
import { getNintendoAccountToken, getNintendoAccountUser } from './na.js';
|
||||
import { ErrorResponse } from './util.js';
|
||||
import { DailySummaries, Devices, MonthlySummaries, MonthlySummary, MoonError, SmartDevices, User } from './moon-types.js';
|
||||
|
||||
const debug = createDebug('api:moon');
|
||||
|
||||
const MOON_URL = 'https://api-lp1.pctl.srv.nintendo.net/moon';
|
||||
export const MOON_CLIENT_ID = '54789befb391a838';
|
||||
|
||||
export default class MoonApi {
|
||||
constructor(
|
||||
public token: string,
|
||||
public naId: string
|
||||
) {}
|
||||
|
||||
async fetch<T = unknown>(url: string, method = 'GET', body?: string, headers?: object) {
|
||||
const response = await fetch(MOON_URL + url, {
|
||||
method: method,
|
||||
headers: Object.assign({
|
||||
'Authorization': 'Bearer ' + this.token,
|
||||
'Cache-Control': 'no-store',
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'X-Moon-App-Id': 'com.nintendo.znma',
|
||||
'X-Moon-Os': 'ANDROID',
|
||||
'X-Moon-Os-Version': '26',
|
||||
'X-Moon-Model': '',
|
||||
'X-Moon-TimeZone': 'Europe/London',
|
||||
'X-Moon-Os-Language': 'en-GB',
|
||||
'X-Moon-App-Language': 'en-GB',
|
||||
'X-Moon-App-Display-Version': '1.16.0',
|
||||
'X-Moon-App-Internal-Version': '247',
|
||||
'User-Agent': 'moon_ANDROID/1.16.0 (com.nintendo.znma; build:247; ANDROID 26)',
|
||||
}, headers),
|
||||
body: body,
|
||||
});
|
||||
|
||||
debug('fetch %s %s, response %s', method, url, response.status);
|
||||
|
||||
const data = await response.json() as T | MoonError;
|
||||
|
||||
if ('errorCode' in data) {
|
||||
throw new ErrorResponse('[moon] ' + data.title, response, data);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async getUser() {
|
||||
return this.fetch<User>('/v1/users/' + this.naId);
|
||||
}
|
||||
|
||||
async getSmartDevices() {
|
||||
return this.fetch<SmartDevices>('/v1/users/' + this.naId + '/smart_devices');
|
||||
}
|
||||
|
||||
async getDevices() {
|
||||
return this.fetch<Devices>('/v1/users/' + this.naId + '/devices');
|
||||
}
|
||||
|
||||
async getDailySummaries(id: string) {
|
||||
return this.fetch<DailySummaries>('/v1/devices/' + id + '/daily_summaries');
|
||||
}
|
||||
|
||||
async getMonthlySummaries(id: string) {
|
||||
return this.fetch<MonthlySummaries>('/v1/devices/' + id + '/monthly_summaries');
|
||||
}
|
||||
|
||||
async getMonthlySummary(id: string, month: string) {
|
||||
return this.fetch<MonthlySummary>('/v1/devices/' + id + '/monthly_summaries/' + month);
|
||||
}
|
||||
|
||||
async getParentalControlSettingState(id: string) {
|
||||
return this.fetch<unknown>('/v1/devices/' + id + '/parental_control_setting_state');
|
||||
}
|
||||
|
||||
static async createWithSessionToken(token: string) {
|
||||
const data = await this.loginWithSessionToken(token);
|
||||
|
||||
return {
|
||||
moon: new this(data.nintendoAccountToken.access_token!, data.user.id),
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
async renewToken(token: string) {
|
||||
const data = await MoonApi.loginWithSessionToken(token);
|
||||
|
||||
this.token = data.nintendoAccountToken.access_token!;
|
||||
this.naId = data.user.id;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static async loginWithSessionToken(token: string) {
|
||||
// Nintendo Account token
|
||||
const nintendoAccountToken = await getNintendoAccountToken(token, MOON_CLIENT_ID);
|
||||
|
||||
// Nintendo Account user data
|
||||
const user = await getNintendoAccountUser(nintendoAccountToken);
|
||||
|
||||
return {
|
||||
nintendoAccountToken,
|
||||
user,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ export async function getNintendoAccountSessionToken(code: string, verifier: str
|
||||
return token;
|
||||
}
|
||||
|
||||
export async function getNintendoAccountToken(token: string) {
|
||||
export async function getNintendoAccountToken(token: string, client_id: string) {
|
||||
debug('Getting Nintendo Account token');
|
||||
|
||||
const response = await fetch('https://accounts.nintendo.com/connect/1.0.0/api/token', {
|
||||
@@ -44,7 +44,7 @@ export async function getNintendoAccountToken(token: string) {
|
||||
'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 8.0.0)',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
client_id: '71b963c1b7b6d119',
|
||||
client_id,
|
||||
session_token: token,
|
||||
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer-session-token',
|
||||
}),
|
||||
@@ -91,7 +91,7 @@ export interface NintendoAccountSessionToken {
|
||||
}
|
||||
|
||||
export interface NintendoAccountToken {
|
||||
scope: ['openid', 'user', 'user.birthday', 'user.mii', 'user.screenName'];
|
||||
scope: string[];
|
||||
token_type: 'Bearer';
|
||||
id_token: string;
|
||||
access_token?: string;
|
||||
|
||||
@@ -14,6 +14,7 @@ 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 const ZNCA_CLIENT_ID = '71b963c1b7b6d119';
|
||||
|
||||
export default class ZncApi {
|
||||
constructor(
|
||||
@@ -112,7 +113,7 @@ export default class ZncApi {
|
||||
const timestamp = '' + Math.floor(Date.now() / 1000);
|
||||
|
||||
// Nintendo Account token
|
||||
const nintendoAccountToken = await getNintendoAccountToken(token);
|
||||
const nintendoAccountToken = await getNintendoAccountToken(token, ZNCA_CLIENT_ID);
|
||||
|
||||
// Nintendo Account user data
|
||||
const user = await getNintendoAccountUser(nintendoAccountToken);
|
||||
|
||||
Reference in New Issue
Block a user