mirror of
https://github.com/samuelthomas2774/nxapi.git
synced 2026-03-21 18:04:10 -05:00
Implement APIs for web services (required for NookLink)
This commit is contained in:
parent
3502b7ab87
commit
68246fbb6b
|
|
@ -29,6 +29,28 @@ const preload = {
|
|||
],
|
||||
};
|
||||
|
||||
const preload_webservice = {
|
||||
input: 'src/app/preload-webservice/index.ts',
|
||||
output: {
|
||||
file: 'dist/app/bundle/preload-webservice.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: {
|
||||
|
|
@ -63,5 +85,6 @@ const browser = {
|
|||
|
||||
export default [
|
||||
preload,
|
||||
preload_webservice,
|
||||
browser,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@ export const shell = electron.shell;
|
|||
export const Tray = electron.Tray;
|
||||
|
||||
export type BrowserWindow = import('electron').BrowserWindow;
|
||||
export type IpcMainInvokeEvent = import('electron').IpcMainInvokeEvent;
|
||||
export type Menu = import('electron').Menu;
|
||||
export type MenuItem = import('electron').MenuItem;
|
||||
export type Notification = import('electron').Notification;
|
||||
export type Tray = import('electron').Tray;
|
||||
export type WebContents = import('electron').WebContents;
|
||||
|
|
|
|||
|
|
@ -8,11 +8,12 @@ import { EventEmitter } from 'events';
|
|||
|
||||
import dotenv from 'dotenv';
|
||||
import dotenvExpand from 'dotenv-expand';
|
||||
import { WebServiceIpc } from './webservices.js';
|
||||
|
||||
const debug = createDebug('app:main');
|
||||
|
||||
const __dirname = path.join(import.meta.url.substr(7), '..');
|
||||
const bundlepath = path.join(import.meta.url.substr(7), '..', '..', 'bundle');
|
||||
export const bundlepath = path.join(import.meta.url.substr(7), '..', '..', 'bundle');
|
||||
|
||||
function createWindow() {
|
||||
// Create the browser window.
|
||||
|
|
@ -48,6 +49,13 @@ app.whenReady().then(async () => {
|
|||
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));
|
||||
|
||||
const webserviceipc = new WebServiceIpc(store);
|
||||
ipcMain.handle('nxapi:webserviceapi:invokeNativeShare', (e, data: string) => webserviceipc.invokeNativeShare(e, data));
|
||||
ipcMain.handle('nxapi:webserviceapi:invokeNativeShareUrl', (e, data: string) => webserviceipc.invokeNativeShareUrl(e, data));
|
||||
ipcMain.handle('nxapi:webserviceapi:requestGameWebToken', e => webserviceipc.requestGameWebToken(e));
|
||||
ipcMain.handle('nxapi:webserviceapi:restorePersistentData', e => webserviceipc.restorePersistentData(e));
|
||||
ipcMain.handle('nxapi:webserviceapi:storePersistentData', (e, data: string) => webserviceipc.storePersistentData(e, data));
|
||||
|
||||
const sendToAllWindows = (channel: string, ...args: any[]) =>
|
||||
BrowserWindow.getAllWindows().forEach(w => w.webContents.send(channel, ...args));
|
||||
store.on('update-nintendo-accounts', () => sendToAllWindows('nxapi:accounts:shouldrefresh'));
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ export default class MenuApp {
|
|||
...await this.getWebServiceItems(token) as any,
|
||||
],
|
||||
});
|
||||
|
||||
menu.append(item);
|
||||
}
|
||||
|
||||
menu.append(new MenuItem({label: 'Add account', click: this.addNsoAccount}));
|
||||
|
|
@ -146,7 +148,7 @@ export default class MenuApp {
|
|||
click: async () => {
|
||||
const {nso, data} = await getToken(this.store.storage, token);
|
||||
|
||||
await openWebService(nso, data, webservice);
|
||||
await openWebService(this.store, token, nso, data, webservice);
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
import * as path from 'path';
|
||||
import createDebug from 'debug';
|
||||
import { BrowserWindow, session, shell } from '../electron.js';
|
||||
import { BrowserWindow, IpcMainInvokeEvent, session, shell, WebContents } from '../electron.js';
|
||||
import ZncApi from '../../api/znc.js';
|
||||
import { SavedToken } from '../../util.js';
|
||||
import { WebService } from '../../api/znc-types.js';
|
||||
import { bundlepath, Store } from './index.js';
|
||||
import type { NativeShareRequest, NativeShareUrlRequest } from '../preload-webservice/index.js';
|
||||
|
||||
const debug = createDebug('app:main:webservices');
|
||||
|
||||
export function createWebServiceWindow(na_id: string, webservice: WebService) {
|
||||
const browser_session = session.fromPartition('webservices-' + na_id, {
|
||||
export function createWebServiceWindow(nsa_id: string, webservice: WebService) {
|
||||
const browser_session = session.fromPartition('webservices-' + nsa_id, {
|
||||
cache: false,
|
||||
});
|
||||
|
||||
|
|
@ -18,6 +21,8 @@ export function createWebServiceWindow(na_id: string, webservice: WebService) {
|
|||
title: webservice.name,
|
||||
webPreferences: {
|
||||
session: browser_session,
|
||||
preload: path.join(bundlepath, 'preload-webservice.cjs'),
|
||||
contextIsolation: false,
|
||||
scrollBounce: true,
|
||||
},
|
||||
});
|
||||
|
|
@ -26,9 +31,12 @@ export function createWebServiceWindow(na_id: string, webservice: WebService) {
|
|||
}
|
||||
|
||||
const windows = new Map<string, BrowserWindow>();
|
||||
const windowapi = new WeakMap<WebContents, [Store, string, ZncApi, SavedToken, WebService]>();
|
||||
|
||||
export default async function openWebService(nso: ZncApi, data: SavedToken, webservice: WebService) {
|
||||
const windowid = data.user.id + ':' + webservice.id;
|
||||
export default async function openWebService(
|
||||
store: Store, token: string, nso: ZncApi, data: SavedToken, webservice: WebService
|
||||
) {
|
||||
const windowid = data.nsoAccount.user.nsaId + ':' + webservice.id;
|
||||
|
||||
if (windows.has(windowid)) {
|
||||
const window = windows.get(windowid)!;
|
||||
|
|
@ -38,10 +46,14 @@ export default async function openWebService(nso: ZncApi, data: SavedToken, webs
|
|||
return;
|
||||
}
|
||||
|
||||
const window = createWebServiceWindow(data.user.id, webservice);
|
||||
const window = createWebServiceWindow(data.nsoAccount.user.nsaId, webservice);
|
||||
|
||||
windows.set(windowid, window);
|
||||
windowapi.set(window.webContents, [store, token, nso, data, webservice]);
|
||||
|
||||
window.on('closed', () => {
|
||||
windows.delete(windowid);
|
||||
// windowapi.delete(window.webContents);
|
||||
});
|
||||
|
||||
window.webContents.userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.3 Mobile/15E148 Safari/604.1';
|
||||
|
|
@ -77,3 +89,76 @@ export default async function openWebService(nso: ZncApi, data: SavedToken, webs
|
|||
}).map(([key, value]) => key + ': ' + value).join('\n'),
|
||||
});
|
||||
}
|
||||
|
||||
export class WebServiceIpc {
|
||||
constructor(
|
||||
store: Store
|
||||
) {}
|
||||
|
||||
private getWindowData(window: WebContents) {
|
||||
const data = windowapi.get(window);
|
||||
|
||||
if (!data) {
|
||||
throw new Error('Unknown window');
|
||||
}
|
||||
|
||||
return {
|
||||
store: data[0],
|
||||
token: data[1],
|
||||
nso: data[2],
|
||||
nintendoAccountToken: data[3].nintendoAccountToken,
|
||||
user: data[3].user,
|
||||
nsoAccount: data[3].nsoAccount,
|
||||
webservice: data[4],
|
||||
};
|
||||
}
|
||||
|
||||
async invokeNativeShare(event: IpcMainInvokeEvent, json: string): Promise<void> {
|
||||
const {nsoAccount, webservice} = this.getWindowData(event.sender);
|
||||
|
||||
const data: NativeShareRequest = JSON.parse(json);
|
||||
|
||||
debug('invokeNativeShare', webservice.name, nsoAccount.user.name, data);
|
||||
}
|
||||
|
||||
async invokeNativeShareUrl(event: IpcMainInvokeEvent, json: string): Promise<void> {
|
||||
const {nsoAccount, webservice} = this.getWindowData(event.sender);
|
||||
|
||||
const data: NativeShareUrlRequest = JSON.parse(json);
|
||||
|
||||
debug('invokeNativeShareUrl', webservice.name, nsoAccount.user.name, data);
|
||||
}
|
||||
|
||||
async requestGameWebToken(event: IpcMainInvokeEvent): Promise<string> {
|
||||
// TODO: if the web service token has expired the NSO app token will probably have expired as well
|
||||
// This needs to renew that token if necessary
|
||||
|
||||
const {nso, nsoAccount, webservice} = this.getWindowData(event.sender);
|
||||
|
||||
debug('Web service %s, user %s, called requestGameWebToken', webservice.name, nsoAccount.user.name);
|
||||
|
||||
const webserviceToken = await nso.getWebServiceToken('' + webservice.id);
|
||||
|
||||
return webserviceToken.result.accessToken;
|
||||
}
|
||||
|
||||
async restorePersistentData(event: IpcMainInvokeEvent): Promise<string | undefined> {
|
||||
const {store, nsoAccount, webservice} = this.getWindowData(event.sender);
|
||||
|
||||
debug('Web service %s, user %s, called restorePersistentData', webservice.name, nsoAccount.user.name);
|
||||
|
||||
const key = 'WebServicePersistentData.' + nsoAccount.user.nsaId + '.' + webservice.id;
|
||||
const data: string | undefined = await store.storage.getItem(key);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async storePersistentData(event: IpcMainInvokeEvent, data: string): Promise<void> {
|
||||
const {store, nsoAccount, webservice} = this.getWindowData(event.sender);
|
||||
|
||||
debug('Web service %s, user %s, called storePersistentData', webservice.name, nsoAccount.user.name, data);
|
||||
|
||||
const key = 'WebServicePersistentData.' + nsoAccount.user.nsaId + '.' + webservice.id;
|
||||
await store.storage.setItem(key, data);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
161
src/app/preload-webservice/index.ts
Normal file
161
src/app/preload-webservice/index.ts
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
import createDebug from 'debug';
|
||||
|
||||
// Logs are written to the browser window developer tools, and are hidden by default (enable verbose logs)
|
||||
const debug = createDebug('app:preload-webservice');
|
||||
|
||||
const ipc = {
|
||||
invokeNativeShare: (data: string) => ipcRenderer.invoke('nxapi:webserviceapi:invokeNativeShare', data) as Promise<void>,
|
||||
invokeNativeShareUrl: (data: string) => ipcRenderer.invoke('nxapi:webserviceapi:invokeNativeShareUrl', data) as Promise<void>,
|
||||
requestGameWebToken: () => ipcRenderer.invoke('nxapi:webserviceapi:requestGameWebToken') as Promise<string>,
|
||||
restorePersistentData: () => ipcRenderer.invoke('nxapi:webserviceapi:restorePersistentData') as Promise<string | undefined>,
|
||||
storePersistentData: (data: string) => ipcRenderer.invoke('nxapi:webserviceapi:storePersistentData', data) as Promise<void>,
|
||||
};
|
||||
|
||||
// NookLink throws an error if restorePersistentData doesn't exist (and helpfully discards it somewhere)
|
||||
// All others aren't required
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
/**
|
||||
* window.invokeNativeShare(JSON.stringify({text: e.text, image_url: e.url, hashtags: e.hashtags}))
|
||||
*/
|
||||
// SplatNet 2
|
||||
invokeNativeShare?: (data: string) => void;
|
||||
|
||||
/**
|
||||
* window.invokeNativeShareUrl(JSON.stringify({url: '', text: ''}))
|
||||
*/
|
||||
// Smash World
|
||||
invokeNativeShareUrl?: (data: string) => void;
|
||||
|
||||
// NookLink
|
||||
requestGameWebToken?: () => void;
|
||||
onGameWebTokenReceive?: (token: string) => void;
|
||||
|
||||
// NookLink
|
||||
restorePersistentData?: () => void;
|
||||
onPersistentDataRestore?: (data: string) => void;
|
||||
// NookLink
|
||||
storePersistentData?: (data: string) => void;
|
||||
onPersistentDataStore?: () => void;
|
||||
|
||||
// NookLink
|
||||
openQRCodeReader?: (data: string) => void;
|
||||
// NookLink
|
||||
openQRCodeReaderFromPhotoLibrary?: (data: string) => void;
|
||||
onQRCodeRead?: (data: string) => void;
|
||||
// NookLink
|
||||
closeQRCodeReader?: () => void;
|
||||
// NookLink
|
||||
closeQRCodeReaderFromPhotoLibrary?: () => void;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Share
|
||||
//
|
||||
// Used by SplatNet 2 and Smash World (but both have a different implementation because Nintendo)
|
||||
//
|
||||
|
||||
export interface NativeShareRequest {
|
||||
text: string;
|
||||
image_url: string;
|
||||
hashtags: string[];
|
||||
}
|
||||
|
||||
function invokeNativeShare(data: string) {
|
||||
debug('invokeNativeShare called', data);
|
||||
|
||||
ipc.invokeNativeShare(data);
|
||||
}
|
||||
|
||||
export interface NativeShareUrlRequest {
|
||||
url: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
function invokeNativeShareUrl(data: string) {
|
||||
debug('invokeNativeShareUrl called', data);
|
||||
|
||||
ipc.invokeNativeShareUrl(data);
|
||||
}
|
||||
|
||||
window.invokeNativeShare = invokeNativeShare;
|
||||
window.invokeNativeShareUrl = invokeNativeShareUrl;
|
||||
|
||||
//
|
||||
// Web service token
|
||||
//
|
||||
// Used by NookLink
|
||||
//
|
||||
|
||||
function requestGameWebToken() {
|
||||
debug('requestGameWebToken called');
|
||||
|
||||
ipc.requestGameWebToken().then(token => {
|
||||
window.onGameWebTokenReceive?.call(null, token);
|
||||
});
|
||||
}
|
||||
|
||||
window.requestGameWebToken = requestGameWebToken;
|
||||
|
||||
//
|
||||
// Persistent data
|
||||
//
|
||||
// Used by NookLink
|
||||
//
|
||||
|
||||
function restorePersistentData() {
|
||||
debug('restorePersistentData called');
|
||||
|
||||
ipc.restorePersistentData().then(data => {
|
||||
window.onPersistentDataRestore?.call(null, data ?? '');
|
||||
});
|
||||
}
|
||||
|
||||
function storePersistentData(data: string) {
|
||||
debug('storePersistentData called', data);
|
||||
|
||||
ipc.storePersistentData(data).then(() => {
|
||||
window.onPersistentDataStore?.call(null);
|
||||
});
|
||||
}
|
||||
|
||||
window.restorePersistentData = restorePersistentData;
|
||||
window.storePersistentData = storePersistentData;
|
||||
|
||||
//
|
||||
// QR code scanner
|
||||
//
|
||||
// Used by NookLink
|
||||
//
|
||||
|
||||
function openQrCodeReader(data: string) {
|
||||
debug('openQRCodeReader called', data);
|
||||
|
||||
Promise.resolve().then(() => {
|
||||
const base64EncodeText = '';
|
||||
window.onQRCodeRead?.call(null, base64EncodeText);
|
||||
});
|
||||
}
|
||||
function closeQrCodeReader() {
|
||||
//
|
||||
}
|
||||
|
||||
function openQrCodeReaderFromPhotoLibrary(data: string) {
|
||||
debug('openQRCodeReaderFromPhotoLibrary called', data);
|
||||
|
||||
Promise.resolve().then(() => {
|
||||
const base64EncodeText = '';
|
||||
window.onQRCodeRead?.call(null, base64EncodeText);
|
||||
});
|
||||
}
|
||||
function closeQrCodeReaderFromPhotoLibrary() {
|
||||
//
|
||||
}
|
||||
|
||||
window.openQRCodeReader = openQrCodeReader;
|
||||
window.openQRCodeReaderFromPhotoLibrary = openQrCodeReaderFromPhotoLibrary;
|
||||
window.closeQRCodeReader = closeQrCodeReader;
|
||||
window.closeQRCodeReaderFromPhotoLibrary = closeQrCodeReaderFromPhotoLibrary;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
import * as EventEmitter from 'events';
|
||||
import { EventEmitter } from 'events';
|
||||
import { SavedMoonToken, SavedToken } from '../../util.js';
|
||||
|
||||
const ipc = {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
|||
], {
|
||||
stdio: 'inherit',
|
||||
env: {
|
||||
...process.env,
|
||||
NXAPI_SKIP_UPDATE_CHECK: '1',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user