feat: improve og invite preview
This commit is contained in:
+7
-3
@@ -21,12 +21,16 @@ ENV VITE_BUILD_HASH=${VITE_BUILD_HASH}
|
|||||||
|
|
||||||
ENV NODE_OPTIONS=--max_old_space_size=16384
|
ENV NODE_OPTIONS=--max_old_space_size=16384
|
||||||
RUN pnpm run build
|
RUN pnpm run build
|
||||||
|
RUN pnpm prune --prod
|
||||||
|
|
||||||
FROM node:20-alpine
|
FROM node:20-bookworm-slim
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy only the built output - no source, no .env, no dev deps
|
# Copy production runtime only
|
||||||
|
COPY --from=builder /app/package.json ./package.json
|
||||||
|
COPY --from=builder /app/node_modules ./node_modules
|
||||||
COPY --from=builder /app/build ./build
|
COPY --from=builder /app/build ./build
|
||||||
|
COPY --from=builder /app/server.js ./server.js
|
||||||
|
|
||||||
CMD ["npx", "serve", "-s", "build"]
|
CMD ["node", "server.js"]
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ To run your own Flotilla, it's as simple as:
|
|||||||
```sh
|
```sh
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm run build
|
pnpm run build
|
||||||
npx serve -s build
|
node server.js
|
||||||
```
|
```
|
||||||
|
|
||||||
Or, if you prefer to use a container:
|
Or, if you prefer to use a container:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
|
"start": "node server.js",
|
||||||
"build": "./build.sh",
|
"build": "./build.sh",
|
||||||
"release:android": "./build.sh && cap build android --androidreleasetype APK --signing-type apksigner",
|
"release:android": "./build.sh && cap build android --androidreleasetype APK --signing-type apksigner",
|
||||||
"tauri:dev": "tauri dev",
|
"tauri:dev": "tauri dev",
|
||||||
@@ -60,6 +61,7 @@
|
|||||||
"@capawesome/capacitor-badge": "^8.0.0",
|
"@capawesome/capacitor-badge": "^8.0.0",
|
||||||
"@getalby/lightning-tools": "^6.1.0",
|
"@getalby/lightning-tools": "^6.1.0",
|
||||||
"@getalby/sdk": "^5.1.2",
|
"@getalby/sdk": "^5.1.2",
|
||||||
|
"@hono/node-server": "^1.19.14",
|
||||||
"@noble/curves": "^1.9.7",
|
"@noble/curves": "^1.9.7",
|
||||||
"@pomade/core": "^0.2.3",
|
"@pomade/core": "^0.2.3",
|
||||||
"@poppanator/sveltekit-svg": "^4.2.1",
|
"@poppanator/sveltekit-svg": "^4.2.1",
|
||||||
@@ -80,12 +82,14 @@
|
|||||||
"@welshman/signer": "^0.8.13",
|
"@welshman/signer": "^0.8.13",
|
||||||
"@welshman/store": "^0.8.13",
|
"@welshman/store": "^0.8.13",
|
||||||
"@welshman/util": "^0.8.13",
|
"@welshman/util": "^0.8.13",
|
||||||
|
"cheerio": "^1.2.0",
|
||||||
"compressorjs-next": "^1.1.2",
|
"compressorjs-next": "^1.1.2",
|
||||||
"daisyui": "^5.5.19",
|
"daisyui": "^5.5.19",
|
||||||
"date-picker-svelte": "^2.17.0",
|
"date-picker-svelte": "^2.17.0",
|
||||||
"dotenv": "^16.6.1",
|
"dotenv": "^16.6.1",
|
||||||
"emoji-picker-element": "^1.28.1",
|
"emoji-picker-element": "^1.28.1",
|
||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
|
"hono": "^4.12.14",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"idb": "^8.0.3",
|
"idb": "^8.0.3",
|
||||||
"livekit-client": "^2.17.2",
|
"livekit-client": "^2.17.2",
|
||||||
|
|||||||
Generated
+141
@@ -62,6 +62,9 @@ importers:
|
|||||||
'@getalby/sdk':
|
'@getalby/sdk':
|
||||||
specifier: ^5.1.2
|
specifier: ^5.1.2
|
||||||
version: 5.1.2(typescript@5.9.3)
|
version: 5.1.2(typescript@5.9.3)
|
||||||
|
'@hono/node-server':
|
||||||
|
specifier: ^1.19.14
|
||||||
|
version: 1.19.14(hono@4.12.14)
|
||||||
'@noble/curves':
|
'@noble/curves':
|
||||||
specifier: ^1.9.7
|
specifier: ^1.9.7
|
||||||
version: 1.9.7
|
version: 1.9.7
|
||||||
@@ -122,6 +125,9 @@ importers:
|
|||||||
'@welshman/util':
|
'@welshman/util':
|
||||||
specifier: ^0.8.13
|
specifier: ^0.8.13
|
||||||
version: 0.8.13(@noble/curves@1.9.7)(@welshman/lib@0.8.13)(nostr-tools@2.20.0(typescript@5.9.3))
|
version: 0.8.13(@noble/curves@1.9.7)(@welshman/lib@0.8.13)(nostr-tools@2.20.0(typescript@5.9.3))
|
||||||
|
cheerio:
|
||||||
|
specifier: ^1.2.0
|
||||||
|
version: 1.2.0
|
||||||
compressorjs-next:
|
compressorjs-next:
|
||||||
specifier: ^1.1.2
|
specifier: ^1.1.2
|
||||||
version: 1.1.2
|
version: 1.1.2
|
||||||
@@ -140,6 +146,9 @@ importers:
|
|||||||
fuse.js:
|
fuse.js:
|
||||||
specifier: ^7.1.0
|
specifier: ^7.1.0
|
||||||
version: 7.1.0
|
version: 7.1.0
|
||||||
|
hono:
|
||||||
|
specifier: ^4.12.14
|
||||||
|
version: 4.12.14
|
||||||
husky:
|
husky:
|
||||||
specifier: ^9.1.7
|
specifier: ^9.1.7
|
||||||
version: 9.1.7
|
version: 9.1.7
|
||||||
@@ -1096,6 +1105,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-yUF9LhuvdIFOwjV1aG0ryzfwDiGBFk/CRLkRvrrM9dsE38SUjKsf1FDga5jxsKMu80nWcPZR9TiGGASWedoYPA==}
|
resolution: {integrity: sha512-yUF9LhuvdIFOwjV1aG0ryzfwDiGBFk/CRLkRvrrM9dsE38SUjKsf1FDga5jxsKMu80nWcPZR9TiGGASWedoYPA==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
'@hono/node-server@1.19.14':
|
||||||
|
resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==}
|
||||||
|
engines: {node: '>=18.14.1'}
|
||||||
|
peerDependencies:
|
||||||
|
hono: ^4
|
||||||
|
|
||||||
'@humanfs/core@0.19.1':
|
'@humanfs/core@0.19.1':
|
||||||
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
||||||
engines: {node: '>=18.18.0'}
|
engines: {node: '>=18.18.0'}
|
||||||
@@ -2465,6 +2480,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
cheerio-select@2.1.0:
|
||||||
|
resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==}
|
||||||
|
|
||||||
|
cheerio@1.2.0:
|
||||||
|
resolution: {integrity: sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==}
|
||||||
|
engines: {node: '>=20.18.1'}
|
||||||
|
|
||||||
chevrotain@7.1.1:
|
chevrotain@7.1.1:
|
||||||
resolution: {integrity: sha512-wy3mC1x4ye+O+QkEinVJkPf5u2vsrDIYW9G7ZuwFl6v/Yu0LwUuT2POsb+NUWApebyxfkQq6+yDfRExbnI5rcw==}
|
resolution: {integrity: sha512-wy3mC1x4ye+O+QkEinVJkPf5u2vsrDIYW9G7ZuwFl6v/Yu0LwUuT2POsb+NUWApebyxfkQq6+yDfRExbnI5rcw==}
|
||||||
|
|
||||||
@@ -2830,6 +2852,9 @@ packages:
|
|||||||
emoji-regex@8.0.0:
|
emoji-regex@8.0.0:
|
||||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||||
|
|
||||||
|
encoding-sniffer@0.2.1:
|
||||||
|
resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==}
|
||||||
|
|
||||||
enhanced-resolve@5.20.1:
|
enhanced-resolve@5.20.1:
|
||||||
resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==}
|
resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
@@ -2841,6 +2866,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||||
engines: {node: '>=0.12'}
|
engines: {node: '>=0.12'}
|
||||||
|
|
||||||
|
entities@6.0.1:
|
||||||
|
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
|
||||||
|
engines: {node: '>=0.12'}
|
||||||
|
|
||||||
|
entities@7.0.1:
|
||||||
|
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
|
||||||
|
engines: {node: '>=0.12'}
|
||||||
|
|
||||||
env-paths@2.2.1:
|
env-paths@2.2.1:
|
||||||
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
|
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -3234,6 +3267,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
hono@4.12.14:
|
||||||
|
resolution: {integrity: sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==}
|
||||||
|
engines: {node: '>=16.9.0'}
|
||||||
|
|
||||||
hosted-git-info@2.8.9:
|
hosted-git-info@2.8.9:
|
||||||
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
|
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
|
||||||
|
|
||||||
@@ -3241,6 +3278,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==}
|
resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
htmlparser2@10.1.0:
|
||||||
|
resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==}
|
||||||
|
|
||||||
husky@9.1.7:
|
husky@9.1.7:
|
||||||
resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
|
resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -3249,6 +3289,10 @@ packages:
|
|||||||
ico-endec@0.1.6:
|
ico-endec@0.1.6:
|
||||||
resolution: {integrity: sha512-ZdLU38ZoED3g1j3iEyzcQj+wAkY2xfWNkymszfJPoxucIUhK7NayQ+/C4Kv0nDFMIsbtbEHldv3V8PU494/ueQ==}
|
resolution: {integrity: sha512-ZdLU38ZoED3g1j3iEyzcQj+wAkY2xfWNkymszfJPoxucIUhK7NayQ+/C4Kv0nDFMIsbtbEHldv3V8PU494/ueQ==}
|
||||||
|
|
||||||
|
iconv-lite@0.6.3:
|
||||||
|
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
idb@7.1.1:
|
idb@7.1.1:
|
||||||
resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==}
|
resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==}
|
||||||
|
|
||||||
@@ -4015,6 +4059,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
|
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
parse5-htmlparser2-tree-adapter@7.1.0:
|
||||||
|
resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==}
|
||||||
|
|
||||||
|
parse5-parser-stream@7.1.2:
|
||||||
|
resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==}
|
||||||
|
|
||||||
|
parse5@7.3.0:
|
||||||
|
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
|
||||||
|
|
||||||
path-exists@3.0.0:
|
path-exists@3.0.0:
|
||||||
resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==}
|
resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -4456,6 +4509,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
|
resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
safer-buffer@2.1.2:
|
||||||
|
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||||
|
|
||||||
sax@1.1.4:
|
sax@1.1.4:
|
||||||
resolution: {integrity: sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==}
|
resolution: {integrity: sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==}
|
||||||
|
|
||||||
@@ -4891,6 +4947,10 @@ packages:
|
|||||||
undici-types@7.16.0:
|
undici-types@7.16.0:
|
||||||
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
|
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
|
||||||
|
|
||||||
|
undici@7.25.0:
|
||||||
|
resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==}
|
||||||
|
engines: {node: '>=20.18.1'}
|
||||||
|
|
||||||
unicode-canonical-property-names-ecmascript@2.0.1:
|
unicode-canonical-property-names-ecmascript@2.0.1:
|
||||||
resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==}
|
resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -5009,6 +5069,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-5ZZY1+lGq8LEKuDlg9M2RPJHlH3R7OVwyHqMcUsLKCgd9Wvf+QrFTCItkXXYPmrJn8H6gRLXbSgxLLdexiqHxw==}
|
resolution: {integrity: sha512-5ZZY1+lGq8LEKuDlg9M2RPJHlH3R7OVwyHqMcUsLKCgd9Wvf+QrFTCItkXXYPmrJn8H6gRLXbSgxLLdexiqHxw==}
|
||||||
engines: {node: '>=6.0.0', npm: '>=3.10.0'}
|
engines: {node: '>=6.0.0', npm: '>=3.10.0'}
|
||||||
|
|
||||||
|
whatwg-encoding@3.1.1:
|
||||||
|
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
|
||||||
|
|
||||||
|
whatwg-mimetype@4.0.0:
|
||||||
|
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
whatwg-url@5.0.0:
|
whatwg-url@5.0.0:
|
||||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||||
|
|
||||||
@@ -6228,6 +6297,10 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
|
'@hono/node-server@1.19.14(hono@4.12.14)':
|
||||||
|
dependencies:
|
||||||
|
hono: 4.12.14
|
||||||
|
|
||||||
'@humanfs/core@0.19.1': {}
|
'@humanfs/core@0.19.1': {}
|
||||||
|
|
||||||
'@humanfs/node@0.16.7':
|
'@humanfs/node@0.16.7':
|
||||||
@@ -7642,6 +7715,29 @@ snapshots:
|
|||||||
ansi-styles: 4.3.0
|
ansi-styles: 4.3.0
|
||||||
supports-color: 7.2.0
|
supports-color: 7.2.0
|
||||||
|
|
||||||
|
cheerio-select@2.1.0:
|
||||||
|
dependencies:
|
||||||
|
boolbase: 1.0.0
|
||||||
|
css-select: 5.2.2
|
||||||
|
css-what: 6.2.2
|
||||||
|
domelementtype: 2.3.0
|
||||||
|
domhandler: 5.0.3
|
||||||
|
domutils: 3.2.2
|
||||||
|
|
||||||
|
cheerio@1.2.0:
|
||||||
|
dependencies:
|
||||||
|
cheerio-select: 2.1.0
|
||||||
|
dom-serializer: 2.0.0
|
||||||
|
domhandler: 5.0.3
|
||||||
|
domutils: 3.2.2
|
||||||
|
encoding-sniffer: 0.2.1
|
||||||
|
htmlparser2: 10.1.0
|
||||||
|
parse5: 7.3.0
|
||||||
|
parse5-htmlparser2-tree-adapter: 7.1.0
|
||||||
|
parse5-parser-stream: 7.1.2
|
||||||
|
undici: 7.25.0
|
||||||
|
whatwg-mimetype: 4.0.0
|
||||||
|
|
||||||
chevrotain@7.1.1:
|
chevrotain@7.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
regexp-to-ast: 0.5.0
|
regexp-to-ast: 0.5.0
|
||||||
@@ -8032,6 +8128,11 @@ snapshots:
|
|||||||
|
|
||||||
emoji-regex@8.0.0: {}
|
emoji-regex@8.0.0: {}
|
||||||
|
|
||||||
|
encoding-sniffer@0.2.1:
|
||||||
|
dependencies:
|
||||||
|
iconv-lite: 0.6.3
|
||||||
|
whatwg-encoding: 3.1.1
|
||||||
|
|
||||||
enhanced-resolve@5.20.1:
|
enhanced-resolve@5.20.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
@@ -8041,6 +8142,10 @@ snapshots:
|
|||||||
|
|
||||||
entities@4.5.0: {}
|
entities@4.5.0: {}
|
||||||
|
|
||||||
|
entities@6.0.1: {}
|
||||||
|
|
||||||
|
entities@7.0.1: {}
|
||||||
|
|
||||||
env-paths@2.2.1: {}
|
env-paths@2.2.1: {}
|
||||||
|
|
||||||
env-paths@3.0.0: {}
|
env-paths@3.0.0: {}
|
||||||
@@ -8550,16 +8655,29 @@ snapshots:
|
|||||||
|
|
||||||
he@1.2.0: {}
|
he@1.2.0: {}
|
||||||
|
|
||||||
|
hono@4.12.14: {}
|
||||||
|
|
||||||
hosted-git-info@2.8.9: {}
|
hosted-git-info@2.8.9: {}
|
||||||
|
|
||||||
hosted-git-info@4.1.0:
|
hosted-git-info@4.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
lru-cache: 6.0.0
|
lru-cache: 6.0.0
|
||||||
|
|
||||||
|
htmlparser2@10.1.0:
|
||||||
|
dependencies:
|
||||||
|
domelementtype: 2.3.0
|
||||||
|
domhandler: 5.0.3
|
||||||
|
domutils: 3.2.2
|
||||||
|
entities: 7.0.1
|
||||||
|
|
||||||
husky@9.1.7: {}
|
husky@9.1.7: {}
|
||||||
|
|
||||||
ico-endec@0.1.6: {}
|
ico-endec@0.1.6: {}
|
||||||
|
|
||||||
|
iconv-lite@0.6.3:
|
||||||
|
dependencies:
|
||||||
|
safer-buffer: 2.1.2
|
||||||
|
|
||||||
idb@7.1.1: {}
|
idb@7.1.1: {}
|
||||||
|
|
||||||
idb@8.0.3: {}
|
idb@8.0.3: {}
|
||||||
@@ -9266,6 +9384,19 @@ snapshots:
|
|||||||
json-parse-even-better-errors: 2.3.1
|
json-parse-even-better-errors: 2.3.1
|
||||||
lines-and-columns: 1.2.4
|
lines-and-columns: 1.2.4
|
||||||
|
|
||||||
|
parse5-htmlparser2-tree-adapter@7.1.0:
|
||||||
|
dependencies:
|
||||||
|
domhandler: 5.0.3
|
||||||
|
parse5: 7.3.0
|
||||||
|
|
||||||
|
parse5-parser-stream@7.1.2:
|
||||||
|
dependencies:
|
||||||
|
parse5: 7.3.0
|
||||||
|
|
||||||
|
parse5@7.3.0:
|
||||||
|
dependencies:
|
||||||
|
entities: 6.0.1
|
||||||
|
|
||||||
path-exists@3.0.0: {}
|
path-exists@3.0.0: {}
|
||||||
|
|
||||||
path-exists@4.0.0: {}
|
path-exists@4.0.0: {}
|
||||||
@@ -9704,6 +9835,8 @@ snapshots:
|
|||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
is-regex: 1.2.1
|
is-regex: 1.2.1
|
||||||
|
|
||||||
|
safer-buffer@2.1.2: {}
|
||||||
|
|
||||||
sax@1.1.4: {}
|
sax@1.1.4: {}
|
||||||
|
|
||||||
sax@1.4.4: {}
|
sax@1.4.4: {}
|
||||||
@@ -10229,6 +10362,8 @@ snapshots:
|
|||||||
|
|
||||||
undici-types@7.16.0: {}
|
undici-types@7.16.0: {}
|
||||||
|
|
||||||
|
undici@7.25.0: {}
|
||||||
|
|
||||||
unicode-canonical-property-names-ecmascript@2.0.1: {}
|
unicode-canonical-property-names-ecmascript@2.0.1: {}
|
||||||
|
|
||||||
unicode-match-property-ecmascript@2.0.0:
|
unicode-match-property-ecmascript@2.0.0:
|
||||||
@@ -10309,6 +10444,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
sdp: 3.2.1
|
sdp: 3.2.1
|
||||||
|
|
||||||
|
whatwg-encoding@3.1.1:
|
||||||
|
dependencies:
|
||||||
|
iconv-lite: 0.6.3
|
||||||
|
|
||||||
|
whatwg-mimetype@4.0.0: {}
|
||||||
|
|
||||||
whatwg-url@5.0.0:
|
whatwg-url@5.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
tr46: 0.0.3
|
tr46: 0.0.3
|
||||||
|
|||||||
@@ -0,0 +1,785 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import {readFile} from "node:fs/promises"
|
||||||
|
import {dirname, extname, join} from "node:path"
|
||||||
|
import {fileURLToPath} from "node:url"
|
||||||
|
import {load as loadHtml} from "cheerio"
|
||||||
|
import {serve} from "@hono/node-server"
|
||||||
|
import {serveStatic} from "@hono/node-server/serve-static"
|
||||||
|
import {Hono} from "hono"
|
||||||
|
import {request} from "@welshman/net"
|
||||||
|
import {
|
||||||
|
Address,
|
||||||
|
CLASSIFIED,
|
||||||
|
EVENT_TIME,
|
||||||
|
POLL,
|
||||||
|
ROOM_META,
|
||||||
|
THREAD,
|
||||||
|
ZAP_GOAL,
|
||||||
|
displayPubkey,
|
||||||
|
getTagValue,
|
||||||
|
normalizeRelayUrl,
|
||||||
|
readRoomMeta,
|
||||||
|
} from "@welshman/util"
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
const buildDir = join(__dirname, "build")
|
||||||
|
const indexPath = join(buildDir, "index.html")
|
||||||
|
|
||||||
|
const RELAY_CACHE_TTL_MS = 5 * 60 * 1000
|
||||||
|
const NOSTR_CACHE_TTL_MS = 60 * 1000
|
||||||
|
const RELAY_TIMEOUT_MS = 1500
|
||||||
|
const NOSTR_TIMEOUT_MS = 1800
|
||||||
|
|
||||||
|
const staticTitles = new Map([
|
||||||
|
["/", "Redirecting"],
|
||||||
|
["/home", "Home"],
|
||||||
|
["/spaces", "Spaces"],
|
||||||
|
["/spaces/create", "Create a Space"],
|
||||||
|
["/chat", "Messages"],
|
||||||
|
["/join", "Join Space"],
|
||||||
|
["/people", "Find People"],
|
||||||
|
["/settings/about", "About"],
|
||||||
|
["/settings/profile", "Profile Settings"],
|
||||||
|
["/settings/content", "Content Settings"],
|
||||||
|
["/settings/privacy", "Privacy Settings"],
|
||||||
|
["/settings/relays", "Relay Settings"],
|
||||||
|
["/settings/alerts", "Alert Settings"],
|
||||||
|
["/settings/wallet", "Wallet Settings"],
|
||||||
|
])
|
||||||
|
|
||||||
|
const spaceSectionTitles = new Map([
|
||||||
|
["chat", "Space Chat"],
|
||||||
|
["recent", "Recent Activity"],
|
||||||
|
["threads", "Threads"],
|
||||||
|
["classifieds", "Classifieds"],
|
||||||
|
["calendar", "Calendar"],
|
||||||
|
["goals", "Goals"],
|
||||||
|
["polls", "Polls"],
|
||||||
|
])
|
||||||
|
|
||||||
|
const eventRouteKinds = new Map([
|
||||||
|
["threads", THREAD],
|
||||||
|
["goals", ZAP_GOAL],
|
||||||
|
["calendar", EVENT_TIME],
|
||||||
|
["classifieds", CLASSIFIED],
|
||||||
|
["polls", POLL],
|
||||||
|
])
|
||||||
|
|
||||||
|
const reservedSingleSegments = new Set([
|
||||||
|
"home",
|
||||||
|
"spaces",
|
||||||
|
"space",
|
||||||
|
"chat",
|
||||||
|
"join",
|
||||||
|
"people",
|
||||||
|
"settings",
|
||||||
|
])
|
||||||
|
|
||||||
|
const relayInfoCache = new Map()
|
||||||
|
const roomInfoCache = new Map()
|
||||||
|
const eventCache = new Map()
|
||||||
|
|
||||||
|
const indexHtml = await readFile(indexPath, "utf8").catch(error => {
|
||||||
|
console.error("Unable to start server: build/index.html is missing. Run `pnpm run build` first.")
|
||||||
|
|
||||||
|
throw error
|
||||||
|
})
|
||||||
|
|
||||||
|
const defaults = getHtmlDefaults(indexHtml)
|
||||||
|
const app = new Hono()
|
||||||
|
const staticFiles = serveStatic({
|
||||||
|
root: "./build",
|
||||||
|
rewriteRequestPath: path => path.replace(/^\/+/, ""),
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use("*", staticFiles)
|
||||||
|
|
||||||
|
app.get("*", async c => {
|
||||||
|
const requestUrl = new URL(c.req.url)
|
||||||
|
|
||||||
|
if (extname(requestUrl.pathname)) {
|
||||||
|
return c.text("Not Found", 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
const origin = getRequestOrigin(c.req.raw, requestUrl)
|
||||||
|
const meta = await buildRouteMeta(requestUrl, origin)
|
||||||
|
const html = renderHtml(indexHtml, meta)
|
||||||
|
|
||||||
|
c.header("Cache-Control", "no-cache")
|
||||||
|
|
||||||
|
return c.html(html)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.notFound(c => c.text("Not Found", 404))
|
||||||
|
|
||||||
|
app.onError((error, c) => {
|
||||||
|
console.error(error)
|
||||||
|
|
||||||
|
return c.text("Internal Server Error", 500)
|
||||||
|
})
|
||||||
|
|
||||||
|
const port = Number.parseInt(process.env.PORT || "3000", 10)
|
||||||
|
const host = process.env.HOST || "0.0.0.0"
|
||||||
|
|
||||||
|
serve({fetch: app.fetch, hostname: host, port})
|
||||||
|
console.log(`Flotilla server listening on http://${host}:${port}`)
|
||||||
|
|
||||||
|
async function buildRouteMeta(requestUrl, origin) {
|
||||||
|
const absoluteDefaultImage = toAbsoluteHttpUrl(defaults.image, origin)
|
||||||
|
|
||||||
|
if (!absoluteDefaultImage) {
|
||||||
|
throw new Error(`Default twitter:image must resolve to an absolute URL. Found: ${defaults.image}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
card: "summary",
|
||||||
|
description: defaults.description,
|
||||||
|
image: absoluteDefaultImage,
|
||||||
|
site: defaults.site,
|
||||||
|
title: defaults.title,
|
||||||
|
type: "website",
|
||||||
|
url: requestUrl.href,
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = parseRoute(requestUrl.pathname)
|
||||||
|
|
||||||
|
if (route.kind === "join") {
|
||||||
|
return await buildJoinMeta(meta, requestUrl, origin)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.kind === "static") {
|
||||||
|
meta.title = route.title
|
||||||
|
|
||||||
|
return meta
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.kind === "chat") {
|
||||||
|
meta.title = getChatTitle(route.chat)
|
||||||
|
|
||||||
|
return meta
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.kind === "bech32") {
|
||||||
|
meta.title = "Opening Link"
|
||||||
|
|
||||||
|
return meta
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!route.relay) {
|
||||||
|
return meta
|
||||||
|
}
|
||||||
|
|
||||||
|
const relayUrl = normalizeRelayParam(route.relay)
|
||||||
|
const relayInfo = relayUrl ? await loadRelayInfo(relayUrl) : undefined
|
||||||
|
const relayName = relayInfo?.name || (relayUrl ? getRelayDisplay(relayUrl) : "Space")
|
||||||
|
const relayHttpUrl = relayUrl ? toRelayHttpUrl(relayUrl) : undefined
|
||||||
|
|
||||||
|
if (relayInfo?.icon) {
|
||||||
|
meta.image = relayInfo.icon
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relayInfo?.description) {
|
||||||
|
meta.description = relayInfo.description
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.kind === "space") {
|
||||||
|
meta.title = relayName
|
||||||
|
|
||||||
|
return meta
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.kind === "space-section") {
|
||||||
|
meta.title = composeSpaceTitle(relayName, route.sectionTitle)
|
||||||
|
|
||||||
|
return meta
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.kind === "room") {
|
||||||
|
const roomInfo = relayUrl ? await loadRoomInfo(relayUrl, route.h) : undefined
|
||||||
|
const roomName = roomInfo?.name || route.h
|
||||||
|
|
||||||
|
meta.title = composeSpaceTitle(relayName, roomName)
|
||||||
|
meta.description = roomInfo?.about || meta.description
|
||||||
|
|
||||||
|
const roomImage = roomInfo?.picture
|
||||||
|
? toAbsoluteHttpUrl(roomInfo.picture, relayHttpUrl || origin)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
if (roomImage) {
|
||||||
|
meta.image = roomImage
|
||||||
|
}
|
||||||
|
|
||||||
|
return meta
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.kind === "event") {
|
||||||
|
const event = relayUrl
|
||||||
|
? await loadEventForRoute(relayUrl, route.section, route.identifier)
|
||||||
|
: undefined
|
||||||
|
const eventTitle = getEventTitle(route.section, event)
|
||||||
|
|
||||||
|
meta.title = composeSpaceTitle(relayName, eventTitle)
|
||||||
|
meta.description = getEventDescription(route.section, event, meta.description)
|
||||||
|
|
||||||
|
const eventImage = getTagValue("image", event?.tags || [])
|
||||||
|
const absoluteEventImage = eventImage
|
||||||
|
? toAbsoluteHttpUrl(eventImage, relayHttpUrl || origin)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
if (absoluteEventImage) {
|
||||||
|
meta.image = absoluteEventImage
|
||||||
|
meta.card = "summary_large_image"
|
||||||
|
}
|
||||||
|
|
||||||
|
return meta
|
||||||
|
}
|
||||||
|
|
||||||
|
return meta
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRoute(pathname) {
|
||||||
|
const normalizedPath = normalizePathname(pathname)
|
||||||
|
|
||||||
|
if (normalizedPath === "/join") {
|
||||||
|
return {kind: "join"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (staticTitles.has(normalizedPath)) {
|
||||||
|
return {kind: "static", title: staticTitles.get(normalizedPath)}
|
||||||
|
}
|
||||||
|
|
||||||
|
const segments = getPathSegments(normalizedPath)
|
||||||
|
|
||||||
|
if (segments.length === 2 && segments[0] === "chat") {
|
||||||
|
return {chat: segments[1], kind: "chat"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segments.length === 1 && !reservedSingleSegments.has(segments[0])) {
|
||||||
|
return {bech32: segments[0], kind: "bech32"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((segments[0] === "spaces" || segments[0] === "space") && segments.length >= 2) {
|
||||||
|
const relay = segments[1]
|
||||||
|
|
||||||
|
if (segments.length === 2) {
|
||||||
|
return {kind: "space", relay}
|
||||||
|
}
|
||||||
|
|
||||||
|
const section = segments[2]
|
||||||
|
|
||||||
|
if (segments.length === 3) {
|
||||||
|
if (spaceSectionTitles.has(section)) {
|
||||||
|
return {
|
||||||
|
kind: "space-section",
|
||||||
|
relay,
|
||||||
|
section,
|
||||||
|
sectionTitle: spaceSectionTitles.get(section),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {h: section, kind: "room", relay}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segments.length === 4 && eventRouteKinds.has(section)) {
|
||||||
|
return {
|
||||||
|
identifier: segments[3],
|
||||||
|
kind: "event",
|
||||||
|
relay,
|
||||||
|
section,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {kind: "unknown"}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildJoinMeta(meta, requestUrl, origin) {
|
||||||
|
const relayUrl = parseInviteRelay(requestUrl)
|
||||||
|
|
||||||
|
if (!relayUrl) {
|
||||||
|
meta.title = staticTitles.get("/join") || "Join Space"
|
||||||
|
|
||||||
|
return meta
|
||||||
|
}
|
||||||
|
|
||||||
|
const relayInfo = await loadRelayInfo(relayUrl)
|
||||||
|
const relayDisplay = relayInfo?.name || getRelayDisplay(relayUrl)
|
||||||
|
|
||||||
|
meta.title = `Invitation to join ${relayDisplay}`
|
||||||
|
meta.description = relayInfo?.description || `Join this Flotilla space on ${relayDisplay}.`
|
||||||
|
meta.image = relayInfo?.icon || meta.image
|
||||||
|
meta.url = requestUrl.href
|
||||||
|
meta.site = defaults.site
|
||||||
|
|
||||||
|
return meta
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChatTitle(chat) {
|
||||||
|
if (!chat) {
|
||||||
|
return "Chat"
|
||||||
|
}
|
||||||
|
|
||||||
|
const peers = chat
|
||||||
|
.split(",")
|
||||||
|
.map(part => part.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
if (peers.length === 1) {
|
||||||
|
return `Chat with ${displayPubkey(peers[0])}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (peers.length > 1) {
|
||||||
|
return `Group chat (${peers.length})`
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Chat"
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEventTitle(section, event) {
|
||||||
|
if (section === "threads") {
|
||||||
|
return getTagValue("title", event?.tags || []) || "Thread"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (section === "calendar") {
|
||||||
|
return getTagValue("title", event?.tags || []) || "Event"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (section === "classifieds") {
|
||||||
|
return getTagValue("title", event?.tags || []) || "Listing"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (section === "goals") {
|
||||||
|
return event?.content?.trim() || getTagValue("summary", event?.tags || []) || "Goal"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (section === "polls") {
|
||||||
|
return getTagValue("title", event?.tags || []) || "Poll"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Event"
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEventDescription(section, event, fallback) {
|
||||||
|
const summary =
|
||||||
|
getTagValue("summary", event?.tags || []) || getTagValue("description", event?.tags || [])
|
||||||
|
|
||||||
|
if (summary) {
|
||||||
|
return clip(summary, 220)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event?.content?.trim()) {
|
||||||
|
return clip(event.content.trim(), 220)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (section === "threads") {
|
||||||
|
return "Read this thread in Flotilla."
|
||||||
|
}
|
||||||
|
|
||||||
|
if (section === "goals") {
|
||||||
|
return "Track this goal in Flotilla."
|
||||||
|
}
|
||||||
|
|
||||||
|
if (section === "calendar") {
|
||||||
|
return "View this calendar event in Flotilla."
|
||||||
|
}
|
||||||
|
|
||||||
|
if (section === "classifieds") {
|
||||||
|
return "Browse this listing in Flotilla."
|
||||||
|
}
|
||||||
|
|
||||||
|
if (section === "polls") {
|
||||||
|
return "Take this poll in Flotilla."
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
function composeSpaceTitle(spaceName, leafTitle) {
|
||||||
|
const cleanedSpace = spaceName?.trim()
|
||||||
|
const cleanedLeaf = leafTitle?.trim()
|
||||||
|
|
||||||
|
if (cleanedSpace && cleanedLeaf) {
|
||||||
|
return `${cleanedSpace} / ${cleanedLeaf}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleanedLeaf || cleanedSpace || defaults.title
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadRelayInfo(relayUrl) {
|
||||||
|
const cached = getCachedValue(relayInfoCache, relayUrl)
|
||||||
|
|
||||||
|
if (cached) {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
|
const relayHttpUrl = toRelayHttpUrl(relayUrl)
|
||||||
|
|
||||||
|
if (!relayHttpUrl) {
|
||||||
|
setCachedValue(relayInfoCache, relayUrl, undefined, RELAY_CACHE_TTL_MS)
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new AbortController()
|
||||||
|
const timeout = setTimeout(() => controller.abort(), RELAY_TIMEOUT_MS)
|
||||||
|
|
||||||
|
let value
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(relayHttpUrl, {
|
||||||
|
headers: {Accept: "application/nostr+json"},
|
||||||
|
signal: controller.signal,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const json = await response.json()
|
||||||
|
const name = typeof json.name === "string" ? json.name.trim() : ""
|
||||||
|
const description = typeof json.description === "string" ? json.description.trim() : ""
|
||||||
|
const icon = typeof json.icon === "string" ? toAbsoluteHttpUrl(json.icon, relayHttpUrl) : undefined
|
||||||
|
|
||||||
|
value = {
|
||||||
|
description: description || undefined,
|
||||||
|
icon,
|
||||||
|
name: name || undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
value = undefined
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
setCachedValue(relayInfoCache, relayUrl, value, RELAY_CACHE_TTL_MS)
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadRoomInfo(relayUrl, h) {
|
||||||
|
const cacheKey = `${relayUrl}|${h}`
|
||||||
|
const cached = getCachedValue(roomInfoCache, cacheKey)
|
||||||
|
|
||||||
|
if (cached !== undefined) {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = await requestEvents(relayUrl, [{"#d": [h], kinds: [ROOM_META], limit: 20}])
|
||||||
|
const roomMetas = []
|
||||||
|
|
||||||
|
for (const event of events) {
|
||||||
|
try {
|
||||||
|
const roomMeta = readRoomMeta(event)
|
||||||
|
|
||||||
|
if (roomMeta.h === h) {
|
||||||
|
roomMetas.push(roomMeta)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore malformed room metadata.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const latest = roomMetas.sort((a, b) => b.event.created_at - a.event.created_at)[0]
|
||||||
|
const roomInfo = latest
|
||||||
|
? {
|
||||||
|
about: latest.about,
|
||||||
|
name: latest.name,
|
||||||
|
picture: latest.picture,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
setCachedValue(roomInfoCache, cacheKey, roomInfo, NOSTR_CACHE_TTL_MS)
|
||||||
|
|
||||||
|
return roomInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadEventForRoute(relayUrl, section, identifier) {
|
||||||
|
const kind = eventRouteKinds.get(section)
|
||||||
|
|
||||||
|
if (!kind || !identifier) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheKey = `${relayUrl}|${section}|${identifier}`
|
||||||
|
const cached = getCachedValue(eventCache, cacheKey)
|
||||||
|
|
||||||
|
if (cached !== undefined) {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
|
const filters = getEventFilters(kind, identifier)
|
||||||
|
const events = filters.length > 0 ? await requestEvents(relayUrl, filters) : []
|
||||||
|
const event = events[0]
|
||||||
|
|
||||||
|
setCachedValue(eventCache, cacheKey, event, NOSTR_CACHE_TTL_MS)
|
||||||
|
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEventFilters(kind, identifier) {
|
||||||
|
if (kind === EVENT_TIME || kind === CLASSIFIED) {
|
||||||
|
try {
|
||||||
|
const address = Address.from(identifier)
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"#d": [address.identifier],
|
||||||
|
authors: [address.pubkey],
|
||||||
|
kinds: [address.kind],
|
||||||
|
limit: 1,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
} catch {
|
||||||
|
return [{ids: [identifier], kinds: [kind], limit: 1}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [{ids: [identifier], kinds: [kind], limit: 1}]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestEvents(relayUrl, filters) {
|
||||||
|
const controller = new AbortController()
|
||||||
|
const timeout = setTimeout(() => controller.abort(), NOSTR_TIMEOUT_MS)
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await request({
|
||||||
|
autoClose: true,
|
||||||
|
filters,
|
||||||
|
relays: [relayUrl],
|
||||||
|
signal: controller.signal,
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHtml(html, meta) {
|
||||||
|
const $ = loadHtml(html)
|
||||||
|
|
||||||
|
upsertTitle($, meta.title)
|
||||||
|
upsertMetaTag($, "name", "description", meta.description)
|
||||||
|
upsertMetaTag($, "name", "og:url", meta.url)
|
||||||
|
upsertMetaTag($, "name", "og:type", meta.type)
|
||||||
|
upsertMetaTag($, "name", "og:title", meta.title)
|
||||||
|
upsertMetaTag($, "name", "og:description", meta.description)
|
||||||
|
upsertMetaTag($, "name", "twitter:card", meta.card)
|
||||||
|
upsertMetaTag($, "name", "twitter:site", meta.site)
|
||||||
|
upsertMetaTag($, "name", "twitter:title", meta.title)
|
||||||
|
upsertMetaTag($, "name", "twitter:description", meta.description)
|
||||||
|
upsertMetaTag($, "name", "twitter:image", meta.image)
|
||||||
|
|
||||||
|
upsertMetaTag($, "property", "og:url", meta.url)
|
||||||
|
upsertMetaTag($, "property", "og:type", meta.type)
|
||||||
|
upsertMetaTag($, "property", "og:title", meta.title)
|
||||||
|
upsertMetaTag($, "property", "og:description", meta.description)
|
||||||
|
upsertMetaTag($, "property", "og:image", meta.image)
|
||||||
|
|
||||||
|
return $.html()
|
||||||
|
}
|
||||||
|
|
||||||
|
function upsertTitle($, value) {
|
||||||
|
let titleTag = $("head > title").first()
|
||||||
|
|
||||||
|
if (titleTag.length === 0) {
|
||||||
|
$("head").prepend("<title></title>")
|
||||||
|
titleTag = $("head > title").first()
|
||||||
|
}
|
||||||
|
|
||||||
|
titleTag.text(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function upsertMetaTag($, attribute, key, content) {
|
||||||
|
const selector = `meta[${attribute}="${key}"]`
|
||||||
|
let metaTag = $(selector).first()
|
||||||
|
|
||||||
|
if (metaTag.length === 0) {
|
||||||
|
metaTag = $("<meta>").attr(attribute, key)
|
||||||
|
$("head").append(metaTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
metaTag.attr("content", content)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHtmlDefaults(html) {
|
||||||
|
const $ = loadHtml(html)
|
||||||
|
|
||||||
|
return {
|
||||||
|
description: readRequiredMetaContent($, "og:description"),
|
||||||
|
image: readRequiredMetaContent($, "twitter:image"),
|
||||||
|
site: readRequiredMetaContent($, "twitter:site"),
|
||||||
|
title: readRequiredMetaContent($, "og:title"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readRequiredMetaContent($, key) {
|
||||||
|
const content = readMetaContent($, key)
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
throw new Error(`Missing required meta tag ${key} in build/index.html. Ensure it exists in src/app.html.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
function readMetaContent($, key) {
|
||||||
|
const byName = $(`meta[name="${key}"]`).attr("content")
|
||||||
|
|
||||||
|
if (typeof byName === "string" && byName.trim()) {
|
||||||
|
return byName.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
const byProperty = $(`meta[property="${key}"]`).attr("content")
|
||||||
|
|
||||||
|
return typeof byProperty === "string" && byProperty.trim() ? byProperty.trim() : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseInviteRelay(requestUrl) {
|
||||||
|
const relay = requestUrl.searchParams.get("r") || requestUrl.searchParams.get("relay")
|
||||||
|
|
||||||
|
if (!relay) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeRelayParam(relay)
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRelayParam(value) {
|
||||||
|
const decoded = value.trim()
|
||||||
|
|
||||||
|
if (!decoded) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasProtocol = /^[a-zA-Z][a-zA-Z\d+.-]*:\/\//.test(decoded)
|
||||||
|
const withProtocol = hasProtocol ? decoded : `wss://${decoded}`
|
||||||
|
|
||||||
|
try {
|
||||||
|
const normalized = normalizeRelayUrl(withProtocol)
|
||||||
|
|
||||||
|
if (normalized.startsWith("ws://") || normalized.startsWith("wss://")) {
|
||||||
|
return normalized.replace(/\/+$/, "")
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore malformed relay URLs.
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function toRelayHttpUrl(relayUrl) {
|
||||||
|
if (relayUrl.startsWith("wss://")) {
|
||||||
|
return `https://${relayUrl.slice(6)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relayUrl.startsWith("ws://")) {
|
||||||
|
return `http://${relayUrl.slice(5)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRelayDisplay(relayUrl) {
|
||||||
|
const relayHttpUrl = toRelayHttpUrl(relayUrl)
|
||||||
|
|
||||||
|
if (!relayHttpUrl) {
|
||||||
|
return relayUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new URL(relayHttpUrl).host
|
||||||
|
} catch {
|
||||||
|
return relayUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toAbsoluteHttpUrl(value, baseUrl) {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(value, baseUrl)
|
||||||
|
|
||||||
|
if (parsed.protocol === "http:" || parsed.protocol === "https:") {
|
||||||
|
return parsed.href
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore malformed URLs.
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRequestOrigin(req, requestUrl) {
|
||||||
|
const protocol = firstHeaderValue(req.headers.get("x-forwarded-proto")) || requestUrl.protocol.slice(0, -1)
|
||||||
|
const host =
|
||||||
|
firstHeaderValue(req.headers.get("x-forwarded-host")) ||
|
||||||
|
req.headers.get("host") ||
|
||||||
|
requestUrl.host ||
|
||||||
|
"localhost"
|
||||||
|
|
||||||
|
return `${protocol}://${host}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstHeaderValue(value) {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value.split(",")[0].trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePathname(pathname) {
|
||||||
|
const cleanPath = pathname.replace(/\/+/g, "/")
|
||||||
|
|
||||||
|
if (cleanPath === "/") {
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleanPath.replace(/\/+$/, "") || "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPathSegments(pathname) {
|
||||||
|
const normalized = normalizePathname(pathname)
|
||||||
|
|
||||||
|
if (normalized === "/") {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized
|
||||||
|
.slice(1)
|
||||||
|
.split("/")
|
||||||
|
.map(decodeSegment)
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeSegment(value) {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(value)
|
||||||
|
} catch {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clip(text, maxLength) {
|
||||||
|
if (text.length <= maxLength) {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${text.slice(0, maxLength - 1).trimEnd()}…`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCachedValue(cache, key) {
|
||||||
|
const cached = cache.get(key)
|
||||||
|
|
||||||
|
if (!cached) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cached.expiresAt <= Date.now()) {
|
||||||
|
cache.delete(key)
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return cached.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCachedValue(cache, key, value, ttlMs) {
|
||||||
|
cache.set(key, {expiresAt: Date.now() + ttlMs, value})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user