summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.github/CODEOWNERS3
-rw-r--r--.github/workflows/nix-build.yml18
-rw-r--r--.gitignore3
-rwxr-xr-x.husky/pre-commit26
-rw-r--r--assets/inline-plugins/.gitkeep0
-rw-r--r--assets/plugins/.gitkeep0
-rw-r--r--assets/preload-plugins/.gitkeep0
-rw-r--r--assets/preload-plugins/checkLocale.js38
-rw-r--r--assets/preload-plugins/oauth2.js9
-rw-r--r--assets/public/fosscord-login.css68
-rw-r--r--assets/public/fosscord.css92
-rw-r--r--assets/public/user.css1
-rw-r--r--assets/webrtc.js82
-rw-r--r--flake.lock61
-rw-r--r--flake.nix70
-rw-r--r--hashes.json3
-rwxr-xr-xnix-update.sh10
-rw-r--r--package-lock.json220
-rw-r--r--package.json2
-rw-r--r--src/cdn/routes/embed.ts20
-rw-r--r--src/util/connections/ConnectionLoader.ts2
21 files changed, 433 insertions, 295 deletions
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 00000000..b55d76cd
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,3 @@
+# Nix stuff is owned by Rory& - we& want to be notified if these are changed.
+/flake.nix root@rory.gay
+/nix-update.sh root@rory.gay
\ No newline at end of file
diff --git a/.github/workflows/nix-build.yml b/.github/workflows/nix-build.yml
new file mode 100644
index 00000000..ae9afa2e
--- /dev/null
+++ b/.github/workflows/nix-build.yml
@@ -0,0 +1,18 @@
+name: Nix build
+
+on:
+  push:
+  pull_request:
+
+jobs:
+  build-nix:
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v4
+    - uses: cachix/install-nix-action@v25
+      with:
+        nix_path: nixpkgs=channel:nixos-unstable
+    - uses: DeterminateSystems/magic-nix-cache-action@v2
+    - run: nix build -L
+    - run: nix develop --command echo OK
diff --git a/.gitignore b/.gitignore
index bc780d64..e62c03d6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,4 +18,5 @@ build
 *.log.ansi
 *.tmp
 tmp/
-dump/
\ No newline at end of file
+dump/
+result
diff --git a/.husky/pre-commit b/.husky/pre-commit
index 1bc5a8c6..702231b5 100755
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -1,4 +1,28 @@
 #!/usr/bin/env sh
+#!nix-shell -i "bash" -p bash prefetch-npm-deps jq nodejs nix-output-monitor
 . "$(dirname -- "$0")/_/husky.sh"
 
