summary refs log tree commit diff
diff options
context:
space:
mode:
authorTheArcaneBrony <myrainbowdash949@gmail.com>2022-08-26 04:14:54 +0200
committerTheArcaneBrony <myrainbowdash949@gmail.com>2022-09-04 15:15:25 +0200
commit09b5e9c0815f2e9861cacbfd3a15fb79a60cf39b (patch)
treebeacffc3c6da6e39302d3a5b03eb8a012f08cf8c
parentRevert "Merge pull request #873 from fosscord/dev/Maddy/fix/listeningAfterDb" (diff)
downloadserver-09b5e9c0815f2e9861cacbfd3a15fb79a60cf39b.tar.xz
Basic client patching system
-rw-r--r--.gitignore2
-rw-r--r--assets/fosscord-login.css68
-rw-r--r--assets/fosscord.css94
-rw-r--r--assets/index.html8
-rw-r--r--assets/private/icons/custom/.gitkeep0
-rw-r--r--assets/private/icons/homeIcon.path1
-rw-r--r--fosscord-server.code-workspace8
-rw-r--r--scripts/patches/applyPatches.js24
-rw-r--r--scripts/patches/mkPatches.js40
-rw-r--r--scripts/patches/prepWS.js75
-rw-r--r--scripts/patches/resetWS.js24
-rw-r--r--src/api/Server.ts3
-rw-r--r--src/api/middlewares/TestClient.ts15
-rw-r--r--src/api/util/TestClientPatcher.ts107
-rw-r--r--src/api/util/index.ts2
15 files changed, 304 insertions, 167 deletions
diff --git a/.gitignore b/.gitignore
index a582a2f3..8631a69d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,3 +32,5 @@ yarn.lock
 dbconf.json
 
 migrations.db
+
+assets/cache_src/
diff --git a/assets/fosscord-login.css b/assets/fosscord-login.css
deleted file mode 100644
index 975bf908..00000000
--- a/assets/fosscord-login.css
+++ /dev/null
@@ -1,68 +0,0 @@
-/* replace tos acceptance popup */
-#app-mount > div:nth-child(7) > div > div > div.tooltipContent-bqVLWK {
-	visibility: hidden;
-}
-#app-mount > div:nth-child(7) > div > div > div.tooltipContent-bqVLWK::after {
-	visibility: visible;
-	display: block;
-	content: "You need to agree to this instance's rules to continue";
-	margin-top: -32px;
-}
-/* replace login header */
-#app-mount > div.app-1q1i1E > div > div > div > div > form > div > div > div.mainLoginContainer-1ddwnR > h3 {
-	visibility: hidden;
-}
-h3.title-jXR8lp.marginBottom8-AtZOdT.base-1x0h_U.size24-RIRrxO::after {
-	margin-top: -32px;
-	content: "Welcome to Fosscord!";
-	visibility: visible;
-	display: block;
-}
-
-/* Logo in top left when bg removed */
-#app-mount > div.app-1q1i1E > div > a {
-	/* replace me: original dimensions: 130x36 */
-	background: url(https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Fosscord-Wordmark-Gradient.svg);
-	width: 130px;
-	height: 23px;
-	background-size: contain;
-}
-
-/* replace TOS text */
-
-#app-mount
-	> div.app-1q1i1E
-	> div
-	> div
-	> div
-	> form
-	> div
-	> div
-	> div.flex-1xMQg5.flex-1O1GKY.horizontal-1ae9ci.horizontal-2EEEnY.flex-1O1GKY.directionRow-3v3tfG.justifyStart-2NDFzi.alignCenter-1dQNNs.noWrap-3jynv6.marginTop20-3TxNs6
-	> label
-	> div.label-cywgfr.labelClickable-11AuB8.labelForward-1wfipV
-	> * {
-	visibility: hidden;
-}
-
-#app-mount
-	> div.app-1q1i1E
-	> div
-	> div
-	> div
-	> form
-	> div
-	> div
-	> div.flex-1xMQg5.flex-1O1GKY.horizontal-1ae9ci.horizontal-2EEEnY.flex-1O1GKY.directionRow-3v3tfG.justifyStart-2NDFzi.alignCenter-1dQNNs.noWrap-3jynv6.marginTop20-3TxNs6
-	> label
-	> div.label-cywgfr.labelClickable-11AuB8.labelForward-1wfipV::after {
-	visibility: visible;
-	content: "I have read and agree with the rules set by this instance.";
-	display: block;
-	margin-top: -16px;
-}
-
-/* shrink login box to same size as register */
-.authBoxExpanded-2jqaBe {
-	width: 480px !important;
-}
diff --git a/assets/fosscord.css b/assets/fosscord.css
index fa503d39..ba54b1cf 100644
--- a/assets/fosscord.css
+++ b/assets/fosscord.css
@@ -1,92 +1,4 @@
 /* loading spinner */
