From 14ea7b964f65460c08f39d42e1621aabd1db22fc Mon Sep 17 00:00:00 2001 From: AAGaming Date: Sat, 4 May 2024 22:39:30 -0400 Subject: implement fetch and external resource request apis --- frontend/src/plugin-loader.tsx | 62 +++++++++++++++++++++++++++++++++--------- frontend/src/start.tsx | 2 +- frontend/src/wsrouter.ts | 8 +++--- 3 files changed, 54 insertions(+), 18 deletions(-) (limited to 'frontend/src') diff --git a/frontend/src/plugin-loader.tsx b/frontend/src/plugin-loader.tsx index 75b09091..bb6fa6dd 100644 --- a/frontend/src/plugin-loader.tsx +++ b/frontend/src/plugin-loader.tsx @@ -42,7 +42,7 @@ const FilePicker = lazy(() => import('./components/modals/filepicker')); declare global { interface Window { - __DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyPluginBackendAPIInit?: { + __DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyLoaderAPIInit?: { connect: (version: number, key: string) => any; // Returns the backend API used above, no real point adding types to this. }; } @@ -51,6 +51,10 @@ declare global { /** Map of event names to event listeners */ type listenerMap = Map any>>; +interface DeckyRequestInit extends RequestInit { + excludedHeaders: string[]; +} + const callPluginMethod = DeckyBackend.callable<[pluginName: string, method: string, ...args: any], any>( 'loader/call_plugin_method', ); @@ -357,7 +361,7 @@ class PluginLoader extends Logger { let res = await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`, { credentials: 'include', headers: { - Authentication: deckyAuthToken, + 'X-Decky-Auth': deckyAuthToken, }, }); if (res.ok) { @@ -484,12 +488,31 @@ class PluginLoader extends Logger { }); } - /* TODO replace with the following flow (or similar) so we can reuse the JS Fetch API - frontend --request URL only--> backend (ws method) - backend --new temporary backend URL--> frontend (ws response) - frontend <--> backend <--> target URL (over http!) - */ - async fetchNoCors(url: string, request: any = {}) { + // Useful for audio/video streams + getExternalResourceURL(url: string) { + return `http://127.0.0.1:1337/fetch?auth=${deckyAuthToken}&fetch_url=${encodeURIComponent(url)}`; + } + + // Same syntax as fetch but only supports the url-based syntax and an object for headers since it's the most common usage pattern + fetch(input: string, init?: DeckyRequestInit | undefined): Promise { + const headers: { [name: string]: string } = { + ...(init?.headers as { [name: string]: string }), + 'X-Decky-Auth': deckyAuthToken, + 'X-Decky-Fetch-URL': input, + }; + + if (init?.excludedHeaders) { + headers['X-Decky-Fetch-Excluded-Headers'] = init.excludedHeaders.join(', '); + } + + return fetch('http://127.0.0.1:1337/fetch', { + ...init, + credentials: 'include', + headers, + }); + } + + async legacyFetchNoCors(url: string, request: any = {}) { let method: string; const req = { headers: {}, ...request, data: request.body }; req?.body && delete req.body; @@ -513,10 +536,10 @@ class PluginLoader extends Logger { initPluginBackendAPI() { // Things will break *very* badly if plugin code touches this outside of @decky/backend, so lets make that clear. - window.__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyPluginBackendAPIInit = { + window.__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyLoaderAPIInit = { connect: (version: number, pluginName: string) => { - if (version <= 0) { - throw new Error(`Plugin ${pluginName} requested invalid backend api version ${version}.`); + if (version < 1 || version > 1) { + throw new Error(`Plugin ${pluginName} requested unsupported backend api version ${version}.`); } const eventListeners: listenerMap = new Map(); @@ -543,9 +566,22 @@ class PluginLoader extends Logger { set?.delete(listener); } }, + openFilePicker: this.openFilePicker.bind(this), + executeInTab: DeckyBackend.callable< + [tab: String, runAsync: Boolean, code: string], + { success: boolean; result: any } + >('utilities/execute_in_tab'), + fetch: this.fetch.bind(this), + getExternalResourceURL: this.getExternalResourceURL.bind(this), + injectCssIntoTab: DeckyBackend.callable<[tab: string, style: string], string>( + 'utilities/inject_css_into_tab', + ), + removeCssFromTab: DeckyBackend.callable<[tab: string, cssId: string]>('utilities/remove_css_from_tab'), + routerHook: this.routerHook, + toaster: this.toaster, }; - this.debug(`${pluginName} connected to backend API.`); + this.debug(`${pluginName} connected to loader API.`); return backendAPI; }, }; @@ -591,7 +627,7 @@ class PluginLoader extends Logger { args, ); }, - fetchNoCors: this.fetchNoCors, + fetchNoCors: this.legacyFetchNoCors, executeInTab: DeckyBackend.callable< [tab: String, runAsync: Boolean, code: string], { success: boolean; result: any } diff --git a/frontend/src/start.tsx b/frontend/src/start.tsx index e9a2a8ec..1d0d821c 100644 --- a/frontend/src/start.tsx +++ b/frontend/src/start.tsx @@ -32,7 +32,7 @@ declare global { backend: { loadPath: 'http://127.0.0.1:1337/locales/{{lng}}.json', customHeaders: { - Authentication: deckyAuthToken, + 'X-Decky-Auth': deckyAuthToken, }, requestOptions: { credentials: 'include', diff --git a/frontend/src/wsrouter.ts b/frontend/src/wsrouter.ts index 34184a7f..643d24f5 100644 --- a/frontend/src/wsrouter.ts +++ b/frontend/src/wsrouter.ts @@ -30,7 +30,7 @@ interface ReplyMessage { interface ErrorMessage { type: MessageType.ERROR; - error: { name: string; message: string; traceback: string | null }; + error: { name: string; error: string; traceback: string | null }; id: number; } @@ -40,8 +40,8 @@ interface ErrorMessage { export class PyError extends Error { pythonTraceback: string | null; - constructor(name: string, message: string, traceback: string | null) { - super(message); + constructor(name: string, error: string, traceback: string | null) { + super(error); this.name = `Python ${name}`; if (traceback) { // traceback will always start with `Traceback (most recent call last):` @@ -142,7 +142,7 @@ export class WSRouter extends Logger { case MessageType.ERROR: if (this.runningCalls.has(data.id)) { - let err = new PyError(data.error.name, data.error.message, data.error.traceback); + let err = new PyError(data.error.name, data.error.error, data.error.traceback); this.runningCalls.get(data.id)!.reject(err); this.runningCalls.delete(data.id); this.debug(`Rejected PY call ${data.id} with error`, data.error); -- cgit v1.2.3