package httpd import ( "encoding/xml" "net/http" "time" "github.com/barerepo/server/internal/store" "github.com/barerepo/server/internal/token" "github.com/barerepo/server/internal/transport" ) // Atom is the whole notification system: static XML, in software the reader already chose. type atomFeed struct { XMLName xml.Name `xml:"http://www.w3.org/2005/Atom feed"` Title string `xml:"title"` ID string `xml:"id"` Updated string `xml:"updated"` Link []atomLink `xml:"link"` Entries []atomEntry `xml:"entry"` } type atomLink struct { Href string `xml:"href,attr"` Rel string `xml:"rel,attr,omitempty"` } type atomEntry struct { Title string `xml:"title"` ID string `xml:"id"` Updated string `xml:"updated"` Link atomLink `xml:"link"` Author atomAuthor `xml:"author"` Summary string `xml:"summary,omitempty"` } type atomAuthor struct { Name string `xml:"name"` } func (s *Server) writeFeed(w http.ResponseWriter, r *http.Request, title, self string, events []store.Event) { base := s.Cfg.Server.ExternalURL feed := atomFeed{ Title: title, ID: base + self, Updated: time.Now().UTC().Format(time.RFC3339), Link: []atomLink{{Href: base + self, Rel: "self"}}, } if len(events) > 0 { feed.Updated = events[0].Created.UTC().Format(time.RFC3339) } for _, e := range events { feed.Entries = append(feed.Entries, atomEntry{ Title: eventLine(e), ID: base + eventHref(e) + "#" + itoa(int(e.ID)), Updated: e.Created.UTC().Format(time.RFC3339), Link: atomLink{Href: base + eventHref(e)}, Author: atomAuthor{Name: e.Actor}, Summary: eventDetail(e), }) } w.Header().Set("Content-Type", "application/atom+xml; charset=utf-8") w.Write([]byte(xml.Header)) enc := xml.NewEncoder(w) enc.Indent("", " ") if err := enc.Encode(feed); err != nil { s.log(r, err) } } // serveInboxFeed takes a read-only token, because a feed reader keeps URLs in plain text. func (s *Server) serveInboxFeed(w http.ResponseWriter, r *http.Request) { // A feed reader has no session, so it carries a token. A browser already has one. 19.5. who := s.viewer(r) if tok := r.URL.Query().Get("token"); tok != "" || who == "" { t, err := s.DB.AccountForToken(r.Context(), token.Feed, tok) if err != nil { http.Error(w, "this feed needs a feed token. make one on your keys page.", http.StatusUnauthorized) return } who = t.Account } events, err := s.DB.Inbox(r.Context(), who, 50) if err != nil { s.oops(w, r, err) return } s.writeFeed(w, r, "barerepo inbox for "+who, "/inbox.atom", events) } // serveRepoFeed is public repositories only and needs no token, per chapter 19.5. func (s *Server) serveRepoFeed(w http.ResponseWriter, r *http.Request, owner, name string) { res, _, ok := s.openRepoFor(w, r, owner, name, "") if !ok { return } if !res.Config.Public() { http.NotFound(w, r) return } events, err := s.DB.EventsFor(r.Context(), owner+"/"+name, 50) if err != nil { s.oops(w, r, err) return } s.writeFeed(w, r, owner+"/"+name, "/"+owner+"/"+name+".atom", events) } // serveUserFeed answers /.atom, one account's public activity. func (s *Server) serveUserFeed(w http.ResponseWriter, r *http.Request, account string) { if _, err := s.DB.Account(r.Context(), account); err != nil { http.NotFound(w, r) return } events, err := s.DB.EventsBy(r.Context(), account, 50) if err != nil { s.oops(w, r, err) return } // An account's feed carries only what happened somewhere anyone can see, asked once per repository because fifty events are usually a handful of them. public := make([]store.Event, 0, len(events)) seen := map[string]bool{} for _, e := range events { visible, known := seen[e.Repo] if !known { owner, repoName, _ := cutRepo(e.Repo) res, err := s.Transport.Open(r.Context(), owner, repoName, "", transport.Read) visible = err == nil && res.Config.Public() seen[e.Repo] = visible } if visible { public = append(public, e) } } s.writeFeed(w, r, account, "/"+account+".atom", public) } func cutRepo(repo string) (owner, name string, ok bool) { for i := 0; i < len(repo); i++ { if repo[i] == '/' { return repo[:i], repo[i+1:], true } } return repo, "", false } // serveThreadsFeed answers ///threads.atom, the discussion alone, per chapter 19.5. func (s *Server) serveThreadsFeed(w http.ResponseWriter, r *http.Request, owner, name string) { res, _, ok := s.openRepoFor(w, r, owner, name, "") if !ok { return } if !res.Config.Public() { http.NotFound(w, r) return } events, err := s.DB.ThreadEventsFor(r.Context(), owner+"/"+name, 50) if err != nil { s.oops(w, r, err) return } s.writeFeed(w, r, owner+"/"+name+" threads", "/"+owner+"/"+name+"/threads.atom", events) }