-#app-mount > div.app-1q1i1E > div.container-16j22k.fixClipping-3qAKRb > div.content-1-zrf2 > video {
-	filter: opacity(1);
-	background: url("http://www.clipartbest.com/cliparts/7ca/6Rr/7ca6RrLAi.gif");
-	background-size: contain;
-	/* width: 64px;
-    height: 64px; */
-	padding-bottom: 64px;
-	background-repeat: no-repeat;
-}
-
-/* home button icon */
-#app-mount
-	> div.app-1q1i1E
-	> div
-	> div.layers-3iHuyZ.layers-3q14ss
-	> div
-	> div
-	> nav
-	> ul
-	> div.scroller-1Bvpku.none-2Eo-qx.scrollerBase-289Jih
-	> div.tutorialContainer-2sGCg9
-	> div
-	> div.listItemWrapper-KhRmzM
-	> div
-	> svg
-	> foreignObject
-	> div
-	> div {
-	background-image: url(https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Fosscord-Icon-Rounded-Subtract.svg);
-	background-size: contain;
-	border-radius: 50%;
-}
-
-#app-mount
-	> div.app-1q1i1E
-	> div
-	> div.layers-3iHuyZ.layers-3q14ss
-	> div
-	> div
-	> nav
-	> ul
-	> div.scroller-1Bvpku.none-2Eo-qx.scrollerBase-289Jih
-	> div.tutorialContainer-2sGCg9
-	> div
-	> div.listItemWrapper-KhRmzM
-	> div
-	> svg
-	> foreignObject
-	> div
-	> div,
-#app-mount
-	> div.app-1q1i1E
-	> div
-	> div.layers-3iHuyZ.layers-3q14ss
-	> div
-	> div
-	> nav
-	> ul
-	> div.scroller-1Bvpku.none-2Eo-qx.scrollerBase-289Jih
-	> div.tutorialContainer-2sGCg9
-	> div
-	> div.listItemWrapper-KhRmzM
-	> div
-	> svg
-	> foreignObject
-	> div
-	> div:hover {
-	background-color: white;
-}
-/* Login QR */
-#app-mount > div.app-1q1i1E > div > div > div > div > form > div > div > div.transitionGroup-aR7y1d.qrLogin-1AOZMt,
-#app-mount > div.app-1q1i1E > div > div > div > div > form > div > div > div.verticalSeparator-3huAjp,
-/* Remove login bg */
-#app-mount > div.app-1q1i1E > div > svg,
-/* Download bar */
-#app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > div > div.notice-3bPHh-.colorDefault-22HBa0,
-/* Connection problem links */
-#app-mount > div.app-1q1i1E > div.container-16j22k.fixClipping-3qAKRb > div.problems-3mgf6w.slideIn-sCvzGz > div:nth-child(2),
-/* Downloads button */
-#app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > nav > ul > div.scroller-1Bvpku.none-2Eo-qx.scrollerBase-289Jih > div:nth-child(7) > div.listItemWrapper-KhRmzM > div > svg > foreignObject > div,
-#app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > nav > ul > div.scroller-1Bvpku.none-2Eo-qx.scrollerBase-289Jih > div:nth-child(6) > div,
-/* help button */
-#app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > div > div.content-98HsJk > div.chat-3bRxxu > section > div.toolbar-1t6TWx > a,
-/* download button start of guild */
-#chat-messages-899316648933185083 > div > div > div:nth-child(5),
-/* Thread permissions etc popups */
-#app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > div > div.content-98HsJk > div.sidebar-2K8pFh.hasNotice-1XRy4h > nav > div.container-3O_wAf,
-/* home button icon */
-#app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > nav > ul > div.scroller-1Bvpku.none-2Eo-qx.scrollerBase-289Jih > div.tutorialContainer-2sGCg9 > div > div.listItemWrapper-KhRmzM > div > svg > foreignObject > div > div > svg {
-	display: none;
-}
+:root {
+    --brand-hue: 22;
+}
\ No newline at end of file
diff --git a/assets/index.html b/assets/index.html
index 4513d0d2..0cd90c8e 100644
--- a/assets/index.html
+++ b/assets/index.html
@@ -5,7 +5,6 @@
 		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
 		<title>Discord Test Client</title>
 		<link rel="stylesheet" href="/assets/fosscord.css" />
