hacker-news.go 2.5 KB

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