From 5328ada1e72c92fb21941591bf8841e5ee5343b5 Mon Sep 17 00:00:00 2001 From: Samuel Elliott Date: Sun, 29 May 2022 18:02:07 +0100 Subject: [PATCH] Show share menu for web services --- src/api/f.ts | 2 +- src/app/main/electron.ts | 2 + src/app/main/index.ts | 22 +++-------- src/app/main/na-auth.ts | 24 ++---------- src/app/main/util.ts | 17 ++++++++ src/app/main/webservices.ts | 58 +++++++++++++++++++++++++++- src/common/splatnet2/dump-records.ts | 8 ++-- src/common/splatnet2/dump-results.ts | 8 ++-- 8 files changed, 95 insertions(+), 46 deletions(-) create mode 100644 src/app/main/util.ts diff --git a/src/api/f.ts b/src/api/f.ts index f4f2ffb..cb156f0 100644 --- a/src/api/f.ts +++ b/src/api/f.ts @@ -266,7 +266,7 @@ function getEnvApi(): FApi { throw new Error('Unknown znca API provider'); } - if (process.env.ZNCA_API_URL?.startsWith('https://')) { + if (process.env.ZNCA_API_URL) { return ['nxapi', process.env.ZNCA_API_URL + '/f']; } diff --git a/src/app/main/electron.ts b/src/app/main/electron.ts index 8ca2e0a..29f8241 100644 --- a/src/app/main/electron.ts +++ b/src/app/main/electron.ts @@ -12,6 +12,7 @@ export const MenuItem = electron.MenuItem; export const nativeImage = electron.nativeImage; export const Notification = electron.Notification; export const session = electron.session; +export const ShareMenu = electron.ShareMenu; export const shell = electron.shell; export const Tray = electron.Tray; @@ -20,5 +21,6 @@ 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 ShareMenu = import('electron').ShareMenu; export type Tray = import('electron').Tray; export type WebContents = import('electron').WebContents; diff --git a/src/app/main/index.ts b/src/app/main/index.ts index 685cdfd..90e5bbb 100644 --- a/src/app/main/index.ts +++ b/src/app/main/index.ts @@ -1,9 +1,8 @@ -import { app, BrowserWindow, dialog, ipcMain, nativeImage, Notification } from './electron.js'; +import { app, BrowserWindow, dialog, ipcMain, Notification } from './electron.js'; import * as path from 'path'; import { EventEmitter } from 'events'; import createDebug from 'debug'; import * as persist from 'node-persist'; -import fetch from 'node-fetch'; import dotenv from 'dotenv'; import dotenvExpand from 'dotenv-expand'; import MenuApp from './menu.js'; @@ -18,6 +17,7 @@ import { getToken } from '../../common/auth/nso.js'; import { dir } from '../../util/product.js'; import { initStorage, paths } from '../../util/storage.js'; import { LoopResult } from '../../util/loop.js'; +import { tryGetNativeImageFromUrl } from './util.js'; const debug = createDebug('app:main'); @@ -251,16 +251,6 @@ export class EmbeddedPresenceMonitor extends ZncDiscordPresence { } export class ElectronNotificationManager extends NotificationManager { - private async getNativeImageFromUrl(url: string) { - try { - const response = await fetch(url); - const image = await response.buffer(); - return nativeImage.createFromBuffer(image); - } catch (err) {} - - return undefined; - } - async onFriendOnline(friend: CurrentUser | Friend, prev?: CurrentUser | Friend, naid?: string, ir?: boolean) { const currenttitle = friend.presence.game as Game; @@ -268,7 +258,7 @@ export class ElectronNotificationManager extends NotificationManager { title: friend.name, body: 'Playing ' + currenttitle.name + (currenttitle.sysDescription ? '\n' + currenttitle.sysDescription : ''), - icon: await this.getNativeImageFromUrl(friend.imageUri), + icon: await tryGetNativeImageFromUrl(friend.imageUri), }).show(); } @@ -276,7 +266,7 @@ export class ElectronNotificationManager extends NotificationManager { new Notification({ title: friend.name, body: 'Offline', - icon: await this.getNativeImageFromUrl(friend.imageUri), + icon: await tryGetNativeImageFromUrl(friend.imageUri), }).show(); } @@ -287,7 +277,7 @@ export class ElectronNotificationManager extends NotificationManager { title: friend.name, body: 'Playing ' + currenttitle.name + (currenttitle.sysDescription ? '\n' + currenttitle.sysDescription : ''), - icon: await this.getNativeImageFromUrl(friend.imageUri), + icon: await tryGetNativeImageFromUrl(friend.imageUri), }).show(); } @@ -298,7 +288,7 @@ export class ElectronNotificationManager extends NotificationManager { title: friend.name, body: 'Playing ' + currenttitle.name + (currenttitle.sysDescription ? '\n' + currenttitle.sysDescription : ''), - icon: await this.getNativeImageFromUrl(friend.imageUri), + icon: await tryGetNativeImageFromUrl(friend.imageUri), }).show(); } } diff --git a/src/app/main/na-auth.ts b/src/app/main/na-auth.ts index 2b93e72..889aca3 100644 --- a/src/app/main/na-auth.ts +++ b/src/app/main/na-auth.ts @@ -1,14 +1,14 @@ import * as crypto from 'crypto'; import createDebug from 'debug'; import * as persist from 'node-persist'; -import fetch from 'node-fetch'; -import { BrowserWindow, nativeImage, Notification, session, shell } from './electron.js'; +import { BrowserWindow, Notification, session, shell } from './electron.js'; import { getNintendoAccountSessionToken, NintendoAccountSessionToken } from '../../api/na.js'; import { ZNCA_CLIENT_ID } from '../../api/znc.js'; import { ZNMA_CLIENT_ID } from '../../api/moon.js'; import { getToken, SavedToken } from '../../common/auth/nso.js'; import { getPctlToken, SavedMoonToken } from '../../common/auth/moon.js'; import { Jwt } from '../../util/jwt.js'; +import { tryGetNativeImageFromUrl } from './util.js'; const debug = createDebug('app:main:na-auth'); @@ -157,18 +157,10 @@ export async function addNsoAccount(storage: persist.LocalStorage) { debug('Already authenticated', data); - let icon = undefined; - - try { - const response = await fetch(data!.nsoAccount.user.imageUri); - const image = await response.buffer(); - icon = nativeImage.createFromBuffer(image); - } catch (err) {} - new Notification({ title: 'Nintendo Switch Online', body: 'Already signed in as ' + data?.nsoAccount.user.name + ' (' + data?.user.nickname + ')', - icon, + icon: await tryGetNativeImageFromUrl(data!.nsoAccount.user.imageUri), }).show(); return getToken(storage, nsotoken, process.env.ZNC_PROXY_URL); @@ -184,18 +176,10 @@ export async function addNsoAccount(storage: persist.LocalStorage) { users.add(data.user.id); await storage.setItem('NintendoAccountIds', [...users]); - let icon = undefined; - - try { - const response = await fetch(data.nsoAccount.user.imageUri); - const image = await response.buffer(); - icon = nativeImage.createFromBuffer(image); - } catch (err) {} - new Notification({ title: 'Nintendo Switch Online', body: 'Authenticated as ' + data.nsoAccount.user.name + ' (NSO ' + data.user.nickname + ')', - icon, + icon: await tryGetNativeImageFromUrl(data.nsoAccount.user.imageUri), }).show(); return {nso, data}; diff --git a/src/app/main/util.ts b/src/app/main/util.ts new file mode 100644 index 0000000..c788103 --- /dev/null +++ b/src/app/main/util.ts @@ -0,0 +1,17 @@ +import { nativeImage } from './electron.js'; +import { Buffer } from 'buffer'; +import fetch from 'node-fetch'; + +export async function getNativeImageFromUrl(url: URL | string) { + const response = await fetch(url.toString()); + const image = await response.arrayBuffer(); + return nativeImage.createFromBuffer(Buffer.from(image)); +} + +export async function tryGetNativeImageFromUrl(url: URL | string) { + try { + return await getNativeImageFromUrl(url); + } catch (err) {} + + return undefined; +} diff --git a/src/app/main/webservices.ts b/src/app/main/webservices.ts index 2b6bc4d..d68220f 100644 --- a/src/app/main/webservices.ts +++ b/src/app/main/webservices.ts @@ -1,6 +1,10 @@ import * as path from 'path'; +import { constants } from 'fs'; +import * as fs from 'fs/promises'; +import { Buffer } from 'buffer'; import createDebug from 'debug'; -import { BrowserWindow, IpcMainInvokeEvent, session, shell, WebContents } from './electron.js'; +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'; @@ -149,6 +153,51 @@ export class WebServiceIpc { 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 { @@ -157,6 +206,13 @@ export class WebServiceIpc { 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 { diff --git a/src/common/splatnet2/dump-records.ts b/src/common/splatnet2/dump-records.ts index 314a4bb..8149632 100644 --- a/src/common/splatnet2/dump-records.ts +++ b/src/common/splatnet2/dump-records.ts @@ -99,7 +99,7 @@ export async function dumpProfileImage( debug('Fetching profile image'); const image_response = await fetch(share.url); - const image = await image_response.buffer(); + const image = await image_response.arrayBuffer(); debug('Writing profile image data %s', filename); await fs.writeFile(file, JSON.stringify({ @@ -109,7 +109,7 @@ export async function dumpProfileImage( }, null, 4) + '\n', 'utf-8'); debug('Writing profile image %s', image_filename); - await fs.writeFile(image_file, image); + await fs.writeFile(image_file, Buffer.from(image)); await fs.writeFile(latest_file, JSON.stringify({timestamp, etag, etag_data}, null, 4) + '\n', 'utf-8'); } @@ -136,13 +136,13 @@ export async function dumpChallenges( debug('Fetching challenge image for %s', challenge.key); const image_response = await fetch(share.url); - const image = await image_response.buffer(); + const image = await image_response.arrayBuffer(); debug('Writing challenge image data %s', filename); await fs.writeFile(file, JSON.stringify({share}, null, 4) + '\n', 'utf-8'); debug('Writing challenge image %s', filename); - await fs.writeFile(image_file, image); + await fs.writeFile(image_file, Buffer.from(image)); } } diff --git a/src/common/splatnet2/dump-results.ts b/src/common/splatnet2/dump-results.ts index 30403eb..f58ce97 100644 --- a/src/common/splatnet2/dump-results.ts +++ b/src/common/splatnet2/dump-results.ts @@ -46,7 +46,7 @@ export async function dumpResults( debug('Fetching battle results summary image'); const image_response = await fetch(share.url); - const image = await image_response.buffer(); + const image = await image_response.arrayBuffer(); debug('Writing battle results summary image data %s', filename); await fs.writeFile(file, JSON.stringify({ @@ -54,7 +54,7 @@ export async function dumpResults( }, null, 4) + '\n', 'utf-8'); debug('Writing battle results summary image %s', filename); - await fs.writeFile(image_file, image); + await fs.writeFile(image_file, Buffer.from(image)); } const skipped = []; @@ -103,7 +103,7 @@ export async function dumpResults( debug('Fetching battle results image'); const image_response = await fetch(share.url); - const image = await image_response.buffer(); + const image = await image_response.arrayBuffer(); debug('Writing battle results image data %s', filename); await fs.writeFile(file, JSON.stringify({ @@ -111,7 +111,7 @@ export async function dumpResults( }, null, 4) + '\n', 'utf-8'); debug('Writing battle results image %s', filename); - await fs.writeFile(image_file, image); + await fs.writeFile(image_file, Buffer.from(image)); } } }