big rewrite (#15)

This commit is contained in:
Lexie
2024-10-11 16:02:11 +02:00
committed by GitHub
parent 4ee85a79ba
commit b4e44be4bf
22 changed files with 338 additions and 466 deletions

BIN
.github/README/raw-media.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 KiB

View File

@@ -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. ![Direct Link](./.github/README/raw-media.png)
## Authors ## Authors

49
conf/nginx.conf Normal file
View 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
View File

@@ -0,0 +1,5 @@
{
"singleQuote": true,
"tabWidth": 2,
"endOfLine": "lf"
}

View File

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

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

View File

@@ -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)}" />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 (AppBskyEmbedRecordWithMedia.isView(post.embed)) { if (checkType('app.bsky.embed.recordWithMedia#view', embed)) {
const { success: isView } = AppBskyEmbedRecordWithMedia.validateView( if (checkType('app.bsky.embed.images#view', embed.media)) {
post.embed images = [
); ...images,
if (isView && AppBskyEmbedImages.isView(post.embed.media)) { ...(embed.media.images as AppBskyEmbedImages.ViewImage[]),
const { success: isImageView } = AppBskyEmbedImages.validateView( ];
post.embed.media
);
if (isImageView) {
images = [...images, ...post.embed.media.images];
}
} }
} }
if (AppBskyEmbedImages.isView(post.embed)) { if (checkType('app.bsky.embed.images#view', embed)) {
const { success: isImageView } = AppBskyEmbedImages.validateView( images = [...images, ...embed.images];
post.embed }
);
if (isImageView) { const isEmptyImages = images.length === 0;
images = [...images, ...post.embed.images];
if (isEmptyImages) {
if (checkType('app.bsky.embed.external#view', embed)) {
return embed.external.uri;
} }
} }
return images.length === 0 ? post.author.avatar ?? "" : images; return isEmptyImages ? (post.author.avatar ?? '') : images;
} }

View File

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

View File

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

View File

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

View File

@@ -1,45 +1,86 @@
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(
<Post
post={fetchedPost}
url={c.req.path}
appDomain={c.env.VIXBLUESKY_APP_DOMAIN}
videoMetadata={videoMetaData}
apiUrl={c.env.VIXBLUESKY_API_URL}
images={images}
/>,
); );
} }
return c.html( if (Array.isArray(images) && images.length !== 0) {
<Post const url = images[0].fullsize;
post={fetchedPost} return c.redirect(url);
url={c.req.path} }
appDomain={c.env.VIXBLUESKY_APP_DOMAIN}
videoMetadata={videoMetaData} if (videoMetaData) {
apiUrl={c.env.VIXBLUESKY_API_URL} return c.redirect(videoMetaData.url.toString());
/> }
);
}; };

View File

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

View File

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

View File

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