-		<link id="logincss" rel="stylesheet" href="/assets/fosscord-login.css" />
 		<link id="customcss" rel="stylesheet" href="/assets/user.css" />
 		<!-- inline plugin marker -->
 		<!-- preload plugin marker -->
@@ -28,20 +27,21 @@
 				INVITE_HOST: `${location.hostname}/invite`,
 				GUILD_TEMPLATE_HOST: "${location.host}",
 				GIFT_CODE_HOST: "${location.hostname}",
-				RELEASE_CHANNEL: "stable",
 				MARKETING_ENDPOINT: "//discord.com",
 				BRAINTREE_KEY: "production_5st77rrc_49pp2rp4phym7387",
 				STRIPE_KEY: "pk_live_CUQtlpQUF0vufWpnpUmQvcdi",
 				NETWORKING_ENDPOINT: "//router.discordapp.net",
 				RTC_LATENCY_ENDPOINT: "//${location.hostname}/rtc",
 				ACTIVITY_APPLICATION_HOST: "discordsays.com",
-				PROJECT_ENV: "production",
 				REMOTE_AUTH_ENDPOINT: "//localhost:3020",
 				SENTRY_TAGS: { buildId: "75e36d9", buildType: "normal" },
 				MIGRATION_SOURCE_ORIGIN: "https://${location.hostname}",
 				MIGRATION_DESTINATION_ORIGIN: "https://${location.hostname}",
 				HTML_TIMESTAMP: Date.now(),
-				ALGOLIA_KEY: "aca0d7082e4e63af5ba5917d5e96bed0"
+				ALGOLIA_KEY: "aca0d7082e4e63af5ba5917d5e96bed0",
+				SENTRY_TAGS: { instance: document.location.host },
+				PROJECT_ENV: "development",
+				RELEASE_CHANNEL: "staging",
 			};
 			GLOBAL_ENV.MEDIA_PROXY_ENDPOINT = location.protocol + "//" + GLOBAL_ENV.CDN_HOST;
 			const localStorage = window.localStorage;