-npx -y lint-staged
\ No newline at end of file
+# Check if nix is available
+if [ -x "$(/usr/bin/env which nix-shell 2>/dev/null)" ]; then
+  # Check if we haven't re-executed ourselves yet
+  if [ ! "$HOOK_NIX_SHELL" ]; then
+    echo "Nix is available, updating nix flake..."
+    export HOOK_NIX_SHELL=1
+    nix-shell $0
+    exit $?
+  else
+    nix flake update
+    # run ./nix-update.sh if package lock has changed and has no unstaged changes
+    if [ -n "$(git status --porcelain=v1 2>/dev/null | grep -E '^(MM| M) package-lock.json')" ]; then
+      echo "package-lock.json has unstaged changes. Skipping update of nix dependencies."
+    elif [ ! -n "$(git status --porcelain=v1 2>/dev/null | grep -E '^M package-lock.json')" ]; then
+      echo "package-lock.json has no changes. Skipping update of nix dependencies."
+    else
+      ./nix-update.sh || exit $?
+    fi
+  fi
+else
+  echo "You do not appear to have nix installed. Skipping update of nix dependencies."
+fi
+
+npx -y lint-staged
diff --git a/assets/inline-plugins/.gitkeep b/assets/inline-plugins/.gitkeep
deleted file mode 100644
index e69de29b..00000000
--- a/assets/inline-plugins/.gitkeep
+++ /dev/null
diff --git a/assets/plugins/.gitkeep b/assets/plugins/.gitkeep
deleted file mode 100644
index e69de29b..00000000
--- a/assets/plugins/.gitkeep
+++ /dev/null
diff --git a/assets/preload-plugins/.gitkeep b/assets/preload-plugins/.gitkeep
deleted file mode 100644
index e69de29b..00000000
--- a/assets/preload-plugins/.gitkeep
+++ /dev/null
diff --git a/assets/preload-plugins/checkLocale.js b/assets/preload-plugins/checkLocale.js
deleted file mode 100644
index 1b5e7b71..00000000
--- a/assets/preload-plugins/checkLocale.js
+++ /dev/null
@@ -1,38 +0,0 @@
-const supportedLocales = [
-	"bg",
-	"cs",
-	"da",
-	"de",
-	"el",
-	"en-GB",
-	"es-ES",
-	"fi",
-	"fr",
-	"hi",
-	"hr",
-	"hu",
-	"it",
-	"ja",
-	"ko",
-	"lt",
-	"nl",
-	"no",
-	"pl",
-	"pt-BR",
-	"ro",
-	"ru",
-	"sv-SE",
-	"th",
-	"tr",
-	"uk",
-	"vi",
-	"zh-CN",
-	"zh-TW"
-];
-
-const settings = JSON.parse(window.localStorage.getItem("UserSettingsStore"));
-if (settings && !supportedLocales.includes(settings.locale)) {
-	// fix client locale wrong and client not loading at all
-	settings.locale = "en-US";
-	window.localStorage.setItem("UserSettingsStore", JSON.stringify(settings));
-}
\ No newline at end of file
diff --git a/assets/preload-plugins/oauth2.js b/assets/preload-plugins/oauth2.js
deleted file mode 100644
index 5b78ec83..00000000
--- a/assets/preload-plugins/oauth2.js
+++ /dev/null
@@ -1,9 +0,0 @@
-// Fixes /oauth2 endpoints not requesting a CSS file
-
-if (location.pathname.startsWith("/oauth2/")) {
-	const link = document.createElement("link");
-	link.rel = "stylesheet"
-	link.type = "text/css"
-	link.href = "/assets/40532.f7b1e10347ef10e790ac.css"
-	document.head.appendChild(link)
-}
\ No newline at end of file
diff --git a/assets/public/fosscord-login.css b/assets/public/fosscord-login.css
deleted file mode 100644
index e66e70a0..00000000
--- a/assets/public/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 Spacebar!";
-	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/spacebarchat/spacebarchat/master/branding/svg/Spacebar__Logo-Blue.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;
-}
\ No newline at end of file
diff --git a/assets/public/fosscord.css b/assets/public/fosscord.css
deleted file mode 100644
index 53ffd41b..00000000
--- a/assets/public/fosscord.css
+++ /dev/null
@@ -1,92 +0,0 @@
-/* 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/spacebarchat/spacebarchat/master/branding/svg/Spacebar__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;
-}
\ No newline at end of file
diff --git a/assets/public/user.css b/assets/public/user.css
deleted file mode 100644
index a7e5c4f3..00000000
--- a/assets/public/user.css
+++ /dev/null
@@ -1 +0,0 @@
-/* Your custom CSS goes here, enjoy! */
\ No newline at end of file
diff --git a/assets/webrtc.js b/assets/webrtc.js
deleted file mode 100644
index b56e41c4..00000000
--- a/assets/webrtc.js
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
-	This file is used to patch client version 134842 ( and probably a lot more ) to send additional info when using webrtc.
-	If you want to use it, throw it into the `preload-plugins` folder.
-	TODO: Make it so this file is not required for webrtc.
-	
-	Do note that webrtc, as of 17/12/2022, is not implemented yet in spacebarchat/server.
-*/
-
-(this.webpackChunkdiscord_app = this.webpackChunkdiscord_app || []).push([
-	[[228974]],
-	{
-		632540: (module, exports, req) => {
-			window.find = (filter, options = {}) => {
-				const { cacheOnly = false } = options;
-				for (let i in req.c) {
-					if (req.c.hasOwnProperty(i)) {
-						let m = req.c[i].exports;
-						if (m && m.__esModule && m.default && filter(m.default)) return m.default;
-						if (m && filter(m)) return m;
-					}
-				}
-				if (cacheOnly) {
-					console.warn("Cannot find loaded module in cache");
-					return null;
-				}
-				console.warn("Cannot find loaded module in cache. Loading all modules may have unexpected side effects");
-				for (let i = 0; i < req.m.length; ++i) {
-					let m = req(i);
-					if (m && m.__esModule && m.default && filter(m.default)) return m.default;
-					if (m && filter(m)) return m;
-				}
-				console.warn("Cannot find module");
-				return null;
-			};
-			window.findByUniqueProperties = (propNames, options) =>
-				find((module) => propNames.every((prop) => module[prop] !== undefined), options);
-			window.findByDisplayName = (displayName, options) => find((module) => module.displayName === displayName, options);
-			window.req = req;
-
-			init();
-		}
-	},
-	(t) => t(632540)
-]);
-
-function retry(callback) {
-	return new Promise((resolve) => {
-		const interval = setInterval(() => {
-			const mod = callback();
-			if (!mod) return;
-
-			clearInterval(interval);
-			resolve(mod);
-		}, 50);
-	});
-}
-
-async function init() {
-	const SDP = await retry(() => findByUniqueProperties(["truncateSDP"]));
-	const StringManipulator = findByUniqueProperties(["uniq"]);
-
-	const truncateSDP = SDP.truncateSDP;
-	SDP.truncateSDP = (e) => {
-		const result = truncateSDP(e);
-		const i = result.codecs.find((x) => x.name === "VP8");
-		const a = new RegExp("^a=ice|a=extmap|opus|VP8|fingerprint|" + i?.rtxPayloadType + " rtx", "i");
-		return {
-			sdp: StringManipulator(e)
-				.split(/\r\n/)
-				.filter(function (e) {
-					return a.test(e);
-				})
-				.uniq()
-				.join("\n"),
-			codecs: result.codecs
-		};
-	};
-	// SDP.generateUnifiedSessionDescription = (e) => {
-	// 	console.log(e);
-	// 	return new RTCSessionDescription({ sdp: e.baseSDP.replace(/sendonly/g, "recvonly"), type: "answer" });
-	// };
-}
\ No newline at end of file
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 00000000..ae5e8b23
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,61 @@
+{
+  "nodes": {
+    "flake-utils": {
+      "inputs": {
+        "systems": "systems"
+      },
+      "locked": {
+        "lastModified": 1705309234,
+        "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "type": "github"
+      }
+    },
+    "nixpkgs": {
+      "locked": {
+        "lastModified": 1708118438,
+        "narHash": "sha256-kk9/0nuVgA220FcqH/D2xaN6uGyHp/zoxPNUmPCMmEE=",
+        "owner": "nixos",
+        "repo": "nixpkgs",
+        "rev": "5863c27340ba4de8f83e7e3c023b9599c3cb3c80",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nixos",
+        "ref": "nixos-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "root": {
+      "inputs": {
+        "flake-utils": "flake-utils",
+        "nixpkgs": "nixpkgs"
+      }
+    },
+    "systems": {
+      "locked": {
+        "lastModified": 1681028828,
+        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+        "owner": "nix-systems",
+        "repo": "default",
+        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-systems",
+        "repo": "default",
+        "type": "github"
+      }
+    }
+  },
+  "root": "root",
+  "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 00000000..00a18f64
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,70 @@
+{
+  description = "Spacebar server, written in Typescript.";
+
+  inputs = {
+    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
+    flake-utils.url = "github:numtide/flake-utils";
+  };
+
+  outputs = { self, nixpkgs, flake-utils }:
+    flake-utils.lib.eachSystem flake-utils.lib.allSystems (system:
+      let
+        pkgs = import nixpkgs {
+          inherit system;
+        };
+        hashesFile = builtins.fromJSON (builtins.readFile ./hashes.json);
+      in rec {
+        packages.default = pkgs.buildNpmPackage {
+          pname = "spacebar-server-ts";
+          src = ./.;
+          name = "spacebar-server-ts";
+          nativeBuildInputs = with pkgs; [ python3 ];
+          npmDepsHash = hashesFile.npmDepsHash;
+          makeCacheWritable = true;
+          postPatch = ''
+            substituteInPlace package.json --replace 'npx patch-package' '${pkgs.nodePackages.patch-package}/bin/patch-package'
+          '';
+          installPhase = ''
+            runHook preInstall
+            set -x
+            #remove packages not needed for production, or at least try to...
+            npm prune --omit dev --no-save $npmInstallFlags "''${npmInstallFlagsArray[@]}" $npmFlags "''${npmFlagsArray[@]}"
+            find node_modules -maxdepth 1 -type d -empty -delete
+
+            mkdir -p $out/node_modules/
+            cp -r node_modules/* $out/node_modules/
+            cp -r dist/ $out/node_modules/@spacebar
+            for i in dist/**/start.js
+            do
+              makeWrapper ${pkgs.nodejs-slim}/bin/node $out/bin/start-`dirname ''${i/dist\//}` --prefix NODE_PATH : $out/node_modules --add-flags $out/node_modules/@spacebar`dirname ''${i/dist/}`/start.js
+            done
+            set +x
+            substituteInPlace package.json --replace 'dist/' 'node_modules/@spacebar/'
+            find $out/node_modules/@spacebar/ -type f -name "*.js" | while read srcFile; do
+              echo Patching imports in ''${srcFile/$out\/node_modules\/@spacebar//}...
+              substituteInPlace $srcFile --replace 'require("./' 'require(__dirname + "/'
+              substituteInPlace $srcFile --replace 'require("../' 'require(__dirname + "/../'
+              substituteInPlace $srcFile --replace ', "assets"' ', "..", "assets"'
+              #substituteInPlace $srcFile --replace 'require("@spacebar/' 'require("
+            done
+            set -x
+            cp -r assets/ $out/
+            cp package.json $out/
+            rm -v $out/assets/openapi.json
+            #rm -v $out/assets/schemas.json
+
+            #debug utils:
+            #cp $out/node_modules/@spacebar/ $out/build_output -r
+            set +x
+            runHook postInstall
+          '';
+        };
+        devShell = pkgs.mkShell {
+          buildInputs = with pkgs; [
+            nodejs
+            nodePackages.typescript
+          ];
+        };
+      }
+    );
+}
diff --git a/hashes.json b/hashes.json
new file mode 100644
index 00000000..dd55b81d
--- /dev/null
+++ b/hashes.json
@@ -0,0 +1,3 @@
+{
+	"npmDepsHash": "sha256-fZNDN2/fNy6Nu7tbr0RhQ8j4BP7X1Yhrh/fSTH7hbJc="
+}
diff --git a/nix-update.sh b/nix-update.sh
new file mode 100755
index 00000000..4413e6e0
--- /dev/null
+++ b/nix-update.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env nix-shell
+#!nix-shell -i "bash -x" -p bash prefetch-npm-deps jq git nix-output-monitor
+nix flake update
+DEPS_HASH=`prefetch-npm-deps package-lock.json`
+TMPFILE=$(mktemp)
+jq '.npm_deps_hash = "'$DEPS_HASH'"' hashes.json > $TMPFILE
+mv -- "$TMPFILE" hashes.json
+
+nom build .# || exit $?
+git add hashes.json flake.lock flake.nix
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index bc58336f..875aba1b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -38,10 +38,12 @@
 				"morgan": "^1.10.0",
 				"multer": "^1.4.5-lts.1",
 				"murmurhash-js": "^1.0.0",
+				"mysql": "*",
 				"node-2fa": "^2.0.3",
 				"node-fetch": "^2.6.12",
 				"node-os-utils": "^1.3.7",
 				"nodemailer": "^6.9.4",
+				"pg": "*",
 				"picocolors": "^1.0.0",
 				"probe-image-size": "^7.2.3",
 				"proxy-agent": "^6.3.0",
@@ -83,9 +85,11 @@
 			},
 			"optionalDependencies": {
 				"erlpack": "^0.1.4",
+				"mysql": "^2.18.1",
 				"nodemailer-mailgun-transport": "^2.1.5",
 				"nodemailer-mailjet-transport": "github:n0script22/nodemailer-mailjet-transport",
 				"nodemailer-sendgrid-transport": "github:Maria-Golomb/nodemailer-sendgrid-transport",
+				"pg": "^8.11.3",
 				"sqlite3": "^5.1.6"
 			}
 		},
@@ -2996,6 +3000,15 @@
 			"resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz",
 			"integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg=="
 		},
