import type { SourceModule } from "../registry"; import { HackerNewsConfig, HackerNewsPayload, type HackerNewsConfig as Config, type HackerNewsPayload as Payload, } from "@/lib/schemas/sources/hackernews"; type Story = Payload["stories"][number]; async function fetchHN(limit: number, signal?: AbortSignal): Promise { const res = await fetch(`https://hn.algolia.com/api/v1/search?tags=front_page&hitsPerPage=${limit}`, { signal, }); if (!res.ok) return []; const j = (await res.json()) as { hits: Array<{ objectID: string; title: string; points?: number; num_comments?: number; url?: string }>; }; return j.hits.map((h) => ({ source: "HN", title: h.title, points: h.points ?? null, comments: h.num_comments ?? null, url: h.url ?? `https://news.ycombinator.com/item?id=${h.objectID}`, })); } async function fetchSub(sub: string, limit: number, signal?: AbortSignal): Promise { try { const res = await fetch(`https://www.reddit.com/r/${encodeURIComponent(sub)}/top.json?t=day&limit=${limit}`, { headers: { "User-Agent": "PersonalDashboard/1.0" }, signal, }); if (!res.ok) return []; const j = (await res.json()) as { data?: { children?: Array<{ data?: { title?: string; ups?: number; num_comments?: number; permalink?: string; url?: string } }> }; }; return (j.data?.children ?? []).map((c) => { const d = c.data ?? {}; return { source: `r/${sub}`, title: d.title ?? "", points: d.ups ?? null, comments: d.num_comments ?? null, url: d.permalink ? `https://www.reddit.com${d.permalink}` : d.url ?? null, }; }); } catch { return []; } } export const hackerNewsModule: SourceModule = { kind: "hackernews", label: "Hacker News", keyless: true, defaultRefreshSeconds: 1800, configSchema: HackerNewsConfig, payloadSchema: HackerNewsPayload, isConfigured: () => true, // HN always works; subreddits are optional extras async fetch({ config, signal }) { const lists = await Promise.all([ fetchHN(config.limit, signal), ...config.subreddits.map((s) => fetchSub(s.trim(), config.limit, signal)), ]); const stories = lists .flat() .filter((s) => s.title) .sort((a, b) => (b.points ?? 0) - (a.points ?? 0)) .slice(0, config.limit); return { stories }; }, };