Add sendMessage, copyToClipboard and downloadImages web service functions

This commit is contained in:
Samuel Elliott 2022-10-26 16:05:18 +01:00
parent 628c87c97b
commit 9d290563d8
No known key found for this signature in database
GPG Key ID: 8420C7CDE43DC4D6
4 changed files with 334 additions and 90 deletions

View File

@ -2,7 +2,7 @@ import { app, BrowserWindow, clipboard, dialog, IpcMain, KeyboardEvent, LoginIte
import * as util from 'node:util';
import createDebug from 'debug';
import { User } from 'discord-rpc';
import openWebService, { WebServiceIpc, WebServiceValidationError } from './webservices.js';
import openWebService, { QrCodeReaderOptions, WebServiceIpc, WebServiceValidationError } from './webservices.js';
import { createWindow, getWindowConfiguration } from './windows.js';
import { DiscordPresenceConfiguration, DiscordPresenceSource, WindowType } from '../common/types.js';
import { CurrentUser, Friend, Game, PresenceState, WebService } from '../../api/coral-types.js';
@ -197,6 +197,11 @@ export function setupIpc(appinstance: App, ipcMain: IpcMain) {
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));
ipcMain.handle('nxapi:webserviceapi:openQrCodeReader', (e, data: QrCodeReaderOptions) => webserviceipc.openQrCodeReader(e, data));
ipcMain.handle('nxapi:webserviceapi:closeQrCodeReader', e => webserviceipc.closeQrCodeReader(e));
ipcMain.handle('nxapi:webserviceapi:sendMessage', (e, data: string) => webserviceipc.sendMessage(e, data));
ipcMain.handle('nxapi:webserviceapi:copyToClipboard', (e, data: string) => webserviceipc.copyToClipboard(e, data));
ipcMain.handle('nxapi:webserviceapi:downloadImages', (e, data: string) => webserviceipc.downloadImages(e, data));
ipcMain.handle('nxapi:webserviceapi:completeLoading', e => webserviceipc.completeLoading(e));
store.on('update-nintendo-accounts', () => sendToAllWindows('nxapi:accounts:shouldrefresh'));

View File