+		"node_modules/buffer-writer": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
+			"integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==",
+			"optional": true,
+			"engines": {
+				"node": ">=4"
+			}
+		},
 		"node_modules/busboy": {
 			"version": "1.6.0",
 			"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
@@ -5724,6 +5737,60 @@
 			"resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
 			"integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw=="
 		},
+		"node_modules/mysql": {
+			"version": "2.18.1",
+			"resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz",
+			"integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==",
+			"optional": true,
+			"dependencies": {
+				"bignumber.js": "9.0.0",
+				"readable-stream": "2.3.7",
+				"safe-buffer": "5.1.2",
+				"sqlstring": "2.3.1"
+			},
+			"engines": {
+				"node": ">= 0.6"
+			}
+		},
+		"node_modules/mysql/node_modules/bignumber.js": {
+			"version": "9.0.0",
+			"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz",
+			"integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==",
+			"optional": true,
+			"engines": {
+				"node": "*"
+			}
+		},
+		"node_modules/mysql/node_modules/isarray": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+			"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+			"optional": true
+		},
+		"node_modules/mysql/node_modules/readable-stream": {
+			"version": "2.3.7",
+			"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
+			"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
+			"optional": true,
+			"dependencies": {
+				"core-util-is": "~1.0.0",
+				"inherits": "~2.0.3",
+				"isarray": "~1.0.0",
+				"process-nextick-args": "~2.0.0",
+				"safe-buffer": "~5.1.1",
+				"string_decoder": "~1.1.1",
+				"util-deprecate": "~1.0.1"
+			}
+		},
+		"node_modules/mysql/node_modules/string_decoder": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+			"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+			"optional": true,
+			"dependencies": {
+				"safe-buffer": "~5.1.0"
+			}
+		},
 		"node_modules/mz": {
 			"version": "2.7.0",
 			"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
@@ -6276,6 +6343,12 @@
 				"node": ">= 14"
 			}
 		},
