1package hackernews23// TODO: Add tests.45import (6 "context"7 "encoding/json"8 "fmt"9 "log"10 "net/http"11 "strconv"12 "time"1314 "website-feeds/model"15 "website-feeds/util"16)1718const (19 topPostsURL = "https://hacker-news.firebaseio.com/v0/topstories.json"20 postInfoURL = "https://hacker-news.firebaseio.com/v0/item/%d.json"21)2223type Client struct {24 client *http.Client25}2627type Post struct {28 ID int29 Score int30 Title string31 URL string32 Created time.Time33 IsText bool34}3536func New() *Client {37 return &Client{38 client: util.NewHttpClient(),39 }40}4142func (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, err50 }5152 log.Printf("Got %d ids", len(ids))53 log.Print("Fetching full posts...")5455 if len(ids) <= numPosts {56 numPosts = len(ids) - 157 }5859 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, err64 }6566 posts = append(posts, *post)67 }6869 return posts, nil70}7172func (*Client) URL() string {73 return "https://news.ycombinator.com"74}7576func (*Client) CommentURL(post model.Post) string {77 return fmt.Sprintf(78 "https://news.ycombinator.com/item?id=%s",79 post.SiteUniqueID,80 )81}8283func (c *Client) DisplayName(84 _ctx context.Context,85) (string, error) {86 return "Hackernews", nil87}8889func (c *Client) topPostIDs(ctx context.Context) ([]int, error) {90 req, err := http.NewRequestWithContext(ctx, "GET", topPostsURL, nil)91 if err != nil {92 return nil, err93 }9495 resp, err := c.client.Do(req)96 if err != nil {97 return nil, err98 }99 defer func() {100 if err := resp.Body.Close(); err != nil {101 log.Printf("failed to close body: %s", err)102 }103 }()104105 posts := make([]int, 0)106 err = json.NewDecoder(resp.Body).Decode(&posts)107 if err != nil {108 return nil, err109 }110111 return posts, nil112}113114func (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 }122123 req, err := http.NewRequestWithContext(124 ctx,125 "GET",126 fmt.Sprintf(postInfoURL, id),127 nil,128 )129 if err != nil {130 return nil, err131 }132133 resp, err := c.client.Do(req)134 if err != nil {135 return nil, err136 }137 defer func() {138 if err := resp.Body.Close(); err != nil {139 log.Printf("failed to close body: %s", err)140 }141 }()142143 post := rawPost{}144 err = json.NewDecoder(resp.Body).Decode(&post)145 if err != nil {146 return nil, err147 }148149 internalPost := post.URL == ""150 var url *string151 if !internalPost {152 url = &post.URL153 }154155 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 }, nil163}