diff --git a/assets/private/icons/custom/.gitkeep b/assets/private/icons/custom/.gitkeep
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/assets/private/icons/custom/.gitkeep
diff --git a/assets/private/icons/homeIcon.path b/assets/private/icons/homeIcon.path
new file mode 100644
index 00000000..b4b062ca
--- /dev/null
+++ b/assets/private/icons/homeIcon.path
@@ -0,0 +1 @@
+M 0,0 47.999993,2.7036528e-4 C 48.001796,3.3028172 47.663993,6.5968018 46.991821,9.8301938 43.116101,28.454191 28.452575,43.116441 9.8293509,46.992163 6.5960834,47.664163 3.3023222,48.001868 0,47.999992 Z m 9.8293509,28.735114 v 9.248482 C 22.673599,33.047696 32.857154,22.749268 37.63852,9.829938 H 9.8293509 v 8.679899 H 22.931288 c -3.554489,3.93617 -7.735383,7.257633 -12.373436,9.829938 -0.241031,0.133684 -0.483864,0.265492 -0.7285011,0.395339 z
\ No newline at end of file
diff --git a/fosscord-server.code-workspace b/fosscord-server.code-workspace
index 56450f85..a9b15856 100644
--- a/fosscord-server.code-workspace
+++ b/fosscord-server.code-workspace
@@ -16,6 +16,8 @@
 	"settings": {
 		"files.exclude": {
 			"*.ansi": true,
+			"**/cache": true,
+			"**/cache_src": true
 		}
 	},
 	"launch": {
@@ -32,6 +34,12 @@
 				"name": "Run Fosscord with debugger (kitty)",
 				"request": "launch",
 				"type": "node-terminal"
+			},
+			{
+				"command": "[ \"$(basename $PWD)\" != \"fosscord-server\" ] && cd ..; $(ps -o comm= $PPID) assets/cache",
+				"name": "Open testclient patch workspace",
+				"request": "launch",
+				"type": "node-terminal"
 			}
 		]
 	}
