diff --git a/src/Server.ts b/src/Server.ts
new file mode 100644
index 00000000..ca03e0d2
--- /dev/null
+++ b/src/Server.ts
@@ -0,0 +1,72 @@
+import express, { Application, Router } from "express";
+import { traverseDirectory } from "./Utils";
+import { Server as HTTPServer } from "http";
+import fetch from "node-fetch";
+import fs from "fs/promises";
+
+export type ServerOptions = {
+ port: number;
+};
+
+export class Server {
+ private app: Application;
+ private http: HTTPServer;
+ private options: ServerOptions;
+ private routes: Router[];
+ private initalized: Promise<any>;
+
+ constructor(opts: ServerOptions = { port: 8080 }) {
+ this.options = opts;
+
+ this.app = express();
+
+ this.initalized = this.init();
+ }
+
+ async init() {
+ // recursively loads files in routes/
+ this.routes = await this.registerRoutes(__dirname + "/routes/");
+ // const indexHTML = await (await fetch("https://discord.com/app")).buffer();
+ const indexHTML = await fs.readFile(__dirname + "/../client/index.html");
+
+ this.app.get("*", (req, res) => {
+ res.set("Cache-Control", "public, max-age=" + 60 * 60 * 24);
+ res.set("content-type", "text/html");
+ res.send(indexHTML);
+ });
+ }
+
+ async start() {
+ await this.initalized;
+ await new Promise<void>((res) => this.app.listen(this.options.port, () => res()));
+ console.log(`[Server] started on ${this.options.port}`);
+ }
+
+ async registerRoutes(root: string) {
+ return await traverseDirectory({ dirname: root, recursive: true }, this.registerRoute.bind(this, root));
+ }
+
+ registerRoute(root: string, file: string): any {
+ if (root.endsWith("/") || root.endsWith("\\")) root = root.slice(0, -1); // removes slash at the end of the root dir
+ let path = file.replace(root, ""); // remove root from path and
+ path = path.split(".").slice(0, -1).join("."); // trancate .js/.ts file extension of path
+ if (path.endsWith("/index")) path = path.slice(0, -6); // delete index from path
+
+ try {
+ var router = require(file);
+ if (router.router) router = router.router;
+ if (router.default) router = router.default;
+ if (!router || router.prototype.constructor.name !== "router")
+ throw `File doesn't export any default router`;
+ this.app.use(path, <Router>router);
+ console.log(`[Server] Route ${path} registerd`);
+ return router;
+ } catch (error) {
+ console.error(new Error(`[Server] Failed to register route ${file}: ${error}`));
+ }
+ }
+
+ async stop() {
+ return new Promise<void>((res) => this.http.close(() => res()));
+ }
+}
diff --git a/src/Utils.ts b/src/Utils.ts
new file mode 100644
index 00000000..09d8e8c6
--- /dev/null
+++ b/src/Utils.ts
@@ -0,0 +1,47 @@
+import fs from "fs/promises";
+
+declare global {
+ interface Array<T> {
+ flat(): T;
+ }
+}
+
+Array.prototype.flat = function () {
+ return this.reduce((acc, val) => (Array.isArray(val) ? acc.concat(val.flat()) : acc.concat(val)), []);
+};
+
+export interface traverseDirectoryOptions {
+ dirname: string;
+ filter?: RegExp;
+ excludeDirs?: RegExp;
+ recursive?: boolean;
+}
+
+const DEFAULT_EXCLUDE_DIR = /^\./;
+const DEFAULT_FILTER = /^([^\.].*)\.js$/;
+
+export async function traverseDirectory<T>(
+ options: traverseDirectoryOptions,
+ action: (path: string) => T
+): Promise<T[]> {
+ if (!options.filter) options.filter = DEFAULT_FILTER;
+ if (!options.excludeDirs) options.excludeDirs = DEFAULT_EXCLUDE_DIR;
+
+ const routes = await fs.readdir(options.dirname);
+ const promises = <Promise<T | T[] | undefined>[]>routes.map(async (file) => {
+ const path = options.dirname + file;
+ const stat = await fs.lstat(path);
+ if (path.match(<RegExp>options.excludeDirs)) return;
+
+ if (stat.isFile() && path.match(<RegExp>options.filter)) {
+ return action(path);
+ } else if (options.recursive && stat.isDirectory()) {
+ return traverseDirectory({ ...options, dirname: path + "/" }, action);
+ }
+ });
+ const result = await Promise.all(promises);
+
+ const t = <(T | undefined)[]>result.flat();
+
+ return <T[]>t.filter((x) => x != undefined);
+}
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 00000000..8765d6dc
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,4 @@
+import { Server } from "./Server";
+
+const server = new Server();
+server.start().catch(console.error);
diff --git a/src/routes/assets/index.ts b/src/routes/assets/index.ts
new file mode 100644
index 00000000..d3683f43
--- /dev/null
+++ b/src/routes/assets/index.ts
@@ -0,0 +1,30 @@
+import { Router } from "express";
+import fetch from "node-fetch";
+
+const router = Router();
+const cache = new Map();
+const assetEndpoint = "https://discord.com/assets/";
+
+export async function getCache(key: string): Promise<Response> {
+ let cachedRessource = cache.get(key);
+
+ if (!cachedRessource) {
+ const res = await fetch(assetEndpoint + key);
+ // @ts-ignore
+ res.bufferResponse = await res.buffer();
+ cache.set(key, res);
+ cachedRessource = res;
+ }
+
+ return cachedRessource;
+}
+
+router.get("/:hash", async (req, res) => {
+ res.set("Cache-Control", "public, max-age=" + 60 * 60 * 24);
+ const cache = await getCache(req.params.hash);
+ res.set("content-type", <string>cache.headers.get("content-type"));
+ // @ts-ignore
+ res.send(cache.bufferResponse);
+});
+
+export default router;
|