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 }} + {{ i.alt }}
+ {{ /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