forked from tangled.org/core
this repo has no description

appview: generate atom feed from profile timeline for users

Signed-off-by: dusk <y.bera003.06@protonmail.com>

Change-Id: uxnwsxtukznolvmwopkpxmtsvztvsusu

authored by ptr.pet and committed by Tangled aa1e87eb 5e8270d9

Changed files
+117 -1
appview
+1 -1
appview/middleware/middleware.go
··· 183 183 id, err := mw.idResolver.ResolveIdent(req.Context(), didOrHandle) 184 184 if err != nil { 185 185 // invalid did or handle 186 - log.Println("failed to resolve did/handle:", err) 186 + log.Printf("failed to resolve did/handle '%s': %s\n", didOrHandle, err) 187 187 mw.pages.Error404(w) 188 188 return 189 189 }
+112
appview/state/profile.go
··· 1 1 package state 2 2 3 3 import ( 4 + "context" 4 5 "fmt" 5 6 "log" 6 7 "net/http" ··· 13 14 "github.com/bluesky-social/indigo/atproto/syntax" 14 15 lexutil "github.com/bluesky-social/indigo/lex/util" 15 16 "github.com/go-chi/chi/v5" 17 + "github.com/gorilla/feeds" 16 18 "tangled.sh/tangled.sh/core/api/tangled" 17 19 "tangled.sh/tangled.sh/core/appview/db" 18 20 "tangled.sh/tangled.sh/core/appview/pages" ··· 202 204 Following: following, 203 205 }, 204 206 }) 207 + } 208 + 209 + func (s *State) feedFromRequest(w http.ResponseWriter, r *http.Request) *feeds.Feed { 210 + ident, ok := r.Context().Value("resolvedId").(identity.Identity) 211 + if !ok { 212 + s.pages.Error404(w) 213 + return nil 214 + } 215 + 216 + feed, err := s.GetProfileFeed(r.Context(), ident.Handle.String(), ident.DID.String()) 217 + if err != nil { 218 + s.pages.Error500(w) 219 + return nil 220 + } 221 + 222 + return feed 223 + } 224 + 225 + func (s *State) AtomFeedPage(w http.ResponseWriter, r *http.Request) { 226 + feed := s.feedFromRequest(w, r) 227 + if feed == nil { 228 + return 229 + } 230 + 231 + atom, err := feed.ToAtom() 232 + if err != nil { 233 + s.pages.Error500(w) 234 + return 235 + } 236 + 237 + w.Header().Set("content-type", "application/atom+xml") 238 + w.Write([]byte(atom)) 239 + } 240 + 241 + func (s *State) GetProfileFeed(ctx context.Context, handle string, did string) (*feeds.Feed, error) { 242 + timeline, err := db.MakeProfileTimeline(s.db, did) 243 + if err != nil { 244 + return nil, err 245 + } 246 + 247 + author := &feeds.Author{ 248 + Name: fmt.Sprintf("@%s", handle), 249 + } 250 + feed := &feeds.Feed{ 251 + Title: fmt.Sprintf("timeline feed for %s", author.Name), 252 + Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s", s.config.Core.AppviewHost, handle), Type: "text/html", Rel: "alternate"}, 253 + Items: make([]*feeds.Item, 0), 254 + Updated: time.UnixMilli(0), 255 + Author: author, 256 + } 257 + for _, byMonth := range timeline.ByMonth { 258 + for _, pull := range byMonth.PullEvents.Items { 259 + owner, err := s.idResolver.ResolveIdent(ctx, pull.Repo.Did) 260 + if err != nil { 261 + return nil, err 262 + } 263 + feed.Items = append(feed.Items, &feeds.Item{ 264 + Title: fmt.Sprintf("%s created pull request '%s' in @%s/%s", author.Name, pull.Title, owner.Handle, pull.Repo.Name), 265 + Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/pulls/%d", s.config.Core.AppviewHost, owner.Handle, pull.Repo.Name, pull.PullId), Type: "text/html", Rel: "alternate"}, 266 + Created: pull.Created, 267 + Author: author, 268 + }) 269 + for _, submission := range pull.Submissions { 270 + feed.Items = append(feed.Items, &feeds.Item{ 271 + Title: fmt.Sprintf("%s submitted pull request '%s' (round #%d) in @%s/%s", author.Name, pull.Title, submission.RoundNumber, owner.Handle, pull.Repo.Name), 272 + Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/pulls/%d", s.config.Core.AppviewHost, owner.Handle, pull.Repo.Name, pull.PullId), Type: "text/html", Rel: "alternate"}, 273 + Created: submission.Created, 274 + Author: author, 275 + }) 276 + } 277 + } 278 + for _, issue := range byMonth.IssueEvents.Items { 279 + owner, err := s.idResolver.ResolveIdent(ctx, issue.Metadata.Repo.Did) 280 + if err != nil { 281 + return nil, err 282 + } 283 + feed.Items = append(feed.Items, &feeds.Item{ 284 + Title: fmt.Sprintf("%s created issue '%s' in @%s/%s", author.Name, issue.Title, owner.Handle, issue.Metadata.Repo.Name), 285 + Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/issues/%d", s.config.Core.AppviewHost, owner.Handle, issue.Metadata.Repo.Name, issue.IssueId), Type: "text/html", Rel: "alternate"}, 286 + Created: issue.Created, 287 + Author: author, 288 + }) 289 + } 290 + for _, repo := range byMonth.RepoEvents { 291 + var title string 292 + if repo.Source != nil { 293 + id, err := s.idResolver.ResolveIdent(ctx, repo.Source.Did) 294 + if err != nil { 295 + return nil, err 296 + } 297 + title = fmt.Sprintf("%s forked repository @%s/%s to '%s'", author.Name, id.Handle, repo.Source.Name, repo.Repo.Name) 298 + } else { 299 + title = fmt.Sprintf("%s created repository '%s'", author.Name, repo.Repo.Name) 300 + } 301 + feed.Items = append(feed.Items, &feeds.Item{ 302 + Title: title, 303 + Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s", s.config.Core.AppviewHost, handle, repo.Repo.Name), Type: "text/html", Rel: "alternate"}, 304 + Created: repo.Repo.Created, 305 + Author: author, 306 + }) 307 + } 308 + } 309 + slices.SortFunc(feed.Items, func(a *feeds.Item, b *feeds.Item) int { 310 + return int(b.Created.UnixMilli()) - int(a.Created.UnixMilli()) 311 + }) 312 + if len(feed.Items) > 0 { 313 + feed.Updated = feed.Items[0].Created 314 + } 315 + 316 + return feed, nil 205 317 } 206 318 207 319 func (s *State) UpdateProfileBio(w http.ResponseWriter, r *http.Request) {
+1
appview/state/router.go
··· 70 70 71 71 r.With(mw.ResolveIdent()).Route("/{user}", func(r chi.Router) { 72 72 r.Get("/", s.Profile) 73 + r.Get("/feed.atom", s.AtomFeedPage) 73 74 74 75 r.With(mw.ResolveRepo()).Route("/{repo}", func(r chi.Router) { 75 76 r.Use(mw.GoImport())
+1
go.mod
··· 88 88 github.com/golang/mock v1.6.0 // indirect 89 89 github.com/google/go-querystring v1.1.0 // indirect 90 90 github.com/gorilla/css v1.0.1 // indirect 91 + github.com/gorilla/feeds v1.2.0 // indirect 91 92 github.com/gorilla/securecookie v1.1.2 // indirect 92 93 github.com/hashicorp/errwrap v1.1.0 // indirect 93 94 github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
+2
go.sum
··· 173 173 github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 174 174 github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= 175 175 github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= 176 + github.com/gorilla/feeds v1.2.0 h1:O6pBiXJ5JHhPvqy53NsjKOThq+dNFm8+DFrxBEdzSCc= 177 + github.com/gorilla/feeds v1.2.0/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y= 176 178 github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= 177 179 github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= 178 180 github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=