Bluesky support

This commit is contained in:
Dakedres 2024-04-11 13:21:33 -06:00
parent 80a082fd45
commit 694184d728
2 changed files with 230 additions and 3 deletions

8
lib.js
View File

@ -121,7 +121,7 @@ export const retryDelayedFetch = async (url, options, courtesyWait, retryAttempt
// | `-. `-.
// ' `-' `-'
export async function fetchChannel(source) {
export const fetchChannel = async (source) => {
let { hostname } = source
let error
let response
@ -206,9 +206,11 @@ export const createPosts = async (channel, source, fromDate, reducerCallback) =>
}
export const createPost = (item, source) => {
let description = new JSDOM(item.querySelector('description').textContent).window.document
let description = item.querySelector('description')
description = description === null ? '' : new JSDOM(description.textContent).window.document
let date = readPubDate(item.querySelector('pubDate'))
let link = item.querySelector('link').textContent
let link = item.querySelector('link')?.textContent
let guid = item.querySelector('guid')?.textContent
let title = item.querySelector('title')?.textContent

225
platforms/bluesky.js Normal file
View File

@ -0,0 +1,225 @@
import Path from "path"
import { createSource, createSourceOptions, downloadImages, fetchChannel, getPostIdFromPathname, isUnset } from "../lib.js"
import { channel } from "diagnostics_channel"
class BlueskyError extends Error {}
let bluesky = {}
bluesky.createEndpoint = (domain, method) => {
let endpoint = new URL('https://' + domain)
endpoint.pathname = Path.join('xrpc', method)
return endpoint
}
bluesky.createHeaders = (client) => {
return {
// 'Accept': 'application/json',
'Authorization': 'Bearer ' + client.accessJwt
}
}
bluesky.handleRequest = response => {
return response
.then(response => {
response = response.json()
if(response.error) {
throw new BlueskyError(`${response.error}: ${response.message}`)
}
return response
})
.catch(err => {
throw new BlueskyError(err)
})
}
bluesky.login = async (handle, password, domain = 'bsky.social') => {
let body = { identifier: handle, password }
let endpoint = bluesky.createEndpoint(domain, 'com.atproto.server.createSession')
let { accessJwt, refreshJwt } = await bluesky.handleRequest(
fetch(endpoint.href, {
method: 'POST',
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(body)
})
)
return {
domain,
accessJwt,
refreshJwt
}
}
const getPostUriChunks = channel => {
let guids = channel.querySelectorAll('guid')
let storedUris = new Set()
let chunks = []
let uris = []
let index = 0
for(let guid of guids) {
let uri = guid?.textContent
if(!uri || storedUris.has(uri))
continue
if(index !== 0 && index % 25 === 0) {
chunks.push(uris)
uris = []
}
uris.push(uri)
storedUris.add(uri)
index++
}
chunks.push(uris)
return chunks
}
bluesky.fetchAnnotations = async (client, channel) => {
let chunks = getPostUriChunks(channel)
let annotations = new Map()
for(let uris of chunks) {
let endpoint = bluesky.createEndpoint(client.domain, 'app.bsky.feed.getPosts')
for(let uri of uris)
endpoint.searchParams.append('uris', uri)
let { posts } = await bluesky.handleRequest(
fetch(endpoint.href, {
headers: bluesky.createHeaders(client)
})
)
for(let post of posts) {
annotations.set(post.uri, post)
}
}
return annotations
}
export default bluesky
bluesky.createSource = (handle, options = {}, client, postReducerCallback, cache) => {
let startOfServerHostname = handle.indexOf('.')
let user = handle.slice(0, startOfServerHostname)
let serverHostname = handle.slice(startOfServerHostname + 1)
let source = {
type: 'bluesky',
description: `Aggregate feed for @${user} on bluesky`,
hostname: 'bsky.app',
pathname: 'profile/' + handle + "/rss",
name: `${serverHostname}-${user}`,
displayName: handle,
user,
handle,
postsToPopulate: new Map(),
postsChecked: new Set(),
...createSourceOptions(options)
}
return createSource(
source,
isUnset(client) ? fetchChannel : bluesky.fetchAnnotatedChannelFor(client),
postReducerCallback,
cache
)
}
bluesky.fetchAnnotatedChannelFor = (client) => async (source) => {
let channel = await fetchChannel(source)
source.postAnnotations = await bluesky.fetchAnnotations(client, channel)
return channel
}
bluesky.isRepost = (post) => {
let annotation = post.source.postAnnotations.get(post.guid)
let beenChecked = post.source.postsChecked.has(post.guid)
post.source.postsChecked.add(post.guid)
return annotation ?
beenChecked && annotation.author.handle === post.source.handle :
beenChecked
}
bluesky.pullImages = async (post, view, discardPostIfNoImages) => {
let annotation = post.source.postAnnotations.get(post.guid)
let noImages = isUnset(annotation.embed)
|| annotation.embed.type === 'app.bsky.embed.images#view'
if(isUnset(annotation) || noImages) {
return discardPostIfNoImages ? null : post
}
post.images = await downloadImages(
annotation.embed.images.map(image => image.fullsize),
post.source,
getPostIdFromPathname(post),
view
)
return post
}
// export const bluesky = {
// login(identifier, password) {
// },
// createSource(usertag, options, postReducerCallback) {
// let startOfServerHostname = usertag.indexOf('.')
// let user = usertag.slice(0, startOfServerHostname)
// let serverHostname = usertag.slice(startOfServerHostname + 1)
// let source = {
// type: 'bluesky',
// description: `Aggregate feed for @${user} on bluesky`,
// hostname: 'bsky.app',
// pathname: 'profile/' + usertag + "/rss",
// name: `${serverHostname}-${user}`,
// displayName: usertag,
// user,
// postsToPopulate: new Map(),
// ...createSourceOptions(options)
// }
// return createSource(source, fetchRss, postReducerCallback)
// },
// isRepost(post) {
// // Bluesky's rss does not provide retweets/retoots
// return false
// },
// async pullImages(post, view, discardPostIfNoImages) {
// let getPostsEndpoint = new URL('https://bsky.social/xrpc/app.bsky.feed.getPosts')
// getPostsEndpoint.searchParams.set('uris', [ post.guid ])
// console.log(getPostsEndpoint)
// // fetch(getPostsEndpoint)
// // post.source.postsToPopulate.set(post.guid, {
// // discardPostIfNoImages,
// // view,
// // post
// // })
// },
// populatePosts(source) {
// console.log(getPostsEndpoint)
// }
// }