├── .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 | development 780 780
--------------------------------------------------------------------------------
/branches/tool/reactdevtools/ext/icons/development.svg:
--------------------------------------------------------------------------------
1 | development 780 780
--------------------------------------------------------------------------------
/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 |
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 |
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 |
50 | ${speakerSvg}
51 |
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 | 
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("