mirror of
https://github.com/samuelthomas2774/nxapi.git
synced 2026-03-21 18:04:10 -05:00
Add parental controls
This commit is contained in:
parent
1b71e8b8fe
commit
3919ceeb21
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"files.associations": {
|
||||
"**/data/*": "json"
|
||||
"**/data/persist/*": "json"
|
||||
},
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
|
|
|
|||
36
package-lock.json
generated
36
package-lock.json
generated
|
|
@ -13,6 +13,7 @@
|
|||
"discord-rpc": "^4.0.1",
|
||||
"env-paths": "^3.0.0",
|
||||
"express": "^4.17.3",
|
||||
"mkdirp": "^1.0.4",
|
||||
"node-fetch": "^3.2.2",
|
||||
"node-notifier": "^10.0.1",
|
||||
"node-persist": "^3.1.0",
|
||||
|
|
@ -28,6 +29,7 @@
|
|||
"@types/debug": "^4.1.7",
|
||||
"@types/discord-rpc": "^4.0.0",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/mkdirp": "^1.0.2",
|
||||
"@types/node": "^17.0.21",
|
||||
"@types/node-notifier": "^8.0.2",
|
||||
"@types/node-persist": "^3.1.2",
|
||||
|
|
@ -142,6 +144,15 @@
|
|||
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/mkdirp": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-1.0.2.tgz",
|
||||
"integrity": "sha512-o0K1tSO0Dx5X6xlU5F1D6625FawhC3dU3iqr25lluNv/+/QIVH8RLNEiVokgIZo+mz+87w/3Mkg/VvQS+J51fQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ms": {
|
||||
"version": "0.7.31",
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
|
||||
|
|
@ -1514,6 +1525,17 @@
|
|||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
|
|
@ -2419,6 +2441,15 @@
|
|||
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/mkdirp": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-1.0.2.tgz",
|
||||
"integrity": "sha512-o0K1tSO0Dx5X6xlU5F1D6625FawhC3dU3iqr25lluNv/+/QIVH8RLNEiVokgIZo+mz+87w/3Mkg/VvQS+J51fQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/ms": {
|
||||
"version": "0.7.31",
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
|
||||
|
|
@ -3485,6 +3516,11 @@
|
|||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
"discord-rpc": "^4.0.1",
|
||||
"env-paths": "^3.0.0",
|
||||
"express": "^4.17.3",
|
||||
"mkdirp": "^1.0.4",
|
||||
"node-fetch": "^3.2.2",
|
||||
"node-notifier": "^10.0.1",
|
||||
"node-persist": "^3.1.0",
|
||||
|
|
@ -38,6 +39,7 @@
|
|||
"@types/debug": "^4.1.7",
|
||||
"@types/discord-rpc": "^4.0.0",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/mkdirp": "^1.0.2",
|
||||
"@types/node": "^17.0.21",
|
||||
"@types/node-notifier": "^8.0.2",
|
||||
"@types/node-persist": "^3.1.2",
|
||||
|
|
|
|||
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);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import * as crypto from 'crypto';
|
|||
import type { Arguments as ParentArguments } from '../cli.js';
|
||||
import { ArgumentsCamelCase, Argv, getToken, initStorage, YargsArguments } from '../util.js';
|
||||
import { getNintendoAccountSessionToken } from '../api/na.js';
|
||||
import { ZNCA_CLIENT_ID } from '../api/znc.js';
|
||||
|
||||
const debug = createDebug('cli:auth');
|
||||
|
||||
|
|
@ -31,7 +32,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
|||
const params = {
|
||||
state,
|
||||
redirect_uri: 'npf71b963c1b7b6d119://auth',
|
||||
client_id: '71b963c1b7b6d119',
|
||||
client_id: ZNCA_CLIENT_ID,
|
||||
scope: 'openid user user.birthday user.mii user.screenName',
|
||||
response_type: 'session_token_code',
|
||||
session_token_code_challenge: challenge,
|
||||
|
|
@ -71,8 +72,8 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
|||
const authorisedparams = new URLSearchParams(authorisedurl.hash.substr(1));
|
||||
debug('Redirect URL parameters', [...authorisedparams.entries()]);
|
||||
|
||||
const token = await getNintendoAccountSessionToken(
|
||||
authorisedparams.get('session_token_code')!, verifier, '71b963c1b7b6d119');
|
||||
const code = authorisedparams.get('session_token_code')!;
|
||||
const token = await getNintendoAccountSessionToken(code, verifier, ZNCA_CLIENT_ID);
|
||||
|
||||
console.log('Session token', token);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import createDebug from 'debug';
|
|||
import Table from 'cli-table/lib/index.js';
|
||||
import { PresenceState } from '../api/znc-types.js';
|
||||
import type { Arguments as ParentArguments } from '../cli.js';
|
||||
import { ArgumentsCamelCase, Argv, getToken, initStorage, YargsArguments } from '../util.js';
|
||||
import { ArgumentsCamelCase, Argv, getToken, hrduration, initStorage, YargsArguments } from '../util.js';
|
||||
|
||||
const debug = createDebug('cli:friends');
|
||||
|
||||
|
|
@ -67,8 +67,6 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
|||
for (const friend of friends.result.friends) {
|
||||
const online = friend.presence.state === PresenceState.ONLINE ||
|
||||
friend.presence.state === PresenceState.PLAYING;
|
||||
const hours = 'name' in friend.presence.game ? Math.floor(friend.presence.game.totalPlayTime / 60) : 0;
|
||||
const minutes = 'name' in friend.presence.game ? friend.presence.game.totalPlayTime - (hours * 60) : 0;
|
||||
|
||||
table.push([
|
||||
friend.id,
|
||||
|
|
@ -76,10 +74,9 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
|||
friend.name,
|
||||
online ?
|
||||
'name' in friend.presence.game ?
|
||||
'Playing ' + friend.presence.game.name +
|
||||
'; played for ' + (hours || !minutes ? hours + ' hour' + (hours === 1 ? '' : 's') : '') +
|
||||
(minutes ? ', ' + minutes + ' minute' + (minutes === 1 ? '' : 's'): '') +
|
||||
' since ' + new Date(friend.presence.game.firstPlayedAt * 1000).toLocaleDateString('en-GB') :
|
||||
'Playing ' + friend.presence.game.name + '; played for ' +
|
||||
hrduration(friend.presence.game.totalPlayTime) + ' since ' +
|
||||
new Date(friend.presence.game.firstPlayedAt * 1000).toLocaleDateString('en-GB') :
|
||||
'Online' :
|
||||
friend.presence.logoutAt ?
|
||||
'Last seen ' + new Date(friend.presence.logoutAt * 1000).toISOString() :
|
||||
|
|
|
|||
|
|
@ -9,3 +9,4 @@ export * as friends from './friends.js';
|
|||
export * as presence from './presence.js';
|
||||
export * as notify from './notify.js';
|
||||
export * as httpServer from './http-server.js';
|
||||
export * as pctl from './pctl.js';
|
||||
|
|
|
|||
16
src/cli/pctl.ts
Normal file
16
src/cli/pctl.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../cli.js';
|
||||
import { Argv } from '../util.js';
|
||||
import * as commands from './pctl/index.js';
|
||||
|
||||
const debug = createDebug('cli:pctl');
|
||||
|
||||
export const command = 'pctl <command>';
|
||||
export const desc = 'Nintendo Switch Parental Controls';
|
||||
|
||||
export function builder(yargs: Argv<ParentArguments>) {
|
||||
for (const command of Object.values(commands)) {
|
||||
// @ts-expect-error
|
||||
yargs.command(command);
|
||||
}
|
||||
}
|
||||
114
src/cli/pctl/auth.ts
Normal file
114
src/cli/pctl/auth.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import * as util from 'util';
|
||||
import createDebug from 'debug';
|
||||
import * as crypto from 'crypto';
|
||||
import type { Arguments as ParentArguments } from '../../cli.js';
|
||||
import { ArgumentsCamelCase, Argv, getPctlToken, initStorage, YargsArguments } from '../../util.js';
|
||||
import { getNintendoAccountSessionToken } from '../../api/na.js';
|
||||
import { MOON_CLIENT_ID } from '../../api/moon.js';
|
||||
|
||||
const debug = createDebug('cli:pctl:auth');
|
||||
|
||||
export const command = 'auth';
|
||||
export const desc = 'Generate a link to login to a Nintendo Account';
|
||||
|
||||
export function builder(yargs: Argv<ParentArguments>) {
|
||||
return yargs.option('auth', {
|
||||
describe: 'Authenticate immediately',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
}).option('select', {
|
||||
describe: 'Set as default user (default: true if only user)',
|
||||
type: 'boolean',
|
||||
});
|
||||
}
|
||||
|
||||
type Arguments = YargsArguments<ReturnType<typeof builder>>;
|
||||
|
||||
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
const state = crypto.randomBytes(36).toString('base64url');
|
||||
const verifier = crypto.randomBytes(32).toString('base64url');
|
||||
const challenge = crypto.createHash('sha256').update(verifier).digest().toString('base64url');
|
||||
|
||||
const params = {
|
||||
state,
|
||||
redirect_uri: 'npf54789befb391a838://auth',
|
||||
client_id: MOON_CLIENT_ID,
|
||||
scope: [
|
||||
'openid',
|
||||
'user',
|
||||
'user.mii',
|
||||
'moonUser:administration',
|
||||
'moonDevice:create',
|
||||
'moonOwnedDevice:administration',
|
||||
'moonParentalControlSetting',
|
||||
'moonParentalControlSetting:update',
|
||||
'moonParentalControlSettingState',
|
||||
'moonPairingState',
|
||||
'moonSmartDevice:administration',
|
||||
'moonDailySummary',
|
||||
'moonMonthlySummary',
|
||||
].join(' '),
|
||||
response_type: 'session_token_code',
|
||||
session_token_code_challenge: challenge,
|
||||
session_token_code_challenge_method: 'S256',
|
||||
};
|
||||
|
||||
const authoriseurl = 'https://accounts.nintendo.com/connect/1.0.0/authorize?' +
|
||||
new URLSearchParams(params).toString();
|
||||
|
||||
debug('Authentication parameters', {
|
||||
state,
|
||||
verifier,
|
||||
challenge,
|
||||
}, params);
|
||||
|
||||
console.log('1. Open this URL and login to your Nintendo Account:');
|
||||
console.log('');
|
||||
console.log(authoriseurl);
|
||||
console.log('');
|
||||
|
||||
console.log('2. On the "Linking an External Account" page, right click "Select this person" and copy the link. It should start with "npf54789befb391a838://auth".');
|
||||
console.log('');
|
||||
|
||||
const read = await import('read');
|
||||
// @ts-expect-error
|
||||
const prompt = util.promisify(read.default as typeof read);
|
||||
|
||||
const applink = await prompt({
|
||||
prompt: `Paste the link: `,
|
||||
// silent: true,
|
||||
output: process.stderr,
|
||||
});
|
||||
|
||||
console.log('');
|
||||
|
||||
const authorisedurl = new URL(applink);
|
||||
const authorisedparams = new URLSearchParams(authorisedurl.hash.substr(1));
|
||||
debug('Redirect URL parameters', [...authorisedparams.entries()]);
|
||||
|
||||
const code = authorisedparams.get('session_token_code')!;
|
||||
const token = await getNintendoAccountSessionToken(code, verifier, MOON_CLIENT_ID);
|
||||
|
||||
console.log('Session token', token);
|
||||
|
||||
if (argv.auth) {
|
||||
const storage = await initStorage(argv.dataPath);
|
||||
|
||||
const {moon, data} = await getPctlToken(storage, token.session_token);
|
||||
|
||||
console.log('Authenticated as Nintendo Account %s (%s)',
|
||||
data.user.nickname, data.user.id);
|
||||
|
||||
await storage.setItem('NintendoAccountToken-pctl.' + data.user.id, token.session_token);
|
||||
|
||||
const users = new Set(await storage.getItem('NintendoAccountIds') ?? []);
|
||||
users.add(data.user.id);
|
||||
await storage.setItem('NintendoAccountIds', [...users]);
|
||||
|
||||
if ('select' in argv ? argv.select : users.size === 1) {
|
||||
await storage.setItem('SelectedUser', data.user.id);
|
||||
|
||||
console.log('Set as default user');
|
||||
}
|
||||
}
|
||||
}
|
||||
79
src/cli/pctl/daily-summaries.ts
Normal file
79
src/cli/pctl/daily-summaries.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import createDebug from 'debug';
|
||||
// @ts-expect-error
|
||||
import Table from 'cli-table/lib/index.js';
|
||||
import type { Arguments as ParentArguments } from '../../cli.js';
|
||||
import { ArgumentsCamelCase, Argv, getPctlToken, hrduration, initStorage, YargsArguments } from '../../util.js';
|
||||
|
||||
const debug = createDebug('cli:pctl:daily-summaries');
|
||||
|
||||
export const command = 'daily-summaries <device>';
|
||||
export const desc = 'Show daily summaries';
|
||||
|
||||
export function builder(yargs: Argv<ParentArguments>) {
|
||||
return yargs.positional('device', {
|
||||
describe: 'Nintendo Switch device ID',
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
}).option('user', {
|
||||
describe: 'Nintendo Account ID',
|
||||
type: 'string',
|
||||
}).option('token', {
|
||||
describe: 'Nintendo Account session token',
|
||||
type: 'string',
|
||||
}).option('json', {
|
||||
describe: 'Output raw JSON',
|
||||
type: 'boolean',
|
||||
}).option('json-pretty-print', {
|
||||
describe: 'Output pretty-printed JSON',
|
||||
type: 'boolean',
|
||||
});
|
||||
}
|
||||
|
||||
type Arguments = YargsArguments<ReturnType<typeof builder>>;
|
||||
|
||||
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
const storage = await initStorage(argv.dataPath);
|
||||
|
||||
const usernsid = argv.user ?? await storage.getItem('SelectedUser');
|
||||
const token: string = argv.token ||
|
||||
await storage.getItem('NintendoAccountToken-pctl.' + usernsid);
|
||||
const {moon, data} = await getPctlToken(storage, token);
|
||||
|
||||
const summaries = await moon.getDailySummaries(argv.device);
|
||||
|
||||
if (argv.jsonPrettyPrint) {
|
||||
console.log(JSON.stringify(summaries, null, 4));
|
||||
return;
|
||||
}
|
||||
if (argv.json) {
|
||||
console.log(JSON.stringify(summaries));
|
||||
return;
|
||||
}
|
||||
|
||||
const table = new Table({
|
||||
head: [
|
||||
'Date',
|
||||
'Status',
|
||||
'Play time',
|
||||
'Misc. time',
|
||||
'Titles played',
|
||||
'Users played',
|
||||
'Notices',
|
||||
],
|
||||
});
|
||||
|
||||
for (const summary of summaries.items) {
|
||||
table.push([
|
||||
summary.date,
|
||||
summary.result,
|
||||
hrduration(summary.playingTime / 60, true),
|
||||
hrduration(summary.miscTime / 60, true),
|
||||
summary.playedApps.map(t => t.title).join('\n'),
|
||||
summary.devicePlayers.map(p => p.nickname)
|
||||
.concat(summary.anonymousPlayer ? ['Unknown user'] : []).join('\n'),
|
||||
[...summary.importantInfos, ...summary.observations.map(o => o.type)].join('\n'),
|
||||
]);
|
||||
}
|
||||
|
||||
console.log(table.toString());
|
||||
}
|
||||
59
src/cli/pctl/devices.ts
Normal file
59
src/cli/pctl/devices.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import createDebug from 'debug';
|
||||
// @ts-expect-error
|
||||
import Table from 'cli-table/lib/index.js';
|
||||
import type { Arguments as ParentArguments } from '../../cli.js';
|
||||
import { ArgumentsCamelCase, Argv, getPctlToken, initStorage, YargsArguments } from '../../util.js';
|
||||
|
||||
const debug = createDebug('cli:pctl:devices');
|
||||
|
||||
export const command = 'devices';
|
||||
export const desc = 'List Nintendo Switch consoles';
|
||||
|
||||
export function builder(yargs: Argv<ParentArguments>) {
|
||||
return yargs.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>) {
|
||||
console.warn('Listing devices');
|
||||
|
||||
const storage = await initStorage(argv.dataPath);
|
||||
|
||||
const usernsid = argv.user ?? await storage.getItem('SelectedUser');
|
||||
const token: string = argv.token ||
|
||||
await storage.getItem('NintendoAccountToken-pctl.' + usernsid);
|
||||
const {moon, data} = await getPctlToken(storage, token);
|
||||
|
||||
const devices = await moon.getDevices();
|
||||
|
||||
const table = new Table({
|
||||
head: [
|
||||
'ID',
|
||||
'Label',
|
||||
'Serial number',
|
||||
'Software version',
|
||||
'PIN',
|
||||
'Last synchronised',
|
||||
],
|
||||
});
|
||||
|
||||
for (const device of devices.items) {
|
||||
table.push([
|
||||
device.deviceId,
|
||||
device.label,
|
||||
device.device.serialNumber,
|
||||
device.device.firmwareVersion.displayedVersion + ' (' + device.device.firmwareVersion.internalVersion + ')',
|
||||
device.device.synchronizedUnlockCode,
|
||||
new Date(device.device.synchronizedParentalControlSetting.synchronizedAt * 1000).toISOString(),
|
||||
]);
|
||||
}
|
||||
|
||||
console.log(table.toString());
|
||||
}
|
||||
95
src/cli/pctl/dump-summaries.ts
Normal file
95
src/cli/pctl/dump-summaries.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import createDebug from 'debug';
|
||||
import * as fs from 'fs/promises';
|
||||
import mkdirp from 'mkdirp';
|
||||
import type { Arguments as ParentArguments } from '../../cli.js';
|
||||
import { ArgumentsCamelCase, Argv, getPctlToken, initStorage, YargsArguments } from '../../util.js';
|
||||
import * as path from 'path';
|
||||
import { DailySummaryResult } from '../../api/moon-types.js';
|
||||
import MoonApi from '../../api/moon.js';
|
||||
|
||||
const debug = createDebug('cli:pctl:dump-summaries');
|
||||
|
||||
export const command = 'dump-summaries <directory>';
|
||||
export const desc = 'Download all daily and monthly summaries';
|
||||
|
||||
export function builder(yargs: Argv<ParentArguments>) {
|
||||
return yargs.positional('directory', {
|
||||
describe: 'Directory to write summary data to',
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
}).option('user', {
|
||||
describe: 'Nintendo Account ID',
|
||||
type: 'string',
|
||||
}).option('token', {
|
||||
describe: 'Nintendo Account session token',
|
||||
type: 'string',
|
||||
}).option('device', {
|
||||
describe: 'Nintendo Switch device ID',
|
||||
type: 'array',
|
||||
});
|
||||
}
|
||||
|
||||
type Arguments = YargsArguments<ReturnType<typeof builder>>;
|
||||
|
||||
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
const storage = await initStorage(argv.dataPath);
|
||||
|
||||
const usernsid = argv.user ?? await storage.getItem('SelectedUser');
|
||||
const token: string = argv.token ||
|
||||
await storage.getItem('NintendoAccountToken-pctl.' + usernsid);
|
||||
const {moon, data} = await getPctlToken(storage, token);
|
||||
|
||||
await mkdirp(argv.directory);
|
||||
|
||||
const devices = await moon.getDevices();
|
||||
|
||||
for (const device of argv.device ?? devices.items.map(d => d.deviceId)) {
|
||||
console.warn('Downloading summaries for device %s', device);
|
||||
|
||||
await dumpMonthlySummariesForDevice(moon, argv.directory, '' + device);
|
||||
await dumpDailySummariesForDevice(moon, argv.directory, '' + device);
|
||||
}
|
||||
}
|
||||
|
||||
async function dumpMonthlySummariesForDevice(moon: MoonApi, directory: string, device: string) {
|
||||
debug('Fetching monthly summaries for device %s', device);
|
||||
const monthlySummaries = await moon.getMonthlySummaries(device);
|
||||
|
||||
for (const item of monthlySummaries.items) {
|
||||
const filename = 'pctl-monthly-' + item.deviceId + '-' + item.month + '.json';
|
||||
const file = path.join(directory, filename);
|
||||
|
||||
try {
|
||||
await fs.stat(file);
|
||||
debug('Skipping monthly summary %s for device %s, file already exists', item.month, item.deviceId);
|
||||
continue;
|
||||
} catch (err) {}
|
||||
|
||||
debug('Fetching monthly summary %s for device %s', item.month, item.deviceId);
|
||||
const summary = await moon.getMonthlySummary(item.deviceId, item.month);
|
||||
|
||||
debug('Writing %s', filename);
|
||||
await fs.writeFile(file, JSON.stringify(summary, null, 4) + '\n', 'utf-8');
|
||||
}
|
||||
}
|
||||
|
||||
async function dumpDailySummariesForDevice(moon: MoonApi, directory: string, device: string) {
|
||||
debug('Fetching daily summaries for device %s', device);
|
||||
const summaries = await moon.getDailySummaries(device);
|
||||
const timestamp = Date.now();
|
||||
|
||||
for (const summary of summaries.items) {
|
||||
const filename = 'pctl-daily-' + summary.deviceId + '-' + summary.date +
|
||||
(summary.result === DailySummaryResult.ACHIEVED ? '' : '-' + timestamp) + '.json';
|
||||
const file = path.join(directory, filename);
|
||||
|
||||
try {
|
||||
await fs.stat(file);
|
||||
debug('Skipping daily summary %s for device %s, file already exists', summary.date, summary.deviceId);
|
||||
continue;
|
||||
} catch (err) {}
|
||||
|
||||
debug('Writing %s', filename);
|
||||
await fs.writeFile(file, JSON.stringify(summary, null, 4) + '\n', 'utf-8');
|
||||
}
|
||||
}
|
||||
8
src/cli/pctl/index.ts
Normal file
8
src/cli/pctl/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export * as token from './token.js';
|
||||
export * as auth from './auth.js';
|
||||
export * as devices from './devices.js';
|
||||
export * as dailySummaries from './daily-summaries.js';
|
||||
export * as monthlySummaries from './monthly-summaries.js';
|
||||
export * as monthlySummary from './monthly-summary.js';
|
||||
export * as settings from './settings.js';
|
||||
export * as dumpSummaries from './dump-summaries.js';
|
||||
51
src/cli/pctl/monthly-summaries.ts
Normal file
51
src/cli/pctl/monthly-summaries.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import createDebug from 'debug';
|
||||
// @ts-expect-error
|
||||
import Table from 'cli-table/lib/index.js';
|
||||
import type { Arguments as ParentArguments } from '../../cli.js';
|
||||
import { ArgumentsCamelCase, Argv, getPctlToken, initStorage, YargsArguments } from '../../util.js';
|
||||
|
||||
const debug = createDebug('cli:pctl:monthly-summaries');
|
||||
|
||||
export const command = 'monthly-summaries <device>';
|
||||
export const desc = 'List monthly summaries';
|
||||
|
||||
export function builder(yargs: Argv<ParentArguments>) {
|
||||
return yargs.positional('device', {
|
||||
describe: 'Nintendo Switch device ID',
|
||||
type: 'string',
|
||||
demandOption: 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>) {
|
||||
const storage = await initStorage(argv.dataPath);
|
||||
|
||||
const usernsid = argv.user ?? await storage.getItem('SelectedUser');
|
||||
const token: string = argv.token ||
|
||||
await storage.getItem('NintendoAccountToken-pctl.' + usernsid);
|
||||
const {moon, data} = await getPctlToken(storage, token);
|
||||
|
||||
const summaries = await moon.getMonthlySummaries(argv.device);
|
||||
|
||||
const table = new Table({
|
||||
head: [
|
||||
'Month',
|
||||
],
|
||||
});
|
||||
|
||||
for (const summary of summaries.items) {
|
||||
table.push([
|
||||
summary.month,
|
||||
]);
|
||||
}
|
||||
|
||||
console.log(table.toString());
|
||||
}
|
||||
76
src/cli/pctl/monthly-summary.ts
Normal file
76
src/cli/pctl/monthly-summary.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import createDebug from 'debug';
|
||||
// @ts-expect-error
|
||||
import Table from 'cli-table/lib/index.js';
|
||||
import type { Arguments as ParentArguments } from '../../cli.js';
|
||||
import { ArgumentsCamelCase, Argv, getPctlToken, initStorage, YargsArguments } from '../../util.js';
|
||||
|
||||
const debug = createDebug('cli:pctl:monthly-summary');
|
||||
|
||||
export const command = 'monthly-summary <device> <month>';
|
||||
export const desc = 'Show monthly summary data';
|
||||
|
||||
export function builder(yargs: Argv<ParentArguments>) {
|
||||
return yargs.positional('device', {
|
||||
describe: 'Nintendo Switch device ID',
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
}).positional('month', {
|
||||
describe: 'Report month',
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
}).option('user', {
|
||||
describe: 'Nintendo Account ID',
|
||||
type: 'string',
|
||||
}).option('token', {
|
||||
describe: 'Nintendo Account session token',
|
||||
type: 'string',
|
||||
}).option('json', {
|
||||
describe: 'Output raw JSON',
|
||||
type: 'boolean',
|
||||
}).option('json-pretty-print', {
|
||||
describe: 'Output pretty-printed JSON',
|
||||
type: 'boolean',
|
||||
});
|
||||
}
|
||||
|
||||
type Arguments = YargsArguments<ReturnType<typeof builder>>;
|
||||
|
||||
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
const storage = await initStorage(argv.dataPath);
|
||||
|
||||
const usernsid = argv.user ?? await storage.getItem('SelectedUser');
|
||||
const token: string = argv.token ||
|
||||
await storage.getItem('NintendoAccountToken-pctl.' + usernsid);
|
||||
const {moon, data} = await getPctlToken(storage, token);
|
||||
|
||||
const summary = await moon.getMonthlySummary(argv.device, argv.month);
|
||||
|
||||
if (argv.jsonPrettyPrint) {
|
||||
console.log(JSON.stringify(summary, null, 4));
|
||||
return;
|
||||
}
|
||||
if (argv.json) {
|
||||
console.log(JSON.stringify(summary));
|
||||
return;
|
||||
}
|
||||
|
||||
const titles = new Table({
|
||||
head: [
|
||||
'Title',
|
||||
'First played',
|
||||
'Days',
|
||||
'Ranking',
|
||||
],
|
||||
});
|
||||
|
||||
for (const title of summary.playedApps) {
|
||||
titles.push([
|
||||
title.title,
|
||||
title.firstPlayDate,
|
||||
title.playingDays,
|
||||
title.position,
|
||||
]);
|
||||
}
|
||||
|
||||
console.log(titles.toString());
|
||||
}
|
||||
37
src/cli/pctl/settings.ts
Normal file
37
src/cli/pctl/settings.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../../cli.js';
|
||||
import { ArgumentsCamelCase, Argv, getPctlToken, initStorage, YargsArguments } from '../../util.js';
|
||||
|
||||
const debug = createDebug('cli:pctl:settings');
|
||||
|
||||
export const command = 'settings <device>';
|
||||
export const desc = 'Show parental control setting state';
|
||||
|
||||
export function builder(yargs: Argv<ParentArguments>) {
|
||||
return yargs.positional('device', {
|
||||
describe: 'Nintendo Switch device ID',
|
||||
type: 'string',
|
||||
demandOption: 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>) {
|
||||
const storage = await initStorage(argv.dataPath);
|
||||
|
||||
const usernsid = argv.user ?? await storage.getItem('SelectedUser');
|
||||
const token: string = argv.token ||
|
||||
await storage.getItem('NintendoAccountToken-pctl.' + usernsid);
|
||||
const {moon, data} = await getPctlToken(storage, token);
|
||||
|
||||
const d = await moon.getParentalControlSettingState(argv.device);
|
||||
|
||||
console.log(d);
|
||||
}
|
||||
56
src/cli/pctl/token.ts
Normal file
56
src/cli/pctl/token.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import * as util from 'util';
|
||||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../../cli.js';
|
||||
import { ArgumentsCamelCase, Argv, getPctlToken, initStorage, YargsArguments } from '../../util.js';
|
||||
|
||||
const debug = createDebug('cli:pctl:token');
|
||||
|
||||
export const command = 'token [token]';
|
||||
export const desc = 'Authenticate with a Nintendo Account session token';
|
||||
|
||||
export function builder(yargs: Argv<ParentArguments>) {
|
||||
return yargs.positional('token', {
|
||||
describe: 'Nintendo Account session token (it is recommended this is not set and you enter it interactively)',
|
||||
type: 'string',
|
||||
}).option('select', {
|
||||
describe: 'Set as default user (default: true if only user)',
|
||||
type: 'boolean',
|
||||
});
|
||||
}
|
||||
|
||||
type Arguments = YargsArguments<ReturnType<typeof builder>>;
|
||||
|
||||
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
const storage = await initStorage(argv.dataPath);
|
||||
|
||||
if (!argv.token) {
|
||||
const read = await import('read');
|
||||
// @ts-expect-error
|
||||
const prompt = util.promisify(read.default as typeof read);
|
||||
|
||||
argv.token = await prompt({
|
||||
prompt: `Token: `,
|
||||
silent: true,
|
||||
output: process.stderr,
|
||||
});
|
||||
}
|
||||
|
||||
const {moon, data} = await getPctlToken(storage, argv.token);
|
||||
|
||||
console.warn('Authenticated as Nintendo Account %s (%s)',
|
||||
data.user.nickname, data.user.id);
|
||||
|
||||
await storage.setItem('NintendoAccountToken-pctl.' + data.user.id, argv.token);
|
||||
|
||||
const users = new Set(await storage.getItem('NintendoAccountIds') ?? []);
|
||||
users.add(data.user.id);
|
||||
await storage.setItem('NintendoAccountIds', [...users]);
|
||||
|
||||
console.log('Saved token');
|
||||
|
||||
if ('select' in argv ? argv.select : users.size === 1) {
|
||||
await storage.setItem('SelectedUser', data.user.id);
|
||||
|
||||
console.log('Set as default user');
|
||||
}
|
||||
}
|
||||
31
src/cli/pctl/user.ts
Normal file
31
src/cli/pctl/user.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../../cli.js';
|
||||
import { ArgumentsCamelCase, Argv, getPctlToken, initStorage, YargsArguments } from '../../util.js';
|
||||
|
||||
const debug = createDebug('cli:pctl:user');
|
||||
|
||||
export const command = 'user';
|
||||
export const desc = 'Get the authenticated Nintendo Account';
|
||||
|
||||
export function builder(yargs: Argv<ParentArguments>) {
|
||||
return yargs.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>) {
|
||||
const storage = await initStorage(argv.dataPath);
|
||||
|
||||
const usernsid = argv.user ?? await storage.getItem('SelectedUser');
|
||||
const token: string = argv.token ||
|
||||
await storage.getItem('NintendoAccountToken-pctl.' + usernsid);
|
||||
const {moon, data} = await getPctlToken(storage, token);
|
||||
|
||||
console.log('Nintendo Account', data.user);
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import createDebug from 'debug';
|
|||
// @ts-expect-error
|
||||
import Table from 'cli-table/lib/index.js';
|
||||
import type { Arguments as ParentArguments } from '../cli.js';
|
||||
import { Argv, initStorage, SavedToken } from '../util.js';
|
||||
import { Argv, initStorage, SavedMoonToken, SavedToken } from '../util.js';
|
||||
|
||||
const debug = createDebug('cli:users');
|
||||
|
||||
|
|
@ -24,22 +24,27 @@ export function builder(yargs: Argv<ParentArguments>) {
|
|||
'Country',
|
||||
'Nintendo Switch ID',
|
||||
'Nintendo Switch username',
|
||||
'Parental Controls',
|
||||
],
|
||||
});
|
||||
|
||||
for (const userid of users ?? []) {
|
||||
const token: string | undefined = await storage.getItem('NintendoAccountToken.' + userid);
|
||||
if (!token) continue;
|
||||
const cache: SavedToken | undefined = await storage.getItem('NsoToken.' + token);
|
||||
if (!cache) continue;
|
||||
const nsoCache: SavedToken | undefined = token ? await storage.getItem('NsoToken.' + token) : undefined;
|
||||
const moonToken: string | undefined = await storage.getItem('NintendoAccountToken-pctl.' + userid);
|
||||
const moonCache: SavedMoonToken | undefined = moonToken ? await storage.getItem('MoonToken.' + moonToken) : undefined;
|
||||
|
||||
const user = nsoCache?.user ?? moonCache?.user;
|
||||
if (!user) continue;
|
||||
|
||||
table.push([
|
||||
cache.user.id + (selected === cache.user.id ? ' *' : ''),
|
||||
cache.user.screenName,
|
||||
cache.user.nickname,
|
||||
cache.user.country,
|
||||
cache.nsoAccount.user.nsaId,
|
||||
cache.nsoAccount.user.name,
|
||||
user.id + (selected === user.id ? ' *' : ''),
|
||||
user.screenName,
|
||||
user.nickname,
|
||||
user.country,
|
||||
nsoCache?.nsoAccount.user.nsaId ?? 'Not signed in',
|
||||
nsoCache?.nsoAccount.user.name ?? 'Not signed in',
|
||||
moonCache ? 'Signed in' : 'Not signed in',
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
70
src/util.ts
70
src/util.ts
|
|
@ -1,3 +1,4 @@
|
|||
import * as path from 'path';
|
||||
import * as yargs from 'yargs';
|
||||
import type * as yargstypes from '../node_modules/@types/yargs/index.js';
|
||||
import createDebug from 'debug';
|
||||
|
|
@ -10,6 +11,7 @@ import { AccountLogin, CurrentUser, Game } from './api/znc-types.js';
|
|||
import ZncApi from './api/znc.js';
|
||||
import titles, { defaultTitle } from './titles.js';
|
||||
import ZncProxyApi from './api/znc-proxy.js';
|
||||
import MoonApi from './api/moon.js';
|
||||
|
||||
const debug = createDebug('cli');
|
||||
|
||||
|
|
@ -32,9 +34,16 @@ export interface SavedToken {
|
|||
proxy_url?: string;
|
||||
}
|
||||
|
||||
export interface SavedMoonToken {
|
||||
nintendoAccountToken: NintendoAccountToken;
|
||||
user: NintendoAccountUser;
|
||||
|
||||
expires_at: number;
|
||||
}
|
||||
|
||||
export async function initStorage(dir: string) {
|
||||
const storage = persist.create({
|
||||
dir,
|
||||
dir: path.join(dir, 'persist'),
|
||||
stringify: data => JSON.stringify(data, null, 4) + '\n',
|
||||
});
|
||||
await storage.init();
|
||||
|
|
@ -79,6 +88,40 @@ export async function getToken(storage: persist.LocalStorage, token: string, pro
|
|||
};
|
||||
}
|
||||
|
||||
export async function getPctlToken(storage: persist.LocalStorage, token: string) {
|
||||
if (!token) {
|
||||
console.error('No token set. Set a Nintendo Account session token using the `--token` option or by running `nintendo-znc pctl users add`.');
|
||||
throw new Error('Invalid token');
|
||||
}
|
||||
|
||||
const existingToken: SavedMoonToken | undefined = await storage.getItem('MoonToken.' + token);
|
||||
|
||||
if (!existingToken || existingToken.expires_at <= Date.now()) {
|
||||
console.warn('Authenticating to Nintendo Switch Parental Controls app');
|
||||
debug('Authenticating to pctl with session token');
|
||||
|
||||
const {moon, data} = await MoonApi.createWithSessionToken(token);
|
||||
|
||||
const existingToken: SavedMoonToken = {
|
||||
...data,
|
||||
expires_at: Date.now() + (data.nintendoAccountToken.expires_in * 1000),
|
||||
};
|
||||
|
||||
await storage.setItem('MoonToken.' + token, existingToken);
|
||||
await storage.setItem('NintendoAccountToken-pctl.' + data.user.id, token);
|
||||
|
||||
return {moon, data: existingToken};
|
||||
}
|
||||
|
||||
debug('Using existing token');
|
||||
await storage.setItem('NintendoAccountToken-pctl.' + existingToken.user.id, token);
|
||||
|
||||
return {
|
||||
moon: new MoonApi(existingToken.nintendoAccountToken.access_token!, existingToken.user.id),
|
||||
data: existingToken,
|
||||
};
|
||||
}
|
||||
|
||||
export function getTitleIdFromEcUrl(url: string) {
|
||||
const match = url.match(/^https:\/\/ec\.nintendo\.com\/apps\/([0-9a-f]{16})\//);
|
||||
return match?.[1] ?? null;
|
||||
|
|
@ -93,9 +136,6 @@ export function getDiscordPresence(game: Game, friendcode?: CurrentUser['links']
|
|||
const titleid = getTitleIdFromEcUrl(game.shopUri);
|
||||
const title = titles.find(t => t.id === titleid) || defaultTitle;
|
||||
|
||||
const hours = Math.floor(game.totalPlayTime / 60);
|
||||
const minutes = game.totalPlayTime - (hours * 60);
|
||||
|
||||
const text = [];
|
||||
|
||||
if (title.titleName === true) text.push(game.name);
|
||||
|
|
@ -103,9 +143,10 @@ export function getDiscordPresence(game: Game, friendcode?: CurrentUser['links']
|
|||
|
||||
if (game.sysDescription) text.push(game.sysDescription);
|
||||
|
||||
if (hours >= 1) text.push('Played for ' + hours + ' hour' + (hours === 1 ? '' : 's') +
|
||||
(minutes ? ', ' + minutes + ' minute' + (minutes === 1 ? '' : 's'): '') +
|
||||
' since ' + new Date(game.firstPlayedAt * 1000).toLocaleDateString('en-GB'));
|
||||
if (game.totalPlayTime >= 60) {
|
||||
text.push('Played for ' + hrduration(game.totalPlayTime) +
|
||||
' since ' + new Date(game.firstPlayedAt * 1000).toLocaleDateString('en-GB'));
|
||||
}
|
||||
|
||||
return {
|
||||
id: title.client || defaultTitle.client,
|
||||
|
|
@ -138,3 +179,18 @@ export interface Title {
|
|||
smallImageKey?: string;
|
||||
showTimestamp?: boolean;
|
||||
}
|
||||
|
||||
export function hrduration(duration: number, short = false) {
|
||||
const hours = Math.floor(duration / 60);
|
||||
const minutes = duration - (hours * 60);
|
||||
|
||||
const hour_str = short ? 'hr' : 'hour';
|
||||
const minute_str = short ? 'min' : 'minute';
|
||||
|
||||
if (hours >= 1) {
|
||||
return hours + ' ' + hour_str + (hours === 1 ? '' : 's') +
|
||||
(minutes ? ', ' + minutes + ' ' + minute_str + (minutes === 1 ? '' : 's') : '');
|
||||
} else {
|
||||
return minutes + ' ' + minute_str + (minutes === 1 ? '' : 's');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user