@ -4,13 +4,12 @@ import * as fs from 'node:fs/promises';
import { Buffer } from 'node:buffer';
import * as util from 'node:util';
import createDebug from 'debug';
import { app, BrowserWindow, dialog, IpcMainInvokeEvent, nativeTheme, ShareMenu, shell, WebContents } from './electron.js';
import { app, BrowserWindow, clipboard, dialog, IpcMainInvokeEvent, nativeImage, nativeTheme, Notification, ShareMenu, shell, WebContents } from './electron.js';
import fetch from 'node-fetch';
import CoralApi from '../../api/coral.js';
import { dev } from '../../util/product.js';
import { CurrentUser, WebService, WebServiceToken } from '../../api/coral-types.js';
import { Store } from './index.js';
import type { NativeShareRequest, NativeShareUrlRequest } from '../preload-webservice/znca-js-api.js';
import type { DownloadImagesRequest, NativeShareRequest, NativeShareUrlRequest, QrCodeReaderCameraOptions, QrCodeReaderCheckinOptions, QrCodeReaderCheckinResult, QrCodeReaderPhotoLibraryOptions, SendMessageOptions } from '../preload-webservice/znca-js-api.js';
import { SavedToken } from '../../common/auth/coral.js';
import { createWebServiceWindow } from './windows.js';
import { askUserForUri } from './util.js';
@ -201,6 +200,11 @@ export interface WebServiceData {
url: string;
}
export interface QrCodeReaderOptions {
type: 'camera' | 'photolibrary' | 'checkin';
data: string;
}
export class WebServiceIpc {
constructor(
store: Store
@ -251,7 +255,7 @@ export class WebServiceIpc {
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 imagepath = await this.downloadShareImage(data.image_url);
const menu = new ShareMenu({
texts,
@ -261,9 +265,9 @@ export class WebServiceIpc {
menu.popup({window: BrowserWindow.fromWebContents(event.sender)!});
}
private async downloadShareImage(req: NativeShareRequest) {
private async downloadShareImage(image_url: string) {
const dir = app.getPath('downloads');
const basename = path.basename(new URL(req.image_url).pathname);
const basename = path.basename(new URL(image_url).pathname);
const extname = path.extname(basename);
let filename;
let i = 0;
@ -274,9 +278,9 @@ export class WebServiceIpc {
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);
debug('Downloading image %s to %s as %s', image_url, dir, filename);
const response = await fetch(req.image_url, {
const response = await fetch(image_url, {
headers: {
'User-Agent': '',
},
@ -343,6 +347,83 @@ export class WebServiceIpc {
await store.storage.setItem(key, data);
}
async openQrCodeReader(event: IpcMainInvokeEvent, options: QrCodeReaderOptions): Promise<string> {
const {nsoAccount, webservice} = this.getWindowData(event.sender);
debug('openQrCodeReader', webservice.name, nsoAccount.user.name, options);
if (options.type === 'checkin') {
const request: QrCodeReaderCheckinOptions = JSON.parse(options.data);
const result: QrCodeReaderCheckinResult = {
status: 'ERROR',
text: null,
};
return JSON.stringify(result);
}
// camera/photolibrary
const request: QrCodeReaderCameraOptions | QrCodeReaderPhotoLibraryOptions = JSON.parse(options.data);
return '';
}
async closeQrCodeReader(event: IpcMainInvokeEvent): Promise<void> {
const {nsoAccount, webservice} = this.getWindowData(event.sender);
debug('closeQrCodeReader', webservice.name, nsoAccount.user.name);
//
}
async sendMessage(event: IpcMainInvokeEvent, json: string): Promise<void> {
const {nsoAccount, webservice} = this.getWindowData(event.sender);
const data: SendMessageOptions = JSON.parse(json);
debug('sendMessage', webservice.name, nsoAccount.user.name, data);
if (data.type === 'B_SHOW_SUCCESS') {
dialog.showMessageBox(BrowserWindow.fromWebContents(event.sender)!, {
message: data.message,
});
} else if (data.type === 'B_SHOW_ERROR') {
dialog.showMessageBox(BrowserWindow.fromWebContents(event.sender)!, {
type: 'error',
message: data.message,
});
} else {
debug('Unsupported message type', data.type);
}
}
async copyToClipboard(event: IpcMainInvokeEvent, data: string): Promise<void> {
const {nsoAccount, webservice} = this.getWindowData(event.sender);
debug('copyToClipboard', webservice.name, nsoAccount.user.name, data);
clipboard.writeText(data);
}
async downloadImages(event: IpcMainInvokeEvent, json: string): Promise<void> {
const {nsoAccount, webservice} = this.getWindowData(event.sender);
const data: DownloadImagesRequest = JSON.parse(json);
debug('downloadImages', webservice.name, nsoAccount.user.name, data);
for (const url of data.image_urls) {
const imagepath = await this.downloadShareImage(url);
new Notification({
title: 'Image saved from ' + webservice.name,
body: 'Image downloaded to ' + imagepath,
icon: nativeImage.createFromPath(imagepath),
}).show();
}
}
async completeLoading(event: IpcMainInvokeEvent): Promise<void> {
const {nsoAccount, webservice} = this.getWindowData(event.sender);

View File

@ -1,7 +1,7 @@
import { ipcRenderer, IpcRendererEvent } from 'electron';
import { EventEmitter } from 'events';
import createDebug from 'debug';
import { WebServiceData } from '../main/webservices.js';
import { QrCodeReaderOptions, WebServiceData } from '../main/webservices.js';
const debug = createDebug('app:preload-webservice:ipc');
@ -14,6 +14,11 @@ const ipc = {
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>,
openQrCodeReader: (data: QrCodeReaderOptions) => ipcRenderer.invoke('nxapi:webserviceapi:openQrCodeReader', data) as Promise<string>,
closeQrCodeReader: () => ipcRenderer.invoke('nxapi:webserviceapi:closeQrCodeReader') as Promise<void>,
sendMessage: (data: string) => ipcRenderer.invoke('nxapi:webserviceapi:sendMessage', data) as Promise<void>,
copyToClipboard: (data: string) => ipcRenderer.invoke('nxapi:webserviceapi:copyToClipboard', data) as Promise<void>,
downloadImages: (data: string) => ipcRenderer.invoke('nxapi:webserviceapi:downloadImages', data) as Promise<void>,
completeLoading: () => ipcRenderer.invoke('nxapi:webserviceapi:completeLoading') as Promise<void>,
};

View File

@ -7,58 +7,143 @@ const debug = createDebug('app:preload-webservice:znca-js-api');
// 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;
interface Window extends Partial<WebServiceJsApi> {
jsBridge?: WebServiceJsApi;
/**
* 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?: (data: string) => void;
// NookLink
openQRCodeReader?: (data: string) => void;
// NookLink
openQRCodeReaderFromPhotoLibrary?: (data: string) => void;
onQRCodeRead?: (data: string) => void;
// NookLink
closeQRCodeReader?: () => void;
// NookLink
closeQRCodeReaderFromPhotoLibrary?: () => void;
// Unused
sendMessage?(data: string): void;
// SplatNet 3
copyToClipboard?(data: string): void;
// SplatNet 3
openQRCodeReaderForCheckin?(data: string): void;
onQRCodeReadForCheckin?(data: string): void;
// SplatNet 3
downloadImages?(imagesJson: string): void;
// SplatNet 3
completeLoading?(): void;
// SplatNet 3
closeWebView?(): void;
// SplatNet 3
reloadExtension?(): void;
}
}
interface WebServiceJsApi {
/**
* Downloads an image and opens the native share menu.
*
* Used by SplatNet 2.
*
* Called as:
*
* ```js
* window.invokeNativeShare(JSON.stringify({text: e.text, image_url: e.url, hashtags: e.hashtags}))
* ```
*/
invokeNativeShare(data: string): void;
/**
* Opens the native share menu.
*
* Used by Smash World.
*
* ```js
* window.invokeNativeShareUrl(JSON.stringify({url: '', text: ''}))
* ```
*/
invokeNativeShareUrl(data: string): void;
/**
* Requests a web service token from the Coral API.
* `window.onGameWebTokenReceive` is called with the returned token.
*
* Used by NookLink and SplatNet 3.
*/
requestGameWebToken(): void;
/**
* Load persistent data for this web service.
* `window.onPersistentDataRestore` is called with the stored data.
*
* Used by NookLink.
*/
restorePersistentData(): void;
/**
* Store persistent data for this web service.
* `window.onPersistentDataStore` is called when complete.
*
* Used by NookLink.
*/
storePersistentData(data: string): void;
/**
* Open the QR code reader.
* `window.onQRCodeRead` is called with the base64-encoded result.
*
* Used by NookLink.
*/
openQRCodeReader(data: string): void;
/**
* Open the QR code reader.
* `window.onQRCodeRead` is called with the base64-encoded result.
*
* Used by NookLink.
*/
openQRCodeReaderFromPhotoLibrary(data: string): void;
/**
* Close the QR code reader.
*
* Used by NookLink.
*/
closeQRCodeReader(): void;
/**
* Close the QR code reader.
*
* Used by NookLink.
*/
closeQRCodeReaderFromPhotoLibrary(): void;
/**
* Send a message to the app's main thread.
* This is used to show native message dialogs and control the QR code reader.
*
* Used by NookLink.
*/
sendMessage(data: string): void;
/**
* Writes text to the clipboard.
*
* Used by SplatNet 3.
*/
copyToClipboard(data: string): void;
/**
* Opens the QR code reader.
* `window.onQRCodeReadForCheckin` is called with a JSON document containing the result.
*
* Used by SplatNet 3.
*/
openQRCodeReaderForCheckin(data: string): void;
/**
* Download images and save them to the photo library.
*
* Used by SplatNet 3.
*/
downloadImages(imagesJson: string): void;
/**
* Report the web service is ready to show and hide the loading screen.
* Web services that set the `fullScreen` attribute to `true` must call this.
*
* Used by SplatNet 3.
*/
completeLoading(): void;
/**
* Closes the web service.
* Web services that set the `fullScreen` attribute to `true` must have a button that calls this.
*
* Used by SplatNet 3.
*/
closeWebView(): void;
/**
* Asks the OS to reload any native widget extensions.
*
* Used by SplatNet 3.
*/
reloadExtension(): void;
}
//
// Share
//
@ -86,9 +171,6 @@ function invokeNativeShareUrl(data: string) {
ipc.invokeNativeShareUrl(data);
}
window.invokeNativeShare = invokeNativeShare;
window.invokeNativeShareUrl = invokeNativeShareUrl;
//
// Web service token
//
@ -103,8 +185,6 @@ function requestGameWebToken() {
});
}
window.requestGameWebToken = requestGameWebToken;
//
// Persistent data
//
@ -125,68 +205,126 @@ function storePersistentData(data: string) {
});
}
window.restorePersistentData = restorePersistentData;
window.storePersistentData = storePersistentData;
//
// QR code scanner
//
function openQrCodeReader(data: string) {
export interface QrCodeReaderCameraOptions {
messageResources: {
Camera_Page_Title: string;
Camera_Label_WaitingCameraPermission: string;
Camera_Label_WaitingCameraPermissionDescription: string;
Camera_Label_ChangeSetting: string;
Camera_Label_Searching: string;
Camera_Label_ProDialog1stQRCode: string;
Camera_Label_ProDialog1stQRCodeDescription: string;
Camera_Label_Pro2ndQRCodeRead: string;
Camera_Label_Pro3rdQRCodeRead: string;
Camera_Label_Pro4thQRCodeRead: string;
Cmn_Dialog_Button_Ok: string;
Cmn_Dialog_Button_Close: string;
};
}
export interface QrCodeReaderPhotoLibraryOptions {
messageResources: {
PhotoLibrary_Page_Title: string;
PhotoLibrary_Label_WaitingPhotoLibraryPermission: string;
PhotoLibrary_Label_WaitingPhotoLibraryPermissionDescription: string;
PhotoLibrary_Label_ChangeSetting: string;
PhotoLibrary_Label_Header: string;
PhotoLibrary_Label_Notice: string;
PhotoLibrary_Label_SelectPhoto: string;
PhotoLibrary_Label_ProDialog1stQRCode: string;
PhotoLibrary_Label_ProDialog1stQRCodeDescription: string;
PhotoLibrary_Label_Pro2ndQRCodeRead: string;
PhotoLibrary_Label_Pro3rdQRCodeRead: string;
PhotoLibrary_Label_Pro4thQRCodeRead: string;
Cmn_Dialog_Button_Ok: string;
Cmn_Dialog_Button_Close: string;
Error_Dialog_Message_Multiple_Error: string;
Error_Dialog_Message_Unknown_Error: string;
};
}
function openQrCodeReader(/** JSON.stringify(data: QrCodeReaderCameraOptions) */ data: string) {
debug('openQRCodeReader called', data);
Promise.resolve().then(() => {
const base64EncodeText = '';
ipc.openQrCodeReader({
type: 'camera',
data,
}).then(result => {
const base64EncodeText = result;
window.onQRCodeRead?.call(null, base64EncodeText);
});
}
function closeQrCodeReader() {
//
ipc.closeQrCodeReader();
}
function openQrCodeReaderFromPhotoLibrary(data: string) {
function openQrCodeReaderFromPhotoLibrary(/** JSON.stringify(data: QrCodeReaderPhotoLibraryOptions) */ data: string) {
debug('openQRCodeReaderFromPhotoLibrary called', data);
Promise.resolve().then(() => {
const base64EncodeText = '';
ipc.openQrCodeReader({
type: 'photolibrary',
data,
}).then(result => {
const base64EncodeText = result;
window.onQRCodeRead?.call(null, base64EncodeText);
});
}
function closeQrCodeReaderFromPhotoLibrary() {
//
ipc.closeQrCodeReader();
}
function openQRCodeReaderForCheckin(data: string) {
//
export interface QrCodeReaderCheckinOptions {
source: 'camera' | 'photo_library';
}
export type QrCodeReaderCheckinResult = {
status: 'SUCCEEDED';
/** base64 encoded data */
text: string;
} | {
status: 'CANCELLED' | 'ERROR';
text: null;
};
Promise.resolve().then(() => {
const base64EncodeText = '';
window.onQRCodeReadForCheckin?.call(null, base64EncodeText);
function openQRCodeReaderForCheckin(/** JSON.stringify(data: QrCodeReaderCheckinOptions) */ data: string) {
debug('openQRCodeReaderForCheckin called', data);
ipc.openQrCodeReader({
type: 'checkin',
data,
}).then(result => {
window.onQRCodeReadForCheckin?.call(null, result);
});
}
window.openQRCodeReader = openQrCodeReader;
window.openQRCodeReaderFromPhotoLibrary = openQrCodeReaderFromPhotoLibrary;
window.closeQRCodeReader = closeQrCodeReader;
window.closeQRCodeReaderFromPhotoLibrary = closeQrCodeReaderFromPhotoLibrary;
window.openQRCodeReaderForCheckin = openQRCodeReaderForCheckin;
//
// Other
//
function sendMessage(data: string) {
//
export interface SendMessageOptions {
type: 'B_SHOW_SUCCESS' | 'B_SHOW_ERROR' | 'B_SET_INDEX';
message: string;
}
function sendMessage(/** JSON.stringify(data: SendMessageOptions) */ data: string) {
debug('sendMessage called', data);
ipc.sendMessage(data);
}
function copyToClipboard(data: string) {
//
debug('copyToClipboard called', data);
ipc.copyToClipboard(data);
}
function downloadImages(imagesJson: string) {
export interface DownloadImagesRequest {
image_urls: string[];
}
function downloadImages(/** JSON.stringify(data: DownloadImagesRequest) */ imagesJson: string) {
debug('downloadImages called', imagesJson);
ipc.downloadImages(imagesJson);
}
function completeLoading() {
@ -201,9 +339,24 @@ function reloadExtension() {
debug('reloadExtension called');
}
window.sendMessage = sendMessage;
window.copyToClipboard = copyToClipboard;
window.downloadImages = downloadImages;
window.completeLoading = completeLoading;
window.closeWebView = closeWebView;
window.reloadExtension = reloadExtension;
const api: WebServiceJsApi = {
invokeNativeShare,
invokeNativeShareUrl,
requestGameWebToken,
restorePersistentData,
storePersistentData,
openQRCodeReader: openQrCodeReader,
closeQRCodeReader: closeQrCodeReader,
openQRCodeReaderFromPhotoLibrary: openQrCodeReaderFromPhotoLibrary,
closeQRCodeReaderFromPhotoLibrary: closeQrCodeReaderFromPhotoLibrary,
sendMessage,
copyToClipboard,
openQRCodeReaderForCheckin: openQRCodeReaderForCheckin,
downloadImages,
completeLoading,
closeWebView,
reloadExtension,
};
window.jsBridge = api;
Object.assign(window, api);