diff --git a/scripts/patches/applyPatches.js b/scripts/patches/applyPatches.js
new file mode 100644
index 00000000..66524ede
--- /dev/null
+++ b/scripts/patches/applyPatches.js
@@ -0,0 +1,24 @@
+const { execSync } = require("child_process");
+const path = require("path");
+const fs = require("fs");
+const { argv, stdout, exit } = require("process");
+const { execIn, parts, getDirs, walk, sanitizeVarName } = require("../utils");
+
+//apply patches
+const patchDir = path.join(__dirname, "..", "..", "assets", "testclient_patches");
+const targetDir = path.join(__dirname, "..", "..", "assets", "cache");
+const files = fs.readdirSync(patchDir);
+files.forEach((file) => {
+    const filePath = path.join(patchDir, file);
+    const stats = fs.statSync(filePath);
+    if (stats.isFile()) {
+        const ext = path.extname(file);
+        if (ext === ".patch") {
+            execSync(`git apply ${filePath}`, {
+                cwd: targetDir,
+                maxBuffer: 1024 * 1024 * 10,
+            });
+            console.log(`Applied patch ${file} to ${newFilePath}`);
+        }
+    }
+});
\ No newline at end of file
diff --git a/scripts/patches/mkPatches.js b/scripts/patches/mkPatches.js
new file mode 100644
index 00000000..1e1e0475
--- /dev/null
+++ b/scripts/patches/mkPatches.js
@@ -0,0 +1,40 @@
+const { execSync } = require("child_process");
+const path = require("path");
+const fs = require("fs");
+const { argv, stdout, exit } = require("process");
+const { execIn, parts, getDirs, walk, sanitizeVarName } = require("../utils");
+
+//generate git patch for each file in assets/cache
+const srcDir = path.join(__dirname, "..", "..", "assets", "cache");
+const destDir = path.join(__dirname, "..", "..", "assets", "cache_src");
+const patchDir = path.join(__dirname, "..", "..", "assets", "testclient_patches");
+if(!fs.existsSync(patchDir)) fs.mkdirSync(patchDir);
+const files = fs.readdirSync(srcDir);
+files.forEach((file) => {
+    const filePath = path.join(srcDir, file);
+    const stats = fs.statSync(filePath);
+    if (stats.isFile()) {
+        const ext = path.extname(file);
+        if (ext === ".js" || ext === ".css") {
+            const newFilePath = path.join(destDir, file);
+            //check if file has been modified
+            let patch; 
+            try {
+                let es = execSync(`diff -du --speed-large-files --horizon-lines=0 ${newFilePath} ${filePath}`, {
+                    maxBuffer: 1024 * 1024 * 10,
+                }).toString();
+                patch="";
+            } catch (e) {
+                patch = e.stdout.toString().replaceAll(path.join(destDir, file), file).replaceAll(path.join(srcDir, file), file);
+            }
+            if (patch.length > 0) {
+                //generate patch;
+                fs.writeFileSync(path.join(patchDir, file + ".patch"), patch);
+                console.log(`Generated patch for ${file}: ${patch.length} bytes, ${patch.split("\n").length} lines, ${patch.split("\n").filter((x) => x.startsWith("+")).length} additions, ${patch.split("\n").filter((x) => x.startsWith("-")).length} deletions`);
+            }
+            else {
+                //console.log(`No changes for ${file}`);
+            }
+        }
+    }
+});
\ No newline at end of file
diff --git a/scripts/patches/prepWS.js b/scripts/patches/prepWS.js
new file mode 100644
index 00000000..bc4d0a9a
--- /dev/null
+++ b/scripts/patches/prepWS.js
@@ -0,0 +1,75 @@
+const { execSync } = require("child_process");
+const path = require("path");
+const fs = require("fs");
+const { argv, stdout, exit } = require("process");
+const { execIn, parts, getDirs, walk, sanitizeVarName } = require("../utils");
+
+//copy all js and css files from assets/cache to assets/dist
+const srcDir = path.join(__dirname, "..", "..", "assets", "cache");
+const destDir = path.join(__dirname, "..", "..", "assets", "cache_src");
+if(!fs.existsSync(destDir)) fs.mkdirSync(destDir);
+const files = fs.readdirSync(srcDir);
+files.forEach((file) => {
+    const filePath = path.join(srcDir, file);
+    const stats = fs.statSync(filePath);
+    if (stats.isFile()) {
+        const ext = path.extname(file);
+        if (ext === ".js" || ext === ".css") {
+            const newFilePath = path.join(destDir, file);
+            if(!fs.existsSync(newFilePath)) {
+                fs.copyFileSync(filePath, newFilePath);
+                console.log(`Copied ${file} to ${newFilePath}`);
+            }
+        }
+    }
+});
+if(!fs.existsSync(path.join(srcDir, ".vscode"))) fs.mkdirSync(path.join(srcDir, ".vscode"));
+fs.writeFileSync(path.join(srcDir, ".vscode", "settings.json"), JSON.stringify({
+    "codemetrics.basics.DecorationModeEnabled": false,
+    "codemetrics.basics.CodeLensEnabled": false,
+    "editor.codeLens": false,
+    //"editor.minimap.enabled": false,
+    "codemetrics.basics.MetricsForArrowFunctionsToggled": false,
+    "codemetrics.basics.MetricsForClassDeclarationsToggled": false,
+    "codemetrics.basics.MetricsForConstructorsToggled": false,
+    "codemetrics.basics.MetricsForEnumDeclarationsToggled": false,
+    "codemetrics.basics.MetricsForFunctionExpressionsToggled": false,
+    "codemetrics.basics.MetricsForFunctionDeclarationsToggled": false,
+    "codemetrics.basics.MetricsForMethodDeclarationsToggled": false,
+    "codemetrics.basics.OverviewRulerModeEnabled": false,
+    "editor.fontFamily": "'JetBrainsMono Nerd Font', 'JetBrainsMono', 'Droid Sans Mono', 'monospace', monospace",
+    "editor.accessibilityPageSize": 1,
+    "editor.accessibilitySupport": "off",
+    "editor.autoClosingDelete": "never",
+    //"editor.autoIndent": "none",
+    //"editor.colorDecorators": false,
+    "editor.comments.ignoreEmptyLines": false,
+    "editor.copyWithSyntaxHighlighting": false,
+    "editor.comments.insertSpace": false,
+    "editor.detectIndentation": false,
+    "editor.dragAndDrop": false,
+    "editor.dropIntoEditor.enabled": false,
+    "editor.experimental.pasteActions.enabled": false,
+    "editor.guides.highlightActiveIndentation": false,
+    "color-highlight.enable": false,
+    "gitlens.blame.highlight.locations": [
+        "gutter"
+    ],
+    "todohighlight.isEnable": false,
+    "todohighlight.maxFilesForSearch": 1,
+    "editor.maxTokenizationLineLength": 1200,
+    "editor.minimap.maxColumn": 140,
+    "explorer.openEditors.visible": 0,
+    "editor.fontLigatures": false,
+    "files.exclude": {
+        "*.mp3": true,
+        "*.png": true,
+        "*.svg": true,
+        "*.webm": true,
+        "*.webp": true,
+        "*.woff2": true,
+        "**/.vscode/": true
+    },
+    "editor.guides.bracketPairs": true
+}, null, 4));
+console.log(`Workspace prepared at ${srcDir}!`);
\ No newline at end of file
diff --git a/scripts/patches/resetWS.js b/scripts/patches/resetWS.js
new file mode 100644
index 00000000..d34b74fd
--- /dev/null
+++ b/scripts/patches/resetWS.js
@@ -0,0 +1,24 @@
+const { execSync } = require("child_process");
+const path = require("path");
+const fs = require("fs");
+const { argv, stdout, exit } = require("process");
+const { execIn, parts, getDirs, walk, sanitizeVarName } = require("../utils");
+
+//copy all js and css files from assets/cache_src to assets/cache
+const srcDir = path.join(__dirname, "..", "..", "assets", "cache_src");
+const destDir = path.join(__dirname, "..", "..", "assets", "cache");
+if(!fs.existsSync(destDir)) fs.mkdirSync(destDir);
+const files = fs.readdirSync(srcDir);
+files.forEach((file) => {
+    const filePath = path.join(srcDir, file);
+    const stats = fs.statSync(filePath);
+    if (stats.isFile()) {
+        const ext = path.extname(file);
+        if (ext === ".js" || ext === ".css") {
+            const newFilePath = path.join(destDir, file);
+            fs.rmSync(newFilePath);
+            fs.copyFileSync(filePath, newFilePath);
+            console.log(`Copied ${file} to ${newFilePath}`);
+        }
+    }
+});
\ No newline at end of file
diff --git a/src/api/Server.ts b/src/api/Server.ts
index e92335a5..560014d0 100644
--- a/src/api/Server.ts
+++ b/src/api/Server.ts
@@ -11,6 +11,7 @@ import { initRateLimits } from "./middlewares/RateLimit";
 import TestClient from "./middlewares/TestClient";
 import { initTranslation } from "./middlewares/Translation";
 import { initInstance } from "./util/handlers/Instance";
