diff --git a/package-lock.json b/package-lock.json index 3137138e..3ce41a6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "qr-svg": "^1.1.0", "react": "^18.2.0", "resend": "^3.3.0", + "satori": "^0.10.14", "slugify": "^1.6.6", "stripe": "^14.2.0", "worker-auth-providers": "^0.0.13-beta.4", @@ -6965,6 +6966,28 @@ "url": "https://ko-fi.com/killymxi" } }, + "node_modules/@shuding/opentype.js": { + "version": "1.4.0-beta.0", + "resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz", + "integrity": "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==", + "license": "MIT", + "dependencies": { + "fflate": "^0.7.3", + "string.prototype.codepointat": "^0.2.1" + }, + "bin": { + "ot": "bin/ot" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@shuding/opentype.js/node_modules/fflate": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", + "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==", + "license": "MIT" + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -9838,6 +9861,15 @@ "node": ">= 6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001651", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", @@ -10395,6 +10427,38 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/css-background-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz", + "integrity": "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==", + "license": "MIT" + }, + "node_modules/css-box-shadow": { + "version": "1.0.0-3", + "resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz", + "integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==", + "license": "MIT" + }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -12024,6 +12088,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -13792,6 +13862,18 @@ "tslib": "^2.0.3" } }, + "node_modules/hex-rgb": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz", + "integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/hono": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/hono/-/hono-3.9.0.tgz", @@ -14955,6 +15037,25 @@ "node": ">=10" } }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/linebreak/node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -16099,6 +16200,12 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -16120,6 +16227,16 @@ "node": ">=6" } }, + "node_modules/parse-css-color": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz", + "integrity": "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.1.4", + "hex-rgb": "^4.1.0" + } + }, "node_modules/parse-filepath": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", @@ -16572,8 +16689,7 @@ "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, "node_modules/postgres": { "version": "3.4.4", @@ -18025,6 +18141,33 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/satori": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/satori/-/satori-0.10.14.tgz", + "integrity": "sha512-abovcqmwl97WKioxpkfuMeZmndB1TuDFY/R+FymrZyiGP+pMYomvgSzVPnbNMWHHESOPosVHGL352oFbdAnJcA==", + "license": "MPL-2.0", + "dependencies": { + "@shuding/opentype.js": "1.4.0-beta.0", + "css-background-parser": "^0.1.0", + "css-box-shadow": "1.0.0-3", + "css-to-react-native": "^3.0.0", + "emoji-regex": "^10.2.1", + "escape-html": "^1.0.3", + "linebreak": "^1.1.0", + "parse-css-color": "^0.2.1", + "postcss-value-parser": "^4.2.0", + "yoga-wasm-web": "^0.3.3" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/satori/node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", @@ -18567,6 +18710,12 @@ "node": ">=8" } }, + "node_modules/string.prototype.codepointat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", + "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==", + "license": "MIT" + }, "node_modules/string.prototype.trim": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", @@ -19059,6 +19208,12 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.0.tgz", @@ -19455,6 +19610,16 @@ "ufo": "^1.5.3" } }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, "node_modules/unique-string": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", @@ -20716,6 +20881,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoga-wasm-web": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz", + "integrity": "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==", + "license": "MIT" + }, "node_modules/youch": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/youch/-/youch-3.3.3.tgz", diff --git a/package.json b/package.json index d7534e6e..e33a1f01 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,7 @@ "qr-svg": "^1.1.0", "react": "^18.2.0", "resend": "^3.3.0", + "satori": "^0.10.14", "slugify": "^1.6.6", "stripe": "^14.2.0", "worker-auth-providers": "^0.0.13-beta.4", diff --git a/workers/svg_renderer/index.ts b/workers/svg_renderer/index.ts deleted file mode 100644 index 67acd877..00000000 --- a/workers/svg_renderer/index.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Resvg, initWasm } from "@resvg/resvg-wasm"; -import { Hono } from "hono"; -import { QR } from "qr-svg"; - -import { createLogger } from "~/logging"; -import { isValidUUID } from "~/schema/shared/helpers"; - -// @ts-expect-error This import actually exists -import resvgwasm from "./index_bg.wasm"; - -const app = new Hono(); - -app.get("/qr/raw/:id", (c) => { - const logger = createLogger("qr-render"); - const uuid = c.req.param("id").trim().toLowerCase(); - - if (!isValidUUID(uuid)) { - logger.error("Invalid id"); - throw new Error("Invalid id"); - } - - const svg = QR(uuid); - - return c.text(svg); -}); - -app.get("/qr/svg/:id", (c) => { - const logger = createLogger("qr-render"); - const uuid = c.req.param("id").trim().toLowerCase(); - - if (!isValidUUID(uuid)) { - logger.error("Invalid id"); - throw new Error("Invalid id"); - } - - const svg = QR(uuid); - - c.res.headers.set("Content-Type", "image/svg+xml"); - - return c.text(svg); -}); - -app.get("/qr/png/:id", async (c) => { - const logger = createLogger("qr-render-png"); - const uuid = c.req.param("id").trim().toLowerCase(); - - if (!isValidUUID(uuid)) { - logger.error("Invalid id"); - throw new Error("Invalid id"); - } - - try { - await initWasm(resvgwasm as WebAssembly.Module); - } catch (error) { - logger.error("Resvg wasm not initialized"); - } - - const svg = QR(uuid); - - const resvg = new Resvg(svg, { - background: "white", - fitTo: { - mode: "width", - value: 1200, - }, - font: { - loadSystemFonts: false, - }, - }); - - const pngData = resvg.render(); - const pngBuffer = pngData.asPng(); - - c.res.headers.set("Content-Type", "image/png"); - - return new Response(pngBuffer, { - headers: { - "Content-Type": "image/png", - "Cache-Control": "public, immutable, no-transform, max-age=31536000", - }, - status: 200, - }); -}); - -app.get("/", (c) => { - return c.json({ - message: "Greetings and salutations from the CommunityOS team", - }); -}); - -export default app; diff --git a/workers/svg_renderer/index.tsx b/workers/svg_renderer/index.tsx new file mode 100644 index 00000000..884d5e4e --- /dev/null +++ b/workers/svg_renderer/index.tsx @@ -0,0 +1,275 @@ +import { Resvg, initWasm } from "@resvg/resvg-wasm"; +import { Hono } from "hono"; +import { QR } from "qr-svg"; +import * as React from "react"; +import satori from "satori"; + +import { createLogger } from "~/logging"; +import { isValidUUID } from "~/schema/shared/helpers"; +import { loadGoogleFont } from "~workers/svg_renderer/loadGoogleFont"; + +// @ts-expect-error This import actually exists +import resvgwasm from "./index_bg.wasm"; + +const app = new Hono(); + +app.get("/qr/raw/:id", (c) => { + const logger = createLogger("qr-render"); + const uuid = c.req.param("id").trim().toLowerCase(); + + if (!isValidUUID(uuid)) { + logger.error("Invalid id"); + throw new Error("Invalid id"); + } + + const svg = QR(uuid); + + return c.text(svg); +}); + +app.get("/qr/svg/:id", (c) => { + const logger = createLogger("qr-render"); + const uuid = c.req.param("id").trim().toLowerCase(); + + if (!isValidUUID(uuid)) { + logger.error("Invalid id"); + throw new Error("Invalid id"); + } + + const svg = QR(uuid); + + c.res.headers.set("Content-Type", "image/svg+xml"); + + return c.text(svg); +}); + +const toUpperCase = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); + +app.get("/hackathon/og/:userName/:tag/:eventName/:date", async (c) => { + const userName = c.req.param("userName").trim(); + const tag = c.req.param("tag").trim(); + const eventName = c.req.param("eventName").trim(); + const date = c.req.param("date").trim(); + + if (!userName || !tag || !eventName || !date) { + throw new Error("Invalid parameters"); + } + + const poppins = await loadGoogleFont({ + family: "Poppins", + weight: 400, + }); + + try { + const svg = await satori( +