Mirror of @tangled.org/core. Running on a Raspberry Pi Zero 2 (Please be gentle).

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

+117 -1
+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" ··· 14 13 "github.com/bluesky-social/indigo/atproto/syntax" 15 14 lexutil "github.com/bluesky-social/indigo/lex/util" 16 15 "github.com/go-chi/chi/v5" 16 + "github.com/gorilla/feeds" 17 17 "tangled.sh/tangled.sh/core/api/tangled" 18 18 "tangled.sh/tangled.sh/core/appview/db" 19 19 "tangled.sh/tangled.sh/core/appview/pages" ··· 204 202 Following: following, 205 203 }, 206 204 }) 205 + } 206 + 207 + func (s *State) feedFromRequest(w http.ResponseWriter, r *http.Request) *feeds.Feed { 208 + ident, ok := r.Context().Value("resolvedId").(identity.Identity) 209 + if !ok { 210 + s.pages.Error404(w) 211 + return nil 212 + } 213 + 214 + feed, err := s.GetProfileFeed(r.Context(), ident.Handle.String(), ident.DID.String()) 215 + if err != nil { 216 + s.pages.Error500(w) 217 + return nil 218 + } 219 + 220 + return feed 221 + } 222 + 223 + func (s *State) AtomFeedPage(w http.ResponseWriter, r *http.Request) { 224 + feed := s.feedFromRequest(w, r) 225 + if feed == nil { 226 + return 227 + } 228 + 229 + atom, err := feed.ToAtom() 230 + if err != nil { 231 + s.pages.Error500(w) 232 + return 233 + } 234 + 235 + w.Header().Set("content-type", "application/atom+xml") 236 + w.Write([]byte(atom)) 237 + } 238 + 239 + func (s *State) GetProfileFeed(ctx context.Context, handle string, did string) (*feeds.Feed, error) { 240 + timeline, err := db.MakeProfileTimeline(s.db, did) 241 + if err != nil { 242 + return nil, err 243 + } 244 + 245 + author := &feeds.Author{ 246 + Name: fmt.Sprintf("@%s", handle), 247 + } 248 + feed := &feeds.Feed{ 249 + Title: fmt.Sprintf("timeline feed for %s", author.Name), 250 + Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s", s.config.Core.AppviewHost, handle), Type: "text/html", Rel: "alternate"}, 251 + Items: make([]*feeds.Item, 0), 252 + Updated: time.UnixMilli(0), 253 + Author: author, 254 + } 255 + for _, byMonth := range timeline.ByMonth { 256 + for _, pull := range byMonth.PullEvents.Items { 257 + owner, err := s.idResolver.ResolveIdent(ctx, pull.Repo.Did) 258 + if err != nil { 259 + return nil, err 260 + } 261 + feed.Items = append(feed.Items, &feeds.Item{ 262 + Title: fmt.Sprintf("%s created pull request '%s' in @%s/%s", author.Name, pull.Title, owner.Handle, pull.Repo.Name), 263 + 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"}, 264 + Created: pull.Created, 265 + Author: author, 266 + }) 267 + for _, submission := range pull.Submissions { 268 + feed.Items = append(feed.Items, &feeds.Item{ 269 + Title: fmt.Sprintf("%s submitted pull request '%s' (round #%d) in @%s/%s", author.Name, pull.Title, submission.RoundNumber, owner.Handle, pull.Repo.Name), 270 + 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"}, 271 + Created: submission.Created, 272 + Author: author, 273 + }) 274 + } 275 + } 276 + for _, issue := range byMonth.IssueEvents.Items { 277 + owner, err := s.idResolver.ResolveIdent(ctx, issue.Metadata.Repo.Did) 278 + if err != nil { 279 + return nil, err 280 + } 281 + feed.Items = append(feed.Items, &feeds.Item{ 282 + Title: fmt.Sprintf("%s created issue '%s' in @%s/%s", author.Name, issue.Title, owner.Handle, issue.Metadata.Repo.Name), 283 + 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"}, 284 + Created: issue.Created, 285 + Author: author, 286 + }) 287 + } 288 + for _, repo := range byMonth.RepoEvents { 289 + var title string 290 + if repo.Source != nil { 291 + id, err := s.idResolver.ResolveIdent(ctx, repo.Source.Did) 292 + if err != nil { 293 + return nil, err 294 + } 295 + title = fmt.Sprintf("%s forked repository @%s/%s to '%s'", author.Name, id.Handle, repo.Source.Name, repo.Repo.Name) 296 + } else { 297 + title = fmt.Sprintf("%s created repository '%s'", author.Name, repo.Repo.Name) 298 + } 299 + feed.Items = append(feed.Items, &feeds.Item{ 300 + Title: title, 301 + Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s", s.config.Core.AppviewHost, handle, repo.Repo.Name), Type: "text/html", Rel: "alternate"}, 302 + Created: repo.Repo.Created, 303 + Author: author, 304 + }) 305 + } 306 + } 307 + slices.SortFunc(feed.Items, func(a *feeds.Item, b *feeds.Item) int { 308 + return int(b.Created.UnixMilli()) - int(a.Created.UnixMilli()) 309 + }) 310 + if len(feed.Items) > 0 { 311 + feed.Updated = feed.Items[0].Created 312 + } 313 + 314 + return feed, nil 207 315 } 208 316 209 317 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=