Something went wrong. Try again.
Browser extension: detect and subscribe to standard.site publications on ATProto
Something went wrong. Try again.
7.0 kB · 170 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171// The account behind a publication.//// A publication record says nothing about who wrote it beyond the repo it// lives in, so the card is assembled from that account's own data: its DID// document for the handle, and its `app.bsky.actor.profile` record — read// straight off its PDS — for the display name, bio and avatar. An account// that has never touched Bluesky still has a DID document, so it still gets// a card; it just has less on it.//// The handle is the part worth being careful with. A DID document's// `alsoKnownAs` is written by whoever controls the DID, so on its own it is a// claim, not a fact: an impostor can put any handle in it. It is only true if// the handle resolves back to the same DID, which is the same bidirectional// shape detection already uses for publications (a well-known that names the// record that names the site). An unproven handle is shown as the DID// instead of being shown as somebody else's name.
import { getRecord, isBlobRef, blobUrl, resolveDid, resolveHandleToDid } from './atproto'import type { Viewer } from './subscribers'import type { BlobRef } from './types'
// `profileAvatarUrl` in atproto.ts reads this same record for the signed-in// account, where the avatar is the only field the header has room for. This// module is the publication owner's version, which needs the rest of it.export const PROFILE_COLLECTION = 'app.bsky.actor.profile'
export interface Owner { did: string /** Only set once it has been proven to point back at `did`. */ handle?: string /** The handle the DID document claimed, whether or not it checked out. */ claimedHandle?: string displayName?: string description?: string avatarUrl?: string}
interface ProfileRecord { displayName?: string description?: string avatar?: unknown}
/** * Whether `handle` really belongs to `did`. Both halves have to agree: the * DID document claims the handle, and resolving the handle returns the DID. */export async function handleMatches(handle: string, did: string): Promise<boolean> { try { return (await resolveHandleToDid(handle)) === did } catch (err) { console.debug('[substandard] handle did not resolve', handle, err) return false }}
/** * Everything the card shows, from the owner's own PDS. Never throws: the card * is decoration on a publication that has already been verified, so a missing * profile record, an unreachable PDS or an unresolvable handle each cost a * field rather than the card. */export async function fetchOwner(did: string): Promise<Owner> { const owner: Owner = { did } let pds: string try { const resolved = await resolveDid(did) pds = resolved.pds owner.claimedHandle = resolved.handle } catch (err) { console.debug('[substandard] could not resolve publication owner', did, err) return owner }
const [verified, profile] = await Promise.all([ owner.claimedHandle ? handleMatches(owner.claimedHandle, did) : Promise.resolve(false), getRecord<ProfileRecord>(pds, did, PROFILE_COLLECTION, 'self').catch((err) => { console.debug('[substandard] no profile record for', did, err) return undefined }), ])
if (verified) owner.handle = owner.claimedHandle if (profile) { owner.displayName = clean(profile.value.displayName) owner.description = clean(profile.value.description) if (isBlobRef(profile.value.avatar)) { owner.avatarUrl = blobUrl(pds, did, profile.value.avatar as BlobRef) } } return owner}
/** * A profile record is third-party data typed as a string. Anything that is * not one is dropped, and what is left is trimmed — a display name that is * all whitespace would otherwise render as an empty line where a name goes. */function clean(value: unknown): string | undefined { if (typeof value !== 'string') return undefined const trimmed = value.trim() return trimmed || undefined}
/** What to call the owner: their name, their proven handle, or their DID. */export function ownerName(owner: Owner): string { return owner.displayName ?? (owner.handle ? `@${owner.handle}` : owner.did)}
/** * The line under the name. A handle that did not prove itself is not shown at * all — showing it greyed out still shows it, and this is the one place the * popup could be made to vouch for a name that is not the account's. */export function ownerSubtitle(owner: Owner): string | undefined { if (owner.handle) return `@${owner.handle}` return owner.displayName ? owner.did : undefined}
/** * Whether the card says the reader follows this account. * * Four ways the answer is no, and only one of them is "you do not follow * them": * * - A blocked account never gets the line. Blocking somebody does not delete * the follow record in the blocker's repo — the appview stops honouring it, * but the record is still there, so reading follows straight off the repo * can say "Following" about an account the reader has decided not to be * shown. That reading is the wrong one to publish, and it would sit next * to a card that is already refusing to show them (see blockedIdentity). * - No viewer: signed out, or the follow list could not be read. Neither is * evidence of not following, and the line is only ever drawn as a fact. * - The reader's own publication. Nobody follows themselves, and the line * would read as a broken one rather than as an answer. * - A follow set that stopped at its cap (`viewer.truncated`). Absence from * a prefix is not absence. Nothing extra is needed to handle it — a hit is * still a hit, and a miss already draws nothing — but it is the reason the * missing line must never be read as "not following". */export function showsFollowing(owner: Owner, blocked: boolean, viewer?: Viewer): boolean { if (blocked || !viewer || viewer.did === owner.did) return false return viewer.following.has(owner.did)}
/** What the card calls an account the reader has blocked. */export const BLOCKED_NAME = 'Blocked user'
/** * The card for an account the reader has blocked, which is a card that does not * show them: no avatar, no display name they chose, no bio. A block is a * decision not to be shown someone, and the popup putting their face and their * words on the screen anyway — on a page the reader landed on without asking — * is the one thing it must not do here. * * The handle stays. It is not the account presenting itself; it is which * account this is, which is what makes the block reviewable (and it is already * in the status pill above the card, where the profile link is). * * The publication is untouched by this: its name, icon and description are the * publication's, and the block is on the account, not on it. */export function blockedIdentity(owner: Owner): Owner { return { did: owner.did, handle: owner.handle, claimedHandle: owner.claimedHandle, displayName: BLOCKED_NAME, }}