hnatom.go (3380B)
1 // Command hnatom generates a RFC 4287 Atom feed 2 // of current top stories from the Hacker News front page. 3 // The feed is written to the standard output. 4 // The flags are: 5 // 6 // -n count 7 // Include count items in the feed. The default is 30. 8 // 9 // See also the [Hacker News API]. 10 // 11 // [Hacker News API]: https://raw.githubusercontent.com/HackerNews/API/refs/heads/master/README.md 12 package main 13 14 import ( 15 "bytes" 16 "encoding/json" 17 "encoding/xml" 18 "flag" 19 "fmt" 20 "log" 21 "net/http" 22 "os" 23 "time" 24 25 "olowe.co/x/atom" 26 ) 27 28 const apiRoot = "https://hacker-news.firebaseio.com/v0" 29 30 type Item struct { 31 ID int 32 Type string 33 By string 34 Time int 35 Text string 36 Parent int 37 URL string 38 Title string 39 Score int 40 Descendants int 41 } 42 43 func Get(id int) (*Item, error) { 44 u := fmt.Sprintf("%s/item/%d.json", apiRoot, id) 45 resp, err := http.Get(u) 46 if err != nil { 47 return nil, err 48 } 49 defer resp.Body.Close() 50 var item Item 51 err = json.NewDecoder(resp.Body).Decode(&item) 52 return &item, err 53 } 54 55 func Top() ([]int, error) { 56 resp, err := http.Get(apiRoot + "/topstories.json") 57 if err != nil { 58 return nil, err 59 } 60 defer resp.Body.Close() 61 ids := make([]int, 500) // we know the API returns at most 500 items. 62 err = json.NewDecoder(resp.Body).Decode(&ids) 63 return ids, err 64 } 65 66 func entryContent(item *Item) []byte { 67 buf := bytes.NewBufferString(item.Text) 68 if item.Text != "" { 69 buf.WriteString("<p>") 70 defer buf.WriteString("</p>") 71 } 72 fmt.Fprintf(buf, "Score: %d<br>", item.Score) 73 comments := fmt.Sprintf("https://news.ycombinator.com/item?id=%d", item.ID) 74 fmt.Fprintf(buf, "<a href=%q>Comments: %d", comments, item.Descendants) 75 return buf.Bytes() 76 } 77 78 // The most number of items the top API endpoint will return. 79 const maxItems = 500 80 81 // 30 is the item count on the front page of Hacker News. 82 var numItems = flag.Uint("n", 30, "number of items to fetch") 83 84 func init() { 85 flag.Parse() 86 if *numItems > maxItems { 87 *numItems = maxItems 88 fmt.Fprintln(os.Stderr, "warning: maximum of 500 entries can be fetched") 89 } 90 } 91 92 func main() { 93 top, err := Top() 94 if err != nil { 95 log.Fatal("get top items:", err) 96 } 97 98 feed := &atom.Feed{ 99 ID: "http://home.olowe.co/hnatom/feed.atom", 100 Title: "HN Atom", 101 Subtitle: "Top posts from Hacker News", 102 Link: []atom.Link{ 103 { 104 Rel: "alternate", 105 Type: "html", 106 HRef: "https://news.ycombinator.com", 107 }, 108 }, 109 Updated: time.Now(), 110 Entries: make([]atom.Entry, *numItems), 111 } 112 113 var j int 114 for i := range top[:len(feed.Entries)] { 115 item, err := Get(top[i]) 116 if err != nil { 117 log.Printf("get item %d: %v", top[i], err) 118 continue 119 } 120 if item.Type != "story" { 121 continue 122 } 123 link := item.URL 124 if link == "" { 125 // Ask HN posts have no external URL set 126 link = fmt.Sprintf("https://news.ycombinator.com/item?id=%d", item.ID) 127 } 128 feed.Entries[j] = atom.Entry{ 129 ID: fmt.Sprintf("%s/item/%d.json", apiRoot, top[i]), 130 Title: item.Title, 131 Updated: time.Unix(int64(item.Time), 0), 132 Author: &atom.Author{ 133 Name: item.By, 134 URI: "https://news.ycombinator.com/user?id=" + item.By, 135 }, 136 Content: []byte(entryContent(item)), 137 Links: []atom.Link{{HRef: link}}, 138 } 139 j++ 140 } 141 feed.Entries = feed.Entries[:j] 142 143 b, err := xml.MarshalIndent(feed, "", "\t") 144 if err != nil { 145 fmt.Fprintln(os.Stderr, err) 146 os.Exit(1) 147 } 148 os.Stdout.Write(b) 149 }