diff --git a/lib/v2/bsky/maintainer.js b/lib/v2/bsky/maintainer.js index 61f792cbb..f7a7bde20 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 000000000..c53c59fb9 --- /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 ef04b280a..70a5f2d0d 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 3d4d1c62d..ed6101bd4 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 000000000..06b42960a --- /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 000000000..09f5a9f6e --- /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 7246fb282..2fad79f99 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}