Add parental controls

This commit is contained in:
Samuel Elliott
2022-03-16 20:58:15 +00:00
parent 1b71e8b8fe
commit 3919ceeb21
23 changed files with 1137 additions and 32 deletions

276
src/api/moon-types.ts Normal file
View 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
View 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,
};
}
}

View File

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

View File

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