big rewrite (#15)
This commit is contained in:
BIN
.github/README/raw-media.png
vendored
Normal file
BIN
.github/README/raw-media.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 363 KiB |
@@ -10,10 +10,10 @@ Embed Bluesky links in Discord.
|
|||||||
|
|
||||||
#### Simply append `x` at the end of `bsky.app`.
|
#### Simply append `x` at the end of `bsky.app`.
|
||||||
|
|
||||||
## FAQ
|
## Direct Links
|
||||||
|
|
||||||
### [Video is too long to embed] in an embed description.
|
You want to link to a media directly? You can prepend `r.` to the URL to get a direct link.
|
||||||
Due to Discord's and BlueSky limitations, the video cannot be embedded if it exceeds ~30s. You can still view the video by clicking on the `VixBluesky` in the author field.
|

|
||||||
|
|
||||||
## Authors
|
## Authors
|
||||||
|
|
||||||
|
|||||||
49
conf/nginx.conf
Normal file
49
conf/nginx.conf
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# r.bskyx.app
|
||||||
|
server {
|
||||||
|
set $scheme https;
|
||||||
|
set $server "bskyx.app";
|
||||||
|
set $port 80;
|
||||||
|
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
|
||||||
|
listen 443 ssl http2;
|
||||||
|
listen [::]:443 ssl http2;
|
||||||
|
|
||||||
|
|
||||||
|
server_name r.bskyx.app;
|
||||||
|
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/privkey.pem;
|
||||||
|
|
||||||
|
access_log /var/nginx/log/r.bskyx.app-access.log proxy;
|
||||||
|
error_log /var/nginx/log/r.bskyx.app-error.log warn;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
rewrite ^(.*)$ $scheme://bskyx.app$1?direct=true permanent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# api.bskyx.app
|
||||||
|
server {
|
||||||
|
set $scheme http;
|
||||||
|
set $server "localhost";
|
||||||
|
set $port 2598;
|
||||||
|
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
|
||||||
|
listen 443 ssl http2;
|
||||||
|
listen [::]:443 ssl http2;
|
||||||
|
|
||||||
|
|
||||||
|
server_name api.bskyx.app;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/privkey.pem;
|
||||||
|
|
||||||
|
|
||||||
|
access_log /var/nginx/logs/api.bskyx.app-access.log proxy;
|
||||||
|
error_log /var/nginx/logs/api.bskyx.app-error.log warn;
|
||||||
|
}
|
||||||
5
pkgs/app/.prettierrc
Normal file
5
pkgs/app/.prettierrc
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"endOfLine": "lf"
|
||||||
|
}
|
||||||
@@ -4,11 +4,13 @@
|
|||||||
"deploy": "wrangler deploy --minify src/index.ts"
|
"deploy": "wrangler deploy --minify src/index.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atproto/api": "^0.12.24",
|
"@atcute/bluesky": "^1.0.7",
|
||||||
|
"@atcute/client": "^2.0.3",
|
||||||
"hono": "^4.5.1"
|
"hono": "^4.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/workers-types": "^4.20230628.0",
|
"@cloudflare/workers-types": "^4.20230628.0",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
"typescript": "^5.1.6",
|
"typescript": "^5.1.6",
|
||||||
"wrangler": "^3.75.0"
|
"wrangler": "^3.75.0"
|
||||||
}
|
}
|
||||||
|
|||||||
108
pkgs/app/pnpm-lock.yaml
generated
108
pkgs/app/pnpm-lock.yaml
generated
@@ -8,9 +8,12 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@atproto/api':
|
'@atcute/bluesky':
|
||||||
specifier: ^0.12.24
|
specifier: ^1.0.7
|
||||||
version: 0.12.29
|
version: 1.0.7(@atcute/client@2.0.3)
|
||||||
|
'@atcute/client':
|
||||||
|
specifier: ^2.0.3
|
||||||
|
version: 2.0.3
|
||||||
hono:
|
hono:
|
||||||
specifier: ^4.5.1
|
specifier: ^4.5.1
|
||||||
version: 4.5.11
|
version: 4.5.11
|
||||||
@@ -18,6 +21,9 @@ importers:
|
|||||||
'@cloudflare/workers-types':
|
'@cloudflare/workers-types':
|
||||||
specifier: ^4.20230628.0
|
specifier: ^4.20230628.0
|
||||||
version: 4.20230710.1
|
version: 4.20230710.1
|
||||||
|
prettier:
|
||||||
|
specifier: ^3.3.3
|
||||||
|
version: 3.3.3
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.1.6
|
specifier: ^5.1.6
|
||||||
version: 5.1.6
|
version: 5.1.6
|
||||||
@@ -27,20 +33,13 @@ importers:
|
|||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
'@atproto/api@0.12.29':
|
'@atcute/bluesky@1.0.7':
|
||||||
resolution: {integrity: sha512-PyzPLjGWR0qNOMrmj3Nt3N5NuuANSgOk/33Bu3j+rFjjPrHvk9CI6iQPU6zuDaDCoyOTRJRafw8X/aMQw+ilgw==}
|
resolution: {integrity: sha512-2jPHzl7WbcqRtcAXanJy4Lp638ujqnoGmPCPmBlmpEDP34D7EVKQqjN/mlvglb5n539dThA9xlSgIS8yOxwzDA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@atcute/client': ^1.0.0 || ^2.0.0
|
||||||
|
|
||||||
'@atproto/common-web@0.3.0':
|
'@atcute/client@2.0.3':
|
||||||
resolution: {integrity: sha512-67VnV6JJyX+ZWyjV7xFQMypAgDmjVaR9ZCuU/QW+mqlqI7fex2uL4Fv+7/jHadgzhuJHVd6OHOvNn0wR5WZYtA==}
|
resolution: {integrity: sha512-j9GryA5l+4F0BTQWa6/1XmsuSPSq+bqNCY3mrHUGD592hMqUZxgpYDLgRWL+719V287AW/56AwvFYlbjlENp7A==}
|
||||||
|
|
||||||
'@atproto/lexicon@0.4.1':
|
|
||||||
resolution: {integrity: sha512-bzyr+/VHXLQWbumViX5L7h1NKQObfs8Z+XZJl43OUK8nYFUI4e/sW1IZKRNfw7Wvi5YVNK+J+yP3DWIBZhkCYA==}
|
|
||||||
|
|
||||||
'@atproto/syntax@0.3.0':
|
|
||||||
resolution: {integrity: sha512-Weq0ZBxffGHDXHl9U7BQc2BFJi/e23AL+k+i5+D9hUq/bzT4yjGsrCejkjq0xt82xXDjmhhvQSZ0LqxyZ5woxA==}
|
|
||||||
|
|
||||||
'@atproto/xrpc@0.5.0':
|
|
||||||
resolution: {integrity: sha512-swu+wyOLvYW4l3n+VAuJbHcPcES+tin2Lsrp8Bw5aIXIICiuFn1YMFlwK9JwVUzTH21Py1s1nHEjr4CJeElJog==}
|
|
||||||
|
|
||||||
'@cloudflare/kv-asset-handler@0.3.4':
|
'@cloudflare/kv-asset-handler@0.3.4':
|
||||||
resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==}
|
resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==}
|
||||||
@@ -259,9 +258,6 @@ packages:
|
|||||||
as-table@1.0.55:
|
as-table@1.0.55:
|
||||||
resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==}
|
resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==}
|
||||||
|
|
||||||
await-lock@2.2.2:
|
|
||||||
resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==}
|
|
||||||
|
|
||||||
binary-extensions@2.2.0:
|
binary-extensions@2.2.0:
|
||||||
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
|
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -340,9 +336,6 @@ packages:
|
|||||||
glob-to-regexp@0.4.1:
|
glob-to-regexp@0.4.1:
|
||||||
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
|
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
|
||||||
|
|
||||||
graphemer@1.4.0:
|
|
||||||
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
|
|
||||||
|
|
||||||
hasown@2.0.2:
|
hasown@2.0.2:
|
||||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -371,9 +364,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
||||||
engines: {node: '>=0.12.0'}
|
engines: {node: '>=0.12.0'}
|
||||||
|
|
||||||
iso-datestring-validator@2.2.2:
|
|
||||||
resolution: {integrity: sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==}
|
|
||||||
|
|
||||||
magic-string@0.25.9:
|
magic-string@0.25.9:
|
||||||
resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
|
resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
|
||||||
|
|
||||||
@@ -390,9 +380,6 @@ packages:
|
|||||||
ms@2.1.2:
|
ms@2.1.2:
|
||||||
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
|
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
|
||||||
|
|
||||||
multiformats@9.9.0:
|
|
||||||
resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==}
|
|
||||||
|
|
||||||
mustache@4.2.0:
|
mustache@4.2.0:
|
||||||
resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==}
|
resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -426,6 +413,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||||
engines: {node: '>=8.6'}
|
engines: {node: '>=8.6'}
|
||||||
|
|
||||||
|
prettier@3.3.3:
|
||||||
|
resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
printable-characters@1.0.42:
|
printable-characters@1.0.42:
|
||||||
resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==}
|
resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==}
|
||||||
|
|
||||||
@@ -474,10 +466,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
tlds@1.240.0:
|
|
||||||
resolution: {integrity: sha512-1OYJQenswGZSOdRw7Bql5Qu7uf75b+F3HFBXbqnG/ifHa0fev1XcG+3pJf3pA/KC6RtHQzfKgIf1vkMlMG7mtQ==}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
to-regex-range@5.0.1:
|
to-regex-range@5.0.1:
|
||||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||||
engines: {node: '>=8.0'}
|
engines: {node: '>=8.0'}
|
||||||
@@ -493,9 +481,6 @@ packages:
|
|||||||
ufo@1.5.4:
|
ufo@1.5.4:
|
||||||
resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==}
|
resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==}
|
||||||
|
|
||||||
uint8arrays@3.0.0:
|
|
||||||
resolution: {integrity: sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==}
|
|
||||||
|
|
||||||
undici@5.28.4:
|
undici@5.28.4:
|
||||||
resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==}
|
resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==}
|
||||||
engines: {node: '>=14.0'}
|
engines: {node: '>=14.0'}
|
||||||
@@ -536,45 +521,16 @@ packages:
|
|||||||
youch@3.2.3:
|
youch@3.2.3:
|
||||||
resolution: {integrity: sha512-ZBcWz/uzZaQVdCvfV4uk616Bbpf2ee+F/AvuKDR5EwX/Y4v06xWdtMluqTD7+KlZdM93lLm9gMZYo0sKBS0pgw==}
|
resolution: {integrity: sha512-ZBcWz/uzZaQVdCvfV4uk616Bbpf2ee+F/AvuKDR5EwX/Y4v06xWdtMluqTD7+KlZdM93lLm9gMZYo0sKBS0pgw==}
|
||||||
|
|
||||||
zod@3.21.4:
|
|
||||||
resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==}
|
|
||||||
|
|
||||||
zod@3.23.8:
|
zod@3.23.8:
|
||||||
resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
|
resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@atproto/api@0.12.29':
|
'@atcute/bluesky@1.0.7(@atcute/client@2.0.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@atproto/common-web': 0.3.0
|
'@atcute/client': 2.0.3
|
||||||
'@atproto/lexicon': 0.4.1
|
|
||||||
'@atproto/syntax': 0.3.0
|
|
||||||
'@atproto/xrpc': 0.5.0
|
|
||||||
await-lock: 2.2.2
|
|
||||||
multiformats: 9.9.0
|
|
||||||
tlds: 1.240.0
|
|
||||||
|
|
||||||
'@atproto/common-web@0.3.0':
|
'@atcute/client@2.0.3': {}
|
||||||
dependencies:
|
|
||||||
graphemer: 1.4.0
|
|
||||||
multiformats: 9.9.0
|
|
||||||
uint8arrays: 3.0.0
|
|
||||||
zod: 3.21.4
|
|
||||||
|
|
||||||
'@atproto/lexicon@0.4.1':
|
|
||||||
dependencies:
|
|
||||||
'@atproto/common-web': 0.3.0
|
|
||||||
'@atproto/syntax': 0.3.0
|
|
||||||
iso-datestring-validator: 2.2.2
|
|
||||||
multiformats: 9.9.0
|
|
||||||
zod: 3.23.8
|
|
||||||
|
|
||||||
'@atproto/syntax@0.3.0': {}
|
|
||||||
|
|
||||||
'@atproto/xrpc@0.5.0':
|
|
||||||
dependencies:
|
|
||||||
'@atproto/lexicon': 0.4.1
|
|
||||||
zod: 3.21.4
|
|
||||||
|
|
||||||
'@cloudflare/kv-asset-handler@0.3.4':
|
'@cloudflare/kv-asset-handler@0.3.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -703,8 +659,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
printable-characters: 1.0.42
|
printable-characters: 1.0.42
|
||||||
|
|
||||||
await-lock@2.2.2: {}
|
|
||||||
|
|
||||||
binary-extensions@2.2.0: {}
|
binary-extensions@2.2.0: {}
|
||||||
|
|
||||||
blake3-wasm@2.1.5: {}
|
blake3-wasm@2.1.5: {}
|
||||||
@@ -795,8 +749,6 @@ snapshots:
|
|||||||
|
|
||||||
glob-to-regexp@0.4.1: {}
|
glob-to-regexp@0.4.1: {}
|
||||||
|
|
||||||
graphemer@1.4.0: {}
|
|
||||||
|
|
||||||
hasown@2.0.2:
|
hasown@2.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.2
|
function-bind: 1.1.2
|
||||||
@@ -819,8 +771,6 @@ snapshots:
|
|||||||
|
|
||||||
is-number@7.0.0: {}
|
is-number@7.0.0: {}
|
||||||
|
|
||||||
iso-datestring-validator@2.2.2: {}
|
|
||||||
|
|
||||||
magic-string@0.25.9:
|
magic-string@0.25.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
sourcemap-codec: 1.4.8
|
sourcemap-codec: 1.4.8
|
||||||
@@ -848,8 +798,6 @@ snapshots:
|
|||||||
|
|
||||||
ms@2.1.2: {}
|
ms@2.1.2: {}
|
||||||
|
|
||||||
multiformats@9.9.0: {}
|
|
||||||
|
|
||||||
mustache@4.2.0: {}
|
mustache@4.2.0: {}
|
||||||
|
|
||||||
nanoid@3.3.6: {}
|
nanoid@3.3.6: {}
|
||||||
@@ -868,6 +816,8 @@ snapshots:
|
|||||||
|
|
||||||
picomatch@2.3.1: {}
|
picomatch@2.3.1: {}
|
||||||
|
|
||||||
|
prettier@3.3.3: {}
|
||||||
|
|
||||||
printable-characters@1.0.42: {}
|
printable-characters@1.0.42: {}
|
||||||
|
|
||||||
readdirp@3.6.0:
|
readdirp@3.6.0:
|
||||||
@@ -913,8 +863,6 @@ snapshots:
|
|||||||
|
|
||||||
supports-preserve-symlinks-flag@1.0.0: {}
|
supports-preserve-symlinks-flag@1.0.0: {}
|
||||||
|
|
||||||
tlds@1.240.0: {}
|
|
||||||
|
|
||||||
to-regex-range@5.0.1:
|
to-regex-range@5.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-number: 7.0.0
|
is-number: 7.0.0
|
||||||
@@ -925,10 +873,6 @@ snapshots:
|
|||||||
|
|
||||||
ufo@1.5.4: {}
|
ufo@1.5.4: {}
|
||||||
|
|
||||||
uint8arrays@3.0.0:
|
|
||||||
dependencies:
|
|
||||||
multiformats: 9.9.0
|
|
||||||
|
|
||||||
undici@5.28.4:
|
undici@5.28.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@fastify/busboy': 2.1.1
|
'@fastify/busboy': 2.1.1
|
||||||
@@ -986,6 +930,4 @@ snapshots:
|
|||||||
mustache: 4.2.0
|
mustache: 4.2.0
|
||||||
stacktracey: 2.1.8
|
stacktracey: 2.1.8
|
||||||
|
|
||||||
zod@3.21.4: {}
|
|
||||||
|
|
||||||
zod@3.23.8: {}
|
zod@3.23.8: {}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { html } from "hono/html";
|
import { html } from 'hono/html';
|
||||||
|
|
||||||
export interface LayoutProps {
|
export interface LayoutProps {
|
||||||
url: string;
|
url: string;
|
||||||
@@ -7,11 +7,11 @@ export interface LayoutProps {
|
|||||||
|
|
||||||
export const Layout = ({ url, children }: LayoutProps) => {
|
export const Layout = ({ url, children }: LayoutProps) => {
|
||||||
const removeLeadingSlash = url.substring(1);
|
const removeLeadingSlash = url.substring(1);
|
||||||
const redirectUrl = removeLeadingSlash.startsWith("https://")
|
const redirectUrl = removeLeadingSlash.startsWith('https://')
|
||||||
? removeLeadingSlash
|
? removeLeadingSlash
|
||||||
: `https://bsky.app/${removeLeadingSlash}`;
|
: `https://bsky.app/${removeLeadingSlash}`;
|
||||||
return html`
|
return html`
|
||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<link rel="canonical" href="${url.substring(1)}" />
|
<link rel="canonical" href="${url.substring(1)}" />
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
import { AppBskyEmbedImages, AppBskyFeedDefs } from "@atproto/api";
|
import { Layout } from './Layout';
|
||||||
|
import { OEmbedTypes } from '../routes/getOEmbed';
|
||||||
import { Layout } from "./Layout";
|
import { parseEmbedDescription } from '../lib/parseEmbedDescription';
|
||||||
import { OEmbedTypes } from "../routes/getOEmbed";
|
import { checkType } from '../lib/utils';
|
||||||
import { parseEmbedImages } from "../lib/parseEmbedImages";
|
import { VideoInfo } from '../routes/getPost';
|
||||||
import { parseEmbedDescription } from "../lib/parseEmbedDescription";
|
import { AppBskyEmbedImages, AppBskyFeedDefs } from '@atcute/client/lexicons';
|
||||||
import { StreamInfo } from "../lib/processVideoEmbed";
|
|
||||||
import { checkType, join } from "../lib/utils";
|
|
||||||
|
|
||||||
interface PostProps {
|
interface PostProps {
|
||||||
post: AppBskyFeedDefs.PostView;
|
post: AppBskyFeedDefs.PostView;
|
||||||
url: string;
|
url: string;
|
||||||
appDomain: string;
|
appDomain: string;
|
||||||
videoMetadata?: StreamInfo[] | undefined;
|
videoMetadata?: VideoInfo;
|
||||||
apiUrl: string;
|
apiUrl: string;
|
||||||
|
images: string | AppBskyEmbedImages.ViewImage[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const Meta = ({ post }: { post: AppBskyFeedDefs.PostView }) => (
|
const Meta = ({ post }: { post: AppBskyFeedDefs.PostView }) => (
|
||||||
@@ -21,16 +20,6 @@ const Meta = ({ post }: { post: AppBskyFeedDefs.PostView }) => (
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const constructVideoUrl = (streamInfo: StreamInfo, apiUrl: string) => {
|
|
||||||
const url = new URL(streamInfo.masterUri);
|
|
||||||
|
|
||||||
const [did, id, quality] = url.pathname.split("/").slice(2);
|
|
||||||
|
|
||||||
const parts = [did, id, quality];
|
|
||||||
|
|
||||||
return `${apiUrl}generate/${btoa(join(parts, ";"))}.mp4`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Video = ({
|
const Video = ({
|
||||||
streamInfo,
|
streamInfo,
|
||||||
apiUrl,
|
apiUrl,
|
||||||
@@ -38,13 +27,13 @@ const Video = ({
|
|||||||
post,
|
post,
|
||||||
description,
|
description,
|
||||||
}: {
|
}: {
|
||||||
streamInfo: StreamInfo;
|
streamInfo: VideoInfo;
|
||||||
apiUrl: string;
|
apiUrl: string;
|
||||||
appDomain: string;
|
appDomain: string;
|
||||||
post: AppBskyFeedDefs.PostView;
|
post: AppBskyFeedDefs.PostView;
|
||||||
description: string;
|
description: string;
|
||||||
}) => {
|
}) => {
|
||||||
const url = constructVideoUrl(streamInfo, apiUrl);
|
const url = streamInfo.url.toString();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -57,19 +46,19 @@ const Video = ({
|
|||||||
<meta property="og:video:type" content="video/mp4" />
|
<meta property="og:video:type" content="video/mp4" />
|
||||||
<meta
|
<meta
|
||||||
property="og:video:width"
|
property="og:video:width"
|
||||||
content={streamInfo.resolution.width.toString()}
|
content={streamInfo.aspectRatio.width.toString()}
|
||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
property="og:video:height"
|
property="og:video:height"
|
||||||
content={streamInfo.resolution.height.toString()}
|
content={streamInfo.aspectRatio.height.toString()}
|
||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
property="twitter:player:width"
|
property="twitter:player:width"
|
||||||
content={streamInfo.resolution.width.toString()}
|
content={streamInfo.aspectRatio.width.toString()}
|
||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
property="twitter:player:height"
|
property="twitter:player:height"
|
||||||
content={streamInfo.resolution.height.toString()}
|
content={streamInfo.aspectRatio.height.toString()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<link
|
<link
|
||||||
@@ -80,7 +69,7 @@ const Video = ({
|
|||||||
}&reposts=${post.repostCount}&likes=${
|
}&reposts=${post.repostCount}&likes=${
|
||||||
post.likeCount
|
post.likeCount
|
||||||
}&avatar=${encodeURIComponent(
|
}&avatar=${encodeURIComponent(
|
||||||
post.author.avatar ?? ""
|
post.author.avatar ?? '',
|
||||||
)}&description=${encodeURIComponent(description)}`}
|
)}&description=${encodeURIComponent(description)}`}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@@ -93,7 +82,7 @@ const Images = ({
|
|||||||
images: AppBskyEmbedImages.ViewImage[] | string;
|
images: AppBskyEmbedImages.ViewImage[] | string;
|
||||||
}) => (
|
}) => (
|
||||||
<>
|
<>
|
||||||
{typeof images === "string" ? (
|
{typeof images === 'string' ? (
|
||||||
<>
|
<>
|
||||||
<meta property="og:image" content={images} />
|
<meta property="og:image" content={images} />
|
||||||
<meta property="twitter:image" content={images} />
|
<meta property="twitter:image" content={images} />
|
||||||
@@ -115,25 +104,18 @@ export const Post = ({
|
|||||||
appDomain,
|
appDomain,
|
||||||
videoMetadata,
|
videoMetadata,
|
||||||
apiUrl,
|
apiUrl,
|
||||||
|
images,
|
||||||
}: PostProps) => {
|
}: PostProps) => {
|
||||||
const images = parseEmbedImages(post);
|
|
||||||
const isAuthor = images === post.author.avatar;
|
const isAuthor = images === post.author.avatar;
|
||||||
let description = parseEmbedDescription(post);
|
let description = parseEmbedDescription(post);
|
||||||
const isVideo = checkType(
|
const isVideo = checkType(
|
||||||
"app.bsky.embed.video",
|
'app.bsky.embed.video',
|
||||||
post.embed?.media ?? post.embed
|
// @ts-expect-error
|
||||||
|
post.embed?.media ?? post.embed,
|
||||||
);
|
);
|
||||||
const streamInfo = videoMetadata?.at(-1);
|
|
||||||
const isTooLong = isVideo && streamInfo!.uri.length > 4;
|
|
||||||
const shouldOverrideForVideo = isVideo && isTooLong;
|
|
||||||
|
|
||||||
let videoUrl;
|
let videoUrl;
|
||||||
|
|
||||||
if (isVideo && isTooLong) {
|
|
||||||
videoUrl = constructVideoUrl(streamInfo!, apiUrl);
|
|
||||||
description += `\n[Video is too long to embed!]`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout url={url}>
|
<Layout url={url}>
|
||||||
<meta name="twitter:creator" content={`@${post.author.handle}`} />
|
<meta name="twitter:creator" content={`@${post.author.handle}`} />
|
||||||
@@ -151,21 +133,19 @@ export const Post = ({
|
|||||||
|
|
||||||
{!isAuthor && <Meta post={post} />}
|
{!isAuthor && <Meta post={post} />}
|
||||||
|
|
||||||
{images.length !== 0 && (shouldOverrideForVideo || !isVideo) && (
|
{images.length !== 0 && !isVideo && <Images images={images} />}
|
||||||
<Images images={images} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isVideo && streamInfo!.uri.length <= 4 && (
|
{isVideo && (
|
||||||
<Video
|
<Video
|
||||||
apiUrl={apiUrl}
|
apiUrl={apiUrl}
|
||||||
streamInfo={streamInfo!}
|
streamInfo={videoMetadata!}
|
||||||
appDomain={appDomain}
|
appDomain={appDomain}
|
||||||
description={description}
|
description={description}
|
||||||
post={post}
|
post={post}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(shouldOverrideForVideo || !isVideo) && (
|
{!isVideo && (
|
||||||
<link
|
<link
|
||||||
rel="alternate"
|
rel="alternate"
|
||||||
type="application/json+oembed"
|
type="application/json+oembed"
|
||||||
@@ -174,10 +154,8 @@ export const Post = ({
|
|||||||
}&reposts=${post.repostCount}&likes=${
|
}&reposts=${post.repostCount}&likes=${
|
||||||
post.likeCount
|
post.likeCount
|
||||||
}&avatar=${encodeURIComponent(
|
}&avatar=${encodeURIComponent(
|
||||||
post.author.avatar ?? ""
|
post.author.avatar ?? '',
|
||||||
)}&description=${encodeURIComponent(description)}${
|
)}&description=${encodeURIComponent(description)}`}
|
||||||
videoUrl ? `&videoUrl=${encodeURIComponent(videoUrl)}` : ""
|
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { AppBskyActorDefs } from "@atproto/api";
|
import { Layout } from './Layout';
|
||||||
|
import { OEmbedTypes } from '../routes/getOEmbed';
|
||||||
import { Layout } from "./Layout";
|
import { AppBskyActorDefs } from '@atcute/client/lexicons';
|
||||||
import { OEmbedTypes } from "../routes/getOEmbed";
|
|
||||||
|
|
||||||
interface ProfileProps {
|
interface ProfileProps {
|
||||||
profile: AppBskyActorDefs.ProfileViewDetailed;
|
profile: AppBskyActorDefs.ProfileViewDetailed;
|
||||||
@@ -13,7 +12,7 @@ export const Profile = ({ profile, url, appDomain }: ProfileProps) => (
|
|||||||
<Layout url={url}>
|
<Layout url={url}>
|
||||||
<meta name="og:type" content="article" />
|
<meta name="og:type" content="article" />
|
||||||
<meta name="twitter:creator" content={`@${profile.handle}`} />
|
<meta name="twitter:creator" content={`@${profile.handle}`} />
|
||||||
<meta property="og:description" content={profile.description ?? ""} />
|
<meta property="og:description" content={profile.description ?? ''} />
|
||||||
<meta
|
<meta
|
||||||
property="og:title"
|
property="og:title"
|
||||||
content={`${profile.displayName} (@${profile.handle})`}
|
content={`${profile.displayName} (@${profile.handle})`}
|
||||||
@@ -26,7 +25,7 @@ export const Profile = ({ profile, url, appDomain }: ProfileProps) => (
|
|||||||
href={`https://${appDomain}/oembed?type=${OEmbedTypes.Profile}&follows=${
|
href={`https://${appDomain}/oembed?type=${OEmbedTypes.Profile}&follows=${
|
||||||
profile.followsCount
|
profile.followsCount
|
||||||
}&posts=${profile.postsCount}&avatar=${encodeURIComponent(
|
}&posts=${profile.postsCount}&avatar=${encodeURIComponent(
|
||||||
profile.avatar ?? ""
|
profile.avatar ?? '',
|
||||||
)}`}
|
)}`}
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
6
pkgs/app/src/globals.d.ts
vendored
6
pkgs/app/src/globals.d.ts
vendored
@@ -1,5 +1,5 @@
|
|||||||
import { BskyAgent } from "@atproto/api";
|
import { XRPC } from '@atcute/client';
|
||||||
import type { KVNamespace } from "@cloudflare/workers-types";
|
import type { KVNamespace } from '@cloudflare/workers-types';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Env {
|
interface Env {
|
||||||
@@ -12,7 +12,7 @@ declare global {
|
|||||||
bskyx: KVNamespace;
|
bskyx: KVNamespace;
|
||||||
};
|
};
|
||||||
Variables: {
|
Variables: {
|
||||||
Agent: BskyAgent;
|
Agent: XRPC;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,43 @@
|
|||||||
import { Hono } from "hono";
|
import { Hono } from 'hono';
|
||||||
import { AtpSessionData, BskyAgent } from "@atproto/api";
|
import { XRPC, CredentialManager, AtpSessionData } from '@atcute/client';
|
||||||
import { getPost } from "./routes/getPost";
|
import '@atcute/bluesky/lexicons';
|
||||||
import { getPostData } from "./routes/getPostData";
|
import { getPost } from './routes/getPost';
|
||||||
import { getOEmbed } from "./routes/getOEmbed";
|
import { getPostData } from './routes/getPostData';
|
||||||
import { getProfileData } from "./routes/getProfileData";
|
import { getOEmbed } from './routes/getOEmbed';
|
||||||
import { getProfile } from "./routes/getProfile";
|
import { getProfileData } from './routes/getProfileData';
|
||||||
import { HTTPException } from "hono/http-exception";
|
import { getProfile } from './routes/getProfile';
|
||||||
|
import { HTTPException } from 'hono/http-exception';
|
||||||
|
|
||||||
const app = new Hono<Env>();
|
const app = new Hono<Env>();
|
||||||
|
|
||||||
app.use("*", async (c, next) => {
|
app.use('*', async (c, next) => {
|
||||||
const agent = new BskyAgent({
|
const creds = new CredentialManager({
|
||||||
service: c.env.BSKY_SERVICE_URL,
|
service: c.env.BSKY_SERVICE_URL,
|
||||||
async persistSession(_, session) {
|
onRefresh(session) {
|
||||||
if (session) {
|
return c.env.bskyx.put('session', JSON.stringify(session));
|
||||||
return c.env.bskyx.put("session", JSON.stringify(session));
|
},
|
||||||
}
|
onExpired(session) {
|
||||||
|
return c.env.bskyx.delete('session');
|
||||||
|
},
|
||||||
|
onSessionUpdate(session) {
|
||||||
|
return c.env.bskyx.put('session', JSON.stringify(session));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const agent = new XRPC({ handler: creds });
|
||||||
try {
|
try {
|
||||||
const rawSession = await c.env.bskyx.get("session");
|
const rawSession = await c.env.bskyx.get('session');
|
||||||
if (rawSession) {
|
if (rawSession) {
|
||||||
const session = JSON.parse(rawSession) as AtpSessionData;
|
const session = JSON.parse(rawSession) as AtpSessionData;
|
||||||
await agent.resumeSession(session);
|
await creds.resume(session);
|
||||||
} else {
|
} else {
|
||||||
await agent.login({
|
await creds.login({
|
||||||
identifier: c.env.BSKY_AUTH_USERNAME,
|
identifier: c.env.BSKY_AUTH_USERNAME,
|
||||||
password: c.env.BSKY_AUTH_PASSWORD,
|
password: c.env.BSKY_AUTH_PASSWORD,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
c.set("Agent", agent);
|
c.set('Agent', agent);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = new Error("Failed to login to Bluesky!", {
|
const err = new Error('Failed to login to Bluesky!', {
|
||||||
cause: error,
|
cause: error,
|
||||||
});
|
});
|
||||||
throw new HTTPException(500, {
|
throw new HTTPException(500, {
|
||||||
@@ -41,22 +47,22 @@ app.use("*", async (c, next) => {
|
|||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/", async (c) => {
|
app.get('/', async (c) => {
|
||||||
return c.redirect("https://github.com/Rapougnac/VixBluesky");
|
return c.redirect('https://github.com/Rapougnac/VixBluesky');
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/profile/:user/post/:post", getPost);
|
app.get('/profile/:user/post/:post', getPost);
|
||||||
app.get("/https://bsky.app/profile/:user/post/:post", getPost);
|
app.get('/https://bsky.app/profile/:user/post/:post', getPost);
|
||||||
|
|
||||||
app.get("/profile/:user/post/:post/json", getPostData);
|
app.get('/profile/:user/post/:post/json', getPostData);
|
||||||
app.get("/https://bsky.app/profile/:user/post/:post/json", getPostData);
|
app.get('/https://bsky.app/profile/:user/post/:post/json', getPostData);
|
||||||
|
|
||||||
app.get("/profile/:user", getProfile);
|
app.get('/profile/:user', getProfile);
|
||||||
app.get("/https://bsky.app/profile/:user", getProfile);
|
app.get('/https://bsky.app/profile/:user', getProfile);
|
||||||
|
|
||||||
app.get("/profile/:user/json", getProfileData);
|
app.get('/profile/:user/json', getProfileData);
|
||||||
app.get("/https://bsky.app/profile/:user/json", getProfileData);
|
app.get('/https://bsky.app/profile/:user/json', getProfileData);
|
||||||
|
|
||||||
app.get("/oembed", getOEmbed);
|
app.get('/oembed', getOEmbed);
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
import { BskyAgent } from "@atproto/api";
|
import { fetchProfile } from './fetchProfile';
|
||||||
|
import { XRPC } from '@atcute/client';
|
||||||
|
|
||||||
export interface fetchPostOptions {
|
export interface FetchPostOptions {
|
||||||
user: string;
|
user: string;
|
||||||
post: string;
|
post: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchPost(
|
export async function fetchPost(agent: XRPC, { user, post }: FetchPostOptions) {
|
||||||
agent: BskyAgent,
|
const { data: userData } = await fetchProfile(agent, { user });
|
||||||
{ user, post }: fetchPostOptions
|
return agent.get('app.bsky.feed.getPosts', {
|
||||||
) {
|
params: { uris: [`at://${userData.did}/app.bsky.feed.post/${post}`] },
|
||||||
const { data: userData } = await agent.getProfile({
|
|
||||||
actor: user,
|
|
||||||
});
|
|
||||||
return agent.getPosts({
|
|
||||||
uris: [`at://${userData.did}/app.bsky.feed.post/${post}`],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
import { BskyAgent } from "@atproto/api";
|
import { XRPC } from '@atcute/client';
|
||||||
|
|
||||||
export interface fetchProfileOptions {
|
export interface FetchProfileOptions {
|
||||||
user: string;
|
user: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchProfile(
|
export async function fetchProfile(agent: XRPC, { user }: FetchProfileOptions) {
|
||||||
agent: BskyAgent,
|
return agent.get('app.bsky.actor.getProfile', { params: { actor: user } });
|
||||||
{ user }: fetchProfileOptions
|
|
||||||
) {
|
|
||||||
return agent.getProfile({
|
|
||||||
actor: user,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,18 @@
|
|||||||
import {
|
import { AppBskyFeedDefs } from '@atcute/client/lexicons';
|
||||||
AppBskyEmbedRecord,
|
import { checkType, indent } from './utils';
|
||||||
AppBskyEmbedRecordWithMedia,
|
|
||||||
AppBskyFeedDefs,
|
|
||||||
AppBskyFeedPost,
|
|
||||||
} from "@atproto/api";
|
|
||||||
import { indent } from "./utils";
|
|
||||||
|
|
||||||
export function parseEmbedDescription(post: AppBskyFeedDefs.PostView) {
|
export function parseEmbedDescription(post: AppBskyFeedDefs.PostView): string {
|
||||||
if (AppBskyFeedPost.isRecord(post.record)) {
|
const isQuote =
|
||||||
if (AppBskyEmbedRecord.isView(post.embed)) {
|
checkType('app.bsky.feed.post', post.record) &&
|
||||||
const { success: isView } = AppBskyEmbedRecord.validateView(post.embed);
|
(checkType('app.bsky.embed.record#view', post.embed) ||
|
||||||
if (isView && AppBskyEmbedRecord.isViewRecord(post.embed.record)) {
|
checkType('app.bsky.embed.recordWithMedia#view', post.embed));
|
||||||
const { success: isViewRecord } = AppBskyEmbedRecord.validateViewRecord(
|
|
||||||
post.embed.record
|
// @ts-expect-error
|
||||||
);
|
const embed = post.embed.record?.record ?? post.embed.record;
|
||||||
if (isViewRecord) {
|
|
||||||
// @ts-expect-error For some reason the original post value is typed as {}
|
return isQuote
|
||||||
return `${post.record.text}\n\nQuoting @${post.embed.record.author.handle}\n➥${indent(post.embed.record.value.text, 2)}`;
|
? // @ts-expect-error
|
||||||
}
|
`${post.record.text}\n\nQuoting @${embed.author.handle}\n➥${indent(embed.value.text, 2)}`
|
||||||
}
|
: // @ts-expect-error
|
||||||
}
|
post.record.text;
|
||||||
if (AppBskyEmbedRecordWithMedia.isView(post.embed)) {
|
|
||||||
const { success: isView } = AppBskyEmbedRecordWithMedia.validateView(
|
|
||||||
post.embed
|
|
||||||
);
|
|
||||||
if (isView && AppBskyEmbedRecord.isViewRecord(post.embed.record.record)) {
|
|
||||||
const { success: isViewRecord } = AppBskyEmbedRecord.validateViewRecord(
|
|
||||||
post.embed.record.record
|
|
||||||
);
|
|
||||||
if (isViewRecord) {
|
|
||||||
// @ts-expect-error For some reason the original post value is typed as {}
|
|
||||||
return `${post.record.text}\n\nQuoting @${post.embed.record.record.author.handle}\n➥${indent(post.embed.record.record.value.text, 2)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return post.record.text;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +1,50 @@
|
|||||||
import {
|
import { AppBskyFeedDefs, AppBskyEmbedImages } from '@atcute/client/lexicons';
|
||||||
AppBskyEmbedImages,
|
import { checkType } from './utils';
|
||||||
AppBskyEmbedRecord,
|
|
||||||
AppBskyEmbedRecordWithMedia,
|
|
||||||
AppBskyFeedDefs,
|
|
||||||
} from "@atproto/api";
|
|
||||||
|
|
||||||
export function parseEmbedImages(
|
export function parseEmbedImages(
|
||||||
post: AppBskyFeedDefs.PostView
|
post: AppBskyFeedDefs.PostView,
|
||||||
): string | AppBskyEmbedImages.ViewImage[] {
|
): string | AppBskyEmbedImages.ViewImage[] {
|
||||||
let images: AppBskyEmbedImages.ViewImage[] = [];
|
let images: AppBskyEmbedImages.ViewImage[] = [];
|
||||||
|
|
||||||
if (AppBskyEmbedRecord.isView(post.embed)) {
|
const embed = post.embed as typeof post.embed & {
|
||||||
const { success: isView } = AppBskyEmbedRecord.validateView(post.embed);
|
record: any;
|
||||||
if (isView && AppBskyEmbedRecord.isViewRecord(post.embed.record)) {
|
media: any;
|
||||||
const { success: isViewRecord } = AppBskyEmbedRecord.validateViewRecord(
|
images: any;
|
||||||
post.embed.record
|
external: any;
|
||||||
);
|
};
|
||||||
|
|
||||||
|
if (checkType('app.bsky.embed.record#view', embed)) {
|
||||||
|
if (checkType('app.bsky.embed.record#viewRecord', embed?.record)) {
|
||||||
if (
|
if (
|
||||||
isViewRecord &&
|
embed?.record.embeds &&
|
||||||
post.embed.record.embeds &&
|
checkType('app.bsky.embed.images#view', embed.record.embeds[0])
|
||||||
AppBskyEmbedImages.isView(post.embed.record.embeds[0])
|
|
||||||
) {
|
) {
|
||||||
const { success: isImageView } = AppBskyEmbedImages.validateView(
|
images = [
|
||||||
post.embed.record.embeds[0]
|
...images,
|
||||||
);
|
...(embed.record.embeds[0].images as AppBskyEmbedImages.ViewImage[]),
|
||||||
if (isImageView) {
|
];
|
||||||
images = [...images, ...post.embed.record.embeds[0].images];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (checkType('app.bsky.embed.recordWithMedia#view', embed)) {
|
||||||
if (AppBskyEmbedRecordWithMedia.isView(post.embed)) {
|
if (checkType('app.bsky.embed.images#view', embed.media)) {
|
||||||
const { success: isView } = AppBskyEmbedRecordWithMedia.validateView(
|
images = [
|
||||||
post.embed
|
...images,
|
||||||
);
|
...(embed.media.images as AppBskyEmbedImages.ViewImage[]),
|
||||||
if (isView && AppBskyEmbedImages.isView(post.embed.media)) {
|
];
|
||||||
const { success: isImageView } = AppBskyEmbedImages.validateView(
|
|
||||||
post.embed.media
|
|
||||||
);
|
|
||||||
if (isImageView) {
|
|
||||||
images = [...images, ...post.embed.media.images];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (checkType('app.bsky.embed.images#view', embed)) {
|
||||||
|
images = [...images, ...embed.images];
|
||||||
}
|
}
|
||||||
if (AppBskyEmbedImages.isView(post.embed)) {
|
|
||||||
const { success: isImageView } = AppBskyEmbedImages.validateView(
|
const isEmptyImages = images.length === 0;
|
||||||
post.embed
|
|
||||||
);
|
if (isEmptyImages) {
|
||||||
if (isImageView) {
|
if (checkType('app.bsky.embed.external#view', embed)) {
|
||||||
images = [...images, ...post.embed.images];
|
return embed.external.uri;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return images.length === 0 ? post.author.avatar ?? "" : images;
|
return isEmptyImages ? (post.author.avatar ?? '') : images;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
import { AppBskyFeedDefs } from "@atproto/api";
|
|
||||||
|
|
||||||
export interface StreamInfo {
|
|
||||||
bandwidth: number;
|
|
||||||
resolution: {
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
};
|
|
||||||
codecs: string;
|
|
||||||
uri: string | string[];
|
|
||||||
masterUri: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface M3U8Data {
|
|
||||||
version: number;
|
|
||||||
streams: StreamInfo[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VideoMedia {
|
|
||||||
$type: `app.bsky.embed.video${string}`;
|
|
||||||
cid: string;
|
|
||||||
playlist: string;
|
|
||||||
thumbnail: string;
|
|
||||||
aspectRatio: {
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function processVideoEmbed(source?: VideoMedia | undefined) {
|
|
||||||
const videoUrl = source?.playlist as string | undefined;
|
|
||||||
|
|
||||||
if (!videoUrl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const contents = await fetch(videoUrl).then((res) => res.text());
|
|
||||||
|
|
||||||
const initalUrl = removeLastPathSegment(videoUrl);
|
|
||||||
|
|
||||||
const { streams } = await parseM3U8(initalUrl, contents);
|
|
||||||
|
|
||||||
return streams;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function parseM3U8(
|
|
||||||
initalUrl: string,
|
|
||||||
contents: string
|
|
||||||
): Promise<M3U8Data> {
|
|
||||||
const lines = contents
|
|
||||||
.split("\n")
|
|
||||||
.map((l) => l.trim())
|
|
||||||
.filter((l) => l.length > 0);
|
|
||||||
let version = 0;
|
|
||||||
const streams: StreamInfo[] = [];
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.startsWith("#EXT-X-VERSION:")) {
|
|
||||||
version = Number(line.split(":")[1]);
|
|
||||||
} else if (line.startsWith("#EXT-X-STREAM-INF:")) {
|
|
||||||
const attribs = line.split(":")[1].split(",");
|
|
||||||
const sInfo: StreamInfo = {
|
|
||||||
bandwidth: 0,
|
|
||||||
resolution: {
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
},
|
|
||||||
codecs: "",
|
|
||||||
uri: "",
|
|
||||||
masterUri: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const attrib of attribs) {
|
|
||||||
const [key, value] = attrib.split("=");
|
|
||||||
switch (key) {
|
|
||||||
case "BANDWIDTH":
|
|
||||||
sInfo.bandwidth = Number(value);
|
|
||||||
break;
|
|
||||||
case "RESOLUTION":
|
|
||||||
const [width, height] = value.split("x").map(Number);
|
|
||||||
sInfo.resolution = { width, height };
|
|
||||||
break;
|
|
||||||
case "CODECS":
|
|
||||||
sInfo.codecs = value.replaceAll('"', "");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
streams.push(sInfo);
|
|
||||||
} else if (line.includes("m3u8")) {
|
|
||||||
const resolvedUrl = `${initalUrl}/${line}`;
|
|
||||||
|
|
||||||
streams.at(-1)!.masterUri = resolvedUrl;
|
|
||||||
|
|
||||||
const cont = await fetch(resolvedUrl).then((res) => res.text());
|
|
||||||
|
|
||||||
const parsed = await parseM3U8(removeLastPathSegment(resolvedUrl), cont);
|
|
||||||
|
|
||||||
streams.at(-1)!.uri = parsed.streams.map((s) => s.uri as string);
|
|
||||||
} else if (line.includes(".ts")) {
|
|
||||||
streams.push({
|
|
||||||
bandwidth: 0,
|
|
||||||
resolution: {
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
},
|
|
||||||
codecs: "",
|
|
||||||
uri: `${initalUrl}/${line}`,
|
|
||||||
masterUri: initalUrl,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Discard
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { version, streams };
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeLastPathSegment(url: string) {
|
|
||||||
return url.slice(0, url.lastIndexOf("/"));
|
|
||||||
}
|
|
||||||
@@ -2,20 +2,24 @@ export const concatQueryParams = (params: Record<string, string | string[]>) =>
|
|||||||
Object.entries(params)
|
Object.entries(params)
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return value.map((v) => `${key}=${v}`).join("&");
|
return value.map((v) => `${key}=${v}`).join('&');
|
||||||
}
|
}
|
||||||
return `${key}=${value}`;
|
return `${key}=${value}`;
|
||||||
})
|
})
|
||||||
.join("&");
|
.join('&');
|
||||||
|
|
||||||
export const join = (t: string | string[], s: string) =>
|
export const join = (t: string | string[], s: string) =>
|
||||||
Array.isArray(t) ? t.join(s) : t;
|
Array.isArray(t) ? t.join(s) : t;
|
||||||
|
|
||||||
export const checkType = (t: string, o: any) =>
|
export const checkType = (t: string, o: any): boolean =>
|
||||||
(typeof o?.$type === "string" && o?.$type.startsWith(t)) || o?.$type === t;
|
typeof o?.$type === 'string' && (o?.$type === t || o?.$type.startsWith(t));
|
||||||
|
|
||||||
export const indent = (s: string, n: number) =>
|
export const indent = (s: string, n: number) =>
|
||||||
s
|
s
|
||||||
.split("\n")
|
.split('\n')
|
||||||
.map((l) => " ".repeat(n) + l)
|
.map((l) => ' '.repeat(n) + l)
|
||||||
.join("\n");
|
.join('\n');
|
||||||
|
|
||||||
|
export function isObj(v: unknown): v is Record<string, unknown> {
|
||||||
|
return typeof v === 'object' && v !== null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Handler } from "hono";
|
import { Handler } from 'hono';
|
||||||
|
|
||||||
export enum OEmbedTypes {
|
export enum OEmbedTypes {
|
||||||
Post = 1,
|
Post = 1,
|
||||||
@@ -6,13 +6,13 @@ export enum OEmbedTypes {
|
|||||||
Video,
|
Video,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getOEmbed: Handler<Env, "/oembed"> = async (c) => {
|
export const getOEmbed: Handler<Env, '/oembed'> = async (c) => {
|
||||||
const type = +(c.req.query("type") ?? 0);
|
const type = +(c.req.query('type') ?? 0);
|
||||||
const avatar = c.req.query("avatar");
|
const avatar = c.req.query('avatar');
|
||||||
|
|
||||||
const defaults = {
|
const defaults = {
|
||||||
provider_name: "VixBluesky",
|
provider_name: 'VixBluesky',
|
||||||
provider_url: "https://bskyx.app/",
|
provider_url: 'https://bskyx.app/',
|
||||||
thumbnail_width: 1000,
|
thumbnail_width: 1000,
|
||||||
thumbnail_height: 1000,
|
thumbnail_height: 1000,
|
||||||
};
|
};
|
||||||
@@ -23,12 +23,11 @@ export const getOEmbed: Handler<Env, "/oembed"> = async (c) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type === OEmbedTypes.Post) {
|
if (type === OEmbedTypes.Post) {
|
||||||
const { replies, reposts, likes, videoUrl } = c.req.query();
|
const { replies, reposts, likes } = c.req.query();
|
||||||
|
|
||||||
return c.json({
|
return c.json({
|
||||||
...defaults,
|
...defaults,
|
||||||
author_name: `🗨️ ${replies} ♻️ ${reposts} 💙 ${likes}`,
|
author_name: `🗨️ ${replies} ♻️ ${reposts} 💙 ${likes}`,
|
||||||
provider_url: videoUrl ? videoUrl : defaults.provider_url,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (type === OEmbedTypes.Profile) {
|
if (type === OEmbedTypes.Profile) {
|
||||||
|
|||||||
@@ -1,38 +1,68 @@
|
|||||||
import { Handler } from "hono";
|
import { Handler } from 'hono';
|
||||||
import { HTTPException } from "hono/http-exception";
|
import { HTTPException } from 'hono/http-exception';
|
||||||
import { fetchPost } from "../lib/fetchPostData";
|
import { fetchPost } from '../lib/fetchPostData';
|
||||||
import { Post } from "../components/Post";
|
import { Post } from '../components/Post';
|
||||||
import { processVideoEmbed, StreamInfo } from "../lib/processVideoEmbed";
|
import { parseEmbedImages } from '../lib/parseEmbedImages';
|
||||||
import { checkType } from "../lib/utils";
|
import { checkType } from '../lib/utils';
|
||||||
|
import { AppBskyFeedGetPosts } from '@atcute/client/lexicons';
|
||||||
|
|
||||||
|
export interface VideoInfo {
|
||||||
|
url: URL;
|
||||||
|
aspectRatio: {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VideoEmbed {
|
||||||
|
$type: string;
|
||||||
|
cid: string;
|
||||||
|
playlist: string;
|
||||||
|
thumbnail: string;
|
||||||
|
aspectRatio: {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const getPost: Handler<
|
export const getPost: Handler<
|
||||||
Env,
|
Env,
|
||||||
"/profile/:user/post/:post" | "/https://bsky.app/profile/:user/post/:post"
|
'/profile/:user/post/:post' | '/https://bsky.app/profile/:user/post/:post'
|
||||||
> = async (c) => {
|
> = async (c) => {
|
||||||
const { user, post } = c.req.param();
|
const { user, post } = c.req.param();
|
||||||
const agent = c.get("Agent");
|
const isDirect = c.req.query('direct');
|
||||||
const { data, success } = await fetchPost(agent, { user, post });
|
|
||||||
|
|
||||||
if (!success) {
|
const agent = c.get('Agent');
|
||||||
|
try {
|
||||||
|
var { data } = await fetchPost(agent, { user, post });
|
||||||
|
} catch (e) {
|
||||||
throw new HTTPException(500, {
|
throw new HTTPException(500, {
|
||||||
message: "Failed to fetch the post!",
|
message: `Failed to fetch the post!\n${e}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchedPost = data.posts[0];
|
const fetchedPost = data.posts[0];
|
||||||
|
|
||||||
let videoMetaData: StreamInfo[] | undefined;
|
const images = parseEmbedImages(fetchedPost);
|
||||||
|
|
||||||
|
let videoMetaData: VideoInfo | undefined;
|
||||||
|
|
||||||
|
const embed = fetchedPost.embed as typeof fetchedPost.embed & { media: any };
|
||||||
|
|
||||||
if (
|
if (
|
||||||
checkType("app.bsky.embed.video", fetchedPost.embed) ||
|
checkType('app.bsky.embed.video', embed) ||
|
||||||
checkType("app.bsky.embed.video", fetchedPost.embed?.media)
|
checkType('app.bsky.embed.video', embed?.media)
|
||||||
) {
|
) {
|
||||||
videoMetaData = await processVideoEmbed(
|
const videoEmbed = (embed?.media ?? fetchedPost.embed) as VideoEmbed;
|
||||||
// @ts-expect-error
|
videoMetaData = {
|
||||||
fetchedPost.embed?.media ?? fetchedPost.embed
|
url: new URL(
|
||||||
);
|
`https://bsky.social/xrpc/com.atproto.sync.getBlob?cid=${videoEmbed.cid}&did=${fetchedPost.author.did}`,
|
||||||
|
),
|
||||||
|
aspectRatio: videoEmbed.aspectRatio,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isDirect) {
|
||||||
return c.html(
|
return c.html(
|
||||||
<Post
|
<Post
|
||||||
post={fetchedPost}
|
post={fetchedPost}
|
||||||
@@ -40,6 +70,17 @@ export const getPost: Handler<
|
|||||||
appDomain={c.env.VIXBLUESKY_APP_DOMAIN}
|
appDomain={c.env.VIXBLUESKY_APP_DOMAIN}
|
||||||
videoMetadata={videoMetaData}
|
videoMetadata={videoMetaData}
|
||||||
apiUrl={c.env.VIXBLUESKY_API_URL}
|
apiUrl={c.env.VIXBLUESKY_API_URL}
|
||||||
/>
|
images={images}
|
||||||
|
/>,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(images) && images.length !== 0) {
|
||||||
|
const url = images[0].fullsize;
|
||||||
|
return c.redirect(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoMetaData) {
|
||||||
|
return c.redirect(videoMetaData.url.toString());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
import { Handler } from "hono";
|
import { Handler } from 'hono';
|
||||||
import { HTTPException } from "hono/http-exception";
|
import { HTTPException } from 'hono/http-exception';
|
||||||
import { fetchPost } from "../lib/fetchPostData";
|
import { fetchPost } from '../lib/fetchPostData';
|
||||||
|
|
||||||
export const getPostData: Handler<
|
export const getPostData: Handler<
|
||||||
Env,
|
Env,
|
||||||
| "/profile/:user/post/:post/json"
|
| '/profile/:user/post/:post/json'
|
||||||
| "/https://bsky.app/profile/:user/post/:post/json"
|
| '/https://bsky.app/profile/:user/post/:post/json'
|
||||||
> = async (c) => {
|
> = async (c) => {
|
||||||
const { user, post } = c.req.param();
|
const { user, post } = c.req.param();
|
||||||
const agent = c.get("Agent");
|
const agent = c.get('Agent');
|
||||||
const { data, success } = await fetchPost(agent, { user, post });
|
try {
|
||||||
if (!success) {
|
var { data } = await fetchPost(agent, { user, post });
|
||||||
|
} catch (e) {
|
||||||
throw new HTTPException(500, {
|
throw new HTTPException(500, {
|
||||||
message: "Failed to fetch the post!",
|
message: `Failed to fetch the post!\n${e}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.json(data);
|
return c.json(data);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
import { Handler } from "hono";
|
import { Handler } from 'hono';
|
||||||
import { HTTPException } from "hono/http-exception";
|
import { HTTPException } from 'hono/http-exception';
|
||||||
import { fetchProfile } from "../lib/fetchProfile";
|
import { fetchProfile } from '../lib/fetchProfile';
|
||||||
import { Profile } from "../components/Profile";
|
import { Profile } from '../components/Profile';
|
||||||
|
|
||||||
export const getProfile: Handler<
|
export const getProfile: Handler<
|
||||||
Env,
|
Env,
|
||||||
"/profile/:user" | "/https://bsky.app/profile/:user"
|
'/profile/:user' | '/https://bsky.app/profile/:user'
|
||||||
> = async (c) => {
|
> = async (c) => {
|
||||||
const { user } = c.req.param();
|
const { user } = c.req.param();
|
||||||
const agent = c.get("Agent");
|
const agent = c.get('Agent');
|
||||||
const { data, success } = await fetchProfile(agent, { user });
|
try {
|
||||||
if (!success) {
|
var { data } = await fetchProfile(agent, { user });
|
||||||
|
} catch (e) {
|
||||||
throw new HTTPException(500, {
|
throw new HTTPException(500, {
|
||||||
message: "Failed to fetch the profile!",
|
message: `Failed to fetch the profile!\n${e}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.html(
|
return c.html(
|
||||||
<Profile
|
<Profile
|
||||||
profile={data}
|
profile={data}
|
||||||
url={c.req.path}
|
url={c.req.path}
|
||||||
appDomain={c.env.VIXBLUESKY_APP_DOMAIN}
|
appDomain={c.env.VIXBLUESKY_APP_DOMAIN}
|
||||||
/>
|
/>,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
/** @jsx jsx */
|
/** @jsx jsx */
|
||||||
import { jsx } from "hono/jsx";
|
import { jsx } from 'hono/jsx';
|
||||||
import { Handler } from "hono";
|
import { Handler } from 'hono';
|
||||||
import { HTTPException } from "hono/http-exception";
|
import { HTTPException } from 'hono/http-exception';
|
||||||
import { fetchProfile } from "../lib/fetchProfile";
|
import { fetchProfile } from '../lib/fetchProfile';
|
||||||
|
|
||||||
export const getProfileData: Handler<
|
export const getProfileData: Handler<
|
||||||
Env,
|
Env,
|
||||||
"/profile/:user/json" | "/https://bsky.app/profile/:user/json"
|
'/profile/:user/json' | '/https://bsky.app/profile/:user/json'
|
||||||
> = async (c) => {
|
> = async (c) => {
|
||||||
const { user } = c.req.param();
|
const { user } = c.req.param();
|
||||||
const agent = c.get("Agent");
|
const agent = c.get('Agent');
|
||||||
const { data, success } = await fetchProfile(agent, { user });
|
try {
|
||||||
if (!success) {
|
var { data } = await fetchProfile(agent, { user });
|
||||||
|
} catch (e) {
|
||||||
throw new HTTPException(500, {
|
throw new HTTPException(500, {
|
||||||
message: "Failed to fetch the profile!",
|
message: `Failed to fetch the profile!\n${e}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.json(data);
|
return c.json(data);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user