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) // } // }