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`.
|
||||
|
||||
## FAQ
|
||||
## Direct Links
|
||||
|
||||
### [Video is too long to embed] in an embed description.
|
||||
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.
|
||||
You want to link to a media directly? You can prepend `r.` to the URL to get a direct link.
|
||||

|
||||
|
||||
## 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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.12.24",
|
||||
"@atcute/bluesky": "^1.0.7",
|
||||
"@atcute/client": "^2.0.3",
|
||||
"hono": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20230628.0",
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "^5.1.6",
|
||||
"wrangler": "^3.75.0"
|
||||
}
|
||||
|
||||
108
pkgs/app/pnpm-lock.yaml
generated
108
pkgs/app/pnpm-lock.yaml
generated
@@ -8,9 +8,12 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@atproto/api':
|
||||
specifier: ^0.12.24
|
||||
version: 0.12.29
|
||||
'@atcute/bluesky':
|
||||
specifier: ^1.0.7
|
||||
version: 1.0.7(@atcute/client@2.0.3)
|
||||
'@atcute/client':
|
||||
specifier: ^2.0.3
|
||||
version: 2.0.3
|
||||
hono:
|
||||
specifier: ^4.5.1
|
||||
version: 4.5.11
|
||||
@@ -18,6 +21,9 @@ importers:
|
||||
'@cloudflare/workers-types':
|
||||
specifier: ^4.20230628.0
|
||||
version: 4.20230710.1
|
||||
prettier:
|
||||
specifier: ^3.3.3
|
||||
version: 3.3.3
|
||||
typescript:
|
||||
specifier: ^5.1.6
|
||||
version: 5.1.6
|
||||
@@ -27,20 +33,13 @@ importers:
|
||||
|
||||
packages:
|
||||
|
||||
'@atproto/api@0.12.29':
|
||||
resolution: {integrity: sha512-PyzPLjGWR0qNOMrmj3Nt3N5NuuANSgOk/33Bu3j+rFjjPrHvk9CI6iQPU6zuDaDCoyOTRJRafw8X/aMQw+ilgw==}
|
||||
'@atcute/bluesky@1.0.7':
|
||||
resolution: {integrity: sha512-2jPHzl7WbcqRtcAXanJy4Lp638ujqnoGmPCPmBlmpEDP34D7EVKQqjN/mlvglb5n539dThA9xlSgIS8yOxwzDA==}
|
||||
peerDependencies:
|
||||
'@atcute/client': ^1.0.0 || ^2.0.0
|
||||
|
||||
'@atproto/common-web@0.3.0':
|
||||
resolution: {integrity: sha512-67VnV6JJyX+ZWyjV7xFQMypAgDmjVaR9ZCuU/QW+mqlqI7fex2uL4Fv+7/jHadgzhuJHVd6OHOvNn0wR5WZYtA==}
|
||||
|
||||
'@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==}
|
||||
'@atcute/client@2.0.3':
|
||||
resolution: {integrity: sha512-j9GryA5l+4F0BTQWa6/1XmsuSPSq+bqNCY3mrHUGD592hMqUZxgpYDLgRWL+719V287AW/56AwvFYlbjlENp7A==}
|
||||
|
||||
'@cloudflare/kv-asset-handler@0.3.4':
|
||||
resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==}
|
||||
@@ -259,9 +258,6 @@ packages:
|
||||
as-table@1.0.55:
|
||||
resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==}
|
||||
|
||||
await-lock@2.2.2:
|
||||
resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==}
|
||||
|
||||
binary-extensions@2.2.0:
|
||||
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -340,9 +336,6 @@ packages:
|
||||
glob-to-regexp@0.4.1:
|
||||
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
|
||||
|
||||
graphemer@1.4.0:
|
||||
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
|
||||
|
||||
hasown@2.0.2:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -371,9 +364,6 @@ packages:
|
||||
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
||||
engines: {node: '>=0.12.0'}
|
||||
|
||||
iso-datestring-validator@2.2.2:
|
||||
resolution: {integrity: sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==}
|
||||
|
||||
magic-string@0.25.9:
|
||||
resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
|
||||
|
||||
@@ -390,9 +380,6 @@ packages:
|
||||
ms@2.1.2:
|
||||
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
|
||||
|
||||
multiformats@9.9.0:
|
||||
resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==}
|
||||
|
||||
mustache@4.2.0:
|
||||
resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==}
|
||||
hasBin: true
|
||||
@@ -426,6 +413,11 @@ packages:
|
||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==}
|
||||
|
||||
@@ -474,10 +466,6 @@ packages:
|
||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
tlds@1.240.0:
|
||||
resolution: {integrity: sha512-1OYJQenswGZSOdRw7Bql5Qu7uf75b+F3HFBXbqnG/ifHa0fev1XcG+3pJf3pA/KC6RtHQzfKgIf1vkMlMG7mtQ==}
|
||||
hasBin: true
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||
engines: {node: '>=8.0'}
|
||||
@@ -493,9 +481,6 @@ packages:
|
||||
ufo@1.5.4:
|
||||
resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==}
|
||||
|
||||
uint8arrays@3.0.0:
|
||||
resolution: {integrity: sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==}
|
||||
|
||||
undici@5.28.4:
|
||||
resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==}
|
||||
engines: {node: '>=14.0'}
|
||||
@@ -536,45 +521,16 @@ packages:
|
||||
youch@3.2.3:
|
||||
resolution: {integrity: sha512-ZBcWz/uzZaQVdCvfV4uk616Bbpf2ee+F/AvuKDR5EwX/Y4v06xWdtMluqTD7+KlZdM93lLm9gMZYo0sKBS0pgw==}
|
||||
|
||||
zod@3.21.4:
|
||||
resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==}
|
||||
|
||||
zod@3.23.8:
|
||||
resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@atproto/api@0.12.29':
|
||||
'@atcute/bluesky@1.0.7(@atcute/client@2.0.3)':
|
||||
dependencies:
|
||||
'@atproto/common-web': 0.3.0
|
||||
'@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
|
||||
'@atcute/client': 2.0.3
|
||||
|
||||
'@atproto/common-web@0.3.0':
|
||||
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
|
||||
'@atcute/client@2.0.3': {}
|
||||
|
||||
'@cloudflare/kv-asset-handler@0.3.4':
|
||||
dependencies:
|
||||
@@ -703,8 +659,6 @@ snapshots:
|
||||
dependencies:
|
||||
printable-characters: 1.0.42
|
||||
|
||||
await-lock@2.2.2: {}
|
||||
|
||||
binary-extensions@2.2.0: {}
|
||||
|
||||
blake3-wasm@2.1.5: {}
|
||||
@@ -795,8 +749,6 @@ snapshots:
|
||||
|
||||
glob-to-regexp@0.4.1: {}
|
||||
|
||||
graphemer@1.4.0: {}
|
||||
|
||||
hasown@2.0.2:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
@@ -819,8 +771,6 @@ snapshots:
|
||||
|
||||
is-number@7.0.0: {}
|
||||
|
||||
iso-datestring-validator@2.2.2: {}
|
||||
|
||||
magic-string@0.25.9:
|
||||
dependencies:
|
||||
sourcemap-codec: 1.4.8
|
||||
@@ -848,8 +798,6 @@ snapshots:
|
||||
|
||||
ms@2.1.2: {}
|
||||
|
||||
multiformats@9.9.0: {}
|
||||
|
||||
mustache@4.2.0: {}
|
||||
|
||||
nanoid@3.3.6: {}
|
||||
@@ -868,6 +816,8 @@ snapshots:
|
||||
|
||||
picomatch@2.3.1: {}
|
||||
|
||||
prettier@3.3.3: {}
|
||||
|
||||
printable-characters@1.0.42: {}
|
||||
|
||||
readdirp@3.6.0:
|
||||
@@ -913,8 +863,6 @@ snapshots:
|
||||
|
||||
supports-preserve-symlinks-flag@1.0.0: {}
|
||||
|
||||
tlds@1.240.0: {}
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
dependencies:
|
||||
is-number: 7.0.0
|
||||
@@ -925,10 +873,6 @@ snapshots:
|
||||
|
||||
ufo@1.5.4: {}
|
||||
|
||||
uint8arrays@3.0.0:
|
||||
dependencies:
|
||||
multiformats: 9.9.0
|
||||
|
||||
undici@5.28.4:
|
||||
dependencies:
|
||||
'@fastify/busboy': 2.1.1
|
||||
@@ -986,6 +930,4 @@ snapshots:
|
||||
mustache: 4.2.0
|
||||
stacktracey: 2.1.8
|
||||
|
||||
zod@3.21.4: {}
|
||||
|
||||
zod@3.23.8: {}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { html } from "hono/html";
|
||||
import { html } from 'hono/html';
|
||||
|
||||
export interface LayoutProps {
|
||||
url: string;
|
||||
@@ -7,11 +7,11 @@ export interface LayoutProps {
|
||||
|
||||
export const Layout = ({ url, children }: LayoutProps) => {
|
||||
const removeLeadingSlash = url.substring(1);
|
||||
const redirectUrl = removeLeadingSlash.startsWith("https://")
|
||||
const redirectUrl = removeLeadingSlash.startsWith('https://')
|
||||
? removeLeadingSlash
|
||||
: `https://bsky.app/${removeLeadingSlash}`;
|
||||
return html`
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<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 { parseEmbedImages } from "../lib/parseEmbedImages";
|
||||
import { parseEmbedDescription } from "../lib/parseEmbedDescription";
|
||||
import { StreamInfo } from "../lib/processVideoEmbed";
|
||||
import { checkType, join } from "../lib/utils";
|
||||
import { Layout } from './Layout';
|
||||
import { OEmbedTypes } from '../routes/getOEmbed';
|
||||
import { parseEmbedDescription } from '../lib/parseEmbedDescription';
|
||||
import { checkType } from '../lib/utils';
|
||||
import { VideoInfo } from '../routes/getPost';
|
||||
import { AppBskyEmbedImages, AppBskyFeedDefs } from '@atcute/client/lexicons';
|
||||
|
||||
interface PostProps {
|
||||
post: AppBskyFeedDefs.PostView;
|
||||
url: string;
|
||||
appDomain: string;
|
||||
videoMetadata?: StreamInfo[] | undefined;
|
||||
videoMetadata?: VideoInfo;
|
||||
apiUrl: string;
|
||||
images: string | AppBskyEmbedImages.ViewImage[];
|
||||
}
|
||||
|
||||
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 = ({
|
||||
streamInfo,
|
||||
apiUrl,
|
||||
@@ -38,13 +27,13 @@ const Video = ({
|
||||
post,
|
||||
description,
|
||||
}: {
|
||||
streamInfo: StreamInfo;
|
||||
streamInfo: VideoInfo;
|
||||
apiUrl: string;
|
||||
appDomain: string;
|
||||
post: AppBskyFeedDefs.PostView;
|
||||
description: string;
|
||||
}) => {
|
||||
const url = constructVideoUrl(streamInfo, apiUrl);
|
||||
const url = streamInfo.url.toString();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -57,19 +46,19 @@ const Video = ({
|
||||
<meta property="og:video:type" content="video/mp4" />
|
||||
<meta
|
||||
property="og:video:width"
|
||||
content={streamInfo.resolution.width.toString()}
|
||||
content={streamInfo.aspectRatio.width.toString()}
|
||||
/>
|
||||
<meta
|
||||
property="og:video:height"
|
||||
content={streamInfo.resolution.height.toString()}
|
||||
content={streamInfo.aspectRatio.height.toString()}
|
||||
/>
|
||||
<meta
|
||||
property="twitter:player:width"
|
||||
content={streamInfo.resolution.width.toString()}
|
||||
content={streamInfo.aspectRatio.width.toString()}
|
||||
/>
|
||||
<meta
|
||||
property="twitter:player:height"
|
||||
content={streamInfo.resolution.height.toString()}
|
||||
content={streamInfo.aspectRatio.height.toString()}
|
||||
/>
|
||||
|
||||
<link
|
||||
@@ -80,7 +69,7 @@ const Video = ({
|
||||
}&reposts=${post.repostCount}&likes=${
|
||||
post.likeCount
|
||||
}&avatar=${encodeURIComponent(
|
||||
post.author.avatar ?? ""
|
||||
post.author.avatar ?? '',
|
||||
)}&description=${encodeURIComponent(description)}`}
|
||||
/>
|
||||
</>
|
||||
@@ -93,7 +82,7 @@ const Images = ({
|
||||
images: AppBskyEmbedImages.ViewImage[] | string;
|
||||
}) => (
|
||||
<>
|
||||
{typeof images === "string" ? (
|
||||
{typeof images === 'string' ? (
|
||||
<>
|
||||
<meta property="og:image" content={images} />
|
||||
<meta property="twitter:image" content={images} />
|
||||
@@ -115,25 +104,18 @@ export const Post = ({
|
||||
appDomain,
|
||||
videoMetadata,
|
||||
apiUrl,
|
||||
images,
|
||||
}: PostProps) => {
|
||||
const images = parseEmbedImages(post);
|
||||
const isAuthor = images === post.author.avatar;
|
||||
let description = parseEmbedDescription(post);
|
||||
const isVideo = checkType(
|
||||
"app.bsky.embed.video",
|
||||
post.embed?.media ?? post.embed
|
||||
'app.bsky.embed.video',
|
||||
// @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;
|
||||
|
||||
if (isVideo && isTooLong) {
|
||||
videoUrl = constructVideoUrl(streamInfo!, apiUrl);
|
||||
description += `\n[Video is too long to embed!]`;
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout url={url}>
|
||||
<meta name="twitter:creator" content={`@${post.author.handle}`} />
|
||||
@@ -151,21 +133,19 @@ export const Post = ({
|
||||
|
||||
{!isAuthor && <Meta post={post} />}
|
||||
|
||||
{images.length !== 0 && (shouldOverrideForVideo || !isVideo) && (
|
||||
<Images images={images} />
|
||||
)}
|
||||
{images.length !== 0 && !isVideo && <Images images={images} />}
|
||||
|
||||
{isVideo && streamInfo!.uri.length <= 4 && (
|
||||
{isVideo && (
|
||||
<Video
|
||||
apiUrl={apiUrl}
|
||||
streamInfo={streamInfo!}
|
||||
streamInfo={videoMetadata!}
|
||||
appDomain={appDomain}
|
||||
description={description}
|
||||
post={post}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(shouldOverrideForVideo || !isVideo) && (
|
||||
{!isVideo && (
|
||||
<link
|
||||
rel="alternate"
|
||||
type="application/json+oembed"
|
||||
@@ -174,10 +154,8 @@ export const Post = ({
|
||||
}&reposts=${post.repostCount}&likes=${
|
||||
post.likeCount
|
||||
}&avatar=${encodeURIComponent(
|
||||
post.author.avatar ?? ""
|
||||
)}&description=${encodeURIComponent(description)}${
|
||||
videoUrl ? `&videoUrl=${encodeURIComponent(videoUrl)}` : ""
|
||||
}`}
|
||||
post.author.avatar ?? '',
|
||||
)}&description=${encodeURIComponent(description)}`}
|
||||
/>
|
||||
)}
|
||||
</Layout>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { AppBskyActorDefs } from "@atproto/api";
|
||||
|
||||
import { Layout } from "./Layout";
|
||||
import { OEmbedTypes } from "../routes/getOEmbed";
|
||||
import { Layout } from './Layout';
|
||||
import { OEmbedTypes } from '../routes/getOEmbed';
|
||||
import { AppBskyActorDefs } from '@atcute/client/lexicons';
|
||||
|
||||
interface ProfileProps {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed;
|
||||
@@ -13,7 +12,7 @@ export const Profile = ({ profile, url, appDomain }: ProfileProps) => (
|
||||
<Layout url={url}>
|
||||
<meta name="og:type" content="article" />
|
||||
<meta name="twitter:creator" content={`@${profile.handle}`} />
|
||||
<meta property="og:description" content={profile.description ?? ""} />
|
||||
<meta property="og:description" content={profile.description ?? ''} />
|
||||
<meta
|
||||
property="og:title"
|
||||
content={`${profile.displayName} (@${profile.handle})`}
|
||||
@@ -26,7 +25,7 @@ export const Profile = ({ profile, url, appDomain }: ProfileProps) => (
|
||||
href={`https://${appDomain}/oembed?type=${OEmbedTypes.Profile}&follows=${
|
||||
profile.followsCount
|
||||
}&posts=${profile.postsCount}&avatar=${encodeURIComponent(
|
||||
profile.avatar ?? ""
|
||||
profile.avatar ?? '',
|
||||
)}`}
|
||||
/>
|
||||
</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 type { KVNamespace } from "@cloudflare/workers-types";
|
||||
import { XRPC } from '@atcute/client';
|
||||
import type { KVNamespace } from '@cloudflare/workers-types';
|
||||
|
||||
declare global {
|
||||
interface Env {
|
||||
@@ -12,7 +12,7 @@ declare global {
|
||||
bskyx: KVNamespace;
|
||||
};
|
||||
Variables: {
|
||||
Agent: BskyAgent;
|
||||
Agent: XRPC;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,43 @@
|
||||
import { Hono } from "hono";
|
||||
import { AtpSessionData, BskyAgent } from "@atproto/api";
|
||||
import { getPost } from "./routes/getPost";
|
||||
import { getPostData } from "./routes/getPostData";
|
||||
import { getOEmbed } from "./routes/getOEmbed";
|
||||
import { getProfileData } from "./routes/getProfileData";
|
||||
import { getProfile } from "./routes/getProfile";
|
||||
import { HTTPException } from "hono/http-exception";
|
||||
import { Hono } from 'hono';
|
||||
import { XRPC, CredentialManager, AtpSessionData } from '@atcute/client';
|
||||
import '@atcute/bluesky/lexicons';
|
||||
import { getPost } from './routes/getPost';
|
||||
import { getPostData } from './routes/getPostData';
|
||||
import { getOEmbed } from './routes/getOEmbed';
|
||||
import { getProfileData } from './routes/getProfileData';
|
||||
import { getProfile } from './routes/getProfile';
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
|
||||
const app = new Hono<Env>();
|
||||
|
||||
app.use("*", async (c, next) => {
|
||||
const agent = new BskyAgent({
|
||||
app.use('*', async (c, next) => {
|
||||
const creds = new CredentialManager({
|
||||
service: c.env.BSKY_SERVICE_URL,
|
||||
async persistSession(_, session) {
|
||||
if (session) {
|
||||
return c.env.bskyx.put("session", JSON.stringify(session));
|
||||
}
|
||||
onRefresh(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 {
|
||||
const rawSession = await c.env.bskyx.get("session");
|
||||
const rawSession = await c.env.bskyx.get('session');
|
||||
if (rawSession) {
|
||||
const session = JSON.parse(rawSession) as AtpSessionData;
|
||||
await agent.resumeSession(session);
|
||||
await creds.resume(session);
|
||||
} else {
|
||||
await agent.login({
|
||||
await creds.login({
|
||||
identifier: c.env.BSKY_AUTH_USERNAME,
|
||||
password: c.env.BSKY_AUTH_PASSWORD,
|
||||
});
|
||||
}
|
||||
c.set("Agent", agent);
|
||||
c.set('Agent', agent);
|
||||
} catch (error) {
|
||||
const err = new Error("Failed to login to Bluesky!", {
|
||||
const err = new Error('Failed to login to Bluesky!', {
|
||||
cause: error,
|
||||
});
|
||||
throw new HTTPException(500, {
|
||||
@@ -41,22 +47,22 @@ app.use("*", async (c, next) => {
|
||||
return next();
|
||||
});
|
||||
|
||||
app.get("/", async (c) => {
|
||||
return c.redirect("https://github.com/Rapougnac/VixBluesky");
|
||||
app.get('/', async (c) => {
|
||||
return c.redirect('https://github.com/Rapougnac/VixBluesky');
|
||||
});
|
||||
|
||||
app.get("/profile/:user/post/:post", getPost);
|
||||
app.get("/https://bsky.app/profile/:user/post/:post", getPost);
|
||||
app.get('/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("/https://bsky.app/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("/profile/:user", getProfile);
|
||||
app.get("/https://bsky.app/profile/:user", getProfile);
|
||||
app.get('/profile/:user', getProfile);
|
||||
app.get('/https://bsky.app/profile/:user', getProfile);
|
||||
|
||||
app.get("/profile/:user/json", getProfileData);
|
||||
app.get("/https://bsky.app/profile/:user/json", getProfileData);
|
||||
app.get('/profile/:user/json', getProfileData);
|
||||
app.get('/https://bsky.app/profile/:user/json', getProfileData);
|
||||
|
||||
app.get("/oembed", getOEmbed);
|
||||
app.get('/oembed', getOEmbed);
|
||||
|
||||
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;
|
||||
post: string;
|
||||
}
|
||||
|
||||
export async function fetchPost(
|
||||
agent: BskyAgent,
|
||||
{ user, post }: fetchPostOptions
|
||||
) {
|
||||
const { data: userData } = await agent.getProfile({
|
||||
actor: user,
|
||||
});
|
||||
return agent.getPosts({
|
||||
uris: [`at://${userData.did}/app.bsky.feed.post/${post}`],
|
||||
export async function fetchPost(agent: XRPC, { user, post }: FetchPostOptions) {
|
||||
const { data: userData } = await fetchProfile(agent, { user });
|
||||
return agent.get('app.bsky.feed.getPosts', {
|
||||
params: { 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;
|
||||
}
|
||||
|
||||
export async function fetchProfile(
|
||||
agent: BskyAgent,
|
||||
{ user }: fetchProfileOptions
|
||||
) {
|
||||
return agent.getProfile({
|
||||
actor: user,
|
||||
});
|
||||
export async function fetchProfile(agent: XRPC, { user }: FetchProfileOptions) {
|
||||
return agent.get('app.bsky.actor.getProfile', { params: { actor: user } });
|
||||
}
|
||||
|
||||
@@ -1,40 +1,18 @@
|
||||
import {
|
||||
AppBskyEmbedRecord,
|
||||
AppBskyEmbedRecordWithMedia,
|
||||
AppBskyFeedDefs,
|
||||
AppBskyFeedPost,
|
||||
} from "@atproto/api";
|
||||
import { indent } from "./utils";
|
||||
import { AppBskyFeedDefs } from '@atcute/client/lexicons';
|
||||
import { checkType, indent } from './utils';
|
||||
|
||||
export function parseEmbedDescription(post: AppBskyFeedDefs.PostView) {
|
||||
if (AppBskyFeedPost.isRecord(post.record)) {
|
||||
if (AppBskyEmbedRecord.isView(post.embed)) {
|
||||
const { success: isView } = AppBskyEmbedRecord.validateView(post.embed);
|
||||
if (isView && AppBskyEmbedRecord.isViewRecord(post.embed.record)) {
|
||||
const { success: isViewRecord } = AppBskyEmbedRecord.validateViewRecord(
|
||||
post.embed.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.author.handle}\n➥${indent(post.embed.record.value.text, 2)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
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 "";
|
||||
export function parseEmbedDescription(post: AppBskyFeedDefs.PostView): string {
|
||||
const isQuote =
|
||||
checkType('app.bsky.feed.post', post.record) &&
|
||||
(checkType('app.bsky.embed.record#view', post.embed) ||
|
||||
checkType('app.bsky.embed.recordWithMedia#view', post.embed));
|
||||
|
||||
// @ts-expect-error
|
||||
const embed = post.embed.record?.record ?? post.embed.record;
|
||||
|
||||
return isQuote
|
||||
? // @ts-expect-error
|
||||
`${post.record.text}\n\nQuoting @${embed.author.handle}\n➥${indent(embed.value.text, 2)}`
|
||||
: // @ts-expect-error
|
||||
post.record.text;
|
||||
}
|
||||
|
||||
@@ -1,56 +1,50 @@
|
||||
import {
|
||||
AppBskyEmbedImages,
|
||||
AppBskyEmbedRecord,
|
||||
AppBskyEmbedRecordWithMedia,
|
||||
AppBskyFeedDefs,
|
||||
} from "@atproto/api";
|
||||
import { AppBskyFeedDefs, AppBskyEmbedImages } from '@atcute/client/lexicons';
|
||||
import { checkType } from './utils';
|
||||
|
||||
export function parseEmbedImages(
|
||||
post: AppBskyFeedDefs.PostView
|
||||
post: AppBskyFeedDefs.PostView,
|
||||
): string | AppBskyEmbedImages.ViewImage[] {
|
||||
let images: AppBskyEmbedImages.ViewImage[] = [];
|
||||
|
||||
if (AppBskyEmbedRecord.isView(post.embed)) {
|
||||
const { success: isView } = AppBskyEmbedRecord.validateView(post.embed);
|
||||
if (isView && AppBskyEmbedRecord.isViewRecord(post.embed.record)) {
|
||||
const { success: isViewRecord } = AppBskyEmbedRecord.validateViewRecord(
|
||||
post.embed.record
|
||||
);
|
||||
const embed = post.embed as typeof post.embed & {
|
||||
record: any;
|
||||
media: any;
|
||||
images: any;
|
||||
external: any;
|
||||
};
|
||||
|
||||
if (checkType('app.bsky.embed.record#view', embed)) {
|
||||
if (checkType('app.bsky.embed.record#viewRecord', embed?.record)) {
|
||||
if (
|
||||
isViewRecord &&
|
||||
post.embed.record.embeds &&
|
||||
AppBskyEmbedImages.isView(post.embed.record.embeds[0])
|
||||
embed?.record.embeds &&
|
||||
checkType('app.bsky.embed.images#view', embed.record.embeds[0])
|
||||
) {
|
||||
const { success: isImageView } = AppBskyEmbedImages.validateView(
|
||||
post.embed.record.embeds[0]
|
||||
);
|
||||
if (isImageView) {
|
||||
images = [...images, ...post.embed.record.embeds[0].images];
|
||||
}
|
||||
images = [
|
||||
...images,
|
||||
...(embed.record.embeds[0].images as AppBskyEmbedImages.ViewImage[]),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (AppBskyEmbedRecordWithMedia.isView(post.embed)) {
|
||||
const { success: isView } = AppBskyEmbedRecordWithMedia.validateView(
|
||||
post.embed
|
||||
);
|
||||
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.recordWithMedia#view', embed)) {
|
||||
if (checkType('app.bsky.embed.images#view', embed.media)) {
|
||||
images = [
|
||||
...images,
|
||||
...(embed.media.images as AppBskyEmbedImages.ViewImage[]),
|
||||
];
|
||||
}
|
||||
}
|
||||
if (AppBskyEmbedImages.isView(post.embed)) {
|
||||
const { success: isImageView } = AppBskyEmbedImages.validateView(
|
||||
post.embed
|
||||
);
|
||||
if (isImageView) {
|
||||
images = [...images, ...post.embed.images];
|
||||
if (checkType('app.bsky.embed.images#view', embed)) {
|
||||
images = [...images, ...embed.images];
|
||||
}
|
||||
|
||||
const isEmptyImages = images.length === 0;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
.map(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((v) => `${key}=${v}`).join("&");
|
||||
return value.map((v) => `${key}=${v}`).join('&');
|
||||
}
|
||||
return `${key}=${value}`;
|
||||
})
|
||||
.join("&");
|
||||
.join('&');
|
||||
|
||||
export const join = (t: string | string[], s: string) =>
|
||||
Array.isArray(t) ? t.join(s) : t;
|
||||
|
||||
export const checkType = (t: string, o: any) =>
|
||||
(typeof o?.$type === "string" && o?.$type.startsWith(t)) || o?.$type === t;
|
||||
export const checkType = (t: string, o: any): boolean =>
|
||||
typeof o?.$type === 'string' && (o?.$type === t || o?.$type.startsWith(t));
|
||||
|
||||
export const indent = (s: string, n: number) =>
|
||||
s
|
||||
.split("\n")
|
||||
.map((l) => " ".repeat(n) + l)
|
||||
.join("\n");
|
||||
.split('\n')
|
||||
.map((l) => ' '.repeat(n) + l)
|
||||
.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 {
|
||||
Post = 1,
|
||||
@@ -6,13 +6,13 @@ export enum OEmbedTypes {
|
||||
Video,
|
||||
}
|
||||
|
||||
export const getOEmbed: Handler<Env, "/oembed"> = async (c) => {
|
||||
const type = +(c.req.query("type") ?? 0);
|
||||
const avatar = c.req.query("avatar");
|
||||
export const getOEmbed: Handler<Env, '/oembed'> = async (c) => {
|
||||
const type = +(c.req.query('type') ?? 0);
|
||||
const avatar = c.req.query('avatar');
|
||||
|
||||
const defaults = {
|
||||
provider_name: "VixBluesky",
|
||||
provider_url: "https://bskyx.app/",
|
||||
provider_name: 'VixBluesky',
|
||||
provider_url: 'https://bskyx.app/',
|
||||
thumbnail_width: 1000,
|
||||
thumbnail_height: 1000,
|
||||
};
|
||||
@@ -23,12 +23,11 @@ export const getOEmbed: Handler<Env, "/oembed"> = async (c) => {
|
||||
}
|
||||
|
||||
if (type === OEmbedTypes.Post) {
|
||||
const { replies, reposts, likes, videoUrl } = c.req.query();
|
||||
const { replies, reposts, likes } = c.req.query();
|
||||
|
||||
return c.json({
|
||||
...defaults,
|
||||
author_name: `🗨️ ${replies} ♻️ ${reposts} 💙 ${likes}`,
|
||||
provider_url: videoUrl ? videoUrl : defaults.provider_url,
|
||||
});
|
||||
}
|
||||
if (type === OEmbedTypes.Profile) {
|
||||
|
||||
@@ -1,45 +1,86 @@
|
||||
import { Handler } from "hono";
|
||||
import { HTTPException } from "hono/http-exception";
|
||||
import { fetchPost } from "../lib/fetchPostData";
|
||||
import { Post } from "../components/Post";
|
||||
import { processVideoEmbed, StreamInfo } from "../lib/processVideoEmbed";
|
||||
import { checkType } from "../lib/utils";
|
||||
import { Handler } from 'hono';
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
import { fetchPost } from '../lib/fetchPostData';
|
||||
import { Post } from '../components/Post';
|
||||
import { parseEmbedImages } from '../lib/parseEmbedImages';
|
||||
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<
|
||||
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) => {
|
||||
const { user, post } = c.req.param();
|
||||
const agent = c.get("Agent");
|
||||
const { data, success } = await fetchPost(agent, { user, post });
|
||||
const isDirect = c.req.query('direct');
|
||||
|
||||
if (!success) {
|
||||
const agent = c.get('Agent');
|
||||
try {
|
||||
var { data } = await fetchPost(agent, { user, post });
|
||||
} catch (e) {
|
||||
throw new HTTPException(500, {
|
||||
message: "Failed to fetch the post!",
|
||||
message: `Failed to fetch the post!\n${e}`,
|
||||
});
|
||||
}
|
||||
|
||||
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 (
|
||||
checkType("app.bsky.embed.video", fetchedPost.embed) ||
|
||||
checkType("app.bsky.embed.video", fetchedPost.embed?.media)
|
||||
checkType('app.bsky.embed.video', embed) ||
|
||||
checkType('app.bsky.embed.video', embed?.media)
|
||||
) {
|
||||
videoMetaData = await processVideoEmbed(
|
||||
// @ts-expect-error
|
||||
fetchedPost.embed?.media ?? fetchedPost.embed
|
||||
const videoEmbed = (embed?.media ?? fetchedPost.embed) as VideoEmbed;
|
||||
videoMetaData = {
|
||||
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(
|
||||
<Post
|
||||
post={fetchedPost}
|
||||
url={c.req.path}
|
||||
appDomain={c.env.VIXBLUESKY_APP_DOMAIN}
|
||||
videoMetadata={videoMetaData}
|
||||
apiUrl={c.env.VIXBLUESKY_API_URL}
|
||||
/>
|
||||
);
|
||||
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 { HTTPException } from "hono/http-exception";
|
||||
import { fetchPost } from "../lib/fetchPostData";
|
||||
import { Handler } from 'hono';
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
import { fetchPost } from '../lib/fetchPostData';
|
||||
|
||||
export const getPostData: Handler<
|
||||
Env,
|
||||
| "/profile/:user/post/:post/json"
|
||||
| "/https://bsky.app/profile/:user/post/:post/json"
|
||||
| '/profile/:user/post/:post/json'
|
||||
| '/https://bsky.app/profile/:user/post/:post/json'
|
||||
> = async (c) => {
|
||||
const { user, post } = c.req.param();
|
||||
const agent = c.get("Agent");
|
||||
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, {
|
||||
message: "Failed to fetch the post!",
|
||||
message: `Failed to fetch the post!\n${e}`,
|
||||
});
|
||||
}
|
||||
|
||||
return c.json(data);
|
||||
};
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import { Handler } from "hono";
|
||||
import { HTTPException } from "hono/http-exception";
|
||||
import { fetchProfile } from "../lib/fetchProfile";
|
||||
import { Profile } from "../components/Profile";
|
||||
import { Handler } from 'hono';
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
import { fetchProfile } from '../lib/fetchProfile';
|
||||
import { Profile } from '../components/Profile';
|
||||
|
||||
export const getProfile: Handler<
|
||||
Env,
|
||||
"/profile/:user" | "/https://bsky.app/profile/:user"
|
||||
'/profile/:user' | '/https://bsky.app/profile/:user'
|
||||
> = async (c) => {
|
||||
const { user } = c.req.param();
|
||||
const agent = c.get("Agent");
|
||||
const { data, success } = await fetchProfile(agent, { user });
|
||||
if (!success) {
|
||||
const agent = c.get('Agent');
|
||||
try {
|
||||
var { data } = await fetchProfile(agent, { user });
|
||||
} catch (e) {
|
||||
throw new HTTPException(500, {
|
||||
message: "Failed to fetch the profile!",
|
||||
message: `Failed to fetch the profile!\n${e}`,
|
||||
});
|
||||
}
|
||||
|
||||
return c.html(
|
||||
<Profile
|
||||
profile={data}
|
||||
url={c.req.path}
|
||||
appDomain={c.env.VIXBLUESKY_APP_DOMAIN}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
/** @jsx jsx */
|
||||
import { jsx } from "hono/jsx";
|
||||
import { Handler } from "hono";
|
||||
import { HTTPException } from "hono/http-exception";
|
||||
import { fetchProfile } from "../lib/fetchProfile";
|
||||
import { jsx } from 'hono/jsx';
|
||||
import { Handler } from 'hono';
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
import { fetchProfile } from '../lib/fetchProfile';
|
||||
|
||||
export const getProfileData: Handler<
|
||||
Env,
|
||||
"/profile/:user/json" | "/https://bsky.app/profile/:user/json"
|
||||
'/profile/:user/json' | '/https://bsky.app/profile/:user/json'
|
||||
> = async (c) => {
|
||||
const { user } = c.req.param();
|
||||
const agent = c.get("Agent");
|
||||
const { data, success } = await fetchProfile(agent, { user });
|
||||
if (!success) {
|
||||
const agent = c.get('Agent');
|
||||
try {
|
||||
var { data } = await fetchProfile(agent, { user });
|
||||
} catch (e) {
|
||||
throw new HTTPException(500, {
|
||||
message: "Failed to fetch the profile!",
|
||||
message: `Failed to fetch the profile!\n${e}`,
|
||||
});
|
||||
}
|
||||
|
||||
return c.json(data);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user