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
This commit is contained in:
Husky
2026-01-13 22:49:58 -05:00
committed by GitHub
parent 63ac2b8ffc
commit 1eaec4c3e8
8 changed files with 67 additions and 119 deletions

View File

@@ -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)}`;
});
</script>

View File

@@ -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: {

View File

@@ -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",

81
pnpm-lock.yaml generated
View File

@@ -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

View File

@@ -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;
});

View File

@@ -40,6 +40,8 @@ export const userACLDescriptions: ObjectFromList<typeof userACLs> = {
"news:read": "Read the server's news articles.",
"emoji:read": "Read built in emojis",
"settings:read": "Read system settings.",
};

View File

@@ -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);

View File

@@ -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(