+		"node_modules/packet-reader": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
+			"integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==",
+			"optional": true
+		},
 		"node_modules/parent-module": {
 			"version": "1.0.1",
 			"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -6371,6 +6444,96 @@
 				"url": "https://github.com/sponsors/Borewit"
 			}
 		},
+		"node_modules/pg": {
+			"version": "8.11.3",
+			"resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz",
+			"integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==",
+			"optional": true,
+			"dependencies": {
+				"buffer-writer": "2.0.0",
+				"packet-reader": "1.0.0",
+				"pg-connection-string": "^2.6.2",
+				"pg-pool": "^3.6.1",
+				"pg-protocol": "^1.6.0",
+				"pg-types": "^2.1.0",
+				"pgpass": "1.x"
+			},
+			"engines": {
+				"node": ">= 8.0.0"
+			},
+			"optionalDependencies": {
+				"pg-cloudflare": "^1.1.1"
+			},
+			"peerDependencies": {
+				"pg-native": ">=3.0.1"
+			},
+			"peerDependenciesMeta": {
+				"pg-native": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/pg-cloudflare": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz",
+			"integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==",
+			"optional": true
+		},
+		"node_modules/pg-connection-string": {
+			"version": "2.6.2",
+			"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz",
+			"integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==",
+			"optional": true
+		},
+		"node_modules/pg-int8": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
+			"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
+			"optional": true,
+			"engines": {
+				"node": ">=4.0.0"
+			}
+		},
+		"node_modules/pg-pool": {
+			"version": "3.6.1",
+			"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz",
+			"integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==",
+			"optional": true,
+			"peerDependencies": {
+				"pg": ">=8.0"
+			}
+		},
+		"node_modules/pg-protocol": {
+			"version": "1.6.0",
+			"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz",
+			"integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==",
+			"optional": true
+		},
+		"node_modules/pg-types": {
+			"version": "2.2.0",
+			"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
+			"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
+			"optional": true,
+			"dependencies": {
+				"pg-int8": "1.0.1",
+				"postgres-array": "~2.0.0",
+				"postgres-bytea": "~1.0.0",
+				"postgres-date": "~1.0.4",
+				"postgres-interval": "^1.1.0"
+			},
+			"engines": {
+				"node": ">=4"
+			}
+		},
+		"node_modules/pgpass": {
+			"version": "1.0.5",
+			"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
+			"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
+			"optional": true,
+			"dependencies": {
+				"split2": "^4.1.0"
+			}
+		},
 		"node_modules/picocolors": {
 			"version": "1.0.0",
 			"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@@ -6403,6 +6566,45 @@
 				"node": ">=12.0.0"
 			}
 		},
