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