hacker-news.go 2.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
  1. package feed
  2. import (
  3. "fmt"
  4. "log/slog"
  5. "net/http"
  6. "strconv"
  7. "time"
  8. )
  9. type hackerNewsPostResponseJson struct {
  10. Id int `json:"id"`
  11. Score int `json:"score"`
  12. Title string `json:"title"`
  13. TargetUrl string `json:"url,omitempty"`
  14. CommentCount int `json:"descendants"`
  15. TimePosted int64 `json:"time"`
  16. }
  17. func getHackerNewsTopPostIds() ([]int, error) {
  18. request, _ := http.NewRequest("GET", "https://hacker-news.firebaseio.com/v0/topstories.json", nil)
  19. response, err := decodeJsonFromRequest[[]int](defaultClient, request)
  20. if err != nil {
  21. return nil, fmt.Errorf("%w: could not fetch list of post IDs", ErrNoContent)
  22. }
  23. return response, nil
  24. }
  25. func getHackerNewsPostsFromIds(postIds []int) (ForumPosts, error) {
  26. requests := make([]*http.Request, len(postIds))
  27. for i, id := range postIds {
  28. request, _ := http.NewRequest("GET", fmt.Sprintf("https://hacker-news.firebaseio.com/v0/item/%d.json", id), nil)
  29. requests[i] = request
  30. }
  31. task := decodeJsonFromRequestTask[hackerNewsPostResponseJson](defaultClient)
  32. job := newJob(task, requests).withWorkers(30)
  33. results, errs, err := workerPoolDo(job)
  34. if err != nil {
  35. return nil, err
  36. }
  37. posts := make(ForumPosts, 0, len(postIds))
  38. for i := range results {
  39. if errs[i] != nil {
  40. slog.Error("Failed to fetch or parse hacker news post", "error", errs[i], "url", requests[i].URL)
  41. continue
  42. }
  43. posts = append(posts, ForumPost{
  44. Title: results[i].Title,
  45. DiscussionUrl: "https://news.ycombinator.com/item?id=" + strconv.Itoa(results[i].Id),
  46. TargetUrl: results[i].TargetUrl,
  47. TargetUrlDomain: extractDomainFromUrl(results[i].TargetUrl),
  48. CommentCount: results[i].CommentCount,
  49. Score: results[i].Score,
  50. TimePosted: time.Unix(results[i].TimePosted, 0),
  51. })
  52. }
  53. if len(posts) == 0 {
  54. return nil, ErrNoContent
  55. }
  56. if len(posts) != len(postIds) {
  57. return posts, fmt.Errorf("%w could not fetch some hacker news posts", ErrPartialContent)
  58. }
  59. return posts, nil
  60. }
  61. func FetchHackerNewsTopPosts(limit int) (ForumPosts, error) {
  62. postIds, err := getHackerNewsTopPostIds()
  63. if err != nil {
  64. return nil, err
  65. }
  66. if len(postIds) > limit {
  67. postIds = postIds[:limit]
  68. }
  69. return getHackerNewsPostsFromIds(postIds)
  70. }