forked from
tangled.org/core
Monorepo for Tangled — https://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 "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}