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/appview/reporesolver"
15
16 "github.com/bluesky-social/indigo/atproto/syntax"
17 "github.com/gorilla/feeds"
18)
19
20func (rp *Repo) getRepoFeed(ctx context.Context, f *reporesolver.ResolvedRepo) (*feeds.Feed, error) {
21 const feedLimitPerType = 100
22
23 pulls, err := db.GetPullsWithLimit(rp.db, feedLimitPerType, db.FilterEq("repo_at", f.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", f.RepoAt()),
32 )
33 if err != nil {
34 return nil, err
35 }
36
37 feed := &feeds.Feed{
38 Title: fmt.Sprintf("activity feed for %s", f.OwnerSlashRepo()),
39 Link: &feeds.Link{Href: fmt.Sprintf("%s/%s", rp.config.Core.AppviewHost, f.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, f)
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, f)
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, f *reporesolver.ResolvedRepo) ([]*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, f.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, f.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, f.OwnerSlashRepo()),
102 Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/pulls/%d/round/%d/", rp.config.Core.AppviewHost, f.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, f *reporesolver.ResolvedRepo) (*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, f.OwnerSlashRepo()),
126 Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/issues/%d", rp.config.Core.AppviewHost, f.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) RepoAtomFeed(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
156 feed, err := rp.getRepoFeed(r.Context(), f)
157 if err != nil {
158 log.Println("failed to get repo feed:", err)
159 rp.pages.Error500(w)
160 return
161 }
162
163 atom, err := feed.ToAtom()
164 if err != nil {
165 rp.pages.Error500(w)
166 return
167 }
168
169 w.Header().Set("content-type", "application/atom+xml")
170 w.Write([]byte(atom))
171}