From 7bf5d628b6612aaf3650a3d1b3a62431d74a18e9 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Mon, 15 Apr 2024 21:46:49 -1000 Subject: [PATCH] refactor(frontend): :art: Make glitch-soc server prettier --- packages/glitch-server/main.ts | 564 ++++++++++++++++++--------------- 1 file changed, 306 insertions(+), 258 deletions(-) diff --git a/packages/glitch-server/main.ts b/packages/glitch-server/main.ts index 071d3ca8..3dab26aa 100644 --- a/packages/glitch-server/main.ts +++ b/packages/glitch-server/main.ts @@ -1,140 +1,117 @@ import { join } from "node:path"; import { redirect } from "@response"; import { config } from "config-manager"; -import { retrieveUserFromToken, userToAPI } from "~database/entities/User"; +import { + retrieveUserFromToken, + userToAPI, + type UserWithRelations, +} from "~database/entities/User"; import type { LogManager, MultiLogManager } from "~packages/log-manager"; import { languages } from "./glitch-languages"; +import type { BunFile } from "bun"; -export const handleGlitchRequest = async ( +const handleManifestRequest = async () => { + const manifest = { + id: "/home", + name: config.instance.name, + short_name: config.instance.name, + icons: [ + { + src: "/packs/media/icons/android-chrome-36x36-e67f2bc645cc669c04ffcbc17203aeac.png", + sizes: "36x36", + type: "image/png", + purpose: "any maskable", + }, + { + src: "/packs/media/icons/android-chrome-48x48-d3afc36e9388913fb6add2476a556f67.png", + sizes: "48x48", + type: "image/png", + purpose: "any maskable", + }, + { + src: "/packs/media/icons/android-chrome-72x72-23ee104da45dc5388d59b8b0fad866f2.png", + sizes: "72x72", + type: "image/png", + purpose: "any maskable", + }, + { + src: "/packs/media/icons/android-chrome-96x96-fb2abfd885ab5de94025e09f6f9408b5.png", + sizes: "96x96", + type: "image/png", + purpose: "any maskable", + }, + { + src: "/packs/media/icons/android-chrome-144x144-99b386f89a3a2a22440964eba3b9f242.png", + sizes: "144x144", + type: "image/png", + purpose: "any maskable", + }, + { + src: "/packs/media/icons/android-chrome-192x192-8b4d35fdd9b5fa4592056ce687c9d0ba.png", + sizes: "192x192", + type: "image/png", + purpose: "any maskable", + }, + { + src: "/packs/media/icons/android-chrome-256x256-fecf6504157e3b195dd0e604cd711730.png", + sizes: "256x256", + type: "image/png", + purpose: "any maskable", + }, + { + src: "/packs/media/icons/android-chrome-384x384-dc559d916be51de4965dd7b8abf9c7c8.png", + sizes: "384x384", + type: "image/png", + purpose: "any maskable", + }, + { + src: "/packs/media/icons/android-chrome-512x512-85515d059c83f47d8e77e0703ebb7ff5.png", + sizes: "512x512", + type: "image/png", + purpose: "any maskable", + }, + ], + theme_color: "#191b22", + background_color: "#191b22", + display: "standalone", + start_url: "/", + scope: "/", + share_target: { + url_template: "share?title={title}\u0026text={text}\u0026url={url}", + action: "share", + method: "GET", + enctype: "application/x-www-form-urlencoded", + params: { title: "title", text: "text", url: "url" }, + }, + shortcuts: [ + { name: "Compose new post", url: "/publish" }, + { name: "Notifications", url: "/notifications" }, + { name: "Explore", url: "/explore" }, + ], + }; + + return new Response(JSON.stringify(manifest), { + headers: { + "Content-Type": "application/json; charset=utf-8", + "Content-Length": String(JSON.stringify(manifest).length), + Date: new Date().toUTCString(), + }, + }); +}; + +const handleSignInRequest = async ( req: Request, - logger: LogManager | MultiLogManager, -): Promise => { - const url = new URL(req.url); - let path = url.pathname; - const accessToken = req.headers - .get("Cookie") - ?.match(/_session_id=(.*?)(;|$)/)?.[1]; - const user = await retrieveUserFromToken(accessToken ?? ""); + path: string, + url: URL, + user: UserWithRelations | null, + accessToken: string, +) => { + if (req.method === "POST") { + if (url.searchParams.get("error")) { + const fileContents = await Bun.file( + join(config.frontend.glitch.assets, "/auth/sign_in.html"), + ).text(); - // Strip leading /web from path - if (path.startsWith("/web")) path = path.slice(4); - - if (path === "/manifest") { - const manifest = { - id: "/home", - name: config.instance.name, - short_name: config.instance.name, - icons: [ - { - src: "/packs/media/icons/android-chrome-36x36-e67f2bc645cc669c04ffcbc17203aeac.png", - sizes: "36x36", - type: "image/png", - purpose: "any maskable", - }, - { - src: "/packs/media/icons/android-chrome-48x48-d3afc36e9388913fb6add2476a556f67.png", - sizes: "48x48", - type: "image/png", - purpose: "any maskable", - }, - { - src: "/packs/media/icons/android-chrome-72x72-23ee104da45dc5388d59b8b0fad866f2.png", - sizes: "72x72", - type: "image/png", - purpose: "any maskable", - }, - { - src: "/packs/media/icons/android-chrome-96x96-fb2abfd885ab5de94025e09f6f9408b5.png", - sizes: "96x96", - type: "image/png", - purpose: "any maskable", - }, - { - src: "/packs/media/icons/android-chrome-144x144-99b386f89a3a2a22440964eba3b9f242.png", - sizes: "144x144", - type: "image/png", - purpose: "any maskable", - }, - { - src: "/packs/media/icons/android-chrome-192x192-8b4d35fdd9b5fa4592056ce687c9d0ba.png", - sizes: "192x192", - type: "image/png", - purpose: "any maskable", - }, - { - src: "/packs/media/icons/android-chrome-256x256-fecf6504157e3b195dd0e604cd711730.png", - sizes: "256x256", - type: "image/png", - purpose: "any maskable", - }, - { - src: "/packs/media/icons/android-chrome-384x384-dc559d916be51de4965dd7b8abf9c7c8.png", - sizes: "384x384", - type: "image/png", - purpose: "any maskable", - }, - { - src: "/packs/media/icons/android-chrome-512x512-85515d059c83f47d8e77e0703ebb7ff5.png", - sizes: "512x512", - type: "image/png", - purpose: "any maskable", - }, - ], - theme_color: "#191b22", - background_color: "#191b22", - display: "standalone", - start_url: "/", - scope: "/", - share_target: { - url_template: - "share?title={title}\u0026text={text}\u0026url={url}", - action: "share", - method: "GET", - enctype: "application/x-www-form-urlencoded", - params: { title: "title", text: "text", url: "url" }, - }, - shortcuts: [ - { name: "Compose new post", url: "/publish" }, - { name: "Notifications", url: "/notifications" }, - { name: "Explore", url: "/explore" }, - ], - }; - - return new Response(JSON.stringify(manifest), { - headers: { - "Content-Type": "application/json; charset=utf-8", - "Content-Length": String(JSON.stringify(manifest).length), - Date: new Date().toUTCString(), - }, - }); - } - - if (path === "/auth/sign_in") { - if (req.method === "POST") { - return redirect("/api/auth/mastodon-login", 307); - } - path = "/auth/sign_in.html"; - } - - if (path === "/auth/sign_out") { - if (req.method === "POST") { - return redirect("/api/auth/mastodon-logout", 307); - } - } - - // Redirect / to /index.html - if (path === "/" || path === "") path = "/index.html"; - // If path doesn't have an extension (e.g. /about), serve index.html - // Also check if Accept header contains text/html - if (!path.includes(".") && req.headers.get("Accept")?.includes("text/html")) - path = "/index.html"; - - const file = Bun.file(join(config.frontend.glitch.assets, path)); - - if (await file.exists()) { - let fileContents = await file.text(); - - if (path === "/auth/sign_in.html" && url.searchParams.get("error")) { // Insert error message as first child of form.form_container const rewriter = new HTMLRewriter() .on("div.form-container", { @@ -153,140 +130,211 @@ export const handleGlitchRequest = async ( }) .transform(new Response(fileContents)); - fileContents = await rewriter.text(); - } - for (const server of config.frontend.glitch.server) { - fileContents = fileContents.replaceAll( - `${new URL(server).origin}/`, - "/", - ); - fileContents = fileContents.replaceAll( - new URL(server).host, - new URL(config.http.base_url).host, + return returnFile( + Bun.file( + join(config.frontend.glitch.assets, "/auth/sign_in.html"), + ), + await indexTransforms(await rewriter.text(), accessToken, user), ); } + return redirect("/api/auth/mastodon-login", 307); + } - fileContents = fileContents.replaceAll( - "Glitch-soc is free open source software forked from Mastodon.", - "Lysand is free and open-source software using the Glitch-Soc frontend.", + const file = Bun.file( + join(config.frontend.glitch.assets, "/auth/sign_in.html"), + ); + + return returnFile( + file, + await indexTransforms(await file.text(), accessToken, user), + ); +}; + +const handleSignOutRequest = async (req: Request) => { + if (req.method === "POST") { + return redirect("/api/auth/mastodon-logout", 307); + } + + return redirect("/", 307); +}; + +const returnFile = async (file: BunFile, content?: string) => { + return new Response(content ?? (await file.text()), { + headers: { + "Content-Type": `${file.type}; charset=utf-8`, + "Content-Length": String(file.size), + Date: new Date().toUTCString(), + }, + }); +}; + +const handleDefaultRequest = async ( + req: Request, + path: string, + user: UserWithRelations | null, + accessToken: string, +) => { + const file = Bun.file(join(config.frontend.glitch.assets, path)); + + if (await file.exists()) { + return returnFile( + file, + await indexTransforms(await file.text(), accessToken, user), ); - fileContents = fileContents.replaceAll("Mastodon", "Lysand"); - fileContents = fileContents.replaceAll( - "Lysand is free, open-source software, and a trademark of Mastodon gGmbH.", - "This is not a Mastodon instance.", - ); - fileContents = fileContents.replaceAll( - "joinmastodon.org", - "lysand.org", - ); - - // Strip integrity attributes from script and link tags - const rewriter = new HTMLRewriter() - .on("script", { - element(element) { - element.removeAttribute("integrity"); - }, - }) - .on("link", { - element(element) { - element.removeAttribute("integrity"); - }, - }) - .transform(new Response(fileContents)); - - fileContents = await rewriter.text(); - - // Check if file is index - if (path === "/index.html") { - // Find script id="initial-state" and replace its contents with custom json - const rewriter = new HTMLRewriter() - .on("script#initial-state", { - element(element) { - element.setInnerContent( - JSON.stringify({ - meta: { - access_token: accessToken || null, - activity_api_enabled: true, - admin: null, - domain: new URL(config.http.base_url).host, - limited_federation_mode: false, - locale: "en", - mascot: "https://media.tech.lgbt/site_uploads/files/000/000/004/original/1a16a73feb5c2463.png", - profile_directory: true, - registrations_open: true, - repository: "lysand-org/lysand", - search_enabled: true, - single_user_mode: false, - source_url: - "https://github.com/lysand-org/lysand", - sso_redirect: null, - status_page_url: null, - streaming_api_base_url: `wss://${ - new URL(config.http.base_url).host - }`, - timeline_preview: true, - title: config.instance.name, - trends_as_landing_page: false, - trends_enabled: true, - version: "4.3.0-alpha.3+glitch", - auto_play_gif: null, - display_media: null, - reduce_motion: null, - use_blurhash: null, - me: user ? user.id : undefined, - }, - compose: user - ? { - text: "", - me: user.id, - default_privacy: "public", - default_sensitive: false, - default_language: "en", - } - : { - text: "", - }, - accounts: user - ? { - [user.id]: userToAPI(user, true), - } - : {}, - media_attachments: { - accept_content_types: - config.validation.allowed_mime_types, - }, - settings: {}, - max_feed_hashtags: 4, - poll_limits: { - max_options: - config.validation.max_poll_options, - max_option_chars: - config.validation.max_poll_option_size, - min_expiration: - config.validation.min_poll_duration, - max_expiration: - config.validation.max_poll_duration, - }, - languages: languages, - push_subscription: null, - role: null, - }), - ); - }, - }) - .transform(new Response(fileContents)); - - fileContents = await rewriter.text(); - } - - return new Response(fileContents, { - headers: { - "Content-Type": `${file.type}; charset=utf-8`, - "Content-Length": String(file.size), - Date: new Date().toUTCString(), - }, - }); } return null; }; + +const indexTransforms = async ( + fileContents: string, + accessToken: string, + user: UserWithRelations | null, +) => { + let newFileContents = fileContents; + // Find script id="initial-state" and replace its contents with custom json + const rewriter = new HTMLRewriter() + .on("script#initial-state", { + element(element) { + element.setInnerContent( + JSON.stringify({ + meta: { + access_token: accessToken || null, + activity_api_enabled: true, + admin: null, + domain: new URL(config.http.base_url).host, + limited_federation_mode: false, + locale: "en", + mascot: "https://media.tech.lgbt/site_uploads/files/000/000/004/original/1a16a73feb5c2463.png", + profile_directory: true, + registrations_open: true, + repository: "lysand-org/lysand", + search_enabled: true, + single_user_mode: false, + source_url: "https://github.com/lysand-org/lysand", + sso_redirect: null, + status_page_url: null, + streaming_api_base_url: `wss://${ + new URL(config.http.base_url).host + }`, + timeline_preview: true, + title: config.instance.name, + trends_as_landing_page: false, + trends_enabled: true, + version: "4.3.0-alpha.3+glitch", + auto_play_gif: null, + display_media: null, + reduce_motion: null, + use_blurhash: null, + me: user ? user.id : undefined, + }, + compose: user + ? { + text: "", + me: user.id, + default_privacy: "public", + default_sensitive: false, + default_language: "en", + } + : { + text: "", + }, + accounts: user + ? { + [user.id]: userToAPI(user, true), + } + : {}, + media_attachments: { + accept_content_types: + config.validation.allowed_mime_types, + }, + settings: {}, + max_feed_hashtags: 4, + poll_limits: { + max_options: config.validation.max_poll_options, + max_option_chars: + config.validation.max_poll_option_size, + min_expiration: config.validation.min_poll_duration, + max_expiration: config.validation.max_poll_duration, + }, + languages: languages, + push_subscription: null, + role: null, + }), + ); + }, + }) + .on("script", { + element(element) { + element.removeAttribute("integrity"); + }, + }) + .on("link", { + element(element) { + element.removeAttribute("integrity"); + }, + }) + .transform(new Response(newFileContents)); + + for (const server of config.frontend.glitch.server) { + newFileContents = newFileContents.replaceAll( + `${new URL(server).origin}/`, + "/", + ); + newFileContents = newFileContents.replaceAll( + new URL(server).host, + new URL(config.http.base_url).host, + ); + } + + newFileContents = newFileContents.replaceAll( + "Glitch-soc is free open source software forked from Mastodon.", + "Lysand is free and open-source software using the Glitch-Soc frontend.", + ); + newFileContents = newFileContents.replaceAll("Mastodon", "Lysand"); + newFileContents = newFileContents.replaceAll( + "Lysand is free, open-source software, and a trademark of Mastodon gGmbH.", + "This is not a Mastodon instance.", + ); + newFileContents = newFileContents.replaceAll( + "joinmastodon.org", + "lysand.org", + ); + + return rewriter.text(); +}; + +export const handleGlitchRequest = async ( + req: Request, + logger: LogManager | MultiLogManager, +): Promise => { + const url = new URL(req.url); + let path = url.pathname; + const accessToken = + req.headers.get("Cookie")?.match(/_session_id=(.*?)(;|$)/)?.[1] ?? ""; + const user = await retrieveUserFromToken(accessToken ?? ""); + + // Strip leading /web from path + if (path.startsWith("/web")) path = path.slice(4); + + if (path === "/manifest") { + return handleManifestRequest(); + } + + if (path === "/auth/sign_in") { + return handleSignInRequest(req, path, url, user, accessToken); + } + + if (path === "/auth/sign_out") { + return handleSignOutRequest(req); + } + + // Redirect / to /index.html + if (path === "/" || path === "") path = "/index.html"; + // If path doesn't have an extension (e.g. /about), serve index.html + // Also check if Accept header contains text/html + if (!path.includes(".") && req.headers.get("Accept")?.includes("text/html")) + path = "/index.html"; + + return handleDefaultRequest(req, path, user, accessToken); +};