mirror of
https://github.com/samuelthomas2774/nxapi.git
synced 2026-04-26 08:18:59 -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": {
|
"files.associations": {
|
||||||
"**/data/*": "json"
|
"**/data/persist/*": "json"
|
||||||
},
|
},
|
||||||
"typescript.tsdk": "node_modules/typescript/lib"
|
"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",
|
"discord-rpc": "^4.0.1",
|
||||||
"env-paths": "^3.0.0",
|
"env-paths": "^3.0.0",
|
||||||
"express": "^4.17.3",
|
"express": "^4.17.3",
|
||||||
|
"mkdirp": "^1.0.4",
|
||||||
"node-fetch": "^3.2.2",
|
"node-fetch": "^3.2.2",
|
||||||
"node-notifier": "^10.0.1",
|
"node-notifier": "^10.0.1",
|
||||||
"node-persist": "^3.1.0",
|
"node-persist": "^3.1.0",
|
||||||
|
|
@ -28,6 +29,7 @@
|
||||||
"@types/debug": "^4.1.7",
|
"@types/debug": "^4.1.7",
|
||||||
"@types/discord-rpc": "^4.0.0",
|
"@types/discord-rpc": "^4.0.0",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
|
"@types/mkdirp": "^1.0.2",
|
||||||
"@types/node": "^17.0.21",
|
"@types/node": "^17.0.21",
|
||||||
"@types/node-notifier": "^8.0.2",
|
"@types/node-notifier": "^8.0.2",
|
||||||
"@types/node-persist": "^3.1.2",
|
"@types/node-persist": "^3.1.2",
|
||||||
|
|
@ -142,6 +144,15 @@
|
||||||
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
|
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/@types/ms": {
|
||||||
"version": "0.7.31",
|
"version": "0.7.31",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
|
||||||
|
|
@ -1514,6 +1525,17 @@
|
||||||
"node": "*"
|
"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": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
|
|
@ -2419,6 +2441,15 @@
|
||||||
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
|
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
|
||||||
"dev": true
|
"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": {
|
"@types/ms": {
|
||||||
"version": "0.7.31",
|
"version": "0.7.31",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
|
||||||
|
|
@ -3485,6 +3516,11 @@
|
||||||
"brace-expansion": "^1.1.7"
|
"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": {
|
"ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
"discord-rpc": "^4.0.1",
|
"discord-rpc": "^4.0.1",
|
||||||
"env-paths": "^3.0.0",
|
"env-paths": "^3.0.0",
|
||||||
"express": "^4.17.3",
|
"express": "^4.17.3",
|
||||||
|
"mkdirp": "^1.0.4",
|
||||||
"node-fetch": "^3.2.2",
|
"node-fetch": "^3.2.2",
|
||||||
"node-notifier": "^10.0.1",
|
"node-notifier": "^10.0.1",
|
||||||
"node-persist": "^3.1.0",
|
"node-persist": "^3.1.0",
|
||||||
|
|
@ -38,6 +39,7 @@
|
||||||
"@types/debug": "^4.1.7",
|
"@types/debug": "^4.1.7",
|
||||||
"@types/discord-rpc": "^4.0.0",
|
"@types/discord-rpc": "^4.0.0",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
|
"@types/mkdirp": "^1.0.2",
|
||||||
"@types/node": "^17.0.21",
|
"@types/node": "^17.0.21",
|
||||||
"@types/node-notifier": "^8.0.2",
|
"@types/node-notifier": "^8.0.2",
|
||||||
"@types/node-persist": "^3.1.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;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getNintendoAccountToken(token: string) {
|
export async function getNintendoAccountToken(token: string, client_id: string) {
|
||||||
debug('Getting Nintendo Account token');
|
debug('Getting Nintendo Account token');
|
||||||
|
|
||||||
const response = await fetch('https://accounts.nintendo.com/connect/1.0.0/api/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)',
|
'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 8.0.0)',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
client_id: '71b963c1b7b6d119',
|
client_id,
|
||||||
session_token: token,
|
session_token: token,
|
||||||
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer-session-token',
|
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer-session-token',
|
||||||
}),
|
}),
|
||||||
|
|
@ -91,7 +91,7 @@ export interface NintendoAccountSessionToken {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NintendoAccountToken {
|
export interface NintendoAccountToken {
|
||||||
scope: ['openid', 'user', 'user.birthday', 'user.mii', 'user.screenName'];
|
scope: string[];
|
||||||
token_type: 'Bearer';
|
token_type: 'Bearer';
|
||||||
id_token: string;
|
id_token: string;
|
||||||
access_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 ZNCA_USER_AGENT = `com.nintendo.znca/${ZNCA_VERSION}(${ZNCA_PLATFORM}/${ZNCA_PLATFORM_VERSION})`;
|
||||||
|
|
||||||
const ZNC_URL = 'https://api-lp1.znc.srv.nintendo.net';
|
const ZNC_URL = 'https://api-lp1.znc.srv.nintendo.net';
|
||||||
|
export const ZNCA_CLIENT_ID = '71b963c1b7b6d119';
|
||||||
|
|
||||||
export default class ZncApi {
|
export default class ZncApi {
|
||||||
constructor(
|
constructor(
|
||||||
|
|
@ -112,7 +113,7 @@ export default class ZncApi {
|
||||||
const timestamp = '' + Math.floor(Date.now() / 1000);
|
const timestamp = '' + Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
// Nintendo Account token
|
// Nintendo Account token
|
||||||
const nintendoAccountToken = await getNintendoAccountToken(token);
|
const nintendoAccountToken = await getNintendoAccountToken(token, ZNCA_CLIENT_ID);
|
||||||
|
|
||||||
// Nintendo Account user data
|
// Nintendo Account user data
|
||||||
const user = await getNintendoAccountUser(nintendoAccountToken);
|
const user = await getNintendoAccountUser(nintendoAccountToken);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import * as crypto from 'crypto';
|
||||||
import type { Arguments as ParentArguments } from '../cli.js';
|
import type { Arguments as ParentArguments } from '../cli.js';
|
||||||
import { ArgumentsCamelCase, Argv, getToken, initStorage, YargsArguments } from '../util.js';
|
import { ArgumentsCamelCase, Argv, getToken, initStorage, YargsArguments } from '../util.js';
|
||||||
import { getNintendoAccountSessionToken } from '../api/na.js';
|
import { getNintendoAccountSessionToken } from '../api/na.js';
|
||||||
|
import { ZNCA_CLIENT_ID } from '../api/znc.js';
|
||||||
|
|
||||||
const debug = createDebug('cli:auth');
|
const debug = createDebug('cli:auth');
|
||||||
|
|
||||||
|
|
@ -31,7 +32,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||||
const params = {
|
const params = {
|
||||||
state,
|
state,
|
||||||
redirect_uri: 'npf71b963c1b7b6d119://auth',
|
redirect_uri: 'npf71b963c1b7b6d119://auth',
|
||||||
client_id: '71b963c1b7b6d119',
|
client_id: ZNCA_CLIENT_ID,
|
||||||
scope: 'openid user user.birthday user.mii user.screenName',
|
scope: 'openid user user.birthday user.mii user.screenName',
|
||||||
response_type: 'session_token_code',
|
response_type: 'session_token_code',
|
||||||
session_token_code_challenge: challenge,
|
session_token_code_challenge: challenge,
|
||||||
|
|
@ -71,8 +72,8 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||||
const authorisedparams = new URLSearchParams(authorisedurl.hash.substr(1));
|
const authorisedparams = new URLSearchParams(authorisedurl.hash.substr(1));
|
||||||
debug('Redirect URL parameters', [...authorisedparams.entries()]);
|
debug('Redirect URL parameters', [...authorisedparams.entries()]);
|
||||||
|
|
||||||
const token = await getNintendoAccountSessionToken(
|
const code = authorisedparams.get('session_token_code')!;
|
||||||
authorisedparams.get('session_token_code')!, verifier, '71b963c1b7b6d119');
|
const token = await getNintendoAccountSessionToken(code, verifier, ZNCA_CLIENT_ID);
|
||||||
|
|
||||||
console.log('Session token', token);
|
console.log('Session token', token);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import createDebug from 'debug';
|
||||||
import Table from 'cli-table/lib/index.js';
|
import Table from 'cli-table/lib/index.js';
|
||||||
import { PresenceState } from '../api/znc-types.js';
|
import { PresenceState } from '../api/znc-types.js';
|
||||||
import type { Arguments as ParentArguments } from '../cli.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');
|
const debug = createDebug('cli:friends');
|
||||||
|
|
||||||
|
|
@ -67,8 +67,6 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||||
for (const friend of friends.result.friends) {
|
for (const friend of friends.result.friends) {
|
||||||
const online = friend.presence.state === PresenceState.ONLINE ||
|
const online = friend.presence.state === PresenceState.ONLINE ||
|
||||||
friend.presence.state === PresenceState.PLAYING;
|
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([
|
table.push([
|
||||||
friend.id,
|
friend.id,
|
||||||
|
|
@ -76,10 +74,9 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||||
friend.name,
|
friend.name,
|
||||||
online ?
|
online ?
|
||||||
'name' in friend.presence.game ?
|
'name' in friend.presence.game ?
|
||||||
'Playing ' + friend.presence.game.name +
|
'Playing ' + friend.presence.game.name + '; played for ' +
|
||||||
'; played for ' + (hours || !minutes ? hours + ' hour' + (hours === 1 ? '' : 's') : '') +
|
hrduration(friend.presence.game.totalPlayTime) + ' since ' +
|
||||||
(minutes ? ', ' + minutes + ' minute' + (minutes === 1 ? '' : 's'): '') +
|
new Date(friend.presence.game.firstPlayedAt * 1000).toLocaleDateString('en-GB') :
|
||||||
' since ' + new Date(friend.presence.game.firstPlayedAt * 1000).toLocaleDateString('en-GB') :
|
|
||||||
'Online' :
|
'Online' :
|
||||||
friend.presence.logoutAt ?
|
friend.presence.logoutAt ?
|
||||||
'Last seen ' + new Date(friend.presence.logoutAt * 1000).toISOString() :
|
'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 presence from './presence.js';
|
||||||
export * as notify from './notify.js';
|
export * as notify from './notify.js';
|
||||||
export * as httpServer from './http-server.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
|
// @ts-expect-error
|
||||||
import Table from 'cli-table/lib/index.js';
|
import Table from 'cli-table/lib/index.js';
|
||||||
import type { Arguments as ParentArguments } from '../cli.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');
|
const debug = createDebug('cli:users');
|
||||||
|
|
||||||
|
|
@ -24,22 +24,27 @@ export function builder(yargs: Argv<ParentArguments>) {
|
||||||
'Country',
|
'Country',
|
||||||
'Nintendo Switch ID',
|
'Nintendo Switch ID',
|
||||||
'Nintendo Switch username',
|
'Nintendo Switch username',
|
||||||
|
'Parental Controls',
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const userid of users ?? []) {
|
for (const userid of users ?? []) {
|
||||||
const token: string | undefined = await storage.getItem('NintendoAccountToken.' + userid);
|
const token: string | undefined = await storage.getItem('NintendoAccountToken.' + userid);
|
||||||
if (!token) continue;
|
const nsoCache: SavedToken | undefined = token ? await storage.getItem('NsoToken.' + token) : undefined;
|
||||||
const cache: SavedToken | undefined = await storage.getItem('NsoToken.' + token);
|
const moonToken: string | undefined = await storage.getItem('NintendoAccountToken-pctl.' + userid);
|
||||||
if (!cache) continue;
|
const moonCache: SavedMoonToken | undefined = moonToken ? await storage.getItem('MoonToken.' + moonToken) : undefined;
|
||||||
|
|
||||||
|
const user = nsoCache?.user ?? moonCache?.user;
|
||||||
|
if (!user) continue;
|
||||||
|
|
||||||
table.push([
|
table.push([
|
||||||
cache.user.id + (selected === cache.user.id ? ' *' : ''),
|
user.id + (selected === user.id ? ' *' : ''),
|
||||||
cache.user.screenName,
|
user.screenName,
|
||||||
cache.user.nickname,
|
user.nickname,
|
||||||
cache.user.country,
|
user.country,
|
||||||
cache.nsoAccount.user.nsaId,
|
nsoCache?.nsoAccount.user.nsaId ?? 'Not signed in',
|
||||||
cache.nsoAccount.user.name,
|
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 * as yargs from 'yargs';
|
||||||
import type * as yargstypes from '../node_modules/@types/yargs/index.js';
|
import type * as yargstypes from '../node_modules/@types/yargs/index.js';
|
||||||
import createDebug from 'debug';
|
import createDebug from 'debug';
|
||||||
|
|
@ -10,6 +11,7 @@ import { AccountLogin, CurrentUser, Game } from './api/znc-types.js';
|
||||||
import ZncApi from './api/znc.js';
|
import ZncApi from './api/znc.js';
|
||||||
import titles, { defaultTitle } from './titles.js';
|
import titles, { defaultTitle } from './titles.js';
|
||||||
import ZncProxyApi from './api/znc-proxy.js';
|
import ZncProxyApi from './api/znc-proxy.js';
|
||||||
|
import MoonApi from './api/moon.js';
|
||||||
|
|
||||||
const debug = createDebug('cli');
|
const debug = createDebug('cli');
|
||||||
|
|
||||||
|
|
@ -32,9 +34,16 @@ export interface SavedToken {
|
||||||
proxy_url?: string;
|
proxy_url?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SavedMoonToken {
|
||||||
|
nintendoAccountToken: NintendoAccountToken;
|
||||||
|
user: NintendoAccountUser;
|
||||||
|
|
||||||
|
expires_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
export async function initStorage(dir: string) {
|
export async function initStorage(dir: string) {
|
||||||
const storage = persist.create({
|
const storage = persist.create({
|
||||||
dir,
|
dir: path.join(dir, 'persist'),
|
||||||
stringify: data => JSON.stringify(data, null, 4) + '\n',
|
stringify: data => JSON.stringify(data, null, 4) + '\n',
|
||||||
});
|
});
|
||||||
await storage.init();
|
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) {
|
export function getTitleIdFromEcUrl(url: string) {
|
||||||
const match = url.match(/^https:\/\/ec\.nintendo\.com\/apps\/([0-9a-f]{16})\//);
|
const match = url.match(/^https:\/\/ec\.nintendo\.com\/apps\/([0-9a-f]{16})\//);
|
||||||
return match?.[1] ?? null;
|
return match?.[1] ?? null;
|
||||||
|
|
@ -93,9 +136,6 @@ export function getDiscordPresence(game: Game, friendcode?: CurrentUser['links']
|
||||||
const titleid = getTitleIdFromEcUrl(game.shopUri);
|
const titleid = getTitleIdFromEcUrl(game.shopUri);
|
||||||
const title = titles.find(t => t.id === titleid) || defaultTitle;
|
const title = titles.find(t => t.id === titleid) || defaultTitle;
|
||||||
|
|
||||||
const hours = Math.floor(game.totalPlayTime / 60);
|
|
||||||
const minutes = game.totalPlayTime - (hours * 60);
|
|
||||||
|
|
||||||
const text = [];
|
const text = [];
|
||||||
|
|
||||||
if (title.titleName === true) text.push(game.name);
|
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 (game.sysDescription) text.push(game.sysDescription);
|
||||||
|
|
||||||
if (hours >= 1) text.push('Played for ' + hours + ' hour' + (hours === 1 ? '' : 's') +
|
if (game.totalPlayTime >= 60) {
|
||||||
(minutes ? ', ' + minutes + ' minute' + (minutes === 1 ? '' : 's'): '') +
|
text.push('Played for ' + hrduration(game.totalPlayTime) +
|
||||||
' since ' + new Date(game.firstPlayedAt * 1000).toLocaleDateString('en-GB'));
|
' since ' + new Date(game.firstPlayedAt * 1000).toLocaleDateString('en-GB'));
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: title.client || defaultTitle.client,
|
id: title.client || defaultTitle.client,
|
||||||
|
|
@ -138,3 +179,18 @@ export interface Title {
|
||||||
smallImageKey?: string;
|
smallImageKey?: string;
|
||||||
showTimestamp?: boolean;
|
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