| 1 |
// Hacker News story feed. Fetches story IDs then individual items from |
| 2 |
// the Firebase JSON API. Supports top/new/best/ask/show/job story types. |
| 3 |
// Strips basic HTML from self-post text; paginates via offset cursor. |
| 4 |
|
| 5 |
const HN_API = "https://hacker-news.firebaseio.com/v0"; |
| 6 |
|
| 7 |
fn id() { |
| 8 |
"hackernews" |
| 9 |
} |
| 10 |
|
| 11 |
fn name() { |
| 12 |
"Hacker News" |
| 13 |
} |
| 14 |
|
| 15 |
fn capabilities() { |
| 16 |
#{ |
| 17 |
supports_pagination: true |
| 18 |
} |
| 19 |
} |
| 20 |
|
| 21 |
fn config_schema() { |
| 22 |
#{ |
| 23 |
description: "Fetch stories from Hacker News. Choose which type of stories to follow.", |
| 24 |
fields: [ |
| 25 |
#{ |
| 26 |
key: "story_type", |
| 27 |
label: "Story Type", |
| 28 |
field_type: "select", |
| 29 |
required: true, |
| 30 |
description: "Which type of HN stories to fetch", |
| 31 |
default_value: "top", |
| 32 |
options: ["top", "new", "best", "ask", "show", "job"] |
| 33 |
}, |
| 34 |
#{ |
| 35 |
key: "limit", |
| 36 |
label: "Items per fetch", |
| 37 |
field_type: "text", |
| 38 |
description: "Number of stories to fetch (max 100)", |
| 39 |
default_value: "30", |
| 40 |
placeholder: "30" |
| 41 |
} |
| 42 |
] |
| 43 |
} |
| 44 |
} |
| 45 |
|
| 46 |
fn fetch(config, cursor) { |
| 47 |
// Validate story type |
| 48 |
let story_type = "top"; |
| 49 |
if config.story_type != () { |
| 50 |
story_type = config.story_type; |
| 51 |
let valid = ["top", "new", "best", "ask", "show", "job"]; |
| 52 |
let found = false; |
| 53 |
for v in valid { |
| 54 |
if story_type == v { found = true; } |
| 55 |
} |
| 56 |
if !found { |
| 57 |
throw_config_error("Invalid story type: " + story_type); |
| 58 |
} |
| 59 |
} |
| 60 |
|
| 61 |
// Get limit from config |
| 62 |
let limit = 30; |
| 63 |
if config.limit != () { |
| 64 |
let parsed = parse_int(config.limit); |
| 65 |
if parsed != () && parsed > 0 { |
| 66 |
if parsed > 100 { |
| 67 |
limit = 100; |
| 68 |
} else { |
| 69 |
limit = parsed; |
| 70 |
} |
| 71 |
} |
| 72 |
} |
| 73 |
|
| 74 |
// Parse cursor for offset |
| 75 |
let offset = 0; |
| 76 |
if cursor != () { |
| 77 |
let parsed = parse_int(cursor); |
| 78 |
if parsed != () { |
| 79 |
offset = parsed; |
| 80 |
} |
| 81 |
} |
| 82 |
|
| 83 |
// Fetch story IDs |
| 84 |
let endpoint = get_endpoint(story_type); |
| 85 |
let ids_url = HN_API + "/" + endpoint + ".json"; |
| 86 |
let all_ids = http_get_json(ids_url); |
| 87 |
|
| 88 |
// Slice to get the items we want |
| 89 |
let ids = []; |
| 90 |
let i = offset; |
| 91 |
while i < all_ids.len() && ids.len() < limit { |
| 92 |
ids.push(all_ids[i]); |
| 93 |
i += 1; |
| 94 |
} |
| 95 |
|
| 96 |
// Fetch each story |
| 97 |
let items = []; |
| 98 |
for story_id in ids { |
| 99 |
let item = fetch_story(story_id, story_type, HN_API); |
| 100 |
if item != () { |
| 101 |
items.push(item); |
| 102 |
} |
| 103 |
} |
| 104 |
|
| 105 |
// Calculate next cursor |
| 106 |
let next_offset = offset + limit; |
| 107 |
let has_more = next_offset < 500; |
| 108 |
|
| 109 |
let result = #{ |
| 110 |
items: items, |
| 111 |
has_more: has_more |
| 112 |
}; |
| 113 |
|
| 114 |
if has_more { |
| 115 |
result.next_cursor = "" + next_offset; |
| 116 |
} |
| 117 |
|
| 118 |
result |
| 119 |
} |
| 120 |
|
| 121 |
fn get_endpoint(story_type) { |
| 122 |
if story_type == "new" { return "newstories"; } |
| 123 |
if story_type == "best" { return "beststories"; } |
| 124 |
if story_type == "ask" { return "askstories"; } |
| 125 |
if story_type == "show" { return "showstories"; } |
| 126 |
if story_type == "job" { return "jobstories"; } |
| 127 |
"topstories" |
| 128 |
} |
| 129 |
|
| 130 |
fn fetch_story(story_id, story_type, api_base) { |
| 131 |
let url = api_base + "/item/" + story_id + ".json"; |
| 132 |
let item = http_get_json(url); |
| 133 |
|
| 134 |
if item == () { |
| 135 |
return (); |
| 136 |
} |
| 137 |
if item.title == () { |
| 138 |
return (); |
| 139 |
} |
| 140 |
|
| 141 |
let title = item.title; |
| 142 |
|
| 143 |
let score = 0; |
| 144 |
if item.score != () { |
| 145 |
score = item.score; |
| 146 |
} |
| 147 |
|
| 148 |
let comments = 0; |
| 149 |
if item.descendants != () { |
| 150 |
comments = item.descendants; |
| 151 |
} |
| 152 |
|
| 153 |
let author = "anonymous"; |
| 154 |
if item.by != () { |
| 155 |
author = item.by; |
| 156 |
} |
| 157 |
|
| 158 |
let time = timestamp_now(); |
| 159 |
if item.time != () { |
| 160 |
time = item.time; |
| 161 |
} |
| 162 |
|
| 163 |
let item_url = item.url; |
| 164 |
let text = item.text; |
| 165 |
|
| 166 |
let item_type = "story"; |
| 167 |
if item.type != () { |
| 168 |
item_type = item.type; |
| 169 |
} |
| 170 |
|
| 171 |
// Determine indicator |
| 172 |
let indicator = "๐"; |
| 173 |
if item_type == "job" { |
| 174 |
indicator = "๐ผ"; |
| 175 |
} else if item_type == "poll" { |
| 176 |
indicator = "๐"; |
| 177 |
} else if item_url == () { |
| 178 |
indicator = "๐ฌ"; |
| 179 |
} |
| 180 |
|
| 181 |
// Create secondary text |
| 182 |
let secondary = "" + score + " pts ยท " + comments + " comments"; |
| 183 |
|
| 184 |
// Create content |
| 185 |
let body = (); |
| 186 |
if text != () { |
| 187 |
body = text; |
| 188 |
body = str_replace(body, "<p>", "\n\n"); |
| 189 |
body = str_replace(body, "</p>", ""); |
| 190 |
body = str_replace(body, "<i>", "_"); |
| 191 |
body = str_replace(body, "</i>", "_"); |
| 192 |
body = str_replace(body, "<b>", "**"); |
| 193 |
body = str_replace(body, "</b>", "**"); |
| 194 |
body = str_replace(body, ">", ">"); |
| 195 |
body = str_replace(body, "<", "<"); |
| 196 |
body = str_replace(body, "&", "&"); |
| 197 |
body = str_replace(body, "'", "'"); |
| 198 |
body = str_replace(body, """, `"`); |
| 199 |
} |
| 200 |
|
| 201 |
// URL |
| 202 |
let content_url = "https://news.ycombinator.com/item?id=" + story_id; |
| 203 |
if item_url != () { |
| 204 |
content_url = item_url; |
| 205 |
} |
| 206 |
|
| 207 |
// Source name |
| 208 |
let source_name = "Hacker News"; |
| 209 |
if story_type == "ask" { |
| 210 |
source_name = "Ask HN"; |
| 211 |
} else if story_type == "show" { |
| 212 |
source_name = "Show HN"; |
| 213 |
} else if story_type == "job" { |
| 214 |
source_name = "HN Jobs"; |
| 215 |
} |
| 216 |
|
| 217 |
#{ |
| 218 |
id: #{ source: "hackernews", item_id: "" + story_id }, |
| 219 |
bite: #{ |
| 220 |
author: "HN", |
| 221 |
text: title, |
| 222 |
secondary: secondary, |
| 223 |
indicator: indicator |
| 224 |
}, |
| 225 |
content: #{ |
| 226 |
title: title, |
| 227 |
body: body, |
| 228 |
url: content_url |
| 229 |
}, |
| 230 |
meta: #{ |
| 231 |
source_name: source_name, |
| 232 |
published_at: time, |
| 233 |
score: score, |
| 234 |
tags: ["hackernews"] |
| 235 |
} |
| 236 |
} |
| 237 |
} |
| 238 |
|