rip out go code
This commit is contained in:
parent
d8aea8424b
commit
0aad5be271
|
@ -1,41 +0,0 @@
|
||||||
name: "Code scanning - action"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ master ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ master ]
|
|
||||||
schedule:
|
|
||||||
- cron: '0 18 * * 6'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
CodeQL-Build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
# We must fetch at least the immediate parents so that if this is
|
|
||||||
# a pull request then we can checkout the head.
|
|
||||||
fetch-depth: 2
|
|
||||||
|
|
||||||
# If this run was triggered by a pull request event, then checkout
|
|
||||||
# the head of the pull request instead of the merge commit.
|
|
||||||
- run: git checkout HEAD^2
|
|
||||||
if: ${{ github.event_name == 'pull_request' }}
|
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@v1
|
|
||||||
# Override language selection by uncommenting this and choosing your languages
|
|
||||||
with:
|
|
||||||
languages: go
|
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
|
||||||
- name: Autobuild
|
|
||||||
uses: github/codeql-action/autobuild@v1
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@v1
|
|
|
@ -1,21 +0,0 @@
|
||||||
name: Go
|
|
||||||
on:
|
|
||||||
- push
|
|
||||||
- pull_request
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Set up Go 1.14
|
|
||||||
uses: actions/setup-go@v1
|
|
||||||
with:
|
|
||||||
go-version: 1.14
|
|
||||||
id: go
|
|
||||||
- name: Check out code into the Go module directory
|
|
||||||
uses: actions/checkout@v1
|
|
||||||
- name: Test
|
|
||||||
run: go test -v ./...
|
|
||||||
env:
|
|
||||||
GO111MODULE: on
|
|
||||||
GOPROXY: https://cache.greedo.xeserv.us
|
|
|
@ -1,25 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ClackSet []string
|
|
||||||
|
|
||||||
func (cs ClackSet) Name() string {
|
|
||||||
return "GNU " + cs[rand.Intn(len(cs))]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cs ClackSet) Middleware(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Add("X-Clacks-Overhead", cs.Name())
|
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
rand.Seed(time.Now().Unix())
|
|
||||||
}
|
|
245
cmd/site/html.go
245
cmd/site/html.go
|
@ -1,245 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"html/template"
|
|
||||||
"net/http"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"christine.website/cmd/site/internal"
|
|
||||||
"christine.website/cmd/site/internal/blog"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
|
||||||
"within.website/ln"
|
|
||||||
"within.website/ln/opname"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
templateRenderTime = promauto.NewHistogramVec(prometheus.HistogramOpts{
|
|
||||||
Name: "template_render_time",
|
|
||||||
Help: "Template render time in nanoseconds",
|
|
||||||
}, []string{"name"})
|
|
||||||
)
|
|
||||||
|
|
||||||
func logTemplateTime(ctx context.Context, name string, f ln.F, from time.Time) {
|
|
||||||
dur := time.Since(from)
|
|
||||||
templateRenderTime.With(prometheus.Labels{"name": name}).Observe(float64(dur))
|
|
||||||
ln.Log(ctx, f, ln.F{"dur": dur, "name": name})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Site) renderTemplatePage(templateFname string, data interface{}) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := opname.With(r.Context(), "renderTemplatePage")
|
|
||||||
fetag := "W/" + internal.Hash(templateFname, etag) + "-1"
|
|
||||||
|
|
||||||
f := ln.F{"etag": fetag, "if_none_match": r.Header.Get("If-None-Match")}
|
|
||||||
|
|
||||||
if r.Header.Get("If-None-Match") == fetag {
|
|
||||||
http.Error(w, "Cached data OK", http.StatusNotModified)
|
|
||||||
ln.Log(ctx, f, ln.Info("Cache hit"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer logTemplateTime(ctx, templateFname, f, time.Now())
|
|
||||||
|
|
||||||
var t *template.Template
|
|
||||||
var err error
|
|
||||||
|
|
||||||
t, err = template.ParseFiles("templates/base.html", "templates/"+templateFname)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
ln.Error(ctx, err, ln.F{"action": "renderTemplatePage", "page": templateFname})
|
|
||||||
fmt.Fprintf(w, "error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("ETag", fetag)
|
|
||||||
w.Header().Set("Cache-Control", "max-age=432000")
|
|
||||||
|
|
||||||
err = t.Execute(w, data)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var postView = promauto.NewCounterVec(prometheus.CounterOpts{
|
|
||||||
Name: "posts_viewed",
|
|
||||||
Help: "The number of views per post or talk",
|
|
||||||
}, []string{"base"})
|
|
||||||
|
|
||||||
func (s *Site) listSeries(w http.ResponseWriter, r *http.Request) {
|
|
||||||
s.renderTemplatePage("series.html", s.Series).ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Site) showSeries(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.RequestURI == "/blog/series/" {
|
|
||||||
http.Redirect(w, r, "/blog/series", http.StatusSeeOther)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
series := filepath.Base(r.URL.Path)
|
|
||||||
var posts []blog.Post
|
|
||||||
|
|
||||||
for _, p := range s.Posts {
|
|
||||||
if p.Series == series {
|
|
||||||
posts = append(posts, p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
s.renderTemplatePage("serieslist.html", struct {
|
|
||||||
Name string
|
|
||||||
Posts []blog.Post
|
|
||||||
}{
|
|
||||||
Name: series,
|
|
||||||
Posts: posts,
|
|
||||||
}).ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Site) showGallery(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.RequestURI == "/gallery/" {
|
|
||||||
http.Redirect(w, r, "/gallery", http.StatusSeeOther)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cmp := r.URL.Path[1:]
|
|
||||||
var p blog.Post
|
|
||||||
var found bool
|
|
||||||
for _, pst := range s.Gallery {
|
|
||||||
if pst.Link == cmp {
|
|
||||||
p = pst
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
|
||||||
s.renderTemplatePage("error.html", "no such post found: "+r.RequestURI).ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var tags string
|
|
||||||
if len(p.Tags) != 0 {
|
|
||||||
for _, t := range p.Tags {
|
|
||||||
tags = tags + " #" + strings.ReplaceAll(t, "-", "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h := s.renderTemplatePage("gallerypost.html", struct {
|
|
||||||
Title string
|
|
||||||
Link string
|
|
||||||
BodyHTML template.HTML
|
|
||||||
Date string
|
|
||||||
Tags string
|
|
||||||
Image string
|
|
||||||
}{
|
|
||||||
Title: p.Title,
|
|
||||||
Link: p.Link,
|
|
||||||
BodyHTML: p.BodyHTML,
|
|
||||||
Date: internal.IOS13Detri(p.Date),
|
|
||||||
Tags: tags,
|
|
||||||
Image: p.ImageURL,
|
|
||||||
})
|
|
||||||
|
|
||||||
if h == nil {
|
|
||||||
panic("how did we get here?")
|
|
||||||
}
|
|
||||||
|
|
||||||
h.ServeHTTP(w, r)
|
|
||||||
postView.With(prometheus.Labels{"base": filepath.Base(p.Link)}).Inc()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Site) showTalk(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.RequestURI == "/talks/" {
|
|
||||||
http.Redirect(w, r, "/talks", http.StatusSeeOther)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cmp := r.URL.Path[1:]
|
|
||||||
var p blog.Post
|
|
||||||
var found bool
|
|
||||||
for _, pst := range s.Talks {
|
|
||||||
if pst.Link == cmp {
|
|
||||||
p = pst
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
|
||||||
s.renderTemplatePage("error.html", "no such post found: "+r.RequestURI).ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h := s.renderTemplatePage("talkpost.html", struct {
|
|
||||||
Title string
|
|
||||||
Link string
|
|
||||||
BodyHTML template.HTML
|
|
||||||
Date string
|
|
||||||
SlidesLink string
|
|
||||||
}{
|
|
||||||
Title: p.Title,
|
|
||||||
Link: p.Link,
|
|
||||||
BodyHTML: p.BodyHTML,
|
|
||||||
Date: internal.IOS13Detri(p.Date),
|
|
||||||
SlidesLink: p.SlidesLink,
|
|
||||||
})
|
|
||||||
|
|
||||||
if h == nil {
|
|
||||||
panic("how did we get here?")
|
|
||||||
}
|
|
||||||
|
|
||||||
h.ServeHTTP(w, r)
|
|
||||||
postView.With(prometheus.Labels{"base": filepath.Base(p.Link)}).Inc()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Site) showPost(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.RequestURI == "/blog/" {
|
|
||||||
http.Redirect(w, r, "/blog", http.StatusSeeOther)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cmp := r.URL.Path[1:]
|
|
||||||
var p blog.Post
|
|
||||||
var found bool
|
|
||||||
for _, pst := range s.Posts {
|
|
||||||
if pst.Link == cmp {
|
|
||||||
p = pst
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
|
||||||
s.renderTemplatePage("error.html", "no such post found: "+r.RequestURI).ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var tags string
|
|
||||||
|
|
||||||
if len(p.Tags) != 0 {
|
|
||||||
for _, t := range p.Tags {
|
|
||||||
tags = tags + " #" + strings.ReplaceAll(t, "-", "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
s.renderTemplatePage("blogpost.html", struct {
|
|
||||||
Title string
|
|
||||||
Link string
|
|
||||||
BodyHTML template.HTML
|
|
||||||
Date string
|
|
||||||
Series, SeriesTag string
|
|
||||||
Tags string
|
|
||||||
}{
|
|
||||||
Title: p.Title,
|
|
||||||
Link: p.Link,
|
|
||||||
BodyHTML: p.BodyHTML,
|
|
||||||
Date: internal.IOS13Detri(p.Date),
|
|
||||||
Series: p.Series,
|
|
||||||
SeriesTag: strings.ReplaceAll(p.Series, "-", ""),
|
|
||||||
Tags: tags,
|
|
||||||
}).ServeHTTP(w, r)
|
|
||||||
postView.With(prometheus.Labels{"base": filepath.Base(p.Link)}).Inc()
|
|
||||||
}
|
|
|
@ -1,137 +0,0 @@
|
||||||
package blog
|
|
||||||
|
|
||||||
import (
|
|
||||||
"html/template"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"christine.website/cmd/site/internal/front"
|
|
||||||
"github.com/russross/blackfriday"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Post is a single blogpost.
|
|
||||||
type Post struct {
|
|
||||||
Title string `json:"title"`
|
|
||||||
Link string `json:"link"`
|
|
||||||
Summary string `json:"summary,omitifempty"`
|
|
||||||
Body string `json:"-"`
|
|
||||||
BodyHTML template.HTML `json:"body"`
|
|
||||||
Series string `json:"series"`
|
|
||||||
Tags []string `json:"tags"`
|
|
||||||
SlidesLink string `json:"slides_link"`
|
|
||||||
ImageURL string `json:"image_url"`
|
|
||||||
ThumbURL string `json:"thumb_url"`
|
|
||||||
Date time.Time
|
|
||||||
DateString string `json:"date"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Posts implements sort.Interface for a slice of Post objects.
|
|
||||||
type Posts []Post
|
|
||||||
|
|
||||||
func (p Posts) Series() []string {
|
|
||||||
names := map[string]struct{}{}
|
|
||||||
|
|
||||||
for _, ps := range p {
|
|
||||||
if ps.Series != "" {
|
|
||||||
names[ps.Series] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var result []string
|
|
||||||
|
|
||||||
for name := range names {
|
|
||||||
result = append(result, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p Posts) Len() int { return len(p) }
|
|
||||||
func (p Posts) Less(i, j int) bool {
|
|
||||||
iDate := p[i].Date
|
|
||||||
jDate := p[j].Date
|
|
||||||
|
|
||||||
return iDate.Unix() < jDate.Unix()
|
|
||||||
}
|
|
||||||
func (p Posts) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
|
||||||
|
|
||||||
// LoadPosts loads posts for a given directory.
|
|
||||||
func LoadPosts(path string, prepend string) (Posts, error) {
|
|
||||||
type postFM struct {
|
|
||||||
Title string
|
|
||||||
Date string
|
|
||||||
Series string
|
|
||||||
Tags []string
|
|
||||||
SlidesLink string `yaml:"slides_link"`
|
|
||||||
Image string
|
|
||||||
Thumb string
|
|
||||||
Show string
|
|
||||||
}
|
|
||||||
var result Posts
|
|
||||||
|
|
||||||
err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.IsDir() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
fin, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer fin.Close()
|
|
||||||
|
|
||||||
content, err := ioutil.ReadAll(fin)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var fm postFM
|
|
||||||
remaining, err := front.Unmarshal(content, &fm)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
output := blackfriday.Run(remaining)
|
|
||||||
|
|
||||||
const timeFormat = `2006-01-02`
|
|
||||||
date, err := time.Parse(timeFormat, fm.Date)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fname := filepath.Base(path)
|
|
||||||
fname = strings.TrimSuffix(fname, filepath.Ext(fname))
|
|
||||||
|
|
||||||
p := Post{
|
|
||||||
Title: fm.Title,
|
|
||||||
Date: date,
|
|
||||||
DateString: fm.Date,
|
|
||||||
Link: filepath.Join(prepend, fname),
|
|
||||||
Body: string(remaining),
|
|
||||||
BodyHTML: template.HTML(output),
|
|
||||||
SlidesLink: fm.SlidesLink,
|
|
||||||
Series: fm.Series,
|
|
||||||
Tags: fm.Tags,
|
|
||||||
ImageURL: fm.Image,
|
|
||||||
ThumbURL: fm.Thumb,
|
|
||||||
}
|
|
||||||
result = append(result, p)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Sort(sort.Reverse(result))
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
|
@ -1,66 +0,0 @@
|
||||||
package blog
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLoadPosts(t *testing.T) {
|
|
||||||
posts, err := LoadPosts("../../../../blog", "blog")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, post := range posts {
|
|
||||||
t.Run(post.Link, post.test)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoadTalks(t *testing.T) {
|
|
||||||
talks, err := LoadPosts("../../../../talks", "talks")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, talk := range talks {
|
|
||||||
t.Run(talk.Link, talk.test)
|
|
||||||
if talk.SlidesLink == "" {
|
|
||||||
t.Errorf("talk %s (%s) doesn't have a slides link", talk.Title, talk.DateString)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoadGallery(t *testing.T) {
|
|
||||||
gallery, err := LoadPosts("../../../../gallery", "gallery")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, art := range gallery {
|
|
||||||
t.Run(art.Link, art.test)
|
|
||||||
if art.ImageURL == "" {
|
|
||||||
t.Errorf("art %s (%s) doesn't have an image link", art.Title, art.DateString)
|
|
||||||
}
|
|
||||||
if art.ThumbURL == "" {
|
|
||||||
t.Errorf("art %s (%s) doesn't have a thumbnail link", art.Title, art.DateString)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p Post) test(t *testing.T) {
|
|
||||||
if p.Title == "" {
|
|
||||||
t.Error("no post title")
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.DateString == "" {
|
|
||||||
t.Error("no date")
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.Link == "" {
|
|
||||||
t.Error("no link")
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.Body == "" {
|
|
||||||
t.Error("no body")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
package internal
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
const iOS13DetriFormat = `2006 M1 2`
|
|
||||||
|
|
||||||
// IOS13Detri formats a datestamp like iOS 13 does with the Lojban locale.
|
|
||||||
func IOS13Detri(t time.Time) string {
|
|
||||||
return t.Format(iOS13DetriFormat)
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestIOS13Detri(t *testing.T) {
|
|
||||||
cases := []struct {
|
|
||||||
in time.Time
|
|
||||||
out string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
in: time.Date(2019, time.March, 30, 0, 0, 0, 0, time.FixedZone("UTC", 0)),
|
|
||||||
out: "2019 M3 30",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, cs := range cases {
|
|
||||||
t.Run(fmt.Sprintf("%s -> %s", cs.in.Format(time.RFC3339), cs.out), func(t *testing.T) {
|
|
||||||
result := IOS13Detri(cs.in)
|
|
||||||
if result != cs.out {
|
|
||||||
t.Fatalf("wanted: %s, got: %s", cs.out, result)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
Copyright (c) 2017 TJ Holowaychuk <tj@vision-media.ca>
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
|
@ -1,24 +0,0 @@
|
||||||
// Package front provides YAML frontmatter unmarshalling.
|
|
||||||
package front
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
|
|
||||||
"gopkg.in/yaml.v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Delimiter.
|
|
||||||
var delim = []byte("---")
|
|
||||||
|
|
||||||
// Unmarshal parses YAML frontmatter and returns the content. When no
|
|
||||||
// frontmatter delimiters are present the original content is returned.
|
|
||||||
func Unmarshal(b []byte, v interface{}) (content []byte, err error) {
|
|
||||||
if !bytes.HasPrefix(b, delim) {
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := bytes.SplitN(b, delim, 3)
|
|
||||||
content = parts[2]
|
|
||||||
err = yaml.Unmarshal(parts[1], v)
|
|
||||||
return
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
package front_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"christine.website/cmd/site/internal/front"
|
|
||||||
)
|
|
||||||
|
|
||||||
var markdown = []byte(`---
|
|
||||||
title: Ferrets
|
|
||||||
authors:
|
|
||||||
- Tobi
|
|
||||||
- Loki
|
|
||||||
- Jane
|
|
||||||
---
|
|
||||||
Some content here, so
|
|
||||||
interesting, you just
|
|
||||||
want to keep reading.`)
|
|
||||||
|
|
||||||
type article struct {
|
|
||||||
Title string
|
|
||||||
Authors []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func Example() {
|
|
||||||
var a article
|
|
||||||
|
|
||||||
content, err := front.Unmarshal(markdown, &a)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("error unmarshalling: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("%#v\n", a)
|
|
||||||
fmt.Printf("%s\n", string(content))
|
|
||||||
// Output:
|
|
||||||
// front_test.article{Title:"Ferrets", Authors:[]string{"Tobi", "Loki", "Jane"}}
|
|
||||||
//
|
|
||||||
// Some content here, so
|
|
||||||
// interesting, you just
|
|
||||||
// want to keep reading.
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/md5"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Hash is a simple wrapper around the MD5 algorithm implementation in the
|
|
||||||
// Go standard library. It takes in data and a salt and returns the hashed
|
|
||||||
// representation.
|
|
||||||
func Hash(data string, salt string) string {
|
|
||||||
output := md5.Sum([]byte(data + salt))
|
|
||||||
return fmt.Sprintf("%x", output)
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
requestCounter = prometheus.NewCounterVec(
|
|
||||||
prometheus.CounterOpts{
|
|
||||||
Name: "handler_requests_total",
|
|
||||||
Help: "Total number of request/responses by HTTP status code.",
|
|
||||||
}, []string{"handler", "code"})
|
|
||||||
|
|
||||||
requestDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
|
||||||
Name: "handler_request_duration",
|
|
||||||
Help: "Handler request duration.",
|
|
||||||
}, []string{"handler", "method"})
|
|
||||||
|
|
||||||
requestInFlight = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
|
||||||
Name: "handler_requests_in_flight",
|
|
||||||
Help: "Current number of requests being served.",
|
|
||||||
}, []string{"handler"})
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
_ = prometheus.Register(requestCounter)
|
|
||||||
_ = prometheus.Register(requestDuration)
|
|
||||||
_ = prometheus.Register(requestInFlight)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metrics captures request duration, request count and in-flight request count
|
|
||||||
// metrics for HTTP handlers. The family field is used to discriminate handlers.
|
|
||||||
func Metrics(family string, next http.Handler) http.Handler {
|
|
||||||
return promhttp.InstrumentHandlerDuration(
|
|
||||||
requestDuration.MustCurryWith(prometheus.Labels{"handler": family}),
|
|
||||||
promhttp.InstrumentHandlerCounter(requestCounter.MustCurryWith(prometheus.Labels{"handler": family}),
|
|
||||||
promhttp.InstrumentHandlerInFlight(requestInFlight.With(prometheus.Labels{"handler": family}), next),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/celrenheit/sandflake"
|
|
||||||
"within.website/ln"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RequestID appends a unique (sandflake) request ID to each request's
|
|
||||||
// X-Request-Id header field, much like Heroku's router does.
|
|
||||||
func RequestID(next http.Handler) http.Handler {
|
|
||||||
var g sandflake.Generator
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
id := g.Next().String()
|
|
||||||
|
|
||||||
if rid := r.Header.Get("X-Request-Id"); rid != "" {
|
|
||||||
id = rid + "," + id
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := ln.WithF(r.Context(), ln.F{
|
|
||||||
"request_id": id,
|
|
||||||
})
|
|
||||||
r = r.WithContext(ctx)
|
|
||||||
|
|
||||||
w.Header().Set("X-Request-Id", id)
|
|
||||||
r.Header.Set("X-Request-Id", id)
|
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
296
cmd/site/main.go
296
cmd/site/main.go
|
@ -1,296 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"html/template"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"christine.website/cmd/site/internal/blog"
|
|
||||||
"christine.website/cmd/site/internal/middleware"
|
|
||||||
"christine.website/jsonfeed"
|
|
||||||
"github.com/gorilla/feeds"
|
|
||||||
_ "github.com/joho/godotenv/autoload"
|
|
||||||
"github.com/povilasv/prommod"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
||||||
blackfriday "github.com/russross/blackfriday"
|
|
||||||
"github.com/sebest/xff"
|
|
||||||
"github.com/snabb/sitemap"
|
|
||||||
"within.website/ln"
|
|
||||||
"within.website/ln/ex"
|
|
||||||
"within.website/ln/opname"
|
|
||||||
)
|
|
||||||
|
|
||||||
var port = os.Getenv("PORT")
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
if port == "" {
|
|
||||||
port = "29384"
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := ln.WithF(opname.With(context.Background(), "main"), ln.F{
|
|
||||||
"port": port,
|
|
||||||
"git_rev": gitRev,
|
|
||||||
})
|
|
||||||
|
|
||||||
_ = prometheus.Register(prommod.NewCollector("christine"))
|
|
||||||
|
|
||||||
s, err := Build()
|
|
||||||
if err != nil {
|
|
||||||
ln.FatalErr(ctx, err, ln.Action("Build"))
|
|
||||||
}
|
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
mux.HandleFunc("/.within/health", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
http.Error(w, "OK", http.StatusOK)
|
|
||||||
})
|
|
||||||
mux.Handle("/", s)
|
|
||||||
|
|
||||||
ln.Log(ctx, ln.Action("http_listening"))
|
|
||||||
ln.FatalErr(ctx, http.ListenAndServe(":"+port, mux))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Site is the parent object for https://christine.website's backend.
|
|
||||||
type Site struct {
|
|
||||||
Posts blog.Posts
|
|
||||||
Talks blog.Posts
|
|
||||||
Gallery blog.Posts
|
|
||||||
Resume template.HTML
|
|
||||||
Series []string
|
|
||||||
SignalBoost []Person
|
|
||||||
|
|
||||||
clacks ClackSet
|
|
||||||
patrons []string
|
|
||||||
rssFeed *feeds.Feed
|
|
||||||
jsonFeed *jsonfeed.Feed
|
|
||||||
|
|
||||||
mux *http.ServeMux
|
|
||||||
xffmw *xff.XFF
|
|
||||||
}
|
|
||||||
|
|
||||||
var gitRev = os.Getenv("GIT_REV")
|
|
||||||
|
|
||||||
func envOr(key, or string) string {
|
|
||||||
if result, ok := os.LookupEnv(key); ok {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
return or
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Site) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := opname.With(r.Context(), "site.ServeHTTP")
|
|
||||||
ctx = ln.WithF(ctx, ln.F{
|
|
||||||
"user_agent": r.Header.Get("User-Agent"),
|
|
||||||
})
|
|
||||||
r = r.WithContext(ctx)
|
|
||||||
if gitRev != "" {
|
|
||||||
w.Header().Add("X-Git-Rev", gitRev)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Add("X-Hacker", "If you are reading this, check out /signalboost to find people for your team")
|
|
||||||
|
|
||||||
s.clacks.Middleware(
|
|
||||||
middleware.RequestID(
|
|
||||||
s.xffmw.Handler(
|
|
||||||
ex.HTTPLog(s.mux),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
var arbDate = time.Date(2020, time.May, 21, 0, 0, 0, 0, time.UTC)
|
|
||||||
|
|
||||||
// Build creates a new Site instance or fails.
|
|
||||||
func Build() (*Site, error) {
|
|
||||||
pc, err := NewPatreonClient()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
pledges, err := GetPledges(pc)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
people, err := loadPeople("./signalboost.dhall")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
smi := sitemap.New()
|
|
||||||
smi.Add(&sitemap.URL{
|
|
||||||
Loc: "https://christine.website/resume",
|
|
||||||
LastMod: &arbDate,
|
|
||||||
ChangeFreq: sitemap.Monthly,
|
|
||||||
})
|
|
||||||
|
|
||||||
smi.Add(&sitemap.URL{
|
|
||||||
Loc: "https://christine.website/contact",
|
|
||||||
LastMod: &arbDate,
|
|
||||||
ChangeFreq: sitemap.Monthly,
|
|
||||||
})
|
|
||||||
|
|
||||||
smi.Add(&sitemap.URL{
|
|
||||||
Loc: "https://christine.website/",
|
|
||||||
LastMod: &arbDate,
|
|
||||||
ChangeFreq: sitemap.Monthly,
|
|
||||||
})
|
|
||||||
|
|
||||||
smi.Add(&sitemap.URL{
|
|
||||||
Loc: "https://christine.website/patrons",
|
|
||||||
LastMod: &arbDate,
|
|
||||||
ChangeFreq: sitemap.Weekly,
|
|
||||||
})
|
|
||||||
|
|
||||||
smi.Add(&sitemap.URL{
|
|
||||||
Loc: "https://christine.website/blog",
|
|
||||||
LastMod: &arbDate,
|
|
||||||
ChangeFreq: sitemap.Weekly,
|
|
||||||
})
|
|
||||||
|
|
||||||
xffmw, err := xff.Default()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
s := &Site{
|
|
||||||
rssFeed: &feeds.Feed{
|
|
||||||
Title: "Christine Dodrill's Blog",
|
|
||||||
Link: &feeds.Link{Href: "https://christine.website/blog"},
|
|
||||||
Description: "My blog posts and rants about various technology things.",
|
|
||||||
Author: &feeds.Author{Name: "Christine Dodrill", Email: "me@christine.website"},
|
|
||||||
Created: bootTime,
|
|
||||||
Copyright: "This work is copyright Christine Dodrill. My viewpoints are my own and not the view of any employer past, current or future.",
|
|
||||||
},
|
|
||||||
jsonFeed: &jsonfeed.Feed{
|
|
||||||
Version: jsonfeed.CurrentVersion,
|
|
||||||
Title: "Christine Dodrill's Blog",
|
|
||||||
HomePageURL: "https://christine.website",
|
|
||||||
FeedURL: "https://christine.website/blog.json",
|
|
||||||
Description: "My blog posts and rants about various technology things.",
|
|
||||||
UserComment: "This is a JSON feed of my blogposts. For more information read: https://jsonfeed.org/version/1",
|
|
||||||
Icon: icon,
|
|
||||||
Favicon: icon,
|
|
||||||
Author: jsonfeed.Author{
|
|
||||||
Name: "Christine Dodrill",
|
|
||||||
Avatar: icon,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mux: http.NewServeMux(),
|
|
||||||
xffmw: xffmw,
|
|
||||||
|
|
||||||
clacks: ClackSet(strings.Split(envOr("CLACK_SET", "Ashlynn"), ",")),
|
|
||||||
patrons: pledges,
|
|
||||||
SignalBoost: people,
|
|
||||||
}
|
|
||||||
|
|
||||||
posts, err := blog.LoadPosts("./blog/", "blog")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
s.Posts = posts
|
|
||||||
s.Series = posts.Series()
|
|
||||||
sort.Strings(s.Series)
|
|
||||||
|
|
||||||
talks, err := blog.LoadPosts("./talks", "talks")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
s.Talks = talks
|
|
||||||
|
|
||||||
gallery, err := blog.LoadPosts("./gallery", "gallery")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
s.Gallery = gallery
|
|
||||||
|
|
||||||
var everything blog.Posts
|
|
||||||
everything = append(everything, posts...)
|
|
||||||
everything = append(everything, talks...)
|
|
||||||
everything = append(everything, gallery...)
|
|
||||||
|
|
||||||
sort.Sort(sort.Reverse(everything))
|
|
||||||
|
|
||||||
resumeData, err := ioutil.ReadFile("./static/resume/resume.md")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Resume = template.HTML(blackfriday.Run(resumeData))
|
|
||||||
|
|
||||||
for _, item := range everything {
|
|
||||||
s.rssFeed.Items = append(s.rssFeed.Items, &feeds.Item{
|
|
||||||
Title: item.Title,
|
|
||||||
Link: &feeds.Link{Href: "https://christine.website/" + item.Link},
|
|
||||||
Description: item.Summary,
|
|
||||||
Created: item.Date,
|
|
||||||
Content: string(item.BodyHTML),
|
|
||||||
})
|
|
||||||
|
|
||||||
s.jsonFeed.Items = append(s.jsonFeed.Items, jsonfeed.Item{
|
|
||||||
ID: "https://christine.website/" + item.Link,
|
|
||||||
URL: "https://christine.website/" + item.Link,
|
|
||||||
Title: item.Title,
|
|
||||||
DatePublished: item.Date,
|
|
||||||
ContentHTML: string(item.BodyHTML),
|
|
||||||
Tags: item.Tags,
|
|
||||||
})
|
|
||||||
|
|
||||||
smi.Add(&sitemap.URL{
|
|
||||||
Loc: "https://christine.website/" + item.Link,
|
|
||||||
LastMod: &item.Date,
|
|
||||||
ChangeFreq: sitemap.Monthly,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add HTTP routes here
|
|
||||||
s.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.URL.Path != "/" {
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
|
||||||
s.renderTemplatePage("error.html", "can't find "+r.URL.Path).ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s.renderTemplatePage("index.html", nil).ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
s.mux.Handle("/metrics", promhttp.Handler())
|
|
||||||
s.mux.Handle("/feeds", middleware.Metrics("feeds", s.renderTemplatePage("feeds.html", nil)))
|
|
||||||
s.mux.Handle("/patrons", middleware.Metrics("patrons", s.renderTemplatePage("patrons.html", s.patrons)))
|
|
||||||
s.mux.Handle("/signalboost", middleware.Metrics("signalboost", s.renderTemplatePage("signalboost.html", s.SignalBoost)))
|
|
||||||
s.mux.Handle("/resume", middleware.Metrics("resume", s.renderTemplatePage("resume.html", s.Resume)))
|
|
||||||
s.mux.Handle("/blog", middleware.Metrics("blog", s.renderTemplatePage("blogindex.html", s.Posts)))
|
|
||||||
s.mux.Handle("/talks", middleware.Metrics("talks", s.renderTemplatePage("talkindex.html", s.Talks)))
|
|
||||||
s.mux.Handle("/gallery", middleware.Metrics("gallery", s.renderTemplatePage("galleryindex.html", s.Gallery)))
|
|
||||||
s.mux.Handle("/contact", middleware.Metrics("contact", s.renderTemplatePage("contact.html", nil)))
|
|
||||||
s.mux.Handle("/blog.rss", middleware.Metrics("blog.rss", http.HandlerFunc(s.createFeed)))
|
|
||||||
s.mux.Handle("/blog.atom", middleware.Metrics("blog.atom", http.HandlerFunc(s.createAtom)))
|
|
||||||
s.mux.Handle("/blog.json", middleware.Metrics("blog.json", http.HandlerFunc(s.createJSONFeed)))
|
|
||||||
s.mux.Handle("/blog/", middleware.Metrics("blogpost", http.HandlerFunc(s.showPost)))
|
|
||||||
s.mux.Handle("/blog/series", http.HandlerFunc(s.listSeries))
|
|
||||||
s.mux.Handle("/blog/series/", http.HandlerFunc(s.showSeries))
|
|
||||||
s.mux.Handle("/talks/", middleware.Metrics("talks", http.HandlerFunc(s.showTalk)))
|
|
||||||
s.mux.Handle("/gallery/", middleware.Metrics("gallery", http.HandlerFunc(s.showGallery)))
|
|
||||||
s.mux.Handle("/css/", http.FileServer(http.Dir(".")))
|
|
||||||
s.mux.Handle("/static/", http.FileServer(http.Dir(".")))
|
|
||||||
s.mux.HandleFunc("/sw.js", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
http.ServeFile(w, r, "./static/js/sw.js")
|
|
||||||
})
|
|
||||||
s.mux.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
http.ServeFile(w, r, "./static/robots.txt")
|
|
||||||
})
|
|
||||||
s.mux.Handle("/sitemap.xml", middleware.Metrics("sitemap", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/xml")
|
|
||||||
_, _ = smi.WriteTo(w)
|
|
||||||
})))
|
|
||||||
s.mux.HandleFunc("/api/pageview-timer", handlePageViewTimer)
|
|
||||||
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const icon = "https://christine.website/static/img/avatar.png"
|
|
|
@ -1,53 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
"within.website/ln"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
readTimes = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
|
||||||
Name: "blogpage_read_times",
|
|
||||||
Help: "This tracks how much time people spend reading articles on my blog",
|
|
||||||
}, []string{"path"})
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
_ = prometheus.Register(readTimes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlePageViewTimer(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Header.Get("DNT") == "1" {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := ioutil.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
ln.Error(r.Context(), err, ln.Info("while reading data"))
|
|
||||||
http.Error(w, "oopsie whoopsie uwu", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
r.Body.Close()
|
|
||||||
|
|
||||||
type metricsData struct {
|
|
||||||
Path string `json:"path"`
|
|
||||||
StartTime time.Time `json:"start_time"`
|
|
||||||
EndTime time.Time `json:"end_time"`
|
|
||||||
}
|
|
||||||
var md metricsData
|
|
||||||
err = json.Unmarshal(data, &md)
|
|
||||||
if err != nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
diff := md.EndTime.Sub(md.StartTime).Seconds()
|
|
||||||
|
|
||||||
readTimes.WithLabelValues(md.Path).Observe(float64(diff))
|
|
||||||
}
|
|
|
@ -1,112 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"sort"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mxpv/patreon-go"
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
"within.website/ln"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewPatreonClient() (*patreon.Client, error) {
|
|
||||||
for _, name := range []string{"CLIENT_ID", "CLIENT_SECRET", "ACCESS_TOKEN", "REFRESH_TOKEN"} {
|
|
||||||
if os.Getenv("PATREON_"+name) == "" {
|
|
||||||
return nil, fmt.Errorf("wanted envvar PATREON_%s", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config := oauth2.Config{
|
|
||||||
ClientID: os.Getenv("PATREON_CLIENT_ID"),
|
|
||||||
ClientSecret: os.Getenv("PATREON_CLIENT_SECRET"),
|
|
||||||
Endpoint: oauth2.Endpoint{
|
|
||||||
AuthURL: patreon.AuthorizationURL,
|
|
||||||
TokenURL: patreon.AccessTokenURL,
|
|
||||||
},
|
|
||||||
Scopes: []string{"users", "campaigns", "pledges", "pledges-to-me", "my-campaign"},
|
|
||||||
}
|
|
||||||
|
|
||||||
token := oauth2.Token{
|
|
||||||
AccessToken: os.Getenv("PATREON_ACCESS_TOKEN"),
|
|
||||||
RefreshToken: os.Getenv("PATREON_REFRESH_TOKEN"),
|
|
||||||
// Must be non-nil, otherwise token will not be expired
|
|
||||||
Expiry: time.Now().Add(90 * 24 * time.Hour),
|
|
||||||
}
|
|
||||||
|
|
||||||
tc := config.Client(context.Background(), &token)
|
|
||||||
|
|
||||||
trans := tc.Transport
|
|
||||||
tc.Transport = lnLoggingTransport{next: trans}
|
|
||||||
client := patreon.NewClient(tc)
|
|
||||||
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetPledges(pc *patreon.Client) ([]string, error) {
|
|
||||||
campaign, err := pc.FetchCampaign()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("campaign fetch error: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
campaignID := campaign.Data[0].ID
|
|
||||||
|
|
||||||
cursor := ""
|
|
||||||
var result []string
|
|
||||||
|
|
||||||
for {
|
|
||||||
pledgesResponse, err := pc.FetchPledges(campaignID, patreon.WithPageSize(25), patreon.WithCursor(cursor))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
users := make(map[string]*patreon.User)
|
|
||||||
for _, item := range pledgesResponse.Included.Items {
|
|
||||||
u, ok := item.(*patreon.User)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
users[u.ID] = u
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, pledge := range pledgesResponse.Data {
|
|
||||||
pid := pledge.Relationships.Patron.Data.ID
|
|
||||||
patronFullName := users[pid].Attributes.FullName
|
|
||||||
|
|
||||||
result = append(result, patronFullName)
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor = pledgesResponse.Links.Next
|
|
||||||
if cursor == "" {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Strings(result)
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type lnLoggingTransport struct{ next http.RoundTripper }
|
|
||||||
|
|
||||||
func (l lnLoggingTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
|
||||||
ctx := r.Context()
|
|
||||||
f := ln.F{
|
|
||||||
"url": r.URL.String(),
|
|
||||||
"has_token": r.Header.Get("Authorization") != "",
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := l.next.RoundTrip(r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
f["status"] = resp.Status
|
|
||||||
|
|
||||||
ln.Log(ctx, f)
|
|
||||||
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
|
@ -1,91 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"christine.website/cmd/site/internal"
|
|
||||||
"within.website/ln"
|
|
||||||
"within.website/ln/opname"
|
|
||||||
)
|
|
||||||
|
|
||||||
var bootTime = time.Now()
|
|
||||||
var etag = internal.Hash(bootTime.String(), IncrediblySecureSalt)
|
|
||||||
|
|
||||||
// IncrediblySecureSalt *******
|
|
||||||
const IncrediblySecureSalt = "hunter2"
|
|
||||||
|
|
||||||
func (s *Site) createFeed(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := opname.With(r.Context(), "rss-feed")
|
|
||||||
fetag := "W/" + internal.Hash(bootTime.String(), IncrediblySecureSalt)
|
|
||||||
w.Header().Set("ETag", fetag)
|
|
||||||
|
|
||||||
if r.Header.Get("If-None-Match") == fetag {
|
|
||||||
http.Error(w, "Cached data OK", http.StatusNotModified)
|
|
||||||
ln.Log(ctx, ln.Info("cache hit"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/rss+xml")
|
|
||||||
err := s.rssFeed.WriteRss(w)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
||||||
ln.Error(r.Context(), err, ln.F{
|
|
||||||
"remote_addr": r.RemoteAddr,
|
|
||||||
"action": "generating_rss",
|
|
||||||
"uri": r.RequestURI,
|
|
||||||
"host": r.Host,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Site) createAtom(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := opname.With(r.Context(), "atom-feed")
|
|
||||||
fetag := "W/" + internal.Hash(bootTime.String(), IncrediblySecureSalt)
|
|
||||||
w.Header().Set("ETag", fetag)
|
|
||||||
|
|
||||||
if r.Header.Get("If-None-Match") == fetag {
|
|
||||||
http.Error(w, "Cached data OK", http.StatusNotModified)
|
|
||||||
ln.Log(ctx, ln.Info("cache hit"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/atom+xml")
|
|
||||||
err := s.rssFeed.WriteAtom(w)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
||||||
ln.Error(ctx, err, ln.F{
|
|
||||||
"remote_addr": r.RemoteAddr,
|
|
||||||
"action": "generating_atom",
|
|
||||||
"uri": r.RequestURI,
|
|
||||||
"host": r.Host,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Site) createJSONFeed(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := opname.With(r.Context(), "atom-feed")
|
|
||||||
fetag := "W/" + internal.Hash(bootTime.String(), IncrediblySecureSalt)
|
|
||||||
w.Header().Set("ETag", fetag)
|
|
||||||
|
|
||||||
if r.Header.Get("If-None-Match") == fetag {
|
|
||||||
http.Error(w, "Cached data OK", http.StatusNotModified)
|
|
||||||
ln.Log(ctx, ln.Info("cache hit"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
e := json.NewEncoder(w)
|
|
||||||
e.SetIndent("", "\t")
|
|
||||||
err := e.Encode(s.jsonFeed)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
||||||
ln.Error(ctx, err, ln.F{
|
|
||||||
"remote_addr": r.RemoteAddr,
|
|
||||||
"action": "generating_jsonfeed",
|
|
||||||
"uri": r.RequestURI,
|
|
||||||
"host": r.Host,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/ioutil"
|
|
||||||
|
|
||||||
"github.com/philandstuff/dhall-golang"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Person struct {
|
|
||||||
Name string `dhall:"name"`
|
|
||||||
GitLink string `dhall:"gitLink"`
|
|
||||||
Twitter string `dhall:"twitter"`
|
|
||||||
Tags []string `dhall:"tags"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadPeople(path string) ([]Person, error) {
|
|
||||||
data, err := ioutil.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var people []Person
|
|
||||||
err = dhall.Unmarshal(data, &people)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return people, nil
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestLoadPeople(t *testing.T) {
|
|
||||||
people, err := loadPeople("../../signalboost.dhall")
|
|
||||||
if err != nil {t.Fatal(err)}
|
|
||||||
|
|
||||||
for _, person := range people {
|
|
||||||
t.Run(person.Name, func(t *testing.T) {
|
|
||||||
if person.Name == "" {
|
|
||||||
t.Error("missing name")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(person.Tags) == 0 {
|
|
||||||
t.Error("missing tags")
|
|
||||||
}
|
|
||||||
|
|
||||||
if person.Twitter == "" {
|
|
||||||
t.Error("missing twitter")
|
|
||||||
}
|
|
||||||
|
|
||||||
if person.GitLink == "" {
|
|
||||||
t.Error("missing git link")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
21
go.mod
21
go.mod
|
@ -1,21 +0,0 @@
|
||||||
module christine.website
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/celrenheit/sandflake v0.0.0-20190410195419-50a943690bc2
|
|
||||||
github.com/gorilla/feeds v1.1.1
|
|
||||||
github.com/joho/godotenv v1.3.0
|
|
||||||
github.com/mxpv/patreon-go v0.0.0-20190917022727-646111f1d983
|
|
||||||
github.com/philandstuff/dhall-golang v1.0.0
|
|
||||||
github.com/povilasv/prommod v0.0.12
|
|
||||||
github.com/prometheus/client_golang v1.7.1
|
|
||||||
github.com/russross/blackfriday v2.0.0+incompatible
|
|
||||||
github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35
|
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
|
||||||
github.com/snabb/sitemap v1.0.0
|
|
||||||
github.com/stretchr/testify v1.6.1
|
|
||||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
|
||||||
gopkg.in/yaml.v2 v2.3.0
|
|
||||||
within.website/ln v0.9.1
|
|
||||||
)
|
|
||||||
|
|
||||||
go 1.13
|
|
188
go.sum
188
go.sum
|
@ -1,188 +0,0 @@
|
||||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
|
||||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
|
||||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
|
||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
|
||||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
|
||||||
github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
|
|
||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
|
||||||
github.com/celrenheit/sandflake v0.0.0-20190410195419-50a943690bc2 h1:/BpnZPo/sk1vPlt62dLya5KCn7PN9ZBDrpTGlQzgUZI=
|
|
||||||
github.com/celrenheit/sandflake v0.0.0-20190410195419-50a943690bc2/go.mod h1:7L8gY0+4GYeBc9TvqVuDUq7tXuM6Sj7llnt7HkVwWlQ=
|
|
||||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
|
||||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
|
||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
|
||||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
|
||||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
|
||||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
|
||||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
|
||||||
github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ=
|
|
||||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
|
||||||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
|
||||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
|
||||||
github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
|
|
||||||
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
|
|
||||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
|
||||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
|
||||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
|
||||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
|
||||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
|
||||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
|
||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|
||||||
github.com/leanovate/gopter v0.2.5-0.20190402064358-634a59d12406 h1:+OUpk+IVvmKU0jivOVFGtOzA6U5AWFs8HE4DRzWLOUE=
|
|
||||||
github.com/leanovate/gopter v0.2.5-0.20190402064358-634a59d12406/go.mod h1:gNcbPWNEWRe4lm+bycKqxUYoH5uoVje5SkOJ3uoLer8=
|
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
|
||||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
|
||||||
github.com/mxpv/patreon-go v0.0.0-20190917022727-646111f1d983 h1:r32TFg+FHLnoF8PCqCQNp+R9EjMBuP62FXkD/Eqp9Us=
|
|
||||||
github.com/mxpv/patreon-go v0.0.0-20190917022727-646111f1d983/go.mod h1:ksYjm2GAbGlgIP7jO9Q5/AdyE4MwwEbgQ+lFMx3hyiM=
|
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
|
||||||
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
|
|
||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
|
||||||
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
|
|
||||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
|
||||||
github.com/philandstuff/dhall-golang v1.0.0 h1:4iYE+OfVjpXtwB6todsw5w+rnBvAhufgpNzAo9K0ljw=
|
|
||||||
github.com/philandstuff/dhall-golang v1.0.0/go.mod h1:nYfzcKjqq6UDCStpXV6UxRwD0HX9IK9z/MuHmHghbEY=
|
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/povilasv/prommod v0.0.12 h1:0bk9QJ7kD6SmSsk9MeHhz5Qe6OpQl11Fvo7cvvmNUQM=
|
|
||||||
github.com/povilasv/prommod v0.0.12/go.mod h1:GnuK7wLoVBwZXj8bhbJNx/xFSldy7Q49A44RJKNM8XQ=
|
|
||||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
|
||||||
github.com/prometheus/client_golang v1.0.0 h1:vrDKnkGzuGvhNAL56c7DBz29ZL+KxnoR0x7enabFceM=
|
|
||||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
|
||||||
github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA=
|
|
||||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
|
||||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
|
||||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
|
|
||||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
|
||||||
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
|
|
||||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
|
||||||
github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw=
|
|
||||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
|
||||||
github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc=
|
|
||||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
|
||||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
|
||||||
github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs=
|
|
||||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
|
||||||
github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8=
|
|
||||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
|
||||||
github.com/russross/blackfriday v2.0.0+incompatible h1:cBXrhZNUf9C+La9/YpS+UHpUT8YD6Td9ZMSU9APFcsk=
|
|
||||||
github.com/russross/blackfriday v2.0.0+incompatible/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
|
||||||
github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35 h1:eajwn6K3weW5cd1ZXLu2sJ4pvwlBiCWY4uDejOr73gM=
|
|
||||||
github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35/go.mod h1:wozgYq9WEBQBaIJe4YZ0qTSFAMxmcwBhQH0fO0R34Z0=
|
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
|
||||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
|
||||||
github.com/snabb/diagio v1.0.0 h1:kovhQ1rDXoEbmpf/T5N2sUp2iOdxEg+TcqzbYVHV2V0=
|
|
||||||
github.com/snabb/diagio v1.0.0/go.mod h1:ZyGaWFhfBVqstGUw6laYetzeTwZ2xxVPqTALx1QQa1w=
|
|
||||||
github.com/snabb/sitemap v1.0.0 h1:7vJeNPAaaj7fQSRS3WYuJHzUjdnhLdSLLpvVtnhbzC0=
|
|
||||||
github.com/snabb/sitemap v1.0.0/go.mod h1:Id8uz1+WYdiNmSjEi4BIvL5UwNPYLsTHzRbjmDwNDzA=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
|
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
|
||||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
|
||||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/ugorji/go v1.1.5-0.20190603013658-a2c9fa250719 h1:UW5IeyWBDAPQ+Qu1hT/lwtxL7pP3L+ETA8WuBvvvBWU=
|
|
||||||
github.com/ugorji/go v1.1.5-0.20190603013658-a2c9fa250719/go.mod h1:RaaajvHwnCbhlqWLTIB78hyPWp24YUXhQ3YXM7Hg7os=
|
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU=
|
|
||||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM/fAoGlaiiHYiFYdm80=
|
|
||||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
|
||||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
|
||||||
google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw=
|
|
||||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
|
||||||
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
|
|
||||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
|
||||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
within.website/ln v0.9.1 h1:Qi8IjeCnU43jXijKtr5qtcbjuiCVAudOIxqTim7svnc=
|
|
||||||
within.website/ln v0.9.1/go.mod h1:I+Apk8qxMStNXTZdyDMqDqe6CB8Hn6+W/Gyf5QbY+2E=
|
|
|
@ -1,17 +0,0 @@
|
||||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/
|
|
||||||
|
|
||||||
language: go
|
|
||||||
|
|
||||||
go:
|
|
||||||
- 1.8.1
|
|
||||||
|
|
||||||
before_install:
|
|
||||||
- go get -t -v ./...
|
|
||||||
|
|
||||||
script:
|
|
||||||
- go test -race -coverprofile=coverage.txt -covermode=atomic
|
|
||||||
|
|
||||||
after_success:
|
|
||||||
- bash <(curl -s https://codecov.io/bash)
|
|
363
jsonfeed/LICENSE
363
jsonfeed/LICENSE
|
@ -1,363 +0,0 @@
|
||||||
Mozilla Public License, version 2.0
|
|
||||||
|
|
||||||
1. Definitions
|
|
||||||
|
|
||||||
1.1. "Contributor"
|
|
||||||
|
|
||||||
means each individual or legal entity that creates, contributes to the
|
|
||||||
creation of, or owns Covered Software.
|
|
||||||
|
|
||||||
1.2. "Contributor Version"
|
|
||||||
|
|
||||||
means the combination of the Contributions of others (if any) used by a
|
|
||||||
Contributor and that particular Contributor's Contribution.
|
|
||||||
|
|
||||||
1.3. "Contribution"
|
|
||||||
|
|
||||||
means Covered Software of a particular Contributor.
|
|
||||||
|
|
||||||
1.4. "Covered Software"
|
|
||||||
|
|
||||||
means Source Code Form to which the initial Contributor has attached the
|
|
||||||
notice in Exhibit A, the Executable Form of such Source Code Form, and
|
|
||||||
Modifications of such Source Code Form, in each case including portions
|
|
||||||
thereof.
|
|
||||||
|
|
||||||
1.5. "Incompatible With Secondary Licenses"
|
|
||||||
means
|
|
||||||
|
|
||||||
a. that the initial Contributor has attached the notice described in
|
|
||||||
Exhibit B to the Covered Software; or
|
|
||||||
|
|
||||||
b. that the Covered Software was made available under the terms of
|
|
||||||
version 1.1 or earlier of the License, but not also under the terms of
|
|
||||||
a Secondary License.
|
|
||||||
|
|
||||||
1.6. "Executable Form"
|
|
||||||
|
|
||||||
means any form of the work other than Source Code Form.
|
|
||||||
|
|
||||||
1.7. "Larger Work"
|
|
||||||
|
|
||||||
means a work that combines Covered Software with other material, in a
|
|
||||||
separate file or files, that is not Covered Software.
|
|
||||||
|
|
||||||
1.8. "License"
|
|
||||||
|
|
||||||
means this document.
|
|
||||||
|
|
||||||
1.9. "Licensable"
|
|
||||||
|
|
||||||
means having the right to grant, to the maximum extent possible, whether
|
|
||||||
at the time of the initial grant or subsequently, any and all of the
|
|
||||||
rights conveyed by this License.
|
|
||||||
|
|
||||||
1.10. "Modifications"
|
|
||||||
|
|
||||||
means any of the following:
|
|
||||||
|
|
||||||
a. any file in Source Code Form that results from an addition to,
|
|
||||||
deletion from, or modification of the contents of Covered Software; or
|
|
||||||
|
|
||||||
b. any new file in Source Code Form that contains any Covered Software.
|
|
||||||
|
|
||||||
1.11. "Patent Claims" of a Contributor
|
|
||||||
|
|
||||||
means any patent claim(s), including without limitation, method,
|
|
||||||
process, and apparatus claims, in any patent Licensable by such
|
|
||||||
Contributor that would be infringed, but for the grant of the License,
|
|
||||||
by the making, using, selling, offering for sale, having made, import,
|
|
||||||
or transfer of either its Contributions or its Contributor Version.
|
|
||||||
|
|
||||||
1.12. "Secondary License"
|
|
||||||
|
|
||||||
means either the GNU General Public License, Version 2.0, the GNU Lesser
|
|
||||||
General Public License, Version 2.1, the GNU Affero General Public
|
|
||||||
License, Version 3.0, or any later versions of those licenses.
|
|
||||||
|
|
||||||
1.13. "Source Code Form"
|
|
||||||
|
|
||||||
means the form of the work preferred for making modifications.
|
|
||||||
|
|
||||||
1.14. "You" (or "Your")
|
|
||||||
|
|
||||||
means an individual or a legal entity exercising rights under this
|
|
||||||
License. For legal entities, "You" includes any entity that controls, is
|
|
||||||
controlled by, or is under common control with You. For purposes of this
|
|
||||||
definition, "control" means (a) the power, direct or indirect, to cause
|
|
||||||
the direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (b) ownership of more than fifty percent (50%) of the
|
|
||||||
outstanding shares or beneficial ownership of such entity.
|
|
||||||
|
|
||||||
|
|
||||||
2. License Grants and Conditions
|
|
||||||
|
|
||||||
2.1. Grants
|
|
||||||
|
|
||||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
|
||||||
non-exclusive license:
|
|
||||||
|
|
||||||
a. under intellectual property rights (other than patent or trademark)
|
|
||||||
Licensable by such Contributor to use, reproduce, make available,
|
|
||||||
modify, display, perform, distribute, and otherwise exploit its
|
|
||||||
Contributions, either on an unmodified basis, with Modifications, or
|
|
||||||
as part of a Larger Work; and
|
|
||||||
|
|
||||||
b. under Patent Claims of such Contributor to make, use, sell, offer for
|
|
||||||
sale, have made, import, and otherwise transfer either its
|
|
||||||
Contributions or its Contributor Version.
|
|
||||||
|
|
||||||
2.2. Effective Date
|
|
||||||
|
|
||||||
The licenses granted in Section 2.1 with respect to any Contribution
|
|
||||||
become effective for each Contribution on the date the Contributor first
|
|
||||||
distributes such Contribution.
|
|
||||||
|
|
||||||
2.3. Limitations on Grant Scope
|
|
||||||
|
|
||||||
The licenses granted in this Section 2 are the only rights granted under
|
|
||||||
this License. No additional rights or licenses will be implied from the
|
|
||||||
distribution or licensing of Covered Software under this License.
|
|
||||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
|
||||||
Contributor:
|
|
||||||
|
|
||||||
a. for any code that a Contributor has removed from Covered Software; or
|
|
||||||
|
|
||||||
b. for infringements caused by: (i) Your and any other third party's
|
|
||||||
modifications of Covered Software, or (ii) the combination of its
|
|
||||||
Contributions with other software (except as part of its Contributor
|
|
||||||
Version); or
|
|
||||||
|
|
||||||
c. under Patent Claims infringed by Covered Software in the absence of
|
|
||||||
its Contributions.
|
|
||||||
|
|
||||||
This License does not grant any rights in the trademarks, service marks,
|
|
||||||
or logos of any Contributor (except as may be necessary to comply with
|
|
||||||
the notice requirements in Section 3.4).
|
|
||||||
|
|
||||||
2.4. Subsequent Licenses
|
|
||||||
|
|
||||||
No Contributor makes additional grants as a result of Your choice to
|
|
||||||
distribute the Covered Software under a subsequent version of this
|
|
||||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
|
||||||
permitted under the terms of Section 3.3).
|
|
||||||
|
|
||||||
2.5. Representation
|
|
||||||
|
|
||||||
Each Contributor represents that the Contributor believes its
|
|
||||||
Contributions are its original creation(s) or it has sufficient rights to
|
|
||||||
grant the rights to its Contributions conveyed by this License.
|
|
||||||
|
|
||||||
2.6. Fair Use
|
|
||||||
|
|
||||||
This License is not intended to limit any rights You have under
|
|
||||||
applicable copyright doctrines of fair use, fair dealing, or other
|
|
||||||
equivalents.
|
|
||||||
|
|
||||||
2.7. Conditions
|
|
||||||
|
|
||||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
|
|
||||||
Section 2.1.
|
|
||||||
|
|
||||||
|
|
||||||
3. Responsibilities
|
|
||||||
|
|
||||||
3.1. Distribution of Source Form
|
|
||||||
|
|
||||||
All distribution of Covered Software in Source Code Form, including any
|
|
||||||
Modifications that You create or to which You contribute, must be under
|
|
||||||
the terms of this License. You must inform recipients that the Source
|
|
||||||
Code Form of the Covered Software is governed by the terms of this
|
|
||||||
License, and how they can obtain a copy of this License. You may not
|
|
||||||
attempt to alter or restrict the recipients' rights in the Source Code
|
|
||||||
Form.
|
|
||||||
|
|
||||||
3.2. Distribution of Executable Form
|
|
||||||
|
|
||||||
If You distribute Covered Software in Executable Form then:
|
|
||||||
|
|
||||||
a. such Covered Software must also be made available in Source Code Form,
|
|
||||||
as described in Section 3.1, and You must inform recipients of the
|
|
||||||
Executable Form how they can obtain a copy of such Source Code Form by
|
|
||||||
reasonable means in a timely manner, at a charge no more than the cost
|
|
||||||
of distribution to the recipient; and
|
|
||||||
|
|
||||||
b. You may distribute such Executable Form under the terms of this
|
|
||||||
License, or sublicense it under different terms, provided that the
|
|
||||||
license for the Executable Form does not attempt to limit or alter the
|
|
||||||
recipients' rights in the Source Code Form under this License.
|
|
||||||
|
|
||||||
3.3. Distribution of a Larger Work
|
|
||||||
|
|
||||||
You may create and distribute a Larger Work under terms of Your choice,
|
|
||||||
provided that You also comply with the requirements of this License for
|
|
||||||
the Covered Software. If the Larger Work is a combination of Covered
|
|
||||||
Software with a work governed by one or more Secondary Licenses, and the
|
|
||||||
Covered Software is not Incompatible With Secondary Licenses, this
|
|
||||||
License permits You to additionally distribute such Covered Software
|
|
||||||
under the terms of such Secondary License(s), so that the recipient of
|
|
||||||
the Larger Work may, at their option, further distribute the Covered
|
|
||||||
Software under the terms of either this License or such Secondary
|
|
||||||
License(s).
|
|
||||||
|
|
||||||
3.4. Notices
|
|
||||||
|
|
||||||
You may not remove or alter the substance of any license notices
|
|
||||||
(including copyright notices, patent notices, disclaimers of warranty, or
|
|
||||||
limitations of liability) contained within the Source Code Form of the
|
|
||||||
Covered Software, except that You may alter any license notices to the
|
|
||||||
extent required to remedy known factual inaccuracies.
|
|
||||||
|
|
||||||
3.5. Application of Additional Terms
|
|
||||||
|
|
||||||
You may choose to offer, and to charge a fee for, warranty, support,
|
|
||||||
indemnity or liability obligations to one or more recipients of Covered
|
|
||||||
Software. However, You may do so only on Your own behalf, and not on
|
|
||||||
behalf of any Contributor. You must make it absolutely clear that any
|
|
||||||
such warranty, support, indemnity, or liability obligation is offered by
|
|
||||||
You alone, and You hereby agree to indemnify every Contributor for any
|
|
||||||
liability incurred by such Contributor as a result of warranty, support,
|
|
||||||
indemnity or liability terms You offer. You may include additional
|
|
||||||
disclaimers of warranty and limitations of liability specific to any
|
|
||||||
jurisdiction.
|
|
||||||
|
|
||||||
4. Inability to Comply Due to Statute or Regulation
|
|
||||||
|
|
||||||
If it is impossible for You to comply with any of the terms of this License
|
|
||||||
with respect to some or all of the Covered Software due to statute,
|
|
||||||
judicial order, or regulation then You must: (a) comply with the terms of
|
|
||||||
this License to the maximum extent possible; and (b) describe the
|
|
||||||
limitations and the code they affect. Such description must be placed in a
|
|
||||||
text file included with all distributions of the Covered Software under
|
|
||||||
this License. Except to the extent prohibited by statute or regulation,
|
|
||||||
such description must be sufficiently detailed for a recipient of ordinary
|
|
||||||
skill to be able to understand it.
|
|
||||||
|
|
||||||
5. Termination
|
|
||||||
|
|
||||||
5.1. The rights granted under this License will terminate automatically if You
|
|
||||||
fail to comply with any of its terms. However, if You become compliant,
|
|
||||||
then the rights granted under this License from a particular Contributor
|
|
||||||
are reinstated (a) provisionally, unless and until such Contributor
|
|
||||||
explicitly and finally terminates Your grants, and (b) on an ongoing
|
|
||||||
basis, if such Contributor fails to notify You of the non-compliance by
|
|
||||||
some reasonable means prior to 60 days after You have come back into
|
|
||||||
compliance. Moreover, Your grants from a particular Contributor are
|
|
||||||
reinstated on an ongoing basis if such Contributor notifies You of the
|
|
||||||
non-compliance by some reasonable means, this is the first time You have
|
|
||||||
received notice of non-compliance with this License from such
|
|
||||||
Contributor, and You become compliant prior to 30 days after Your receipt
|
|
||||||
of the notice.
|
|
||||||
|
|
||||||
5.2. If You initiate litigation against any entity by asserting a patent
|
|
||||||
infringement claim (excluding declaratory judgment actions,
|
|
||||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
|
||||||
directly or indirectly infringes any patent, then the rights granted to
|
|
||||||
You by any and all Contributors for the Covered Software under Section
|
|
||||||
2.1 of this License shall terminate.
|
|
||||||
|
|
||||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
|
|
||||||
license agreements (excluding distributors and resellers) which have been
|
|
||||||
validly granted by You or Your distributors under this License prior to
|
|
||||||
termination shall survive termination.
|
|
||||||
|
|
||||||
6. Disclaimer of Warranty
|
|
||||||
|
|
||||||
Covered Software is provided under this License on an "as is" basis,
|
|
||||||
without warranty of any kind, either expressed, implied, or statutory,
|
|
||||||
including, without limitation, warranties that the Covered Software is free
|
|
||||||
of defects, merchantable, fit for a particular purpose or non-infringing.
|
|
||||||
The entire risk as to the quality and performance of the Covered Software
|
|
||||||
is with You. Should any Covered Software prove defective in any respect,
|
|
||||||
You (not any Contributor) assume the cost of any necessary servicing,
|
|
||||||
repair, or correction. This disclaimer of warranty constitutes an essential
|
|
||||||
part of this License. No use of any Covered Software is authorized under
|
|
||||||
this License except under this disclaimer.
|
|
||||||
|
|
||||||
7. Limitation of Liability
|
|
||||||
|
|
||||||
Under no circumstances and under no legal theory, whether tort (including
|
|
||||||
negligence), contract, or otherwise, shall any Contributor, or anyone who
|
|
||||||
distributes Covered Software as permitted above, be liable to You for any
|
|
||||||
direct, indirect, special, incidental, or consequential damages of any
|
|
||||||
character including, without limitation, damages for lost profits, loss of
|
|
||||||
goodwill, work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses, even if such party shall have been
|
|
||||||
informed of the possibility of such damages. This limitation of liability
|
|
||||||
shall not apply to liability for death or personal injury resulting from
|
|
||||||
such party's negligence to the extent applicable law prohibits such
|
|
||||||
limitation. Some jurisdictions do not allow the exclusion or limitation of
|
|
||||||
incidental or consequential damages, so this exclusion and limitation may
|
|
||||||
not apply to You.
|
|
||||||
|
|
||||||
8. Litigation
|
|
||||||
|
|
||||||
Any litigation relating to this License may be brought only in the courts
|
|
||||||
of a jurisdiction where the defendant maintains its principal place of
|
|
||||||
business and such litigation shall be governed by laws of that
|
|
||||||
jurisdiction, without reference to its conflict-of-law provisions. Nothing
|
|
||||||
in this Section shall prevent a party's ability to bring cross-claims or
|
|
||||||
counter-claims.
|
|
||||||
|
|
||||||
9. Miscellaneous
|
|
||||||
|
|
||||||
This License represents the complete agreement concerning the subject
|
|
||||||
matter hereof. If any provision of this License is held to be
|
|
||||||
unenforceable, such provision shall be reformed only to the extent
|
|
||||||
necessary to make it enforceable. Any law or regulation which provides that
|
|
||||||
the language of a contract shall be construed against the drafter shall not
|
|
||||||
be used to construe this License against a Contributor.
|
|
||||||
|
|
||||||
|
|
||||||
10. Versions of the License
|
|
||||||
|
|
||||||
10.1. New Versions
|
|
||||||
|
|
||||||
Mozilla Foundation is the license steward. Except as provided in Section
|
|
||||||
10.3, no one other than the license steward has the right to modify or
|
|
||||||
publish new versions of this License. Each version will be given a
|
|
||||||
distinguishing version number.
|
|
||||||
|
|
||||||
10.2. Effect of New Versions
|
|
||||||
|
|
||||||
You may distribute the Covered Software under the terms of the version
|
|
||||||
of the License under which You originally received the Covered Software,
|
|
||||||
or under the terms of any subsequent version published by the license
|
|
||||||
steward.
|
|
||||||
|
|
||||||
10.3. Modified Versions
|
|
||||||
|
|
||||||
If you create software not governed by this License, and you want to
|
|
||||||
create a new license for such software, you may create and use a
|
|
||||||
modified version of this License if you rename the license and remove
|
|
||||||
any references to the name of the license steward (except to note that
|
|
||||||
such modified license differs from this License).
|
|
||||||
|
|
||||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
|
||||||
Licenses If You choose to distribute Source Code Form that is
|
|
||||||
Incompatible With Secondary Licenses under the terms of this version of
|
|
||||||
the License, the notice described in Exhibit B of this License must be
|
|
||||||
attached.
|
|
||||||
|
|
||||||
Exhibit A - Source Code Form License Notice
|
|
||||||
|
|
||||||
This Source Code Form is subject to the
|
|
||||||
terms of the Mozilla Public License, v.
|
|
||||||
2.0. If a copy of the MPL was not
|
|
||||||
distributed with this file, You can
|
|
||||||
obtain one at
|
|
||||||
http://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
If it is not possible or desirable to put the notice in a particular file,
|
|
||||||
then You may include the notice in a location (such as a LICENSE file in a
|
|
||||||
relevant directory) where a recipient would be likely to look for such a
|
|
||||||
notice.
|
|
||||||
|
|
||||||
You may add additional accurate notices of copyright ownership.
|
|
||||||
|
|
||||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
|
||||||
|
|
||||||
This Source Code Form is "Incompatible
|
|
||||||
With Secondary Licenses", as defined by
|
|
||||||
the Mozilla Public License, v. 2.0.
|
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
# JSONFeed - Go Package to parse JSON Feed streams
|
|
||||||
|
|
||||||
[](https://travis-ci.org/st3fan/jsonfeed) [](https://goreportcard.com/report/github.com/st3fan/jsonfeed) [](https://codecov.io/gh/st3fan/jsonfeed)
|
|
||||||
|
|
||||||
|
|
||||||
*Stefan Arentz, May 2017*
|
|
||||||
|
|
||||||
Work in progress. Minimal package to parse JSON Feed streams. Please file feature requests.
|
|
|
@ -1,73 +0,0 @@
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/
|
|
||||||
|
|
||||||
package jsonfeed
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const CurrentVersion = "https://jsonfeed.org/version/1"
|
|
||||||
|
|
||||||
type Item struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
ExternalURL string `json:"external_url"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
ContentHTML string `json:"content_html"`
|
|
||||||
ContentText string `json:"content_text"`
|
|
||||||
Summary string `json:"summary"`
|
|
||||||
Image string `json:"image"`
|
|
||||||
BannerImage string `json:"banner_image"`
|
|
||||||
DatePublished time.Time `json:"date_published"`
|
|
||||||
DateModified time.Time `json:"date_modified"`
|
|
||||||
Author Author `json:"author"`
|
|
||||||
Tags []string `json:"tags"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Author struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
Avatar string `json:"avatar"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Hub struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Attachment struct {
|
|
||||||
URL string `json:"url"`
|
|
||||||
MIMEType string `json:"mime_type"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
SizeInBytes int64 `json:"size_in_bytes"`
|
|
||||||
DurationInSeconds int64 `json:"duration_in_seconds"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Feed struct {
|
|
||||||
Version string `json:"version"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
HomePageURL string `json:"home_page_url"`
|
|
||||||
FeedURL string `json:"feed_url"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
UserComment string `json:"user_comment"`
|
|
||||||
NextURL string `json:"next_url"`
|
|
||||||
Icon string `json:"icon"`
|
|
||||||
Favicon string `json:"favicon"`
|
|
||||||
Author Author `json:"author"`
|
|
||||||
Expired bool `json:"expired"`
|
|
||||||
Hubs []Hub `json:"hubs"`
|
|
||||||
Items []Item `json:"items"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func Parse(r io.Reader) (Feed, error) {
|
|
||||||
var feed Feed
|
|
||||||
decoder := json.NewDecoder(r)
|
|
||||||
if err := decoder.Decode(&feed); err != nil {
|
|
||||||
return Feed{}, err
|
|
||||||
}
|
|
||||||
return feed, nil
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/
|
|
||||||
|
|
||||||
package jsonfeed
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestParseSimple(t *testing.T) {
|
|
||||||
r, err := os.Open("testdata/feed.json")
|
|
||||||
assert.NoError(t, err, "Could not open testdata/feed.json")
|
|
||||||
|
|
||||||
feed, err := Parse(r)
|
|
||||||
assert.NoError(t, err, "Could not parse testdata/feed.json")
|
|
||||||
|
|
||||||
assert.Equal(t, "https://jsonfeed.org/version/1", feed.Version)
|
|
||||||
assert.Equal(t, "JSON Feed", feed.Title)
|
|
||||||
assert.Equal(t, "JSON Feed is a ...", feed.Description)
|
|
||||||
assert.Equal(t, "https://jsonfeed.org/", feed.HomePageURL)
|
|
||||||
assert.Equal(t, "https://jsonfeed.org/feed.json", feed.FeedURL)
|
|
||||||
assert.Equal(t, "This feed allows ...", feed.UserComment)
|
|
||||||
assert.Equal(t, "https://jsonfeed.org/graphics/icon.png", feed.Favicon)
|
|
||||||
assert.Equal(t, "Brent Simmons and Manton Reece", feed.Author.Name)
|
|
||||||
|
|
||||||
assert.Equal(t, 1, len(feed.Items))
|
|
||||||
|
|
||||||
assert.Equal(t, "https://jsonfeed.org/2017/05/17/announcing_json_feed", feed.Items[0].ID)
|
|
||||||
assert.Equal(t, "https://jsonfeed.org/2017/05/17/announcing_json_feed", feed.Items[0].URL)
|
|
||||||
assert.Equal(t, "Announcing JSON Feed", feed.Items[0].Title)
|
|
||||||
assert.Equal(t, "<p>We ...", feed.Items[0].ContentHTML)
|
|
||||||
|
|
||||||
datePublished, err := time.Parse("2006-01-02T15:04:05-07:00", "2017-05-17T08:02:12-07:00")
|
|
||||||
assert.NoError(t, err, "Could not parse timestamp")
|
|
||||||
|
|
||||||
assert.Equal(t, datePublished, feed.Items[0].DatePublished)
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
{
|
|
||||||
"version": "https://jsonfeed.org/version/1",
|
|
||||||
"title": "JSON Feed",
|
|
||||||
"description": "JSON Feed is a ...",
|
|
||||||
"home_page_url": "https://jsonfeed.org/",
|
|
||||||
"feed_url": "https://jsonfeed.org/feed.json",
|
|
||||||
"user_comment": "This feed allows ...",
|
|
||||||
"favicon": "https://jsonfeed.org/graphics/icon.png",
|
|
||||||
"author": {
|
|
||||||
"name": "Brent Simmons and Manton Reece"
|
|
||||||
},
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": "https://jsonfeed.org/2017/05/17/announcing_json_feed",
|
|
||||||
"url": "https://jsonfeed.org/2017/05/17/announcing_json_feed",
|
|
||||||
"title": "Announcing JSON Feed",
|
|
||||||
"content_html": "<p>We ...",
|
|
||||||
"date_published": "2017-05-17T08:02:12-07:00"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
Loading…
Reference in New Issue