hacker news alerts in slack (incessant pings if you make front page)
1export interface User {
2 id: string;
3 created: number;
4 karma: number;
5 about?: string;
6 submitted?: number[];
7}
8
9export interface Story {
10 id: number;
11 by: string;
12 title: string;
13 url?: string;
14 text?: string;
15 time: number;
16 score: number;
17 descendants: number;
18 type: "story" | "job" | "comment" | "poll" | "pollopt";
19 kids?: number[];
20}
21
22export interface Comment {
23 id: number;
24 by: string;
25 text: string;
26 time: number;
27 parent: number;
28 type: "comment";
29 kids?: number[];
30}
31
32export type HNItem = Story | Comment;
33
34/**
35 * Fetches user data by user ID from the Hacker News API.
36 * Only users with public activity (comments or story submissions) are available.
37 *
38 * @param userId - The user's unique username (case-sensitive)
39 * @returns Promise resolving to the user data
40 * @throws Error if the user cannot be found or if there's a network error
41 */
42export async function getUser(userId: string): Promise<User> {
43 if (!userId) {
44 throw new Error("User ID is required");
45 }
46
47 try {
48 const response = await fetch(
49 `https://hacker-news.firebaseio.com/v0/user/${userId}.json`,
50 );
51
52 if (!response.ok) {
53 throw new Error(
54 `Failed to fetch user with ID ${userId}: ${response.statusText}`,
55 );
56 }
57
58 const userData = await response.json();
59
60 if (!userData) {
61 throw new Error(`User with ID ${userId} not found`);
62 }
63
64 return userData as User;
65 } catch (error) {
66 if (error instanceof Error) {
67 throw error;
68 }
69 throw new Error(`Failed to fetch user with ID ${userId}: ${String(error)}`);
70 }
71}
72
73/**
74 * Fetches the list of newest story IDs from Hacker News.
75 *
76 * @returns Promise resolving to an array of story IDs
77 */
78export async function getNewStories(): Promise<number[]> {
79 try {
80 console.log("getNewStories: Fetching new story IDs from HackerNews API...");
81
82 const response = await fetch(
83 "https://hacker-news.firebaseio.com/v0/newstories.json",
84 );
85
86 if (!response.ok) {
87 throw new Error(`Failed to fetch new stories: ${response.statusText}`);
88 }
89
90 const stories = (await response.json()) as number[];
91 console.log(
92 `getNewStories: Successfully fetched ${stories.length} story IDs`,
93 );
94
95 return stories;
96 } catch (error) {
97 if (error instanceof Error) {
98 throw error;
99 }
100 throw new Error(`Failed to fetch new stories: ${String(error)}`);
101 }
102}
103
104/**
105 * Fetches the list of top story IDs from Hacker News.
106 *
107 * @returns Promise resolving to an array of story IDs
108 */
109export async function getTopStories(): Promise<number[]> {
110 try {
111 const response = await fetch(
112 "https://hacker-news.firebaseio.com/v0/topstories.json",
113 );
114
115 if (!response.ok) {
116 throw new Error(`Failed to fetch top stories: ${response.statusText}`);
117 }
118
119 return (await response.json()) as number[];
120 } catch (error) {
121 if (error instanceof Error) {
122 throw error;
123 }
124 throw new Error(`Failed to fetch top stories: ${String(error)}`);
125 }
126}
127
128/**
129 * Fetches an item (story, comment, etc) from Hacker News by its ID.
130 *
131 * @param itemId - The unique ID of the item to fetch
132 * @returns Promise resolving to the item data or null if not found
133 */
134export async function getItem<T extends HNItem>(
135 itemId: number,
136): Promise<T | null> {
137 try {
138 // Uncomment for detailed debugging of individual item fetches
139 // console.log(`getItem: Fetching item ${itemId}...`);
140
141 const response = await fetch(
142 `https://hacker-news.firebaseio.com/v0/item/${itemId}.json`,
143 );
144
145 if (!response.ok) {
146 throw new Error(`Failed to fetch item ${itemId}: ${response.statusText}`);
147 }
148
149 const item = await response.json();
150
151 // Uncomment for detailed debugging of individual item results
152 // if (item) {
153 // console.log(`getItem: Successfully fetched item ${itemId} (type: ${item.type})`);
154 // } else {
155 // console.log(`getItem: Item ${itemId} not found (null response)`);
156 // }
157
158 return item ? (item as T) : null;
159 } catch (error) {
160 if (error instanceof Error) {
161 throw error;
162 }
163 throw new Error(`Failed to fetch item ${itemId}: ${String(error)}`);
164 }
165}
166
167/**
168 * Fetches multiple items (stories, comments, etc.) from Hacker News by their IDs.
169 * Uses parallel requests for better performance.
170 *
171 * @param itemIds - Array of item IDs to fetch
172 * @param limit - Optional limit on number of items to fetch
173 * @param batchSize - Optional batch size for parallel requests (default: 20)
174 * @returns Promise resolving to an array of successfully fetched items
175 */
176export async function getItems<T extends HNItem>(
177 itemIds: number[],
178 limit?: number,
179 batchSize = 20,
180): Promise<T[]> {
181 const ids = limit ? itemIds.slice(0, limit) : itemIds;
182
183 console.log(
184 `getItems: Fetching ${ids.length} items from HackerNews API in parallel...`,
185 );
186 let successCount = 0;
187 let errorCount = 0;
188
189 const items: T[] = [];
190
191 // Process in batches to avoid overwhelming the API
192 for (let i = 0; i < ids.length; i += batchSize) {
193 const batchIds = ids.slice(i, i + batchSize);
194 console.log(
195 `getItems: Processing batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(ids.length / batchSize)} (${batchIds.length} items)`,
196 );
197
198 // Create an array of promises for this batch
199 const batchPromises = batchIds.map((id) =>
200 getItem<T>(id)
201 .then((item) => {
202 if (item !== null) {
203 successCount++;
204 return item;
205 }
206 return null;
207 })
208 .catch((error) => {
209 console.error(`Failed to fetch item ${id}:`, error);
210 errorCount++;
211 return null;
212 }),
213 );
214
215 // Wait for all promises in this batch to resolve
216 const batchResults = await Promise.all(batchPromises);
217
218 // Add non-null results to our items array
219 items.push(
220 ...batchResults.filter(
221 (item): item is NonNullable<typeof item> => item !== null,
222 ),
223 );
224
225 console.log(
226 `getItems: Batch complete - ${successCount} successful, ${errorCount} failed so far`,
227 );
228 }
229
230 console.log(
231 `getItems: Completed fetching items - ${successCount} successful, ${errorCount} failed, ${items.length} total items returned`,
232 );
233 return items;
234}
235
236/**
237 * Generates a URL to a Hacker News item.
238 *
239 * @param itemId - The ID of the item
240 * @returns The URL to the item on Hacker News
241 */
242export function getItemUrl(itemId: number): string {
243 return `https://news.ycombinator.com/item?id=${itemId}`;
244}
245
246/**
247 * Generates a URL to a Hacker News user profile.
248 *
249 * @param username - The username of the user
250 * @returns The URL to the user's profile on Hacker News
251 */
252export function getUserProfileUrl(username: string): string {
253 return `https://news.ycombinator.com/user?id=${username}`;
254}