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