diff --git a/appview/repo/feed.go b/appview/repo/feed.go new file mode 100644 index 00000000..d41ccee5 --- /dev/null +++ b/appview/repo/feed.go @@ -0,0 +1,165 @@ +package repo + +import ( + "context" + "fmt" + "log" + "net/http" + "slices" + "time" + + "tangled.sh/tangled.sh/core/appview/db" + "tangled.sh/tangled.sh/core/appview/reporesolver" + + "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/gorilla/feeds" +) + +func (rp *Repo) getRepoFeed(ctx context.Context, f *reporesolver.ResolvedRepo) (*feeds.Feed, error) { + const feedLimitPerType = 100 + + pulls, err := db.GetPullsWithLimit(rp.db, feedLimitPerType, db.FilterEq("repo_at", f.RepoAt())) + if err != nil { + return nil, err + } + + issues, err := db.GetIssuesWithLimit(rp.db, feedLimitPerType, db.FilterEq("repo_at", f.RepoAt())) + if err != nil { + return nil, err + } + + feed := &feeds.Feed{ + Title: fmt.Sprintf("activity feed for %s", f.OwnerSlashRepo()), + Link: &feeds.Link{Href: fmt.Sprintf("%s/%s", rp.config.Core.AppviewHost, f.OwnerSlashRepo()), Type: "text/html", Rel: "alternate"}, + Items: make([]*feeds.Item, 0), + Updated: time.UnixMilli(0), + } + + for _, pull := range pulls { + items, err := rp.createPullItems(ctx, pull, f) + if err != nil { + return nil, err + } + feed.Items = append(feed.Items, items...) + } + + for _, issue := range issues { + item, err := rp.createIssueItem(ctx, issue, f) + if err != nil { + return nil, err + } + feed.Items = append(feed.Items, item) + } + + slices.SortFunc(feed.Items, func(a, b *feeds.Item) int { + if a.Created.After(b.Created) { + return -1 + } + return 1 + }) + + if len(feed.Items) > 0 { + feed.Updated = feed.Items[0].Created + } + + return feed, nil +} + +func (rp *Repo) createPullItems(ctx context.Context, pull *db.Pull, f *reporesolver.ResolvedRepo) ([]*feeds.Item, error) { + owner, err := rp.idResolver.ResolveIdent(ctx, pull.OwnerDid) + if err != nil { + return nil, err + } + + var items []*feeds.Item + + state := rp.getPullState(pull) + description := rp.buildPullDescription(owner.Handle, state, pull, f.OwnerSlashRepo()) + + mainItem := &feeds.Item{ + Title: fmt.Sprintf("[PR #%d] %s", pull.PullId, pull.Title), + Description: description, + Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/pulls/%d", rp.config.Core.AppviewHost, f.OwnerSlashRepo(), pull.PullId)}, + Created: pull.Created, + Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)}, + } + items = append(items, mainItem) + + for _, round := range pull.Submissions { + if round == nil || round.RoundNumber == 0 { + continue + } + + roundItem := &feeds.Item{ + Title: fmt.Sprintf("[PR #%d] %s (round #%d)", pull.PullId, pull.Title, round.RoundNumber), + Description: fmt.Sprintf("@%s submitted changes (at round #%d) on PR #%d in %s", owner.Handle, round.RoundNumber, pull.PullId, f.OwnerSlashRepo()), + Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/pulls/%d/round/%d/", rp.config.Core.AppviewHost, f.OwnerSlashRepo(), pull.PullId, round.RoundNumber)}, + Created: round.Created, + Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)}, + } + items = append(items, roundItem) + } + + return items, nil +} + +func (rp *Repo) createIssueItem(ctx context.Context, issue db.Issue, f *reporesolver.ResolvedRepo) (*feeds.Item, error) { + owner, err := rp.idResolver.ResolveIdent(ctx, issue.OwnerDid) + if err != nil { + return nil, err + } + + state := "closed" + if issue.Open { + state = "opened" + } + + return &feeds.Item{ + Title: fmt.Sprintf("[Issue #%d] %s", issue.IssueId, issue.Title), + Description: fmt.Sprintf("@%s %s issue #%d in %s", owner.Handle, state, issue.IssueId, f.OwnerSlashRepo()), + Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/issues/%d", rp.config.Core.AppviewHost, f.OwnerSlashRepo(), issue.IssueId)}, + Created: issue.Created, + Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)}, + }, nil +} + +func (rp *Repo) getPullState(pull *db.Pull) string { + if pull.State == db.PullOpen { + return "opened" + } + return pull.State.String() +} + +func (rp *Repo) buildPullDescription(handle syntax.Handle, state string, pull *db.Pull, repoName string) string { + base := fmt.Sprintf("@%s %s pull request #%d", handle, state, pull.PullId) + + if pull.State == db.PullMerged { + return fmt.Sprintf("%s (on round #%d) in %s", base, pull.LastRoundNumber(), repoName) + } + + return fmt.Sprintf("%s in %s", base, repoName) +} + +func (rp *Repo) RepoAtomFeed(w http.ResponseWriter, r *http.Request) { + f, err := rp.repoResolver.Resolve(r) + if err != nil { + log.Println("failed to fully resolve repo:", err) + return + } + + feed, err := rp.getRepoFeed(r.Context(), f) + if err != nil { + log.Println("failed to get repo feed:", err) + rp.pages.Error500(w) + return + } + + atom, err := feed.ToAtom() + if err != nil { + rp.pages.Error500(w) + return + } + + w.Header().Set("content-type", "application/atom+xml") + w.Write([]byte(atom)) +} diff --git a/appview/repo/repo.go b/appview/repo/repo.go index d4c54d54..c1f1d338 100644 --- a/appview/repo/repo.go +++ b/appview/repo/repo.go @@ -37,7 +37,6 @@ import ( securejoin "github.com/cyphar/filepath-securejoin" "github.com/go-chi/chi/v5" "github.com/go-git/go-git/v5/plumbing" - "github.com/gorilla/feeds" comatproto "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/atproto/syntax" @@ -289,127 +288,6 @@ func (rp *Repo) RepoDescription(w http.ResponseWriter, r *http.Request) { } } -func (rp *Repo) getRepoFeed(ctx context.Context, f *reporesolver.ResolvedRepo) (*feeds.Feed, error) { - const feedLimitPerType = 100 - - pulls, err := db.GetPullsWithLimit(rp.db, feedLimitPerType, db.FilterEq("repo_at", f.RepoAt())) - if err != nil { - return nil, err - } - - issues, err := db.GetIssuesWithLimit(rp.db, feedLimitPerType, db.FilterEq("repo_at", f.RepoAt())) - if err != nil { - return nil, err - } - - feed := &feeds.Feed{ - Title: fmt.Sprintf("activity feed for %s", f.OwnerSlashRepo()), - Link: &feeds.Link{Href: fmt.Sprintf("%s/%s", rp.config.Core.AppviewHost, f.OwnerSlashRepo()), Type: "text/html", Rel: "alternate"}, - Items: make([]*feeds.Item, 0), - Updated: time.UnixMilli(0), - } - - for _, pull := range pulls { - owner, err := rp.idResolver.ResolveIdent(ctx, pull.OwnerDid) - if err != nil { - return nil, err - } - - var state string - if pull.State == db.PullOpen { - state = "opened" - } else { - state = pull.State.String() - } - mergedAtRounds := "" - if pull.State == db.PullMerged { - mergedAtRounds = fmt.Sprintf(" (on round #%d)", pull.LastRoundNumber()) - } - item := &feeds.Item{ - Title: fmt.Sprintf("[PR #%d] %s", pull.PullId, pull.Title), - Description: fmt.Sprintf("@%s %s pull request #%d%s in %s", owner.Handle, state, pull.PullId, mergedAtRounds, f.OwnerSlashRepo()), - Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/pulls/%d", rp.config.Core.AppviewHost, f.OwnerSlashRepo(), pull.PullId)}, - Created: pull.Created, - Author: &feeds.Author{ - Name: fmt.Sprintf("@%s", owner.Handle), - }, - } - feed.Items = append(feed.Items, item) - - for _, round := range pull.Submissions { - if round == nil || round.RoundNumber == 0 { - continue - } - item := &feeds.Item{ - Title: fmt.Sprintf("[PR #%d] %s (round #%d)", pull.PullId, pull.Title, round.RoundNumber), - Description: fmt.Sprintf("@%s submitted changes (at round #%d) on PR #%d in %s", owner.Handle, round.RoundNumber, pull.PullId, f.OwnerSlashRepo()), - Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/pulls/%d/round/%d/", rp.config.Core.AppviewHost, f.OwnerSlashRepo(), pull.PullId, round.RoundNumber)}, - Created: round.Created, - Author: &feeds.Author{ - Name: fmt.Sprintf("@%s", owner.Handle), - }, - } - feed.Items = append(feed.Items, item) - } - } - - for _, issue := range issues { - owner, err := rp.idResolver.ResolveIdent(ctx, issue.OwnerDid) - if err != nil { - return nil, err - } - var state string - if issue.Open { - state = "opened" - } else { - state = "closed" - } - item := &feeds.Item{ - Title: fmt.Sprintf("[Issue #%d] %s", issue.IssueId, issue.Title), - Description: fmt.Sprintf("@%s %s issue #%d in %s", owner.Handle, state, issue.IssueId, f.OwnerSlashRepo()), - Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/issues/%d", rp.config.Core.AppviewHost, f.OwnerSlashRepo(), issue.IssueId)}, - Created: issue.Created, - Author: &feeds.Author{ - Name: fmt.Sprintf("@%s", owner.Handle), - }, - } - feed.Items = append(feed.Items, item) - } - - slices.SortFunc(feed.Items, func(a *feeds.Item, b *feeds.Item) int { - return int(b.Created.UnixMilli()) - int(a.Created.UnixMilli()) - }) - if len(feed.Items) > 0 { - feed.Updated = feed.Items[0].Created - } - - return feed, nil -} - -func (rp *Repo) RepoAtomFeed(w http.ResponseWriter, r *http.Request) { - f, err := rp.repoResolver.Resolve(r) - if err != nil { - log.Println("failed to fully resolve repo:", err) - return - } - - feed, err := rp.getRepoFeed(r.Context(), f) - if err != nil { - log.Println("failed to get repo feed:", err) - rp.pages.Error500(w) - return - } - - atom, err := feed.ToAtom() - if err != nil { - rp.pages.Error500(w) - return - } - - w.Header().Set("content-type", "application/atom+xml") - w.Write([]byte(atom)) -} - func (rp *Repo) RepoCommit(w http.ResponseWriter, r *http.Request) { f, err := rp.repoResolver.Resolve(r) if err != nil {