mirror of
https://github.com/samuelthomas2774/nxapi.git
synced 2026-04-24 23:16:53 -05:00
248 lines
8.2 KiB
TypeScript
248 lines
8.2 KiB
TypeScript
import * as path from 'node:path';
|
|
import { constants } from 'node:fs';
|
|
import * as fs from 'node:fs/promises';
|
|
import { Buffer } from 'node:buffer';
|
|
import createDebug from 'debug';
|
|
import { app, BrowserWindow, IpcMainInvokeEvent, session, ShareMenu, shell, WebContents } from './electron.js';
|
|
import fetch from 'node-fetch';
|
|
import ZncApi from '../../api/znc.js';
|
|
import { dev } from '../../util/product.js';
|
|
import { WebService } from '../../api/znc-types.js';
|
|
import { bundlepath, Store } from './index.js';
|
|
import type { NativeShareRequest, NativeShareUrlRequest } from '../preload-webservice/znca-js-api.js';
|
|
import { SavedToken } from '../../common/auth/nso.js';
|
|
|
|
const debug = createDebug('app:main:webservices');
|
|
|
|
export function createWebServiceWindow(nsa_id: string, webservice: WebService) {
|
|
const browser_session = session.fromPartition('persist:webservices-' + nsa_id, {
|
|
cache: false,
|
|
});
|
|
|
|
const window = new BrowserWindow({
|
|
width: 375,
|
|
height: 667,
|
|
resizable: false,
|
|
title: webservice.name,
|
|
webPreferences: {
|
|
session: browser_session,
|
|
preload: path.join(bundlepath, 'preload-webservice.cjs'),
|
|
contextIsolation: false,
|
|
scrollBounce: true,
|
|
},
|
|
});
|
|
|
|
return window;
|
|
}
|
|
|
|
const windows = new Map<string, BrowserWindow>();
|
|
const windowapi = new WeakMap<WebContents, [Store, string, ZncApi, SavedToken, WebService]>();
|
|
|
|
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)!;
|
|
|
|
window.focus();
|
|
|
|
return;
|
|
}
|
|
|
|
const verifymembership = webservice.customAttributes.find(a => a.attrKey === 'verifyMembership');
|
|
|
|
if (verifymembership?.attrValue === 'true') {
|
|
const membership = data.nsoAccount.user.links.nintendoAccount.membership;
|
|
const active = typeof membership.active === 'object' ? membership.active.active : membership.active;
|
|
if (!active) throw new Error('Nintendo Switch Online membership required');
|
|
}
|
|
|
|
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';
|
|
|
|
window.webContents.setWindowOpenHandler(details => {
|
|
debug('open', details);
|
|
shell.openExternal(details.url);
|
|
return {action: 'deny'};
|
|
});
|
|
|
|
const webserviceToken = await nso.getWebServiceToken('' + webservice.id);
|
|
|
|
const url = new URL(webservice.uri);
|
|
url.search = new URLSearchParams({
|
|
lang: data.user.language,
|
|
na_country: data.user.country,
|
|
na_lang: data.user.language,
|
|
}).toString();
|
|
|
|
debug('Loading web service', {
|
|
url: url.toString(),
|
|
webservice,
|
|
webserviceToken,
|
|
});
|
|
|
|
if (dev) window.webContents.openDevTools();
|
|
|
|
window.loadURL(url.toString(), {
|
|
extraHeaders: Object.entries({
|
|
'x-gamewebtoken': webserviceToken.result.accessToken,
|
|
'dnt': '1',
|
|
'X-Requested-With': 'com.nintendo.znca',
|
|
}).map(([key, value]) => key + ': ' + value).join('\n'),
|
|
});
|
|
}
|
|
|
|
export interface WebServiceData {
|
|
webservice: WebService;
|
|
url: string;
|
|
}
|
|
|
|
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],
|
|
};
|
|
}
|
|
|
|
getWebService(event: IpcMainInvokeEvent): WebServiceData {
|
|
const {user, webservice} = this.getWindowData(event.sender);
|
|
|
|
const url = new URL(webservice.uri);
|
|
url.search = new URLSearchParams({
|
|
lang: user.language,
|
|
na_country: user.country,
|
|
na_lang: user.language,
|
|
}).toString();
|
|
|
|
return {
|
|
webservice,
|
|
url: url.toString(),
|
|
};
|
|
}
|
|
|
|
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);
|
|
|
|
const texts: string[] = [];
|
|
if (data.text) texts.push(data.text);
|
|
if (data.hashtags) texts.push(data.hashtags.map(t => '#' + t).join(' '));
|
|
|
|
const imagepath = await this.downloadShareImage(data);
|
|
|
|
const menu = new ShareMenu({
|
|
texts,
|
|
filePaths: [imagepath],
|
|
});
|
|
|
|
menu.popup({window: BrowserWindow.fromWebContents(event.sender)!});
|
|
}
|
|
|
|
private async downloadShareImage(req: NativeShareRequest) {
|
|
const dir = app.getPath('downloads');
|
|
const basename = path.basename(new URL(req.image_url).pathname);
|
|
const extname = path.extname(basename);
|
|
let filename;
|
|
let i = 0;
|
|
|
|
do {
|
|
i++;
|
|
|
|
filename = i === 1 ? basename : basename.substr(0, basename.length - extname.length) + ' ' + i + extname;
|
|
} while (await this.pathExists(path.join(dir, filename)));
|
|
|
|
debug('Downloading image %s to %s as %s', req.image_url, dir, filename);
|
|
|
|
const response = await fetch(req.image_url);
|
|
const image = await response.arrayBuffer();
|
|
await fs.writeFile(path.join(dir, filename), Buffer.from(image));
|
|
|
|
return path.join(dir, filename);
|
|
}
|
|
|
|
private async pathExists(path: string) {
|
|
try {
|
|
await fs.access(path, constants.F_OK);
|
|
return true;
|
|
} catch (err) {
|
|
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
const menu = new ShareMenu({
|
|
texts: [data.text],
|
|
urls: [data.url],
|
|
});
|
|
|
|
menu.popup({window: BrowserWindow.fromWebContents(event.sender)!});
|
|
}
|
|
|
|
async requestGameWebToken(event: IpcMainInvokeEvent): Promise<string> {
|
|
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);
|
|
}
|
|
}
|