github.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. package feed
  2. import (
  3. "fmt"
  4. "log/slog"
  5. "net/http"
  6. "sync"
  7. "time"
  8. )
  9. type githubReleaseLatestResponseJson struct {
  10. TagName string `json:"tag_name"`
  11. PublishedAt string `json:"published_at"`
  12. HtmlUrl string `json:"html_url"`
  13. Reactions struct {
  14. Downvotes int `json:"-1"`
  15. } `json:"reactions"`
  16. }
  17. func parseGithubTime(t string) time.Time {
  18. parsedTime, err := time.Parse("2006-01-02T15:04:05Z", t)
  19. if err != nil {
  20. return time.Now()
  21. }
  22. return parsedTime
  23. }
  24. func FetchLatestReleasesFromGithub(repositories []string, token string) (AppReleases, error) {
  25. appReleases := make(AppReleases, 0, len(repositories))
  26. if len(repositories) == 0 {
  27. return appReleases, nil
  28. }
  29. requests := make([]*http.Request, len(repositories))
  30. for i, repository := range repositories {
  31. request, _ := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", repository), nil)
  32. if token != "" {
  33. request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
  34. }
  35. requests[i] = request
  36. }
  37. task := decodeJsonFromRequestTask[githubReleaseLatestResponseJson](defaultClient)
  38. job := newJob(task, requests).withWorkers(15)
  39. responses, errs, err := workerPoolDo(job)
  40. if err != nil {
  41. return nil, err
  42. }
  43. var failed int
  44. for i := range responses {
  45. if errs[i] != nil {
  46. failed++
  47. slog.Error("Failed to fetch or parse github release", "error", errs[i], "url", requests[i].URL)
  48. continue
  49. }
  50. liveRelease := &responses[i]
  51. if liveRelease == nil {
  52. slog.Error("No live release found", "repository", repositories[i], "url", requests[i].URL)
  53. continue
  54. }
  55. version := liveRelease.TagName
  56. if version[0] != 'v' {
  57. version = "v" + version
  58. }
  59. appReleases = append(appReleases, AppRelease{
  60. Name: repositories[i],
  61. Version: version,
  62. NotesUrl: liveRelease.HtmlUrl,
  63. TimeReleased: parseGithubTime(liveRelease.PublishedAt),
  64. Downvotes: liveRelease.Reactions.Downvotes,
  65. })
  66. }
  67. if len(appReleases) == 0 {
  68. return nil, ErrNoContent
  69. }
  70. appReleases.SortByNewest()
  71. if failed > 0 {
  72. return appReleases, fmt.Errorf("%w: could not get %d releases", ErrPartialContent, failed)
  73. }
  74. return appReleases, nil
  75. }
  76. type GithubTicket struct {
  77. Number int
  78. CreatedAt time.Time
  79. Title string
  80. }
  81. type RepositoryDetails struct {
  82. Name string
  83. Stars int
  84. Forks int
  85. OpenPullRequests int
  86. PullRequests []GithubTicket
  87. OpenIssues int
  88. Issues []GithubTicket
  89. }
  90. type githubRepositoryDetailsResponseJson struct {
  91. Name string `json:"full_name"`
  92. Stars int `json:"stargazers_count"`
  93. Forks int `json:"forks_count"`
  94. }
  95. type githubTicketResponseJson struct {
  96. Count int `json:"total_count"`
  97. Tickets []struct {
  98. Number int `json:"number"`
  99. CreatedAt string `json:"created_at"`
  100. Title string `json:"title"`
  101. } `json:"items"`
  102. }
  103. func FetchRepositoryDetailsFromGithub(repository string, token string, maxPRs int, maxIssues int) (RepositoryDetails, error) {
  104. repositoryRequest, err := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/repos/%s", repository), nil)
  105. if err != nil {
  106. return RepositoryDetails{}, fmt.Errorf("%w: could not create request with repository: %v", ErrNoContent, err)
  107. }
  108. PRsRequest, _ := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/search/issues?q=is:pr+is:open+repo:%s&per_page=%d", repository, maxPRs), nil)
  109. issuesRequest, _ := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/search/issues?q=is:issue+is:open+repo:%s&per_page=%d", repository, maxIssues), nil)
  110. if token != "" {
  111. token = fmt.Sprintf("Bearer %s", token)
  112. repositoryRequest.Header.Add("Authorization", token)
  113. PRsRequest.Header.Add("Authorization", token)
  114. issuesRequest.Header.Add("Authorization", token)
  115. }
  116. var detailsResponse githubRepositoryDetailsResponseJson
  117. var detailsErr error
  118. var PRsResponse githubTicketResponseJson
  119. var PRsErr error
  120. var issuesResponse githubTicketResponseJson
  121. var issuesErr error
  122. var wg sync.WaitGroup
  123. wg.Add(1)
  124. go (func() {
  125. defer wg.Done()
  126. detailsResponse, detailsErr = decodeJsonFromRequest[githubRepositoryDetailsResponseJson](defaultClient, repositoryRequest)
  127. })()
  128. if maxPRs > 0 {
  129. wg.Add(1)
  130. go (func() {
  131. defer wg.Done()
  132. PRsResponse, PRsErr = decodeJsonFromRequest[githubTicketResponseJson](defaultClient, PRsRequest)
  133. })()
  134. }
  135. if maxIssues > 0 {
  136. wg.Add(1)
  137. go (func() {
  138. defer wg.Done()
  139. issuesResponse, issuesErr = decodeJsonFromRequest[githubTicketResponseJson](defaultClient, issuesRequest)
  140. })()
  141. }
  142. wg.Wait()
  143. if detailsErr != nil {
  144. return RepositoryDetails{}, fmt.Errorf("%w: could not get repository details: %s", ErrNoContent, detailsErr)
  145. }
  146. details := RepositoryDetails{
  147. Name: detailsResponse.Name,
  148. Stars: detailsResponse.Stars,
  149. Forks: detailsResponse.Forks,
  150. PullRequests: make([]GithubTicket, 0, len(PRsResponse.Tickets)),
  151. Issues: make([]GithubTicket, 0, len(issuesResponse.Tickets)),
  152. }
  153. err = nil
  154. if maxPRs > 0 {
  155. if PRsErr != nil {
  156. err = fmt.Errorf("%w: could not get PRs: %s", ErrPartialContent, PRsErr)
  157. } else {
  158. details.OpenPullRequests = PRsResponse.Count
  159. for i := range PRsResponse.Tickets {
  160. details.PullRequests = append(details.PullRequests, GithubTicket{
  161. Number: PRsResponse.Tickets[i].Number,
  162. CreatedAt: parseGithubTime(PRsResponse.Tickets[i].CreatedAt),
  163. Title: PRsResponse.Tickets[i].Title,
  164. })
  165. }
  166. }
  167. }
  168. if maxIssues > 0 {
  169. if issuesErr != nil {
  170. // TODO: fix, overwriting the previous error
  171. err = fmt.Errorf("%w: could not get issues: %s", ErrPartialContent, issuesErr)
  172. } else {
  173. details.OpenIssues = issuesResponse.Count
  174. for i := range issuesResponse.Tickets {
  175. details.Issues = append(details.Issues, GithubTicket{
  176. Number: issuesResponse.Tickets[i].Number,
  177. CreatedAt: parseGithubTime(issuesResponse.Tickets[i].CreatedAt),
  178. Title: issuesResponse.Tickets[i].Title,
  179. })
  180. }
  181. }
  182. }
  183. return details, err
  184. }