1
0
mirror of https://github.com/spacebarchat/server.git synced 2024-11-22 02:12:40 +01:00

Basic client patching system

This commit is contained in:
TheArcaneBrony 2022-08-26 04:14:54 +02:00
parent 58111e9df4
commit 09b5e9c081
No known key found for this signature in database
GPG Key ID: 32FC5AAADAD75A22
15 changed files with 304 additions and 167 deletions

2
.gitignore vendored
View File

@ -32,3 +32,5 @@ yarn.lock
dbconf.json
migrations.db
assets/cache_src/

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;

View File

View File

@ -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

View File

@ -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"
}
]
}

View File

@ -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}`);
}
}
});

View File

@ -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}`);
}
}
}
});

75
scripts/patches/prepWS.js Normal file
View File

@ -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}!`);

View File

@ -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}`);
}
}
});

View File

@ -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;

View File

@ -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) => {

View File

@ -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;
}

View File

@ -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";