changedetection.go 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. package feed
  2. import (
  3. "fmt"
  4. "log/slog"
  5. "net/http"
  6. "sort"
  7. "strings"
  8. "time"
  9. )
  10. type ChangeDetectionWatch struct {
  11. Title string
  12. URL string
  13. LastChanged time.Time
  14. DiffURL string
  15. PreviousHash string
  16. }
  17. type ChangeDetectionWatches []ChangeDetectionWatch
  18. func (r ChangeDetectionWatches) SortByNewest() ChangeDetectionWatches {
  19. sort.Slice(r, func(i, j int) bool {
  20. return r[i].LastChanged.After(r[j].LastChanged)
  21. })
  22. return r
  23. }
  24. type changeDetectionResponseJson struct {
  25. Title string `json:"title"`
  26. URL string `json:"url"`
  27. LastChanged int64 `json:"last_changed"`
  28. DateCreated int64 `json:"date_created"`
  29. PreviousHash string `json:"previous_md5"`
  30. }
  31. func FetchWatchUUIDsFromChangeDetection(instanceURL string, token string) ([]string, error) {
  32. request, _ := http.NewRequest("GET", fmt.Sprintf("%s/api/v1/watch", instanceURL), nil)
  33. if token != "" {
  34. request.Header.Add("x-api-key", token)
  35. }
  36. uuidsMap, err := decodeJsonFromRequest[map[string]struct{}](defaultClient, request)
  37. if err != nil {
  38. return nil, fmt.Errorf("could not fetch list of watch UUIDs: %v", err)
  39. }
  40. uuids := make([]string, 0, len(uuidsMap))
  41. for uuid := range uuidsMap {
  42. uuids = append(uuids, uuid)
  43. }
  44. return uuids, nil
  45. }
  46. func FetchWatchesFromChangeDetection(instanceURL string, requestedWatchIDs []string, token string) (ChangeDetectionWatches, error) {
  47. watches := make(ChangeDetectionWatches, 0, len(requestedWatchIDs))
  48. if len(requestedWatchIDs) == 0 {
  49. return watches, nil
  50. }
  51. requests := make([]*http.Request, len(requestedWatchIDs))
  52. for i, repository := range requestedWatchIDs {
  53. request, _ := http.NewRequest("GET", fmt.Sprintf("%s/api/v1/watch/%s", instanceURL, repository), nil)
  54. if token != "" {
  55. request.Header.Add("x-api-key", token)
  56. }
  57. requests[i] = request
  58. }
  59. task := decodeJsonFromRequestTask[changeDetectionResponseJson](defaultClient)
  60. job := newJob(task, requests).withWorkers(15)
  61. responses, errs, err := workerPoolDo(job)
  62. if err != nil {
  63. return nil, err
  64. }
  65. var failed int
  66. for i := range responses {
  67. if errs[i] != nil {
  68. failed++
  69. slog.Error("Failed to fetch or parse change detection watch", "error", errs[i], "url", requests[i].URL)
  70. continue
  71. }
  72. watchJson := responses[i]
  73. watch := ChangeDetectionWatch{
  74. URL: watchJson.URL,
  75. DiffURL: fmt.Sprintf("%s/diff/%s?from_version=%d", instanceURL, requestedWatchIDs[i], watchJson.LastChanged-1),
  76. }
  77. if watchJson.LastChanged == 0 {
  78. watch.LastChanged = time.Unix(watchJson.DateCreated, 0)
  79. } else {
  80. watch.LastChanged = time.Unix(watchJson.LastChanged, 0)
  81. }
  82. if watchJson.Title != "" {
  83. watch.Title = watchJson.Title
  84. } else {
  85. watch.Title = strings.TrimPrefix(strings.Trim(stripURLScheme(watchJson.URL), "/"), "www.")
  86. }
  87. if watchJson.PreviousHash != "" {
  88. var hashLength = 8
  89. if len(watchJson.PreviousHash) < hashLength {
  90. hashLength = len(watchJson.PreviousHash)
  91. }
  92. watch.PreviousHash = watchJson.PreviousHash[0:hashLength]
  93. }
  94. watches = append(watches, watch)
  95. }
  96. if len(watches) == 0 {
  97. return nil, ErrNoContent
  98. }
  99. watches.SortByNewest()
  100. if failed > 0 {
  101. return watches, fmt.Errorf("%w: could not get %d watches", ErrPartialContent, failed)
  102. }
  103. return watches, nil
  104. }