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 };
},
};
|