123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344 |
- package feed
- import (
- "errors"
- "fmt"
- "log/slog"
- "net/http"
- "net/url"
- "sync"
- "time"
- )
- type githubReleaseLatestResponseJson struct {
- TagName string `json:"tag_name"`
- PublishedAt string `json:"published_at"`
- HtmlUrl string `json:"html_url"`
- Reactions struct {
- Downvotes int `json:"-1"`
- } `json:"reactions"`
- }
- type gitlabReleaseResponseJson struct {
- TagName string `json:"tag_name"`
- PublishedAt string `json:"created_at"`
- Links struct {
- Self string `json:"self"`
- } `json:"_links"`
- Draft bool `json:"draft"`
- PreRelease bool `json:"prerelease"`
- Reactions struct {
- Downvotes int `json:"-1"`
- } `json:"reactions"`
- }
- func parseGithubTime(t string) time.Time {
- parsedTime, err := time.Parse("2006-01-02T15:04:05Z", t)
- if err != nil {
- return time.Now()
- }
- return parsedTime
- }
- func FetchLatestReleasesFromGitForge(repositories []string, token string, source string) (AppReleases, error) {
- switch source {
- case "github":
- return fetchLatestReleasesFromGithub(repositories, token)
- case "gitlab":
- return fetchLatestReleasesFromGitlab(repositories, token)
- default:
- return nil, errors.New(fmt.Sprintf("Release source %s is invalid", source))
- }
- }
- func fetchLatestReleasesFromGitlab(repositories []string, token string) (AppReleases, error) {
- appReleases := make(AppReleases, 0, len(repositories))
- if len(repositories) == 0 {
- return appReleases, nil
- }
- requests := make([]*http.Request, len(repositories))
- for i, repository := range repositories {
- request, _ := http.NewRequest("GET", fmt.Sprintf("https://gitlab.com/api/v4/projects/%s/releases/", url.QueryEscape(repository)), nil)
- if token != "" {
- request.Header.Add("PRIVATE-TOKEN", token)
- }
- requests[i] = request
- }
- task := decodeJsonFromRequestTask[[]gitlabReleaseResponseJson](defaultClient)
- job := newJob(task, requests).withWorkers(15)
- responses, errs, err := workerPoolDo(job)
- if err != nil {
- return nil, err
- }
- var failed int
- for i := range responses {
- if errs[i] != nil {
- failed++
- slog.Error("Failed to fetch or parse gitlab release", "error", errs[i], "url", requests[i].URL)
- continue
- }
- releases := responses[i]
- if len(releases) < 1 {
- failed++
- slog.Error("No releases found", "repository", repositories[i], "url", requests[i].URL)
- continue
- }
- var liveRelease *gitlabReleaseResponseJson
- for i := range releases {
- release := &releases[i]
- if !release.Draft && !release.PreRelease {
- liveRelease = release
- break
- }
- }
- if liveRelease == nil {
- slog.Error("No live release found", "repository", repositories[i], "url", requests[i].URL)
- continue
- }
- version := liveRelease.TagName
- if version[0] != 'v' {
- version = "v" + version
- }
- appReleases = append(appReleases, AppRelease{
- Name: repositories[i],
- Version: version,
- NotesUrl: liveRelease.Links.Self,
- TimeReleased: parseGithubTime(liveRelease.PublishedAt),
- Downvotes: liveRelease.Reactions.Downvotes,
- })
- }
- if len(appReleases) == 0 {
- return nil, ErrNoContent
- }
- appReleases.SortByNewest()
- if failed > 0 {
- return appReleases, fmt.Errorf("%w: could not get %d releases", ErrPartialContent, failed)
- }
- return appReleases, nil
- }
- func fetchLatestReleasesFromGithub(repositories []string, token string) (AppReleases, error) {
- appReleases := make(AppReleases, 0, len(repositories))
- if len(repositories) == 0 {
- return appReleases, nil
- }
- requests := make([]*http.Request, len(repositories))
- for i, repository := range repositories {
- request, _ := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", repository), nil)
- if token != "" {
- request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
- }
- requests[i] = request
- }
- task := decodeJsonFromRequestTask[githubReleaseLatestResponseJson](defaultClient)
- job := newJob(task, requests).withWorkers(15)
- responses, errs, err := workerPoolDo(job)
- if err != nil {
- return nil, err
- }
- var failed int
- for i := range responses {
- if errs[i] != nil {
- failed++
- slog.Error("Failed to fetch or parse github release", "error", errs[i], "url", requests[i].URL)
- continue
- }
- liveRelease := &responses[i]
- if liveRelease == nil {
- slog.Error("No live release found", "repository", repositories[i], "url", requests[i].URL)
- continue
- }
- version := liveRelease.TagName
- if version[0] != 'v' {
- version = "v" + version
- }
- appReleases = append(appReleases, AppRelease{
- Name: repositories[i],
- Version: version,
- NotesUrl: liveRelease.HtmlUrl,
- TimeReleased: parseGithubTime(liveRelease.PublishedAt),
- Downvotes: liveRelease.Reactions.Downvotes,
- })
- }
- if len(appReleases) == 0 {
- return nil, ErrNoContent
- }
- appReleases.SortByNewest()
- if failed > 0 {
- return appReleases, fmt.Errorf("%w: could not get %d releases", ErrPartialContent, failed)
- }
- return appReleases, nil
- }
- type GithubTicket struct {
- Number int
- CreatedAt time.Time
- Title string
- }
- type RepositoryDetails struct {
- Name string
- Stars int
- Forks int
- OpenPullRequests int
- PullRequests []GithubTicket
- OpenIssues int
- Issues []GithubTicket
- }
- type githubRepositoryDetailsResponseJson struct {
- Name string `json:"full_name"`
- Stars int `json:"stargazers_count"`
- Forks int `json:"forks_count"`
- }
- type githubTicketResponseJson struct {
- Count int `json:"total_count"`
- Tickets []struct {
- Number int `json:"number"`
- CreatedAt string `json:"created_at"`
- Title string `json:"title"`
- } `json:"items"`
- }
- func FetchRepositoryDetailsFromGithub(repository string, token string, maxPRs int, maxIssues int) (RepositoryDetails, error) {
- repositoryRequest, err := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/repos/%s", repository), nil)
- if err != nil {
- return RepositoryDetails{}, fmt.Errorf("%w: could not create request with repository: %v", ErrNoContent, err)
- }
- 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)
- 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)
- if token != "" {
- token = fmt.Sprintf("Bearer %s", token)
- repositoryRequest.Header.Add("Authorization", token)
- PRsRequest.Header.Add("Authorization", token)
- issuesRequest.Header.Add("Authorization", token)
- }
- var detailsResponse githubRepositoryDetailsResponseJson
- var detailsErr error
- var PRsResponse githubTicketResponseJson
- var PRsErr error
- var issuesResponse githubTicketResponseJson
- var issuesErr error
- var wg sync.WaitGroup
- wg.Add(1)
- go (func() {
- defer wg.Done()
- detailsResponse, detailsErr = decodeJsonFromRequest[githubRepositoryDetailsResponseJson](defaultClient, repositoryRequest)
- })()
- if maxPRs > 0 {
- wg.Add(1)
- go (func() {
- defer wg.Done()
- PRsResponse, PRsErr = decodeJsonFromRequest[githubTicketResponseJson](defaultClient, PRsRequest)
- })()
- }
- if maxIssues > 0 {
- wg.Add(1)
- go (func() {
- defer wg.Done()
- issuesResponse, issuesErr = decodeJsonFromRequest[githubTicketResponseJson](defaultClient, issuesRequest)
- })()
- }
- wg.Wait()
- if detailsErr != nil {
- return RepositoryDetails{}, fmt.Errorf("%w: could not get repository details: %s", ErrNoContent, detailsErr)
- }
- details := RepositoryDetails{
- Name: detailsResponse.Name,
- Stars: detailsResponse.Stars,
- Forks: detailsResponse.Forks,
- PullRequests: make([]GithubTicket, 0, len(PRsResponse.Tickets)),
- Issues: make([]GithubTicket, 0, len(issuesResponse.Tickets)),
- }
- err = nil
- if maxPRs > 0 {
- if PRsErr != nil {
- err = fmt.Errorf("%w: could not get PRs: %s", ErrPartialContent, PRsErr)
- } else {
- details.OpenPullRequests = PRsResponse.Count
- for i := range PRsResponse.Tickets {
- details.PullRequests = append(details.PullRequests, GithubTicket{
- Number: PRsResponse.Tickets[i].Number,
- CreatedAt: parseGithubTime(PRsResponse.Tickets[i].CreatedAt),
- Title: PRsResponse.Tickets[i].Title,
- })
- }
- }
- }
- if maxIssues > 0 {
- if issuesErr != nil {
- // TODO: fix, overwriting the previous error
- err = fmt.Errorf("%w: could not get issues: %s", ErrPartialContent, issuesErr)
- } else {
- details.OpenIssues = issuesResponse.Count
- for i := range issuesResponse.Tickets {
- details.Issues = append(details.Issues, GithubTicket{
- Number: issuesResponse.Tickets[i].Number,
- CreatedAt: parseGithubTime(issuesResponse.Tickets[i].CreatedAt),
- Title: issuesResponse.Tickets[i].Title,
- })
- }
- }
- }
- return details, err
- }
|