internal/httpd/atom.go

7756246a087737fe2633ac6e794424e139120737
gitbay/internal/httpd/atom.go history · blame · raw

195 lines · 6047 bytes

  1package httpd
  2
  3import (
  4	"encoding/json"
  5	"encoding/xml"
  6	"fmt"
  7	"net/http"
  8	"strings"
  9	"time"
 10
 11	"gitbay.org/gitbay/internal/gitutil"
 12	"gitbay.org/gitbay/internal/sig"
 13	"gitbay.org/gitbay/internal/store"
 14)
 15
 16// Atom feeds (#192): a repository's releases and commits, and an owner's
 17// public activity. Renderers over rows other surfaces already serve;
 18// nothing is stored for them. A feed reader carries no session, so the
 19// owner feed covers public repositories only, and a private repository
 20// answers 404 as its pages do.
 21
 22type atomFeed struct {
 23	XMLName xml.Name    `xml:"http://www.w3.org/2005/Atom feed"`
 24	ID      string      `xml:"id"`
 25	Title   string      `xml:"title"`
 26	Updated string      `xml:"updated"`
 27	Links   []atomLink  `xml:"link"`
 28	Entries []atomEntry `xml:"entry"`
 29}
 30
 31type atomLink struct {
 32	Rel  string `xml:"rel,attr"`
 33	Href string `xml:"href,attr"`
 34	Type string `xml:"type,attr,omitempty"`
 35}
 36
 37type atomEntry struct {
 38	ID      string       `xml:"id"`
 39	Title   string       `xml:"title"`
 40	Updated string       `xml:"updated"`
 41	Author  *atomAuthor  `xml:"author,omitempty"`
 42	Link    atomLink     `xml:"link"`
 43	Content *atomContent `xml:"content,omitempty"`
 44}
 45
 46type atomAuthor struct {
 47	Name string `xml:"name"`
 48}
 49
 50type atomContent struct {
 51	Type string `xml:"type,attr"`
 52	Text string `xml:",chardata"`
 53}
 54
 55const atomLimit = 50
 56
 57func (s *Server) site() string { return strings.TrimSuffix(s.cfg.Server.SiteURL, "/") }
 58
 59// writeAtom serialises the feed. Updated falls back to the newest entry,
 60// then to now: a feed with no entries is still a feed.
 61func (s *Server) writeAtom(w http.ResponseWriter, path, title string, entries []atomEntry) {
 62	f := atomFeed{ID: s.site() + path, Title: title, Entries: entries,
 63		Links: []atomLink{
 64			{Rel: "self", Href: s.site() + path, Type: "application/atom+xml"},
 65			{Rel: "alternate", Href: s.site() + strings.TrimSuffix(strings.TrimSuffix(path, ".atom"), "/activity")},
 66		}}
 67	if len(entries) > 0 {
 68		f.Updated = entries[0].Updated
 69	} else {
 70		f.Updated = time.Now().UTC().Format(time.RFC3339)
 71	}
 72	w.Header().Set("Content-Type", "application/atom+xml; charset=utf-8")
 73	w.Header().Set("X-Content-Type-Options", "nosniff")
 74	fmt.Fprint(w, xml.Header)
 75	xml.NewEncoder(w).Encode(f)
 76}
 77
 78func (s *Server) releasesAtom(w http.ResponseWriter, r *http.Request) {
 79	p, ok := s.repoFor(w, r, "")
 80	if !ok {
 81		return
 82	}
 83	rels, err := s.st.ListReleases(p.Repo.ID)
 84	if err != nil {
 85		http.Error(w, "internal error", http.StatusInternalServerError)
 86		return
 87	}
 88	base := "/" + p.Repo.Path()
 89	var entries []atomEntry
 90	for _, rel := range rels {
 91		title := rel.Tag
 92		if rel.Title != "" {
 93			title = rel.Tag + ": " + rel.Title
 94		}
 95		e := atomEntry{ID: s.site() + base + "/releases#" + rel.Tag, Title: title, Updated: rel.CreatedAt,
 96			Link: atomLink{Rel: "alternate", Href: s.site() + base + "/releases#" + rel.Tag}}
 97		if rel.Author != "" {
 98			e.Author = &atomAuthor{Name: rel.Author}
 99		}
100		if rel.Notes != "" {
101			e.Content = &atomContent{Type: "text", Text: rel.Notes}
102		}
103		entries = append(entries, e)
104	}
105	s.writeAtom(w, base+"/releases.atom", p.Repo.Path()+" releases", entries)
106}
107
108func (s *Server) logAtom(w http.ResponseWriter, r *http.Request) {
109	p, ok := s.repoFor(w, r, r.PathValue("ref"))
110	if !ok {
111		return
112	}
113	shas, err := gitutil.RevList(p.Dir, p.Ref, atomLimit)
114	if err != nil {
115		s.notFound(w, r)
116		return
117	}
118	base := "/" + p.Repo.Path()
119	var entries []atomEntry
120	for _, sha := range shas {
121		e := atomEntry{ID: s.site() + base + "/commit/" + sha, Title: sha[:10],
122			Link: atomLink{Rel: "alternate", Href: s.site() + base + "/commit/" + sha}}
123		if raw, err := gitutil.ReadCommit(p.Dir, sha); err == nil {
124			if c, err := sig.ParseCommit(raw); err == nil {
125				e.Title = c.Subject
126				e.Updated = time.Unix(c.AuthorUnix, 0).UTC().Format(time.RFC3339)
127				e.Author = &atomAuthor{Name: c.AuthorName}
128			}
129		}
130		entries = append(entries, e)
131	}
132	path := base + "/log.atom"
133	if r.PathValue("ref") != "" {
134		path += "/" + p.Ref
135	}
136	s.writeAtom(w, path, p.Repo.Path()+" commits on "+p.Ref, entries)
137}
138
139// ownerAtom is an owner's activity on their public repositories, the
140// feed command's rows without a viewer.
141func (s *Server) ownerAtom(w http.ResponseWriter, r *http.Request) {
142	name := r.PathValue("owner")
143	kind, id := "", int64(0)
144	if u, err := s.st.UserByUsername(name); err == nil {
145		kind, id = "user", u.ID
146	} else if o, err := s.st.OrgByName(name); err == nil {
147		kind, id = "org", o.ID
148	} else {
149		s.notFound(w, r)
150		return
151	}
152	events, err := s.st.OwnerPublicEvents(kind, id, atomLimit)
153	if err != nil {
154		http.Error(w, "internal error", http.StatusInternalServerError)
155		return
156	}
157	var entries []atomEntry
158	for _, ev := range events {
159		title, link := eventTitle(ev)
160		e := atomEntry{ID: fmt.Sprintf("%s/%s/activity.atom#%d", s.site(), name, ev.ID), Title: title, Updated: ev.CreatedAt,
161			Link: atomLink{Rel: "alternate", Href: s.site() + link}}
162		if ev.Actor != "" {
163			e.Author = &atomAuthor{Name: ev.Actor}
164		}
165		entries = append(entries, e)
166	}
167	s.writeAtom(w, "/"+name+"/activity.atom", name+" activity", entries)
168}
169
170// eventTitle names an event and where it points, from the kind and the
171// number or tag its data carries.
172func eventTitle(ev store.FeedEvent) (title, link string) {
173	var data struct {
174		Number int64  `json:"number"`
175		Tag    string `json:"tag"`
176	}
177	json.Unmarshal([]byte(ev.Data), &data)
178	link = "/" + ev.RepoPath
179	title = ev.Kind + " in " + ev.RepoPath
180	switch {
181	case strings.HasPrefix(ev.Kind, "issue.") && data.Number > 0:
182		link += fmt.Sprintf("/issues/%d", data.Number)
183		title = fmt.Sprintf("%s %s#%d", ev.Kind, ev.RepoPath, data.Number)
184	case strings.HasPrefix(ev.Kind, "mr.") && data.Number > 0:
185		link += fmt.Sprintf("/mrs/%d", data.Number)
186		title = fmt.Sprintf("%s %s!%d", ev.Kind, ev.RepoPath, data.Number)
187	case strings.HasPrefix(ev.Kind, "release.") && data.Tag != "":
188		link += "/releases#" + data.Tag
189		title = fmt.Sprintf("%s %s %s", ev.Kind, ev.RepoPath, data.Tag)
190	}
191	if ev.Actor != "" {
192		title = ev.Actor + ": " + title
193	}
194	return title, link
195}