From 1eaec4c3e8c1c2c5a80ffdf608218dd76e07d4eb Mon Sep 17 00:00:00 2001 From: Husky <39809509+Huskydog9988@users.noreply.github.com> Date: Tue, 13 Jan 2026 22:49:58 -0500 Subject: [PATCH] Switch to nuxt assets for emojis (#311) * switch to nuxt assets for emojis * add auth to emoji endpoint * fix cache control header * fix type error --- components/EmojiText.vue | 2 +- nuxt.config.ts | 54 +++++-------- package.json | 1 - pnpm-lock.yaml | 81 -------------------- server/api/v1/emoji/[codepoint]/index.get.ts | 39 ++++++++++ server/internal/acls/descriptions.ts | 2 + server/internal/acls/index.ts | 4 +- server/internal/tasks/index.ts | 3 +- 8 files changed, 67 insertions(+), 119 deletions(-) create mode 100644 server/api/v1/emoji/[codepoint]/index.get.ts diff --git a/components/EmojiText.vue b/components/EmojiText.vue index 694f68a..570d937 100644 --- a/components/EmojiText.vue +++ b/components/EmojiText.vue @@ -10,6 +10,6 @@ const props = defineProps<{ }>(); const url = computed(() => { - return `/twemoji/${twemoji.convert.toCodePoint(props.emoji)}.svg`; + return `/api/v1/emoji/${twemoji.convert.toCodePoint(props.emoji)}`; }); diff --git a/nuxt.config.ts b/nuxt.config.ts index 6404f4f..2ebb756 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -1,9 +1,8 @@ import tailwindcss from "@tailwindcss/vite"; import { execSync } from "node:child_process"; -import { cpSync, readFileSync, existsSync } from "node:fs"; +import { readFileSync, existsSync } from "node:fs"; import path from "node:path"; -import { findPackageJSON } from "node:module"; -import { viteStaticCopy } from "vite-plugin-static-copy"; +import module from "module"; import { type } from "arktype"; const packageJsonSchema = type({ @@ -11,6 +10,14 @@ const packageJsonSchema = type({ version: "string", }); +const twemojiJson = module.findPackageJSON( + "@discordapp/twemoji", + import.meta.url, +); +if (!twemojiJson) { + throw new Error("Could not find @discordapp/twemoji package."); +} + // get drop version const dropVersion = getDropVersion(); @@ -56,7 +63,7 @@ export default defineNuxtConfig({ experimental: { buildCache: true, - viewTransition: false, + viewTransition: true, componentIslands: true, }, @@ -68,39 +75,9 @@ export default defineNuxtConfig({ plugins: [ // eslint-disable-next-line @typescript-eslint/no-explicit-any tailwindcss() as any, - // only used in dev server, not build because nitro sucks - // see build hook below - viteStaticCopy({ - targets: [ - { - src: "node_modules/@discordapp/twemoji/dist/svg/*", - dest: "twemoji", - }, - ], - // eslint-disable-next-line @typescript-eslint/no-explicit-any - }) as any, ], }, - hooks: { - "nitro:build:public-assets": (nitro) => { - const twemojiJson = findPackageJSON( - "@discordapp/twemoji", - import.meta.url, - ); - if (!twemojiJson) { - throw new Error("Could not find @discordapp/twemoji package."); - } - // this is only run during build, not dev server - // https://github.com/nuxt/nuxt/issues/18918#issuecomment-1925774964 - // copy emojis to .output/public/twemoji - const targetDir = path.join(nitro.options.output.publicDir, "twemoji"); - cpSync(path.join(path.dirname(twemojiJson), "dist", "svg"), targetDir, { - recursive: true, - }); - }, - }, - runtimeConfig: { gitRef: commitHash, dropVersion: dropVersion, @@ -139,6 +116,7 @@ export default defineNuxtConfig({ scheduledTasks: { "0 * * * *": ["dailyTasks"], + "*/30 * * * *": ["downloadCleanup"], }, storage: { @@ -154,6 +132,14 @@ export default defineNuxtConfig({ base: "./.data/appCache", }, }, + + serverAssets: [ + { + baseName: "twemoji", + // get path to twemoji svg assets + dir: path.join(path.dirname(twemojiJson), "dist", "svg"), + }, + ], }, typescript: { diff --git a/package.json b/package.json index aa08541..13c2f92 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,6 @@ "stream-mime-type": "^2.0.0", "turndown": "^7.2.0", "unstorage": "^1.15.0", - "vite-plugin-static-copy": "^3.1.2", "vue": "latest", "vue-router": "latest", "vue3-carousel": "^0.16.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 14f5a64..c5aefc2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -128,9 +128,6 @@ importers: unstorage: specifier: ^1.15.0 version: 1.16.1(@netlify/blobs@9.1.2)(db0@0.3.4)(ioredis@5.8.2) - vite-plugin-static-copy: - specifier: ^3.1.2 - version: 3.1.2(vite@7.2.2(@types/node@22.16.5)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.1)) vue: specifier: latest version: 3.5.26(typescript@5.8.3) @@ -3058,10 +3055,6 @@ packages: resolution: {integrity: sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==} hasBin: true - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} - bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} @@ -3203,10 +3196,6 @@ packages: resolution: {integrity: sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==} engines: {node: '>=20.18.1'} - chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} - chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -4094,10 +4083,6 @@ packages: fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} - fs-extra@11.3.0: - resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} - engines: {node: '>=14.14'} - fs-extra@8.1.0: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} engines: {node: '>=6 <7 || >=8'} @@ -4344,10 +4329,6 @@ packages: is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - is-builtin-module@3.2.1: resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} engines: {node: '>=6'} @@ -4541,9 +4522,6 @@ packages: jsonfile@5.0.0: resolution: {integrity: sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==} - jsonfile@6.1.0: - resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} - junk@4.0.1: resolution: {integrity: sha512-Qush0uP+G8ZScpGMZvHUiRfI0YBWuB3gVBYlI0v0vvOJt5FLicco+IkP0a50LqTTQhmts/m6tP5SWE+USyIvcQ==} engines: {node: '>=12.20'} @@ -5702,10 +5680,6 @@ packages: readdir-glob@1.1.3: resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -6341,10 +6315,6 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} - universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} - unixify@1.0.0: resolution: {integrity: sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg==} engines: {node: '>=0.10.0'} @@ -6626,12 +6596,6 @@ packages: '@nuxt/kit': optional: true - vite-plugin-static-copy@3.1.2: - resolution: {integrity: sha512-aVmYOzptLVOI2b1jL+cmkF7O6uhRv1u5fvOkQgbohWZp2CbR22kn9ZqkCUIt9umKF7UhdbsEpshn1rf4720QFg==} - engines: {node: ^18.0.0 || >=20.0.0} - peerDependencies: - vite: ^5.0.0 || ^6.0.0 || ^7.0.0 - vite-plugin-vue-tracer@1.1.3: resolution: {integrity: sha512-fM7hfHELZvbPnSn8EKZwHfzxm5EfYFQIclz8rwcNXfodNbRkwNvh0AGMtaBfMxQ9HC5KVa3KitwHnmE4ezDemw==} peerDependencies: @@ -10047,8 +10011,6 @@ snapshots: bcryptjs@3.0.2: {} - binary-extensions@2.3.0: {} - bindings@1.5.0: dependencies: file-uri-to-path: 1.0.0 @@ -10248,18 +10210,6 @@ snapshots: undici: 7.13.0 whatwg-mimetype: 4.0.0 - chokidar@3.6.0: - dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -11214,12 +11164,6 @@ snapshots: fs-constants@1.0.0: optional: true - fs-extra@11.3.0: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.1.0 - universalify: 2.0.1 - fs-extra@8.1.0: dependencies: graceful-fs: 4.2.11 @@ -11543,10 +11487,6 @@ snapshots: is-arrayish@0.3.2: {} - is-binary-path@2.1.0: - dependencies: - binary-extensions: 2.3.0 - is-builtin-module@3.2.1: dependencies: builtin-modules: 3.3.0 @@ -11696,12 +11636,6 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 - jsonfile@6.1.0: - dependencies: - universalify: 2.0.1 - optionalDependencies: - graceful-fs: 4.2.11 - junk@4.0.1: {} jwt-decode@4.0.0: {} @@ -13262,10 +13196,6 @@ snapshots: dependencies: minimatch: 5.1.6 - readdirp@3.6.0: - dependencies: - picomatch: 2.3.1 - readdirp@4.1.2: {} real-require@0.2.0: {} @@ -13987,8 +13917,6 @@ snapshots: universalify@0.1.2: {} - universalify@2.0.1: {} - unixify@1.0.0: dependencies: normalize-path: 2.1.1 @@ -14281,15 +14209,6 @@ snapshots: transitivePeerDependencies: - supports-color - vite-plugin-static-copy@3.1.2(vite@7.2.2(@types/node@22.16.5)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.1)): - dependencies: - chokidar: 3.6.0 - fs-extra: 11.3.0 - p-map: 7.0.3 - picocolors: 1.1.1 - tinyglobby: 0.2.14 - vite: 7.2.2(@types/node@22.16.5)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.1) - vite-plugin-vue-tracer@1.1.3(vite@7.2.2(@types/node@22.16.5)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.26(typescript@5.8.3)): dependencies: estree-walker: 3.0.3 diff --git a/server/api/v1/emoji/[codepoint]/index.get.ts b/server/api/v1/emoji/[codepoint]/index.get.ts new file mode 100644 index 0000000..1a20479 --- /dev/null +++ b/server/api/v1/emoji/[codepoint]/index.get.ts @@ -0,0 +1,39 @@ +import aclManager from "~/server/internal/acls"; + +export default defineEventHandler(async (h3) => { + const allowed = await aclManager.hasACL(h3, [ + "system:setup", + "user:emoji:read", + ]); + if (!allowed) + throw createError({ + statusCode: 403, + statusMessage: "Requires authentication", + }); + + const codepoint = getRouterParam(h3, "codepoint"); + if (!codepoint) { + throw createError({ + statusCode: 400, + statusMessage: "Missing codepoint parameter", + }); + } + + // Get the emoji SVG from server assets + const asset = await useStorage("assets:twemoji").getItemRaw( + `${codepoint}.svg`, + ); + + if (!asset) { + throw createError({ + statusCode: 404, + statusMessage: "Emoji not found", + }); + } + + // Set proper content type for SVG + setResponseHeader(h3, "Content-Type", "image/svg+xml"); + setResponseHeader(h3, "Cache-Control", "private, max-age=31536000"); + + return asset; +}); diff --git a/server/internal/acls/descriptions.ts b/server/internal/acls/descriptions.ts index 58b8166..5e4a709 100644 --- a/server/internal/acls/descriptions.ts +++ b/server/internal/acls/descriptions.ts @@ -40,6 +40,8 @@ export const userACLDescriptions: ObjectFromList = { "news:read": "Read the server's news articles.", + "emoji:read": "Read built in emojis", + "settings:read": "Read system settings.", }; diff --git a/server/internal/acls/index.ts b/server/internal/acls/index.ts index 189d2b5..2aca8f0 100644 --- a/server/internal/acls/index.ts +++ b/server/internal/acls/index.ts @@ -32,6 +32,8 @@ export const userACLs = [ "clients:read", "clients:revoke", + "emoji:read", + "news:read", "settings:read", @@ -220,7 +222,7 @@ class ACLManager { return false; } - async hasACL(request: MinimumRequestObject | undefined, acls: string[]) { + async hasACL(request: MinimumRequestObject | undefined, acls: GlobalACL[]) { for (const acl of acls) { if (acl.startsWith(userACLPrefix)) { const rawACL = acl.substring(userACLPrefix.length); diff --git a/server/internal/tasks/index.ts b/server/internal/tasks/index.ts index 1ad6d37..7615914 100644 --- a/server/internal/tasks/index.ts +++ b/server/internal/tasks/index.ts @@ -284,7 +284,8 @@ class TaskHandler { return; } - const allowed = await aclManager.hasACL(request, task.acls); + // cast acls due to prisma types being less strict + const allowed = await aclManager.hasACL(request, task.acls as GlobalACL[]); if (!allowed) { // logger.warn("user does not have necessary ACLs"); peer.send(