▍ humdrum codex / soft
2.4 KB raw
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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<Story[]> {
  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<Story[]> {
  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<Config, Payload> = {
  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 };
  },
};