File view with blame information shown in the left gutter beside each line.

barerepo / server / internal/httpd/atom.go
164 lines · 4.7kb · 133728eaa05486504991b230f9d4c7e986b7defc
log files threads runs releases config jump to file t
133728e barerepo 1mo
1
package httpd
133728e barerepo 1mo
2
133728e barerepo 1mo
3
import (
133728e barerepo 1mo
4
"encoding/xml"
133728e barerepo 1mo
5
"net/http"
133728e barerepo 1mo
6
"time"
133728e barerepo 1mo
7
133728e barerepo 1mo
8
"github.com/barerepo/server/internal/store"
133728e barerepo 1mo
9
"github.com/barerepo/server/internal/token"
133728e barerepo 1mo
10
"github.com/barerepo/server/internal/transport"
133728e barerepo 1mo
11
)
133728e barerepo 1mo
12
133728e barerepo 1mo
13
// Atom is the whole notification system: static XML, in software the reader already chose.
133728e barerepo 1mo
14
type atomFeed struct {
133728e barerepo 1mo
15
XMLName xml.Name `xml:"http://www.w3.org/2005/Atom feed"`
133728e barerepo 1mo
16
Title string `xml:"title"`
133728e barerepo 1mo
17
ID string `xml:"id"`
133728e barerepo 1mo
18
Updated string `xml:"updated"`
133728e barerepo 1mo
19
Link []atomLink `xml:"link"`
133728e barerepo 1mo
20
Entries []atomEntry `xml:"entry"`
133728e barerepo 1mo
21
}
133728e barerepo 1mo
22
133728e barerepo 1mo
23
type atomLink struct {
133728e barerepo 1mo
24
Href string `xml:"href,attr"`
133728e barerepo 1mo
25
Rel string `xml:"rel,attr,omitempty"`
133728e barerepo 1mo
26
}
133728e barerepo 1mo
27
133728e barerepo 1mo
28
type atomEntry struct {
133728e barerepo 1mo
29
Title string `xml:"title"`
133728e barerepo 1mo
30
ID string `xml:"id"`
133728e barerepo 1mo
31
Updated string `xml:"updated"`
133728e barerepo 1mo
32
Link atomLink `xml:"link"`
133728e barerepo 1mo
33
Author atomAuthor `xml:"author"`
133728e barerepo 1mo
34
Summary string `xml:"summary,omitempty"`
133728e barerepo 1mo
35
}
133728e barerepo 1mo
36
133728e barerepo 1mo
37
type atomAuthor struct {
133728e barerepo 1mo
38
Name string `xml:"name"`
133728e barerepo 1mo
39
}
133728e barerepo 1mo
40
133728e barerepo 1mo
41
func (s *Server) writeFeed(w http.ResponseWriter, r *http.Request, title, self string, events []store.Event) {
133728e barerepo 1mo
42
base := s.Cfg.Server.ExternalURL
133728e barerepo 1mo
43
feed := atomFeed{
133728e barerepo 1mo
44
Title: title,
133728e barerepo 1mo
45
ID: base + self,
133728e barerepo 1mo
46
Updated: time.Now().UTC().Format(time.RFC3339),
133728e barerepo 1mo
47
Link: []atomLink{{Href: base + self, Rel: "self"}},
133728e barerepo 1mo
48
}
133728e barerepo 1mo
49
if len(events) > 0 {
133728e barerepo 1mo
50
feed.Updated = events[0].Created.UTC().Format(time.RFC3339)
133728e barerepo 1mo
51
}
133728e barerepo 1mo
52
for _, e := range events {
133728e barerepo 1mo
53
feed.Entries = append(feed.Entries, atomEntry{
133728e barerepo 1mo
54
Title: eventLine(e),
133728e barerepo 1mo
55
ID: base + eventHref(e) + "#" + itoa(int(e.ID)),
133728e barerepo 1mo
56
Updated: e.Created.UTC().Format(time.RFC3339),
133728e barerepo 1mo
57
Link: atomLink{Href: base + eventHref(e)},
133728e barerepo 1mo
58
Author: atomAuthor{Name: e.Actor},
133728e barerepo 1mo
59
Summary: eventDetail(e),
133728e barerepo 1mo
60
})
133728e barerepo 1mo
61
}
133728e barerepo 1mo
62
w.Header().Set("Content-Type", "application/atom+xml; charset=utf-8")
133728e barerepo 1mo
63
w.Write([]byte(xml.Header))
133728e barerepo 1mo
64
enc := xml.NewEncoder(w)
133728e barerepo 1mo
65
enc.Indent("", " ")
133728e barerepo 1mo
66
if err := enc.Encode(feed); err != nil {
133728e barerepo 1mo
67
s.log(r, err)
133728e barerepo 1mo
68
}
133728e barerepo 1mo
69
}
133728e barerepo 1mo
70
133728e barerepo 1mo
71
// serveInboxFeed takes a read-only token, because a feed reader keeps URLs in plain text.
133728e barerepo 1mo
72
func (s *Server) serveInboxFeed(w http.ResponseWriter, r *http.Request) {
133728e barerepo 1mo
73
// A feed reader has no session, so it carries a token. A browser already has one. 19.5.
133728e barerepo 1mo
74
who := s.viewer(r)
133728e barerepo 1mo
75
if tok := r.URL.Query().Get("token"); tok != "" || who == "" {
133728e barerepo 1mo
76
t, err := s.DB.AccountForToken(r.Context(), token.Feed, tok)
133728e barerepo 1mo
77
if err != nil {
133728e barerepo 1mo
78
http.Error(w, "this feed needs a feed token. make one on your keys page.",
133728e barerepo 1mo
79
http.StatusUnauthorized)
133728e barerepo 1mo
80
return
133728e barerepo 1mo
81
}
133728e barerepo 1mo
82
who = t.Account
133728e barerepo 1mo
83
}
133728e barerepo 1mo
84
events, err := s.DB.Inbox(r.Context(), who, 50)
133728e barerepo 1mo
85
if err != nil {
133728e barerepo 1mo
86
s.oops(w, r, err)
133728e barerepo 1mo
87
return
133728e barerepo 1mo
88
}
133728e barerepo 1mo
89
s.writeFeed(w, r, "barerepo inbox for "+who, "/inbox.atom", events)
133728e barerepo 1mo
90
}
133728e barerepo 1mo
91
133728e barerepo 1mo
92
// serveRepoFeed is public repositories only and needs no token, per chapter 19.5.
133728e barerepo 1mo
93
func (s *Server) serveRepoFeed(w http.ResponseWriter, r *http.Request, owner, name string) {
133728e barerepo 1mo
94
res, _, ok := s.openRepoFor(w, r, owner, name, "")
133728e barerepo 1mo
95
if !ok {
133728e barerepo 1mo
96
return
133728e barerepo 1mo
97
}
133728e barerepo 1mo
98
if !res.Config.Public() {
133728e barerepo 1mo
99
http.NotFound(w, r)
133728e barerepo 1mo
100
return
133728e barerepo 1mo
101
}
133728e barerepo 1mo
102
events, err := s.DB.EventsFor(r.Context(), owner+"/"+name, 50)
133728e barerepo 1mo
103
if err != nil {
133728e barerepo 1mo
104
s.oops(w, r, err)
133728e barerepo 1mo
105
return
133728e barerepo 1mo
106
}
133728e barerepo 1mo
107
s.writeFeed(w, r, owner+"/"+name, "/"+owner+"/"+name+".atom", events)
133728e barerepo 1mo
108
}
133728e barerepo 1mo
109
133728e barerepo 1mo
110
// serveUserFeed answers /<user>.atom, one account's public activity.
133728e barerepo 1mo
111
func (s *Server) serveUserFeed(w http.ResponseWriter, r *http.Request, account string) {
133728e barerepo 1mo
112
if _, err := s.DB.Account(r.Context(), account); err != nil {
133728e barerepo 1mo
113
http.NotFound(w, r)
133728e barerepo 1mo
114
return
133728e barerepo 1mo
115
}
133728e barerepo 1mo
116
events, err := s.DB.EventsBy(r.Context(), account, 50)
133728e barerepo 1mo
117
if err != nil {
133728e barerepo 1mo
118
s.oops(w, r, err)
133728e barerepo 1mo
119
return
133728e barerepo 1mo
120
}
133728e barerepo 1mo
121
// 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.
133728e barerepo 1mo
122
public := make([]store.Event, 0, len(events))
133728e barerepo 1mo
123
seen := map[string]bool{}
133728e barerepo 1mo
124
for _, e := range events {
133728e barerepo 1mo
125
visible, known := seen[e.Repo]
133728e barerepo 1mo
126
if !known {
133728e barerepo 1mo
127
owner, repoName, _ := cutRepo(e.Repo)
133728e barerepo 1mo
128
res, err := s.Transport.Open(r.Context(), owner, repoName, "", transport.Read)
133728e barerepo 1mo
129
visible = err == nil && res.Config.Public()
133728e barerepo 1mo
130
seen[e.Repo] = visible
133728e barerepo 1mo
131
}
133728e barerepo 1mo
132
if visible {
133728e barerepo 1mo
133
public = append(public, e)
133728e barerepo 1mo
134
}
133728e barerepo 1mo
135
}
133728e barerepo 1mo
136
s.writeFeed(w, r, account, "/"+account+".atom", public)
133728e barerepo 1mo
137
}
133728e barerepo 1mo
138
133728e barerepo 1mo
139
func cutRepo(repo string) (owner, name string, ok bool) {
133728e barerepo 1mo
140
for i := 0; i < len(repo); i++ {
133728e barerepo 1mo
141
if repo[i] == '/' {
133728e barerepo 1mo
142
return repo[:i], repo[i+1:], true
133728e barerepo 1mo
143
}
133728e barerepo 1mo
144
}
133728e barerepo 1mo
145
return repo, "", false
133728e barerepo 1mo
146
}
133728e barerepo 1mo
147
133728e barerepo 1mo
148
// serveThreadsFeed answers /<user>/<repo>/threads.atom, the discussion alone, per chapter 19.5.
133728e barerepo 1mo
149
func (s *Server) serveThreadsFeed(w http.ResponseWriter, r *http.Request, owner, name string) {
133728e barerepo 1mo
150
res, _, ok := s.openRepoFor(w, r, owner, name, "")
133728e barerepo 1mo
151
if !ok {
133728e barerepo 1mo
152
return
133728e barerepo 1mo
153
}
133728e barerepo 1mo
154
if !res.Config.Public() {
133728e barerepo 1mo
155
http.NotFound(w, r)
133728e barerepo 1mo
156
return
133728e barerepo 1mo
157
}
133728e barerepo 1mo
158
events, err := s.DB.ThreadEventsFor(r.Context(), owner+"/"+name, 50)
133728e barerepo 1mo
159
if err != nil {
133728e barerepo 1mo
160
s.oops(w, r, err)
133728e barerepo 1mo
161
return
133728e barerepo 1mo
162
}
133728e barerepo 1mo
163
s.writeFeed(w, r, owner+"/"+name+" threads", "/"+owner+"/"+name+"/threads.atom", events)
133728e barerepo 1mo
164
}
history · rawbarerepo 0.1.0