chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// The public web page that displays a record, when one is known. The mapping
// from an AT-URI to an app's web URL is app-specific and hand-kept, so only the
// collections with a known page get a link; everything else gets none, and the
// card simply shows no link. Apps are added here one at a time as their public
// URL schemes are pinned down.
const BSKY = 'https://bsky.app';
const GRAIN = 'https://grain.social';
const TANGLED = 'https://tangled.org';
/**
* `at://<did>/<collection>/<rkey>` addresses one record. Split it into its
* parts, or null if it isn't a full record URI.
* @param {string} uri
* @returns {{ did: string, collection: string, rkey: string } | null}
*/
export function parseAtUri(uri) {
if (typeof uri !== 'string' || !uri.startsWith('at://')) return null;
const [did, collection, rkey] = uri.slice('at://'.length).split('/');
return did && collection && rkey ? { did, collection, rkey } : null;
}
// A post/list/feed AT-URI -> its bsky.app page. Likes, reposts, and the like
// point *at* one of these rather than having a page of their own.
/** @param {string} uri */
function atUriToBsky(uri) {
const target = parseAtUri(uri);
if (!target) return null;
const { did, collection, rkey } = target;
switch (collection) {
case 'app.bsky.feed.post':
return `${BSKY}/profile/${did}/post/${rkey}`;
case 'app.bsky.graph.list':
return `${BSKY}/profile/${did}/lists/${rkey}`;
case 'app.bsky.feed.generator':
return `${BSKY}/profile/${did}/feed/${rkey}`;
default:
return null;
}
}
// A person's profile page, from the DID a follow/block/listitem points at.
/** @param {unknown} did */
function bskyProfile(did) {
return typeof did === 'string' && did.startsWith('did:')
? `${BSKY}/profile/${did}`
: null;
}
// A gallery or profile Grain AT-URI -> its grain.social page. A favorite has no
// page of its own; it points at the gallery it favorited.
/** @param {string} uri */
function atUriToGrain(uri) {
const target = parseAtUri(uri);
if (!target) return null;
const { did, collection, rkey } = target;
switch (collection) {
case 'social.grain.gallery':
return `${GRAIN}/profile/${did}/gallery/${rkey}`;
case 'social.grain.actor.profile':
return `${GRAIN}/profile/${did}`;
default:
return null;
}
}
/**
* The public web page for a record, or null when none is known. Records that
* have a page of their own link to it; a like or repost links the post it
* points at (its `subject`), and a follow/block/listitem links that person's
* profile.
* @param {string} uri the record's own AT-URI
* @param {any} value the record body, for the subject-pointing types
* @returns {string | null}
*/
export function recordWebLink(uri, value) {
const target = parseAtUri(uri);
if (!target) return null;
const { did, collection, rkey } = target;
switch (collection) {
case 'app.bsky.feed.post':
case 'app.bsky.graph.list':
case 'app.bsky.feed.generator':
return atUriToBsky(uri);
case 'app.bsky.actor.profile':
return bskyProfile(did);
case 'app.bsky.graph.starterpack':
return `${BSKY}/starter-pack/${did}/${rkey}`;
// A like or repost has no page; link the post it points at.
case 'app.bsky.feed.like':
case 'app.bsky.feed.repost':
return atUriToBsky(value?.subject?.uri);
// These point at a person; link their profile.
case 'app.bsky.graph.follow':
case 'app.bsky.graph.block':
case 'app.bsky.graph.listitem':
return bskyProfile(value?.subject);
// Grain — a photo app. Galleries and profiles have their own page.
case 'social.grain.gallery':
case 'social.grain.actor.profile':
return atUriToGrain(uri);
// A favorite has no page; link the gallery it points at. The subject is a
// plain at-uri string here, not a strongRef object.
case 'social.grain.favorite':
return atUriToGrain(value?.subject);
// Tangled — git on atproto; tangled.org takes a DID in the path. A repo's
// URL is keyed by its cosmetic `name`, not its rkey, so the repo record
// itself links (it carries the name) but a star can't — a star only
// references the repo by knot/DID/at-uri, never the name.
case 'sh.tangled.repo':
return value?.name ? `${TANGLED}/${did}/${value.name}` : null;
case 'sh.tangled.graph.follow':
return typeof value?.subject === 'string' &&
value.subject.startsWith('did:')
? `${TANGLED}/${value.subject}`
: null;
default:
return null;
}
}