appview: repo: implement generating feed #476

merged
opened by ptr.pet targeting master from [deleted fork]: repo-feed
Changed files
+120
appview
+119
appview/repo/repo.go
··· 37 37 securejoin "github.com/cyphar/filepath-securejoin" 38 38 "github.com/go-chi/chi/v5" 39 39 "github.com/go-git/go-git/v5/plumbing" 40 + "github.com/gorilla/feeds" 40 41 41 42 comatproto "github.com/bluesky-social/indigo/api/atproto" 42 43 "github.com/bluesky-social/indigo/atproto/syntax" ··· 288 289 } 289 290 } 290 291 292 + func (rp *Repo) getRepoFeed(ctx context.Context, f *reporesolver.ResolvedRepo) (*feeds.Feed, error) { 293 + const feedLimitPerType = 100 294 + 295 + pulls, err := db.GetPullsWithLimit(rp.db, feedLimitPerType, db.FilterEq("repo_at", f.RepoAt())) 296 + if err != nil { 297 + return nil, err 298 + } 299 + 300 + issues, err := db.GetIssuesWithLimit(rp.db, feedLimitPerType, db.FilterEq("repo_at", f.RepoAt())) 301 + if err != nil { 302 + return nil, err 303 + } 304 + 305 + feed := &feeds.Feed{ 306 + Title: fmt.Sprintf("activity feed for %s", f.OwnerSlashRepo()), 307 + Link: &feeds.Link{Href: fmt.Sprintf("%s/%s", rp.config.Core.AppviewHost, f.OwnerSlashRepo()), Type: "text/html", Rel: "alternate"}, 308 + Items: make([]*feeds.Item, 0), 309 + Updated: time.UnixMilli(0), 310 + } 311 + 312 + for _, pull := range pulls { 313 + owner, err := rp.idResolver.ResolveIdent(ctx, pull.OwnerDid) 314 + if err != nil { 315 + return nil, err 316 + } 317 + 318 + for _, round := range pull.Submissions { 319 + item := &feeds.Item{ 320 + Title: fmt.Sprintf("[PR #%d] %s (round #%d)", pull.PullId, pull.Title, round.RoundNumber), 321 + Description: fmt.Sprintf("@%s submitted changes (at round #%d) on PR #%d in %s", owner.Handle, round.RoundNumber, pull.PullId, f.OwnerSlashRepo()), 322 + Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/pulls/%d", rp.config.Core.AppviewHost, f.OwnerSlashRepo(), pull.PullId)}, 323 + Created: pull.Created, 324 + Author: &feeds.Author{ 325 + Name: fmt.Sprintf("@%s", owner.Handle), 326 + }, 327 + } 328 + feed.Items = append(feed.Items, item) 329 + } 330 + 331 + var state string 332 + if pull.State == db.PullOpen { 333 + state = "opened" 334 + } else { 335 + state = pull.State.String() 336 + } 337 + mergedAtRounds := "" 338 + if pull.State == db.PullMerged { 339 + mergedAtRounds = fmt.Sprintf(" (on round #%d)", pull.LastRoundNumber()) 340 + } 341 + item := &feeds.Item{ 342 + Title: fmt.Sprintf("[PR #%d] %s", pull.PullId, pull.Title), 343 + Description: fmt.Sprintf("@%s %s pull request #%d%s in %s", owner.Handle, state, pull.PullId, mergedAtRounds, f.OwnerSlashRepo()), 344 + Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/pulls/%d", rp.config.Core.AppviewHost, f.OwnerSlashRepo(), pull.PullId)}, 345 + Created: pull.Created, 346 + Author: &feeds.Author{ 347 + Name: fmt.Sprintf("@%s", owner.Handle), 348 + }, 349 + } 350 + feed.Items = append(feed.Items, item) 351 + } 352 + 353 + for _, issue := range issues { 354 + owner, err := rp.idResolver.ResolveIdent(ctx, issue.OwnerDid) 355 + if err != nil { 356 + return nil, err 357 + } 358 + var state string 359 + if issue.Open { 360 + state = "opened" 361 + } else { 362 + state = "closed" 363 + } 364 + item := &feeds.Item{ 365 + Title: fmt.Sprintf("[Issue #%d] %s", issue.IssueId, issue.Title), 366 + Description: fmt.Sprintf("@%s %s issue #%d in %s", owner.Handle, state, issue.IssueId, f.OwnerSlashRepo()), 367 + Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/issues/%d", rp.config.Core.AppviewHost, f.OwnerSlashRepo(), issue.IssueId)}, 368 + Created: issue.Created, 369 + Author: &feeds.Author{ 370 + Name: fmt.Sprintf("@%s", owner.Handle), 371 + }, 372 + } 373 + feed.Items = append(feed.Items, item) 374 + } 375 + 376 + slices.SortFunc(feed.Items, func(a *feeds.Item, b *feeds.Item) int { 377 + return int(b.Created.UnixMilli()) - int(a.Created.UnixMilli()) 378 + }) 379 + if len(feed.Items) > 0 { 380 + feed.Updated = feed.Items[0].Created 381 + } 382 + 383 + return feed, nil 384 + } 385 + 386 + func (rp *Repo) RepoAtomFeed(w http.ResponseWriter, r *http.Request) { 387 + f, err := rp.repoResolver.Resolve(r) 388 + if err != nil { 389 + log.Println("failed to fully resolve repo:", err) 390 + return 391 + } 392 + 393 + feed, err := rp.getRepoFeed(r.Context(), f) 394 + if err != nil { 395 + log.Println("failed to get repo feed:", err) 396 + rp.pages.Error500(w) 397 + return 398 + } 399 + 400 + atom, err := feed.ToAtom() 401 + if err != nil { 402 + rp.pages.Error500(w) 403 + return 404 + } 405 + 406 + w.Header().Set("content-type", "application/atom+xml") 407 + w.Write([]byte(atom)) 408 + } 409 + 291 410 func (rp *Repo) RepoCommit(w http.ResponseWriter, r *http.Request) { 292 411 f, err := rp.repoResolver.Resolve(r) 293 412 if err != nil {
+1
appview/repo/router.go
··· 10 10 func (rp *Repo) Router(mw *middleware.Middleware) http.Handler { 11 11 r := chi.NewRouter() 12 12 r.Get("/", rp.RepoIndex) 13 + r.Get("/feed.atom", rp.RepoAtomFeed) 13 14 r.Get("/commits/{ref}", rp.RepoLog) 14 15 r.Route("/tree/{ref}", func(r chi.Router) { 15 16 r.Get("/", rp.RepoIndex)