├── .github ├── FUNDING.yml └── workflows │ └── publish.yml ├── pnpm-workspace.yaml ├── branches ├── mod │ ├── betterdiscord │ │ ├── main.js │ │ └── meta.js │ ├── equicord │ │ ├── main.js │ │ ├── preload.js │ │ └── meta.js │ ├── vencord │ │ ├── main.js │ │ ├── preload.js │ │ └── meta.js │ ├── shelter │ │ ├── meta.js │ │ ├── preload.js │ │ ├── main.js │ │ └── selector-ui.js │ └── moonlight │ │ ├── main.js │ │ └── meta.js ├── ordering.json ├── tool │ └── reactdevtools │ │ ├── ext │ │ ├── icons │ │ │ ├── 16-deadcode.png │ │ │ ├── 16-disabled.png │ │ │ ├── 16-outdated.png │ │ │ ├── 32-deadcode.png │ │ │ ├── 32-disabled.png │ │ │ ├── 32-outdated.png │ │ │ ├── 48-deadcode.png │ │ │ ├── 48-disabled.png │ │ │ ├── 48-outdated.png │ │ │ ├── 128-deadcode.png │ │ │ ├── 128-disabled.png │ │ │ ├── 128-outdated.png │ │ │ ├── 128-production.png │ │ │ ├── 128-restricted.png │ │ │ ├── 128-unminified.png │ │ │ ├── 16-development.png │ │ │ ├── 16-production.png │ │ │ ├── 16-restricted.png │ │ │ ├── 16-unminified.png │ │ │ ├── 32-development.png │ │ │ ├── 32-production.png │ │ │ ├── 32-restricted.png │ │ │ ├── 32-unminified.png │ │ │ ├── 48-development.png │ │ │ ├── 48-production.png │ │ │ ├── 48-restricted.png │ │ │ ├── 48-unminified.png │ │ │ ├── 128-development.png │ │ │ ├── outdated.svg │ │ │ ├── disabled.svg │ │ │ ├── restricted.svg │ │ │ ├── production.svg │ │ │ ├── deadcode.svg │ │ │ └── development.svg │ │ ├── build │ │ │ ├── panel.js │ │ │ ├── prepareInjection.js │ │ │ ├── fileFetcher.js │ │ │ ├── hookSettingsInjector.js │ │ │ ├── proxy.js │ │ │ ├── background.js │ │ │ └── backendManager.js │ │ ├── main.html │ │ ├── popups │ │ │ ├── restricted.html │ │ │ ├── shared.css │ │ │ ├── production.html │ │ │ ├── disabled.html │ │ │ ├── outdated.html │ │ │ ├── development.html │ │ │ ├── shared.js │ │ │ ├── unminified.html │ │ │ └── deadcode.html │ │ ├── manifest.json │ │ ├── panel.html │ │ └── _metadata │ │ │ └── verified_contents.json │ │ ├── meta.js │ │ └── main.js └── tweak │ ├── spotify_embed_volume │ ├── meta.js │ ├── main.js │ └── spotify-embed-volume-script.js │ ├── yt_ad_block │ ├── meta.js │ ├── yt-ad-block-script.js │ └── main.js │ ├── native_titlebar │ ├── meta.js │ └── main.js │ └── yt_embed_fix │ └── meta.js ├── .gitignore ├── .editorconfig ├── src ├── desktopCore │ ├── preload.js │ ├── index.js │ └── main.js ├── common │ ├── fsCache.js │ ├── originatingIp.js │ ├── redirect.js │ ├── proxy │ │ ├── cache.js │ │ └── index.js │ ├── logging │ │ ├── prettyLogger.js │ │ └── index.js │ ├── reusableResponse.js │ ├── config.js │ ├── tracer.js │ └── branchesLoader.js ├── apiV2 │ ├── index.js │ ├── module.js │ ├── manifest.js │ └── patchModule.js ├── apiV1 │ ├── index.js │ ├── host.js │ ├── modules.js │ └── moduleDownload │ │ ├── index.js │ │ └── patchModule.js ├── webhook.js ├── index.js └── dashboard │ ├── index.js │ ├── reporting.js │ ├── dashboard.css │ ├── template.html │ └── dashboard.js ├── shup-ha ├── package.json ├── README.md ├── tsconfig.json ├── wrangler.jsonc ├── .gitignore └── src │ └── index.ts ├── dprint.json ├── Dockerfile ├── config.example.json ├── package.json ├── LICENSE ├── CHANGELOG.md └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: CanadaHonk 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - . 3 | - shup-ha 4 | -------------------------------------------------------------------------------- /branches/mod/betterdiscord/main.js: -------------------------------------------------------------------------------- 1 | require("./betterdiscord.asar"); 2 | -------------------------------------------------------------------------------- /branches/mod/equicord/main.js: -------------------------------------------------------------------------------- 1 | require("./equicord-desktop/equibopMain.js"); 2 | -------------------------------------------------------------------------------- /branches/mod/vencord/main.js: -------------------------------------------------------------------------------- 1 | require("./vencord-desktop/vencordDesktopMain.js"); 2 | -------------------------------------------------------------------------------- /branches/mod/shelter/meta.js: -------------------------------------------------------------------------------- 1 | export const name = "shelter"; 2 | export const description = "Injects shelter"; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | cache 4 | 5 | *.log 6 | 7 | *.crt 8 | *.key 9 | 10 | config.json 11 | 12 | .idea -------------------------------------------------------------------------------- /branches/ordering.json: -------------------------------------------------------------------------------- 1 | ["shelter", "vencord", "equicord", "moonlight", "betterdiscord", "native_titlebar", "reactdevtools"] 2 | -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/16-deadcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/sheltupdate/HEAD/branches/tool/reactdevtools/ext/icons/16-deadcode.png -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/16-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/sheltupdate/HEAD/branches/tool/reactdevtools/ext/icons/16-disabled.png -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/16-outdated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/sheltupdate/HEAD/branches/tool/reactdevtools/ext/icons/16-outdated.png -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/32-deadcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/sheltupdate/HEAD/branches/tool/reactdevtools/ext/icons/32-deadcode.png -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/32-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/sheltupdate/HEAD/branches/tool/reactdevtools/ext/icons/32-disabled.png -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/32-outdated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/sheltupdate/HEAD/branches/tool/reactdevtools/ext/icons/32-outdated.png -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/48-deadcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/sheltupdate/HEAD/branches/tool/reactdevtools/ext/icons/48-deadcode.png -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/48-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/sheltupdate/HEAD/branches/tool/reactdevtools/ext/icons/48-disabled.png -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/48-outdated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/sheltupdate/HEAD/branches/tool/reactdevtools/ext/icons/48-outdated.png -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/128-deadcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/sheltupdate/HEAD/branches/tool/reactdevtools/ext/icons/128-deadcode.png -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/128-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/sheltupdate/HEAD/branches/tool/reactdevtools/ext/icons/128-disabled.png -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/128-outdated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/sheltupdate/HEAD/branches/tool/reactdevtools/ext/icons/128-outdated.png -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/128-production.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/sheltupdate/HEAD/branches/tool/reactdevtools/ext/icons/128-production.png -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/128-restricted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/sheltupdate/HEAD/branches/tool/reactdevtools/ext/icons/128-restricted.png -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/128-unminified.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/sheltupdate/HEAD/branches/tool/reactdevtools/ext/icons/128-unminified.png -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/16-development.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/sheltupdate/HEAD/branches/tool/reactdevtools/ext/icons/16-development.png -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/16-production.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/sheltupdate/HEAD/branches/tool/reactdevtools/ext/icons/16-production.png -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/16-restricted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/sheltupdate/HEAD/branches/tool/reactdevtools/ext/icons/16-restricted.png -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/16-unminified.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/sheltupdate/HEAD/branches/tool/reactdevtools/ext/icons/16-unminified.png -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/32-development.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/sheltupdate/HEAD/branches/tool/reactdevtools/ext/icons/32-development.png -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/32-production.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/sheltupdate/HEAD/branches/tool/reactdevtools/ext/icons/32-production.png -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/32-restricted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/sheltupdate/HEAD/branches/tool/reactdevtools/ext/icons/32-restricted.png -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/32-unminified.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/sheltupdate/HEAD/branches/tool/reactdevtools/ext/icons/32-unminified.png -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/48-development.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/sheltupdate/HEAD/branches/tool/reactdevtools/ext/icons/48-development.png -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/48-production.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/sheltupdate/HEAD/branches/tool/reactdevtools/ext/icons/48-production.png -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/48-restricted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/sheltupdate/HEAD/branches/tool/reactdevtools/ext/icons/48-restricted.png -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/48-unminified.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/sheltupdate/HEAD/branches/tool/reactdevtools/ext/icons/48-unminified.png -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/128-development.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwu/sheltupdate/HEAD/branches/tool/reactdevtools/ext/icons/128-development.png -------------------------------------------------------------------------------- /branches/tweak/spotify_embed_volume/meta.js: -------------------------------------------------------------------------------- 1 | export const name = "Spotify Embed Volume"; 2 | export const description = "Adds a volume slider to Spotify embeds"; 3 | -------------------------------------------------------------------------------- /branches/tool/reactdevtools/meta.js: -------------------------------------------------------------------------------- 1 | export const name = "React Developer Tools"; 2 | export const description = "Adds the React Dev Tools to the web developer panel"; 3 | -------------------------------------------------------------------------------- /branches/tweak/yt_ad_block/meta.js: -------------------------------------------------------------------------------- 1 | export const name = "YouTube Ad Block"; 2 | export const description = "Removes ads in embeds and in the Watch Together activity"; 3 | -------------------------------------------------------------------------------- /branches/tweak/native_titlebar/meta.js: -------------------------------------------------------------------------------- 1 | export const name = "Native Titlebar"; 2 | export const description = "Replaces Discord's custom titlebar with Windows' native one"; 3 | -------------------------------------------------------------------------------- /branches/tweak/yt_embed_fix/meta.js: -------------------------------------------------------------------------------- 1 | export const name = "YouTube Embed Fix"; 2 | export const description = "Makes more videos viewable from within Discord (like UMG blocked ones)"; 3 | -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/build/panel.js: -------------------------------------------------------------------------------- 1 | (()=>{window.container=document.getElementById("container");let n=!1;window.injectStyles=e=>{if(!n){n=!0;const t=e();for(const n of t)document.head.appendChild(n)}}})(); -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | insert_final_newline = false 6 | trim_trailing_whitespace = true 7 | end_of_line = lf 8 | indent_style = tab 9 | max_line_length = 120 10 | tab_width = 3 11 | -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/desktopCore/preload.js: -------------------------------------------------------------------------------- 1 | const { ipcRenderer } = require("electron"); 2 | 3 | const originalPreload = ipcRenderer.sendSync("SHELTUPDATE_FRAMEWORK_ORIGINAL_PRELOAD"); 4 | if (originalPreload) require(originalPreload); 5 | 6 | // __BRANCHES_PRELOAD__ 7 | -------------------------------------------------------------------------------- /src/common/fsCache.js: -------------------------------------------------------------------------------- 1 | import { mkdtempSync } from "fs"; 2 | import { join } from "path"; 3 | import { tmpdir } from "os"; 4 | 5 | export const cacheBase = mkdtempSync(join(tmpdir(), "sheltupdate-cache-")); 6 | 7 | console.log("file system cache at ", cacheBase); 8 | -------------------------------------------------------------------------------- /branches/tweak/yt_ad_block/yt-ad-block-script.js: -------------------------------------------------------------------------------- 1 | const orig = JSON.parse; 2 | JSON.parse = function () { 3 | const res = orig.apply(this, arguments); 4 | ["adPlacements", "adSlots", "playerAds"].forEach((key) => { 5 | if (key in res) { 6 | res[key] = []; 7 | } 8 | }); 9 | return res; 10 | }; 11 | -------------------------------------------------------------------------------- /branches/mod/equicord/preload.js: -------------------------------------------------------------------------------- 1 | const { readFileSync } = require("fs"); 2 | const { join } = require("path"); 3 | const { webFrame } = require("electron"); 4 | 5 | require("./equicord-desktop/equibopPreload.js"); 6 | 7 | webFrame.top.executeJavaScript(readFileSync(join(__dirname, "equicord-desktop/renderer.js"), "utf8")); 8 | -------------------------------------------------------------------------------- /shup-ha/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shup-ha", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "deploy": "wrangler deploy", 7 | "dev": "wrangler dev", 8 | "start": "wrangler dev", 9 | "cf-typegen": "wrangler types" 10 | }, 11 | "devDependencies": { 12 | "typescript": "^5.5.2", 13 | "wrangler": "^4.27.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /branches/mod/moonlight/main.js: -------------------------------------------------------------------------------- 1 | const asarPath = require("electron").app.getAppPath(); 2 | // disableLoad => Does not try to load the original app.asar once injector.js finishes 3 | // disablePersist => Does not try to traditionally persist throughout win32 host updates 4 | await require("./moonlight/injector.js").inject(asarPath, { disableLoad: true, disablePersist: true }); 5 | -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/popups/restricted.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 |

11 | This is a restricted browser page. 12 |
13 | React devtools cannot access this page. 14 |

