Something went wrong. Try again.
semantic bufo search find-bufo.com
bufo
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302// the bufo post viewer for the bot stats page: click a matched bufo, see every// quote-post the bot made with it; look up a handle, see every post of theirs// that got a bufo.//// data path, no bluesky appview anywhere:// index /posts/<bufo>.json and /posts/by-did/<did>.json — the bot's own// index of every quote-post it has made (posts_index.zig)// live slingshot.microcosm.blue — quoted post record, author profile// (with self-labels), handle — fetched as posts scroll into view, so// a `!no-unauthenticated` author is hidden at the moment of viewing// and a deleted post reads as gone
const SLINGSHOT = 'https://slingshot.microcosm.blue/xrpc';const BOT_HANDLE = 'find-bufo.com';
// preferred atproto client — mirrors the shared registry in status / plyr.fm// (@zzstoatzz.io); keep in sync with those rather than inventing entries.// same localStorage key as those apps, so the choice reads the same way.const bskyStylePost = (base) => (did, rkey) => `${base}/profile/${did}/post/${rkey}`;const AT_CLIENTS = [ { value: 'blacksky', label: 'blacksky', iconUrl: 'https://blacksky.community/static/apple-touch-icon.png', profileUrl: (h) => `https://blacksky.community/profile/${h}`, postUrl: bskyStylePost('https://blacksky.community') }, { value: 'witchsky', label: 'witchsky', iconUrl: 'https://witchsky.app/favicon.ico', profileUrl: (h) => `https://witchsky.app/profile/${h}`, postUrl: bskyStylePost('https://witchsky.app') }, { value: 'mu', label: 'mu', iconUrl: 'https://mu.social/pwa/apple-touch-icon/apple-touch-icon-180.png', profileUrl: (h) => `https://mu.social/profile/${h}`, postUrl: bskyStylePost('https://mu.social') }, { value: 'reddwarf', label: 'red dwarf', iconUrl: 'https://reddwarf.app/redstar.png', profileUrl: (h) => `https://reddwarf.app/profile/${h}`, postUrl: bskyStylePost('https://reddwarf.app') }, { value: 'pdsls', label: 'pdsls', iconUrl: 'https://pdsls.dev/favicon.ico', profileUrl: (h) => `https://pdsls.dev/at/${h}`, recordUrl: (uri) => `https://pdsls.dev/at/${uri.replace(/^at:\/\//, '')}` },];const CLIENT_KEY = 'atprotoClient';const storedClient = () => { try { return localStorage.getItem(CLIENT_KEY); } catch { return null; } };let client = AT_CLIENTS.find((c) => c.value === storedClient()) ?? AT_CLIENTS.find((c) => c.value === 'pdsls');
/** a post in the chosen client; a record-only client gets its record view */function postLink(did, rkey) { if (client.postUrl) return client.postUrl(did, rkey); const uri = `at://${did}/app.bsky.feed.post/${rkey}`; return client.recordUrl?.(uri) ?? `https://pdsls.dev/at/${uri.slice(5)}`;}
/** an <a> to a post that re-targets itself when the client changes */function postAnchor(className, did, rkey, text) { const a = el('a', className, text ?? `open in ${client.label} ↗`); a.dataset.did = did; a.dataset.rkey = rkey; if (text == null) a.dataset.generic = '1'; a.href = postLink(did, rkey); a.target = '_blank'; a.rel = 'noopener'; return a;}
function setClient(value) { client = AT_CLIENTS.find((c) => c.value === value) ?? AT_CLIENTS.find((c) => c.value === 'pdsls'); try { localStorage.setItem(CLIENT_KEY, client.value); } catch {} for (const a of document.querySelectorAll('a[data-rkey]')) { a.href = postLink(a.dataset.did, a.dataset.rkey); if (a.dataset.generic) a.textContent = `open in ${client.label} ↗`; } for (const b of clientBar.querySelectorAll('button')) b.setAttribute('aria-pressed', String(b.dataset.value === client.value));}const NO_UNAUTHENTICATED = '!no-unauthenticated';const PAGE = 20;
const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];const fmtWhen = (sec) => { const d = new Date(sec * 1000); return `${MON[d.getUTCMonth()]} ${d.getUTCDate()}, ${d.getUTCFullYear()}`;};const el = (tag, className, text) => { const node = document.createElement(tag); if (className) node.className = className; if (text != null) node.textContent = text; return node;};
async function getJson(url) { const res = await fetch(url); if (res.status === 404 || res.status === 400) return null; if (!res.ok) throw new Error(`${res.status} ${url}`); return res.json();}
const postsFor = (name) => getJson(`/posts/${encodeURIComponent(name)}.json`);const postsQuoting = (did) => getJson(`/posts/by-did/${encodeURIComponent(did)}.json`);
// per-did identity and profile are cached for the page's lifetime: one// person quoted many times costs one lookupconst profiles = new Map();async function profileOf(did) { if (!profiles.has(did)) { profiles.set(did, (async () => { const [mini, prof] = await Promise.all([ getJson(`${SLINGSHOT}/blue.microcosm.identity.resolveMiniDoc?identifier=${encodeURIComponent(did)}`), getJson(`${SLINGSHOT}/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=app.bsky.actor.profile&rkey=self`), ]); const value = prof?.value ?? {}; const labels = (value.labels?.values ?? []).map((l) => l.val); return { did, handle: mini?.handle ?? null, displayName: typeof value.displayName === 'string' ? value.displayName : '', hidden: labels.includes(NO_UNAUTHENTICATED), }; })()); } return profiles.get(did);}
async function quotedPost(uri) { const rec = await getJson(`${SLINGSHOT}/blue.microcosm.repo.getRecordByUri?at_uri=${encodeURIComponent(uri)}`); const text = rec?.value?.text; return rec ? { text: typeof text === 'string' ? text : '', createdAt: rec.value?.createdAt ?? null } : null;}
const didOf = (uri) => (uri && uri.startsWith('at://') ? uri.slice(5).split('/')[0] : null);const rkeyOf = (uri) => (uri ? uri.slice(uri.lastIndexOf('/') + 1) : null);
/** one post row: the bot's post (date, link) and what it quoted */function postRow([rkey, sec, quotedUri, bufo], showBufo) { const row = el('article', 'pv-post'); const head = el('div', 'pv-post-head'); const when = el('div'); when.append(el('time', 'pv-when', fmtWhen(sec))); if (showBufo && bufo) when.append(el('span', 'pv-bufo', bufo)); head.append(when); head.appendChild(postAnchor('pv-open', BOT_HANDLE, rkey)); row.appendChild(head); const quote = el('div', 'pv-quote pv-quote-pending'); quote.appendChild(el('span', 'pv-muted', quotedUri ? 'loading the quoted post…' : 'no quoted post')); row.appendChild(quote); row.dataset.quoted = quotedUri ?? ''; return row;}
/** fill the quote block once the row is on screen. respects the author's * self-label: a `!no-unauthenticated` author's row is removed outright — * not their words, not their name, not even that the post exists. */async function hydrate(row) { const uri = row.dataset.quoted; const quote = row.querySelector('.pv-quote'); if (!uri) { quote.classList.remove('pv-quote-pending'); return; } const did = didOf(uri); try { const profile = await profileOf(did); quote.replaceChildren(); quote.classList.remove('pv-quote-pending'); if (profile.hidden) { row.remove(); hiddenCount += 1; viewerSub.textContent = `${baseSub} · ${hiddenCount} not shown at the author's request`; return; } const post = await quotedPost(uri); if (!post) { quote.appendChild(el('span', 'pv-muted', 'the quoted post no longer exists')); return; } const who = el('div', 'pv-who'); if (profile.displayName) who.appendChild(el('span', 'pv-name', profile.displayName)); if (profile.handle) who.appendChild(postAnchor('pv-handle', did, rkeyOf(uri), `@${profile.handle}`)); quote.appendChild(who); quote.appendChild(el('p', 'pv-text', post.text || '(no text)')); } catch (err) { quote.replaceChildren(el('span', 'pv-muted', `could not load the quoted post (${err.message})`)); quote.classList.remove('pv-quote-pending'); }}
// ---- the viewer ----
const viewer = document.getElementById('viewer');const viewerTitle = document.getElementById('viewer-title');const viewerSub = document.getElementById('viewer-sub');const viewerThumb = document.getElementById('viewer-thumb');const viewerList = document.getElementById('viewer-list');const viewerMore = document.getElementById('viewer-more');const clientBar = document.getElementById('viewer-clients');clientBar.append(el('span', 'pv-clients-label', 'open in'));for (const c of AT_CLIENTS) { const b = el('button', 'pv-client'); b.type = 'button'; b.dataset.value = c.value; b.title = c.label; b.setAttribute('aria-pressed', String(c.value === client.value)); const icon = el('img'); icon.src = c.iconUrl; icon.alt = ''; icon.loading = 'lazy'; b.append(icon, el('span', null, c.label)); b.addEventListener('click', () => setClient(c.value)); clientBar.append(b);}let posts = [];let shown = 0;let showBufoChip = false;let hiddenCount = 0;let baseSub = '';let lastFocus = null;
const observer = new IntersectionObserver((entries) => { for (const e of entries) { if (!e.isIntersecting) continue; observer.unobserve(e.target); if (e.target === viewerMore) renderMore(); else hydrate(e.target); }}, { root: viewerList, rootMargin: '200px' });
function renderMore() { const next = posts.slice(shown, shown + PAGE); for (const p of next) { const row = postRow(p, showBufoChip); viewerList.insertBefore(row, viewerMore); observer.observe(row); } shown += next.length; viewerMore.textContent = shown < posts.length ? `${posts.length - shown} more…` : ''; if (shown < posts.length) observer.observe(viewerMore);}
function openViewer({ title, sub, thumb, items, withBufo = false }) { posts = items; shown = 0; showBufoChip = withBufo; viewerTitle.textContent = title; viewerSub.textContent = sub; baseSub = sub; hiddenCount = 0; if (thumb) { viewerThumb.src = thumb; viewerThumb.alt = title; viewerThumb.hidden = false; } else viewerThumb.hidden = true; for (const node of [...viewerList.children]) if (node !== viewerMore) node.remove(); viewerMore.textContent = ''; lastFocus = document.activeElement; viewer.hidden = false; document.body.classList.add('viewer-open'); viewerList.scrollTop = 0; if (posts.length === 0) viewerList.insertBefore(el('p', 'pv-empty', 'no posts yet'), viewerMore); else renderMore(); viewer.querySelector('.pv-close').focus();}
function closeViewer() { viewer.hidden = true; document.body.classList.remove('viewer-open'); if (lastFocus && lastFocus.focus) lastFocus.focus();}
viewer.querySelector('.pv-close').addEventListener('click', closeViewer);viewer.addEventListener('click', (e) => { if (e.target === viewer) closeViewer(); });document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && !viewer.hidden) closeViewer(); });
// ---- matched bufo → its posts ----
async function showBufo(card) { const name = card.dataset.name; const thumb = card.querySelector('img')?.src; openViewer({ title: name, sub: 'loading…', thumb, items: [] }); try { const rec = await postsFor(name); const items = rec?.posts ?? []; const first = items.length ? fmtWhen(items[items.length - 1][1]) : null; openViewer({ title: name, thumb, items, sub: items.length ? `${items.length.toLocaleString()} post${items.length === 1 ? '' : 's'} by @${BOT_HANDLE} · since ${first}` : 'the bot has not posted this bufo yet', }); } catch (err) { viewerSub.textContent = `could not load posts (${err.message})`; }}
const grid = document.querySelector('.bufo-grid');grid?.addEventListener('click', (e) => { const card = e.target.closest('.bufo-card[data-name]'); if (card) showBufo(card);});grid?.addEventListener('keydown', (e) => { const card = e.target.closest('.bufo-card[data-name]'); if (card && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); showBufo(card); }});
// ---- handle → posts that quoted them ----
const lookupInput = document.getElementById('lookup-input');const lookupBtn = document.getElementById('lookup-btn');const lookupStatus = document.getElementById('lookup-status');
async function lookupUser() { const handle = lookupInput.value.trim().replace(/^@/, ''); if (!handle) return; lookupBtn.disabled = true; lookupStatus.style.display = 'block'; lookupStatus.textContent = 'resolving handle…'; try { const mini = await getJson(`${SLINGSHOT}/blue.microcosm.identity.resolveMiniDoc?identifier=${encodeURIComponent(handle)}`); if (!mini?.did) { lookupStatus.textContent = 'user not found'; return; } lookupStatus.textContent = 'reading the index…'; const rec = await postsQuoting(mini.did); const items = rec?.posts ?? []; lookupStatus.style.display = 'none'; if (items.length === 0) { lookupStatus.style.display = 'block'; lookupStatus.textContent = `no bufo quotes found for @${mini.handle ?? handle}`; return; } openViewer({ title: `@${mini.handle ?? handle}`, thumb: null, items, withBufo: true, sub: `${items.length} bufo quote${items.length === 1 ? '' : 's'} by @${BOT_HANDLE}`, }); } catch (err) { lookupStatus.style.display = 'block'; lookupStatus.textContent = `error: ${err.message}`; } finally { lookupBtn.disabled = false; }}
lookupBtn?.addEventListener('click', lookupUser);lookupInput?.addEventListener('keydown', (e) => { if (e.key === 'Enter') lookupUser(); });