+		"node_modules/postgres-array": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
+			"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
+			"optional": true,
+			"engines": {
+				"node": ">=4"
+			}
+		},
+		"node_modules/postgres-bytea": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
+			"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
+			"optional": true,
+			"engines": {
+				"node": ">=0.10.0"
+			}
+		},
+		"node_modules/postgres-date": {
+			"version": "1.0.7",
+			"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
+			"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+			"optional": true,
+			"engines": {
+				"node": ">=0.10.0"
+			}
+		},
+		"node_modules/postgres-interval": {
+			"version": "1.2.0",
+			"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
+			"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+			"optional": true,
+			"dependencies": {
+				"xtend": "^4.0.0"
+			},
+			"engines": {
+				"node": ">=0.10.0"
+			}
+		},
 		"node_modules/prelude-ls": {
 			"version": "1.2.1",
 			"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -7105,6 +7307,15 @@
 				"source-map": "^0.6.0"
 			}
 		},
+		"node_modules/split2": {
+			"version": "4.2.0",
+			"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+			"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+			"optional": true,
+			"engines": {
+				"node": ">= 10.x"
+			}
+		},
 		"node_modules/sqlite3": {
 			"version": "5.1.6",
 			"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.6.tgz",
@@ -7134,6 +7345,15 @@
 			"integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==",
 			"optional": true
 		},
