mirror of
https://github.com/samuelthomas2774/nxapi.git
synced 2026-04-24 15:07:05 -05:00
Set environment variables from a file and add Electron app base
This commit is contained in:
parent
d548a0e37e
commit
570e3d6b29
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
|||
node_modules
|
||||
dist
|
||||
data
|
||||
.vscode/schema
|
||||
|
|
|
|||
17
.vscode/generate-schemas.sh
vendored
Executable file
17
.vscode/generate-schemas.sh
vendored
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
#!/bin/sh
|
||||
|
||||
mkdir -p .vscode/schema/{moon,splatnet2,nooklink}
|
||||
|
||||
npx ts-json-schema-generator --path src/api/moon-types.ts --type DailySummary > .vscode/schema/moon/dailysummary.schema.json
|
||||
npx ts-json-schema-generator --path src/api/moon-types.ts --type MonthlySummary > .vscode/schema/moon/monthlysummary.schema.json
|
||||
|
||||
npx ts-json-schema-generator --path src/api/splatnet2-types.ts --type Records > .vscode/schema/splatnet2/records.schema.json
|
||||
npx ts-json-schema-generator --path src/api/splatnet2-types.ts --type NicknameAndIcon > .vscode/schema/splatnet2/ni.schema.json
|
||||
npx ts-json-schema-generator --path src/api/splatnet2-types.ts --type Timeline > .vscode/schema/splatnet2/timeline.schema.json
|
||||
npx ts-json-schema-generator --path src/api/splatnet2-types.ts --type HeroRecords > .vscode/schema/splatnet2/hero.schema.json
|
||||
npx ts-json-schema-generator --path src/api/splatnet2-types.ts --type Results > .vscode/schema/splatnet2/results-summary.schema.json
|
||||
npx ts-json-schema-generator --path src/api/splatnet2-types.ts --type ResultWithPlayerNicknameAndIcons > .vscode/schema/splatnet2/result.schema.json
|
||||
npx ts-json-schema-generator --path src/api/splatnet2-types.ts --type CoopResults > .vscode/schema/splatnet2/coop-summary.schema.json
|
||||
npx ts-json-schema-generator --path src/api/splatnet2-types.ts --type CoopResultWithPlayerNicknameAndIcons > .vscode/schema/splatnet2/coop-result.schema.json
|
||||
|
||||
npx ts-json-schema-generator --path src/api/nooklink-types.ts --type Newspaper > .vscode/schema/nooklink/newspaper.schema.json
|
||||
22
.vscode/launch.json
vendored
Normal file
22
.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug Electron app",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
|
||||
},
|
||||
"args": [
|
||||
"dist/app/main/app-entry.cjs"
|
||||
],
|
||||
"outputCapture": "std"
|
||||
}
|
||||
]
|
||||
}
|
||||
50
.vscode/settings.json
vendored
50
.vscode/settings.json
vendored
|
|
@ -2,5 +2,53 @@
|
|||
"files.associations": {
|
||||
"**/data/persist/*": "json"
|
||||
},
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": ["**/pctl-daily-*.json"],
|
||||
"url": "./.vscode/schema/moon/dailysummary.schema.json"
|
||||
},
|
||||
{
|
||||
"fileMatch": ["**/pctl-monthly-*.json"],
|
||||
"url": "./.vscode/schema/moon/monthlysummary.schema.json"
|
||||
},
|
||||
|
||||
{
|
||||
"fileMatch": ["**/splatnet2-records-*.json", "!**-latest.json"],
|
||||
"url": "./.vscode/schema/splatnet2/records.schema.json"
|
||||
},
|
||||
{
|
||||
"fileMatch": ["**/splatnet2-ni-*.json"],
|
||||
"url": "./.vscode/schema/splatnet2/ni.schema.json"
|
||||
},
|
||||
{
|
||||
"fileMatch": ["**/splatnet2-timeline-*.json"],
|
||||
"url": "./.vscode/schema/splatnet2/timeline.schema.json"
|
||||
},
|
||||
{
|
||||
"fileMatch": ["**/splatnet2-hero-*.json"],
|
||||
"url": "./.vscode/schema/splatnet2/hero.schema.json"
|
||||
},
|
||||
{
|
||||
"fileMatch": ["**/splatnet2-results-summary-*.json", "!**/splatnet2-results-summary-image-*", "!**-latest.json"],
|
||||
"url": "./.vscode/schema/splatnet2/results-summary.schema.json"
|
||||
},
|
||||
{
|
||||
"fileMatch": ["**/splatnet2-result-*.json", "!**/splatnet2-result-image-*.json"],
|
||||
"url": "./.vscode/schema/splatnet2/result.schema.json"
|
||||
},
|
||||
{
|
||||
"fileMatch": ["**/splatnet2-coop-summary-*.json", "!**-latest.json"],
|
||||
"url": "./.vscode/schema/splatnet2/coop-summary.schema.json"
|
||||
},
|
||||
{
|
||||
"fileMatch": ["**/splatnet2-coop-result-*.json"],
|
||||
"url": "./.vscode/schema/splatnet2/coop-result.schema.json"
|
||||
},
|
||||
|
||||
{
|
||||
"fileMatch": ["**/nooklink-newspaper-*.json"],
|
||||
"url": "./.vscode/schema/nooklink/newspaper.schema.json"
|
||||
},
|
||||
],
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
}
|
||||
|
|
|
|||
2535
package-lock.json
generated
2535
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
|
|
@ -30,6 +30,8 @@
|
|||
"cli-table": "^0.3.11",
|
||||
"debug": "^4.3.3",
|
||||
"discord-rpc": "^4.0.1",
|
||||
"dotenv": "^16.0.0",
|
||||
"dotenv-expand": "^8.0.3",
|
||||
"env-paths": "^3.0.0",
|
||||
"express": "^4.17.3",
|
||||
"frida": "^15.1.17",
|
||||
|
|
@ -43,6 +45,12 @@
|
|||
"yargs": "^17.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-alias": "^3.1.9",
|
||||
"@rollup/plugin-commonjs": "^21.0.3",
|
||||
"@rollup/plugin-html": "^0.2.4",
|
||||
"@rollup/plugin-node-resolve": "^13.1.3",
|
||||
"@rollup/plugin-replace": "^4.0.0",
|
||||
"@rollup/plugin-typescript": "^8.3.1",
|
||||
"@types/cli-table": "^0.3.0",
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/discord-rpc": "^4.0.0",
|
||||
|
|
@ -51,10 +59,19 @@
|
|||
"@types/node": "^17.0.21",
|
||||
"@types/node-notifier": "^8.0.2",
|
||||
"@types/node-persist": "^3.1.2",
|
||||
"@types/react": "^17.0.43",
|
||||
"@types/react-native": "^0.67.3",
|
||||
"@types/read": "^0.0.29",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/yargs": "^17.0.9",
|
||||
"caxa": "^2.1.0",
|
||||
"electron": "^17.2.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-native-web": "^0.17.7",
|
||||
"rollup": "^2.70.1",
|
||||
"rollup-plugin-polyfill-node": "^0.8.0",
|
||||
"ts-json-schema-generator": "^1.0.0",
|
||||
"typescript": "^4.7.0-dev.20220308"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
67
rollup.config.js
Normal file
67
rollup.config.js
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import path from 'path';
|
||||
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import alias from '@rollup/plugin-alias';
|
||||
import nodeResolve from '@rollup/plugin-node-resolve';
|
||||
import nodePolyfill from 'rollup-plugin-polyfill-node';
|
||||
import html from '@rollup/plugin-html';
|
||||
|
||||
const preload = {
|
||||
input: 'src/app/preload/index.ts',
|
||||
output: {
|
||||
file: 'dist/app/bundle/preload.cjs',
|
||||
format: 'cjs',
|
||||
},
|
||||
plugins: [
|
||||
typescript({
|
||||
noEmit: true,
|
||||
declaration: false,
|
||||
}),
|
||||
commonjs({
|
||||
// the ".ts" extension is required
|
||||
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
||||
esmExternals: true,
|
||||
}),
|
||||
],
|
||||
external: [
|
||||
'electron',
|
||||
],
|
||||
};
|
||||
|
||||
const browser = {
|
||||
input: 'src/app/browser/index.ts',
|
||||
output: {
|
||||
file: 'dist/app/bundle/browser.js',
|
||||
format: 'es',
|
||||
},
|
||||
plugins: [
|
||||
html({
|
||||
title: 'nxapi',
|
||||
}),
|
||||
typescript({
|
||||
noEmit: true,
|
||||
declaration: false,
|
||||
}),
|
||||
commonjs({
|
||||
// the ".ts" extension is required
|
||||
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
||||
esmExternals: true,
|
||||
}),
|
||||
nodePolyfill(),
|
||||
alias({
|
||||
entries: [
|
||||
{find: 'react-native', replacement: path.resolve(__dirname, 'node_modules', 'react-native-web')},
|
||||
],
|
||||
}),
|
||||
nodeResolve({
|
||||
browser: true,
|
||||
preferBuiltins: false,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
export default [
|
||||
preload,
|
||||
browser,
|
||||
];
|
||||
|
|
@ -93,6 +93,7 @@ export interface NintendoAccountSessionTokenJwtPayload extends JwtPayload {
|
|||
jti: string;
|
||||
typ: 'session_token';
|
||||
iss: 'https://accounts.nintendo.com';
|
||||
/** Unknown - scopes the token is valid for? */
|
||||
'st:scp': number[];
|
||||
/** Subject (Nintendo Account ID) */
|
||||
sub: string;
|
||||
|
|
|
|||
65
src/app/browser/app.tsx
Normal file
65
src/app/browser/app.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import { Button, Image, StyleSheet, Text, TextProps, useColorScheme, View } from 'react-native';
|
||||
import { NintendoAccountUser } from '../../api/na.js';
|
||||
import { SavedMoonToken, SavedToken } from '../../util.js';
|
||||
import ipc from './ipc.js';
|
||||
import { useAsync } from './util.js';
|
||||
|
||||
async function getAccounts() {
|
||||
const ids = await ipc.listNintendoAccounts();
|
||||
|
||||
const accounts: {
|
||||
user: NintendoAccountUser;
|
||||
nso: SavedToken | null;
|
||||
moon: SavedMoonToken | null;
|
||||
}[] = [];
|
||||
|
||||
for (const id of ids ?? []) {
|
||||
const nsotoken = await ipc.getNintendoAccountNsoToken(id);
|
||||
const moontoken = await ipc.getNintendoAccountMoonToken(id);
|
||||
|
||||
const nso = nsotoken ? await ipc.getSavedNsoToken(nsotoken) ?? null : null;
|
||||
const moon = moontoken ? await ipc.getSavedMoonToken(moontoken) ?? null : null;
|
||||
|
||||
if (!nso && !moon) continue;
|
||||
|
||||
accounts.push({user: nso?.user ?? moon!.user, nso, moon});
|
||||
}
|
||||
|
||||
return accounts;
|
||||
}
|
||||
|
||||
function App() {
|
||||
const theme = useColorScheme() === 'light' ? light : dark;
|
||||
|
||||
const [users] = useAsync(useCallback(() => getAccounts(), [ipc]));
|
||||
|
||||
console.log(users);
|
||||
|
||||
return <View style={styles.app}>
|
||||
<Text>Hello from React!</Text>
|
||||
|
||||
{users?.map(u => <Text key={u.user.id} style={theme.text}>
|
||||
{u.user.id} - {u.user.nickname}
|
||||
</Text>)}
|
||||
</View>;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
app: {
|
||||
},
|
||||
});
|
||||
|
||||
const light = StyleSheet.create({
|
||||
text: {
|
||||
color: '#212121',
|
||||
},
|
||||
});
|
||||
|
||||
const dark = StyleSheet.create({
|
||||
text: {
|
||||
color: '#f5f5f5',
|
||||
},
|
||||
});
|
||||
|
||||
export default App;
|
||||
13
src/app/browser/index.ts
Normal file
13
src/app/browser/index.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { AppRegistry } from 'react-native';
|
||||
import App from './app.jsx';
|
||||
|
||||
AppRegistry.registerComponent('App', () => App);
|
||||
|
||||
const rootTag = window.document.createElement('div');
|
||||
|
||||
rootTag.style.minHeight = '100vh';
|
||||
window.document.body.appendChild(rootTag);
|
||||
|
||||
AppRegistry.runApplication('App', {
|
||||
rootTag,
|
||||
});
|
||||
11
src/app/browser/ipc.ts
Normal file
11
src/app/browser/ipc.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import type { NxapiElectronIpc } from '../preload/index.js';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
nxapiElectronIpc: NxapiElectronIpc;
|
||||
}
|
||||
}
|
||||
|
||||
const ipc = window.nxapiElectronIpc;
|
||||
|
||||
export default ipc;
|
||||
81
src/app/browser/util.ts
Normal file
81
src/app/browser/util.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import * as React from 'react';
|
||||
|
||||
export enum RequestState {
|
||||
NOT_LOADING,
|
||||
LOADING,
|
||||
LOADED,
|
||||
}
|
||||
|
||||
export function useAsync<T>(fetch: (() => Promise<T>) | null) {
|
||||
const [[data, requestState, error, i], setData] =
|
||||
React.useState([null as T | null, RequestState.NOT_LOADING, null as Error | null, 0]);
|
||||
const [f, forceUpdate] = React.useReducer(f => !f, false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!fetch) {
|
||||
setData(p => p[1] === RequestState.NOT_LOADING ? p : [data, RequestState.NOT_LOADING, null, p[3] + 1]);
|
||||
return;
|
||||
}
|
||||
|
||||
setData(p => [p[0], RequestState.LOADING, p[2], i + 1]);
|
||||
|
||||
fetch.call(null).then(data => {
|
||||
setData(p => p[3] === i + 1 ? [data, RequestState.LOADED, null, i + 1] : p);
|
||||
}, err => {
|
||||
setData(p => p[3] === i + 1 ? [data, RequestState.LOADED, err, i + 1] : p);
|
||||
});
|
||||
}, [fetch, f]);
|
||||
|
||||
return [data, error, requestState, forceUpdate] as const;
|
||||
}
|
||||
|
||||
export function useFetch<T>(requestInfo: RequestInfo | null, init: RequestInit | undefined, then: (res: Response) => Promise<T>): [T | null, Error | null, RequestState, React.DispatchWithoutAction]
|
||||
export function useFetch(requestInfo: RequestInfo | null, init?: RequestInit): [Response | null, Error | null, RequestState, React.DispatchWithoutAction]
|
||||
export function useFetch<T>(requestInfo: RequestInfo | null, init?: RequestInit, then?: (res: Response) => Promise<T>) {
|
||||
const f = React.useCallback(async () => {
|
||||
const response = await fetch(requestInfo!, init);
|
||||
return then?.call(null, response) ?? response;
|
||||
}, [requestInfo]);
|
||||
|
||||
return useAsync<T | Response>(requestInfo ? f : null);
|
||||
}
|
||||
|
||||
export class ErrorResponse extends Error {
|
||||
readonly data: any | undefined = undefined;
|
||||
|
||||
constructor(message: string, readonly response: Response, readonly body?: string) {
|
||||
super(message);
|
||||
|
||||
try {
|
||||
this.data = body ? JSON.parse(body) : undefined;
|
||||
} catch (err) {}
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(ErrorResponse, Symbol.hasInstance, {
|
||||
configurable: true,
|
||||
value: (instance: ErrorResponse) => {
|
||||
return instance instanceof Error &&
|
||||
'response' in instance &&
|
||||
'body' in instance &&
|
||||
'data' in instance;
|
||||
},
|
||||
});
|
||||
|
||||
export function useFetchJson<T>(requestInfo: RequestInfo | null, init?: RequestInit) {
|
||||
return useFetch(requestInfo, init, response => {
|
||||
if (response.status !== 200) {
|
||||
return response.text().then(body => {
|
||||
throw new ErrorResponse(
|
||||
'Server returned a non-200 status code: ' + response.status + ' ' + response.statusText,
|
||||
response, body);
|
||||
});
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>;
|
||||
});
|
||||
}
|
||||
|
||||
export function useFetchText(requestInfo: RequestInfo | null, init?: RequestInit) {
|
||||
return useFetch(requestInfo, init, response => response.text());
|
||||
}
|
||||
6
src/app/electron.ts
Normal file
6
src/app/electron.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { createRequire } from 'module';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const electron = require('electron');
|
||||
|
||||
export default electron;
|
||||
5
src/app/main/app-entry.cts
Normal file
5
src/app/main/app-entry.cts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
const electron = require('electron');
|
||||
|
||||
// Do anything that must be run before the app is ready...
|
||||
|
||||
electron.app.whenReady().then(() => import('./index.js'));
|
||||
44
src/app/main/index.ts
Normal file
44
src/app/main/index.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import electron from '../electron.js';
|
||||
import * as path from 'path';
|
||||
import persist from 'node-persist';
|
||||
import { initStorage, paths } from '../../util.js';
|
||||
|
||||
const __dirname = path.join(import.meta.url.substr(7), '..');
|
||||
const bundlepath = path.join(import.meta.url.substr(7), '..', '..', 'bundle');
|
||||
|
||||
const { app, BrowserWindow, ipcMain } = electron;
|
||||
|
||||
function createWindow() {
|
||||
// Create the browser window.
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600,
|
||||
vibrancy: 'content',
|
||||
webPreferences: {
|
||||
preload: path.join(bundlepath, 'preload.cjs'),
|
||||
scrollBounce: true,
|
||||
},
|
||||
});
|
||||
|
||||
mainWindow.loadFile(path.join(bundlepath, 'index.html'));
|
||||
}
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
createWindow();
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||
});
|
||||
|
||||
const storage = await initStorage(paths.data);
|
||||
|
||||
ipcMain.handle('nxapi:accounts:list', () => storage.getItem('NintendoAccountIds'));
|
||||
ipcMain.handle('nxapi:nso:gettoken', (e, id: string) => storage.getItem('NintendoAccountToken.' + id));
|
||||
ipcMain.handle('nxapi:nso:getcachedtoken', (e, token: string) => storage.getItem('NsoToken.' + token));
|
||||
ipcMain.handle('nxapi:moon:gettoken', (e, id: string) => storage.getItem('NintendoAccountToken-pctl.' + id));
|
||||
ipcMain.handle('nxapi:moon:getcachedtoken', (e, token: string) => storage.getItem('MoonToken.' + token));
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') app.quit();
|
||||
});
|
||||
15
src/app/preload/index.ts
Normal file
15
src/app/preload/index.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
import * as path from 'path';
|
||||
import { SavedMoonToken, SavedToken } from '../../util.js';
|
||||
|
||||
const ipc = {
|
||||
listNintendoAccounts: () => ipcRenderer.invoke('nxapi:accounts:list') as Promise<string[] | undefined>,
|
||||
getNintendoAccountNsoToken: (id: string) => ipcRenderer.invoke('nxapi:nso:gettoken', id) as Promise<string | undefined>,
|
||||
getSavedNsoToken: (token: string) => ipcRenderer.invoke('nxapi:nso:getcachedtoken', token) as Promise<SavedToken | undefined>,
|
||||
getNintendoAccountMoonToken: (id: string) => ipcRenderer.invoke('nxapi:moon:gettoken', id) as Promise<string | undefined>,
|
||||
getSavedMoonToken: (token: string) => ipcRenderer.invoke('nxapi:moon:getcachedtoken', token) as Promise<SavedMoonToken | undefined>,
|
||||
};
|
||||
|
||||
export type NxapiElectronIpc = typeof ipc;
|
||||
|
||||
contextBridge.exposeInMainWorld('nxapiElectronIpc', ipc);
|
||||
12
src/cli.ts
12
src/cli.ts
|
|
@ -1,10 +1,22 @@
|
|||
import * as path from 'path';
|
||||
import createDebug from 'debug';
|
||||
import Yargs from 'yargs';
|
||||
import dotenv from 'dotenv';
|
||||
import dotenvExpand from 'dotenv-expand';
|
||||
import { paths, YargsArguments } from './util.js';
|
||||
import * as commands from './cli/index.js';
|
||||
|
||||
const debug = createDebug('cli');
|
||||
|
||||
dotenvExpand.expand(dotenv.config({
|
||||
path: path.join(paths.data, '.env'),
|
||||
}));
|
||||
if (process.env.NXAPI_DATA_PATH) dotenvExpand.expand(dotenv.config({
|
||||
path: path.join(process.env.NXAPI_DATA_PATH, '.env'),
|
||||
}));
|
||||
|
||||
if (process.env.DEBUG) createDebug.enable(process.env.DEBUG);
|
||||
|
||||
const yargs = Yargs(process.argv.slice(2)).option('data-path', {
|
||||
describe: 'Data storage path',
|
||||
type: 'string',
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
"strict": true,
|
||||
"target": "es2015",
|
||||
"module": "es2022",
|
||||
"jsx": "react",
|
||||
"moduleResolution": "node12",
|
||||
"declaration": true,
|
||||
"rootDir": "src",
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user