x

Programs, configuration and documentation that don't fit anywhere else
Log | Files | Refs | README | LICENSE

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 }