website-feeds

Make RSS feeds of your favorite "vote on posts" websites

git clone https://code.pdelong.com/website-feeds.git

  1package hackernews
  2
  3// TODO: Add tests.
  4
  5import (
  6	"context"
  7	"encoding/json"
  8	"fmt"
  9	"log"
 10	"net/http"
 11	"strconv"
 12	"time"
 13
 14	"website-feeds/model"
 15	"website-feeds/util"
 16)
 17
 18const (
 19	topPostsURL = "https://hacker-news.firebaseio.com/v0/topstories.json"
 20	postInfoURL = "https://hacker-news.firebaseio.com/v0/item/%d.json"
 21)
 22
 23type Client struct {
 24	client *http.Client
 25}
 26
 27type Post struct {
 28	ID      int
 29	Score   int
 30	Title   string
 31	URL     string
 32	Created time.Time
 33	IsText  bool
 34}
 35
 36func New() *Client {
 37	return &Client{
 38		client: util.NewHttpClient(),
 39	}
 40}
 41
 42func (c *Client) FetchPosts(
 43	ctx context.Context,
 44	numPosts int,
 45) ([]model.Post, error) {
 46	log.Print("Fetching top post IDs")
 47	ids, err := c.topPostIDs(ctx)
 48	if err != nil {
 49		return nil, err
 50	}
 51
 52	log.Printf("Got %d ids", len(ids))
 53	log.Print("Fetching full posts...")
 54
 55	if len(ids) <= numPosts {
 56		numPosts = len(ids) - 1
 57	}
 58
 59	posts := make([]model.Post, 0, numPosts)
 60	for _, id := range ids[:numPosts] {
 61		post, err := c.fullPost(ctx, id)
 62		if err != nil {
 63			return nil, err
 64		}
 65
 66		posts = append(posts, *post)
 67	}
 68
 69	return posts, nil
 70}
 71
 72func (*Client) URL() string {
 73	return "https://news.ycombinator.com"
 74}
 75
 76func (*Client) CommentURL(post model.Post) string {
 77	return fmt.Sprintf(
 78		"https://news.ycombinator.com/item?id=%s",
 79		post.SiteUniqueID,
 80	)
 81}
 82
 83func (c *Client) DisplayName(
 84	_ctx context.Context,
 85) (string, error) {
 86	return "Hackernews", nil
 87}
 88
 89func (c *Client) topPostIDs(ctx context.Context) ([]int, error) {
 90	req, err := http.NewRequestWithContext(ctx, "GET", topPostsURL, nil)
 91	if err != nil {
 92		return nil, err
 93	}
 94
 95	resp, err := c.client.Do(req)
 96	if err != nil {
 97		return nil, err
 98	}
 99	defer func() {
100		if err := resp.Body.Close(); err != nil {
101			log.Printf("failed to close body: %s", err)
102		}
103	}()
104
105	posts := make([]int, 0)
106	err = json.NewDecoder(resp.Body).Decode(&posts)
107	if err != nil {
108		return nil, err
109	}
110
111	return posts, nil
112}
113
114func (c *Client) fullPost(ctx context.Context, id int) (*model.Post, error) {
115	type rawPost struct {
116		ID      int    `json:"id"`
117		Score   int    `json:"score"`
118		Title   string `json:"title"`
119		URL     string `json:"url"`
120		Created int64  `json:"time"`
121	}
122
123	req, err := http.NewRequestWithContext(
124		ctx,
125		"GET",
126		fmt.Sprintf(postInfoURL, id),
127		nil,
128	)
129	if err != nil {
130		return nil, err
131	}
132
133	resp, err := c.client.Do(req)
134	if err != nil {
135		return nil, err
136	}
137	defer func() {
138		if err := resp.Body.Close(); err != nil {
139			log.Printf("failed to close body: %s", err)
140		}
141	}()
142
143	post := rawPost{}
144	err = json.NewDecoder(resp.Body).Decode(&post)
145	if err != nil {
146		return nil, err
147	}
148
149	internalPost := post.URL == ""
150	var url *string
151	if !internalPost {
152		url = &post.URL
153	}
154
155	created := time.Unix(post.Created, 0)
156	return &model.Post{
157		SiteUniqueID: strconv.Itoa(post.ID),
158		Score:        post.Score,
159		Title:        post.Title,
160		URL:          url,
161		Created:      created,
162	}, nil
163}