/* Spacebar: A FOSS re-implementation and extension of the Discord.com backend. Copyright (C) 2023 Spacebar and Spacebar Contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import { Config, ConnectionConfig, ConnectionLoader, Email, JSONReplacer, WebAuthn, initDatabase, initEvent, registerRoutes } from "@spacebar/util"; import { Authentication, CORS, ImageProxy, BodyParser, ErrorHandler, initRateLimits, initTranslation } from "./middlewares"; import { Request, Response, Router } from "express"; import { Server, ServerOptions } from "lambert-server"; import morgan from "morgan"; import path from "path"; import express from "express"; import { red } from "picocolors"; import { initInstance } from "./util/handlers/Instance"; import { configurePush } from "../util/util/WebPush"; const ASSETS_FOLDER = path.join(__dirname, "..", "..", "assets"); const PUBLIC_ASSETS_FOLDER = path.join(ASSETS_FOLDER, "public"); export type SpacebarServerOptions = ServerOptions; declare global { // eslint-disable-next-line @typescript-eslint/no-namespace namespace Express { interface Request { server: SpacebarServer; } } } export class SpacebarServer extends Server { declare public options: SpacebarServerOptions; constructor(opts?: Partial) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore super({ ...opts, errorHandler: false, jsonBody: false }); } async start() { await initDatabase(); await Config.init(); await initEvent(); await Email.init(); await ConnectionConfig.init(); await initInstance(); WebAuthn.init(); configurePush(); const logRequests = process.env["LOG_REQUESTS"] != undefined; if (logRequests) { this.app.use( morgan("combined", { skip: (req, res) => { let skip = !(process.env["LOG_REQUESTS"]?.includes(res.statusCode.toString()) ?? false); if (process.env["LOG_REQUESTS"]?.charAt(0) == "-") skip = !skip; return skip; }, }), ); } this.app.set("json replacer", JSONReplacer); this.app.disable("x-powered-by"); const trustedProxies = Config.get().security.trustedProxies; if (trustedProxies) this.app.set("trust proxy", trustedProxies); this.app.use(CORS); this.app.use(BodyParser({ inflate: true, limit: "10mb" })); const app = this.app; const api = Router({ mergeParams: true }); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore this.app = api; api.use(Authentication); await initRateLimits(api); await initTranslation(api); this.routes = await registerRoutes(this, path.join(__dirname, "routes", "/")); this.app = app; //app.use("/__development", ) //app.use("/__internals", ) app.use("/api/v6", api); app.use("/api/v7", api); app.use("/api/v8", api); app.use("/api/v9", api); app.use("/api", api); // allow unversioned requests app.use("/imageproxy/:hash/:size/:url", ImageProxy); // for the discord frontend, can be replaced with anything else, just respect the way this file is structured app.use("/assets", express.static(path.join(PUBLIC_ASSETS_FOLDER, "client"))); app.get("/verify-email", (req, res) => res.sendFile(path.join(PUBLIC_ASSETS_FOLDER, "verify.html"))); app.get("/_spacebar/api/schemas.json", (req, res) => { res.sendFile(path.join(ASSETS_FOLDER, "schemas.json")); }); app.get("/_spacebar/api/openapi.json", (req, res) => { res.sendFile(path.join(ASSETS_FOLDER, "openapi.json")); }); // current well-known location (new commit 22 nov 2025 from spacebar) app.get("/.well-known/spacebar", (req, res) => { res.json({ api: Config.get().api.endpointPublic, }); }); // new well-known location app.get("/.well-known/spacebar/client", (req, res) => { let erlpackSupported = false; try { require("@yukikaze-bot/erlpack"); erlpackSupported = true; } catch (e) { // empty } res.json({ api: { baseUrl: Config.get().api.endpointPublic?.split("/api")[0] || "", // TODO: migrate database values to not include /api/v9 apiVersions: { default: Config.get().api.defaultVersion, active: Config.get().api.activeVersions, }, }, cdn: { baseUrl: Config.get().cdn.endpointPublic, }, gateway: { baseUrl: Config.get().gateway.endpointPublic, encoding: [...(erlpackSupported ? ["etf"] : []), "json"], compression: ["zstd-stream", "zlib-stream", null], }, }); }); app.use("*_", (req, res) => res.sendFile(path.join(PUBLIC_ASSETS_FOLDER, "index.html"))); this.app.use(ErrorHandler); ConnectionLoader.loadConnections(); if (logRequests) console.log(red(`Warning: Request logging is enabled! This will spam your console!\nTo disable this, unset the 'LOG_REQUESTS' environment variable!`)); return super.start(); } }