diff --git a/lib/v2/bsky/maintainer.js b/lib/v2/bsky/maintainer.js
index 61f792cbb44c2d..f7a7bde201b422 100644
--- a/lib/v2/bsky/maintainer.js
+++ b/lib/v2/bsky/maintainer.js
@@ -1,3 +1,4 @@
module.exports = {
'/keyword/:keyword': ['untitaker'],
+ '/profile/:handle': ['TonyRL'],
};
diff --git a/lib/v2/bsky/posts.js b/lib/v2/bsky/posts.js
new file mode 100644
index 00000000000000..c53c59fb9e4a36
--- /dev/null
+++ b/lib/v2/bsky/posts.js
@@ -0,0 +1,42 @@
+const { parseDate } = require('@/utils/parse-date');
+const { resolveHandle, getProfile, getAuthorFeed } = require('./utils');
+const { art } = require('@/utils/render');
+const { join } = require('path');
+
+module.exports = async (ctx) => {
+ const { handle } = ctx.params;
+ const DID = await resolveHandle(handle, ctx.cache.tryGet);
+ const profile = await getProfile(DID, ctx.cache.tryGet);
+ const authorFeed = await getAuthorFeed(DID, ctx.cache.tryGet);
+
+ const items = authorFeed.feed.map(({ post }) => ({
+ title: post.record.text.split('\n')[0],
+ description: art(join(__dirname, 'templates/post.art'), {
+ text: post.record.text.replace(/\n/g, '
'),
+ embed: post.embed,
+ // embed.$type "app.bsky.embed.record#view" and "app.bsky.embed.recordWithMedia#view"
+ // are not handled
+ }),
+ author: post.author.displayName,
+ pubDate: parseDate(post.record.createdAt),
+ link: `https://bsky.app/profile/${post.author.handle}/post/${post.uri.split('app.bsky.feed.post/')[1]}`,
+ upvotes: post.likeCount,
+ comments: post.replyCount,
+ }));
+
+ ctx.state.data = {
+ title: `${profile.displayName} (@${profile.handle}) — Bluesky`,
+ description: profile.description.replace(/\n/g, ' '),
+ link: `https://bsky.app/profile/${profile.handle}`,
+ image: profile.banner,
+ icon: profile.avatar,
+ logo: profile.avatar,
+ item: items,
+ };
+
+ ctx.state.json = {
+ DID,
+ profile,
+ authorFeed,
+ };
+};
diff --git a/lib/v2/bsky/radar.js b/lib/v2/bsky/radar.js
index ef04b280ad8e7f..70a5f2d0d1ef4c 100644
--- a/lib/v2/bsky/radar.js
+++ b/lib/v2/bsky/radar.js
@@ -8,6 +8,12 @@ module.exports = {
source: '/search',
target: (params, url) => `/bsky/keyword/${new URL(url).searchParams.get('q')}`,
},
+ {
+ title: 'Post',
+ docs: 'https://docs.rsshub.app/routes/social-media#bluesky-bsky',
+ source: '/profile/:handle',
+ target: '/bsky/profile/:handle',
+ },
],
},
};
diff --git a/lib/v2/bsky/router.js b/lib/v2/bsky/router.js
index 3d4d1c62da6d8d..ed6101bd47b048 100644
--- a/lib/v2/bsky/router.js
+++ b/lib/v2/bsky/router.js
@@ -1,3 +1,4 @@
module.exports = (router) => {
router.get('/keyword/:keyword', require('./keyword'));
+ router.get('/profile/:handle', require('./posts'));
};
diff --git a/lib/v2/bsky/templates/post.art b/lib/v2/bsky/templates/post.art
new file mode 100644
index 00000000000000..06b42960a4de92
--- /dev/null
+++ b/lib/v2/bsky/templates/post.art
@@ -0,0 +1,15 @@
+{{ if text }}
+ {{@ text }}
+{{ /if }}
+
+{{ if embed }}
+ {{ if embed.$type == 'app.bsky.embed.images#view'}}
+ {{ each embed.images i }}
+
+ {{ /each }}
+ {{ else if embed.$type == 'app.bsky.embed.external#view' }}
+ {{ embed.external.title }}
+ {{ embed.external.description }}
+
+ {{ /if }}
+{{ /if }}
diff --git a/lib/v2/bsky/utils.js b/lib/v2/bsky/utils.js
new file mode 100644
index 00000000000000..09f5a9f6e8f7c0
--- /dev/null
+++ b/lib/v2/bsky/utils.js
@@ -0,0 +1,52 @@
+const got = require('@/utils/got');
+const config = require('@/config').value;
+
+/**
+ * docs: https://atproto.com/lexicons/app-bsky
+ */
+
+// https://github.com/bluesky-social/atproto/blob/main/lexicons/com/atproto/identity/resolveHandle.json
+const resolveHandle = (handle, tryGet) =>
+ tryGet(`bsky:${handle}`, async () => {
+ const { data } = await got('https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle', {
+ searchParams: {
+ handle,
+ },
+ });
+ return data.did;
+ });
+
+// https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/actor/getProfile.json
+const getProfile = (did, tryGet) =>
+ tryGet(`bsky:profile:${did}`, async () => {
+ const { data } = await got('https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile', {
+ searchParams: {
+ actor: did,
+ },
+ });
+ return data;
+ });
+
+// https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/feed/getAuthorFeed.json
+const getAuthorFeed = (did, tryGet) =>
+ tryGet(
+ `bsky:authorFeed:${did}`,
+ async () => {
+ const { data } = await got('https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed', {
+ searchParams: {
+ actor: did,
+ filter: 'posts_and_author_threads',
+ limit: 30,
+ },
+ });
+ return data;
+ },
+ config.cache.routeExpire,
+ false
+ );
+
+module.exports = {
+ resolveHandle,
+ getProfile,
+ getAuthorFeed,
+};
diff --git a/website/docs/routes/social-media.mdx b/website/docs/routes/social-media.mdx
index 7246fb28213bb9..2fad79f99b6a91 100644
--- a/website/docs/routes/social-media.mdx
+++ b/website/docs/routes/social-media.mdx
@@ -339,6 +339,10 @@
## Bluesky (bsky) {#bluesky-bsky}
+### Post {#bluesky-bsky-post}
+
+
+
### Keywords {#bluesky-bsky-keywords}
diff --git a/website/package.json b/website/package.json
index bf827ad88932fb..0e6235b43414b3 100644
--- a/website/package.json
+++ b/website/package.json
@@ -36,7 +36,7 @@
"@docusaurus/types": "3.0.1",
"@types/markdown-it": "^13.0.7",
"@types/mdx-js__react": "^1.5.8",
- "@types/react": "^18.2.45",
+ "@types/react": "^18.2.46",
"@types/react-dom": "^18.2.18",
"typescript": "^5.3.3"
},
diff --git a/website/pnpm-lock.yaml b/website/pnpm-lock.yaml
index 81fdb8f800e4df..6c9066df733729 100644
--- a/website/pnpm-lock.yaml
+++ b/website/pnpm-lock.yaml
@@ -19,10 +19,10 @@ dependencies:
version: 3.0.1(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3)
'@docusaurus/preset-classic':
specifier: 3.0.1
- version: 3.0.1(@algolia/client-search@4.20.0)(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.11.0)(typescript@5.3.3)
+ version: 3.0.1(@algolia/client-search@4.20.0)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.11.0)(typescript@5.3.3)
'@mdx-js/react':
specifier: ^3.0.0
- version: 3.0.0(@types/react@18.2.45)(react@18.2.0)
+ version: 3.0.0(@types/react@18.2.46)(react@18.2.0)
clsx:
specifier: ^2.0.0
version: 2.0.0
@@ -62,8 +62,8 @@ devDependencies:
specifier: ^1.5.8
version: 1.5.8
'@types/react':
- specifier: ^18.2.45
- version: 18.2.45
+ specifier: ^18.2.46
+ version: 18.2.46
'@types/react-dom':
specifier: ^18.2.18
version: 18.2.18
@@ -1580,7 +1580,7 @@ packages:
resolution: {integrity: sha512-SPiDHaWKQZpwR2siD0KQUwlStvIAnEyK6tAE2h2Wuoq8ue9skzhlyVQ1ddzOxX6khULnAALDiR/isSF3bnuciA==}
dev: false
- /@docsearch/react@3.5.2(@algolia/client-search@4.20.0)(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.11.0):
+ /@docsearch/react@3.5.2(@algolia/client-search@4.20.0)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.11.0):
resolution: {integrity: sha512-9Ahcrs5z2jq/DcAvYtvlqEBHImbm4YJI8M9y0x6Tqg598P40HTEkX7hsMcIuThI+hTFxRGZ9hll0Wygm2yEjng==}
peerDependencies:
'@types/react': '>= 16.8.0 < 19.0.0'
@@ -1600,7 +1600,7 @@ packages:
'@algolia/autocomplete-core': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.11.0)
'@algolia/autocomplete-preset-algolia': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)
'@docsearch/css': 3.5.2
- '@types/react': 18.2.45
+ '@types/react': 18.2.46
algoliasearch: 4.20.0
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@@ -1939,7 +1939,7 @@ packages:
'@docusaurus/react-loadable': 5.5.2(react@18.2.0)
'@docusaurus/types': 3.0.1(react-dom@18.2.0)(react@18.2.0)
'@types/history': 4.7.11
- '@types/react': 18.2.45
+ '@types/react': 18.2.46
'@types/react-router-config': 5.0.10
'@types/react-router-dom': 5.3.3
react: 18.2.0
@@ -2320,7 +2320,7 @@ packages:
- webpack-cli
dev: false
- /@docusaurus/preset-classic@3.0.1(@algolia/client-search@4.20.0)(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.11.0)(typescript@5.3.3):
+ /@docusaurus/preset-classic@3.0.1(@algolia/client-search@4.20.0)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.11.0)(typescript@5.3.3):
resolution: {integrity: sha512-il9m9xZKKjoXn6h0cRcdnt6wce0Pv1y5t4xk2Wx7zBGhKG1idu4IFHtikHlD0QPuZ9fizpXspXcTzjL5FXc1Gw==}
engines: {node: '>=18.0'}
peerDependencies:
@@ -2336,9 +2336,9 @@ packages:
'@docusaurus/plugin-google-gtag': 3.0.1(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3)
'@docusaurus/plugin-google-tag-manager': 3.0.1(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3)
'@docusaurus/plugin-sitemap': 3.0.1(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3)
- '@docusaurus/theme-classic': 3.0.1(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3)
+ '@docusaurus/theme-classic': 3.0.1(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3)
'@docusaurus/theme-common': 3.0.1(@docusaurus/types@3.0.1)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3)
- '@docusaurus/theme-search-algolia': 3.0.1(@algolia/client-search@4.20.0)(@docusaurus/types@3.0.1)(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.11.0)(typescript@5.3.3)
+ '@docusaurus/theme-search-algolia': 3.0.1(@algolia/client-search@4.20.0)(@docusaurus/types@3.0.1)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.11.0)(typescript@5.3.3)
'@docusaurus/types': 3.0.1(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@@ -2368,11 +2368,11 @@ packages:
peerDependencies:
react: '*'
dependencies:
- '@types/react': 18.2.45
+ '@types/react': 18.2.46
prop-types: 15.8.1
react: 18.2.0
- /@docusaurus/theme-classic@3.0.1(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3):
+ /@docusaurus/theme-classic@3.0.1(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3):
resolution: {integrity: sha512-XD1FRXaJiDlmYaiHHdm27PNhhPboUah9rqIH0lMpBt5kYtsGjJzhqa27KuZvHLzOP2OEpqd2+GZ5b6YPq7Q05Q==}
engines: {node: '>=18.0'}
peerDependencies:
@@ -2391,7 +2391,7 @@ packages:
'@docusaurus/utils': 3.0.1(@docusaurus/types@3.0.1)
'@docusaurus/utils-common': 3.0.1(@docusaurus/types@3.0.1)
'@docusaurus/utils-validation': 3.0.1(@docusaurus/types@3.0.1)
- '@mdx-js/react': 3.0.0(@types/react@18.2.45)(react@18.2.0)
+ '@mdx-js/react': 3.0.0(@types/react@18.2.46)(react@18.2.0)
clsx: 2.0.0
copy-text-to-clipboard: 3.2.0
infima: 0.2.0-alpha.43
@@ -2440,7 +2440,7 @@ packages:
'@docusaurus/utils': 3.0.1(@docusaurus/types@3.0.1)
'@docusaurus/utils-common': 3.0.1(@docusaurus/types@3.0.1)
'@types/history': 4.7.11
- '@types/react': 18.2.45
+ '@types/react': 18.2.46
'@types/react-router-config': 5.0.10
clsx: 2.0.0
parse-numeric-range: 1.3.0
@@ -2468,14 +2468,14 @@ packages:
- webpack-cli
dev: false
- /@docusaurus/theme-search-algolia@3.0.1(@algolia/client-search@4.20.0)(@docusaurus/types@3.0.1)(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.11.0)(typescript@5.3.3):
+ /@docusaurus/theme-search-algolia@3.0.1(@algolia/client-search@4.20.0)(@docusaurus/types@3.0.1)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.11.0)(typescript@5.3.3):
resolution: {integrity: sha512-DDiPc0/xmKSEdwFkXNf1/vH1SzJPzuJBar8kMcBbDAZk/SAmo/4lf6GU2drou4Ae60lN2waix+jYWTWcJRahSA==}
engines: {node: '>=18.0'}
peerDependencies:
react: ^18.0.0
react-dom: ^18.0.0
dependencies:
- '@docsearch/react': 3.5.2(@algolia/client-search@4.20.0)(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.11.0)
+ '@docsearch/react': 3.5.2(@algolia/client-search@4.20.0)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.11.0)
'@docusaurus/core': 3.0.1(@docusaurus/types@3.0.1)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3)
'@docusaurus/logger': 3.0.1
'@docusaurus/plugin-content-docs': 3.0.1(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3)
@@ -2534,7 +2534,7 @@ packages:
react-dom: ^18.0.0
dependencies:
'@types/history': 4.7.11
- '@types/react': 18.2.45
+ '@types/react': 18.2.46
commander: 5.1.0
joi: 17.11.0
react: 18.2.0
@@ -2557,7 +2557,7 @@ packages:
react-dom: ^18.0.0
dependencies:
'@types/history': 4.7.11
- '@types/react': 18.2.45
+ '@types/react': 18.2.46
commander: 5.1.0
joi: 17.11.0
react: 18.2.0
@@ -2796,14 +2796,14 @@ packages:
- supports-color
dev: false
- /@mdx-js/react@3.0.0(@types/react@18.2.45)(react@18.2.0):
+ /@mdx-js/react@3.0.0(@types/react@18.2.46)(react@18.2.0):
resolution: {integrity: sha512-nDctevR9KyYFyV+m+/+S4cpzCWHqj+iHDHq3QrsWezcC+B17uZdIWgCguESUkwFhM3n/56KxWVE3V6EokrmONQ==}
peerDependencies:
'@types/react': '>=16'
react: '>=16'
dependencies:
'@types/mdx': 2.0.9
- '@types/react': 18.2.45
+ '@types/react': 18.2.46
react: 18.2.0
dev: false
@@ -3278,7 +3278,7 @@ packages:
/@types/mdx-js__react@1.5.8:
resolution: {integrity: sha512-iLQL8JZ4AZ+rpZvGUsQwENffpsSCMLYB8kE6OhGasLmdYn7aSLq53uOvZrKx5FM+hymE2nm08HDfq7tFx02ElA==}
dependencies:
- '@types/react': 18.2.45
+ '@types/react': 18.2.46
dev: true
/@types/mdx@2.0.10:
@@ -3338,31 +3338,31 @@ packages:
/@types/react-dom@18.2.18:
resolution: {integrity: sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==}
dependencies:
- '@types/react': 18.2.45
+ '@types/react': 18.2.46
dev: true
/@types/react-router-config@5.0.10:
resolution: {integrity: sha512-Wn6c/tXdEgi9adCMtDwx8Q2vGty6TsPTc/wCQQ9kAlye8UqFxj0vGFWWuhywNfkwqth+SOgJxQTLTZukrqDQmQ==}
dependencies:
'@types/history': 4.7.11
- '@types/react': 18.2.45
+ '@types/react': 18.2.46
'@types/react-router': 5.1.20
/@types/react-router-dom@5.3.3:
resolution: {integrity: sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==}
dependencies:
'@types/history': 4.7.11
- '@types/react': 18.2.45
+ '@types/react': 18.2.46
'@types/react-router': 5.1.20
/@types/react-router@5.1.20:
resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==}
dependencies:
'@types/history': 4.7.11
- '@types/react': 18.2.45
+ '@types/react': 18.2.46
- /@types/react@18.2.45:
- resolution: {integrity: sha512-TtAxCNrlrBp8GoeEp1npd5g+d/OejJHFxS3OWmrPBMFaVQMSN0OFySozJio5BHxTuTeug00AVXVAjfDSfk+lUg==}
+ /@types/react@18.2.46:
+ resolution: {integrity: sha512-nNCvVBcZlvX4NU1nRRNV/mFl1nNRuTuslAJglQsq+8ldXe5Xv0Wd2f7WTE3jOxhLH2BFfiZGC6GCp+kHQbgG+w==}
dependencies:
'@types/prop-types': 15.7.9
'@types/scheduler': 0.16.5