appview: repo: implement generating feed #476

merged
opened by ptr.pet targeting master from [deleted fork]: repo-feed
Changed files
+102
appview
+101
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 + var state string 318 + if pull.State == db.PullOpen { 319 + state = "opened" 320 + } else { 321 + state = pull.State.String() 322 + } 323 + item := &feeds.Item{ 324 + Title: fmt.Sprintf("[PR] %s", pull.Title), 325 + Description: fmt.Sprintf("@%s %s pull request on %s", owner.Handle, state, f.OwnerSlashRepo()), 326 + Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/pulls/%d", rp.config.Core.AppviewHost, f.OwnerSlashRepo(), pull.PullId)}, 327 + Created: pull.Created, 328 + Author: &feeds.Author{ 329 + Name: fmt.Sprintf("@%s", owner.Handle), 330 + }, 331 + } 332 + feed.Items = append(feed.Items, item) 333 + } 334 + 335 + for _, issue := range issues { 336 + owner, err := rp.idResolver.ResolveIdent(ctx, issue.OwnerDid) 337 + if err != nil { 338 + return nil, err 339 + } 340 + var state string 341 + if issue.Open { 342 + state = "opened" 343 + } else { 344 + state = "closed" 345 + } 346 + item := &feeds.Item{ 347 + Title: fmt.Sprintf("[Issue] %s", issue.Title), 348 + Description: fmt.Sprintf("@%s %s issue on %s", owner.Handle, state, f.OwnerSlashRepo()), 349 + Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/issues/%d", rp.config.Core.AppviewHost, f.OwnerSlashRepo(), issue.IssueId)}, 350 + Created: issue.Created, 351 + Author: &feeds.Author{ 352 + Name: fmt.Sprintf("@%s", owner.Handle), 353 + }, 354 + } 355 + feed.Items = append(feed.Items, item) 356 + } 357 + 358 + slices.SortFunc(feed.Items, func(a *feeds.Item, b *feeds.Item) int { 359 + return int(b.Created.UnixMilli()) - int(a.Created.UnixMilli()) 360 + }) 361 + if len(feed.Items) > 0 { 362 + feed.Updated = feed.Items[0].Created 363 + } 364 + 365 + return feed, nil 366 + } 367 + 368 + func (rp *Repo) RepoAtomFeed(w http.ResponseWriter, r *http.Request) { 369 + f, err := rp.repoResolver.Resolve(r) 370 + if err != nil { 371 + log.Println("failed to fully resolve repo:", err) 372 + return 373 + } 374 + 375 + feed, err := rp.getRepoFeed(r.Context(), f) 376 + if err != nil { 377 + log.Println("failed to get repo feed:", err) 378 + rp.pages.Error500(w) 379 + return 380 + } 381 + 382 + atom, err := feed.ToAtom() 383 + if err != nil { 384 + rp.pages.Error500(w) 385 + return 386 + } 387 + 388 + w.Header().Set("content-type", "application/atom+xml") 389 + w.Write([]byte(atom)) 390 + } 391 + 291 392 func (rp *Repo) RepoCommit(w http.ResponseWriter, r *http.Request) { 292 393 f, err := rp.repoResolver.Resolve(r) 293 394 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)