15 | -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/build/prepareInjection.js: -------------------------------------------------------------------------------- 1 | (()=>{let e;window.addEventListener("message",(function({data:o,source:n}){if(n===window&&o&&"react-devtools-hook"===o.source){const{source:n,payload:s}=o,t={source:n,payload:s};e=t,chrome.runtime.sendMessage(t)}})),window.addEventListener("pageshow",(function({target:o}){e&&o===window.document&&chrome.runtime.sendMessage(e)}))})(); -------------------------------------------------------------------------------- /src/common/originatingIp.js: -------------------------------------------------------------------------------- 1 | export default (c) => { 2 | const cfIp = c.req.header("cf-connecting-ip"); 3 | const peerAddr = c.env.incoming.socket.remoteAddress; 4 | // NOTE: this is a very not ok way to handle x-forwarded-for, but we trust our reverse proxies! mostly! 5 | const xff = c.req.header("x-forwarded-for")?.split(",")[0]; 6 | 7 | return cfIp ?? xff ?? peerAddr; 8 | }; 9 | -------------------------------------------------------------------------------- /branches/mod/vencord/preload.js: -------------------------------------------------------------------------------- 1 | const { readFileSync } = require("fs"); 2 | const { join } = require("path"); 3 | const { webFrame } = require("electron"); 4 | 5 | // run vencord's preload 6 | require("./vencord-desktop/vencordDesktopPreload.js"); 7 | 8 | // inject vencord renderer 9 | webFrame.top.executeJavaScript(readFileSync(join(__dirname, "vencord-desktop/renderer.js"), "utf8")); 10 | -------------------------------------------------------------------------------- /src/apiV2/index.js: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | 3 | import { handleManifest } from "./manifest.js"; 4 | import { handleModule } from "./module.js"; 5 | 6 | export default new Hono() 7 | .get("/:branch/distributions/app/manifests/latest", handleManifest) 8 | .get( 9 | "/:branch/distro/app/:channel/:platform/:arch/:hostVersion/:moduleName/:moduleVersion/full.distro", 10 | handleModule, 11 | ); 12 | -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/popups/shared.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-size: 14px; 3 | } 4 | 5 | body { 6 | margin: 8px; 7 | } 8 | 9 | @media (prefers-color-scheme: dark) { 10 | :root { 11 | color-scheme: dark; 12 | } 13 | 14 | @supports (-moz-appearance:none) { 15 | :root { 16 | background: black; 17 | } 18 | 19 | body { 20 | color: white; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/popups/production.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 |

14 | This page is using the production build of React. ✅ 15 |
16 | Open the developer tools, and "Components" and "Profiler" tabs will appear to the right. 17 |

18 | -------------------------------------------------------------------------------- /dprint.json: -------------------------------------------------------------------------------- 1 | { 2 | "lineWidth": 120, 3 | "useTabs": true, 4 | "indentWidth": 3, 5 | "newLineKind": "lf", 6 | 7 | "biome": {}, 8 | "yaml": {}, 9 | "markup": {}, 10 | "excludes": ["pnpm-lock.yaml", "**/node_modules", "**/reactdevtools/ext", "src/dashboard/template.html"], 11 | "plugins": [ 12 | "https://plugins.dprint.dev/g-plane/markup_fmt-v0.13.0.wasm", 13 | "https://plugins.dprint.dev/g-plane/pretty_yaml-v0.5.0.wasm", 14 | "https://plugins.dprint.dev/biome-0.6.0.wasm" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /branches/tweak/yt_ad_block/main.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const electron = require("electron"); 4 | const script = fs.readFileSync(path.join(__dirname, "yt-ad-block-script.js")).toString(); 5 | 6 | electron.app.on("browser-window-created", (_, win) => { 7 | win.webContents.on("frame-created", (_, { frame }) => { 8 | frame.on("dom-ready", () => { 9 | if (!frame.url.startsWith("https://www.youtube.com/embed/")) return; 10 | frame.executeJavaScript(script); 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/apiV1/index.js: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { handleNonSquirrel, handleSquirrel } from "./host.js"; 3 | import { handleModuleDownload } from "./moduleDownload/index.js"; 4 | import { handleModules } from "./modules.js"; 5 | 6 | export default new Hono() 7 | .get("/:branch/updates/:channel", handleNonSquirrel) 8 | .get("/:branch/updates/:channel/releases", handleSquirrel) 9 | .get("/:branch/modules/:channel/:module/:version", handleModuleDownload) 10 | .get("/:branch/modules/:channel/versions.json", handleModules); 11 | -------------------------------------------------------------------------------- /src/desktopCore/index.js: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "fs"; 2 | import { join } from "path"; 3 | import { srcDir } from "../common/config.js"; 4 | import { createHash } from "crypto"; 5 | 6 | export const dcMain = readFileSync(join(srcDir, "desktopCore", "main.js"), "utf8"); 7 | export const dcPreload = readFileSync(join(srcDir, "desktopCore", "preload.js"), "utf8"); 8 | 9 | export const dcVersion = parseInt( 10 | createHash("sha256") 11 | .update(dcMain + dcPreload) 12 | .digest("hex") 13 | .substring(0, 2), 14 | 16, 15 | ); 16 | -------------------------------------------------------------------------------- /branches/tweak/spotify_embed_volume/main.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const electron = require("electron"); 4 | const script = fs.readFileSync(path.join(__dirname, "spotify-embed-volume-script.js")).toString(); 5 | 6 | electron.app.on("browser-window-created", (_, win) => { 7 | win.webContents.on("frame-created", (_, { frame }) => { 8 | frame.on("dom-ready", () => { 9 | if (!frame.url.startsWith("https://open.spotify.com/embed/")) return; 10 | frame.executeJavaScript(script); 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/popups/disabled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 |

14 | This page doesn’t appear to be using React. 15 |
16 | If this seems wrong, follow the troubleshooting instructions. 17 |

18 | -------------------------------------------------------------------------------- /shup-ha/README.md: -------------------------------------------------------------------------------- 1 | # sheltupdate High Availability 2 | 3 | This Cloudflare Worker proxies requests to the *real* sheltupdate server, and provides automatic rollover to another 4 | origin server until the primary server comes back up again. 5 | 6 | If all origin servers are down, it will also serve unpatched files from the Discord API, which is bad as shelter then 7 | will not work, but it is preferable over serving nothing (Discord will just not open in this case). 8 | 9 | It pushes webhooks when any downtime events occur. 10 | 11 | todo: incidents page, improve webhooks 12 | -------------------------------------------------------------------------------- /src/common/redirect.js: -------------------------------------------------------------------------------- 1 | import { getProxyURL } from "./proxy/index.js"; 2 | import { reportRedirected } from "../dashboard/reporting.js"; 3 | import { config } from "./config.js"; 4 | import { withSection } from "./tracer.js"; 5 | 6 | export default withSection("redirect", async (span, context, base = config.apiBases.v1) => { 7 | reportRedirected(); 8 | 9 | const rUrl = context.req.url.replace(/.*:\/\/[^/]*/, ""); 10 | const proxyUrl = `${base}${getProxyURL(rUrl)}`; 11 | 12 | span.setAttribute("redirect.proxy_url", proxyUrl); 13 | 14 | return context.redirect(proxyUrl); 15 | }); 16 | -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/build/fileFetcher.js: -------------------------------------------------------------------------------- 1 | (()=>{function fetchResource(e){const reject=t=>{chrome.runtime.sendMessage({source:"react-devtools-fetch-resource-content-script",payload:{type:"fetch-file-with-cache-error",url:e,value:t}})};fetch(e,{cache:"force-cache",signal:AbortSignal.timeout(6e4)}).then((t=>{t.ok?t.text().then((t=>{return c=t,void chrome.runtime.sendMessage({source:"react-devtools-fetch-resource-content-script",payload:{type:"fetch-file-with-cache-complete",url:e,value:c}});var c})).catch((e=>reject(null))):reject(null)}),(e=>reject(null)))}chrome.runtime.onMessage.addListener((e=>{"devtools-page"===e?.source&&"fetch-file-with-cache"===e?.payload?.type&&fetchResource(e.payload.url)}))})(); -------------------------------------------------------------------------------- /src/common/proxy/cache.js: -------------------------------------------------------------------------------- 1 | import { config } from "../config.js"; 2 | import { withSection } from "../tracer.js"; 3 | 4 | const cacheStore = {}; 5 | 6 | const cacheCleaner = withSection("proxy cache cleaner", () => { 7 | for (let k in cacheStore) { 8 | const v = cacheStore[k]; 9 | 10 | if ((Date.now() - v.lastUsed) / 1000 / 60 / 60 > config.proxy.cache.lastUsedRemoveHours) { 11 | // If anything cached was last used longer than an hour ago, remove it 12 | delete cacheStore[k]; 13 | } 14 | } 15 | }); 16 | 17 | export const get = (key) => cacheStore[key]; 18 | export const set = (key, value) => { 19 | cacheStore[key] = value; 20 | }; 21 | 22 | setInterval(cacheCleaner, 1000 * 60 * 60); 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # using two containers means we don't have to upload a docker layer containing pnpm, so it should be smaller. 2 | 3 | # using node 18 here for arm/v7 compat (https://github.com/nodejs/docker-node/issues/1798) 4 | FROM node:18-alpine AS pnpm-container 5 | 6 | RUN npm i -g pnpm 7 | 8 | COPY package.json package.json 9 | COPY pnpm-lock.yaml pnpm-lock.yaml 10 | 11 | RUN pnpm i --frozen-lockfile --prod 12 | 13 | FROM node:23-alpine 14 | 15 | COPY src src 16 | COPY branches branches 17 | COPY package.json package.json 18 | COPY CHANGELOG.md CHANGELOG.md 19 | 20 | COPY --from=pnpm-container node_modules node_modules 21 | 22 | EXPOSE 8080/tcp 23 | ENTRYPOINT ["node", "src/index.js"] 24 | STOPSIGNAL SIGKILL 25 | -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/popups/outdated.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 |

14 | This page is using an outdated version of React. ⌛ 15 |

16 |

17 | We recommend updating React to ensure that you receive important bugfixes and performance improvements. 18 |
19 |
20 | You can find the upgrade instructions on the React blog. 21 |

22 |
23 |

24 | Open the developer tools, and "Components" and "Profiler" tabs will appear to the right. 25 |

26 | -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/popups/development.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 |

14 | This page is using the development build of React. 🚧 15 |

16 |

17 | Note that the development build is not suitable for production. 18 |
19 | Make sure to use the production build before deployment. 20 |

21 |
22 |

23 | Open the developer tools, and "Components" and "Profiler" tabs will appear to the right. 24 |

25 | -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/build/hookSettingsInjector.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("message",(async function messageListener(e){if(e.source===window&&"react-devtools-hook-installer"===e.data.source&&e.data.payload.handshake){const e=await chrome.storage.local.get();"boolean"!=typeof e.appendComponentStack&&(e.appendComponentStack=!0),"boolean"!=typeof e.breakOnConsoleErrors&&(e.breakOnConsoleErrors=!1),"boolean"!=typeof e.showInlineWarningsAndErrors&&(e.showInlineWarningsAndErrors=!0),"boolean"!=typeof e.hideConsoleLogsInStrictMode&&(e.hideConsoleLogsInStrictMode=!1),window.postMessage({source:"react-devtools-hook-settings-injector",payload:{settings:e}}),window.removeEventListener("message",messageListener)}})),window.postMessage({source:"react-devtools-hook-settings-injector",payload:{handshake:!0}}); -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/popups/shared.js: -------------------------------------------------------------------------------- 1 | /* globals chrome */ 2 | 3 | 'use strict'; 4 | 5 | document.addEventListener('DOMContentLoaded', function () { 6 | // Make links work 7 | const links = document.getElementsByTagName('a'); 8 | for (let i = 0; i < links.length; i++) { 9 | (function () { 10 | const ln = links[i]; 11 | const location = ln.href; 12 | ln.onclick = function () { 13 | chrome.tabs.create({active: true, url: location}); 14 | return false; 15 | }; 16 | })(); 17 | } 18 | 19 | // Work around https://bugs.chromium.org/p/chromium/issues/detail?id=428044 20 | document.body.style.opacity = 0; 21 | document.body.style.transition = 'opacity ease-out .4s'; 22 | requestAnimationFrame(function () { 23 | document.body.style.opacity = 1; 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/popups/unminified.html: -------------------------------------------------------------------------------- 1 | 2 | 17 |

18 | This page is using an unminified build of React. 🚧 19 |

20 |

21 | The React build on this page appears to be unminified. 22 |
23 | This makes its size larger, and causes React to run slower. 24 |
25 |
26 | Make sure to set up minification before deployment. 27 |

28 |
29 |

30 | Open the developer tools, and "Components" and "Profiler" tabs will appear to the right. 31 |

32 | -------------------------------------------------------------------------------- /src/apiV2/module.js: -------------------------------------------------------------------------------- 1 | import { getFinal } from "./patchModule.js"; 2 | import { reportEndpoint } from "../dashboard/reporting.js"; 3 | import { getBranch } from "../common/branchesLoader.js"; 4 | import { populateReqAttrs, withSection } from "../common/tracer.js"; 5 | 6 | export const handleModule = withSection("v2 download module", (span, c) => { 7 | if (!getBranch(c.req.param("branch"))) { 8 | return c.notFound("Invalid sheltupdate branch"); 9 | } 10 | 11 | populateReqAttrs(span, c); 12 | 13 | reportEndpoint("v2_module"); 14 | 15 | const buf = getFinal(c.req); 16 | 17 | c.header("Content-Type", "application/octet-stream"); 18 | // hono annoyingly does not send content length by default, and dicor no likey that 19 | // https://github.com/honojs/hono/commit/501854f 20 | c.header("Content-Length", buf.byteLength); 21 | 22 | return c.body(buf); 23 | }); 24 | -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/popups/deadcode.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 |

14 | This page includes an extra development build of React. 🚧 15 |

16 |

17 | The React build on this page includes both development and production versions because dead code elimination has not been applied correctly. 18 |
19 |
20 | This makes its size larger, and causes React to run slower. 21 |
22 |
23 | Make sure to set up dead code elimination before deployment. 24 |

25 |
26 |

27 | Open the developer tools, and "Components" and "Profiler" tabs will appear to the right. 28 |

29 | -------------------------------------------------------------------------------- /config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 8080, 3 | "host": "https://inject.shelter.uwu.network", 4 | "stats": true, 5 | "setupIntervalHours": 3, 6 | "tracing": { 7 | "service": "sheltupdate", 8 | "log": true, 9 | "otlpEndpoint": "http://localhost:4318/v1/traces", 10 | "otlpType": "protobuf" 11 | }, 12 | "proxy": { 13 | "cache": { 14 | "lastUsedRemoveHours": 1, 15 | "maxMinutesToUseCached": 30 16 | }, 17 | "useragent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) discord/0.0.76 Chrome/128.0.6613.186 Electron/32.2.2 Safari/537.36" 18 | }, 19 | "apiBases": { 20 | "v1": "https://discord.com/api", 21 | "v2": "https://discord.com/api/updates" 22 | }, 23 | "webhook": { 24 | "enable": false, 25 | "url": "https://discord.com/api/webhooks/X", 26 | "username": "GooseUpdate", 27 | "avatarUrl": "https://cdn.discordapp.com/avatars/760559484342501406/5125aff2f446ad7c45cf2dfd6abf92ed.png" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /branches/tool/reactdevtools/main.js: -------------------------------------------------------------------------------- 1 | const { app, session } = require("electron"); 2 | const { join } = require("path"); 3 | 4 | // apparently, manifest v3 extensions don't have their service workers started automatically? 5 | // https://github.com/electron/electron/issues/41613#issuecomment-2644018998 6 | function launchExtensionBackgroundWorkers(session) { 7 | return Promise.all( 8 | session.getAllExtensions().map(async (extension) => { 9 | const manifest = extension.manifest; 10 | if (manifest.manifest_version === 3 && manifest?.background?.service_worker) { 11 | await session.serviceWorkers.startWorkerForScope(extension.url); 12 | console.log("[sheltupdate-react-devtools] Manually starting background worker"); 13 | } 14 | }), 15 | ); 16 | } 17 | 18 | app.whenReady().then(async () => { 19 | await session.defaultSession.loadExtension(join(__dirname, "ext")); 20 | await launchExtensionBackgroundWorkers(session.defaultSession); 21 | }); 22 | -------------------------------------------------------------------------------- /src/apiV1/host.js: -------------------------------------------------------------------------------- 1 | import { getBranch } from "../common/branchesLoader.js"; 2 | import { reportEndpoint } from "../dashboard/reporting.js"; 3 | import basicProxy from "../common/proxy/index.js"; 4 | import { populateReqAttrs, withSection } from "../common/tracer.js"; 5 | 6 | export const handleNonSquirrel = withSection("v1 host linux", async (span, c) => { 7 | // Non-Squirrel (Linux) 8 | if (!getBranch(c.req.param("branch"))) { 9 | return c.notFound("Invalid sheltupdate branch"); 10 | } 11 | 12 | reportEndpoint("v1_host_notsquirrel"); 13 | 14 | populateReqAttrs(span, c); 15 | 16 | return basicProxy(c); 17 | }); 18 | 19 | export const handleSquirrel = withSection("v1 host squirrel", async (span, c) => { 20 | // Squirrel (non-Linux) 21 | if (!getBranch(c.req.param("branch"))) { 22 | return c.notFound("Invalid sheltupdate branch"); 23 | } 24 | 25 | reportEndpoint("v1_host_squirrel"); 26 | 27 | populateReqAttrs(span, c); 28 | 29 | return basicProxy(c); 30 | }); 31 | -------------------------------------------------------------------------------- /branches/mod/equicord/meta.js: -------------------------------------------------------------------------------- 1 | import { rm } from "fs/promises"; 2 | import { createWriteStream, mkdirSync } from "fs"; 3 | import { join } from "path"; 4 | import { Writable } from "stream"; 5 | 6 | export const name = "Equicord"; 7 | export const description = "Injects Equicord (a Vencord fork); This is not an officially supported Vencord install method"; 8 | export const incompatibilities = ["betterdiscord", "vencord", "moonlight"]; 9 | 10 | export async function setup(target, log) { 11 | const releaseUrl = "https://github.com/Equicord/Equicord/releases/download/latest/"; 12 | 13 | mkdirSync(join(target, "equicord-desktop"), { recursive: true }); 14 | 15 | for (const f of ["equibopMain.js", "equibopPreload.js", "renderer.js", "renderer.css"]) { 16 | log(`Downloading ${f}...`); 17 | 18 | const p = join(target, "equicord-desktop", f); 19 | await rm(p, { force: true }); 20 | 21 | const req = await fetch(releaseUrl + f); 22 | await req.body.pipeTo(Writable.toWeb(createWriteStream(p))); 23 | } 24 | 25 | log("Done!"); 26 | } 27 | -------------------------------------------------------------------------------- /branches/mod/betterdiscord/meta.js: -------------------------------------------------------------------------------- 1 | import { rm } from "fs/promises"; 2 | import { createWriteStream } from "fs"; 3 | import { join } from "path"; 4 | import { Writable } from "stream"; 5 | 6 | export const name = "BetterDiscord"; 7 | export const description = "Injects BetterDiscord; This is not an officially supported BetterDiscord install method"; 8 | export const incompatibilities = ["vencord", "equicord", "moonlight"]; 9 | 10 | export async function setup(target, log) { 11 | log("Downloading latest asar..."); 12 | 13 | const url = await fetch("https://api.github.com/repos/BetterDiscord/BetterDiscord/releases/latest") 14 | .then((r) => r.json()) 15 | .then((j) => j.assets.find((a) => a.browser_download_url?.includes(".asar")).browser_download_url); 16 | 17 | const asarPath = join(target, "betterdiscord.asar"); 18 | 19 | const fileRes = await fetch(url); 20 | 21 | // pipe into file 22 | await rm(asarPath, { force: true }); 23 | await fileRes.body.pipeTo(Writable.toWeb(createWriteStream(asarPath))); 24 | 25 | log("Done!"); 26 | } 27 | -------------------------------------------------------------------------------- /branches/mod/vencord/meta.js: -------------------------------------------------------------------------------- 1 | import { rm } from "fs/promises"; 2 | import { createWriteStream, mkdirSync } from "fs"; 3 | import { join } from "path"; 4 | import { Writable } from "stream"; 5 | 6 | export const name = "Vencord"; 7 | export const description = "Injects Vencord; This is not an officially supported Vencord install method"; 8 | export const incompatibilities = ["betterdiscord", "equicord", "moonlight"]; 9 | 10 | export async function setup(target, log) { 11 | const releaseUrl = "https://github.com/Vendicated/Vencord/releases/download/devbuild/"; 12 | 13 | mkdirSync(join(target, "vencord-desktop"), { recursive: true }); 14 | 15 | for (const f of ["vencordDesktopMain.js", "vencordDesktopPreload.js", "renderer.js", "vencordDesktopRenderer.css"]) { 16 | log(`Downloading ${f}...`); 17 | 18 | const p = join(target, "vencord-desktop", f); 19 | await rm(p, { force: true }); 20 | 21 | const req = await fetch(releaseUrl + f); 22 | await req.body.pipeTo(Writable.toWeb(createWriteStream(p))); 23 | } 24 | 25 | log("Done!"); 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sheltupdate", 3 | "private": true, 4 | "description": "A modern fork of GooseUpdate to inject shelter with.", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "format": "dprint fmt" 8 | }, 9 | "author": "GooseMod, Yellowsink", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@electron/asar": "^3.2.17", 13 | "@hono/node-server": "^1.13.7", 14 | "@hono/otel": "^0.5.0", 15 | "@opentelemetry/api": "^1.9.0", 16 | "@opentelemetry/context-async-hooks": "^2.0.1", 17 | "@opentelemetry/exporter-trace-otlp-grpc": "^0.203.0", 18 | "@opentelemetry/exporter-trace-otlp-http": "^0.203.0", 19 | "@opentelemetry/exporter-trace-otlp-proto": "^0.203.0", 20 | "@opentelemetry/sdk-node": "^0.203.0", 21 | "archiver": "^5.0.2", 22 | "glob": "^7.1.6", 23 | "hono": "^4.6.12", 24 | "tar": "^6.0.5", 25 | "unzipper": "^0.10.11" 26 | }, 27 | "devDependencies": { 28 | "dprint": "^0.47.6" 29 | }, 30 | "type": "module", 31 | "packageManager": "pnpm@8.12.1+sha1.aa961ffce9b6eaa56307d9b5ff7e984f25b7eb58" 32 | } 33 | -------------------------------------------------------------------------------- /branches/mod/moonlight/meta.js: -------------------------------------------------------------------------------- 1 | import { mkdir, rm } from "fs/promises"; 2 | import { join } from "path"; 3 | import { Writable } from "stream"; 4 | import tar from "tar"; 5 | 6 | export const name = "Moonlight"; 7 | export const description = "Injects moonlight; This is not an officially supported moonlight install method"; 8 | export const incompatibilities = ["vencord", "equicord", "betterdiscord"]; 9 | 10 | export async function setup(target, log) { 11 | log("Downloading latest bundle..."); 12 | 13 | const url = await fetch("https://api.github.com/repos/moonlight-mod/moonlight/releases/latest") 14 | .then((r) => r.json()) 15 | .then((j) => j.assets.find((a) => a.name === "dist.tar.gz").browser_download_url); 16 | 17 | const moonlightPath = join(target, "moonlight"); 18 | const fileRes = await fetch(url); 19 | 20 | await rm(moonlightPath, { recursive: true, force: true }); 21 | await mkdir(moonlightPath); 22 | 23 | await fileRes.body.pipeTo( 24 | Writable.toWeb( 25 | tar.x({ 26 | cwd: moonlightPath, 27 | }), 28 | ), 29 | ); 30 | 31 | log("Done!"); 32 | } 33 | -------------------------------------------------------------------------------- /src/webhook.js: -------------------------------------------------------------------------------- 1 | import { config } from "./common/config.js"; 2 | import { statsState } from "./dashboard/reporting.js"; 3 | 4 | if (config.webhook.enable) { 5 | const url = config.webhook.url; 6 | 7 | const responseBase = { 8 | content: "", 9 | username: config.webhook.username, 10 | avatar_url: config.webhook.avatar_url, 11 | }; 12 | 13 | const send = async (content, embeds = undefined) => { 14 | if (!url) return; 15 | 16 | const json = Object.assign(responseBase, { content, embeds }); 17 | 18 | try { 19 | await fetch(url, { body: JSON.stringify(json), method: "POST" }); 20 | } catch (e) { 21 | console.log(e.response); 22 | } 23 | }; 24 | 25 | const sendStats = async () => { 26 | await send("", [ 27 | { 28 | title: "Stats", 29 | fields: [ 30 | { 31 | name: "Users", 32 | value: Object.values(statsState.uniqueUsers).length, 33 | inline: true, 34 | }, 35 | ], 36 | }, 37 | ]); 38 | }; 39 | 40 | send("", [ 41 | { 42 | title: "Started Up", 43 | }, 44 | ]); 45 | 46 | setTimeout(sendStats, 60 * 1000); 47 | setInterval(sendStats, 60 * 60 * 1000); 48 | } 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 GooseMod 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/apiV1/modules.js: -------------------------------------------------------------------------------- 1 | import basicProxy from "../common/proxy/index.js"; 2 | import { ensureBranchIsReady, getBranch } from "../common/branchesLoader.js"; 3 | import { reportEndpoint, reportUniqueUser } from "../dashboard/reporting.js"; 4 | import originatingIp from "../common/originatingIp.js"; 5 | import { populateReqAttrs, withSection } from "../common/tracer.js"; 6 | 7 | export const handleModules = withSection("v1 module update check", async (span, c) => { 8 | const { branch, channel } = c.req.param(); 9 | const { platform, host_version } = c.req.query(); 10 | 11 | await ensureBranchIsReady(branch); 12 | 13 | const branchObj = getBranch(branch); 14 | if (!branchObj) { 15 | return c.notFound("Invalid sheltupdate branch"); 16 | } 17 | 18 | reportEndpoint("v1_modules"); 19 | 20 | populateReqAttrs(span, c); 21 | 22 | if (platform === "linux" || platform === "win" || platform === "osx") 23 | reportUniqueUser(originatingIp(c), platform, `${platform} ${host_version}`, channel, branch, 1); 24 | 25 | let json = await basicProxy(c).then((r) => r.json()); 26 | 27 | if (json.discord_desktop_core) 28 | json.discord_desktop_core = parseInt(`${branchObj.version}${json.discord_desktop_core.toString()}`); 29 | 30 | return c.json(json); 31 | }); 32 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { otel } from "@hono/otel"; 3 | import { createMiddleware } from "hono/factory"; 4 | import { serve } from "@hono/node-server"; 5 | 6 | import { config, changelog, version } from "./common/config.js"; 7 | import { getSingleBranchMetas } from "./common/branchesLoader.js"; 8 | import "./common/tracer.js"; 9 | 10 | // API handlers 11 | import apiV1 from "./apiV1/index.js"; 12 | import apiV2 from "./apiV2/index.js"; 13 | import dashboard from "./dashboard/index.js"; 14 | 15 | // kick off webhook 16 | import "./webhook.js"; 17 | 18 | const app = new Hono() 19 | .use(otel()) 20 | .use( 21 | createMiddleware(async (c, next) => { 22 | await next(); 23 | c.header("Server", `sheltupdate/r${version}`); 24 | }), 25 | ) 26 | .route("/", apiV1) 27 | .route("/", apiV2) 28 | .route("/", dashboard) 29 | .get("/sheltupdate_branches", async (c) => { 30 | // cors 31 | c.header("Access-Control-Allow-Origin", "*"); 32 | return c.json(getSingleBranchMetas()); 33 | }) 34 | .get("/sheltupdate_changelog", async (c) => { 35 | // cors 36 | c.header("Access-Control-Allow-Origin", "*"); 37 | return c.text(changelog); 38 | }); 39 | 40 | serve({ 41 | fetch: app.fetch, 42 | port: config.port, 43 | }); 44 | -------------------------------------------------------------------------------- /branches/tweak/native_titlebar/main.js: -------------------------------------------------------------------------------- 1 | const electron = require("electron"); 2 | 3 | class BrowserWindow extends electron.BrowserWindow { 4 | constructor(options) { 5 | delete options.frame; 6 | super(options); 7 | 8 | // Account for popout windows 9 | const origSetWOH = this.webContents.setWindowOpenHandler; 10 | this.webContents.setWindowOpenHandler = function () { 11 | const origHandler = arguments[0]; 12 | arguments[0] = function () { 13 | const details = origHandler.apply(this, arguments); 14 | 15 | if (details?.overrideBrowserWindowOptions) { 16 | delete details.overrideBrowserWindowOptions.frame; 17 | } 18 | return details; 19 | }; 20 | return origSetWOH.apply(this, arguments); 21 | }; 22 | } 23 | } 24 | 25 | electron.app.on("browser-window-created", (_, win) => { 26 | // Deleting options.frame in popouts makes their menu bar visible again, so we need to hide it. 27 | win.setMenuBarVisibility(false); 28 | win.webContents.on("dom-ready", () => { 29 | win.webContents.insertCSS("[class *= withFrame][class *= titleBar] { display: none !important; }"); 30 | }); 31 | }); 32 | 33 | const electronPath = require.resolve("electron"); 34 | delete require.cache[electronPath].exports; 35 | require.cache[electronPath].exports = { 36 | ...electron, 37 | BrowserWindow, 38 | }; 39 | -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/build/proxy.js: -------------------------------------------------------------------------------- 1 | (()=>{"use strict";function injectProxy({target:e}){if(!window.__REACT_DEVTOOLS_PROXY_INJECTED__){window.__REACT_DEVTOOLS_PROXY_INJECTED__=!0,connectPort(),sayHelloToBackendManager();const e=setInterval((()=>{n?clearInterval(e):sayHelloToBackendManager()}),500)}}window.addEventListener("pagereveal",injectProxy),window.addEventListener("pageshow",injectProxy),window.addEventListener("pagehide",(function({target:e}){e===window.document&&delete window.__REACT_DEVTOOLS_PROXY_INJECTED__}));let e=null,n=!1;function sayHelloToBackendManager(){window.postMessage({source:"react-devtools-content-script",hello:!0},"*")}function handleMessageFromDevtools(e){window.postMessage({source:"react-devtools-content-script",payload:e},"*")}function handleMessageFromPage(o){if(o.source===window&&o.data)switch(o.data.source){case"react-devtools-bridge":n=!0,e.postMessage(o.data.payload);break;case"react-devtools-backend-manager":{const{source:e,payload:n}=o.data;chrome.runtime.sendMessage({source:e,payload:n});break}}}function handleDisconnect(){window.removeEventListener("message",handleMessageFromPage),e=null,connectPort()}function connectPort(){e=chrome.runtime.connect({name:"proxy"}),window.addEventListener("message",handleMessageFromPage),e.onMessage.addListener(handleMessageFromDevtools),e.onDisconnect.addListener(handleDisconnect)}})(); -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/outdated.svg: -------------------------------------------------------------------------------- 1 | outdated -------------------------------------------------------------------------------- /src/dashboard/index.js: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "fs"; 2 | import { join } from "path"; 3 | import { statsState } from "./reporting.js"; 4 | import { srcDir, startTime, version } from "../common/config.js"; 5 | import { getSingleBranchMetas } from "../common/branchesLoader.js"; 6 | import { Hono } from "hono"; 7 | 8 | const html = readFileSync(join(srcDir, "dashboard", "template.html"), "utf8"); 9 | const css_ = readFileSync(join(srcDir, "dashboard", "dashboard.css"), "utf8"); 10 | const js__ = readFileSync(join(srcDir, "dashboard", "dashboard.js"), "utf8"); 11 | 12 | const hitRatio = ({ hit, miss }) => (hit || miss ? ((100 * hit) / (hit + miss)).toFixed(1) + "%" : "N/A"); 13 | 14 | const template = (temp) => 15 | temp 16 | .replaceAll("__USER_COUNT__", Object.values(statsState.uniqueUsers).length) 17 | .replaceAll("__VERSION__", version) 18 | .replaceAll("__START_TIME__", startTime) 19 | .replaceAll("__STATE__", JSON.stringify(statsState)) 20 | .replaceAll("__BRANCHES__", JSON.stringify(getSingleBranchMetas())) 21 | .replaceAll("__CACHE_PROX__", hitRatio(statsState.proxyCacheHitRatio)) 22 | .replaceAll("__CACHE_V1__", hitRatio(statsState.v1ModuleCacheHitRatio)) 23 | .replaceAll("__CACHE_V2__", hitRatio(statsState.v2ManifestCacheHitRatio)); 24 | 25 | export default new Hono() 26 | .get("/", (c) => c.html(template(html))) 27 | .get("/dashboard.css", (c) => { 28 | c.header("Content-Type", "text/css"); 29 | return c.body(template(css_)); 30 | }) 31 | .get("/dashboard.js", (c) => { 32 | c.header("Content-Type", "text/javascript"); 33 | return c.body(template(js__)); 34 | }); 35 | -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/disabled.svg: -------------------------------------------------------------------------------- 1 | disabled -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/restricted.svg: -------------------------------------------------------------------------------- 1 | disabled -------------------------------------------------------------------------------- /src/common/logging/prettyLogger.js: -------------------------------------------------------------------------------- 1 | const INDENT_CHUNK = " "; 2 | 3 | let indent = ""; 4 | let sections = []; 5 | let cols = []; 6 | 7 | // we could derive the colour with math but that causes collisions and stuff :( 8 | let sectionCols = new Map(); 9 | let lastCol = -1; 10 | 11 | export function log(...stuff) { 12 | if (sections.length) console.log(`${indent}[\x1B[${cols.at(-1)}m${sections.at(-1)}\x1B[39m]`, ...stuff); 13 | else console.log(indent, ...stuff); 14 | } 15 | 16 | export function resetLogger() { 17 | indent = ""; 18 | sections = []; 19 | cols = []; 20 | } 21 | 22 | export function startLogSection(name) { 23 | indent += INDENT_CHUNK; 24 | sections.push(name); 25 | 26 | const ccol = sectionCols.get(name); 27 | if (ccol) cols.push(31 + ccol); 28 | else { 29 | lastCol = ++lastCol % 6; 30 | cols.push(31 + lastCol); 31 | sectionCols.set(name, lastCol); 32 | } 33 | } 34 | 35 | export function logEndSection() { 36 | sections.pop(); 37 | cols.pop(); 38 | indent = indent.slice(INDENT_CHUNK.length); 39 | } 40 | /* 41 | 42 | export const withLogSection = 43 | (name, fn) => 44 | (...args) => 45 | logSection(name, fn, ...args); 46 | 47 | export function logSection(name, fn, ...args) { 48 | startLogSection(name); 49 | try { 50 | const res = fn(...args); 51 | 52 | if (res instanceof Promise) 53 | return res.then( 54 | (r) => { 55 | logEndSection(); 56 | return r; 57 | }, 58 | (e) => { 59 | logEndSection(); 60 | throw e; 61 | }, 62 | ); 63 | else { 64 | logEndSection(); 65 | return res; 66 | } 67 | } catch (e) { 68 | logEndSection(); 69 | throw e; 70 | } 71 | } 72 | */ 73 | -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/production.svg: -------------------------------------------------------------------------------- 1 | production -------------------------------------------------------------------------------- /shup-ha/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 6 | "target": "es2021", 7 | /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 8 | "lib": ["es2021"], 9 | /* Specify what JSX code is generated. */ 10 | "jsx": "react-jsx", 11 | 12 | /* Specify what module code is generated. */ 13 | "module": "es2022", 14 | /* Specify how TypeScript looks up a file from a given module specifier. */ 15 | "moduleResolution": "Bundler", 16 | /* Enable importing .json files */ 17 | "resolveJsonModule": true, 18 | 19 | /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 20 | "allowJs": true, 21 | /* Enable error reporting in type-checked JavaScript files. */ 22 | "checkJs": false, 23 | 24 | /* Disable emitting files from a compilation. */ 25 | "noEmit": true, 26 | 27 | /* Ensure that each file can be safely transpiled without relying on other imports. */ 28 | "isolatedModules": true, 29 | /* Allow 'import x from y' when a module doesn't have a default export. */ 30 | "allowSyntheticDefaultImports": true, 31 | /* Ensure that casing is correct in imports. */ 32 | "forceConsistentCasingInFileNames": true, 33 | 34 | /* Enable all strict type-checking options. */ 35 | "strict": true, 36 | 37 | /* Skip type checking all .d.ts files. */ 38 | "skipLibCheck": true, 39 | "types": ["./worker-configuration.d.ts"] 40 | }, 41 | "exclude": ["test"], 42 | "include": ["worker-configuration.d.ts", "src/**/*.ts"] 43 | } 44 | -------------------------------------------------------------------------------- /shup-ha/wrangler.jsonc: -------------------------------------------------------------------------------- 1 | /** 2 | * For more details on how to configure Wrangler, refer to: 3 | * https://developers.cloudflare.com/workers/wrangler/configuration/ 4 | */ 5 | { 6 | "$schema": "node_modules/wrangler/config-schema.json", 7 | "name": "shup-ha", 8 | "main": "src/index.ts", 9 | "compatibility_date": "2025-08-02", 10 | "observability": { 11 | "enabled": true 12 | }, 13 | 14 | "triggers": { 15 | "crons": [ 16 | "*/5 * * * *" 17 | ] 18 | }, 19 | 20 | /** 21 | * Smart Placement 22 | * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement 23 | */ 24 | // "placement": { "mode": "smart" }, 25 | 26 | /** 27 | * Bindings 28 | * Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including 29 | * databases, object storage, AI inference, real-time communication and more. 30 | * https://developers.cloudflare.com/workers/runtime-apis/bindings/ 31 | */ 32 | 33 | /** 34 | * Environment Variables 35 | * https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables 36 | */ 37 | /** 38 | * Note: Use secrets to store sensitive data. 39 | * https://developers.cloudflare.com/workers/configuration/secrets/ 40 | */ 41 | 42 | 43 | // origin statuses need to be fast to access cause of latency so they live in KV 44 | "kv_namespaces": [ 45 | { 46 | "binding": "origin_status", 47 | "id": "49d74594f97144a6bccf4f3c7b18621b" 48 | } 49 | ], 50 | 51 | // incidents need to not be duplicated so must live in D1 52 | "d1_databases": [ 53 | { 54 | "binding": "incidents_db", 55 | "database_id": "0fe5d021-9382-488c-989e-dfdd754db20f", 56 | "database_name": "shup_ha_incidents" 57 | } 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "update_url": "https://clients2.google.com/service/update2/crx", 3 | 4 | "manifest_version": 3, 5 | "name": "React Developer Tools", 6 | "description": "Adds React debugging tools to the Chrome Developer Tools.\n\nCreated from revision 3cde211b0c on 10/20/2025.", 7 | "version": "7.0.1", 8 | "version_name": "7.0.1 (10/20/2025)", 9 | "minimum_chrome_version": "114", 10 | "icons": { 11 | "16": "icons/16-production.png", 12 | "32": "icons/32-production.png", 13 | "48": "icons/48-production.png", 14 | "128": "icons/128-production.png" 15 | }, 16 | "action": { 17 | "default_icon": { 18 | "16": "icons/16-disabled.png", 19 | "32": "icons/32-disabled.png", 20 | "48": "icons/48-disabled.png", 21 | "128": "icons/128-disabled.png" 22 | }, 23 | "default_popup": "popups/disabled.html" 24 | }, 25 | "devtools_page": "main.html", 26 | "content_security_policy": { 27 | "extension_pages": "script-src 'self'; object-src 'self'" 28 | }, 29 | "web_accessible_resources": [ 30 | { 31 | "resources": [ 32 | "main.html", 33 | "panel.html", 34 | "build/*.js" 35 | ], 36 | "matches": [ 37 | "" 38 | ], 39 | "extension_ids": [] 40 | } 41 | ], 42 | "background": { 43 | "service_worker": "build/background.js" 44 | }, 45 | "permissions": [ 46 | "scripting", 47 | "storage", 48 | "tabs" 49 | ], 50 | "optional_permissions": [ 51 | "clipboardWrite" 52 | ], 53 | "host_permissions": [ 54 | "" 55 | ], 56 | "content_scripts": [ 57 | { 58 | "matches": [ 59 | "" 60 | ], 61 | "js": [ 62 | "build/prepareInjection.js" 63 | ], 64 | "run_at": "document_start" 65 | } 66 | ] 67 | } -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 57 | 58 | 59 | 60 |
61 |

Looks like this page doesn't have React, or it hasn't been loaded yet.

62 |
63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/common/reusableResponse.js: -------------------------------------------------------------------------------- 1 | export const nullBodyStatuses = [101, 204, 205, 304]; 2 | 3 | // I'm sick and tired of resp body reuse issues 4 | export default class ReusableResponse { 5 | #body; 6 | 7 | get #isNullBody() { 8 | return nullBodyStatuses.includes(this.status); 9 | } 10 | 11 | get body() { 12 | // this method does not exist in chrome and safari 13 | // but it DOES exist in both node and deno :) yay 14 | return this.#isNullBody ? null : ReadableStream.from(new Uint8Array(this.#body)); 15 | } 16 | 17 | get bodyUsed() { 18 | return false; 19 | } 20 | 21 | headers; 22 | ok; 23 | redirected; 24 | status; 25 | statusText; 26 | type; 27 | url; 28 | 29 | static async create(resp) { 30 | if (resp.bodyUsed) throw new Error("cannot turn a used response into a reusable one"); 31 | 32 | return new ReusableResponse(resp, await resp.arrayBuffer()); 33 | } 34 | 35 | constructor(resp, bodyBuf) { 36 | this.#body = bodyBuf; 37 | // these are actually stored on a symbol n stuff, annoying 38 | //Object.assign(this, resp); 39 | this.ok = resp.ok; 40 | this.redirected = resp.redirected; 41 | this.status = resp.status; 42 | this.statusText = resp.statusText; 43 | this.type = resp.type; 44 | this.url = resp.url; 45 | this.headers = new Headers(resp.headers); // clone as resp may be immutable 46 | } 47 | 48 | arrayBuffer() { 49 | return Promise.resolve(this.#body); 50 | } 51 | 52 | blob() { 53 | return Promise.resolve(new Blob(this.#body)); 54 | } 55 | 56 | bytes() { 57 | return Promise.resolve(new Uint8Array(this.#body)); 58 | } 59 | 60 | clone() { 61 | return new ReusableResponse(this, this.#body); 62 | } 63 | 64 | formData() { 65 | // bwehhhhh 66 | return new Response(this.#body, this).formData(); 67 | } 68 | 69 | json() { 70 | return this.text().then(JSON.parse); 71 | } 72 | 73 | text() { 74 | return Promise.resolve(new TextDecoder().decode(this.#body)); 75 | } 76 | 77 | toRealRes() { 78 | return new Response(this.#isNullBody ? null : this.#body, { ...this }); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/common/config.js: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "fs"; 2 | import { dirname, resolve } from "path"; 3 | import { fileURLToPath } from "url"; 4 | 5 | export const srcDir = dirname(dirname(fileURLToPath(import.meta.url))); 6 | 7 | export const startTime = Date.now(); 8 | 9 | export const changelog = readFileSync(resolve(srcDir, "../CHANGELOG.md"), "utf8"); 10 | 11 | export const version = changelog.match(/(?<=^## r)\d+/m)[0]; 12 | 13 | if (!version) throw new Error("Version number not found, changelog is missing or has invalid format."); 14 | 15 | let rawCfg; 16 | try { 17 | rawCfg = JSON.parse(readFileSync(resolve(srcDir, "../config.json"), "utf8")); 18 | } catch (e) { 19 | console.error("Failed to load config, using defaults"); 20 | } 21 | 22 | export const config = Object.freeze({ 23 | port: rawCfg?.port || 8080, 24 | host: rawCfg?.host || `http://localhost:${rawCfg?.port || 8080}`, 25 | stats: rawCfg?.stats ?? true, 26 | setupIntervalHours: rawCfg?.setupIntervalHours ?? 3, 27 | tracing: { 28 | service: rawCfg?.tracing?.service ?? "sheltupdate", 29 | log: rawCfg?.tracing?.log ?? true, 30 | otlpEndpoint: rawCfg?.tracing?.otlpEndpoint, 31 | otlpType: rawCfg?.tracing?.otlpType ?? "protobuf", // "protobuf" | "json" | "grpc" 32 | }, 33 | proxy: { 34 | cache: { 35 | lastUsedRemoveHours: rawCfg?.proxy?.cache?.lastUsedRemoveHours ?? 1, 36 | maxMinutesToUseCached: rawCfg?.proxy?.cache?.maxMinutesToUseCached ?? 30, 37 | }, 38 | useragent: 39 | rawCfg?.proxy?.useragent || 40 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) discord/0.0.76 Chrome/128.0.6613.186 Electron/32.2.2 Safari/537.36", 41 | }, 42 | apiBases: { 43 | v1: rawCfg?.apiBases?.v1 || "https://discord.com/api", 44 | v2: rawCfg?.apiBases?.v2 || "https://discord.com/api/updates", 45 | }, 46 | webhook: { 47 | enable: rawCfg?.webhook?.enable ?? false, 48 | url: rawCfg?.webhook?.url || "https://discord.com/api/webhooks/X", 49 | username: rawCfg?.webhook?.username || "GooseUpdate", 50 | avatarUrl: 51 | rawCfg?.webhook?.avatarUrl || 52 | "https://cdn.discordapp.com/avatars/760559484342501406/5125aff2f446ad7c45cf2dfd6abf92ed.png", 53 | }, 54 | }); 55 | 56 | console.log(config); 57 | -------------------------------------------------------------------------------- /src/apiV1/moduleDownload/index.js: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, rmSync } from "fs"; 2 | import path from "path"; 3 | 4 | import basicRedirect from "../../common/redirect.js"; 5 | 6 | import patch from "./patchModule.js"; 7 | import { getBranch } from "../../common/branchesLoader.js"; 8 | import { reportEndpoint, reportV1Cached, reportV1Patched } from "../../dashboard/reporting.js"; 9 | import { populateReqAttrs, withSection } from "../../common/tracer.js"; 10 | import { cacheBase } from "../../common/fsCache.js"; 11 | import { getEtag } from "../../common/proxy/index.js"; 12 | 13 | const cacheEtags = new Map(); 14 | 15 | export const handleModuleDownload = withSection("v1 download module", async (span, c) => { 16 | const { branch, /*channel,*/ module, version } = c.req.param(); 17 | 18 | const branchFull = getBranch(branch); 19 | if (!branchFull) { 20 | return c.notFound("Invalid sheltupdate branch"); 21 | } 22 | 23 | reportEndpoint("v1_module_download"); 24 | 25 | populateReqAttrs(span, c); 26 | 27 | if (module === "discord_desktop_core") { 28 | const cacheName = `${module}-${branch}-${version}`; 29 | const cacheDir = path.join(cacheBase, `v1-desktop-core`, cacheName); 30 | const cacheFinalFile = path.join(cacheDir, "module.zip"); 31 | 32 | const etag = await getEtag(c.req.url, {}, [version, version.substring(branchFull.version.toString().length)]); 33 | 34 | if (existsSync(cacheFinalFile)) { 35 | // if cache is valid 36 | if (etag && etag === cacheEtags.get(cacheFinalFile)) { 37 | span.addEvent("Served cached discord_desktop_core"); 38 | reportV1Cached(); 39 | 40 | c.header("Content-Type", "application/zip"); 41 | return c.body(readFileSync(cacheFinalFile)); 42 | } else { 43 | span.addEvent(`etag mismatch, expecting ${cacheEtags.get(cacheFinalFile)} but got ${etag}`); 44 | 45 | cacheEtags.delete(cacheFinalFile); 46 | // delete cache and fall through to patch 47 | rmSync(cacheDir, { recursive: true, force: true }); 48 | } 49 | } 50 | 51 | // set expected etag 52 | cacheEtags.set(cacheFinalFile, etag); 53 | 54 | reportV1Patched(); 55 | return patch(c, cacheDir, cacheFinalFile); 56 | } 57 | 58 | return basicRedirect(c); 59 | }); 60 | -------------------------------------------------------------------------------- /src/common/proxy/index.js: -------------------------------------------------------------------------------- 1 | import * as Cache from "./cache.js"; 2 | import { config } from "../config.js"; 3 | import { reportProxyHit, reportProxyMiss } from "../../dashboard/reporting.js"; 4 | import ReusableResponse from "../reusableResponse.js"; 5 | import { withSection } from "../tracer.js"; 6 | 7 | export const getProxyURL = (url) => `/${url.split("/").slice(2).join("/")}`; 8 | 9 | function performUrlReplacement(span, ctxturl, options, rpl, base) { 10 | const rUrl = ctxturl.replace(/.*:\/\/[^/]*/, ""); 11 | 12 | let url = rpl !== undefined ? rUrl.replace(rpl[0], rpl[1]) : rUrl; 13 | url = base + getProxyURL(url); 14 | 15 | span.setAttributes({ 16 | "proxy.options": JSON.stringify(options), 17 | "proxy.replacement": rpl, 18 | "proxy.target": url, 19 | }); 20 | 21 | return url; 22 | } 23 | 24 | export const getEtag = withSection( 25 | "etag check", 26 | async (span, ctxtUrl, options = {}, rpl = undefined, base = config.apiBases.v1) => { 27 | const url = performUrlReplacement(span, ctxtUrl, options, rpl, base); 28 | 29 | const resp = await fetch(url, { 30 | method: "HEAD", 31 | ...options, 32 | }); 33 | 34 | return resp.headers.get("ETag"); 35 | }, 36 | ); 37 | 38 | export default withSection("proxy", async (span, context, options = {}, rpl = undefined, base = config.apiBases.v1) => { 39 | const url = performUrlReplacement(span, context.req.url, options, rpl, base); 40 | 41 | const cacheUrl = url.replace(/&_=[0-9]+$/, ""); 42 | const cached = Cache.get(cacheUrl); 43 | 44 | const now = Date.now(); 45 | 46 | if (cached && (now - cached.cachedOn) / 1000 / 60 < config.proxy.cache.maxMinutesToUseCached) { 47 | span.setAttribute("proxy.cache_hit", true); 48 | 49 | cached.lastUsed = now; 50 | 51 | reportProxyHit(); 52 | 53 | return cached.resp.toRealRes(); 54 | } 55 | 56 | reportProxyMiss(); 57 | 58 | span.setAttribute("proxy.cache_hit", false); 59 | 60 | const proxRaw = await fetch(url, { 61 | headers: { "User-Agent": config.proxy.useragent }, 62 | ...options, 63 | }); 64 | 65 | span.addEvent(`got response: ${proxRaw.status}`); 66 | 67 | const prox = await ReusableResponse.create(proxRaw); 68 | prox.headers.delete("Content-Encoding"); 69 | 70 | if (proxRaw.ok) { 71 | Cache.set(cacheUrl, { 72 | resp: prox, 73 | cachedOn: now, 74 | lastUsed: now, 75 | }); 76 | } 77 | 78 | // I do not know why hono/undici will not accept my ReusableResponse as is. 79 | return prox.toRealRes(); 80 | }); 81 | -------------------------------------------------------------------------------- /src/apiV1/moduleDownload/patchModule.js: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync, cpSync, createWriteStream, rmSync } from "fs"; 2 | 3 | import stream from "stream"; 4 | import { join } from "path"; 5 | 6 | import unzipper from "unzipper"; 7 | import archiver from "archiver"; 8 | 9 | import basicProxy from "../../common/proxy/index.js"; 10 | import { ensureBranchIsReady, getBranch, getSingleBranchMetas } from "../../common/branchesLoader.js"; 11 | import { section, withSection } from "../../common/tracer.js"; 12 | import { dcMain, dcPreload } from "../../desktopCore/index.js"; 13 | 14 | export default withSection("v1 module patcher", async (span, c, cacheDir, cacheFinalFile) => { 15 | const { branch: branch_, /*channel,*/ version } = c.req.param(); 16 | //const { platform, host_version } = c.req.query(); 17 | 18 | // wait for branch to be ready! 19 | await ensureBranchIsReady(branch_); 20 | 21 | const branch = getBranch(branch_); 22 | 23 | const cacheExtractDir = join(cacheDir, "extract" + Math.random().toString(16)); 24 | 25 | const s = await section("download original module", async () => { 26 | const prox = await basicProxy(c, {}, [version, version.substring(branch.version.toString().length)]); 27 | 28 | let s = stream.Readable.from(prox.body); 29 | 30 | let t = s.pipe(unzipper.Extract({ path: cacheExtractDir })); 31 | 32 | await new Promise((res) => t.on("close", res)); 33 | 34 | return s; 35 | }); 36 | 37 | section("copy files", () => { 38 | for (const cacheDir of branch.cacheDirs) { 39 | cpSync(cacheDir, cacheExtractDir, { recursive: true }); 40 | } 41 | 42 | writeFileSync(join(cacheExtractDir, "index.js"), dcMain.replace("// __BRANCHES_MAIN__", branch.main)); 43 | writeFileSync(join(cacheExtractDir, "preload.js"), dcPreload.replace("// __BRANCHES_PRELOAD__", branch.preload)); 44 | writeFileSync(join(cacheExtractDir, "branches.json"), JSON.stringify(getSingleBranchMetas(), null, 4)); 45 | }); 46 | 47 | await section("create module zip", async () => { 48 | const outputStream = createWriteStream(`${cacheFinalFile}`); 49 | 50 | const archive = archiver("zip"); 51 | 52 | archive.pipe(outputStream); 53 | 54 | archive.directory(cacheExtractDir, false); 55 | 56 | archive.finalize(); 57 | 58 | await new Promise((res) => outputStream.on("close", res)); 59 | 60 | s.destroy(); 61 | 62 | outputStream.close(); 63 | outputStream.destroy(); 64 | 65 | rmSync(cacheExtractDir, { recursive: true }); 66 | }); 67 | 68 | c.header("Content-Type", "application/zip"); 69 | return c.body(readFileSync(cacheFinalFile)); 70 | }); 71 | -------------------------------------------------------------------------------- /src/dashboard/reporting.js: -------------------------------------------------------------------------------- 1 | import { createHash } from "crypto"; 2 | import { config } from "../common/config.js"; 3 | 4 | // state 5 | export let statsState = { 6 | uniqueUsers: {}, 7 | requestCounts: { 8 | v1_host_squirrel: 0, 9 | v1_host_notsquirrel: 0, 10 | v1_modules: 0, 11 | v1_module_download: 0, 12 | v2_manifest: 0, 13 | v2_module: 0, 14 | }, 15 | proxyOrRedirect: { 16 | proxied: 0, 17 | redirected: 0, 18 | }, 19 | proxyCacheHitRatio: { 20 | hit: 0, 21 | miss: 0, 22 | }, 23 | v1ModuleCacheHitRatio: { 24 | hit: 0, 25 | miss: 0, 26 | }, 27 | v2ManifestCacheHitRatio: { 28 | hit: 0, 29 | miss: 0, 30 | }, 31 | }; 32 | 33 | /// call on every endpoint hit 34 | export function reportEndpoint(name) { 35 | if (!config.stats) return; 36 | statsState.requestCounts[name]++; 37 | } 38 | 39 | /// call on v1 handlemodules, v2 handlemanifest 40 | export function reportUniqueUser(ip, platform, host_version, channel, branch, apiVer) { 41 | if (!config.stats) return; 42 | statsState.uniqueUsers[createHash("sha256").update(ip).digest("hex")] = { 43 | platform, 44 | host_version, 45 | channel, 46 | branch, 47 | apiVer, 48 | //time: Date.now(), 49 | }; 50 | } 51 | 52 | /// call every time the proxy cache is used 53 | export function reportProxyHit() { 54 | if (!config.stats) return; 55 | statsState.proxyOrRedirect.proxied++; 56 | statsState.proxyCacheHitRatio.hit++; 57 | } 58 | 59 | /// call every time a request is proxied 60 | export function reportProxyMiss() { 61 | if (!config.stats) return; 62 | statsState.proxyOrRedirect.proxied++; 63 | statsState.proxyCacheHitRatio.miss++; 64 | } 65 | 66 | /// call every time a request is redirected 67 | export function reportRedirected() { 68 | if (!config.stats) return; 69 | statsState.proxyOrRedirect.redirected++; 70 | } 71 | 72 | /// call every time v1 desktop_core is served from the cache 73 | export function reportV1Cached() { 74 | if (!config.stats) return; 75 | statsState.v1ModuleCacheHitRatio.hit++; 76 | } 77 | 78 | /// call every time v21 desktop_core needs to be patched 79 | export function reportV1Patched() { 80 | if (!config.stats) return; 81 | statsState.v1ModuleCacheHitRatio.miss++; 82 | } 83 | 84 | /// call every time v2 desktop_core is served from the cache 85 | export function reportV2Cached() { 86 | if (!config.stats) return; 87 | statsState.v2ManifestCacheHitRatio.hit++; 88 | } 89 | 90 | /// call every time v2 desktop_core needs to be patched 91 | export function reportV2Patched() { 92 | if (!config.stats) return; 93 | statsState.v2ManifestCacheHitRatio.miss++; 94 | } 95 | -------------------------------------------------------------------------------- /src/common/tracer.js: -------------------------------------------------------------------------------- 1 | import { NodeSDK } from "@opentelemetry/sdk-node"; 2 | import { context, SpanStatusCode, trace } from "@opentelemetry/api"; 3 | import { OTLPTraceExporter as OTLPGrpc } from "@opentelemetry/exporter-trace-otlp-grpc"; 4 | import { OTLPTraceExporter as OTLPJson } from "@opentelemetry/exporter-trace-otlp-http"; 5 | import { OTLPTraceExporter as OTLPProto } from "@opentelemetry/exporter-trace-otlp-proto"; 6 | import { ShupLoggerSpanExporter } from "./logging/index.js"; 7 | import { config } from "./config.js"; 8 | 9 | const exporter = new { 10 | protobuf: OTLPProto, 11 | json: OTLPJson, 12 | grpc: OTLPGrpc, 13 | }[config.tracing.otlpType]({ url: config.tracing.otlpEndpoint }); 14 | 15 | const sdk = new NodeSDK({ 16 | serviceName: config.tracing.service, 17 | traceExporter: config.tracing.log ? new ShupLoggerSpanExporter(exporter) : exporter, 18 | instrumentations: [], 19 | }); 20 | 21 | sdk.start(); 22 | 23 | const tracer = trace.getTracer("sheltupdate-tracer"); 24 | 25 | // i will type this shit properly if we switch to TS but i just couldnt bear not having the span arg inferred omg -- ys 26 | /** 27 | * @import {Span} from "@opentelemetry/api"; 28 | * @import {Context} from "hono" 29 | */ 30 | 31 | /** 32 | * @arg {Span} span 33 | * @arg {Context} ctxt 34 | * */ 35 | export function populateReqAttrs(span, ctxt) { 36 | let params = ctxt.req.param(); 37 | for (const k in params) span.setAttribute("params." + k, params[k]); 38 | 39 | let query = ctxt.req.query(); 40 | for (const k in query) span.setAttribute("params." + k, query[k]); 41 | } 42 | 43 | /** 44 | * @template T 45 | * @arg {string} name 46 | * @arg {(s: Span, ...a: any[]) => T} fn 47 | * */ 48 | export const withSection = 49 | (name, fn) => 50 | (...args) => 51 | section(name, fn, ...args); 52 | 53 | /** 54 | * @template T 55 | * @arg {string} name 56 | * @arg {(s: Span, ...a: any[]) => T} fn 57 | * @arg {any} args 58 | * @returns T 59 | * */ 60 | export function section(name, fn, ...args) { 61 | return tracer.startActiveSpan(name, (span) => { 62 | try { 63 | const res = fn(span, ...args); 64 | 65 | if (res instanceof Promise) 66 | return res.then( 67 | (r) => { 68 | span.end(); 69 | return r; 70 | }, 71 | (e) => { 72 | span.recordException(e); 73 | span.setStatus({ code: SpanStatusCode.ERROR }); 74 | span.end(); 75 | throw e; 76 | }, 77 | ); 78 | else { 79 | span.end(); 80 | return res; 81 | } 82 | } catch (e) { 83 | span.recordException(e); 84 | span.setStatus({ 85 | code: SpanStatusCode.ERROR, 86 | }); 87 | span.end(); 88 | throw e; 89 | } 90 | }); 91 | } 92 | -------------------------------------------------------------------------------- /src/dashboard/dashboard.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 1rem; 4 | background: black; 5 | color: white; 6 | font-family: "IBM Plex Mono", 7 | ui-monospace, SFMono-Regular, Menlo, Monaco, 8 | Consolas, "Liberation Mono", "Courier New", monospace; 9 | } 10 | 11 | #header-wrap { 12 | display: flex; 13 | flex-wrap: wrap; 14 | align-items: baseline; 15 | justify-content: space-between; 16 | border-bottom: 1px solid white; 17 | gap: 1rem; 18 | } 19 | 20 | #header-wrap h1 { 21 | text-align: center; 22 | margin: 0; 23 | } 24 | 25 | #header-links { 26 | display: flex; 27 | justify-content: center; 28 | flex-wrap: wrap; 29 | gap: 0.5rem; 30 | } 31 | 32 | #header-links > * { 33 | display: inline-block; 34 | color: white; 35 | font-size: 1.125rem; 36 | text-decoration: none; 37 | border: 1px solid white; 38 | padding: .25rem .5rem; 39 | } 40 | 41 | #upper-flex { 42 | display: flex; 43 | flex-wrap: wrap; 44 | gap: 1rem; 45 | margin-bottom: 1.25rem; 46 | } 47 | 48 | #lower-flex { 49 | display: flex; 50 | gap: 1.25rem; 51 | } 52 | 53 | #left-flex { 54 | display: flex; 55 | flex-direction: column; 56 | gap: 1.25rem; 57 | } 58 | 59 | .stats-card { 60 | border: 1px solid white; 61 | padding: 1rem; 62 | display: flex; 63 | flex-direction: column; 64 | align-items: center; 65 | column-gap: 2rem; 66 | row-gap: 0.25rem; 67 | flex-grow: 1; 68 | position: relative; 69 | min-width: 12rem; 70 | } 71 | 72 | .stats-card > * { margin: 0; } 73 | 74 | .stats-card .sub { 75 | font-style: italic; 76 | align-self: start; 77 | } 78 | 79 | .stats-card .basic-big { 80 | font-size: 1.25rem; 81 | margin-bottom: 0.5rem; 82 | } 83 | 84 | #left-flex .stats-card { 85 | justify-content: center; 86 | } 87 | 88 | #cache-grid { 89 | display: grid; 90 | grid-template-columns: 1fr 1fr 1fr; 91 | place-items: center; 92 | flex: 1; 93 | align-self: stretch; 94 | } 95 | 96 | #cache-grid .sub { align-self: end; } 97 | #cache-grid > * { margin: 0; } 98 | 99 | #prop-grid { 100 | display: grid; 101 | place-items: center; 102 | grid-template-columns: auto 1fr; 103 | gap: 0; 104 | flex: 1; 105 | } 106 | 107 | #prop-grid > p { 108 | font-size: 0.875rem; 109 | translate: 0 1.2rem; 110 | } 111 | 112 | .card-title { 113 | font-size: 1rem; 114 | position: absolute; 115 | background: black; 116 | display: inline-block; 117 | padding: 0 0.375rem; 118 | top: -.6rem; 119 | left: .5rem; 120 | text-wrap: nowrap; 121 | text-transform: uppercase; 122 | } 123 | 124 | .plot-wrap > * { 125 | font-size: 0.875rem !important; 126 | } 127 | 128 | /* the uwu design language does not call for bold text. */ 129 | h1, h2 { font-weight: normal; } 130 | 131 | @media (max-width: 1024px) { 132 | #lower-flex { flex-direction: column; } 133 | #header-wrap { justify-content: center; } 134 | } 135 | 136 | figure { 137 | margin: 0; 138 | } -------------------------------------------------------------------------------- /shup-ha/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | 3 | logs 4 | _.log 5 | npm-debug.log_ 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | .pnpm-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | 13 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 14 | 15 | # Runtime data 16 | 17 | pids 18 | _.pid 19 | _.seed 20 | \*.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | 28 | coverage 29 | \*.lcov 30 | 31 | # nyc test coverage 32 | 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | 41 | bower_components 42 | 43 | # node-waf configuration 44 | 45 | .lock-wscript 46 | 47 | # Compiled binary addons (https://nodejs.org/api/addons.html) 48 | 49 | build/Release 50 | 51 | # Dependency directories 52 | 53 | node_modules/ 54 | jspm_packages/ 55 | 56 | # Snowpack dependency directory (https://snowpack.dev/) 57 | 58 | web_modules/ 59 | 60 | # TypeScript cache 61 | 62 | \*.tsbuildinfo 63 | 64 | # Optional npm cache directory 65 | 66 | .npm 67 | 68 | # Optional eslint cache 69 | 70 | .eslintcache 71 | 72 | # Optional stylelint cache 73 | 74 | .stylelintcache 75 | 76 | # Microbundle cache 77 | 78 | .rpt2_cache/ 79 | .rts2_cache_cjs/ 80 | .rts2_cache_es/ 81 | .rts2_cache_umd/ 82 | 83 | # Optional REPL history 84 | 85 | .node_repl_history 86 | 87 | # Output of 'npm pack' 88 | 89 | \*.tgz 90 | 91 | # Yarn Integrity file 92 | 93 | .yarn-integrity 94 | 95 | # parcel-bundler cache (https://parceljs.org/) 96 | 97 | .cache 98 | .parcel-cache 99 | 100 | # Next.js build output 101 | 102 | .next 103 | out 104 | 105 | # Nuxt.js build / generate output 106 | 107 | .nuxt 108 | dist 109 | 110 | # Gatsby files 111 | 112 | .cache/ 113 | 114 | # Comment in the public line in if your project uses Gatsby and not Next.js 115 | 116 | # https://nextjs.org/blog/next-9-1#public-directory-support 117 | 118 | # public 119 | 120 | # vuepress build output 121 | 122 | .vuepress/dist 123 | 124 | # vuepress v2.x temp and cache directory 125 | 126 | .temp 127 | .cache 128 | 129 | # Docusaurus cache and generated files 130 | 131 | .docusaurus 132 | 133 | # Serverless directories 134 | 135 | .serverless/ 136 | 137 | # FuseBox cache 138 | 139 | .fusebox/ 140 | 141 | # DynamoDB Local files 142 | 143 | .dynamodb/ 144 | 145 | # TernJS port file 146 | 147 | .tern-port 148 | 149 | # Stores VSCode versions used for testing VSCode extensions 150 | 151 | .vscode-test 152 | 153 | # yarn v2 154 | 155 | .yarn/cache 156 | .yarn/unplugged 157 | .yarn/build-state.yml 158 | .yarn/install-state.gz 159 | .pnp.\* 160 | 161 | # wrangler project 162 | 163 | .dev.vars* 164 | !.dev.vars.example 165 | .env* 166 | !.env.example 167 | .wrangler/ 168 | -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/deadcode.svg: -------------------------------------------------------------------------------- 1 | development780780 -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/icons/development.svg: -------------------------------------------------------------------------------- 1 | development780780 -------------------------------------------------------------------------------- /src/dashboard/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | sheltupdate r__VERSION__ 8 | 9 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 |

sheltupdate r__VERSION__

30 | 31 | 36 |
37 | 38 |

Statistics are not live, even though relative times update.

39 | 40 |
41 | 42 |
43 |

Server Uptime

44 |

45 |

Up since (local time)

46 |
47 | 48 |
49 |

Cache Hit Ratios

50 |
51 |

__CACHE_PROX__

52 |

__CACHE_V1__

53 |

__CACHE_V2__

54 |

Proxy

55 |

V1 Module

56 |

V2 Module

57 |
58 |
59 | 60 |
61 |

Unique Users

62 |

__USER_COUNT__

63 |

Users are 100% anonymised

64 |
65 |
66 | 67 |
68 |
69 |
70 |

Endpoint Hit Counts

71 |
72 |
73 | 74 |
75 |

Branches

76 |
77 |
78 |
79 | 80 |
81 |

Proportions

82 | 83 |
84 |

Platform

85 |
86 |

Channel

87 |
88 |

Host Version

89 |
90 |

API Version

91 |
92 |
93 |
94 |
95 | 96 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker Image 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: main 7 | 8 | jobs: 9 | publish: 10 | name: Publish Docker Image to ghcr.io 11 | runs-on: ubuntu-latest 12 | permissions: 13 | packages: write 14 | steps: 15 | - name: Check out the repo 16 | uses: actions/checkout@v4 17 | 18 | - name: Extract version number 19 | run: | 20 | VERSION=$(grep -oP -m 1 '(?<=^## r)\d+' CHANGELOG.md) 21 | if [[ $VERSION = "" ]]; then 22 | echo "No version found in CHANGELOG.md" 23 | exit 1 24 | fi 25 | echo "VERSION=$VERSION" >> $GITHUB_ENV 26 | 27 | - name: Create Docker Image tags 28 | run: | 29 | TAGS="ghcr.io/${{ github.repository }}:dev" 30 | if docker manifest inspect ghcr.io/${{ github.repository }}:0.0.$VERSION > /dev/null 2>&1; then 31 | echo "Image with version (0.0.$VERSION) already exists in registry" 32 | echo "Publishing a dev image.." 33 | else 34 | echo "Image with version (0.0.$VERSION) does not exist in the registry yet" 35 | echo "Publishing a release and a dev image.." 36 | echo "IS_RELEASE=true" >> $GITHUB_ENV 37 | TAGS="${TAGS},\ 38 | ghcr.io/${{ github.repository }}:0.0.${{ env.VERSION }},\ 39 | ghcr.io/${{ github.repository }}:latest" 40 | fi 41 | echo "TAGS=${TAGS}" >> $GITHUB_ENV 42 | 43 | - name: Log in to ghcr.io 44 | uses: docker/login-action@v3 45 | with: 46 | registry: ghcr.io 47 | username: ${{ github.actor }} 48 | password: ${{ secrets.GITHUB_TOKEN }} 49 | 50 | - name: Set up Docker Buildx 51 | uses: docker/setup-buildx-action@v3 52 | 53 | - name: Build and push Docker Image 54 | uses: docker/build-push-action@v6 55 | with: 56 | context: . 57 | file: ./Dockerfile 58 | push: true 59 | tags: ${{ env.TAGS }} 60 | platforms: linux/amd64,linux/arm64,linux/arm/v7 61 | 62 | - name: Update release container on server 63 | if: env.IS_RELEASE == 'true' 64 | uses: appleboy/ssh-action@master 65 | with: 66 | host: ${{ secrets.DEPLOY_HOST }} 67 | username: ${{ secrets.DEPLOY_USER }} 68 | key: ${{ secrets.DEPLOY_PRIVKEY }} 69 | # this relies on a specific custom update script that is installed on the 70 | # server that currently hosts sheltupdate. Adjust as needed. 71 | script: ~/update.sh sheltupdate 72 | 73 | - name: Update staging container on server 74 | uses: appleboy/ssh-action@master 75 | with: 76 | host: ${{ secrets.DEPLOY_HOST }} 77 | username: ${{ secrets.DEPLOY_USER }} 78 | key: ${{ secrets.DEPLOY_PRIVKEY }} 79 | script: ~/update.sh sheltupdate-staging 80 | -------------------------------------------------------------------------------- /src/apiV2/manifest.js: -------------------------------------------------------------------------------- 1 | import basicProxy from "../common/proxy/index.js"; 2 | import { patch } from "./patchModule.js"; 3 | import { config } from "../common/config.js"; 4 | import { ensureBranchIsReady, getBranch } from "../common/branchesLoader.js"; 5 | import { reportEndpoint, reportUniqueUser } from "../dashboard/reporting.js"; 6 | import originatingIp from "../common/originatingIp.js"; 7 | import { populateReqAttrs, withSection } from "../common/tracer.js"; 8 | 9 | const base = config.apiBases.v2; 10 | const host = config.host; 11 | 12 | // https://discord.com/api/updates/distributions/app/manifests/latest?channel=canary&platform=win&arch=x86 13 | 14 | export const handleManifest = withSection("v2 manifest", async (span, c) => { 15 | const branch = c.req.param("branch"); 16 | if (!getBranch(branch)) { 17 | return c.notFound("Invalid sheltupdate branch"); 18 | } 19 | 20 | populateReqAttrs(span, c); 21 | 22 | reportEndpoint("v2_manifest"); 23 | 24 | reportUniqueUser( 25 | originatingIp(c), 26 | c.req.query("platform"), 27 | `${c.req.query("platform")} ${c.req.query("platform_version")}`, 28 | c.req.query("channel"), 29 | branch, 30 | 2, 31 | ); 32 | 33 | let json = await basicProxy(c, {}, undefined, base).then((r) => r.json()); 34 | 35 | const branchNames = branch.split("+"); 36 | await Promise.all(branchNames.map((b) => ensureBranchIsReady(b))); 37 | 38 | json.modules.discord_desktop_core.deltas = []; // Remove deltas 39 | 40 | const oldVersion = json.modules.discord_desktop_core.full.module_version; 41 | const newVersion = parseInt(`${getBranch(branch).version}${oldVersion.toString()}`); 42 | 43 | // Modify version to prefix branch's version 44 | json.modules.discord_desktop_core.full.module_version = newVersion; 45 | 46 | json.modules.discord_desktop_core.full.package_sha256 = await patch(json.modules.discord_desktop_core.full, branch); 47 | 48 | // Modify URL to use this host 49 | json.modules.discord_desktop_core.full.url = `${host}/${branch}/${json.modules.discord_desktop_core.full.url.split("/").slice(3).join("/").replace(`${oldVersion}/full.distro`, `${newVersion}/full.distro`)}`; 50 | 51 | return c.json(json); 52 | }); 53 | 54 | /* 55 | - Similar to branches except this is way more general use 56 | - Formatted as JSON 57 | 58 | - Method: 59 | - Proxy original request 60 | - Target: discord_desktop_core: 61 | - Update module version 62 | - Pre-patch module: 63 | - Check if already patched in disk cache 64 | - If so: 65 | - We will just send cached file later 66 | - If not: 67 | - Download original module 68 | - Uncompress: 69 | - Brotli decompress 70 | - Extract tar 71 | - Patch: 72 | - Patch index.js 73 | - Update checksum in delta manifest 74 | - UNKNOWN - needs testing: 75 | - Add files to files/ 76 | - [?] Add files to files/manifest.json 77 | - [?] Add files to delta manifest 78 | (- Avoiding those extra steps unless needed) 79 | - Recompress: 80 | - Package into tar 81 | - Brotli compress 82 | - Overwrite url with new self url 83 | - Overwrite checksum with new checksum 84 | - UNKNOWN - needs testing: 85 | - [?] Remove deltas - so client is forced to use full (it might depend on them?) 86 | - [?] Generate new deltas - this will require way more work 87 | */ 88 | -------------------------------------------------------------------------------- /src/common/logging/index.js: -------------------------------------------------------------------------------- 1 | import { log, resetLogger, startLogSection } from "./prettyLogger.js"; 2 | 3 | // https://github.com/uwu/containerspy/blob/6dbe5b766328e76586502203eb5ec9c0582aa1ae/src/s_log.rs#L85 4 | 5 | const SAFE_ALPHABET = `abcdefghijklmnopqrstuvxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_+.,/\\|!@#$%^&*()[]{}`; 6 | 7 | function needsEscaping(s) { 8 | for (const c of s) { 9 | if (!SAFE_ALPHABET.includes(c)) return true; 10 | } 11 | 12 | return false; 13 | } 14 | 15 | // seconds, nanoseconds -> seconds 16 | function hrTimeToS(hrTime) { 17 | return hrTime[0] + hrTime[1] / (1000 * 1000); 18 | } 19 | 20 | function stringifyAttributes(attrs) { 21 | return Object.entries(attrs).map(([k, v]) => 22 | typeof v !== "string" || needsEscaping(v) ? `${k}=${JSON.stringify(v)}` : `${k}=${v}`, 23 | ); 24 | } 25 | 26 | /** 27 | * @import {SpanExporter} from "@opentelemetry/sdk-trace-node" 28 | * */ 29 | 30 | /** 31 | * @implements SpanExporter 32 | * */ 33 | export class ShupLoggerSpanExporter { 34 | passthru; 35 | 36 | constructor(passthru) { 37 | this.passthru = passthru; 38 | } 39 | 40 | export(spans, resultCallback) { 41 | // send to otlp or whatever 42 | this.passthru.export(spans, resultCallback); 43 | 44 | // group spans into traces 45 | const traces = new Map(); 46 | 47 | for (const span of spans) { 48 | let traceArr = traces.get(span.spanContext().traceId); 49 | 50 | if (!traceArr) { 51 | traceArr = []; 52 | traces.set(span.spanContext().traceId, traceArr); 53 | } 54 | 55 | traceArr.push(span); 56 | } 57 | 58 | // process each trace 59 | for (const traceSpans of traces.values()) { 60 | // build lookup map to assist in resolving parents 61 | const spanMap = new Map(); 62 | 63 | for (const span of traceSpans) spanMap.set(span.spanContext().spanId, span); 64 | 65 | // figure out all spans in the trace 66 | const logsToPrint = []; 67 | 68 | for (const span of traceSpans) { 69 | const spanNames = []; 70 | 71 | let parent = span.parentSpanContext; 72 | while (parent) { 73 | const parentSpan = spanMap.get(parent.spanId); 74 | if (!parentSpan) break; 75 | 76 | spanNames.push(parentSpan.name); 77 | parent = parentSpan.parentSpanContext; 78 | } 79 | 80 | spanNames.push(span.name); 81 | 82 | logsToPrint.push({ 83 | msg: `start span ts=${hrTimeToS(span.startTime)} ${stringifyAttributes(span.attributes).join(" ")}`, 84 | ts: hrTimeToS(span.startTime), 85 | spanNames, 86 | }); 87 | 88 | for (const e of span.events) 89 | logsToPrint.push({ 90 | msg: `${e.name} ts=${hrTimeToS(e.time)} ${stringifyAttributes(e.attributes).join(" ")}`, 91 | ts: hrTimeToS(e.time), 92 | spanNames, 93 | }); 94 | 95 | const endTime = hrTimeToS(span.startTime) + hrTimeToS(span.duration); 96 | logsToPrint.push({ 97 | msg: `end span ts=${endTime} dur=${hrTimeToS(span.duration)}`, 98 | ts: endTime, 99 | spanNames, 100 | }); 101 | } 102 | 103 | // sort the logs 104 | logsToPrint.sort((a, b) => a.ts - b.ts); 105 | 106 | // print them 107 | for (const l of logsToPrint) { 108 | resetLogger(); 109 | for (const sec of l.spanNames) startLogSection(sec); 110 | 111 | log(l.msg); 112 | } 113 | } 114 | } 115 | 116 | async shutdown() { 117 | await this.passthru.shutdown(); 118 | } 119 | 120 | async forceFlush() { 121 | await this.passthru.forceFlush(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /branches/tweak/spotify_embed_volume/spotify-embed-volume-script.js: -------------------------------------------------------------------------------- 1 | const speakerSvg = ` 2 | 3 | 4 | 5 | 6 | 7 | `; 8 | const volumeKey = "playbackVolume"; 9 | 10 | if (!localStorage.getItem(volumeKey)) { 11 | localStorage.setItem(volumeKey, 0.5); 12 | } 13 | 14 | let audio; 15 | let volumeSlider; 16 | 17 | const origPlay = Audio.prototype.play; 18 | Audio.prototype.play = function () { 19 | audio = this; 20 | updateVolume(localStorage.getItem(volumeKey)); 21 | return origPlay.apply(this, arguments); 22 | }; 23 | 24 | function updateVolume(volume) { 25 | localStorage.setItem(volumeKey, volume); 26 | 27 | if (audio) audio.volume = volume; 28 | if (volumeSlider) volumeSlider.value = volume * 100; 29 | 30 | const loudCircle = document.querySelector("#inserted_volume_slider .loud"); 31 | const quietCircle = document.querySelector("#inserted_volume_slider .quiet"); 32 | if (!loudCircle || !quietCircle) return; 33 | loudCircle.style.display = volume > 0.5 ? "block" : "none"; 34 | quietCircle.style.display = volume > 0 ? "block" : "none"; 35 | } 36 | 37 | const controlsWrapperQuery = "[class^=PlayerControlsShort_playerControlsWrapper]"; 38 | 39 | const insertVolumeSlider = () => { 40 | const controlsWrapper = document.querySelector(controlsWrapperQuery); 41 | if (!controlsWrapper) return; 42 | if (controlsWrapper.querySelector("#inserted_volume_slider")) return; 43 | 44 | controlsWrapper.insertAdjacentHTML( 45 | "afterbegin", 46 | ` 47 |
48 | 49 | 52 |
53 | `, 54 | ); 55 | 56 | volumeSlider = controlsWrapper.querySelector("#inserted_volume_slider > input"); 57 | volumeSlider.addEventListener("input", () => updateVolume(volumeSlider.value / 100)); 58 | 59 | const volumeButton = controlsWrapper.querySelector("#inserted_volume_slider > button"); 60 | volumeButton.addEventListener("click", () => updateVolume(0)); 61 | volumeButton.addEventListener("mouseenter", () => (volumeSlider.style.visibility = "visible")); 62 | controlsWrapper.addEventListener("mouseleave", () => (volumeSlider.style.visibility = "hidden")); 63 | 64 | updateVolume(localStorage.getItem(volumeKey)); 65 | }; 66 | 67 | insertVolumeSlider(); 68 | const observer = new MutationObserver(insertVolumeSlider); 69 | observer.observe(document, { subtree: true, attributes: true, attributeFilter: ["class"] }); 70 | 71 | const style = document.createElement("style"); 72 | style.innerText = ` 73 | #inserted_volume_slider > input[type="range"] { 74 | -webkit-appearance: none; 75 | background-color: rgba(255, 255, 255, .2); 76 | width: 80px; 77 | height: 12px; 78 | border-radius: 6px; 79 | overflow: hidden; 80 | cursor: pointer; 81 | 82 | &::-webkit-slider-thumb { 83 | -webkit-appearance: none; 84 | width: 0; 85 | border: 0; 86 | box-shadow: -80px 0 0 80px white; 87 | } 88 | } 89 | 90 | #inserted_volume_slider > button { 91 | display: flex; 92 | cursor: pointer; 93 | &:hover { 94 | transform: scale(1.04); 95 | } 96 | } 97 | 98 | #inserted_volume_slider { 99 | display: flex; 100 | align-items: center; 101 | gap: 6px; 102 | margin-bottom: 1px; 103 | }`; 104 | 105 | document.documentElement.append(style); 106 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## r40 2 | - Update React Devtools (6.0.1 -> 7.0.1) 3 | 4 | ## r39 5 | - Fix moonlight branch fetching wrong asset 6 | 7 | ## r38 8 | - add Equicord 9 | - factor sheltupdate revision into module versions 10 | - tolerate pending section in changelog 11 | - UI to change sheltupdate instance 12 | 13 | ## r37 14 | - make tracing service name configurable 15 | 16 | ## r36 17 | - switch from logging to tracing 18 | 19 | ## r35 20 | - Only cache successful responses 21 | 22 | ## r34 23 | - Fix yt_ad_block and disable yt_embed_fix (for now) 24 | 25 | ## r33 26 | - Fix missing import in prod 27 | - Improve host version reporting 28 | 29 | ## r32 30 | - Add cache (in)validation to help prevent breakages (issue #6) 31 | - Reduce the amount of unnecessary detritus left in the v2 cache 32 | 33 | ## r31 34 | - Add Moonlight 35 | - Remove Kernel 36 | - Allow selecting incompatible branches in the client mods tab 37 | - Add support for async branch main scripts 38 | 39 | ## r30 40 | Fix mainScreen.getMainWindowId() returning null 41 | 42 | ## r29 43 | - Fix "MissingContentLength" error when being hosted behind Cloudflare proxy 44 | - Correctly indent branch code in index.js and preload.js 45 | - Catch errors thrown by branches 46 | 47 | ## r28 48 | Disallow incompatible branch combinations in the client mods tab 49 | 50 | ## r27 51 | Improve dashboard load time 52 | 53 | ## r26 54 | - Add branch selector UI compatibility for self hosted instances 55 | - Bundle info about sheltupdate branches into desktop_core 56 | - Fix first launch failing when an update has occured 57 | 58 | ## r25 59 | Add tweak for replacing Discord's titlebar with Windows' native one (TY @koffydrop) 60 | 61 | ## r24 62 | - Fix branches with subdirs (Vencord) 63 | - Fix solid error in client mods settings page 64 | - Remove goose_modules 65 | 66 | ## r23 67 | Fix charts sorting in dashboard 68 | 69 | ## r22 70 | - Fix branchesLoader manifest not including subdirs (TY @marshift) 71 | - Block @sentry/electron requests 72 | 73 | ## r21 74 | - Order charts by value instead of key 75 | - Improve responsiveness in dashboard 76 | 77 | ## r20 78 | - Fix chart order in dashboard 79 | - Also show links on small devices 80 | 81 | ## r19 82 | - Fix stats in dashboard 83 | - Link changelog and branches in dashboard 84 | 85 | ## r18 86 | Improve shelter bundle fetching 87 | 88 | ## r17 89 | - Make the new dashboard look nicer 90 | - Replace useless last module time with cache hit rates 91 | - Read the version number out of the changelog 92 | 93 | ## r16 94 | - Overhaul stats reporting code 95 | - Completely replace stats dashboard 96 | 97 | ## r15 98 | Make the Selector UI get branches from the API 99 | 100 | ## r14 101 | - Decrease size of Docker image 102 | - Fix V1 patcher not cleaning up after itself, throwing and requiring a retry to actually serve 103 | - Fix V1 patcher not waiting for unzip, causing it to serve broken modules 104 | - Move V1 patcher scratch folder into the cache 105 | 106 | ## r13 107 | Add Tweak branches to the Selector UI 108 | 109 | ## r12 110 | - Fix some path portability issues 111 | - Add Spotify Embed Volume 112 | - Add YT Ad Block 113 | - Add YT Embed Fix 114 | 115 | ## r11 116 | - Significantly nicer logging 117 | - Way faster full.distro generation for API V2 (~45s -> ~2s) 118 | - Make stats toggleable 119 | - Switch to JS setups from bash ones 120 | - Add branch metadata API 121 | 122 | ## r10 123 | Fix the shelter devtools patch crashing BD's injection 124 | ## r9 125 | Fix tars being broken on windows 126 | ## r8 127 | ditto. 128 | ## r7 129 | Cap the version numbers so that the API V2 client doesn't break 130 | ## r6 131 | Fix some branches not being generated 132 | ## r5 133 | Fix an infinite loop on Windows making the client unusable 134 | ## r4 135 | Add branch selector UI to shelter 136 | 137 | ## r3 138 | - Fix broken X-Forwarded-For 139 | - Fix typo in ReusableResponse 140 | - Fix shelter injection CSP removal 141 | 142 | ## r2 143 | - Attempt to fix response body reuse bug 144 | - Fix broken stats 145 | 146 | ## r1 147 | - Hello, World! 148 | - Refactor entire GooseUpdate codebase 149 | - Update server technology 150 | - Add shelter branch and remove irrelevant branches 151 | -------------------------------------------------------------------------------- /branches/mod/shelter/preload.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const { contextBridge, ipcRenderer, webFrame } = require("electron"); 4 | 5 | // get selector UI content 6 | const selUiJs = fs.readFileSync(path.join(__dirname, "selector-ui.js"), "utf8"); 7 | 8 | // build shelter injector plugins manifest 9 | const injPlugins = { 10 | "sheltupdate-branch-selector": { 11 | js: selUiJs, 12 | manifest: { 13 | name: "sheltupdate branch selector", 14 | author: "uwu.network", 15 | description: "responsible for the 'Client Mods' UI on sheltupdate installs", 16 | }, 17 | injectorIntegration: { 18 | isVisible: false, 19 | allowedActions: {}, 20 | loaderName: "sheltupdate", 21 | }, 22 | }, 23 | }; 24 | 25 | // inject shelter 26 | ipcRenderer.invoke("SHELTER_BUNDLE_FETCH").then((bundle) => { 27 | webFrame.executeJavaScript(`const SHELTER_INJECTOR_PLUGINS = ${JSON.stringify(injPlugins)}; ${bundle}`); 28 | }); 29 | 30 | // everything below this comment is for the plugin selector UI exclusively 31 | 32 | // read branches and then create a structure like: 33 | /* 34 | const branches = { 35 | shelter: { 36 | name: "shelter", 37 | desc: "Injects shelter", 38 | type: "mod", 39 | }, 40 | vencord: { 41 | name: "Vencord", 42 | desc: "Injects Vencord; This is not an officially supported Vencord install method", 43 | type: "mod", 44 | }, 45 | // ... 46 | } 47 | */ 48 | 49 | const branchesRaw = JSON.parse(fs.readFileSync(path.join(__dirname, "branches.json"), "utf8")); 50 | const branches = Object.fromEntries( 51 | branchesRaw.map((branch) => [branch.name, { ...branch, name: branch.displayName, desc: branch.description }]), 52 | ); 53 | 54 | const readBranches = () => ipcRenderer.invoke("SHELTER_BRANCH_GET"); 55 | 56 | const setBranches = (branches) => ipcRenderer.invoke("SHELTER_BRANCH_SET", branches); 57 | 58 | contextBridge.exposeInMainWorld("SheltupdateNative", { 59 | getAvailableBranches: () => Promise.resolve(branches), 60 | getCurrentBranches: readBranches, 61 | 62 | setBranches: async (br) => { 63 | // validate renderer-side input carefully. this code is actually security-critical 64 | // as if it is not sufficiently safe, privescs such as a plugin enabling BD so that it can 65 | // get access to require("fs") are possible. 66 | if (!Array.isArray(br) && br.length > 0) throw new Error("[sheltupdate] invalid branches passed to setBranches"); 67 | 68 | // don't use `in` or `[]` as those are true for e.g. __proto__ 69 | for (const branch of br) 70 | if (typeof branch !== "string" || !Object.keys(branches).includes(branch)) 71 | throw new Error("[sheltupdate] invalid branches passed to setBranches"); 72 | 73 | // get user permission first, this is our main privesc safeguard 74 | const dialogState = await ipcRenderer.invoke( 75 | "SHELTER_BRANCHCHANGE_SECURITY_DIALOG", 76 | `Confirm you want to change your installed mods to: ${br.map((b) => branches[b].name).join(", ")}?`, 77 | ); 78 | 79 | if (dialogState.response === 0) 80 | throw new Error("[sheltupdate] User declined security check for setting branches"); 81 | 82 | // set the branches 83 | await setBranches(br); 84 | }, 85 | 86 | getCurrentHost: () => ipcRenderer.invoke("SHELTER_HOST_GET"), 87 | 88 | setCurrentHost: async (host) => { 89 | // run validation again to be safe! don't rely on the UI 90 | // again, this is security critical! 91 | let url; 92 | try { url = new URL(host); } 93 | catch {} 94 | 95 | if (!url || typeof host !== "string" || url.pathname !== "/" || host.endsWith("/") || (url.protocol !== "http:" && url.protocol !== "https:")) 96 | throw new Error("[sheltupdate] invalid host passed to setCurrentHost"); 97 | 98 | const res = await ipcRenderer.invoke( 99 | "SHELTER_BRANCHCHANGE_SECURITY_DIALOG", 100 | `Confirm you want to change sheltupdate instance to ${host}?`, 101 | ); 102 | if (res.response === 0) throw new Error("[sheltupdate] User declined security check"); 103 | 104 | await ipcRenderer.invoke("SHELTER_HOST_SET", host); 105 | }, 106 | 107 | // this is a goofy function to have to write 108 | uninstall: async () => { 109 | // once again get user permission 110 | const res = await ipcRenderer.invoke( 111 | "SHELTER_BRANCHCHANGE_SECURITY_DIALOG", 112 | `Confirm you want to uninstall your client mods? Your settings will not be deleted.`, 113 | ); 114 | if (res.response === 0) throw new Error("[sheltupdate] User declined security check"); 115 | 116 | await setBranches([]); 117 | }, 118 | }); 119 | -------------------------------------------------------------------------------- /src/desktopCore/main.js: -------------------------------------------------------------------------------- 1 | const { readdirSync, statSync } = require("original-fs"); 2 | const { join, basename } = require("path"); 3 | 4 | // Discord always launches the desktop_core module with the highest version. 5 | // This means that if the module gets downgraded (by us), Discord won't load the 6 | // correct one. Here we account for that. 7 | let proxyExports; 8 | // Test for a specific modules directory structure with which the issue occurs. 9 | const parentDirName = basename(join(__dirname, "..")); 10 | if (parentDirName.startsWith("discord_desktop_core-")) { 11 | const latestModule = getLatestDesktopCoreModule(); 12 | const currentModule = __dirname; 13 | if (currentModule !== latestModule) { 14 | // The current module is out of date so load the correct one 15 | // and re-export it's exports 16 | proxyExports = require(join(latestModule, "index.js")); 17 | } 18 | } 19 | 20 | // If this is the latest module 21 | if (proxyExports === undefined) { 22 | let resolveBranchesLoaded; 23 | const branchesLoaded = new Promise((res) => { 24 | resolveBranchesLoaded = res; 25 | }); 26 | 27 | proxyExports = new Proxy( 28 | {}, 29 | { 30 | get(target, prop) { 31 | // At the time of writing all core.asar exports are sync functions without 32 | // return values allowing us to just run them later (after our async setup) 33 | return function () { 34 | const origThis = this; 35 | const origArgs = arguments; 36 | console.log("[sheltupdate] Delaying function call:", prop); 37 | branchesLoaded.then((originalExports) => { 38 | console.log("[sheltupdate] Calling original function:", prop); 39 | originalExports[prop].apply(origThis, origArgs); 40 | }); 41 | }; 42 | }, 43 | }, 44 | ); 45 | 46 | setup() 47 | .catch((e) => console.error("[sheltupdate] Error during setup", e)) 48 | .finally(() => resolveBranchesLoaded(require("./core.asar"))); 49 | } 50 | 51 | module.exports = proxyExports; 52 | 53 | function getLatestDesktopCoreModule() { 54 | const modulesDir = join(__dirname, "..", ".."); 55 | const dirs = readdirSync(modulesDir).filter((d) => d.startsWith("discord_desktop_core")); 56 | 57 | let latestVal = 0; 58 | let latestDir; 59 | for (const d of dirs) { 60 | const { birthtimeMs } = statSync(join(modulesDir, d)); 61 | if (birthtimeMs > latestVal) { 62 | latestDir = d; 63 | latestVal = birthtimeMs; 64 | } 65 | } 66 | return join(modulesDir, latestDir, "discord_desktop_core"); 67 | } 68 | 69 | async function setup() { 70 | const electron = require("electron"); 71 | const stream = require("stream"); 72 | 73 | // Block Sentry requests 74 | // We create stubs for electron.net.request to make Sentry think that the requests succeeded. 75 | // Because making them error leads to Sentry adding them to a queue on disk. Meaning that if 76 | // the user were to uninstall sheltupdate, all the requests would still be sent subsequently. 77 | // See https://github.com/getsentry/sentry-electron/blob/3e4e10525b5fb24ffa98b211b91393f81e3555be/src/main/transports/electron-net.ts#L64 78 | // and https://github.com/getsentry/sentry-electron/blob/3e4e10525b5fb24ffa98b211b91393f81e3555be/src/main/transports/offline-store.ts#L52 79 | 80 | class RequestStub extends stream.Writable { 81 | _write(chunk, encoding, cb) { 82 | cb(); 83 | } 84 | setHeader() {} 85 | on(type, cb) { 86 | if (type !== "response") return; 87 | cb({ on: () => {}, headers: {}, statusCode: 200 }); 88 | } 89 | } 90 | 91 | const origRequest = electron.net.request; 92 | electron.net.request = function (options) { 93 | if (!options?.hostname?.endsWith("sentry.io")) { 94 | return origRequest.apply(this, arguments); 95 | } 96 | console.log("[sheltupdate] Blocking Sentry request!"); 97 | return new RequestStub(); 98 | }; 99 | 100 | electron.ipcMain.on("SHELTUPDATE_FRAMEWORK_ORIGINAL_PRELOAD", (event) => { 101 | event.returnValue = event.sender.sheltupdateOriginalPreload; 102 | }); 103 | 104 | class BrowserWindow extends electron.BrowserWindow { 105 | constructor(options) { 106 | let originalPreload; 107 | 108 | if (options.webPreferences?.preload && options.title) { 109 | originalPreload = options.webPreferences.preload; 110 | // We replace the preload instead of using setPreloads because of some 111 | // differences in internal behaviour. 112 | options.webPreferences.preload = join(__dirname, "preload.js"); 113 | } 114 | 115 | super(options); 116 | this.webContents.sheltupdateOriginalPreload = originalPreload; 117 | } 118 | } 119 | 120 | const electronPath = require.resolve("electron"); 121 | delete require.cache[electronPath].exports; 122 | require.cache[electronPath].exports = { 123 | ...electron, 124 | BrowserWindow, 125 | }; 126 | 127 | // __BRANCHES_MAIN__ 128 | } 129 | -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/build/background.js: -------------------------------------------------------------------------------- 1 | (()=>{var e={5603:()=>{const e=[{id:"@react-devtools/proxy",js:["build/proxy.js"],matches:[""],persistAcrossSessions:!0,runAt:"document_start",world:chrome.scripting.ExecutionWorld.ISOLATED},{id:"@react-devtools/file-fetcher",js:["build/fileFetcher.js"],matches:[""],persistAcrossSessions:!0,runAt:"document_end",world:chrome.scripting.ExecutionWorld.ISOLATED},{id:"@react-devtools/hook",js:["build/installHook.js"],matches:[""],persistAcrossSessions:!0,runAt:"document_start",world:chrome.scripting.ExecutionWorld.MAIN},{id:"@react-devtools/hook-settings-injector",js:["build/hookSettingsInjector.js"],matches:[""],persistAcrossSessions:!0,runAt:"document_start"}];!async function(){try{await chrome.scripting.unregisterContentScripts(),await chrome.scripting.registerContentScripts(e)}catch(e){console.error(e)}}()}},t={};function __webpack_require__(n){var o=t[n];if(void 0!==o)return o.exports;var r=t[n]={exports:{}};return e[n](r,r.exports,__webpack_require__),r.exports}(()=>{"use strict";__webpack_require__(5603);const background_setExtensionIconAndPopup=function(e,t){chrome.action.setIcon({tabId:t,path:{16:chrome.runtime.getURL(`icons/16-${e}.png`),32:chrome.runtime.getURL(`icons/32-${e}.png`),48:chrome.runtime.getURL(`icons/48-${e}.png`),128:chrome.runtime.getURL(`icons/128-${e}.png`)}}),chrome.action.setPopup({tabId:t,popup:chrome.runtime.getURL(`popups/${e}.html`)})};function isRestrictedBrowserPage(e){if(!e)return!0;const t=new URL(e).protocol;return"chrome:"===t||"about:"===t}function checkAndHandleRestrictedPageIfSo(e){e&&isRestrictedBrowserPage(e.url)&&background_setExtensionIconAndPopup("restricted",e.id)}chrome.tabs.query({},(e=>e.forEach(checkAndHandleRestrictedPageIfSo))),chrome.tabs.onCreated.addListener((e=>checkAndHandleRestrictedPageIfSo(e))),chrome.tabs.onUpdated.addListener(((e,t,n)=>{t.url&&isRestrictedBrowserPage(t.url)&&background_setExtensionIconAndPopup("restricted",e)}));function executeScriptInMainWorld({target:e,files:t,injectImmediately:n}){return chrome.scripting.executeScript({target:e,files:t,injectImmediately:n,world:chrome.scripting.ExecutionWorld.MAIN})}const e=["compact"];const t={};function registerTab(e){t[e]||(t[e]={extension:null,proxy:null,disconnectPipe:null})}function connectExtensionAndProxyPorts(e,n,o){if(!e)throw new Error("Attempted to connect ports, when extension port is not present");if(!n)throw new Error("Attempted to connect ports, when proxy port is not present");if(t[o].disconnectPipe)throw new Error(`Attempted to connect already connected ports for tab with id ${o}`);function extensionPortMessageListener(e){try{n.postMessage(e)}catch(e){0,disconnectListener()}}function proxyPortMessageListener(t){try{e.postMessage(t)}catch(e){0,disconnectListener()}}function disconnectListener(){e.onMessage.removeListener(extensionPortMessageListener),n.onMessage.removeListener(proxyPortMessageListener),delete t[o].disconnectPipe}t[o].disconnectPipe=disconnectListener,e.onMessage.addListener(extensionPortMessageListener),n.onMessage.addListener(proxyPortMessageListener),e.onDisconnect.addListener(disconnectListener),n.onDisconnect.addListener(disconnectListener)}chrome.runtime.onConnect.addListener((e=>{if("proxy"===e.name){if(null==e.sender?.tab?.id)return;const n=e.sender.tab.id;return t[n]?.proxy&&(t[n].disconnectPipe?.(),t[n].proxy.disconnect()),registerTab(n),function(e,n){t[n].proxy=e,e.onDisconnect.addListener((()=>{t[n].disconnectPipe?.(),delete t[n].proxy}))}(e,n),void(t[n].extension&&connectExtensionAndProxyPorts(t[n].extension,t[n].proxy,n))}if(+(n=e.name)+""===n){const n=+e.name;return registerTab(n),function(e,n){t[n].extension=e,e.onDisconnect.addListener((()=>{t[n].disconnectPipe?.(),delete t[n].extension}))}(e,n),void(t[n].proxy&&connectExtensionAndProxyPorts(t[n].extension,t[n].proxy,n))}var n;console.warn(`Unknown port ${e.name} connected`)})),chrome.runtime.onMessage.addListener(((t,n)=>{switch(t?.source){case"devtools-page":!function(e){const{payload:t}=e;switch(t?.type){case"fetch-file-with-cache":{const{payload:{tabId:t,url:n}}=e;t&&n?chrome.tabs.sendMessage(t,{source:"devtools-page",payload:{type:"fetch-file-with-cache",url:n}}):chrome.runtime.sendMessage({source:"react-devtools-background",payload:{type:"fetch-file-with-cache-error",url:n,value:null}});break}case"inject-backend-manager":{const{payload:{tabId:t}}=e;if(!t)throw new Error("Couldn't inject backend manager: tabId not specified");executeScriptInMainWorld({injectImmediately:!0,target:{tabId:t},files:["/build/backendManager.js"]}).then((()=>{}),(e=>{console.error("Failed to inject backend manager:",e)}));break}}}(t);break;case"react-devtools-fetch-resource-content-script":!function(e){const{payload:t}=e;switch(t?.type){case"fetch-file-with-cache-complete":case"fetch-file-with-cache-error":chrome.runtime.sendMessage({source:"react-devtools-background",payload:t})}}(t);break;case"react-devtools-backend-manager":!function(t,n){const{payload:o}=t;"require-backends"===o?.type&&o.versions.forEach((t=>{e.includes(t)&&executeScriptInMainWorld({injectImmediately:!0,target:{tabId:n.tab.id},files:[`/build/react_devtools_backend_${t}.js`]})}))}(t,n);break;case"react-devtools-hook":!function(e,t){const{payload:n}=e;"react-renderer-attached"===n?.type&&background_setExtensionIconAndPopup(n.reactBuildType,t.tab.id)}(t,n)}})),chrome.tabs.onActivated.addListener((({tabId:e})=>{for(const n in t)if(null!=t[n].proxy&&null!=t[n].extension){const o=e===+n?"resumeElementPolling":"pauseElementPolling";t[n].extension.postMessage({event:o})}}))})()})(); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sheltupdate 2 | 3 | sheltupdate is a fork of [GooseUpdate](https://github.com/goose-nest/gooseupdate), 4 | which replicates Discord's update API, and injects mods and tweaks into the served modules. 5 | 6 | Changes from GooseUpdate: 7 | - Fixes to bring it up to date for 2024 8 | - Different branches, for use with shelter 9 | - Hugely refactored and converted to use much more modern technology 10 | (axios -> fetch, fastify -> hono, etc.) 11 | - A more robust patch system that improves multi-mod support 12 | 13 | # Branches 14 | 15 | Check the [shelter documentation](https://github.com/uwu/shelter/blob/main/README.md) for install instructions. 16 | 17 | The [uwu.network instance](https://inject.shelter.uwu.network) of sheltupdate hosts the branches exactly as found in this repository: 18 | - `shelter` - injects shelter 19 | - `vencord` - injects Vencord 20 | - `equicord` - injects Equicord 21 | - `betterdiscord` - injects BetterDiscord 22 | - `moonlight` - injects Moonlight 23 | - `reactdevtools` - adds React Developer Tools to your client 24 | - `spotify_embed_volume` - adds a volume slider to Spotify embeds 25 | - `yt_ad_block` - removes ads in embeds and in the Watch Together activity 26 | - `yt_embed_fix` - makes more videos viewable from within Discord (like UMG blocked ones) 27 | - `native_titlebar` - replaces Discord's custom titlebar with Windows' native one 28 | 29 | # Deploying 30 | 1. Install SheltUpdate's dependencies with `npm install` 31 | 2. Copy `config.example.json` to `config.json` and modify it to your liking, then run `node src/index.js`. 32 | 33 | The required files to deploy are `src`, `node_modules`, `branches`, `config.json`, `package.json` and `CHANGELOG.md`. 34 | 35 | ## Deploying with Docker 36 | 37 | Run the container as so: 38 | ```sh 39 | docker run -v /path/to/your/config.json:/config.json --tmpfs /tmp -p 8080:8080 ghcr.io/uwu/sheltupdate 40 | ``` 41 | 42 | The `--tmpfs` flag is available only on Linux hosts, and omitting it will not break sheltupdate, but will cause the 43 | sheltupdate cache to be left on your system, inflating disk use over time. 44 | 45 | In a docker compose file, you specify this with: 46 | ```yml 47 | services: 48 | sheltupdate: 49 | # rest of entry omitted here 50 | tmpfs: 51 | - /tmp 52 | ``` 53 | 54 | This step prevents the sheltupdate cache hitting the disk in practice, and therefore no resource leaks will happen when 55 | the container is restarted without being torn down and restored fresh. 56 | 57 | # Usage 58 | Discord fetches the update API URL from a `settings.json` file stored in various directories depending on your operating system. 59 | 60 | Said directories are found below: 61 | * Windows: 62 | * `%appdata%\discord\settings.json` 63 | * Mac: 64 | * `~/Library/Application Support/discord/settings.json` 65 | * Linux: 66 | * Package Manager/tar.gz Installation: `~/.config/discord/settings.json` 67 | * Flatpak: `~/.var/app/com.discordapp.Discord/config/discord/settings.json` 68 | 69 | Set `UPDATE_ENDPOINT` and `NEW_UPDATE_ENDPOINT` in `settings.json` as follows: 70 | 71 | ```json 72 | "UPDATE_ENDPOINT": "https:///branch" 73 | "NEW_UPDATE_ENDPOINT": "https:///branch/" 74 | ``` 75 | 76 | SheltUpdate also supports including multiple branches in updates by separating their names with a `+`, like `https:///branch1+branch2`. 77 | 78 | # Adding a branch 79 | SheltUpdate branches patch `discord_desktop_core` with files stored in `branches///`. 80 | 81 | Branches must have a `main.js` file to handle their injection in their branch directory, which is prepended to Discord's base `index.js` of the module. 82 | 83 | They must have a `meta.js` file that exports a `name` and `description`, and can optionally export a `setup` function, 84 | which may be asynchronous and will be periodically run. Use this to download other branch files you need. 85 | As arguments, you will be passed an absolute path to a folder to leave your files in, 86 | which on scheduled setup reruns *may or may not* contain your previous set-up files, and a log function. 87 | You have access to node and `@electron/asar`. 88 | 89 | They may optionally include a `preload.js` file to supplement their injection, 90 | which will automatically be injected for you by sheltupdate. 91 | 92 | sheltupdate will not automatically pick up and inject anything into the renderer, but it is trivial to implement this 93 | in your preload. 94 | 95 | ```javascript 96 | // main.js 97 | require('mod.js') 98 | ``` 99 | 100 | ```javascript 101 | // preload.js (optional) 102 | const { webFrame } = require("electron"); 103 | 104 | webFrame.top.executeJavaScript("console.log('HELLO FROM THE RENDERER');"); 105 | ``` 106 | 107 | If other files are in the branch directory, they will be added the module directory. 108 | 109 | # Credits 110 | 111 | GooseUpdate was originally written by [Ducko](https://github.com/CanadaHonk/). 112 | 113 | The shelter desktop injector has been contributed to by most of uwu.network at this point, 114 | and is the basis of the `shelter` branch here. 115 | 116 | The `vencord` branch is very very loosely based on the [Kernel vencord loader](https://github.com/kernel-addons/vencord-loader/blob/master/main.js). 117 | 118 | ![](https://github.com/catppuccin/catppuccin/raw/main/assets/footers/gray0_ctp_on_line.svg) 119 | 120 | sheltupdate is a passion project made with love, primarily by [Hazel](https://github.com/yellowsink) and [wiz](https://github.com/ioj4), 121 | with help from other uwu.network contributors. 122 | -------------------------------------------------------------------------------- /src/apiV2/patchModule.js: -------------------------------------------------------------------------------- 1 | import { Readable } from "stream"; 2 | import { createHash } from "crypto"; 3 | 4 | import { mkdirSync, writeFileSync, readFileSync, cpSync, rmSync } from "fs"; 5 | import { join, relative, win32, posix } from "path"; 6 | 7 | import tar from "tar"; 8 | import glob from "glob"; 9 | 10 | import { brotliDecompressSync, brotliCompressSync, constants } from "zlib"; 11 | import { ensureBranchIsReady, getBranch, getSingleBranchMetas } from "../common/branchesLoader.js"; 12 | import { section, withSection } from "../common/tracer.js"; 13 | import { SpanStatusCode } from "@opentelemetry/api"; 14 | import { cacheBase } from "../common/fsCache.js"; 15 | import { reportV2Cached, reportV2Patched } from "../dashboard/reporting.js"; 16 | import { dcMain, dcPreload } from "../desktopCore/index.js"; 17 | 18 | const cache = {}; 19 | 20 | // patched hash -> original hash 21 | const cacheDigests = new Map(); 22 | 23 | const sha256 = (data) => createHash("sha256").update(data).digest("hex"); 24 | 25 | const getCacheName = (moduleName, moduleVersion, branchName) => `${branchName}-${moduleName}-${moduleVersion}`; 26 | 27 | const download = (url) => fetch(url).then((r) => r.arrayBuffer()); 28 | 29 | const getBufferFromStream = withSection("buffer from stream", async (span, stream) => { 30 | const chunks = []; 31 | 32 | stream.read(); 33 | 34 | return await new Promise((resolve, reject) => { 35 | stream.on("data", (chunk) => chunks.push(chunk)); 36 | stream.on("error", reject); 37 | stream.on("end", () => resolve(Buffer.concat(chunks))); 38 | }); 39 | }); 40 | 41 | // node uses quality level 11 by default which is INSANE 42 | const brotlify = withSection("brotli", (span, buf) => 43 | brotliCompressSync(buf, { params: { [constants.BROTLI_PARAM_QUALITY]: 9 } }), 44 | ); 45 | 46 | export const patch = withSection("v2 module patcher", async (span, m, branchName) => { 47 | const cacheName = getCacheName("discord_desktop_core", m.module_version, branchName); 48 | 49 | const cached = cache[cacheName]; 50 | if (cached) { 51 | const expectedSource = cacheDigests.get(cached.hash); 52 | 53 | if (expectedSource && expectedSource === m.package_sha256) { 54 | reportV2Cached(); 55 | return cached.hash; 56 | } else { 57 | // evict cache 58 | cacheDigests.delete(cached.hash); 59 | delete cache[cacheName]; 60 | } 61 | } 62 | reportV2Patched(); 63 | 64 | await ensureBranchIsReady(branchName); 65 | 66 | const branch = getBranch(branchName); 67 | 68 | const brotli = await section("download original module", async () => { 69 | const data = await download(m.url); 70 | return brotliDecompressSync(data); 71 | }); 72 | 73 | const eDir = join(cacheBase, cacheName, "extract"); 74 | const filesDir = join(eDir, "files"); 75 | mkdirSync(eDir, { recursive: true }); 76 | 77 | await section("extract original module", async () => { 78 | const stream = Readable.from(brotli); 79 | 80 | const xTar = stream.pipe( 81 | tar.x({ 82 | cwd: eDir, 83 | }), 84 | ); 85 | 86 | await new Promise((res) => { 87 | xTar.on("finish", () => res()); 88 | }); 89 | }); 90 | 91 | const allFiles = section("patch module files", () => { 92 | let deltaManifest = JSON.parse(readFileSync(join(eDir, "delta_manifest.json"), "utf8")); 93 | 94 | const moddedIndex = dcMain.replace("// __BRANCHES_MAIN__", branch.main); 95 | writeFileSync(join(filesDir, "index.js"), moddedIndex); 96 | deltaManifest.files["index.js"] = { New: { Sha256: sha256(moddedIndex) } }; 97 | 98 | const moddedPreload = dcPreload.replace("// __BRANCHES_PRELOAD__", branch.preload); 99 | writeFileSync(join(filesDir, "preload.js"), moddedPreload); 100 | deltaManifest.files["preload.js"] = { New: { Sha256: sha256(moddedPreload) } }; 101 | 102 | const availableBranches = JSON.stringify(getSingleBranchMetas(), null, 4); 103 | writeFileSync(join(filesDir, "branches.json"), availableBranches); 104 | deltaManifest.files["branches.json"] = { New: { Sha256: sha256(availableBranches) } }; 105 | 106 | for (const cacheDir of branch.cacheDirs) { 107 | cpSync(cacheDir, filesDir, { recursive: true }); 108 | } 109 | 110 | const allFiles = glob.sync(`${filesDir}/**/*.*`); 111 | for (const f of allFiles) { 112 | // The updater always expects '/' as separator in delta_manifest.json (regardless of platform) 113 | const key = relative(filesDir, f).replaceAll(win32.sep, posix.sep); 114 | 115 | deltaManifest.files[key] = { 116 | New: { 117 | Sha256: sha256(readFileSync(f)), 118 | }, 119 | }; 120 | } 121 | 122 | writeFileSync(join(eDir, "delta_manifest.json"), JSON.stringify(deltaManifest)); 123 | 124 | return allFiles; 125 | }); 126 | 127 | return await section("compress final module", async () => { 128 | const tarStream = tar.c( 129 | { 130 | cwd: eDir, 131 | }, 132 | ["delta_manifest.json", ...allFiles.map((f) => relative(eDir, f))], 133 | ); 134 | 135 | const tarBuffer = await getBufferFromStream(tarStream); 136 | 137 | const final = brotlify(tarBuffer); 138 | 139 | const finalHash = sha256(final); 140 | 141 | cache[cacheName] = { 142 | hash: finalHash, 143 | final, 144 | }; 145 | 146 | // for detecting staleness later 147 | cacheDigests.set(finalHash, m.package_sha256); 148 | 149 | rmSync(eDir, { force: true, recursive: true }); 150 | 151 | return finalHash; 152 | }); 153 | }); 154 | 155 | export const getFinal = withSection("v2 module patcher", (span, req) => { 156 | const moduleName = req.param("moduleName"); 157 | const moduleVersion = req.param("moduleVersion"); 158 | const branchName = req.param("branch"); 159 | const cached = cache[getCacheName(moduleName, moduleVersion, branchName)]; 160 | 161 | span.setAttribute("module_patcher.cache_name", getCacheName(moduleName, moduleVersion, branchName)); 162 | 163 | if (!cached) { 164 | span.addEvent("module was not cached, this should never happen."); 165 | span.setStatus({ code: SpanStatusCode.ERROR }); 166 | // uhhh it should always be 167 | return; 168 | } 169 | 170 | return cached.final; 171 | }); 172 | 173 | // export const getChecksum = async (m, branch) => sha256(await patch(m, branch)); 174 | -------------------------------------------------------------------------------- /branches/mod/shelter/main.js: -------------------------------------------------------------------------------- 1 | const electron = require("electron"); 2 | const path = require("path"); 3 | const Module = require("module"); 4 | const fs = require("original-fs"); // "fs" module without electron modifications 5 | const https = require("https"); 6 | const { EOL } = require("os"); 7 | 8 | const logger = new Proxy(console, { 9 | get: (target, key) => 10 | function (...args) { 11 | return target[key].apply(console, ["[shelter]", ...args]); 12 | }, 13 | }); 14 | 15 | logger.log("Loading..."); 16 | 17 | // #region Bundle 18 | const remoteUrl = 19 | process.env.SHELTER_BUNDLE_URL || "https://raw.githubusercontent.com/uwu/shelter-builds/main/shelter.js"; 20 | const distPath = process.env.SHELTER_DIST_PATH; 21 | 22 | let localBundle; 23 | 24 | if (distPath) { 25 | localBundle = 26 | fs.readFileSync(path.join(distPath, "shelter.js"), "utf8") + 27 | `\n//# sourceMappingURL=file://${process.platform === "win32" ? "/" : ""}${path.join(distPath, "shelter.js.map")}`; 28 | } 29 | 30 | let remoteBundle; 31 | let remoteBundlePromise; 32 | 33 | const fetchRemoteBundleIfNeeded = () => { 34 | if (localBundle || remoteBundle) return Promise.resolve(); 35 | 36 | remoteBundlePromise ??= new Promise((resolve) => { 37 | const req = https.get(remoteUrl); 38 | 39 | req.on("response", (res) => { 40 | if (res.statusCode !== 200) { 41 | remoteBundlePromise = null; 42 | resolve(); 43 | return; 44 | } 45 | const chunks = []; 46 | 47 | res.on("data", (chunk) => chunks.push(chunk)); 48 | res.on("end", () => { 49 | let script = Buffer.concat(chunks).toString("utf-8"); 50 | 51 | if (!script.includes("//# sourceMappingURL=")) script += `\n//# sourceMappingURL=${remoteUrl + ".map"}`; 52 | remoteBundle = script; 53 | remoteBundlePromise = null; 54 | resolve(); 55 | }); 56 | }); 57 | 58 | req.on("error", (e) => { 59 | logger.error("Error fetching remote bundle:", e); 60 | remoteBundlePromise = null; 61 | resolve(); 62 | }); 63 | 64 | req.end(); 65 | }); 66 | 67 | return remoteBundlePromise; 68 | }; 69 | 70 | fetchRemoteBundleIfNeeded(); 71 | 72 | const getShelterBundle = () => { 73 | if (localBundle) return localBundle; 74 | if (remoteBundle) return remoteBundle; 75 | return `console.error("[shelter] Bundle could not be fetched in time. Aborting!");`; 76 | }; 77 | // #endregion 78 | 79 | // #region IPC 80 | electron.ipcMain.handle("SHELTER_BUNDLE_FETCH", getShelterBundle); 81 | 82 | // used by preload 83 | electron.ipcMain.handle("SHELTER_BRANCHCHANGE_SECURITY_DIALOG", (_, message) => 84 | electron.dialog.showMessageBox({ 85 | message, 86 | type: "warning", 87 | buttons: ["Cancel", "Confirm"], 88 | title: "Sheltupdate mods change", 89 | detail: 90 | 'We confirm for security reasons that this action is intended by the user. Only continue if you got here from the shelter "Client Mods" UI.', 91 | }), 92 | ); 93 | // #endregion 94 | 95 | // #region CSP 96 | electron.session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders }, done) => { 97 | const cspHeaders = Object.keys(responseHeaders).filter((name) => 98 | name.toLowerCase().startsWith("content-security-policy"), 99 | ); 100 | 101 | for (const header of cspHeaders) { 102 | delete responseHeaders[header]; 103 | } 104 | 105 | done({ responseHeaders }); 106 | }); 107 | 108 | electron.session.defaultSession.webRequest.onHeadersReceived = () => {}; 109 | // #endregion 110 | 111 | // #region Settings 112 | 113 | // Patch DevTools setting, enabled by default 114 | const enableDevTools = process.env.SHELTER_FORCE_DEVTOOLS?.toLowerCase() !== "false"; 115 | 116 | const originalRequire = Module.prototype.require; 117 | 118 | Module.prototype.require = function (path) { 119 | const loadedModule = originalRequire.call(this, path); 120 | if (!path.endsWith("appSettings")) return loadedModule; 121 | 122 | const settingsApi = 123 | loadedModule?.appSettings?.getSettings?.() ?? // stock 124 | loadedModule?.getSettings?.(); // openasar 125 | 126 | const settingsStore = 127 | settingsApi?.settings ?? // Original 128 | settingsApi?.store; // OpenAsar 129 | 130 | if (settingsApi) { 131 | const rg = /^(https?:\/\/.+)\/([a-zA-Z0-9_+-]+)\/?$/; 132 | 133 | const getHost = () => { 134 | const ue1 = settingsApi.get("UPDATE_ENDPOINT"); 135 | const ue2 = settingsApi.get("NEW_UPDATE_ENDPOINT"); 136 | 137 | if (typeof ue1 === "string") { 138 | const match = ue1.match(rg); 139 | if (match?.[1]) { 140 | return match[1]; 141 | } 142 | } 143 | 144 | if (typeof ue2 === "string") { 145 | const match = ue2.match(rg); 146 | if (match?.[1]) { 147 | return match[1]; 148 | } 149 | } 150 | }; 151 | 152 | electron.ipcMain.handle("SHELTER_HOST_GET", getHost); 153 | 154 | electron.ipcMain.handle("SHELTER_HOST_SET", (_, h) => { 155 | const ue1 = settingsApi.get("UPDATE_ENDPOINT"); 156 | const ue2 = settingsApi.get("NEW_UPDATE_ENDPOINT"); 157 | 158 | if (typeof ue1 === "string") { 159 | const match = ue1.match(rg); 160 | if (match?.[2]) { 161 | settingsApi.set("UPDATE_ENDPOINT", `${h}/${match[2]}`); 162 | } 163 | } 164 | 165 | if (typeof ue2 === "string") { 166 | const match = ue2.match(rg); 167 | if (match?.[2]) { 168 | settingsApi.set("NEW_UPDATE_ENDPOINT", `${h}/${match[2]}`); 169 | } 170 | } 171 | }); 172 | 173 | electron.ipcMain.handle("SHELTER_BRANCH_GET", () => { 174 | const ue1 = settingsApi.get("UPDATE_ENDPOINT"); 175 | const ue2 = settingsApi.get("NEW_UPDATE_ENDPOINT"); 176 | 177 | if (typeof ue1 === "string") { 178 | const match = ue1.match(rg); 179 | if (match?.[2]) { 180 | return match[2].split("+"); 181 | } 182 | } 183 | 184 | if (typeof ue2 === "string") { 185 | const match = ue2.match(rg); 186 | if (match?.[2]) { 187 | return match[2].split("+"); 188 | } 189 | } 190 | 191 | return []; 192 | }); 193 | 194 | electron.ipcMain.handle("SHELTER_BRANCH_SET", (_, b) => { 195 | const host = getHost(); 196 | 197 | if (b.length) { 198 | settingsApi.set("UPDATE_ENDPOINT", `${host}/${b.join("+")}`); 199 | settingsApi.set("NEW_UPDATE_ENDPOINT", `${host}/${b.join("+")}/`); 200 | } else { 201 | settingsApi.set("UPDATE_ENDPOINT", undefined); 202 | settingsApi.set("NEW_UPDATE_ENDPOINT", undefined); 203 | } 204 | }); 205 | 206 | try { 207 | if (enableDevTools) 208 | Object.defineProperty(settingsStore, "DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING", { 209 | get: () => true, 210 | set: () => {}, 211 | configurable: false, 212 | enumerable: false, // prevents our patched value from getting saved to settings.json 213 | }); 214 | Module.prototype.require = originalRequire; 215 | } catch (e) { 216 | logger.error(`Error getting settings module: ${e}${EOL}${e.stack}`); 217 | } 218 | } 219 | return loadedModule; 220 | }; 221 | // #endregion 222 | 223 | // #region Patch BrowserWindow 224 | class BrowserWindow extends electron.BrowserWindow { 225 | constructor(options) { 226 | super(options); 227 | const originalLoadURL = this.loadURL; 228 | this.loadURL = async function (url) { 229 | if (url.includes("discord.com/app")) { 230 | await fetchRemoteBundleIfNeeded(); 231 | } 232 | return await originalLoadURL.apply(this, arguments); 233 | }; 234 | } 235 | } 236 | 237 | const electronPath = require.resolve("electron"); 238 | delete require.cache[electronPath].exports; 239 | require.cache[electronPath].exports = { 240 | ...electron, 241 | BrowserWindow, 242 | }; 243 | // #endregion 244 | -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/_metadata/verified_contents.json: -------------------------------------------------------------------------------- 1 | [{"description":"treehash per file","signed_content":{"payload":"eyJjb250ZW50X2hhc2hlcyI6W3siYmxvY2tfc2l6ZSI6NDA5NiwiZGlnZXN0Ijoic2hhMjU2IiwiZmlsZXMiOlt7InBhdGgiOiJidWlsZC9iYWNrZW5kTWFuYWdlci5qcyIsInJvb3RfaGFzaCI6IlFLWHJKYlV5UFlRX0hhWFcwWm53MUJwSGtLYmV5SzNsUG5VN2NMTnFfZVkifSx7InBhdGgiOiJidWlsZC9iYWNrZ3JvdW5kLmpzIiwicm9vdF9oYXNoIjoiZktyNWFSYy1ub2hXSFBQZ0laZThkVzZZRTZhTzRhcGN3OTNXaEJFN05JcyJ9LHsicGF0aCI6ImJ1aWxkL2ZpbGVGZXRjaGVyLmpzIiwicm9vdF9oYXNoIjoiX1FBYkVLcEg0OEx2amtuU1hHd0NWdS1pSkFtUU1aZ01OdEM3cUJIMWgxRSJ9LHsicGF0aCI6ImJ1aWxkL2hvb2tTZXR0aW5nc0luamVjdG9yLmpzIiwicm9vdF9oYXNoIjoicFFrTl9hTUxWZUNJeFlyNTRWWVVValViak0ycTNnYWx1ZWdYRzVvbFdoTSJ9LHsicGF0aCI6ImJ1aWxkL2ltcG9ydEZpbGUud29ya2VyLndvcmtlci5qcyIsInJvb3RfaGFzaCI6IkdVVUIzV2VNdDc3SjBHZ183UnY0Uk8tV0Z0MUE3ZGlxM05XTkVHOEFPN2sifSx7InBhdGgiOiJidWlsZC9pbnN0YWxsSG9vay5qcyIsInJvb3RfaGFzaCI6Ii1NNXFOSFU5Q3BmeWt6TXpkeGFjUXVtaGd2NWhuSWo1M2lfMFAwUUNGcjQifSx7InBhdGgiOiJidWlsZC9pbnN0YWxsSG9vay5qcy5tYXAiLCJyb290X2hhc2giOiJid0JDU25WX3Aza3k5N2tXQVZyajhOWklqNU1MeU5Qdlc3NEYzRHN5eUlvIn0seyJwYXRoIjoiYnVpbGQvbWFpbi5qcyIsInJvb3RfaGFzaCI6IkIweHY1bDhKZVE2ZnJ2UWNndlc1d24wUmZvMXhMZF9uem92Tlp2VjJ0bVUifSx7InBhdGgiOiJidWlsZC9wYW5lbC5qcyIsInJvb3RfaGFzaCI6Ik52M0cxSVU2SGkyaXp5WVVFM0hyaUZCV1pJMDFWYVNXOUFZWWZ6eGNIMWMifSx7InBhdGgiOiJidWlsZC9wYXJzZVNvdXJjZUFuZE1ldGFkYXRhLndvcmtlci53b3JrZXIuanMiLCJyb290X2hhc2giOiIxUU13enFuOWR6UFFZaW9CR2k5SWFpbUVhcWFuNloxYTN1dUJuZ3ZuVWhFIn0seyJwYXRoIjoiYnVpbGQvcHJlcGFyZUluamVjdGlvbi5qcyIsInJvb3RfaGFzaCI6IkJBZGw4ajhLV0hfallmbUMxUWJJaVd2Zk1BTy15aGprY1JnTlhmQTJUczQifSx7InBhdGgiOiJidWlsZC9wcm94eS5qcyIsInJvb3RfaGFzaCI6Ilg3c1lsREVfX2VlM3VFWTQ2QV94MFNKNXNtRGc3em51M05VYkJ3Z2FlME0ifSx7InBhdGgiOiJidWlsZC9yZWFjdF9kZXZ0b29sc19iYWNrZW5kX2NvbXBhY3QuanMiLCJyb290X2hhc2giOiJUQTl2NXpJWFlQS2ZlTDR5ODFkcE5FZE5xbExPa3BicU54dk1wR3pkLUxZIn0seyJwYXRoIjoiYnVpbGQvcmVhY3RfZGV2dG9vbHNfYmFja2VuZF9jb21wYWN0LmpzLm1hcCIsInJvb3RfaGFzaCI6InV3XzFETWNKeUlSckktbkg1dER0UGRzYV9BS2ZwY3ZFTGozc3lvU2ZJdHcifSx7InBhdGgiOiJpY29ucy8xMjgtZGVhZGNvZGUucG5nIiwicm9vdF9oYXNoIjoiNC1UT1ZKWUVaU3BxVnpWUTgwczVsSmt0ZlhDXzA1djJuOE1NX1hhYmRmZyJ9LHsicGF0aCI6Imljb25zLzEyOC1kZXZlbG9wbWVudC5wbmciLCJyb290X2hhc2giOiI0LVRPVkpZRVpTcHFWelZRODBzNWxKa3RmWENfMDV2Mm44TU1fWGFiZGZnIn0seyJwYXRoIjoiaWNvbnMvMTI4LWRpc2FibGVkLnBuZyIsInJvb3RfaGFzaCI6Ink5NHJsellrclhoZk4xSS1fc3lwY2hnVE5VOHFmaUhzMHdWV0Nzd1RqQVkifSx7InBhdGgiOiJpY29ucy8xMjgtb3V0ZGF0ZWQucG5nIiwicm9vdF9oYXNoIjoiN0stdGM2TVczZVk0a2YwMzNYOS02Zm1YYjU5YlV2R3laYmtLSVBnYU5lQSJ9LHsicGF0aCI6Imljb25zLzEyOC1wcm9kdWN0aW9uLnBuZyIsInJvb3RfaGFzaCI6IkdCTnlCYTBnSGJTR1BqRmg4SlhRdjlxdEdXbUFzOXFlczFYMUYzVW9Ga00ifSx7InBhdGgiOiJpY29ucy8xMjgtcmVzdHJpY3RlZC5wbmciLCJyb290X2hhc2giOiJ5OTRybHpZa3JYaGZOMUktX3N5cGNoZ1ROVThxZmlIczB3VldDc3dUakFZIn0seyJwYXRoIjoiaWNvbnMvMTI4LXVubWluaWZpZWQucG5nIiwicm9vdF9oYXNoIjoiNC1UT1ZKWUVaU3BxVnpWUTgwczVsSmt0ZlhDXzA1djJuOE1NX1hhYmRmZyJ9LHsicGF0aCI6Imljb25zLzE2LWRlYWRjb2RlLnBuZyIsInJvb3RfaGFzaCI6ImNGa0N3QTNGejBySVhaNnNVU29Yc3VDN1FuM0pqTExNanhreUtwVmdBSFkifSx7InBhdGgiOiJpY29ucy8xNi1kZXZlbG9wbWVudC5wbmciLCJyb290X2hhc2giOiJjRmtDd0EzRnowcklYWjZzVVNvWHN1QzdRbjNKakxMTWp4a3lLcFZnQUhZIn0seyJwYXRoIjoiaWNvbnMvMTYtZGlzYWJsZWQucG5nIiwicm9vdF9oYXNoIjoiRXg2ZjhrZm82U1Z6VVJLYklEY0oyS2lpVklqM0ZadEpqV1licUZFT19YNCJ9LHsicGF0aCI6Imljb25zLzE2LW91dGRhdGVkLnBuZyIsInJvb3RfaGFzaCI6IkRJWnBvclI4cFVGLUEtU09qUHlrR0FaUXBfd3lZaGlyN1o5XzM5aEdnTnMifSx7InBhdGgiOiJpY29ucy8xNi1wcm9kdWN0aW9uLnBuZyIsInJvb3RfaGFzaCI6IlU5NDBrQnljYjVoSkVFZU5ZcENjNnNQaW1sRDRrOVRTYW4ycTFFQkNKOUkifSx7InBhdGgiOiJpY29ucy8xNi1yZXN0cmljdGVkLnBuZyIsInJvb3RfaGFzaCI6IkV4NmY4a2ZvNlNWelVSS2JJRGNKMktpaVZJajNGWnRKaldZYnFGRU9fWDQifSx7InBhdGgiOiJpY29ucy8xNi11bm1pbmlmaWVkLnBuZyIsInJvb3RfaGFzaCI6ImNGa0N3QTNGejBySVhaNnNVU29Yc3VDN1FuM0pqTExNanhreUtwVmdBSFkifSx7InBhdGgiOiJpY29ucy8zMi1kZWFkY29kZS5wbmciLCJyb290X2hhc2giOiJVN1FweVJRVXZ1UC1KVk1vQXA3X1VUOFhMX19FSDlZOWxhcXR0cGZPY2FVIn0seyJwYXRoIjoiaWNvbnMvMzItZGV2ZWxvcG1lbnQucG5nIiwicm9vdF9oYXNoIjoiVTdRcHlSUVV2dVAtSlZNb0FwN19VVDhYTF9fRUg5WTlsYXF0dHBmT2NhVSJ9LHsicGF0aCI6Imljb25zLzMyLWRpc2FibGVkLnBuZyIsInJvb3RfaGFzaCI6IkVVSGZhYzhkR1lYRW4zbGJaYXk2Vi1UU09aNXY5UXV6NXIzQXR4RHllQVEifSx7InBhdGgiOiJpY29ucy8zMi1vdXRkYXRlZC5wbmciLCJyb290X2hhc2giOiJNaUJxMlpXZDlLcDVENVJrbTNQR2x5ZHBobGVZd0I4T3ZhbFZHak5raUFnIn0seyJwYXRoIjoiaWNvbnMvMzItcHJvZHVjdGlvbi5wbmciLCJyb290X2hhc2giOiJ2dy1BLW1CN2lIVUZBUnFKVEFUXzMya0daUFJsdGktVG1sVmRzOU50UXIwIn0seyJwYXRoIjoiaWNvbnMvMzItcmVzdHJpY3RlZC5wbmciLCJyb290X2hhc2giOiJFVUhmYWM4ZEdZWEVuM2xiWmF5NlYtVFNPWjV2OVF1ejVyM0F0eER5ZUFRIn0seyJwYXRoIjoiaWNvbnMvMzItdW5taW5pZmllZC5wbmciLCJyb290X2hhc2giOiJVN1FweVJRVXZ1UC1KVk1vQXA3X1VUOFhMX19FSDlZOWxhcXR0cGZPY2FVIn0seyJwYXRoIjoiaWNvbnMvNDgtZGVhZGNvZGUucG5nIiwicm9vdF9oYXNoIjoiVkd3Q1lLdGFBOE1IWHBKNGxXQVpJVXUyUXozaVpzYjVXM0Fod0JGUWNPSSJ9LHsicGF0aCI6Imljb25zLzQ4LWRldmVsb3BtZW50LnBuZyIsInJvb3RfaGFzaCI6IlZHd0NZS3RhQThNSFhwSjRsV0FaSVV1MlF6M2lac2I1VzNBaHdCRlFjT0kifSx7InBhdGgiOiJpY29ucy80OC1kaXNhYmxlZC5wbmciLCJyb290X2hhc2giOiJEZF95RlJFajRDTGdCLUNWZHFhd01NT0FJZERfX0hWRzhNbWh1RktZRS1JIn0seyJwYXRoIjoiaWNvbnMvNDgtb3V0ZGF0ZWQucG5nIiwicm9vdF9oYXNoIjoiMUd5NFRmM1hhYnBQOEdVVHlyaHNqalhkTzF4SVROb0xyVXZ3Rm90dE9sQSJ9LHsicGF0aCI6Imljb25zLzQ4LXByb2R1Y3Rpb24ucG5nIiwicm9vdF9oYXNoIjoicVJVYVZaUEI3LVkxUFJVMFJOWlRTTXdUbTlyUmg5TS1JYml4ekpLQ3FHdyJ9LHsicGF0aCI6Imljb25zLzQ4LXJlc3RyaWN0ZWQucG5nIiwicm9vdF9oYXNoIjoiRGRfeUZSRWo0Q0xnQi1DVmRxYXdNTU9BSWREX19IVkc4TW1odUZLWUUtSSJ9LHsicGF0aCI6Imljb25zLzQ4LXVubWluaWZpZWQucG5nIiwicm9vdF9oYXNoIjoiVkd3Q1lLdGFBOE1IWHBKNGxXQVpJVXUyUXozaVpzYjVXM0Fod0JGUWNPSSJ9LHsicGF0aCI6Imljb25zL2RlYWRjb2RlLnN2ZyIsInJvb3RfaGFzaCI6InJNWm5NeVZEelVrUWw3S0JfVFc5U2I5RFYyalV5eXE0OVhsMEFISjFZeE0ifSx7InBhdGgiOiJpY29ucy9kZXZlbG9wbWVudC5zdmciLCJyb290X2hhc2giOiJyTVpuTXlWRHpVa1FsN0tCX1RXOVNiOURWMmpVeXlxNDlYbDBBSEoxWXhNIn0seyJwYXRoIjoiaWNvbnMvZGlzYWJsZWQuc3ZnIiwicm9vdF9oYXNoIjoiSjVNUXd0X29jZlAycUhYdlBWejdpU0xUZjY3cm5CSi13eEt1Ni1IRkhOTSJ9LHsicGF0aCI6Imljb25zL291dGRhdGVkLnN2ZyIsInJvb3RfaGFzaCI6IjE5M2szM3ZqU0xOQjFOZ01xSDRpLVI3NmF4R3l5LVVtVVRTSWJoWnZTSkkifSx7InBhdGgiOiJpY29ucy9wcm9kdWN0aW9uLnN2ZyIsInJvb3RfaGFzaCI6IkVQeHBxSEFPSEc5cFFLRzFyWHpUSWZjdnFfak1GMVNMUUd4NW9UZGJhUDgifSx7InBhdGgiOiJpY29ucy9yZXN0cmljdGVkLnN2ZyIsInJvb3RfaGFzaCI6Iko1TVF3dF9vY2ZQMnFIWHZQVno3aVNMVGY2N3JuQkotd3hLdTYtSEZITk0ifSx7InBhdGgiOiJtYWluLmh0bWwiLCJyb290X2hhc2giOiJzNzlEalhEbXZ4N21nckhWTXJqaHdyZzRZN0lGMndiblFMRHJ1Q0toVFlNIn0seyJwYXRoIjoibWFuaWZlc3QuanNvbiIsInJvb3RfaGFzaCI6IllkWjIxQjl3Umg3a0Q0dXBROFBiTl9KeU1ad3Vjd0puWjRpX2xQYzB6bjQifSx7InBhdGgiOiJwYW5lbC5odG1sIiwicm9vdF9oYXNoIjoiUzA2bnYyLTU2YXREcmZJWnBhMC1xaVhOM0VMcXJBX2p2a3Q1UXh3MTlsQSJ9LHsicGF0aCI6InBvcHVwcy9kZWFkY29kZS5odG1sIiwicm9vdF9oYXNoIjoiYVJaTHYyYTUtdDMtZFAzU04tb1dSUUlKcElvT2o3LU9iUjV0YWpGX1laMCJ9LHsicGF0aCI6InBvcHVwcy9kZXZlbG9wbWVudC5odG1sIiwicm9vdF9oYXNoIjoiZGxaNWFqV0FwOEhDajZWWGxPcGtBaGxIeUZycmhYVnlEWmpUd0praWtERSJ9LHsicGF0aCI6InBvcHVwcy9kaXNhYmxlZC5odG1sIiwicm9vdF9oYXNoIjoiZ2c1V1RCRWd6VEtzekNyTDJ3M3Ntck5KVnBFZl83cHlwTTUzY2Z4SV9VdyJ9LHsicGF0aCI6InBvcHVwcy9vdXRkYXRlZC5odG1sIiwicm9vdF9oYXNoIjoiWlhyaEJTekdBTFZGMlVwNEJOR2l3UTFqTnB6a1hmSU04OFNEVnpacXdIYyJ9LHsicGF0aCI6InBvcHVwcy9wcm9kdWN0aW9uLmh0bWwiLCJyb290X2hhc2giOiJRRmFNYW1zc3JtTk5ZMFdmazVZOE9oNjFDUGx2S2l0ZTdFVmtPYk9rcFZJIn0seyJwYXRoIjoicG9wdXBzL3Jlc3RyaWN0ZWQuaHRtbCIsInJvb3RfaGFzaCI6Ijl2UFZQd1hvVnlPMmhieTV4UGJkWHNBbElKWkVQNFZSWGRmbG82Q055VDQifSx7InBhdGgiOiJwb3B1cHMvc2hhcmVkLmNzcyIsInJvb3RfaGFzaCI6Im1UWThoNXVfZFllMnN2U3daOU4zRVdvZk5HOENlRWFkbHFZbDdYajUxQ3cifSx7InBhdGgiOiJwb3B1cHMvc2hhcmVkLmpzIiwicm9vdF9oYXNoIjoiNEFibjljZ0NEdm0tUHdkWjAweWM5UlRSaV80X0UzRXA3eDJ2eTVBZDRrbyJ9LHsicGF0aCI6InBvcHVwcy91bm1pbmlmaWVkLmh0bWwiLCJyb290X2hhc2giOiJVODFWVk8taHlwSVJwNFFSVE1Nc3FJdlROajR5dVl5N1V0c2FLWV9IaTFzIn1dLCJmb3JtYXQiOiJ0cmVlaGFzaCIsImhhc2hfYmxvY2tfc2l6ZSI6NDA5Nn1dLCJpdGVtX2lkIjoiZm1rYWRtYXBnb2ZhZG9wbGpiamZrYXBka29pZW5paGkiLCJpdGVtX3ZlcnNpb24iOiI3LjAuMSIsInByb3RvY29sX3ZlcnNpb24iOjF9","signatures":[{"header":{"kid":"publisher"},"protected":"eyJhbGciOiJSUzI1NiJ9","signature":"VxM3p0JVmG2SNOlvg4S9X9SiabK7IE_R6weRqdndurVpE8gSnA37Ob4mzYZ71PbZZ1-cDw7KAwcuvb5iUh2wStgR0d84EEL1nPW9znLMgHUmdAmDDp5r0Ci2c-WV9atAEI9fOjVPa58NATLOvf51ZAf8QxdFnFROqNYYQnw0p7GWzucuaD7tY7IlAfBS0R7jvzSwCIo6zV-witbbrewaAEUZoABMnymhtyqCTeqjX8yYeLXi5XKbATPjsx4k4XFmdL00GF-C4BP7pjlj3G_LC-InPG8louhTtbjrF8M4lmDazJwISR9NTwcho2WcHcV1m0MrjZMCU6sPYIqdqpjr1Q"},{"header":{"kid":"webstore"},"protected":"eyJhbGciOiJSUzI1NiJ9","signature":"F_sqDK5C5G6sp3jIjenL3ybWrWx5vJbPdqYWr33W7rFLv5Sk0AjLILxb2y6z480P9k1qwqdzciE3iQNu0qwl-ycTj3ooiZ6HHns71CyOeoSCJLt2XOR2fO-2GaW3rrrYK1DWeAHC4CBTifUIz2nwqvO50QsQWyuwh-OHBHTn_BdmcIR8qxRhGWlXoS4zHvKqcHz2u-F5LiFBgx07lf207FE2Ho6tgbmzBSLo7cJCbFkirf7yKknMW0WyEPbV-qLTao5b-RQEkfxBz16jllEB1AIhXZRcwnXikHrQLC532JGowbb417Q3X012nOqtGHYBFxK9HTO0ao3RDGZi1B4UMQ"}]}}] -------------------------------------------------------------------------------- /src/dashboard/dashboard.js: -------------------------------------------------------------------------------- 1 | // for some reason esm.sh needs bundle-deps for this. probably helps bundle size anyway. 2 | // full bundle: 391.25kB, partial bundle: 246.31kB. its something! 3 | import * as Plot from "@observablehq/plot?bundle-deps&exports=plot,barX,text,gridX"; 4 | // full bundle: 82.36kB, partial bundle: 23.15kB 5 | import { 6 | format, 7 | formatDurationWithOptions, 8 | intervalToDuration, 9 | } from "date-fns/fp?bundle-deps&exports=format,formatDurationWithOptions,intervalToDuration"; 10 | 11 | const since = (t) => intervalToDuration({ start: t, end: new Date() }); 12 | 13 | const cap = (s) => 14 | s 15 | .split(" ") 16 | .map((w) => w[0].toUpperCase() + w.slice(1)) 17 | .join(" "); 18 | 19 | const formatTime = format("h:mm:ss b"); 20 | const formatDurDHM = formatDurationWithOptions({ format: ["days", "hours", "minutes"] }); 21 | const formatDurAuto = formatDurationWithOptions({}); 22 | 23 | const formatSince = (s) => formatDurDHM(since(s)) || formatDurAuto(since(s)); 24 | 25 | const [uptimeEl, startTimeEl, endpointWrap, branchesWrap, channelsWrap, platformsWrap, hostVersWrap, apiVersWrap] = [ 26 | "stat-uptime", 27 | "stat-start-time", 28 | "endpoint-plot-wrap", 29 | "branches-wrap", 30 | "chans-wrap", 31 | "plats-wrap", 32 | "hosts-wrap", 33 | "apiv-wrap", 34 | ].map(document.getElementById.bind(document)); 35 | 36 | const statsState = __STATE__; /*{ 37 | uniqueUsers: { 38 | a: { 39 | platform: "linux", 40 | host_version: "0.0.78", 41 | channel: "stable", 42 | branch: "shelter+reactdevtools+vencord", 43 | apiVer: 1, 44 | time: new Date("2024-12-20 9:00:00"), 45 | }, 46 | b: { 47 | platform: "win", 48 | host_version: "unknown", 49 | channel: "canary", 50 | branch: "shelter+vencord", 51 | apiVer: 2, 52 | time: new Date("2024-12-20 16:00:00"), 53 | }, 54 | c: { 55 | platform: "win", 56 | host_version: "unknown", 57 | channel: "canary", 58 | branch: "shelter+vencord", 59 | apiVer: 2, 60 | time: new Date("2024-12-20 16:00:00"), 61 | }, 62 | d: { 63 | platform: "mac", 64 | host_version: "unknown", 65 | channel: "stable", 66 | branch: "vencord", 67 | apiVer: 2, 68 | time: new Date("2024-12-20 13:00:00"), 69 | }, 70 | }, 71 | requestCounts: { 72 | v1_host_squirrel: 435, 73 | v1_host_notsquirrel: 34, 74 | v1_modules: 45, 75 | v1_module_download: 345, 76 | v2_manifest: 45, 77 | v2_module: 56, 78 | }, 79 | proxyOrRedirect: { 80 | proxied: 89, 81 | redirected: 2, 82 | }, 83 | proxyCacheHitRatio: { 84 | hit: 90, 85 | miss: 34, 86 | }, 87 | v1ModuleCacheHitRatio: { 88 | hit: 90, 89 | miss: 2, 90 | }, 91 | v2ManifestCacheHitRatio: { 92 | hit: 90, 93 | miss: 31, 94 | }, 95 | };*/ 96 | 97 | const branchMetadata = __BRANCHES__; /*[ 98 | { 99 | version: 110, 100 | type: "mod", 101 | name: "shelter", 102 | displayName: "shelter", 103 | description: "Injects shelter", 104 | hidden: false, 105 | }, 106 | { 107 | version: 16, 108 | type: "mod", 109 | name: "vencord", 110 | displayName: "Vencord", 111 | description: "Injects Vencord; This is not an officially supported Vencord install method", 112 | hidden: false, 113 | }, 114 | { 115 | version: 223, 116 | type: "mod", 117 | name: "betterdiscord", 118 | displayName: "BetterDiscord", 119 | description: "Injects BetterDiscord", 120 | hidden: false, 121 | }, 122 | { 123 | version: 231, 124 | type: "tool", 125 | name: "reactdevtools", 126 | displayName: "React Developer Tools", 127 | description: "Adds the React Dev Tools to the web developer panel", 128 | hidden: false, 129 | }, 130 | { 131 | version: 13, 132 | type: "tweak", 133 | name: "spotify_embed_volume", 134 | displayName: "Spotify Embed Volume", 135 | description: "Adds a volume slider to Spotify embeds", 136 | hidden: false, 137 | }, 138 | { 139 | version: 125, 140 | type: "tweak", 141 | name: "yt_ad_block", 142 | displayName: "YouTube Ad Block", 143 | description: "Removes ads in embeds and in the Watch Together activity", 144 | hidden: false, 145 | }, 146 | { 147 | version: 122, 148 | type: "tweak", 149 | name: "yt_embed_fix", 150 | displayName: "YouTube Embed Fix", 151 | description: "Enables more videos to be viewable from within Discord (like UMG blocked ones)", 152 | hidden: false, 153 | }, 154 | ];*/ 155 | 156 | const startTime = new Date(__START_TIME__ /*1734667290000*/); 157 | startTimeEl.textContent = formatTime(startTime); 158 | 159 | const refreshTimes = () => { 160 | uptimeEl.textContent = formatSince(startTime); 161 | }; 162 | refreshTimes(); 163 | setInterval(refreshTimes, 1_000); 164 | 165 | const endpointsEntries = Object.entries(statsState.requestCounts).map(([name, hits]) => { 166 | const ns = name.split("_"); 167 | 168 | return { hits, endpoint: cap(ns.slice(1).join(" ")) + ` [${ns[0].toUpperCase()}]` }; 169 | }); 170 | 171 | const branchCounts = {}; 172 | const platformCounts = {}; 173 | const hostVerCounts = {}; 174 | const apiVerCounts = {}; 175 | const channelCounts = {}; 176 | for (const user of Object.values(statsState.uniqueUsers)) { 177 | const host = user.host_version === "unknown" ? "Unknown" : user.host_version; 178 | 179 | for (const br of user.branch.split("+")) { 180 | const brPretty = branchMetadata.find((b) => b.name === br)?.displayName ?? br; 181 | 182 | branchCounts[brPretty] ??= 0; 183 | branchCounts[brPretty]++; 184 | } 185 | platformCounts[cap(user.platform)] ??= 0; 186 | platformCounts[cap(user.platform)]++; 187 | hostVerCounts[host] ??= 0; 188 | hostVerCounts[host]++; 189 | apiVerCounts["API V" + user.apiVer] ??= 0; 190 | apiVerCounts["API V" + user.apiVer]++; 191 | channelCounts[cap(user.channel)] ??= 0; 192 | channelCounts[cap(user.channel)]++; 193 | } 194 | 195 | endpointWrap.append( 196 | Plot.plot({ 197 | marginTop: 0, 198 | marginLeft: 160, 199 | marginRight: 35, 200 | label: null, 201 | marks: [ 202 | Plot.barX(endpointsEntries, { y: "endpoint", x: "hits", sort: { y: "-x" } }), 203 | Plot.text(endpointsEntries, { y: "endpoint", x: "hits", text: "hits", textAnchor: "start", dx: 4 }), 204 | Plot.gridX(), 205 | ], 206 | }), 207 | ); 208 | 209 | branchesWrap.append( 210 | Plot.plot({ 211 | marginTop: 0, 212 | marginLeft: 160, 213 | marginRight: 35, 214 | label: null, 215 | marks: [ 216 | Plot.barX(Object.entries(branchCounts), { y: "0", x: "1", sort: { y: "-x" } }), 217 | Plot.text(Object.entries(branchCounts), { y: "0", x: "1", text: "1", textAnchor: "start", dx: 4 }), 218 | Plot.gridX(), 219 | ], 220 | }), 221 | ); 222 | 223 | const byValue = ([, valueA], [, valueB]) => (valueA > valueB ? -1 : valueA < valueB ? 1 : 0); 224 | 225 | const sortedPlatformCounts = Object.entries(platformCounts).sort(byValue); 226 | platformsWrap.append( 227 | Plot.plot({ 228 | marginTop: 0, 229 | marginLeft: 35, 230 | marginRight: 35, 231 | height: 20, 232 | label: null, 233 | axis: false, 234 | color: { legend: true, scheme: "dark2", domain: sortedPlatformCounts.map(([k]) => k) }, 235 | marks: [Plot.barX(sortedPlatformCounts, { x: "1", fill: "0" })], 236 | }), 237 | ); 238 | 239 | const sortedChannelCounts = Object.entries(channelCounts).sort(byValue); 240 | channelsWrap.append( 241 | Plot.plot({ 242 | marginTop: 0, 243 | marginLeft: 35, 244 | marginRight: 35, 245 | height: 20, 246 | label: null, 247 | axis: false, 248 | color: { legend: true, scheme: "dark2", domain: sortedChannelCounts.map(([k]) => k) }, 249 | marks: [Plot.barX(sortedChannelCounts, { x: "1", fill: "0" })], 250 | }), 251 | ); 252 | 253 | const sortedHostVerCounts = Object.entries(hostVerCounts).sort(byValue); 254 | hostVersWrap.append( 255 | Plot.plot({ 256 | marginTop: 0, 257 | marginLeft: 35, 258 | marginRight: 35, 259 | height: 20, 260 | label: null, 261 | axis: false, 262 | color: { legend: true, scheme: "dark2", domain: sortedHostVerCounts.map(([k]) => k) }, 263 | marks: [Plot.barX(sortedHostVerCounts, { x: "1", fill: "0" })], 264 | }), 265 | ); 266 | 267 | const sortedApiVerCounts = Object.entries(apiVerCounts).sort(byValue); 268 | apiVersWrap.append( 269 | Plot.plot({ 270 | marginTop: 0, 271 | marginLeft: 35, 272 | marginRight: 35, 273 | height: 20, 274 | label: null, 275 | axis: false, 276 | color: { legend: true, scheme: "dark2", domain: sortedApiVerCounts.map(([k]) => k) }, 277 | marks: [Plot.barX(sortedApiVerCounts, { x: "1", fill: "0" })], 278 | }), 279 | ); 280 | -------------------------------------------------------------------------------- /src/common/branchesLoader.js: -------------------------------------------------------------------------------- 1 | import { mkdirSync, readFileSync, cpSync } from "fs"; 2 | import { join, basename } from "path"; 3 | import { pathToFileURL } from "url"; 4 | import { createHash } from "crypto"; 5 | 6 | import glob from "glob"; 7 | import { config, srcDir, version as shupVersion } from "./config.js"; 8 | import { cacheBase } from "./fsCache.js"; 9 | import { dcVersion } from "../desktopCore/index.js"; 10 | import { withSection, section } from "./tracer.js"; 11 | 12 | let branches = {}; 13 | 14 | const orderingMap = new Map(); // string => number 15 | 16 | const setupPromises = new Map(); // string => [Promise, resolve(), boolean] 17 | 18 | const sha256 = (data) => createHash("sha256").update(data).digest("hex"); 19 | 20 | const sortBranchesInPlace = (b) => { 21 | try { 22 | return b.sort((a, b) => { 23 | const oa = orderingMap.get(a); 24 | const ob = orderingMap.get(b); 25 | if (oa === undefined || ob === undefined) throw new Error("Invalid branch requested"); 26 | 27 | return oa - ob; 28 | }); 29 | } catch { 30 | return undefined; 31 | } 32 | }; 33 | 34 | export const getBranch = (b) => branches[sortBranchesInPlace(b.split("+"))?.join("+")]; 35 | 36 | export const getSingleBranchMetas = () => { 37 | const sbranches = Object.entries(branches).filter(([, b]) => b.type !== "mixed" && !b.hidden); 38 | sbranches.sort(([a], [b]) => orderingMap.get(a) - orderingMap.get(b)); 39 | 40 | return sbranches.map(([n, b]) => ({ 41 | version: b.version, 42 | type: b.type, 43 | name: n, 44 | displayName: b.displayName, 45 | description: b.description, 46 | incompatibilities: b.incompatibilities, 47 | hidden: b.hidden, 48 | })); 49 | }; 50 | 51 | // waits for any active setup to finish 52 | export const ensureBranchIsReady = withSection("wait for branch ready", async (span, br) => { 53 | await Promise.all( 54 | br.split("+").map((b) => { 55 | const [promise, _, isSettingUp] = setupPromises.get(b); 56 | if (isSettingUp) span.addEvent(`Having to wait for branch ${b} to set up...`); 57 | 58 | return promise; 59 | }), 60 | ); 61 | }); 62 | 63 | const getBranchFilesCacheDir = (b) => join(cacheBase, `extra-files-${b}`); 64 | 65 | const init = withSection("branch finder", async (span) => { 66 | const branchDir = join(srcDir, "..", "branches"); 67 | 68 | const dirs = glob.sync(join(branchDir, "*", "*")); 69 | 70 | span.addEvent("found branch list", { dirs }); 71 | 72 | await section("load branches", async (_span) => { 73 | for (let d of dirs) { 74 | const splits = d.split("/"); 75 | 76 | const name = splits.pop(); 77 | const type = splits.pop(); 78 | 79 | let files = glob.sync(`${d}/*`); 80 | 81 | let main = ""; 82 | let preload = ""; 83 | let displayName = name; 84 | let description = ""; 85 | let incompatibilities = []; // optional 86 | let hidden = false; // optional 87 | let setup = undefined; // optional 88 | for (let i = 0; i < files.length; i++) { 89 | const f = files[i]; 90 | const filename = f.split("/").pop(); 91 | 92 | if (filename === "main.js") { 93 | const mainSrc = readFileSync(f, "utf8").split("\n").join("\n\t\t"); 94 | main = `// Branch: ${name}\n\ttry {\n\t\t${mainSrc}\n\t} catch(e) { console.error("[sheltupdate] Main error (${name}):", e) }\n`; 95 | files.splice(i--, 1); 96 | } else if (filename === "preload.js") { 97 | const preloadSrc = readFileSync(f, "utf8").split("\n").join("\n\t"); 98 | preload = `// Branch: ${name}\ntry {\n\t${preloadSrc}\n} catch(e) { console.error("[sheltupdate] Preload error (${name}):", e) }\n`; 99 | files.splice(i--, 1); 100 | } else if (filename === "meta.js") { 101 | const metaMod = await import(pathToFileURL(f)); 102 | displayName = metaMod.name; 103 | description = metaMod.description; 104 | incompatibilities = metaMod.incompatibilities ?? []; 105 | hidden = !!metaMod.hidden; 106 | setup = metaMod.setup; 107 | files.splice(i--, 1); 108 | } 109 | } 110 | 111 | // copy extra files into cache 112 | const cacheDir = getBranchFilesCacheDir(name); 113 | mkdirSync(cacheDir); 114 | 115 | for (let i = 0; i < files.length; i++) { 116 | const oldPath = files[i]; 117 | const newPath = join(cacheDir, oldPath.slice(d.length + 1)); 118 | files[i] = newPath; 119 | 120 | cpSync(oldPath, newPath, { recursive: true }); 121 | } 122 | 123 | // we will reset these anyway later for branches with setups, 124 | // but for the rest of them, just populate it now. 125 | const allFiles = glob.sync(`${d}/**/*.*`); 126 | const fileHashes = allFiles.map((f) => sha256(readFileSync(f))); 127 | 128 | // the list of branches is sent along with the module and accounting for that is difficult so 129 | // just factor the sheltupdate release number into the version and leave it at that. 130 | const version = parseInt(sha256(fileHashes.join(" ") + main + preload + dcVersion + shupVersion).substring(0, 2), 16); 131 | const internalFiles = ["main.js", "preload.js", "meta.js"]; 132 | 133 | branches[name] = { 134 | files: allFiles.filter((f) => !internalFiles.includes(basename(f))), 135 | cacheDirs: [cacheDir], 136 | main, 137 | preload, 138 | version, 139 | type, 140 | displayName, 141 | description, 142 | incompatibilities, 143 | hidden, 144 | setup, 145 | }; 146 | 147 | // create wait-for-setup promises 148 | if (setup) { 149 | let resolve; 150 | const prom = new Promise((r) => (resolve = r)); 151 | setupPromises.set(name, [prom, resolve, false]); 152 | } else { 153 | setupPromises.set(name, [Promise.resolve(), () => {}, false]); 154 | } 155 | } 156 | }); 157 | 158 | section("fix branch ordering", (_span) => { 159 | const orderingJson = JSON.parse(readFileSync(join(branchDir, "ordering.json"), "utf8")); 160 | 161 | // validate 162 | const orderingJsonSet = new Set(orderingJson); 163 | if (orderingJson.length !== orderingJsonSet.size) throw new Error("ordering.json contains duplicates"); 164 | 165 | for (let i = 0; i < orderingJson.length; i++) { 166 | const b = orderingJson[i]; 167 | orderingMap.set(b, i); 168 | 169 | if (!branches[b]) throw new Error(`ordering.json references a non-existent branch ${b}`); 170 | } 171 | 172 | for (const b of Object.keys(branches)) 173 | if (!orderingJsonSet.has(b)) { 174 | span.addEvent(`ordering.json does not mention branch ${b}, it will be sorted to the end`); 175 | 176 | orderingMap.set(b, orderingMap.size); 177 | } 178 | }); 179 | 180 | section("create mixed branches", (_span) => { 181 | const baseBranchNames = Object.keys(branches); 182 | 183 | sortBranchesInPlace(baseBranchNames); 184 | 185 | const allBranches = []; 186 | // thanks lith for this one :) 187 | { 188 | const n = baseBranchNames.length; 189 | 190 | for (let i = 1; i < 1 << n; i++) { 191 | const combination = []; 192 | 193 | for (let j = 0; j < n; j++) if (i & (1 << j)) combination.push(baseBranchNames[j]); 194 | 195 | allBranches.push(combination); 196 | } 197 | } 198 | 199 | for (const bNames of allBranches) { 200 | if (bNames.length === 1) continue; // already just fine 201 | 202 | const key = bNames.join("+"); 203 | 204 | const bs = bNames.map((n) => branches[n]); 205 | 206 | branches[key] = { 207 | // these will be updated by setups later so have to make it lazy 208 | get files() { 209 | return bs.map((x) => x.files).reduce((x, a) => a.concat(x), []); 210 | }, 211 | get cacheDirs() { 212 | return bs.flatMap((b) => b.cacheDirs); 213 | }, 214 | main: bs 215 | .map((x) => x.main) 216 | .filter((p) => p) 217 | .join("\n\t"), 218 | preload: bs 219 | .map((x) => x.preload) 220 | .filter((p) => p) 221 | .join("\n"), 222 | // cap the version well under u32::max or some rust code somewhere in the client dies 223 | // this will be updated by setups later so have to make it lazy 224 | get version() { 225 | return Number(BigInt(bs.map((x) => x.version).reduce((x, a) => `${x}0${a}`)) % BigInt(2 ** 28)) + 100; 226 | }, 227 | type: "mixed", 228 | }; 229 | } 230 | }); 231 | }); 232 | 233 | const runBranchSetups = withSection("periodic branch setups", async (span) => { 234 | // perfect for async code I guess 235 | 236 | await Promise.all(Object.keys(branches).map(singleSetup)); 237 | 238 | async function singleSetup(b) { 239 | if (branches[b].type === "mixed" || !branches[b].setup) return; 240 | 241 | await section(`${b} setup`, async (span) => { 242 | const [_promise, resolve, isSettingUp, goAnyway] = setupPromises.get(b); 243 | if (isSettingUp && !goAnyway) { 244 | span.addEvent(`Skipped setting up ${b} as it was already being setup.`); 245 | return; 246 | } 247 | 248 | let newResolve; 249 | const newProm = new Promise((r) => (newResolve = r)); 250 | setupPromises.set(b, [newProm, newResolve, true]); 251 | 252 | // create a folder in cache 253 | const cacheDir = getBranchFilesCacheDir(b); 254 | try { 255 | await branches[b].setup(cacheDir, (...a) => span.addEvent(a.join(" "))); 256 | } catch (e) { 257 | // we failed! leave it in a "setting up" state until next time. 258 | setupPromises.set(b, [newProm, newResolve, true, true]); 259 | throw e; 260 | } 261 | 262 | // regenerate files and version 263 | const allFiles = glob.sync(`${cacheDir}/**/*.*`); 264 | branches[b].cacheDirs = [cacheDir]; 265 | branches[b].files = allFiles; 266 | 267 | const fileHashes = allFiles.map((f) => sha256(readFileSync(f))); 268 | branches[b].version = parseInt( 269 | sha256(fileHashes.join(" ") + branches[b].main + branches[b].preload + dcVersion).substring(0, 2), 270 | 16, 271 | ); 272 | 273 | setupPromises.set(b, [newProm, newResolve, false]); 274 | 275 | resolve(); 276 | newResolve(); 277 | }); 278 | } 279 | }); 280 | 281 | await init(); // lol top level await go BRRRRRRRRR 282 | 283 | await runBranchSetups(); 284 | 285 | setInterval(runBranchSetups, config.setupIntervalHours * 60 * 60 * 1000); 286 | -------------------------------------------------------------------------------- /shup-ha/src/index.ts: -------------------------------------------------------------------------------- 1 | // D1 database schema: 2 | // CREATE TABLE incidents (timestamp REAL, env TEXT, nodesUp TEXT, allNodes TEXT, message TEXT, PRIMARY KEY (timestamp, env)) 3 | // :) 4 | // there is no migration have fun 5 | 6 | type Origin = { 7 | name: string; 8 | url: string; 9 | }; 10 | 11 | type Config = Record; 12 | 13 | enum OriginStatusType { 14 | UP = 0, // down: false 15 | DOWN = 1, // down: true 16 | GRACE // secret third thing 17 | } 18 | 19 | // key is hostname concated with origin url 20 | type OriginStatus = { down: OriginStatusType; when: string }; 21 | 22 | type Incident = { 23 | timestamp: number, 24 | env: string, 25 | nodesUp: string, 26 | allNodes: string, 27 | message: string 28 | }; 29 | 30 | // the config is passed in via a cf secret. turn it into a useful object here 31 | // config example: 32 | // "inject.uwu.network; CH, https://ch.shup.net; IT, https://it.shup.net ~ staging.shup.net; CH, https://shup.net" 33 | function parseConfig(configStr: string): Config { 34 | const cfg: Config = {}; 35 | 36 | for (const env of configStr.split("~").map((e) => e.trim())) { 37 | // parse out env name 38 | const env_name = env.split(":")[0]; 39 | const nodes_cfg = env.slice(env_name.length + 1); 40 | 41 | const nodes = nodes_cfg.split(";").map((node) => node.split(",").map((s) => s.trim())); 42 | 43 | cfg[env_name] = []; 44 | 45 | for (const [name, url] of nodes) cfg[env_name].push({ name, url }); 46 | } 47 | 48 | return cfg; 49 | } 50 | 51 | function stripUnicode(unicodeStr: string) { 52 | // remove the flags from node names to serve 53 | return [...unicodeStr].filter(c => c.charCodeAt(0) <= 127).join("") 54 | } 55 | 56 | async function reportNodeHealth(up: boolean, env: Env, envName: string, origins: Origin[], origin: Origin) { 57 | 58 | let existingStatus = (await env.origin_status.get(envName + origin.url, "json"))?.down; 59 | // `+` here converts any stored legacy `boolean`s correctly into `OriginStatusType`s 60 | existingStatus = existingStatus ? +existingStatus : undefined; 61 | 62 | // we have a very limited number of kv put()s so we need to be frugal with them 63 | // put only if we have a non-up status stored, and the node is going down 64 | const shouldPut = !up || existingStatus !== OriginStatusType.UP; 65 | 66 | const newStatusType = up ? OriginStatusType.UP : { 67 | [OriginStatusType.UP]: OriginStatusType.GRACE, 68 | [OriginStatusType.GRACE]: OriginStatusType.DOWN, 69 | [OriginStatusType.DOWN]: OriginStatusType.DOWN, 70 | }[existingStatus ?? OriginStatusType.UP]; 71 | 72 | if (shouldPut) 73 | await env.origin_status.put( 74 | envName + origin.url, 75 | JSON.stringify({ 76 | down: newStatusType, 77 | when: new Date().toISOString(), 78 | } satisfies OriginStatus) 79 | ); 80 | 81 | // don't log in the database the first time we encounter 82 | // a downtime, as often cloudflare blinks and a service goes for just one second or so 83 | if (newStatusType === OriginStatusType.GRACE) return; 84 | 85 | // check if this node status is already recorded in D1 86 | const lastIncident = await env.incidents_db.prepare(` 87 | SELECT * FROM incidents 88 | WHERE env = ? AND message IS NULL 89 | ORDER BY timestamp DESC 90 | LIMIT 1 91 | `) 92 | .bind(envName) 93 | .first(); 94 | 95 | if (lastIncident && lastIncident.allNodes.split(";").includes(origin.url)) { 96 | // if we are listed in the last status as the same status as us, then we have nothing new to report. 97 | 98 | const weWerePreviouslyUp = lastIncident.nodesUp.split(";").includes(origin.url); 99 | 100 | if (up === weWerePreviouslyUp) 101 | return; 102 | } 103 | 104 | // update database values 105 | const lastAllNodes = lastIncident?.allNodes.split(";") ?? []; 106 | if (lastIncident?.allNodes === "") lastAllNodes.pop(); // "" parses to [""] annoyingly 107 | 108 | const allNodes = origins.map(o => o.url).sort().join(";") 109 | 110 | // [] feels like a bad default but idfk what else to do 111 | const lastNodesUpSet = new Set(lastIncident?.nodesUp.split(";") ?? []); 112 | if (lastIncident?.nodesUp === "") lastNodesUpSet.clear(); // "" still parses to [""] 113 | 114 | if (lastNodesUpSet.has(origin.url) !== up) { 115 | if (up) 116 | lastNodesUpSet.add(origin.url); 117 | else 118 | lastNodesUpSet.delete(origin.url); 119 | } 120 | 121 | const newNodesUp = [...lastNodesUpSet].sort().join(";"); 122 | 123 | // insert incident report into the database 124 | // message is null because that field is exclusivley for manually added outages 125 | // in which case, nodesUp and allNodes will be null instead 126 | await env.incidents_db.prepare(` 127 | INSERT INTO incidents (timestamp, env, nodesUp, allNodes, message) 128 | VALUES (unixepoch('subsec'), ?1, ?2, ?3, NULL) 129 | `) 130 | .bind(envName, newNodesUp, allNodes) 131 | .run(); 132 | 133 | 134 | const msg = up 135 | ? `sheltupdate origin node back up` 136 | : `sheltupdate origin node reported down`; 137 | 138 | const healthyNodesMsg = newNodesUp.length === allNodes.length 139 | ? `all nodes are healthy` 140 | : `healthy nodes left: ${newNodesUp.length} / ${allNodes.length}`; 141 | 142 | await fetch(env.WEBHOOK, { 143 | method: "POST", 144 | headers: { 145 | "Content-Type": "application/json" 146 | }, 147 | body: JSON.stringify({ 148 | username: "sheltupdate status", 149 | content: `${msg} 150 | \\- environment: \`${envName}\` 151 | \\- node: ${origin.name} 152 | \\- ${healthyNodesMsg}`, 153 | }) 154 | }); 155 | } 156 | 157 | // used by the scheduled worker, and fired off when a request fails to double-check this outcome. 158 | async function checkAndReportHealth(env: Env, environment: string, CONFIG: Config, origin: Origin) { 159 | let resp; 160 | try { 161 | resp = await fetch(origin.url, { method: "HEAD" }); 162 | } catch {} 163 | 164 | const nodeIsDown = !resp || (500 <= resp.status && resp.status <= 599); 165 | 166 | console.log("scheduled origin check: ", environment, origin, nodeIsDown, { 167 | status: resp?.status, 168 | headers: resp && Object.fromEntries(resp.headers.entries()), 169 | }); 170 | 171 | await reportNodeHealth(!nodeIsDown, env, environment, CONFIG[environment], origin); 172 | } 173 | 174 | export default { 175 | async fetch(request, env, ctx): Promise { 176 | 177 | const CONFIG = parseConfig(env.SHUP_CFG); 178 | 179 | const url = new URL(request.url); 180 | 181 | // we had this actually cause a 4 min outage in staging cause someone requested //cdn.js which somehow returns 530 182 | // from every node and rolled us all the way down to discord api 183 | if (url.pathname.startsWith("//")) 184 | return new Response("418 I'm a Teapot. Sincerely, Fuck Off.", { status: 418 }) 185 | 186 | if (!(url.hostname in CONFIG)) 187 | return new Response( 188 | `404 Not Found. This sheltupdate HA instance is not configured to handle requests for ${url.hostname}.`, 189 | { status: 404 } 190 | ); 191 | 192 | const origins = CONFIG[url.hostname as keyof typeof CONFIG]; 193 | 194 | const getStatus = async (originUrl: string) => 195 | await env.origin_status.get(url.hostname + originUrl, "json"); 196 | 197 | const addNodeHeader = (resp: Response, origin: Origin) => 198 | new Response(resp.body, { 199 | status: resp.status, 200 | headers: { 201 | ...Object.fromEntries(resp.headers.entries()), 202 | "Cache-Control": "no-store", 203 | "X-Shup-HA-Env": url.hostname, 204 | "X-Shup-HA-Node": stripUnicode(origin.name).trim(), 205 | }, 206 | webSocket: resp.webSocket, 207 | }); 208 | 209 | 210 | async function injectDashboard(origResp: Response) { 211 | const realHtml = await origResp.text(); 212 | 213 | const statuses: [string, string, string][] = []; 214 | 215 | let hitFirstYet = false; 216 | 217 | for (const o of origins) { 218 | const status = await getStatus(o.url); 219 | statuses.push([ 220 | o.name, 221 | status ? (status.down ? "Down" : hitFirstYet ? "Standby" : "Live") : "Unknown", 222 | status ? (status.down ? "#d22d39" : "#1b9e77") : "#666666", 223 | ]); 224 | 225 | if (status?.down !== OriginStatusType.DOWN) hitFirstYet = true; 226 | } 227 | 228 | const toInject = ` 229 |
230 |

Nodes Status

231 |
232 | ${statuses.map(([name, statusName, statusCol]) => ` 233 |
234 |
235 | ${name}: ${statusName} 236 |
237 | `).join("")} 238 |
239 |

All statistics above this box are counting only for the node currently serving you ("Live")

240 |
241 | `; 242 | 243 | const newHtml = realHtml.replace("", toInject + ""); 244 | 245 | return new Response(newHtml, { 246 | status: origResp.status, 247 | headers: origResp.headers, 248 | webSocket: origResp.webSocket, 249 | }); 250 | } 251 | 252 | async function injectDashCss(origResp: Response) { 253 | const real = await origResp.text(); 254 | 255 | // thanks microsoft, i love you too. 256 | 257 | const fontFaceDecl = ` 258 | @font-face { 259 | font-family: "Twemoji Flags"; 260 | unicode-range: U+1F1E6-1F1FF, U+1F3F4, U+E0062-E0063, U+E0065, U+E0067, U+E006C, U+E006E, U+E0073-E0074, U+E0077, U+E007F; 261 | src: url('https://esm.sh/country-flag-emoji-polyfill/dist/TwemojiCountryFlags.woff2') format('woff2'); 262 | font-display: swap; 263 | } 264 | 265 | `; 266 | 267 | const new_ = fontFaceDecl + real.replace("font-family:", `font-family:"Twemoji Flags",`); 268 | 269 | return new Response(new_, { 270 | status: origResp.status, 271 | headers: origResp.headers, 272 | webSocket: origResp.webSocket, 273 | }); 274 | } 275 | 276 | // get proxyin! 277 | for (const o of origins) { 278 | const status = await getStatus(o.url); 279 | if (status && status.down === OriginStatusType.DOWN) continue; 280 | 281 | let resp; 282 | try { 283 | resp = await fetch(new URL(url.pathname + url.search, o.url).href, { 284 | headers: { 285 | ...Object.fromEntries(request.headers.entries()), 286 | "X-Shup-HA-Env": url.hostname, 287 | }, 288 | method: request.method, 289 | redirect: "manual", 290 | cache: "no-store" 291 | }); 292 | let redirected = resp.status >= 300 && resp.status <= 399; 293 | 294 | if (redirected) { 295 | const location = resp.headers.get("location"); 296 | if (!location) throw new Error("received redirect without location"); 297 | 298 | const url = new URL(location); 299 | if (url.hostname !== "discord.com") { 300 | throw new Error(`node returned unexpected redirect, origin: ${o.name}, redirect: ${location}`); 301 | } 302 | } 303 | 304 | if (redirected || resp.ok) { 305 | // we dont want a slow D1 query in the code path for successful requests, so we dont `await` this. 306 | // after about 60 seconds, once KV caches invalidate, the nodes that are failed will be completely skipped, 307 | // so those are less of a concern for actually awaiting 308 | ctx.waitUntil(checkAndReportHealth(env, url.hostname, CONFIG, o)); 309 | 310 | // dashboard 311 | if (url.pathname === "/") return addNodeHeader(await injectDashboard(resp), o); 312 | if (url.pathname === "/dashboard.css") return addNodeHeader(await injectDashCss(resp), o); 313 | 314 | return addNodeHeader(resp, o); // :) 315 | } 316 | } catch (e) { 317 | console.error("fetch error:", e); 318 | resp = undefined; // make sure node is marked as failed 319 | } 320 | 321 | // something went wrong! 322 | const considerNodeFailed = !resp || (500 <= resp.status && resp.status <= 599); 323 | 324 | console.log("request failed,", considerNodeFailed ? "rolling over" : "returning error", url.hostname, o.name, { status: resp?.status, headers: resp && Object.fromEntries(resp.headers.entries()) }); 325 | 326 | if (considerNodeFailed) 327 | await reportNodeHealth(false, env, url.hostname, origins, o); 328 | 329 | // the user might just be stupid and have hit a 404 or something 330 | if (resp && !considerNodeFailed) return addNodeHeader(resp, o); 331 | } 332 | 333 | // none of our origins are ok! 334 | let proxyPath = "https://discord.com/api/"; 335 | if (url.pathname.includes("/distributions/") || url.pathname.includes("/distro/app/")) proxyPath += "updates/"; 336 | return Response.redirect(new URL(url.pathname.slice(2 + url.pathname.split("/")[1].length) + url.search, proxyPath).href, 307); 337 | }, 338 | 339 | async scheduled(controller, env, ctx) { 340 | 341 | const CONFIG = parseConfig(env.SHUP_CFG); 342 | 343 | // check all servers to see if they're okay 344 | for (const environment in CONFIG) 345 | for (const origin of CONFIG[environment]) 346 | await checkAndReportHealth(env, environment, CONFIG, origin); 347 | }, 348 | } satisfies ExportedHandler; 349 | -------------------------------------------------------------------------------- /branches/tool/reactdevtools/ext/build/backendManager.js: -------------------------------------------------------------------------------- 1 | (()=>{"use strict";var e={3018:(e,t,r)=>{const n=r(5986),i=Symbol("max"),s=Symbol("length"),o=Symbol("lengthCalculator"),l=Symbol("allowStale"),a=Symbol("maxAge"),h=Symbol("dispose"),u=Symbol("noDisposeOnSet"),c=Symbol("lruList"),d=Symbol("cache"),f=Symbol("updateAgeOnGet"),naiveLength=()=>1;const get=(e,t,r)=>{const n=e[d].get(t);if(n){const t=n.value;if(isStale(e,t)){if(del(e,n),!e[l])return}else r&&(e[f]&&(n.value.now=Date.now()),e[c].unshiftNode(n));return t.value}},isStale=(e,t)=>{if(!t||!t.maxAge&&!e[a])return!1;const r=Date.now()-t.now;return t.maxAge?r>t.maxAge:e[a]&&r>e[a]},trim=e=>{if(e[s]>e[i])for(let t=e[c].tail;e[s]>e[i]&&null!==t;){const r=t.prev;del(e,t),t=r}},del=(e,t)=>{if(t){const r=t.value;e[h]&&e[h](r.key,r.value),e[s]-=r.length,e[d].delete(r.key),e[c].removeNode(t)}};class p{constructor(e,t,r,n,i){this.key=e,this.value=t,this.length=r,this.now=n,this.maxAge=i||0}}const forEachStep=(e,t,r,n)=>{let i=r.value;isStale(e,i)&&(del(e,r),e[l]||(i=void 0)),i&&t.call(n,i.value,i.key,e)};e.exports=class{constructor(e){if("number"==typeof e&&(e={max:e}),e||(e={}),e.max&&("number"!=typeof e.max||e.max<0))throw new TypeError("max must be a non-negative number");this[i]=e.max||1/0;const t=e.length||naiveLength;if(this[o]="function"!=typeof t?naiveLength:t,this[l]=e.stale||!1,e.maxAge&&"number"!=typeof e.maxAge)throw new TypeError("maxAge must be a number");this[a]=e.maxAge||0,this[h]=e.dispose,this[u]=e.noDisposeOnSet||!1,this[f]=e.updateAgeOnGet||!1,this.reset()}set max(e){if("number"!=typeof e||e<0)throw new TypeError("max must be a non-negative number");this[i]=e||1/0,trim(this)}get max(){return this[i]}set allowStale(e){this[l]=!!e}get allowStale(){return this[l]}set maxAge(e){if("number"!=typeof e)throw new TypeError("maxAge must be a non-negative number");this[a]=e,trim(this)}get maxAge(){return this[a]}set lengthCalculator(e){"function"!=typeof e&&(e=naiveLength),e!==this[o]&&(this[o]=e,this[s]=0,this[c].forEach((e=>{e.length=this[o](e.value,e.key),this[s]+=e.length}))),trim(this)}get lengthCalculator(){return this[o]}get length(){return this[s]}get itemCount(){return this[c].length}rforEach(e,t){t=t||this;for(let r=this[c].tail;null!==r;){const n=r.prev;forEachStep(this,e,r,t),r=n}}forEach(e,t){t=t||this;for(let r=this[c].head;null!==r;){const n=r.next;forEachStep(this,e,r,t),r=n}}keys(){return this[c].toArray().map((e=>e.key))}values(){return this[c].toArray().map((e=>e.value))}reset(){this[h]&&this[c]&&this[c].length&&this[c].forEach((e=>this[h](e.key,e.value))),this[d]=new Map,this[c]=new n,this[s]=0}dump(){return this[c].map((e=>!isStale(this,e)&&{k:e.key,v:e.value,e:e.now+(e.maxAge||0)})).toArray().filter((e=>e))}dumpLru(){return this[c]}set(e,t,r){if((r=r||this[a])&&"number"!=typeof r)throw new TypeError("maxAge must be a number");const n=r?Date.now():0,l=this[o](t,e);if(this[d].has(e)){if(l>this[i])return del(this,this[d].get(e)),!1;const o=this[d].get(e).value;return this[h]&&(this[u]||this[h](e,o.value)),o.now=n,o.maxAge=r,o.value=t,this[s]+=l-o.length,o.length=l,this.get(e),trim(this),!0}const f=new p(e,t,l,n,r);return f.length>this[i]?(this[h]&&this[h](e,t),!1):(this[s]+=f.length,this[c].unshift(f),this[d].set(e,this[c].head),trim(this),!0)}has(e){if(!this[d].has(e))return!1;const t=this[d].get(e).value;return!isStale(this,t)}get(e){return get(this,e,!0)}peek(e){return get(this,e,!1)}pop(){const e=this[c].tail;return e?(del(this,e),e.value):null}del(e){del(this,this[d].get(e))}load(e){this.reset();const t=Date.now();for(let r=e.length-1;r>=0;r--){const n=e[r],i=n.e||0;if(0===i)this.set(n.k,n.v);else{const e=i-t;e>0&&this.set(n.k,n.v,e)}}}prune(){this[d].forEach(((e,t)=>get(this,t,!1)))}}},7533:e=>{e.exports=function(e){e.prototype[Symbol.iterator]=function*(){for(let e=this.head;e;e=e.next)yield e.value}}},5986:(e,t,r)=>{function Yallist(e){var t=this;if(t instanceof Yallist||(t=new Yallist),t.tail=null,t.head=null,t.length=0,e&&"function"==typeof e.forEach)e.forEach((function(e){t.push(e)}));else if(arguments.length>0)for(var r=0,n=arguments.length;r1)r=t;else{if(!this.head)throw new TypeError("Reduce of empty list with no initial value");n=this.head.next,r=this.head.value}for(var i=0;null!==n;i++)r=e(r,n.value,i),n=n.next;return r},Yallist.prototype.reduceReverse=function(e,t){var r,n=this.tail;if(arguments.length>1)r=t;else{if(!this.tail)throw new TypeError("Reduce of empty list with no initial value");n=this.tail.prev,r=this.tail.value}for(var i=this.length-1;null!==n;i--)r=e(r,n.value,i),n=n.prev;return r},Yallist.prototype.toArray=function(){for(var e=new Array(this.length),t=0,r=this.head;null!==r;t++)e[t]=r.value,r=r.next;return e},Yallist.prototype.toArrayReverse=function(){for(var e=new Array(this.length),t=0,r=this.tail;null!==r;t++)e[t]=r.value,r=r.prev;return e},Yallist.prototype.slice=function(e,t){(t=t||this.length)<0&&(t+=this.length),(e=e||0)<0&&(e+=this.length);var r=new Yallist;if(tthis.length&&(t=this.length);for(var n=0,i=this.head;null!==i&&nthis.length&&(t=this.length);for(var n=this.length,i=this.tail;null!==i&&n>t;n--)i=i.prev;for(;null!==i&&n>e;n--,i=i.prev)r.push(i.value);return r},Yallist.prototype.splice=function(e,t){e>this.length&&(e=this.length-1),e<0&&(e=this.length+e);for(var r=0,n=this.head;null!==n&&r{var t=e&&e.__esModule?()=>e.default:()=>e;return __webpack_require__.d(t,{a:t}),t},__webpack_require__.d=(e,t)=>{for(var r in t)__webpack_require__.o(t,r)&&!__webpack_require__.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},__webpack_require__.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{const esm_compareVersions=(e,t)=>{const r=validateAndParse(e),n=validateAndParse(t),i=r.pop(),s=n.pop(),o=compareSegments(r,n);return 0!==o?o:i&&s?compareSegments(i.split("."),s.split(".")):i||s?i?-1:1:0},e=/^[v^~<>=]*?(\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+))?(?:-([\da-z\-]+(?:\.[\da-z\-]+)*))?(?:\+[\da-z\-]+(?:\.[\da-z\-]+)*)?)?)?$/i,validateAndParse=t=>{if("string"!=typeof t)throw new TypeError("Invalid argument expected string");const r=t.match(e);if(!r)throw new Error(`Invalid argument not valid semver ('${t}' received)`);return r.shift(),r},isWildcard=e=>"*"===e||"x"===e||"X"===e,tryParse=e=>{const t=parseInt(e,10);return isNaN(t)?e:t},compareStrings=(e,t)=>{if(isWildcard(e)||isWildcard(t))return 0;const[r,n]=((e,t)=>typeof e!=typeof t?[String(e),String(t)]:[e,t])(tryParse(e),tryParse(t));return r>n?1:r{for(let r=0;r":[1],">=":[0,1],"=":[0],"<=":[-1,0],"<":[-1]};Object.keys(t);var r=__webpack_require__(3018),n=__webpack_require__.n(r);Symbol.for("react.element"),Symbol.for("react.transitional.element"),Symbol.for("react.portal"),Symbol.for("react.fragment"),Symbol.for("react.strict_mode"),Symbol.for("react.profiler"),Symbol.for("react.consumer"),Symbol.for("react.context"),Symbol.for("react.forward_ref"),Symbol.for("react.suspense"),Symbol.for("react.suspense_list"),Symbol.for("react.memo"),Symbol.for("react.lazy"),Symbol.for("react.scope"),Symbol.for("react.activity"),Symbol.for("react.legacy_hidden"),Symbol.for("react.tracing_marker"),Symbol.for("react.memo_cache_sentinel"),Symbol.for("react.postpone"),Symbol.for("react.view_transition"),Symbol.iterator;Symbol.asyncIterator;const i="React::DevTools::recordChangeDescriptions",s="React::DevTools::recordTimeline",o="React::DevTools::reloadAndProfile";function sessionStorageRemoveItem(e){try{sessionStorage.removeItem(e)}catch(e){}}function sessionStorageSetItem(e,t){try{return sessionStorage.setItem(e,t)}catch(e){}}Array.isArray,Object.prototype.hasOwnProperty,new WeakMap,new(n())({max:1e3}),Symbol.for("react.provider");function getIsReloadAndProfileSupported(){let e=!1;try{localStorage.getItem("test"),e=!0}catch(e){}return e&&!!(window.document&&window.document.featurePolicy&&window.document.featurePolicy.allowsFeature("sync-xhr"))}function getIfReloadedAndProfiling(){return"true"===function(e){try{return sessionStorage.getItem(e)}catch(e){return null}}(o)}function onReloadAndProfile(e,t){sessionStorageSetItem(o,"true"),sessionStorageSetItem(i,e?"true":"false"),sessionStorageSetItem(s,t?"true":"false")}Symbol("inspectable"),Symbol("inspected"),Symbol("name"),Symbol("preview_long"),Symbol("preview_short"),Symbol("readonly"),Symbol("size"),Symbol("type"),Symbol("unserializable");Array.isArray;const l="999.9.9";function hasAssignedBackend(e){return null!=e&&""!==e&&function(e="",t=""){return esm_compareVersions(e,t)>-1}(e,l)}const a="compact";let h=!1;const u=new Set;function registerRenderer(e,t){let r=e.reconcilerVersion||e.version;hasAssignedBackend(r)||(r=a),t.backends.has(r)||u.add(r)}function activateBackend(e,t){const r=t.backends.get(e);if(!r)throw new Error(`Could not find backend for version "${e}"`);const{Agent:n,Bridge:l,initBackend:a,setupNativeStyleEditor:h}=r,c=new l({listen(e){const listener=t=>{t.source===window&&t.data&&"react-devtools-content-script"===t.data.source&&t.data.payload&&e(t.data.payload)};return window.addEventListener("message",listener),()=>{window.removeEventListener("message",listener)}},send(e,t,r){window.postMessage({source:"react-devtools-bridge",payload:{event:e,payload:t}},"*",r)}}),d=new n(c,getIfReloadedAndProfiling(),onReloadAndProfile);sessionStorageRemoveItem(o),sessionStorageRemoveItem(i),sessionStorageRemoveItem(s),d.addListener("shutdown",(()=>{t.emit("shutdown"),delete window.__REACT_DEVTOOLS_BACKEND_MANAGER_INJECTED__})),a(t,d,window,getIsReloadAndProfileSupported()),"function"==typeof h&&t.resolveRNStyle&&h(c,d,t.resolveRNStyle,t.nativeStyleEditorValidAttributes),c.send("extensionBackendInitialized"),u.delete(e)}function updateRequiredBackends(){0!==u.size&&window.postMessage({source:"react-devtools-backend-manager",payload:{type:"require-backends",versions:Array.from(u)}},"*")}window.__REACT_DEVTOOLS_BACKEND_MANAGER_INJECTED__||(window.__REACT_DEVTOOLS_BACKEND_MANAGER_INJECTED__=!0,window.addEventListener("message",(function welcome(e){e.source===window&&"react-devtools-content-script"===e.data.source&&(h?console.warn('React DevTools detected duplicate welcome "message" events from the content script.'):(h=!0,window.removeEventListener("message",welcome),function(e){if(null==e)return;e.renderers.forEach((t=>{registerRenderer(t,e)})),e.backends.forEach(((t,r)=>{u.delete(r),activateBackend(r,e)})),updateRequiredBackends();const t=e.sub("renderer",(({renderer:t})=>{registerRenderer(t,e),updateRequiredBackends()})),r=e.sub("devtools-backend-installed",(t=>{activateBackend(t,e),updateRequiredBackends()})),n=e.sub("shutdown",(()=>{t(),r(),n()}))}(window.__REACT_DEVTOOLS_GLOBAL_HOOK__)))})))})()})(); -------------------------------------------------------------------------------- /branches/mod/shelter/selector-ui.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | const { 3 | plugins: { removePlugin }, 4 | solid: { createSignal, createMemo, onCleanup, untrack }, 5 | solidH: { html }, 6 | ui: { 7 | Text, 8 | TextTags, 9 | LinkButton, 10 | Divider, 11 | Button, 12 | ButtonColors, 13 | ButtonLooks, 14 | ButtonSizes, 15 | TextBox, 16 | showToast, 17 | Header, 18 | HeaderTags, 19 | SwitchItem, 20 | Space, 21 | openModal, 22 | ModalRoot, 23 | ModalHeader, 24 | ModalBody, 25 | ModalFooter, 26 | ModalConfirmFooter, 27 | ModalSizes, 28 | }, 29 | } = shelter; 30 | 31 | const { 32 | settings: { registerSection }, 33 | } = shelter.plugin.scoped; 34 | 35 | // deal with this plugin still existing after sheltupdate is gone! 36 | if (!window.SheltupdateNative) { 37 | const id = shelter.plugin.id; 38 | setTimeout(() => removePlugin(id), 50); 39 | return; // don't init or anything 40 | } 41 | 42 | const [branchMetaRaw, setBranchMetaRaw] = createSignal(); 43 | const [branchMetaGrouped, setBranchMetaGrouped] = createSignal(); 44 | const [currentBranches, setCurrentBranches] = createSignal(); 45 | 46 | const [currentHost, setCurrentHost] = createSignal(); 47 | 48 | // just because i don't trust myself, keep a copy of the branch before uninstalling sheltupdate. 49 | const [uninstallCache, setUninstallCache] = createSignal(); 50 | 51 | const [vencordOtherwiseLoaded, setVencordOtherwiseLoaded] = createSignal(false); 52 | const [bdOtherwiseLoaded, setBdOtherwiseLoaded] = createSignal(false); 53 | 54 | const updateCurrent = () => Promise.all([ 55 | SheltupdateNative.getCurrentBranches().then(setCurrentBranches), 56 | SheltupdateNative.getCurrentHost().then(setCurrentHost), 57 | ]); 58 | updateCurrent().then(() => { 59 | if (window.Vencord && !currentBranches().includes("vencord") && !currentBranches().includes("equicord")) 60 | setVencordOtherwiseLoaded(true); 61 | 62 | if (window.BdApi && !currentBranches().includes("betterdiscord")) setBdOtherwiseLoaded(true); 63 | }); 64 | 65 | SheltupdateNative.getAvailableBranches().then((branches) => { 66 | // group by type, conveniently "mod" is before "tool" alphabetically 67 | const grouped = {}; 68 | for (const branchName in branches) { 69 | const data = branches[branchName]; 70 | if (!grouped[data.type]) grouped[data.type] = {}; 71 | 72 | grouped[data.type][branchName] = data; 73 | } 74 | 75 | setBranchMetaGrouped(grouped); 76 | setBranchMetaRaw(branches); 77 | }); 78 | 79 | const prettyModNames = (branches) => { 80 | const modNames = [...branches.filter((b) => b !== "shelter").map((b) => branchMetaRaw()[b].name)]; 81 | 82 | // make sure that shelter always comes first 83 | if (branches.includes("shelter")) { 84 | modNames.unshift("shelter"); 85 | } 86 | 87 | if (modNames.length === 1) return modNames[0]; 88 | if (modNames.length === 2) return modNames.join(" and "); 89 | const lastMod = modNames.pop(); 90 | return modNames.join(", ") + ", and " + lastMod; 91 | }; 92 | 93 | // ok so this will display *above* the shelter heading which is not ideal but its okay i guess 94 | registerSection("divider"); 95 | registerSection("header", "Sheltupdate"); 96 | registerSection("section", "sheltupdate", "Client Mods", SettingsView); 97 | 98 | function BranchEntry(props /*: { name, data, value, onChange } */) { 99 | // note: if shelter is disabled (i.e. you uninstalled sheltupdate), allow switching back on 100 | const disabled = () => { 101 | if (props.name === "shelter" && props.value) { 102 | return "You need shelter to have access to this menu. Try uninstalling sheltupdate."; 103 | } 104 | if ((props.name === "vencord" || props.name === "equicord") && vencordOtherwiseLoaded()) { 105 | return "Vencord or Equicord are currently loaded by some other mechanism."; 106 | } 107 | if (props.name === "betterdiscord" && bdOtherwiseLoaded()) { 108 | return "BetterDiscord is currently loaded by some other mechanism."; 109 | } 110 | }; 111 | 112 | return html` 113 | <${SwitchItem} 114 | value=${() => props.value} 115 | onChange=${(e) => props.onChange?.(e)} 116 | note=${() => props.data.desc} 117 | disabled=${() => !!disabled()} 118 | tooltip=${() => disabled()} 119 | > 120 | ${() => props.data.name} 121 | 122 | `; 123 | } 124 | 125 | function SettingsView() { 126 | // a Set of branches 127 | const [pendingBranches, setPendingBranches] = createSignal(new Set(currentBranches())); 128 | 129 | // basically a set inequality test 130 | const unsavedChanges = () => { 131 | const a = pendingBranches(); 132 | const b = new Set(currentBranches()); 133 | return a.size !== b.size || !a.isSubsetOf(b); 134 | }; 135 | 136 | const isUninstalled = () => !currentBranches().length; 137 | 138 | // this is really silly 139 | return () => { 140 | if (isUninstalled()) { 141 | return html`<${UninstalledSplash} />`; 142 | } 143 | 144 | return html` 145 | <${Header} tag=${HeaderTags.H1} style="margin-bottom: 1rem">Client Mod Settings 146 | 147 | <${Text}> 148 | Your installation of ${() => prettyModNames(currentBranches())} is being managed by 149 | <${Space} /> 150 | <${LinkButton} href="https://github.com/uwu/sheltupdate">sheltupdate. 151 | You can change the mods you are loading below. 152 | 153 | 154 | <${Divider} mt mb /> 155 | 156 | 157 | ${() => 158 | Object.values(branchMetaGrouped()) 159 | .flatMap(Object.entries) 160 | .map( 161 | ([branchName, branchData]) => html` 162 | <${BranchEntry} 163 | name=${() => branchName} 164 | data=${() => branchData} 165 | value=${() => pendingBranches().has(branchName)} 166 | onChange=${async (e) => { 167 | const pb = pendingBranches(); 168 | if (e) { 169 | const foundIncompatibilities = branchData.incompatibilities.filter((i) => pb.has(i)); 170 | if (foundIncompatibilities.length > 0) { 171 | const res = await openIncompatibilityModal( 172 | branchData.displayName, 173 | foundIncompatibilities, 174 | ); 175 | if (res === "disableOthers") { 176 | foundIncompatibilities.forEach((b) => pb.delete(b)); 177 | } else if (res === "cancel") { 178 | return; 179 | } 180 | } 181 | pb.add(branchName); 182 | } else { 183 | pb.delete(branchName); 184 | } 185 | // reactivity ugh 186 | setPendingBranches(new Set(pb)); 187 | }} 188 | /> 189 | `, 190 | )} 191 | 192 |
193 | ${() => 194 | unsavedChanges() 195 | ? html` 196 | <${Text}> 197 | You have unsaved changes!
198 | Applying will require fully closing and reopening Discord. 199 | 200 |
201 | <${Button} grow onClick=${(e) => { 202 | SheltupdateNative.setBranches([...pendingBranches()]).then(updateCurrent, (err) => { 203 | updateCurrent(); 204 | showToast({ 205 | title: "Failed to change mods!", 206 | content: err?.message ?? err, 207 | duration: 3000, 208 | }); 209 | }); 210 | }} 211 | > 212 | Save Mods 213 | 214 | ` 215 | : html`
`} 216 | 217 | <${Button} 218 | grow 219 | color=${ButtonColors.RED} 220 | onClick=${(e) => { 221 | setUninstallCache(currentBranches()); 222 | SheltupdateNative.uninstall().then(updateCurrent, (err) => { 223 | updateCurrent(); 224 | showToast({ 225 | title: "Failed to change mods!", 226 | content: err?.message ?? err, 227 | duration: 3000, 228 | }); 229 | }); 230 | }} 231 | style=${{ "margin-left": "1rem" }} 232 | > 233 | Uninstall shelter 234 | 235 |
236 | 237 | <${InstanceName} /> 238 | `; 239 | }; 240 | } 241 | 242 | function UninstalledSplash() { 243 | return html` 244 |
245 | <${Header} tag=${HeaderTags.H1} style="margin-bottom: 1rem"> 246 | sheltupdate will be uninstalled at next Discord app restart 247 | 248 | <${Text}> 249 | Your plugins, themes, settings, etc. have not been deleted and will be remembered if 250 | you reinstall sheltupdate in the future, or switch to some other injection method. 251 | 252 | <${Text}> 253 | Change your mind? Until you restart Discord, you can retrieve your installation of 254 | <${Space} /> 255 | ${() => prettyModNames(uninstallCache())} 256 | <${Space} /> 257 | exactly as it was before. 258 | 259 | 260 | <${Button} 261 | grow 262 | color=${ButtonColors.GREEN} 263 | onClick=${(e) => 264 | SheltupdateNative.setBranches(uninstallCache()).then(updateCurrent, (err) => { 265 | updateCurrent(); 266 | showToast({ 267 | title: "Failed to change mods!", 268 | content: err?.message ?? err, 269 | duration: 3000, 270 | }); 271 | })} 272 | style=${{ "margin-top": "2rem" }} 273 | > 274 | Revert uninstall 275 | 276 |
277 | `; 278 | } 279 | 280 | function InstanceName() { 281 | return html` 282 | 283 | <${Text} tag=${TextTags.textSM}> 284 | sheltupdate instance: ${currentHost} 285 | 286 | <${Button} 287 | grow 288 | color=${ButtonColors.SECONDARY} 289 | size=${ButtonSizes.TINY} 290 | style=${{ display: "inline-block", "margin-left": ".5rem" }} 291 | onClick=${(e) => openHostChangeModal().then((v) => 292 | SheltupdateNative.setCurrentHost(v).then(updateCurrent, (err) => { 293 | updateCurrent(); 294 | showToast({ 295 | title: "Failed to change host!", 296 | content: err?.message ?? err, 297 | duration: 3000, 298 | }) 299 | }))} 300 | >Change 301 | 302 | `; 303 | } 304 | 305 | async function openIncompatibilityModal(branchName, incompatibleBranches) { 306 | return new Promise((resolve) => { 307 | openModal(({ close }) => { 308 | onCleanup(() => resolve("cancel")); 309 | const incompatibleBranchNames = prettyModNames(incompatibleBranches); 310 | return html` 311 | <${ModalRoot}> 312 | <${ModalHeader} close=${(e) => close()}> 313 | Things might break! 314 | 315 | <${ModalBody}> 316 | ${branchName} may not work properly alongside ${incompatibleBranchNames}.
317 | Do you want to disable ${incompatibleBranchNames}? 318 | 319 | <${ModalFooter}> 320 |
321 | <${Button} 322 | look=${ButtonLooks.LINK} 323 | size=${ButtonSizes.MEDIUM} 324 | grow 325 | onClick=${(e) => { 326 | resolve("cancel"); 327 | close(); 328 | }} 329 | > 330 | Cancel 331 | 332 | <${Button} 333 | color=${ButtonColors.GREEN} 334 | size=${ButtonSizes.MEDIUM} 335 | grow 336 | onClick=${(e) => { 337 | resolve("disableOthers"); 338 | close(); 339 | }} 340 | > 341 | Disable ${incompatibleBranches.length > 1 ? "them" : "it"} 342 | 343 | <${Button} 344 | color=${ButtonColors.RED} 345 | size=${ButtonSizes.MEDIUM} 346 | grow 347 | onClick=${(e) => { 348 | resolve("proceed"); 349 | close(); 350 | }} 351 | > 352 | Proceed anyways 353 | 354 |
355 | 356 | 357 | `; 358 | }); 359 | }); 360 | } 361 | 362 | async function openHostChangeModal() { 363 | return new Promise((res, rej) => 364 | openModal(({close}) => { 365 | onCleanup(rej); 366 | 367 | const [newHost, setNewHost] = createSignal(untrack(currentHost)); 368 | 369 | const validationIssue = createMemo(() => { 370 | // must be a URL 371 | let url; 372 | try { 373 | url = new URL(newHost()); 374 | } catch { 375 | return "Not a valid URL"; 376 | } 377 | 378 | // must not have a trailing path, else branch setting logic would break 379 | if (url.pathname !== "/") 380 | return "Hosts must not have a path"; 381 | 382 | // don't have a trailing / 383 | if (newHost().endsWith("/")) 384 | return "Hosts must not have a trailing /"; 385 | 386 | // openasar does not support http 387 | if (url.protocol !== "https:") { 388 | if (url.protocol === "http:") { 389 | if (window.openasar) 390 | return "OpenAsar does not work with insecure hosts" 391 | } 392 | else return "Hosts must be http: or https:"; 393 | } 394 | }); 395 | 396 | return html` 397 | <${ModalRoot} size=${ModalSizes.MEDIUM}> 398 | <${ModalHeader} close=${close}>Change sheltupdate instance 399 | <${ModalBody}> 400 |

<${Text}> 401 | You can host your own instance of sheltupdate, instead of using uwu.network's official instance, 402 | and switch to using it here. 403 |

404 |

<${Text}> 405 | We suggest using the official instance as it is always up-to-date, 406 | runs unmodified official sheltupdate code, and has high reliability. 407 | We run a 408 | <${Space} /> 409 | <${LinkButton} onClick=${(e) => setNewHost("https://inject.shelter.uwu.network")}>stable instance 410 | <${Space} /> 411 | and a 412 | <${Space} /> 413 | <${LinkButton} onClick=${(e) => setNewHost("https://staging.shelter.uwu.network")}>staging instance. 414 |

415 |

<${Text}> 416 | If someone has told you to change this, be sure you trust them, as the server that you specify here 417 | can deliver code to you that will be run when you open Discord, with full access to your computer. 418 |

419 | <${Divider} /> 420 | 421 | <${TextBox} 422 | value=${newHost} 423 | onInput=${(v) => setNewHost(v)} 424 | style=${() => validationIssue() ? {border: "1px solid var(--input-border-critical)"} : {}} 425 | /> 426 | <${Text} tag=${TextTags.textSM} style=${{color: "var(--text-critical)"}}> 427 | ${validationIssue} 428 | 429 | 430 | <${ModalConfirmFooter} close=${() => close} disabled=${validationIssue} onConfirm=${() => res(newHost())} /> 431 | 432 | `; 433 | }) 434 | ); 435 | } 436 | })(); 437 | --------------------------------------------------------------------------------