/** * Feed watcher — polls the Paper Skygest feed and extracts paper URLs. * * The feed is a Bluesky custom feed generator at: * at://did:plc:uaadt6f5bbda6cycbmatcm3z/app.bsky.feed.generator/preprintdigest * * We poll it via the public API and extract paper URLs from: * - embed.external.uri (most reliable) * - Post text (arxiv.org, doi.org, nber.org, openreview.net, etc.) */ const FEED_URI = "at://did:plc:uaadt6f5bbda6cycbmatcm3z/app.bsky.feed.generator/preprintdigest"; const BSKY_PUBLIC_API = "https://public.api.bsky.app"; const PAPER_URL_PATTERNS = [ /https?:\/\/arxiv\.org\/abs\/\d+\.\d+/, /https?:\/\/arxiv\.org\/abs\/\d{4}\.\d{4,5}/, /https?:\/\/doi\.org\/10\.\d{4,}\/[^\s]+/, /https?:\/\/www\.nber\.org\/papers\/w\d+/, /https?:\/\/openreview\.net\/forum\?id=[^\s]+/, /https?:\/\/papers\.ssrn\.com\/sol3\/papers\.cfm\?abstract_id=\d+/, /https?:\/\/proceedings\.mlr\.press\/v\d+\//, /https?:\/\/www\.science\.org\/doi\/[^\s]+/, /https?:\/\/www\.nature\.com\/articles\/[^\s]+/, /https?:\/\/journals\.plos\.org\/[^\s]+\/article\?id=[^\s]+/, /https?:\/\/www\.biorxiv\.org\/content\/[^\s]+/, /https?:\/\/www\.medrxiv\.org\/content\/[^\s]+/, /https?:\/\/aclanthology\.org\/[^\s]+/, /https?:\/\/proceedings\.neurips\.cc\/paper_files\/[^\s]+/, /https?:\/\/openaccess\.thecvf\.com\/[^\s]+/, ]; export interface PaperPost { postUri: string; postCid: string; paperUrl: string; posterDid: string; posterHandle: string; postText: string; embedTitle?: string; embedDescription?: string; indexedAt: string; } function extractPaperUrls(text: string): string[] { const urls: string[] = []; for (const pattern of PAPER_URL_PATTERNS) { const match = text.match(pattern); if (match) urls.push(match[0]); } return urls; } function extractFromEmbed(embed: any): string | null { if (!embed) return null; if (embed.$type === "app.bsky.embed.external#view" && embed.external?.uri) { const uri = embed.external.uri as string; for (const pattern of PAPER_URL_PATTERNS) { if (pattern.test(uri)) return uri; } if (/https?:\/\/(arxiv|doi|nber|openreview|ssrn|mlr|science|nature|plos|biorxiv|medrxiv|aclanthology|neurips|thecvf)\./.test(uri)) { return uri; } } return null; } export async function pollFeed(cursor?: string, limit = 50): Promise<{ posts: PaperPost[]; cursor?: string }> { const params = new URLSearchParams({ feed: FEED_URI, limit: String(limit), }); if (cursor) params.set("cursor", cursor); const res = await fetch(`${BSKY_PUBLIC_API}/xrpc/app.bsky.feed.getFeed?${params}`); if (!res.ok) { const body = await res.text(); throw new Error(`Feed poll failed (${res.status}): ${body}`); } const data = await res.json() as { cursor?: string; feed: Array<{ post: { uri: string; cid: string; record: { text?: string }; author: { did: string; handle: string }; embed?: any; indexedAt: string; }; }>; }; const posts: PaperPost[] = []; const seenUris = new Set(); for (const item of data.feed) { const { uri, cid, author, embed, indexedAt } = item.post; const text = item.post.record?.text || ""; let paperUrl = extractFromEmbed(embed); if (!paperUrl) { const textUrls = extractPaperUrls(text); if (textUrls.length > 0) paperUrl = textUrls[0]; } if (!paperUrl) continue; if (seenUris.has(paperUrl)) continue; seenUris.add(paperUrl); let embedTitle: string | undefined; let embedDescription: string | undefined; if (embed?.$type === "app.bsky.embed.external#view" && embed.external) { embedTitle = embed.external.title; embedDescription = embed.external.description; } posts.push({ postUri: uri, postCid: cid, paperUrl, posterDid: author.did, posterHandle: author.handle, postText: text, embedTitle, embedDescription, indexedAt, }); } return { posts, cursor: data.cursor }; } export async function pollNewPapers(lastCursor?: string): Promise<{ posts: PaperPost[]; newCursor?: string }> { const allPosts: PaperPost[] = []; let cursor = lastCursor; for (let i = 0; i < 3; i++) { const result = await pollFeed(cursor, 50); allPosts.push(...result.posts); if (!result.cursor || result.cursor === cursor) break; cursor = result.cursor; if (result.posts.length < 50) break; } return { posts: allPosts, newCursor: cursor }; } // --- Followed accounts feed --- const RESEARCHER_DID = "did:plc:3kkhul7jznlb6ba7rprzawnj"; const PDS_ORIGIN = "https://pds.latha.org"; async function getFollows(): Promise> { const follows: Array<{ did: string; handle: string }> = []; let cursor: string | undefined; for (let i = 0; i < 10; i++) { const params = new URLSearchParams({ repo: RESEARCHER_DID, collection: "app.bsky.graph.follow", limit: "100", }); if (cursor) params.set("cursor", cursor); const res = await fetch(`${PDS_ORIGIN}/xrpc/com.atproto.repo.listRecords?${params}`); if (!res.ok) break; const data = await res.json() as { cursor?: string; records: Array<{ value: { subject: string } }>; }; for (const rec of data.records) { follows.push({ did: rec.value.subject, handle: "" }); } if (!data.cursor || data.cursor === cursor) break; cursor = data.cursor; if (data.records.length < 100) break; } // Resolve DIDs to handles for (const f of follows) { try { const res = await fetch(`${BSKY_PUBLIC_API}/xrpc/app.bsky.actor.getProfile?actor=${f.did}`); if (res.ok) { const profile = await res.json() as { handle: string }; f.handle = profile.handle; } } catch { // skip } } return follows; } export async function pollFollowedAccounts(limit = 5): Promise { const follows = await getFollows(); const allPosts: PaperPost[] = []; const seenUris = new Set(); for (const follow of follows) { if (!follow.handle) continue; try { const params = new URLSearchParams({ actor: follow.handle, limit: String(limit), }); const res = await fetch(`${BSKY_PUBLIC_API}/xrpc/app.bsky.feed.getAuthorFeed?${params}`); if (!res.ok) continue; const data = await res.json() as { feed: Array<{ post: { uri: string; cid: string; record: { text?: string }; author: { did: string; handle: string }; embed?: any; indexedAt: string; }; }>; }; for (const item of data.feed) { const { uri, cid, author, embed, indexedAt } = item.post; const text = item.post.record?.text || ""; let paperUrl = extractFromEmbed(embed); if (!paperUrl) { const textUrls = extractPaperUrls(text); if (textUrls.length > 0) paperUrl = textUrls[0]; } if (!paperUrl) continue; if (seenUris.has(paperUrl)) continue; seenUris.add(paperUrl); let embedTitle: string | undefined; let embedDescription: string | undefined; if (embed?.$type === "app.bsky.embed.external#view" && embed.external) { embedTitle = embed.external.title; embedDescription = embed.external.description; } allPosts.push({ postUri: uri, postCid: cid, paperUrl, posterDid: author.did, posterHandle: author.handle, postText: text, embedTitle, embedDescription, indexedAt, }); } } catch { // skip this account } } return allPosts; }