Upload files to 'src/main/utils'
This commit is contained in:
		
							parent
							
								
									ebfe647cb1
								
							
						
					
					
						commit
						e5bb4e693f
					
				
							
								
								
									
										58
									
								
								src/main/utils/http.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/main/utils/http.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,58 @@ | ||||
| /* | ||||
|  * SPDX-License-Identifier: GPL-3.0 | ||||
|  * Aerocord, a vesktop fork for older microsoft NT releases such as NT 6.0, 6.1, 6.2 and 6.3.  | ||||
|  * Credits to vendicated and the rest of the vesktop contribuitors for making Vesktop! | ||||
|  */ | ||||
| 
 | ||||
| import { createWriteStream } from "fs"; | ||||
| import { Readable } from "stream"; | ||||
| import { pipeline } from "stream/promises"; | ||||
| import { setTimeout } from "timers/promises"; | ||||
| 
 | ||||
| interface FetchieOptions { | ||||
|     retryOnNetworkError?: boolean; | ||||
| } | ||||
| 
 | ||||
| export async function downloadFile(url: string, file: string, options: RequestInit = {}, fetchieOpts?: FetchieOptions) { | ||||
|     const res = await fetchie(url, options, fetchieOpts); | ||||
|     await pipeline( | ||||
|         // @ts-expect-error odd type error
 | ||||
|         Readable.fromWeb(res.body!), | ||||
|         createWriteStream(file, { | ||||
|             autoClose: true | ||||
|         }) | ||||
|     ); | ||||
| } | ||||
| 
 | ||||
| const ONE_MINUTE_MS = 1000 * 60; | ||||
| 
 | ||||
