From 47856a26f1d2b4b0f50a1851c5f4690747629c56 Mon Sep 17 00:00:00 2001 From: Vending Machine Date: Sat, 7 Jun 2025 00:46:49 +0200 Subject: [PATCH] Updater: fix network errors triggering popups (#3436) --- src/Vencord.ts | 6 +++- src/main/updater/common.ts | 5 ++- src/main/updater/http.ts | 28 ++++++++------- src/main/utils/extensions.ts | 7 ++-- src/main/utils/http.ts | 70 ++++++++++++++++++++++++++++++++++++ src/main/utils/simpleGet.ts | 37 ------------------- 6 files changed, 98 insertions(+), 55 deletions(-) create mode 100644 src/main/utils/http.ts delete mode 100644 src/main/utils/simpleGet.ts diff --git a/src/Vencord.ts b/src/Vencord.ts index 48ecce97..f18c7347 100644 --- a/src/Vencord.ts +++ b/src/Vencord.ts @@ -134,7 +134,11 @@ async function init() { if (!IS_WEB && !IS_UPDATER_DISABLED) { runUpdateCheck(); - setInterval(runUpdateCheck, 1000 * 60 * 30); // 30 minutes + + // this tends to get really annoying, so only do this if the user has auto-update without notification enabled + if (Settings.autoUpdate && !Settings.autoUpdateNotification) { + setInterval(runUpdateCheck, 1000 * 60 * 30); // 30 minutes + } } if (IS_DEV) { diff --git a/src/main/updater/common.ts b/src/main/updater/common.ts index 41b9837c..1a0b8da9 100644 --- a/src/main/updater/common.ts +++ b/src/main/updater/common.ts @@ -35,7 +35,10 @@ export function serializeErrors(func: (...args: any[]) => any) { ok: false, error: e instanceof Error ? { // prototypes get lost, so turn error into plain object - ...e + ...e, + message: e.message, + name: e.name, + stack: e.stack } : e }; } diff --git a/src/main/updater/http.ts b/src/main/updater/http.ts index 9d42b5c6..a112dde3 100644 --- a/src/main/updater/http.ts +++ b/src/main/updater/http.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { get } from "@main/utils/simpleGet"; +import { fetchBuffer, fetchJson } from "@main/utils/http"; import { IpcEvents } from "@shared/IpcEvents"; import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent"; import { ipcMain } from "electron"; @@ -31,8 +31,8 @@ import { serializeErrors, VENCORD_FILES } from "./common"; const API_BASE = `https://api.github.com/repos/${gitRemote}`; let PendingUpdates = [] as [string, string][]; -async function githubGet(endpoint: string) { - return get(API_BASE + endpoint, { +async function githubGet(endpoint: string) { + return fetchJson(API_BASE + endpoint, { headers: { Accept: "application/vnd.github+json", // "All API requests MUST include a valid User-Agent header. @@ -46,9 +46,8 @@ async function calculateGitChanges() { const isOutdated = await fetchUpdates(); if (!isOutdated) return []; - const res = await githubGet(`/compare/${gitHash}...HEAD`); + const data = await githubGet(`/compare/${gitHash}...HEAD`); - const data = JSON.parse(res.toString("utf-8")); return data.commits.map((c: any) => ({ // github api only sends the long sha hash: c.sha.slice(0, 7), @@ -58,9 +57,8 @@ async function calculateGitChanges() { } async function fetchUpdates() { - const release = await githubGet("/releases/latest"); + const data = await githubGet("/releases/latest"); - const data = JSON.parse(release.toString()); const hash = data.name.slice(data.name.lastIndexOf(" ") + 1); if (hash === gitHash) return false; @@ -70,16 +68,20 @@ async function fetchUpdates() { PendingUpdates.push([name, browser_download_url]); } }); + return true; } async function applyUpdates() { - await Promise.all(PendingUpdates.map( - async ([name, data]) => writeFile( - join(__dirname, name), - await get(data) - ) - )); + const fileContents = await Promise.all(PendingUpdates.map(async ([name, url]) => { + const contents = await fetchBuffer(url); + return [join(__dirname, name), contents] as const; + })); + + await Promise.all(fileContents.map(async ([filename, contents]) => + writeFile(filename, contents)) + ); + PendingUpdates = []; return true; } diff --git a/src/main/utils/extensions.ts b/src/main/utils/extensions.ts index 1323bd37..1d7559fb 100644 --- a/src/main/utils/extensions.ts +++ b/src/main/utils/extensions.ts @@ -24,7 +24,7 @@ import { join } from "path"; import { DATA_DIR } from "./constants"; import { crxToZip } from "./crxToZip"; -import { get } from "./simpleGet"; +import { fetchBuffer } from "./http"; const extensionCacheDir = join(DATA_DIR, "ExtensionCache"); @@ -69,13 +69,14 @@ export async function installExt(id: string) { } catch (err) { const url = `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=${process.versions.chrome}`; - const buf = await get(url, { + const buf = await fetchBuffer(url, { headers: { "User-Agent": `Electron ${process.versions.electron} ~ Vencord (https://github.com/Vendicated/Vencord)` } }); - await extract(crxToZip(buf), extDir).catch(console.error); + await extract(crxToZip(buf), extDir) + .catch(err => console.error(`Failed to extract extension ${id}`, err)); } session.defaultSession.loadExtension(extDir); diff --git a/src/main/utils/http.ts b/src/main/utils/http.ts new file mode 100644 index 00000000..05dbca40 --- /dev/null +++ b/src/main/utils/http.ts @@ -0,0 +1,70 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { createWriteStream } from "original-fs"; +import { Readable } from "stream"; +import { finished } from "stream/promises"; + +type Url = string | URL; + +export async function checkedFetch(url: Url, options?: RequestInit) { + try { + var res = await fetch(url, options); + } catch (err) { + if (err instanceof Error && err.cause) { + err = err.cause; + } + + throw new Error(`${options?.method ?? "GET"} ${url} failed: ${err}`); + } + + if (res.ok) { + return res; + } + + let message = `${options?.method ?? "GET"} ${url}: ${res.status} ${res.statusText}`; + try { + const reason = await res.text(); + message += `\n${reason}`; + } catch { } + + throw new Error(message); +} + +export async function fetchJson(url: Url, options?: RequestInit) { + const res = await checkedFetch(url, options); + return res.json() as Promise; +} + +export async function fetchBuffer(url: Url, options?: RequestInit) { + const res = await checkedFetch(url, options); + const buf = await res.arrayBuffer(); + + return Buffer.from(buf); +} + +export async function downloadToFile(url: Url, path: string, options?: RequestInit) { + const res = await checkedFetch(url, options); + if (!res.body) { + throw new Error(`Download ${url}: response body is empty`); + } + + // @ts-expect-error weird type conflict + const body = Readable.fromWeb(res.body); + await finished(body.pipe(createWriteStream(path))); +} diff --git a/src/main/utils/simpleGet.ts b/src/main/utils/simpleGet.ts deleted file mode 100644 index 1a8302c0..00000000 --- a/src/main/utils/simpleGet.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -import https from "https"; - -export function get(url: string, options: https.RequestOptions = {}) { - return new Promise((resolve, reject) => { - https.get(url, options, res => { - const { statusCode, statusMessage, headers } = res; - if (statusCode! >= 400) - return void reject(`${statusCode}: ${statusMessage} - ${url}`); - if (statusCode! >= 300) - return void resolve(get(headers.location!, options)); - - const chunks = [] as Buffer[]; - res.on("error", reject); - - res.on("data", chunk => chunks.push(chunk)); - res.once("end", () => resolve(Buffer.concat(chunks))); - }); - }); -}