+		"node_modules/sqlstring": {
+			"version": "2.3.1",
+			"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz",
+			"integrity": "sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ==",
+			"optional": true,
+			"engines": {
+				"node": ">= 0.6"
+			}
+		},
 		"node_modules/ssri": {
 			"version": "8.0.1",
 			"resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz",
diff --git a/package.json b/package.json
index a0db1d88..f506e0be 100644
--- a/package.json
+++ b/package.json
@@ -116,9 +116,11 @@
 	},
 	"optionalDependencies": {
 		"erlpack": "^0.1.4",
+		"mysql": "^2.18.1",
 		"nodemailer-mailgun-transport": "^2.1.5",
 		"nodemailer-mailjet-transport": "github:n0script22/nodemailer-mailjet-transport",
 		"nodemailer-sendgrid-transport": "github:Maria-Golomb/nodemailer-sendgrid-transport",
+		"pg": "^8.11.3",
 		"sqlite3": "^5.1.6"
 	}
 }
diff --git a/src/cdn/routes/embed.ts b/src/cdn/routes/embed.ts
index 9d3469cd..b99396eb 100644
--- a/src/cdn/routes/embed.ts
+++ b/src/cdn/routes/embed.ts
@@ -63,7 +63,15 @@ router.get("/avatars/:id", async (req: Request, res: Response) => {
 	id = id.split(".")[0]; // remove .file extension
 	const hash = defaultAvatarHashMap.get(id);
 	if (!hash) throw new HTTPError("not found", 404);
-	const path = join(process.cwd(), "assets", "public", `${hash}.png`);
+	const path = join(
+		__dirname,
+		"..",
+		"..",
+		"..",
+		"assets",
+		"public",
+		`${hash}.png`,
+	);
 
 	const file = await getFile(path);
 	if (!file) throw new HTTPError("not found", 404);
@@ -80,7 +88,15 @@ router.get("/group-avatars/:id", async (req: Request, res: Response) => {
 	id = id.split(".")[0]; // remove .file extension
 	const hash = defaultGroupDMAvatarHashMap.get(id);
 	if (!hash) throw new HTTPError("not found", 404);
-	const path = join(process.cwd(), "assets", "public", `${hash}.png`);
+	const path = join(
+		__dirname,
+		"..",
+		"..",
+		"..",
+		"assets",
+		"public",
+		`${hash}.png`,
+	);
 
 	const file = await getFile(path);
 	if (!file) throw new HTTPError("not found", 404);
diff --git a/src/util/connections/ConnectionLoader.ts b/src/util/connections/ConnectionLoader.ts
index e9dc6973..31b8e04b 100644
--- a/src/util/connections/ConnectionLoader.ts
+++ b/src/util/connections/ConnectionLoader.ts
@@ -22,7 +22,7 @@ import path from "path";
 import { ConnectionConfig } from "./ConnectionConfig";
 import { ConnectionStore } from "./ConnectionStore";
 
-const root = "dist/connections";
+const root = path.join(__dirname, "..", "..", "connections");
 const connectionsLoaded = false;
 
 export class ConnectionLoader {