github.go 6.3 KB

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