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