| export async function fetchie(url: string, options?: RequestInit, { retryOnNetworkError }: FetchieOptions = {}) { | ||||
|     let res: Response | undefined; | ||||
| 
 | ||||
|     try { | ||||
|         res = await fetch(url, options); | ||||
|     } catch (err) { | ||||
|         if (retryOnNetworkError) { | ||||
|             console.error("Failed to fetch", url + ".", "Gonna retry with backoff."); | ||||
| 
 | ||||
|             for (let tries = 0, delayMs = 500; tries < 20; tries++, delayMs = Math.min(2 * delayMs, ONE_MINUTE_MS)) { | ||||
|                 await setTimeout(delayMs); | ||||
|                 try { | ||||
|                     res = await fetch(url, options); | ||||
|                     break; | ||||
|                 } catch {} | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (!res) throw new Error(`Failed to fetch ${url}\n${err}`); | ||||
|     } | ||||
| 
 | ||||
|     if (res.ok) return res; | ||||
| 
 | ||||
|     let msg = `Got non-OK response for ${url}: ${res.status} ${res.statusText}`; | ||||
| 
 | ||||
|     const reason = await res.text().catch(() => ""); | ||||
|     if (reason) msg += `\n${reason}`; | ||||
| 
 | ||||
|     throw new Error(msg); | ||||
| } | ||||
							
								
								
									
										30
									
								
								src/main/utils/ipcWrappers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/main/utils/ipcWrappers.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | ||||
| /* | ||||
|  * SPDX-License-Identifier: GPL-3.0 | ||||
|  * Aerocord, a vesktop fork for older microsoft NT releases such as NT 6.0, 6.1, 6.2 and 6.3.  | ||||
|  * Credits to vendicated and the rest of the vesktop contribuitors for making Vesktop! | ||||
|  */ | ||||
| 
 | ||||
| import { ipcMain, IpcMainEvent, IpcMainInvokeEvent, WebFrameMain } from "electron"; | ||||
| import { DISCORD_HOSTNAMES } from "main/constants"; | ||||
| import { IpcEvents } from "shared/IpcEvents"; | ||||
| 
 | ||||
| export function validateSender(frame: WebFrameMain) { | ||||
|     const { hostname, protocol } = new URL(frame.url); | ||||
|     if (protocol === "file:") return; | ||||
| 
 | ||||
|     if (!DISCORD_HOSTNAMES.includes(hostname)) throw new Error("ipc: Disallowed host " + hostname); | ||||
| } | ||||
| 
 | ||||
| export function handleSync(event: IpcEvents, cb: (e: IpcMainEvent, ...args: any[]) => any) { | ||||
|     ipcMain.on(event, (e, ...args) => { | ||||
|         validateSender(e.senderFrame); | ||||
|         e.returnValue = cb(e, ...args); | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| export function handle(event: IpcEvents, cb: (e: IpcMainInvokeEvent, ...args: any[]) => any) { | ||||
|     ipcMain.handle(event, (e, ...args) => { | ||||
|         validateSender(e.senderFrame); | ||||
|         return cb(e, ...args); | ||||
|     }); | ||||
| } | ||||
							
								
								
									
										71
									
								
								src/main/utils/makeLinksOpenExternally.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								src/main/utils/makeLinksOpenExternally.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,71 @@ | ||||
| /* | ||||
|  * SPDX-License-Identifier: GPL-3.0 | ||||
|  * Aerocord, a vesktop fork for older microsoft NT releases such as NT 6.0, 6.1, 6.2 and 6.3.  | ||||
|  * Credits to vendicated and the rest of the vesktop contribuitors for making Vesktop! | ||||
|  */ | ||||
| 
 | ||||
| import { BrowserWindow, shell } from "electron"; | ||||
| import { DISCORD_HOSTNAMES } from "main/constants"; | ||||
| 
 | ||||
| import { Settings } from "../settings"; | ||||
| import { createOrFocusPopup, setupPopout } from "./popout"; | ||||
| import { execSteamURL, isDeckGameMode, steamOpenURL } from "./steamOS"; | ||||
| 
 | ||||
| export function handleExternalUrl(url: string, protocol?: string): { action: "deny" | "allow" } { | ||||
|     if (protocol == null) { | ||||
|         try { | ||||
|             protocol = new URL(url).protocol; | ||||
|         } catch { | ||||
|             return { action: "deny" }; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     switch (protocol) { | ||||
|         case "http:": | ||||
|         case "https:": | ||||
|             if (Settings.store.openLinksWithElectron) { | ||||
|                 return { action: "allow" }; | ||||
|             } | ||||
|         // eslint-disable-next-line no-fallthrough
 | ||||
|         case "mailto:": | ||||
|         case "spotify:": | ||||
|             if (isDeckGameMode) { | ||||
|                 steamOpenURL(url); | ||||
|             } else { | ||||
|                 shell.openExternal(url); | ||||
|             } | ||||
|             break; | ||||
|         case "steam:": | ||||
|             if (isDeckGameMode) { | ||||
|                 execSteamURL(url); | ||||
|             } else { | ||||
|                 shell.openExternal(url); | ||||
|             } | ||||
|             break; | ||||
|     } | ||||
| 
 | ||||
|     return { action: "deny" }; | ||||
| } | ||||
| 
 | ||||
| export function makeLinksOpenExternally(win: BrowserWindow) { | ||||
|     win.webContents.setWindowOpenHandler(({ url, frameName, features }) => { | ||||
|         try { | ||||
|             var { protocol, hostname, pathname } = new URL(url); | ||||
|         } catch { | ||||
|             return { action: "deny" }; | ||||
|         } | ||||
| 
 | ||||
|         if (frameName.startsWith("DISCORD_") && pathname === "/popout" && DISCORD_HOSTNAMES.includes(hostname)) { | ||||
|             return createOrFocusPopup(frameName, features); | ||||
|         } | ||||
| 
 | ||||
|         if (url === "about:blank" || (frameName === "authorize" && DISCORD_HOSTNAMES.includes(hostname))) | ||||
|             return { action: "allow" }; | ||||
| 
 | ||||
|         return handleExternalUrl(url, protocol); | ||||
|     }); | ||||
| 
 | ||||
|     win.webContents.on("did-create-window", (win, { frameName }) => { | ||||
|         if (frameName.startsWith("DISCORD_")) setupPopout(win, frameName); | ||||
|     }); | ||||
| } | ||||
							
								
								
									
										116
									
								
								src/main/utils/popout.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								src/main/utils/popout.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,116 @@ | ||||
| /* | ||||
|  * SPDX-License-Identifier: GPL-3.0 | ||||
|  * Aerocord, a vesktop fork for older microsoft NT releases such as NT 6.0, 6.1, 6.2 and 6.3.  | ||||
|  * Credits to vendicated and the rest of the vesktop contribuitors for making Vesktop! | ||||
|  */ | ||||
| 
 | ||||
| import { BrowserWindow, BrowserWindowConstructorOptions } from "electron"; | ||||
| import { Settings } from "main/settings"; | ||||
| 
 | ||||
| import { handleExternalUrl } from "./makeLinksOpenExternally"; | ||||
| 
 | ||||
| const ALLOWED_FEATURES = new Set([ | ||||
|     "width", | ||||
|     "height", | ||||
|     "left", | ||||
|     "top", | ||||
|     "resizable", | ||||
|     "movable", | ||||
|     "alwaysOnTop", | ||||
|     "frame", | ||||
|     "transparent", | ||||
|     "hasShadow", | ||||
|     "closable", | ||||
|     "skipTaskbar", | ||||
|     "backgroundColor", | ||||
|     "menubar", | ||||
|     "toolbar", | ||||
|     "location", | ||||
|     "directories", | ||||
|     "titleBarStyle" | ||||
| ]); | ||||
| 
 | ||||
| const MIN_POPOUT_WIDTH = 320; | ||||
| const MIN_POPOUT_HEIGHT = 180; | ||||
| const DEFAULT_POPOUT_OPTIONS: BrowserWindowConstructorOptions = { | ||||
|     title: "Discord Popout", | ||||
|     backgroundColor: "#2f3136", | ||||
|     minWidth: MIN_POPOUT_WIDTH, | ||||
|     minHeight: MIN_POPOUT_HEIGHT, | ||||
|     frame: Settings.store.customTitleBar !== true, | ||||
|     titleBarStyle: process.platform === "darwin" ? "hidden" : undefined, | ||||
|     trafficLightPosition: | ||||
|         process.platform === "darwin" | ||||
|             ? { | ||||
|                   x: 10, | ||||
|                   y: 3 | ||||
|               } | ||||
|             : undefined, | ||||
|     webPreferences: { | ||||
|         nodeIntegration: false, | ||||
|         contextIsolation: true | ||||
|     }, | ||||
|     autoHideMenuBar: Settings.store.enableMenu | ||||
| }; | ||||
| 
 | ||||
| export const PopoutWindows = new Map<string, BrowserWindow>(); | ||||
| 
 | ||||
| function focusWindow(window: BrowserWindow) { | ||||
|     window.setAlwaysOnTop(true); | ||||
|     window.focus(); | ||||
|     window.setAlwaysOnTop(false); | ||||
| } | ||||
| 
 | ||||
| function parseFeatureValue(feature: string) { | ||||
|     if (feature === "yes") return true; | ||||
|     if (feature === "no") return false; | ||||
| 
 | ||||
|     const n = Number(feature); | ||||
|     if (!isNaN(n)) return n; | ||||
| 
 | ||||
|     return feature; | ||||
| } | ||||
| 
 | ||||
| function parseWindowFeatures(features: string) { | ||||
|     const keyValuesParsed = features.split(","); | ||||
| 
 | ||||
|     return keyValuesParsed.reduce((features, feature) => { | ||||
|         const [key, value] = feature.split("="); | ||||
|         if (ALLOWED_FEATURES.has(key)) features[key] = parseFeatureValue(value); | ||||
| 
 | ||||
|         return features; | ||||
|     }, {}); | ||||
| } | ||||
| 
 | ||||
| export function createOrFocusPopup(key: string, features: string) { | ||||
|     const existingWindow = PopoutWindows.get(key); | ||||
|     if (existingWindow) { | ||||
|         focusWindow(existingWindow); | ||||
|         return <const>{ action: "deny" }; | ||||
|     } | ||||
| 
 | ||||
|     return <const>{ | ||||
|         action: "allow", | ||||
|         overrideBrowserWindowOptions: { | ||||
|             ...DEFAULT_POPOUT_OPTIONS, | ||||
|             ...parseWindowFeatures(features) | ||||
|         } | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| export function setupPopout(win: BrowserWindow, key: string) { | ||||
|     win.setMenuBarVisibility(false); | ||||
| 
 | ||||
|     PopoutWindows.set(key, win); | ||||
| 
 | ||||
|     /* win.webContents.on("will-navigate", (evt, url) => { | ||||
|         // maybe prevent if not origin match
 | ||||
|     })*/ | ||||
| 
 | ||||
|     win.webContents.setWindowOpenHandler(({ url }) => handleExternalUrl(url)); | ||||
| 
 | ||||
|     win.once("closed", () => { | ||||
|         win.removeAllListeners(); | ||||
|         PopoutWindows.delete(key); | ||||
|     }); | ||||
| } | ||||
							
								
								
									
										97
									
								
								src/main/utils/steamOS.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								src/main/utils/steamOS.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,97 @@ | ||||
| /* | ||||
|  * SPDX-License-Identifier: GPL-3.0 | ||||
|  * Aerocord, a vesktop fork for older microsoft NT releases such as NT 6.0, 6.1, 6.2 and 6.3.  | ||||
|  * Credits to vendicated and the rest of the vesktop contribuitors for making Vesktop! | ||||
|  */ | ||||
| 
 | ||||
| import { BrowserWindow, dialog } from "electron"; | ||||
| import { writeFile } from "fs/promises"; | ||||
| import { join } from "path"; | ||||
| 
 | ||||
| import { MessageBoxChoice } from "../constants"; | ||||
| import { State } from "../settings"; | ||||
| 
 | ||||
| // Bump this to re-show the prompt
 | ||||
| const layoutVersion = 2; | ||||
| // Get this from "show details" on the profile after exporting as a shared personal layout or using share with community
 | ||||
| const layoutId = "3080264545"; // Vesktop Layout v2
 | ||||
| const numberRegex = /^[0-9]*$/; | ||||
| 
 | ||||
| let steamPipeQueue = Promise.resolve(); | ||||
| 
 | ||||
| export const isDeckGameMode = process.env.SteamOS === "1" && process.env.SteamGamepadUI === "1"; | ||||
| 
 | ||||
| export function applyDeckKeyboardFix() { | ||||
|     if (!isDeckGameMode) return; | ||||
|     // Prevent constant virtual keyboard spam that eventually crashes Steam.
 | ||||
|     process.env.GTK_IM_MODULE = "None"; | ||||
| } | ||||
| 
 | ||||
| // For some reason SteamAppId is always 0 for non-steam apps so we do this insanity instead.
 | ||||
| function getAppId(): string | null { | ||||
|     // /home/deck/.local/share/Steam/steamapps/shadercache/APPID/fozmediav1
 | ||||
|     const path = process.env.STEAM_COMPAT_MEDIA_PATH; | ||||
|     if (!path) return null; | ||||
|     const pathElems = path?.split("/"); | ||||
|     const appId = pathElems[pathElems.length - 2]; | ||||
|     if (appId.match(numberRegex)) { | ||||
|         console.log(`Got Steam App ID ${appId}`); | ||||
|         return appId; | ||||
|     } | ||||
|     return null; | ||||
| } | ||||
| 
 | ||||
| export function execSteamURL(url: string) { | ||||
|     // This doesn't allow arbitrary execution despite the weird syntax.
 | ||||
|     steamPipeQueue = steamPipeQueue.then(() => | ||||
|         writeFile( | ||||
|             join(process.env.HOME || "/home/deck", ".steam", "steam.pipe"), | ||||
|             // replace ' to prevent argument injection
 | ||||
|             `'${process.env.HOME}/.local/share/Steam/ubuntu12_32/steam' '-ifrunning' '${url.replaceAll("'", "%27")}'\n`, | ||||
|             "utf-8" | ||||
|         ) | ||||
|     ); | ||||
| } | ||||
| 
 | ||||
| export function steamOpenURL(url: string) { | ||||
|     execSteamURL(`steam://openurl/${url}`); | ||||
| } | ||||
| 
 | ||||
| export async function showGamePage() { | ||||
|     const appId = getAppId(); | ||||
|     if (!appId) return; | ||||
|     await execSteamURL(`steam://nav/games/details/${appId}`); | ||||
| } | ||||
| 
 | ||||
| async function showLayout(appId: string) { | ||||
|     execSteamURL(`steam://controllerconfig/${appId}/${layoutId}`); | ||||
| } | ||||
| 
 | ||||
| export async function askToApplySteamLayout(win: BrowserWindow) { | ||||
|     const appId = getAppId(); | ||||
|     if (!appId) return; | ||||
|     if (State.store.steamOSLayoutVersion === layoutVersion) return; | ||||
|     const update = Boolean(State.store.steamOSLayoutVersion); | ||||
| 
 | ||||
|     // Touch screen breaks in some menus when native touch mode is enabled on latest SteamOS beta, remove most of the update specific text once that's fixed.
 | ||||
|     const { response } = await dialog.showMessageBox(win, { | ||||
|         message: `${update ? "Update" : "Apply"} Vesktop Steam Input Layout?`, | ||||
|         detail: `Would you like to ${update ? "Update" : "Apply"} Vesktop's recommended Steam Deck controller settings?
 | ||||
| ${update ? "Click yes using the touchpad" : "Tap yes"}, then press the X button or tap Apply Layout to confirm.${ | ||||
|             update ? " Doing so will undo any customizations you have made." : "" | ||||
|         } | ||||
| ${update ? "Click" : "Tap"} no to keep your current layout.`,
 | ||||
|         buttons: ["Yes", "No"], | ||||
|         cancelId: MessageBoxChoice.Cancel, | ||||
|         defaultId: MessageBoxChoice.Default, | ||||
|         type: "question" | ||||
|     }); | ||||
| 
 | ||||
|     if (State.store.steamOSLayoutVersion !== layoutVersion) { | ||||
|         State.store.steamOSLayoutVersion = layoutVersion; | ||||
|     } | ||||
| 
 | ||||
|     if (response === MessageBoxChoice.Cancel) return; | ||||
| 
 | ||||
|     await showLayout(appId); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user