+import fs from "fs";
 
 export interface FosscordServerOptions extends ServerOptions {}
 
@@ -42,6 +43,8 @@ export class FosscordServer extends Server {
 			this.app.use(
 				morgan("combined", {
 					skip: (req, res) => {
+						if(req.path.endsWith(".map")) return true;
+						if(req.path.includes("/assets/") && !fs.existsSync(path.join(__dirname, "..", "..", "..", "assets", req.path.split("/")[0].split('?')[0]))) return true;
 						let skip = !(process.env["LOG_REQUESTS"]?.includes(res.statusCode.toString()) ?? false);
 						if (process.env["LOG_REQUESTS"]?.charAt(0) == "-") skip = !skip;
 						return skip;
diff --git a/src/api/middlewares/TestClient.ts b/src/api/middlewares/TestClient.ts
index 3afd0339..9090840e 100644
--- a/src/api/middlewares/TestClient.ts
+++ b/src/api/middlewares/TestClient.ts
@@ -6,7 +6,9 @@ import path from "path";
 import { green } from "picocolors";
 import ProxyAgent from "proxy-agent";
 import { AssetCacheItem } from "../util/entities/AssetCacheItem";
+import { patchFile } from "..";
 
+const prettier = require("prettier");
 const AssetsPath = path.join(__dirname, "..", "..", "..", "assets");
 
 export default function TestClient(app: Application) {
@@ -39,7 +41,7 @@ export default function TestClient(app: Application) {
 		let response: FetchResponse;
 		let buffer: Buffer;
 		let assetCacheItem: AssetCacheItem = new AssetCacheItem(req.params.file);
-		if (newAssetCache.has(req.params.file)) {
+		if (newAssetCache.has(req.params.file) && fs.existsSync(newAssetCache.get(req.params.file)!.FilePath)) {
 			assetCacheItem = newAssetCache.get(req.params.file)!;
 			assetCacheItem.Headers.forEach((value: any, name: any) => {
 				res.set(name, value);
@@ -56,16 +58,21 @@ export default function TestClient(app: Application) {
 					...req.headers
 				}
 			});
-
 			//set cache info
 			assetCacheItem.Headers = Object.fromEntries(stripHeaders(response.headers));
-			assetCacheItem.FilePath = path.join(assetCacheDir, req.params.file);
 			assetCacheItem.Key = req.params.file;
 			//add to cache and save
 			newAssetCache.set(req.params.file, assetCacheItem);
+
+			if(response.status != 200) {
+				return res.status(404).send("Not found");
+			}
+			assetCacheItem.FilePath = path.join(assetCacheDir, req.params.file);
+			if(!fs.existsSync(assetCacheDir))
+				fs.mkdirSync(assetCacheDir);
 			fs.writeFileSync(path.join(assetCacheDir, "index.json"), JSON.stringify(Object.fromEntries(newAssetCache), null, 4));
 			//download file
-			fs.writeFileSync(assetCacheItem.FilePath, await response.buffer());
+			fs.writeFileSync(assetCacheItem.FilePath, /.*\.(js|css)/.test(req.params.file) ? patchFile(assetCacheItem.FilePath, (await response.buffer()).toString()) : await response.buffer());
 		}
 
 		assetCacheItem.Headers.forEach((value: string, name: string) => {
diff --git a/src/api/util/TestClientPatcher.ts b/src/api/util/TestClientPatcher.ts
new file mode 100644
index 00000000..2e9bfafe
--- /dev/null
+++ b/src/api/util/TestClientPatcher.ts
@@ -0,0 +1,107 @@
+import path from "path";
+import fs from "fs";
+
+console.log('[TestClient] Loading private assets...');
+
+const privateAssetsRoot = path.join(__dirname, "..", "..", "..", "assets", "private");
+const iconsRoot = path.join(privateAssetsRoot, "icons");
+const icons = new Map<string, Buffer>();
+
+fs.readdirSync(iconsRoot).forEach(file => {
+    const fileName = path.basename(file);
+    //check if dir
+    if(fs.lstatSync(path.join(iconsRoot, file)).isDirectory()){
+        return;
+    }
+    icons.set(fileName,fs.readFileSync(path.join(iconsRoot,file)) as Buffer);
+});
+
+fs.readdirSync(path.join(iconsRoot, "custom")).forEach(file => {
+    const fileName = path.basename(file);
+    if(fs.lstatSync(path.join(iconsRoot,"custom", file)).isDirectory()){
+        return;
+    }
+    icons.set(fileName,fs.readFileSync(path.join(iconsRoot,"custom",file)) as Buffer);
+});
+
+console.log('[TestClient] Patcher ready!');
+
+export function patchFile(filePath: string, content: string): string {
+    console.log(`[TestClient] Patching ${filePath}`);
+    let startTime = Date.now();
+    
+    content = prettier(filePath, content);
+    content = autoPatch(filePath, content);
+
+    console.log(`[TestClient] Patched ${filePath} in ${Date.now() - startTime}ms`);
+    return content;
+}
+function prettier(filePath: string, content: string): string{
+    let prettier = require("prettier");
+    let parser;
+    filePath = filePath.toLowerCase().split('?')[0];
+    if(filePath.endsWith(".js")) {
+        parser = "babel";
+    } else if (filePath.endsWith(".ts")){
+        parser = "typescript";
+    } else if(filePath.endsWith(".css")){
+        parser = "css";
+    } else if(filePath.endsWith(".json")){
+        parser = "json";
+    }
+    else {
+        console.log(`[TestClient] Skipping prettier for ${filePath}, unknown file type!`);
+        return content;
+    }
+    content = prettier.format(content, {
+        tabWidth: 4,
+        useTabs: true,
+        printWidth: 140,
+        trailingComma: "none",
+        parser
+    });
+    console.log(`[TestClient] Prettified ${filePath}!`);
+    return content;
+}
+
+function autoPatch(filePath: string, content: string): string{
+    //remove nitro references
+    content = content.replace(/Discord Nitro/g, "Fosscord Premium");
+    content = content.replace(/"Nitro"/g, "\"Premium\"");
+    content = content.replace(/Nitro /g, "Premium ");
+    content = content.replace(/ Nitro/g, " Premium");
+    content = content.replace(/\[Nitro\]/g, "[Premium]");
+    content = content.replace(/\*Nitro\*/g, "*Premium*");
+    content = content.replace(/\"Nitro \. /g, "\"Premium. ");
+
+    //remove discord references
+    content = content.replace(/ Discord /g, " Fosscord ");
+    content = content.replace(/Discord /g, "Fosscord ");
+    content = content.replace(/ Discord/g, " Fosscord");
+    content = content.replace(/Discord Premium/g, "Fosscord Premium");
+    content = content.replace(/Discord Nitro/g, "Fosscord Premium");
+    content = content.replace(/Discord's/g, "Fosscord's");
+    //content = content.replace(/DiscordTag/g, "FosscordTag");
+    content = content.replace(/\*Discord\*/g, "*Fosscord*");
+
+    //change some vars
+    content = content.replace('dsn: "https://fa97a90475514c03a42f80cd36d147c4@sentry.io/140984"', "dsn: (/true/.test(localStorage.sentryOptIn)?'https://6bad92b0175d41a18a037a73d0cff282@sentry.thearcanebrony.net/12':'')");
+    content = content.replace('t.DSN = "https://fa97a90475514c03a42f80cd36d147c4@sentry.io/140984"', "t.DSN = (/true/.test(localStorage.sentryOptIn)?'https://6bad92b0175d41a18a037a73d0cff282@sentry.thearcanebrony.net/12':'')");
+    content = content.replace('--brand-experiment: hsl(235, calc(var(--saturation-factor, 1) * 85.6%), 64.7%);', '--brand-experiment: hsl(var(--brand-hue), calc(var(--saturation-factor, 1) * 85.6%), 50%);');
+    content = content.replaceAll(/--brand-experiment-(\d{1,4}): hsl\(235/g, '--brand-experiment-\$1: hsl(var(--brand-hue)')
+
+    //logos
+    content = content.replace(/d: "M23\.0212.*/, `d: "${icons.get("homeIcon.path")!.toString()}"`);
+    content = content.replace('width: n, height: o, viewBox: "0 0 28 20"', 'width: 48, height: 48, viewBox: "0 0 48 48"');
+
+    //undo webpacking
+    // - booleans
+    content = content.replace(/!0/g, "true");
+    content = content.replace(/!1/g, "false");
+    // - real esmodule defs
+    content = content.replace(/Object.defineProperty\((.), "__esModule", { value: (.*) }\);/g, '\$1.__esModule = \$2;');
+    
+
+    console.log(`[TestClient] Autopatched ${path.basename(filePath)}!`);
+    return content;
+}
\ No newline at end of file
diff --git a/src/api/util/index.ts b/src/api/util/index.ts
index d06860cd..f01c2f43 100644
--- a/src/api/util/index.ts
+++ b/src/api/util/index.ts
@@ -8,3 +8,5 @@ export * from "./utility/ipAddress";
 export * from "./utility/passwordStrength";
 export * from "./utility/RandomInviteID";
 export * from "./utility/String";
+export * from "./utility/captcha";
+export * from "./TestClientPatcher";
\ No newline at end of file