forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
at master 5.2 kB view raw
1package repo 2 3import ( 4 "context" 5 "fmt" 6 "log" 7 "net/http" 8 "slices" 9 "time" 10 11 "tangled.org/core/appview/db" 12 "tangled.org/core/appview/models" 13 "tangled.org/core/appview/pagination" 14 "tangled.org/core/orm" 15 16 "github.com/bluesky-social/indigo/atproto/identity" 17 "github.com/bluesky-social/indigo/atproto/syntax" 18 "github.com/gorilla/feeds" 19) 20 21func (rp *Repo) getRepoFeed(ctx context.Context, repo *models.Repo, ownerSlashRepo string) (*feeds.Feed, error) { 22 const feedLimitPerType = 100 23 24 pulls, err := db.GetPullsWithLimit(rp.db, feedLimitPerType, orm.FilterEq("repo_at", repo.RepoAt())) 25 if err != nil { 26 return nil, err 27 } 28 29 issues, err := db.GetIssuesPaginated( 30 rp.db, 31 pagination.Page{Limit: feedLimitPerType}, 32 orm.FilterEq("repo_at", repo.RepoAt()), 33 ) 34 if err != nil { 35 return nil, err 36 } 37 38 feed := &feeds.Feed{ 39 Title: fmt.Sprintf("activity feed for @%s", ownerSlashRepo), 40 Link: &feeds.Link{Href: fmt.Sprintf("%s/%s", rp.config.Core.AppviewHost, ownerSlashRepo), Type: "text/html", Rel: "alternate"}, 41 Items: make([]*feeds.Item, 0), 42 Updated: time.UnixMilli(0), 43 } 44 45 for _, pull := range pulls { 46 items, err := rp.createPullItems(ctx, pull, repo, ownerSlashRepo) 47 if err != nil { 48 return nil, err 49 } 50 feed.Items = append(feed.Items, items...) 51 } 52 53 for _, issue := range issues { 54 item, err := rp.createIssueItem(ctx, issue, repo, ownerSlashRepo) 55 if err != nil { 56 return nil, err 57 } 58 feed.Items = append(feed.Items, item) 59 } 60 61 slices.SortFunc(feed.Items, func(a, b *feeds.Item) int { 62 if a.Created.After(b.Created) { 63 return -1 64 } 65 return 1 66 }) 67 68 if len(feed.Items) > 0 { 69 feed.Updated = feed.Items[0].Created 70 } 71 72 return feed, nil 73} 74 75func (rp *Repo) createPullItems(ctx context.Context, pull *models.Pull, repo *models.Repo, ownerSlashRepo string) ([]*feeds.Item, error) { 76 owner, err := rp.idResolver.ResolveIdent(ctx, pull.OwnerDid) 77 if err != nil { 78 return nil, err 79 } 80 81 var items []*feeds.Item 82 83 state := rp.getPullState(pull) 84 description := rp.buildPullDescription(owner.Handle, state, pull, ownerSlashRepo) 85 86 mainItem := &feeds.Item{ 87 Title: fmt.Sprintf("[PR #%d] %s", pull.PullId, pull.Title), 88 Description: description, 89 Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/pulls/%d", rp.config.Core.AppviewHost, ownerSlashRepo, pull.PullId)}, 90 Created: pull.Created, 91 Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)}, 92 } 93 items = append(items, mainItem) 94 95 for _, round := range pull.Submissions { 96 if round == nil || round.RoundNumber == 0 { 97 continue 98 } 99 100 roundItem := &feeds.Item{ 101 Title: fmt.Sprintf("[PR #%d] %s (round #%d)", pull.PullId, pull.Title, round.RoundNumber), 102 Description: fmt.Sprintf("@%s submitted changes (at round #%d) on PR #%d in @%s", owner.Handle, round.RoundNumber, pull.PullId, ownerSlashRepo), 103 Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/pulls/%d/round/%d/", rp.config.Core.AppviewHost, ownerSlashRepo, pull.PullId, round.RoundNumber)}, 104 Created: round.Created, 105 Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)}, 106 } 107 items = append(items, roundItem) 108 } 109 110 return items, nil 111} 112 113func (rp *Repo) createIssueItem(ctx context.Context, issue models.Issue, repo *models.Repo, ownerSlashRepo string) (*feeds.Item, error) { 114 owner, err := rp.idResolver.ResolveIdent(ctx, issue.Did) 115 if err != nil { 116 return nil, err 117 } 118 119 state := "closed" 120 if issue.Open { 121 state = "opened" 122 } 123 124 return &feeds.Item{ 125 Title: fmt.Sprintf("[Issue #%d] %s", issue.IssueId, issue.Title), 126 Description: fmt.Sprintf("@%s %s issue #%d in @%s", owner.Handle, state, issue.IssueId, ownerSlashRepo), 127 Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/issues/%d", rp.config.Core.AppviewHost, ownerSlashRepo, issue.IssueId)}, 128 Created: issue.Created, 129 Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)}, 130 }, nil 131} 132 133func (rp *Repo) getPullState(pull *models.Pull) string { 134 if pull.State == models.PullOpen { 135 return "opened" 136 } 137 return pull.State.String() 138} 139 140func (rp *Repo) buildPullDescription(handle syntax.Handle, state string, pull *models.Pull, repoName string) string { 141 base := fmt.Sprintf("@%s %s pull request #%d", handle, state, pull.PullId) 142 143 if pull.State == models.PullMerged { 144 return fmt.Sprintf("%s (on round #%d) in %s", base, pull.LastRoundNumber(), repoName) 145 } 146 147 return fmt.Sprintf("%s in %s", base, repoName) 148} 149 150func (rp *Repo) AtomFeed(w http.ResponseWriter, r *http.Request) { 151 f, err := rp.repoResolver.Resolve(r) 152 if err != nil { 153 log.Println("failed to fully resolve repo:", err) 154 return 155 } 156 repoOwnerId, ok := r.Context().Value("resolvedId").(identity.Identity) 157 if !ok || repoOwnerId.Handle.IsInvalidHandle() { 158 log.Println("failed to get resolved repo owner id") 159 return 160 } 161 ownerSlashRepo := repoOwnerId.Handle.String() + "/" + f.Name 162 163 feed, err := rp.getRepoFeed(r.Context(), f, ownerSlashRepo) 164 if err != nil { 165 log.Println("failed to get repo feed:", err) 166 rp.pages.Error500(w) 167 return 168 } 169 170 atom, err := feed.ToAtom() 171 if err != nil { 172 rp.pages.Error500(w) 173 return 174 } 175 176 w.Header().Set("content-type", "application/atom+xml") 177 w.Write([]byte(atom)) 178}