mirror of https://github.com/jlelse/GoBlog
Bug fixes, refactoring and other improvements
This commit is contained in:
parent
20df90bf4e
commit
67f2b1fbdb
|
@ -4,7 +4,7 @@ RUN apk add --no-cache git gcc musl-dev
|
||||||
RUN apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main sqlite-dev
|
RUN apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main sqlite-dev
|
||||||
ADD *.go go.mod go.sum /app/
|
ADD *.go go.mod go.sum /app/
|
||||||
ENV GOFLAGS="-tags=linux,libsqlite3,sqlite_fts5"
|
ENV GOFLAGS="-tags=linux,libsqlite3,sqlite_fts5"
|
||||||
RUN go test -cover
|
RUN go test -cover ./...
|
||||||
RUN go build -ldflags '-w -s' -o GoBlog
|
RUN go build -ldflags '-w -s' -o GoBlog
|
||||||
|
|
||||||
FROM alpine:3.13
|
FROM alpine:3.13
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-fed/httpsig"
|
"github.com/go-fed/httpsig"
|
||||||
"github.com/spf13/cast"
|
"github.com/spf13/cast"
|
||||||
|
@ -90,7 +91,7 @@ func (a *goBlog) apHandleWebfinger(w http.ResponseWriter, r *http.Request) {
|
||||||
"links": []map[string]string{
|
"links": []map[string]string{
|
||||||
{
|
{
|
||||||
"rel": "self",
|
"rel": "self",
|
||||||
"type": contentTypeAS,
|
"type": contenttype.AS,
|
||||||
"href": a.apIri(blog),
|
"href": a.apIri(blog),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -100,8 +101,8 @@ func (a *goBlog) apHandleWebfinger(w http.ResponseWriter, r *http.Request) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
w.Header().Set(contentType, "application/jrd+json"+charsetUtf8Suffix)
|
w.Header().Set(contentType, "application/jrd+json"+contenttype.CharsetUtf8Suffix)
|
||||||
_, _ = writeMinified(w, contentTypeJSON, b)
|
_, _ = a.min.Write(w, contenttype.JSON, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) apHandleInbox(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) apHandleInbox(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -244,7 +245,7 @@ func apVerifySignature(r *http.Request) (*asPerson, string, int, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleWellKnownHostMeta(w http.ResponseWriter, r *http.Request) {
|
func handleWellKnownHostMeta(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set(contentType, "application/xrd+xml"+charsetUtf8Suffix)
|
w.Header().Set(contentType, "application/xrd+xml"+contenttype.CharsetUtf8Suffix)
|
||||||
_, _ = w.Write([]byte(`<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" type="application/xrd+xml" template="https://` + r.Host + `/.well-known/webfinger?resource={uri}"/></XRD>`))
|
_, _ = w.Write([]byte(`<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" type="application/xrd+xml" template="https://` + r.Host + `/.well-known/webfinger?resource={uri}"/></XRD>`))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,7 +254,7 @@ func apGetRemoteActor(iri string) (*asPerson, int, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
req.Header.Set("Accept", contentTypeAS)
|
req.Header.Set("Accept", contenttype.AS)
|
||||||
req.Header.Set(userAgent, appUserAgent)
|
req.Header.Set(userAgent, appUserAgent)
|
||||||
resp, err := appHttpClient.Do(req)
|
resp, err := appHttpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -308,7 +309,7 @@ func (db *database) apRemoveInbox(inbox string) error {
|
||||||
func (a *goBlog) apPost(p *post) {
|
func (a *goBlog) apPost(p *post) {
|
||||||
n := a.toASNote(p)
|
n := a.toASNote(p)
|
||||||
a.apSendToAllFollowers(p.Blog, map[string]interface{}{
|
a.apSendToAllFollowers(p.Blog, map[string]interface{}{
|
||||||
"@context": asContext,
|
"@context": []string{asContext},
|
||||||
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
|
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
|
||||||
"id": a.fullPostURL(p),
|
"id": a.fullPostURL(p),
|
||||||
"published": n.Published,
|
"published": n.Published,
|
||||||
|
@ -324,7 +325,7 @@ func (a *goBlog) apPost(p *post) {
|
||||||
|
|
||||||
func (a *goBlog) apUpdate(p *post) {
|
func (a *goBlog) apUpdate(p *post) {
|
||||||
a.apSendToAllFollowers(p.Blog, map[string]interface{}{
|
a.apSendToAllFollowers(p.Blog, map[string]interface{}{
|
||||||
"@context": asContext,
|
"@context": []string{asContext},
|
||||||
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
|
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
|
||||||
"id": a.fullPostURL(p),
|
"id": a.fullPostURL(p),
|
||||||
"published": time.Now().Format("2006-01-02T15:04:05-07:00"),
|
"published": time.Now().Format("2006-01-02T15:04:05-07:00"),
|
||||||
|
@ -335,7 +336,7 @@ func (a *goBlog) apUpdate(p *post) {
|
||||||
|
|
||||||
func (a *goBlog) apAnnounce(p *post) {
|
func (a *goBlog) apAnnounce(p *post) {
|
||||||
a.apSendToAllFollowers(p.Blog, map[string]interface{}{
|
a.apSendToAllFollowers(p.Blog, map[string]interface{}{
|
||||||
"@context": asContext,
|
"@context": []string{asContext},
|
||||||
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
|
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
|
||||||
"id": a.fullPostURL(p) + "#announce",
|
"id": a.fullPostURL(p) + "#announce",
|
||||||
"published": a.toASNote(p).Published,
|
"published": a.toASNote(p).Published,
|
||||||
|
@ -346,7 +347,7 @@ func (a *goBlog) apAnnounce(p *post) {
|
||||||
|
|
||||||
func (a *goBlog) apDelete(p *post) {
|
func (a *goBlog) apDelete(p *post) {
|
||||||
a.apSendToAllFollowers(p.Blog, map[string]interface{}{
|
a.apSendToAllFollowers(p.Blog, map[string]interface{}{
|
||||||
"@context": asContext,
|
"@context": []string{asContext},
|
||||||
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
|
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
|
||||||
"id": a.fullPostURL(p) + "#delete",
|
"id": a.fullPostURL(p) + "#delete",
|
||||||
"type": "Delete",
|
"type": "Delete",
|
||||||
|
@ -383,7 +384,7 @@ func (a *goBlog) apAccept(blogName string, blog *configBlog, follow map[string]i
|
||||||
// remove @context from the inner activity
|
// remove @context from the inner activity
|
||||||
delete(follow, "@context")
|
delete(follow, "@context")
|
||||||
accept := map[string]interface{}{
|
accept := map[string]interface{}{
|
||||||
"@context": asContext,
|
"@context": []string{asContext},
|
||||||
"to": follow["actor"],
|
"to": follow["actor"],
|
||||||
"actor": a.apIri(blog),
|
"actor": a.apIri(blog),
|
||||||
"object": follow,
|
"object": follow,
|
||||||
|
|
|
@ -11,6 +11,8 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
|
||||||
)
|
)
|
||||||
|
|
||||||
type apRequest struct {
|
type apRequest struct {
|
||||||
|
@ -101,8 +103,8 @@ func (a *goBlog) apSendSigned(blogIri, to string, activity []byte) error {
|
||||||
r.Header.Set("Accept-Charset", "utf-8")
|
r.Header.Set("Accept-Charset", "utf-8")
|
||||||
r.Header.Set("Date", time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
|
r.Header.Set("Date", time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
|
||||||
r.Header.Set(userAgent, appUserAgent)
|
r.Header.Set(userAgent, appUserAgent)
|
||||||
r.Header.Set("Accept", contentTypeASUTF8)
|
r.Header.Set("Accept", contenttype.ASUTF8)
|
||||||
r.Header.Set(contentType, contentTypeASUTF8)
|
r.Header.Set(contentType, contenttype.ASUTF8)
|
||||||
r.Header.Set("Host", iri.Host)
|
r.Header.Set("Host", iri.Host)
|
||||||
// Sign request
|
// Sign request
|
||||||
a.apPostSignMutex.Lock()
|
a.apPostSignMutex.Lock()
|
||||||
|
|
|
@ -8,25 +8,26 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
|
||||||
"github.com/araddon/dateparse"
|
"github.com/araddon/dateparse"
|
||||||
"github.com/elnormous/contenttype"
|
ct "github.com/elnormous/contenttype"
|
||||||
)
|
)
|
||||||
|
|
||||||
var asContext = []string{"https://www.w3.org/ns/activitystreams"}
|
const asContext = "https://www.w3.org/ns/activitystreams"
|
||||||
|
|
||||||
var asCheckMediaTypes = []contenttype.MediaType{
|
|
||||||
contenttype.NewMediaType(contentTypeHTML),
|
|
||||||
contenttype.NewMediaType(contentTypeAS),
|
|
||||||
contenttype.NewMediaType("application/ld+json"),
|
|
||||||
}
|
|
||||||
|
|
||||||
const asRequestKey requestContextKey = "asRequest"
|
const asRequestKey requestContextKey = "asRequest"
|
||||||
|
|
||||||
|
var asCheckMediaTypes = []ct.MediaType{
|
||||||
|
ct.NewMediaType(contenttype.HTML),
|
||||||
|
ct.NewMediaType(contenttype.AS),
|
||||||
|
ct.NewMediaType(contenttype.LDJSON),
|
||||||
|
}
|
||||||
|
|
||||||
func (a *goBlog) checkActivityStreamsRequest(next http.Handler) http.Handler {
|
func (a *goBlog) checkActivityStreamsRequest(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
if ap := a.cfg.ActivityPub; ap != nil && ap.Enabled {
|
if ap := a.cfg.ActivityPub; ap != nil && ap.Enabled {
|
||||||
// Check if accepted media type is not HTML
|
// Check if accepted media type is not HTML
|
||||||
if mt, _, err := contenttype.GetAcceptableMediaType(r, asCheckMediaTypes); err == nil && mt.String() != asCheckMediaTypes[0].String() {
|
if mt, _, err := ct.GetAcceptableMediaType(r, asCheckMediaTypes); err == nil && mt.String() != asCheckMediaTypes[0].String() {
|
||||||
next.ServeHTTP(rw, r.WithContext(context.WithValue(r.Context(), asRequestKey, true)))
|
next.ServeHTTP(rw, r.WithContext(context.WithValue(r.Context(), asRequestKey, true)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -89,29 +90,29 @@ type asEndpoints struct {
|
||||||
|
|
||||||
func (a *goBlog) serveActivityStreamsPost(p *post, w http.ResponseWriter) {
|
func (a *goBlog) serveActivityStreamsPost(p *post, w http.ResponseWriter) {
|
||||||
b, _ := json.Marshal(a.toASNote(p))
|
b, _ := json.Marshal(a.toASNote(p))
|
||||||
w.Header().Set(contentType, contentTypeASUTF8)
|
w.Header().Set(contentType, contenttype.ASUTF8)
|
||||||
_, _ = writeMinified(w, contentTypeAS, b)
|
_, _ = a.min.Write(w, contenttype.AS, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) toASNote(p *post) *asNote {
|
func (a *goBlog) toASNote(p *post) *asNote {
|
||||||
// Create a Note object
|
// Create a Note object
|
||||||
as := &asNote{
|
as := &asNote{
|
||||||
Context: asContext,
|
Context: []string{asContext},
|
||||||
To: []string{"https://www.w3.org/ns/activitystreams#Public"},
|
To: []string{"https://www.w3.org/ns/activitystreams#Public"},
|
||||||
MediaType: contentTypeHTML,
|
MediaType: contenttype.HTML,
|
||||||
ID: a.fullPostURL(p),
|
ID: a.fullPostURL(p),
|
||||||
URL: a.fullPostURL(p),
|
URL: a.fullPostURL(p),
|
||||||
AttributedTo: a.apIri(a.cfg.Blogs[p.Blog]),
|
AttributedTo: a.apIri(a.cfg.Blogs[p.Blog]),
|
||||||
}
|
}
|
||||||
// Name and Type
|
// Name and Type
|
||||||
if title := p.title(); title != "" {
|
if title := p.Title(); title != "" {
|
||||||
as.Name = title
|
as.Name = title
|
||||||
as.Type = "Article"
|
as.Type = "Article"
|
||||||
} else {
|
} else {
|
||||||
as.Type = "Note"
|
as.Type = "Note"
|
||||||
}
|
}
|
||||||
// Content
|
// Content
|
||||||
as.Content = string(a.absoluteHTML(p))
|
as.Content = string(a.absolutePostHTML(p))
|
||||||
// Attachments
|
// Attachments
|
||||||
if images := p.Parameters[a.cfg.Micropub.PhotoParam]; len(images) > 0 {
|
if images := p.Parameters[a.cfg.Micropub.PhotoParam]; len(images) > 0 {
|
||||||
for _, image := range images {
|
for _, image := range images {
|
||||||
|
@ -158,7 +159,7 @@ func (a *goBlog) serveActivityStreams(blog string, w http.ResponseWriter, r *htt
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
asBlog := &asPerson{
|
asBlog := &asPerson{
|
||||||
Context: asContext,
|
Context: []string{asContext},
|
||||||
Type: "Person",
|
Type: "Person",
|
||||||
ID: a.apIri(b),
|
ID: a.apIri(b),
|
||||||
URL: a.apIri(b),
|
URL: a.apIri(b),
|
||||||
|
@ -184,6 +185,6 @@ func (a *goBlog) serveActivityStreams(blog string, w http.ResponseWriter, r *htt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jb, _ := json.Marshal(asBlog)
|
jb, _ := json.Marshal(asBlog)
|
||||||
w.Header().Set(contentType, contentTypeASUTF8)
|
w.Header().Set(contentType, contenttype.ASUTF8)
|
||||||
_, _ = writeMinified(w, contentTypeAS, jb)
|
_, _ = a.min.Write(w, contenttype.AS, jb)
|
||||||
}
|
}
|
||||||
|
|
9
app.go
9
app.go
|
@ -6,6 +6,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"git.jlel.se/jlelse/GoBlog/pkgs/minify"
|
||||||
shutdowner "git.jlel.se/jlelse/go-shutdowner"
|
shutdowner "git.jlel.se/jlelse/go-shutdowner"
|
||||||
ts "git.jlel.se/jlelse/template-strings"
|
ts "git.jlel.se/jlelse/template-strings"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
@ -27,6 +28,8 @@ type goBlog struct {
|
||||||
assetFiles map[string]*assetFile
|
assetFiles map[string]*assetFile
|
||||||
// Blogroll
|
// Blogroll
|
||||||
blogrollCacheGroup singleflight.Group
|
blogrollCacheGroup singleflight.Group
|
||||||
|
// Blogstats
|
||||||
|
blogStatsCacheGroup singleflight.Group
|
||||||
// Cache
|
// Cache
|
||||||
cache *cache
|
cache *cache
|
||||||
// Config
|
// Config
|
||||||
|
@ -37,6 +40,9 @@ type goBlog struct {
|
||||||
pPostHooks []postHookFunc
|
pPostHooks []postHookFunc
|
||||||
pUpdateHooks []postHookFunc
|
pUpdateHooks []postHookFunc
|
||||||
pDeleteHooks []postHookFunc
|
pDeleteHooks []postHookFunc
|
||||||
|
hourlyHooks []hourlyHookFunc
|
||||||
|
// HTTP
|
||||||
|
cspDomains string
|
||||||
// HTTP Routers
|
// HTTP Routers
|
||||||
d *dynamicHandler
|
d *dynamicHandler
|
||||||
privateMode bool
|
privateMode bool
|
||||||
|
@ -53,6 +59,7 @@ type goBlog struct {
|
||||||
setBlogMiddlewares map[string]func(http.Handler) http.Handler
|
setBlogMiddlewares map[string]func(http.Handler) http.Handler
|
||||||
sectionMiddlewares map[string]func(http.Handler) http.Handler
|
sectionMiddlewares map[string]func(http.Handler) http.Handler
|
||||||
taxonomyMiddlewares map[string]func(http.Handler) http.Handler
|
taxonomyMiddlewares map[string]func(http.Handler) http.Handler
|
||||||
|
taxValueMiddlewares map[string]func(http.Handler) http.Handler
|
||||||
photosMiddlewares map[string]func(http.Handler) http.Handler
|
photosMiddlewares map[string]func(http.Handler) http.Handler
|
||||||
searchMiddlewares map[string]func(http.Handler) http.Handler
|
searchMiddlewares map[string]func(http.Handler) http.Handler
|
||||||
customPagesMiddlewares map[string]func(http.Handler) http.Handler
|
customPagesMiddlewares map[string]func(http.Handler) http.Handler
|
||||||
|
@ -61,6 +68,8 @@ type goBlog struct {
|
||||||
logf *rotatelogs.RotateLogs
|
logf *rotatelogs.RotateLogs
|
||||||
// Markdown
|
// Markdown
|
||||||
md, absoluteMd goldmark.Markdown
|
md, absoluteMd goldmark.Markdown
|
||||||
|
// Minify
|
||||||
|
min minify.Minifier
|
||||||
// Regex Redirects
|
// Regex Redirects
|
||||||
regexRedirects []*regexRedirect
|
regexRedirects []*regexRedirect
|
||||||
// Rendering
|
// Rendering
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -102,7 +103,7 @@ func (a *goBlog) checkLogin(w http.ResponseWriter, r *http.Request) bool {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if r.Header.Get(contentType) != contentTypeWWWForm {
|
if r.Header.Get(contentType) != contenttype.WWWForm {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if r.FormValue("loginaction") != "login" {
|
if r.FormValue("loginaction") != "login" {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
|
||||||
"github.com/kaorimatz/go-opml"
|
"github.com/kaorimatz/go-opml"
|
||||||
servertiming "github.com/mitchellh/go-server-timing"
|
servertiming "github.com/mitchellh/go-server-timing"
|
||||||
"github.com/thoas/go-funk"
|
"github.com/thoas/go-funk"
|
||||||
|
@ -55,14 +56,14 @@ func (a *goBlog) serveBlogrollExport(w http.ResponseWriter, r *http.Request) {
|
||||||
if a.cfg.Cache != nil && a.cfg.Cache.Enable {
|
if a.cfg.Cache != nil && a.cfg.Cache.Enable {
|
||||||
setInternalCacheExpirationHeader(w, r, int(a.cfg.Cache.Expiration))
|
setInternalCacheExpirationHeader(w, r, int(a.cfg.Cache.Expiration))
|
||||||
}
|
}
|
||||||
w.Header().Set(contentType, contentTypeXMLUTF8)
|
w.Header().Set(contentType, contenttype.XMLUTF8)
|
||||||
var opmlBytes bytes.Buffer
|
var opmlBytes bytes.Buffer
|
||||||
_ = opml.Render(&opmlBytes, &opml.OPML{
|
_ = opml.Render(&opmlBytes, &opml.OPML{
|
||||||
Version: "2.0",
|
Version: "2.0",
|
||||||
DateCreated: time.Now().UTC(),
|
DateCreated: time.Now().UTC(),
|
||||||
Outlines: outlines.([]*opml.Outline),
|
Outlines: outlines.([]*opml.Outline),
|
||||||
})
|
})
|
||||||
_, _ = writeMinified(w, contentTypeXML, opmlBytes.Bytes())
|
_, _ = a.min.Write(w, contenttype.XML, opmlBytes.Bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) getBlogrollOutlines(blog string) ([]*opml.Outline, error) {
|
func (a *goBlog) getBlogrollOutlines(blog string) ([]*opml.Outline, error) {
|
||||||
|
|
|
@ -5,8 +5,6 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"golang.org/x/sync/singleflight"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *goBlog) initBlogStats() {
|
func (a *goBlog) initBlogStats() {
|
||||||
|
@ -31,11 +29,9 @@ func (a *goBlog) serveBlogStats(w http.ResponseWriter, r *http.Request) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var blogStatsCacheGroup singleflight.Group
|
|
||||||
|
|
||||||
func (a *goBlog) serveBlogStatsTable(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveBlogStatsTable(w http.ResponseWriter, r *http.Request) {
|
||||||
blog := r.Context().Value(blogContextKey).(string)
|
blog := r.Context().Value(blogContextKey).(string)
|
||||||
data, err, _ := blogStatsCacheGroup.Do(blog, func() (interface{}, error) {
|
data, err, _ := a.blogStatsCacheGroup.Do(blog, func() (interface{}, error) {
|
||||||
return a.db.getBlogStats(blog)
|
return a.db.getBlogStats(blog)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
|
||||||
"github.com/dchest/captcha"
|
"github.com/dchest/captcha"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -53,7 +54,7 @@ func (a *goBlog) checkCaptcha(w http.ResponseWriter, r *http.Request) bool {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if r.Header.Get(contentType) != contentTypeWWWForm {
|
if r.Header.Get(contentType) != contenttype.WWWForm {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if r.FormValue("captchaaction") != "captcha" {
|
if r.FormValue("captchaaction") != "captcha" {
|
||||||
|
|
2
check.go
2
check.go
|
@ -90,7 +90,7 @@ func (a *goBlog) getExternalLinks(posts []*post, linkChan chan<- stringPair) err
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(p *post) {
|
go func(p *post) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
links, _ := allLinksFromHTMLString(string(a.absoluteHTML(p)), a.fullPostURL(p))
|
links, _ := allLinksFromHTMLString(string(a.absolutePostHTML(p)), a.fullPostURL(p))
|
||||||
for _, link := range links {
|
for _, link := range links {
|
||||||
linkChan <- stringPair{a.fullPostURL(p), link}
|
linkChan <- stringPair{a.fullPostURL(p), link}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
sqlite "github.com/mattn/go-sqlite3"
|
sqlite "github.com/mattn/go-sqlite3"
|
||||||
"github.com/schollz/sqlite3dump"
|
"github.com/schollz/sqlite3dump"
|
||||||
|
@ -19,6 +20,7 @@ type database struct {
|
||||||
stmts map[string]*sql.Stmt
|
stmts map[string]*sql.Stmt
|
||||||
g singleflight.Group
|
g singleflight.Group
|
||||||
pc singleflight.Group
|
pc singleflight.Group
|
||||||
|
pcm sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) initDatabase() (err error) {
|
func (a *goBlog) initDatabase() (err error) {
|
||||||
|
@ -38,7 +40,7 @@ func (a *goBlog) initDatabase() (err error) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if a.cfg.Db.DumpFile != "" {
|
if a.cfg.Db.DumpFile != "" {
|
||||||
hourlyHooks = append(hourlyHooks, func() {
|
a.hourlyHooks = append(a.hourlyHooks, func() {
|
||||||
db.dump(a.cfg.Db.DumpFile)
|
db.dump(a.cfg.Db.DumpFile)
|
||||||
})
|
})
|
||||||
db.dump(a.cfg.Db.DumpFile)
|
db.dump(a.cfg.Db.DumpFile)
|
||||||
|
|
|
@ -7,6 +7,8 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
|
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
|
||||||
)
|
)
|
||||||
|
|
||||||
const editorPath = "/editor"
|
const editorPath = "/editor"
|
||||||
|
@ -71,7 +73,7 @@ func (a *goBlog) serveEditorPost(w http.ResponseWriter, r *http.Request) {
|
||||||
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
req.Header.Set(contentType, contentTypeJSON)
|
req.Header.Set(contentType, contenttype.JSON)
|
||||||
a.editorMicropubPost(w, req, false)
|
a.editorMicropubPost(w, req, false)
|
||||||
case "upload":
|
case "upload":
|
||||||
a.editorMicropubPost(w, r, true)
|
a.editorMicropubPost(w, r, true)
|
||||||
|
|
|
@ -4,7 +4,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/elnormous/contenttype"
|
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
|
||||||
|
ct "github.com/elnormous/contenttype"
|
||||||
)
|
)
|
||||||
|
|
||||||
type errorData struct {
|
type errorData struct {
|
||||||
|
@ -20,12 +21,12 @@ func (a *goBlog) serveNotAllowed(w http.ResponseWriter, r *http.Request) {
|
||||||
a.serveError(w, r, "", http.StatusMethodNotAllowed)
|
a.serveError(w, r, "", http.StatusMethodNotAllowed)
|
||||||
}
|
}
|
||||||
|
|
||||||
var errorCheckMediaTypes = []contenttype.MediaType{
|
var errorCheckMediaTypes = []ct.MediaType{
|
||||||
contenttype.NewMediaType(contentTypeHTML),
|
ct.NewMediaType(contenttype.HTML),
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) serveError(w http.ResponseWriter, r *http.Request, message string, status int) {
|
func (a *goBlog) serveError(w http.ResponseWriter, r *http.Request, message string, status int) {
|
||||||
if mt, _, err := contenttype.GetAcceptableMediaType(r, errorCheckMediaTypes); err != nil || mt.String() != errorCheckMediaTypes[0].String() {
|
if mt, _, err := ct.GetAcceptableMediaType(r, errorCheckMediaTypes); err != nil || mt.String() != errorCheckMediaTypes[0].String() {
|
||||||
// Request doesn't accept HTML
|
// Request doesn't accept HTML
|
||||||
http.Error(w, message, status)
|
http.Error(w, message, status)
|
||||||
return
|
return
|
||||||
|
|
17
feeds.go
17
feeds.go
|
@ -5,6 +5,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
|
||||||
"github.com/araddon/dateparse"
|
"github.com/araddon/dateparse"
|
||||||
"github.com/gorilla/feeds"
|
"github.com/gorilla/feeds"
|
||||||
)
|
)
|
||||||
|
@ -55,11 +56,11 @@ func (a *goBlog) generateFeed(blog string, f feedType, w http.ResponseWriter, r
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
feed.Add(&feeds.Item{
|
feed.Add(&feeds.Item{
|
||||||
Title: p.title(),
|
Title: p.Title(),
|
||||||
Link: &feeds.Link{Href: a.fullPostURL(p)},
|
Link: &feeds.Link{Href: a.fullPostURL(p)},
|
||||||
Description: a.summary(p),
|
Description: a.postSummary(p),
|
||||||
Id: p.Path,
|
Id: p.Path,
|
||||||
Content: string(a.absoluteHTML(p)),
|
Content: string(a.absolutePostHTML(p)),
|
||||||
Created: created,
|
Created: created,
|
||||||
Updated: updated,
|
Updated: updated,
|
||||||
Enclosure: enc,
|
Enclosure: enc,
|
||||||
|
@ -69,13 +70,13 @@ func (a *goBlog) generateFeed(blog string, f feedType, w http.ResponseWriter, r
|
||||||
var feedString, feedMediaType string
|
var feedString, feedMediaType string
|
||||||
switch f {
|
switch f {
|
||||||
case rssFeed:
|
case rssFeed:
|
||||||
feedMediaType = contentTypeRSS
|
feedMediaType = contenttype.RSS
|
||||||
feedString, err = feed.ToRss()
|
feedString, err = feed.ToRss()
|
||||||
case atomFeed:
|
case atomFeed:
|
||||||
feedMediaType = contentTypeATOM
|
feedMediaType = contenttype.ATOM
|
||||||
feedString, err = feed.ToAtom()
|
feedString, err = feed.ToAtom()
|
||||||
case jsonFeed:
|
case jsonFeed:
|
||||||
feedMediaType = contentTypeJSONFeed
|
feedMediaType = contenttype.JSONFeed
|
||||||
feedString, err = feed.ToJSON()
|
feedString, err = feed.ToJSON()
|
||||||
default:
|
default:
|
||||||
return
|
return
|
||||||
|
@ -85,6 +86,6 @@ func (a *goBlog) generateFeed(blog string, f feedType, w http.ResponseWriter, r
|
||||||
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set(contentType, feedMediaType+charsetUtf8Suffix)
|
w.Header().Set(contentType, feedMediaType+contenttype.CharsetUtf8Suffix)
|
||||||
_, _ = writeMinified(w, feedMediaType, []byte(feedString))
|
_, _ = a.min.Write(w, feedMediaType, []byte(feedString))
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,12 +7,16 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
gogeouri "git.jlel.se/jlelse/go-geouri"
|
||||||
geojson "github.com/paulmach/go.geojson"
|
geojson "github.com/paulmach/go.geojson"
|
||||||
"github.com/thoas/go-funk"
|
"github.com/thoas/go-funk"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (db *database) geoTitle(lat, lon float64, lang string) string {
|
func (db *database) geoTitle(g *gogeouri.Geo, lang string) string {
|
||||||
ba, err := db.photonReverse(lat, lon, lang)
|
if name, ok := g.Parameters["name"]; ok && len(name) > 0 && name[0] != "" {
|
||||||
|
return name[0]
|
||||||
|
}
|
||||||
|
ba, err := db.photonReverse(g.Latitude, g.Longitude, lang)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -65,3 +69,7 @@ func (db *database) photonReverse(lat, lon float64, lang string) ([]byte, error)
|
||||||
_ = db.cachePersistently(cacheKey, ba)
|
_ = db.cachePersistently(cacheKey, ba)
|
||||||
return ba, nil
|
return ba, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func geoOSMLink(g *gogeouri.Geo) string {
|
||||||
|
return fmt.Sprintf("https://www.openstreetmap.org/?mlat=%v&mlon=%v", g.Latitude, g.Longitude)
|
||||||
|
}
|
10
hooks.go
10
hooks.go
|
@ -78,7 +78,7 @@ func (cfg *configHooks) executeTemplateCommand(hookType string, tmpl string, dat
|
||||||
executeHookCommand(hookType, cfg.Shell, cmd)
|
executeHookCommand(hookType, cfg.Shell, cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
var hourlyHooks = []func(){}
|
type hourlyHookFunc func()
|
||||||
|
|
||||||
func (a *goBlog) startHourlyHooks() {
|
func (a *goBlog) startHourlyHooks() {
|
||||||
cfg := a.cfg.Hooks
|
cfg := a.cfg.Hooks
|
||||||
|
@ -88,14 +88,14 @@ func (a *goBlog) startHourlyHooks() {
|
||||||
f := func() {
|
f := func() {
|
||||||
executeHookCommand("hourly", cfg.Shell, c)
|
executeHookCommand("hourly", cfg.Shell, c)
|
||||||
}
|
}
|
||||||
hourlyHooks = append(hourlyHooks, f)
|
a.hourlyHooks = append(a.hourlyHooks, f)
|
||||||
}
|
}
|
||||||
// When there are hooks, start ticker
|
// When there are hooks, start ticker
|
||||||
if len(hourlyHooks) > 0 {
|
if len(a.hourlyHooks) > 0 {
|
||||||
// Wait for next full hour
|
// Wait for next full hour
|
||||||
tr := time.AfterFunc(time.Until(time.Now().Truncate(time.Hour).Add(time.Hour)), func() {
|
tr := time.AfterFunc(time.Until(time.Now().Truncate(time.Hour).Add(time.Hour)), func() {
|
||||||
// Execute once
|
// Execute once
|
||||||
for _, f := range hourlyHooks {
|
for _, f := range a.hourlyHooks {
|
||||||
go f()
|
go f()
|
||||||
}
|
}
|
||||||
// Start ticker and execute regularly
|
// Start ticker and execute regularly
|
||||||
|
@ -105,7 +105,7 @@ func (a *goBlog) startHourlyHooks() {
|
||||||
log.Println("Stopped hourly hooks")
|
log.Println("Stopped hourly hooks")
|
||||||
})
|
})
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
for _, f := range hourlyHooks {
|
for _, f := range a.hourlyHooks {
|
||||||
go f()
|
go f()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
46
http.go
46
http.go
|
@ -22,24 +22,6 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
contentType = "Content-Type"
|
contentType = "Content-Type"
|
||||||
|
|
||||||
charsetUtf8Suffix = "; charset=utf-8"
|
|
||||||
|
|
||||||
contentTypeHTML = "text/html"
|
|
||||||
contentTypeXML = "text/xml"
|
|
||||||
contentTypeJSON = "application/json"
|
|
||||||
contentTypeWWWForm = "application/x-www-form-urlencoded"
|
|
||||||
contentTypeMultipartForm = "multipart/form-data"
|
|
||||||
contentTypeAS = "application/activity+json"
|
|
||||||
contentTypeRSS = "application/rss+xml"
|
|
||||||
contentTypeATOM = "application/atom+xml"
|
|
||||||
contentTypeJSONFeed = "application/feed+json"
|
|
||||||
|
|
||||||
contentTypeHTMLUTF8 = contentTypeHTML + charsetUtf8Suffix
|
|
||||||
contentTypeXMLUTF8 = contentTypeXML + charsetUtf8Suffix
|
|
||||||
contentTypeJSONUTF8 = contentTypeJSON + charsetUtf8Suffix
|
|
||||||
contentTypeASUTF8 = contentTypeAS + charsetUtf8Suffix
|
|
||||||
|
|
||||||
userAgent = "User-Agent"
|
userAgent = "User-Agent"
|
||||||
appUserAgent = "GoBlog"
|
appUserAgent = "GoBlog"
|
||||||
)
|
)
|
||||||
|
@ -241,6 +223,7 @@ func (a *goBlog) buildStaticHandlersRouters() error {
|
||||||
a.setBlogMiddlewares = map[string]func(http.Handler) http.Handler{}
|
a.setBlogMiddlewares = map[string]func(http.Handler) http.Handler{}
|
||||||
a.sectionMiddlewares = map[string]func(http.Handler) http.Handler{}
|
a.sectionMiddlewares = map[string]func(http.Handler) http.Handler{}
|
||||||
a.taxonomyMiddlewares = map[string]func(http.Handler) http.Handler{}
|
a.taxonomyMiddlewares = map[string]func(http.Handler) http.Handler{}
|
||||||
|
a.taxValueMiddlewares = map[string]func(http.Handler) http.Handler{}
|
||||||
a.photosMiddlewares = map[string]func(http.Handler) http.Handler{}
|
a.photosMiddlewares = map[string]func(http.Handler) http.Handler{}
|
||||||
a.searchMiddlewares = map[string]func(http.Handler) http.Handler{}
|
a.searchMiddlewares = map[string]func(http.Handler) http.Handler{}
|
||||||
a.customPagesMiddlewares = map[string]func(http.Handler) http.Handler{}
|
a.customPagesMiddlewares = map[string]func(http.Handler) http.Handler{}
|
||||||
|
@ -293,10 +276,6 @@ func (a *goBlog) buildStaticHandlersRouters() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
taxValueMiddlewares = map[string]func(http.Handler) http.Handler{}
|
|
||||||
)
|
|
||||||
|
|
||||||
func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
|
func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
|
|
||||||
|
@ -436,14 +415,14 @@ func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
|
||||||
for _, tv := range taxValues {
|
for _, tv := range taxValues {
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
vPath := taxPath + "/" + urlize(tv)
|
vPath := taxPath + "/" + urlize(tv)
|
||||||
if _, ok := taxValueMiddlewares[vPath]; !ok {
|
if _, ok := a.taxValueMiddlewares[vPath]; !ok {
|
||||||
taxValueMiddlewares[vPath] = middleware.WithValue(indexConfigKey, &indexConfig{
|
a.taxValueMiddlewares[vPath] = middleware.WithValue(indexConfigKey, &indexConfig{
|
||||||
path: vPath,
|
path: vPath,
|
||||||
tax: taxonomy,
|
tax: taxonomy,
|
||||||
taxValue: tv,
|
taxValue: tv,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
r.Use(taxValueMiddlewares[vPath])
|
r.Use(a.taxValueMiddlewares[vPath])
|
||||||
r.Get(vPath, a.serveIndex)
|
r.Get(vPath, a.serveIndex)
|
||||||
r.Get(vPath+feedPath, a.serveIndex)
|
r.Get(vPath+feedPath, a.serveIndex)
|
||||||
r.Get(vPath+paginationPath, a.serveIndex)
|
r.Get(vPath+paginationPath, a.serveIndex)
|
||||||
|
@ -512,10 +491,9 @@ func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(a.privateModeHandler...)
|
r.Use(a.privateModeHandler...)
|
||||||
r.Use(sbm)
|
r.Use(sbm)
|
||||||
blogBasePath := blogConfig.getRelativePath("")
|
r.With(a.checkActivityStreamsRequest, a.cache.cacheMiddleware).Get(blogConfig.getRelativePath(""), a.serveHome)
|
||||||
r.With(a.checkActivityStreamsRequest, a.cache.cacheMiddleware).Get(blogBasePath, a.serveHome)
|
r.With(a.cache.cacheMiddleware).Get(blogConfig.getRelativePath("")+feedPath, a.serveHome)
|
||||||
r.With(a.cache.cacheMiddleware).Get(blogBasePath+feedPath, a.serveHome)
|
r.With(a.cache.cacheMiddleware).Get(blogConfig.getRelativePath(paginationPath), a.serveHome)
|
||||||
r.With(a.cache.cacheMiddleware).Get(blogBasePath+paginationPath, a.serveHome)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -579,17 +557,15 @@ func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
|
||||||
const blogContextKey requestContextKey = "blog"
|
const blogContextKey requestContextKey = "blog"
|
||||||
const pathContextKey requestContextKey = "httpPath"
|
const pathContextKey requestContextKey = "httpPath"
|
||||||
|
|
||||||
var cspDomains = ""
|
|
||||||
|
|
||||||
func (a *goBlog) refreshCSPDomains() {
|
func (a *goBlog) refreshCSPDomains() {
|
||||||
cspDomains = ""
|
a.cspDomains = ""
|
||||||
if mp := a.cfg.Micropub.MediaStorage; mp != nil && mp.MediaURL != "" {
|
if mp := a.cfg.Micropub.MediaStorage; mp != nil && mp.MediaURL != "" {
|
||||||
if u, err := url.Parse(mp.MediaURL); err == nil {
|
if u, err := url.Parse(mp.MediaURL); err == nil {
|
||||||
cspDomains += " " + u.Hostname()
|
a.cspDomains += " " + u.Hostname()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(a.cfg.Server.CSPDomains) > 0 {
|
if len(a.cfg.Server.CSPDomains) > 0 {
|
||||||
cspDomains += " " + strings.Join(a.cfg.Server.CSPDomains, " ")
|
a.cspDomains += " " + strings.Join(a.cfg.Server.CSPDomains, " ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -601,7 +577,7 @@ func (a *goBlog) securityHeaders(next http.Handler) http.Handler {
|
||||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||||
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
|
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
|
||||||
w.Header().Set("X-Xss-Protection", "1; mode=block")
|
w.Header().Set("X-Xss-Protection", "1; mode=block")
|
||||||
w.Header().Set("Content-Security-Policy", "default-src 'self'"+cspDomains)
|
w.Header().Set("Content-Security-Policy", "default-src 'self'"+a.cspDomains)
|
||||||
if a.cfg.Server.Tor && a.torAddress != "" {
|
if a.cfg.Server.Tor && a.torAddress != "" {
|
||||||
w.Header().Set("Onion-Location", fmt.Sprintf("http://%v%v", a.torAddress, r.RequestURI))
|
w.Header().Set("Onion-Location", fmt.Sprintf("http://%v%v", a.torAddress, r.RequestURI))
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
|
||||||
"github.com/spf13/cast"
|
"github.com/spf13/cast"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -137,8 +138,8 @@ func (a *goBlog) indieAuthVerification(w http.ResponseWriter, r *http.Request) {
|
||||||
b, _ := json.Marshal(tokenResponse{
|
b, _ := json.Marshal(tokenResponse{
|
||||||
Me: a.cfg.Server.PublicAddress,
|
Me: a.cfg.Server.PublicAddress,
|
||||||
})
|
})
|
||||||
w.Header().Set(contentType, contentTypeJSONUTF8)
|
w.Header().Set(contentType, contenttype.JSONUTF8)
|
||||||
_, _ = writeMinified(w, contentTypeJSON, b)
|
_, _ = a.min.Write(w, contenttype.JSON, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) indieAuthToken(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) indieAuthToken(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -155,8 +156,8 @@ func (a *goBlog) indieAuthToken(w http.ResponseWriter, r *http.Request) {
|
||||||
ClientID: data.ClientID,
|
ClientID: data.ClientID,
|
||||||
}
|
}
|
||||||
b, _ := json.Marshal(res)
|
b, _ := json.Marshal(res)
|
||||||
w.Header().Set(contentType, contentTypeJSONUTF8)
|
w.Header().Set(contentType, contenttype.JSONUTF8)
|
||||||
_, _ = writeMinified(w, contentTypeJSON, b)
|
_, _ = a.min.Write(w, contenttype.JSON, b)
|
||||||
return
|
return
|
||||||
} else if r.Method == http.MethodPost {
|
} else if r.Method == http.MethodPost {
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
|
@ -208,8 +209,8 @@ func (a *goBlog) indieAuthToken(w http.ResponseWriter, r *http.Request) {
|
||||||
Me: a.cfg.Server.PublicAddress,
|
Me: a.cfg.Server.PublicAddress,
|
||||||
}
|
}
|
||||||
b, _ := json.Marshal(res)
|
b, _ := json.Marshal(res)
|
||||||
w.Header().Set(contentType, contentTypeJSONUTF8)
|
w.Header().Set(contentType, contenttype.JSONUTF8)
|
||||||
_, _ = writeMinified(w, contentTypeJSON, b)
|
_, _ = a.min.Write(w, contenttype.JSON, b)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
a.serveError(w, r, "", http.StatusBadRequest)
|
a.serveError(w, r, "", http.StatusBadRequest)
|
||||||
|
|
5
main.go
5
main.go
|
@ -10,13 +10,12 @@ import (
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`")
|
|
||||||
var memprofile = flag.String("memprofile", "", "write memory profile to `file`")
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
// Init CPU and memory profiling
|
// Init CPU and memory profiling
|
||||||
|
cpuprofile := flag.String("cpuprofile", "", "write cpu profile to `file`")
|
||||||
|
memprofile := flag.String("memprofile", "", "write memory profile to `file`")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
if *cpuprofile != "" {
|
if *cpuprofile != "" {
|
||||||
f, err := os.Create(*cpuprofile)
|
f, err := os.Create(*cpuprofile)
|
||||||
|
|
14
markdown.go
14
markdown.go
|
@ -2,6 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"html/template"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
marktag "git.jlel.se/jlelse/goldmark-mark"
|
marktag "git.jlel.se/jlelse/goldmark-mark"
|
||||||
|
@ -54,6 +55,19 @@ func (a *goBlog) renderMarkdown(source string, absoluteLinks bool) (rendered []b
|
||||||
return buffer.Bytes(), err
|
return buffer.Bytes(), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *goBlog) renderMarkdownAsHTML(source string, absoluteLinks bool) (rendered template.HTML, err error) {
|
||||||
|
b, err := a.renderMarkdown(source, absoluteLinks)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return template.HTML(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *goBlog) safeRenderMarkdownAsHTML(source string) template.HTML {
|
||||||
|
h, _ := a.renderMarkdownAsHTML(source, false)
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
func (a *goBlog) renderText(s string) string {
|
func (a *goBlog) renderText(s string) string {
|
||||||
h, err := a.renderMarkdown(s, false)
|
h, err := a.renderMarkdown(s, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -4,6 +4,8 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_markdown(t *testing.T) {
|
func Test_markdown(t *testing.T) {
|
||||||
|
@ -65,6 +67,11 @@ func Test_markdown(t *testing.T) {
|
||||||
if renderedText != "This is text" {
|
if renderedText != "This is text" {
|
||||||
t.Errorf("Wrong result, got \"%v\"", renderedText)
|
t.Errorf("Wrong result, got \"%v\"", renderedText)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Template func
|
||||||
|
|
||||||
|
renderedText = string(app.safeRenderMarkdownAsHTML("[Relative](/relative)"))
|
||||||
|
assert.Contains(t, renderedText, `href="/relative"`)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,8 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultCompressionWidth = 2000
|
const defaultCompressionWidth = 2000
|
||||||
|
@ -34,7 +36,7 @@ func (a *goBlog) tinify(url string, config *configMicropubMedia) (location strin
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
req.SetBasicAuth("api", config.TinifyKey)
|
req.SetBasicAuth("api", config.TinifyKey)
|
||||||
req.Header.Set(contentType, contentTypeJSON)
|
req.Header.Set(contentType, contenttype.JSON)
|
||||||
resp, err := appHttpClient.Do(req)
|
resp, err := appHttpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
@ -61,7 +63,7 @@ func (a *goBlog) tinify(url string, config *configMicropubMedia) (location strin
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
downloadReq.SetBasicAuth("api", config.TinifyKey)
|
downloadReq.SetBasicAuth("api", config.TinifyKey)
|
||||||
downloadReq.Header.Set(contentType, contentTypeJSON)
|
downloadReq.Header.Set(contentType, contenttype.JSON)
|
||||||
downloadResp, err := appHttpClient.Do(downloadReq)
|
downloadResp, err := appHttpClient.Do(downloadReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|
19
micropub.go
19
micropub.go
|
@ -12,6 +12,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
|
||||||
"github.com/spf13/cast"
|
"github.com/spf13/cast"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
@ -25,12 +26,12 @@ type micropubConfig struct {
|
||||||
func (a *goBlog) serveMicropubQuery(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveMicropubQuery(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.URL.Query().Get("q") {
|
switch r.URL.Query().Get("q") {
|
||||||
case "config":
|
case "config":
|
||||||
w.Header().Set(contentType, contentTypeJSONUTF8)
|
w.Header().Set(contentType, contenttype.JSONUTF8)
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
b, _ := json.Marshal(µpubConfig{
|
b, _ := json.Marshal(µpubConfig{
|
||||||
MediaEndpoint: a.getFullAddress(micropubPath + micropubMediaSubPath),
|
MediaEndpoint: a.getFullAddress(micropubPath + micropubMediaSubPath),
|
||||||
})
|
})
|
||||||
_, _ = writeMinified(w, contentTypeJSON, b)
|
_, _ = a.min.Write(w, contenttype.JSON, b)
|
||||||
case "source":
|
case "source":
|
||||||
var mf interface{}
|
var mf interface{}
|
||||||
if urlString := r.URL.Query().Get("url"); urlString != "" {
|
if urlString := r.URL.Query().Get("url"); urlString != "" {
|
||||||
|
@ -62,10 +63,10 @@ func (a *goBlog) serveMicropubQuery(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
mf = list
|
mf = list
|
||||||
}
|
}
|
||||||
w.Header().Set(contentType, contentTypeJSONUTF8)
|
w.Header().Set(contentType, contenttype.JSONUTF8)
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
b, _ := json.Marshal(mf)
|
b, _ := json.Marshal(mf)
|
||||||
_, _ = writeMinified(w, contentTypeJSON, b)
|
_, _ = a.min.Write(w, contenttype.JSON, b)
|
||||||
case "category":
|
case "category":
|
||||||
allCategories := []string{}
|
allCategories := []string{}
|
||||||
for blog := range a.cfg.Blogs {
|
for blog := range a.cfg.Blogs {
|
||||||
|
@ -76,12 +77,12 @@ func (a *goBlog) serveMicropubQuery(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
allCategories = append(allCategories, values...)
|
allCategories = append(allCategories, values...)
|
||||||
}
|
}
|
||||||
w.Header().Set(contentType, contentTypeJSONUTF8)
|
w.Header().Set(contentType, contenttype.JSONUTF8)
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
b, _ := json.Marshal(map[string]interface{}{
|
b, _ := json.Marshal(map[string]interface{}{
|
||||||
"categories": allCategories,
|
"categories": allCategories,
|
||||||
})
|
})
|
||||||
_, _ = writeMinified(w, contentTypeJSON, b)
|
_, _ = a.min.Write(w, contenttype.JSON, b)
|
||||||
default:
|
default:
|
||||||
a.serve404(w, r)
|
a.serve404(w, r)
|
||||||
}
|
}
|
||||||
|
@ -120,9 +121,9 @@ func (a *goBlog) toMfItem(p *post) *microformatItem {
|
||||||
func (a *goBlog) serveMicropubPost(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveMicropubPost(w http.ResponseWriter, r *http.Request) {
|
||||||
defer r.Body.Close()
|
defer r.Body.Close()
|
||||||
var p *post
|
var p *post
|
||||||
if ct := r.Header.Get(contentType); strings.Contains(ct, contentTypeWWWForm) || strings.Contains(ct, contentTypeMultipartForm) {
|
if ct := r.Header.Get(contentType); strings.Contains(ct, contenttype.WWWForm) || strings.Contains(ct, contenttype.MultipartForm) {
|
||||||
var err error
|
var err error
|
||||||
if strings.Contains(ct, contentTypeMultipartForm) {
|
if strings.Contains(ct, contenttype.MultipartForm) {
|
||||||
err = r.ParseMultipartForm(0)
|
err = r.ParseMultipartForm(0)
|
||||||
} else {
|
} else {
|
||||||
err = r.ParseForm()
|
err = r.ParseForm()
|
||||||
|
@ -149,7 +150,7 @@ func (a *goBlog) serveMicropubPost(w http.ResponseWriter, r *http.Request) {
|
||||||
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else if strings.Contains(ct, contentTypeJSON) {
|
} else if strings.Contains(ct, contenttype.JSON) {
|
||||||
parsedMfItem := µformatItem{}
|
parsedMfItem := µformatItem{}
|
||||||
err := json.NewDecoder(r.Body).Decode(parsedMfItem)
|
err := json.NewDecoder(r.Body).Decode(parsedMfItem)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -11,6 +11,8 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
|
||||||
)
|
)
|
||||||
|
|
||||||
const micropubMediaSubPath = "/media"
|
const micropubMediaSubPath = "/media"
|
||||||
|
@ -20,7 +22,7 @@ func (a *goBlog) serveMicropubMedia(w http.ResponseWriter, r *http.Request) {
|
||||||
a.serveError(w, r, "media scope missing", http.StatusForbidden)
|
a.serveError(w, r, "media scope missing", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if ct := r.Header.Get(contentType); !strings.Contains(ct, contentTypeMultipartForm) {
|
if ct := r.Header.Get(contentType); !strings.Contains(ct, contenttype.MultipartForm) {
|
||||||
a.serveError(w, r, "wrong content-type", http.StatusBadRequest)
|
a.serveError(w, r, "wrong content-type", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
39
minify.go
39
minify.go
|
@ -1,39 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/tdewolff/minify/v2"
|
|
||||||
mCss "github.com/tdewolff/minify/v2/css"
|
|
||||||
mHtml "github.com/tdewolff/minify/v2/html"
|
|
||||||
mJs "github.com/tdewolff/minify/v2/js"
|
|
||||||
mJson "github.com/tdewolff/minify/v2/json"
|
|
||||||
mXml "github.com/tdewolff/minify/v2/xml"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
initMinify sync.Once
|
|
||||||
minifier *minify.M
|
|
||||||
)
|
|
||||||
|
|
||||||
func getMinifier() *minify.M {
|
|
||||||
initMinify.Do(func() {
|
|
||||||
minifier = minify.New()
|
|
||||||
minifier.AddFunc(contentTypeHTML, mHtml.Minify)
|
|
||||||
minifier.AddFunc("text/css", mCss.Minify)
|
|
||||||
minifier.AddFunc(contentTypeXML, mXml.Minify)
|
|
||||||
minifier.AddFunc("application/javascript", mJs.Minify)
|
|
||||||
minifier.AddFunc(contentTypeRSS, mXml.Minify)
|
|
||||||
minifier.AddFunc(contentTypeATOM, mXml.Minify)
|
|
||||||
minifier.AddFunc(contentTypeJSONFeed, mJson.Minify)
|
|
||||||
minifier.AddFunc(contentTypeAS, mJson.Minify)
|
|
||||||
})
|
|
||||||
return minifier
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeMinified(w io.Writer, mediatype string, b []byte) (int, error) {
|
|
||||||
mw := getMinifier().Writer(mediatype, w)
|
|
||||||
defer func() { _ = mw.Close() }()
|
|
||||||
return mw.Write(b)
|
|
||||||
}
|
|
10
nodeinfo.go
10
nodeinfo.go
|
@ -3,6 +3,8 @@ package main
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *goBlog) serveNodeInfoDiscover(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveNodeInfoDiscover(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -14,8 +16,8 @@ func (a *goBlog) serveNodeInfoDiscover(w http.ResponseWriter, r *http.Request) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
w.Header().Set(contentType, contentTypeJSONUTF8)
|
w.Header().Set(contentType, contenttype.JSONUTF8)
|
||||||
_, _ = writeMinified(w, contentTypeJSON, b)
|
_, _ = a.min.Write(w, contenttype.JSON, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) serveNodeInfo(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveNodeInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -41,6 +43,6 @@ func (a *goBlog) serveNodeInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
},
|
},
|
||||||
"metadata": map[string]interface{}{},
|
"metadata": map[string]interface{}{},
|
||||||
})
|
})
|
||||||
w.Header().Set(contentType, contentTypeJSONUTF8)
|
w.Header().Set(contentType, contenttype.JSONUTF8)
|
||||||
_, _ = writeMinified(w, contentTypeJSON, b)
|
_, _ = a.min.Write(w, contenttype.JSON, b)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ package main
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *goBlog) serveOpenSearch(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveOpenSearch(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -17,7 +19,7 @@ func (a *goBlog) serveOpenSearch(w http.ResponseWriter, r *http.Request) {
|
||||||
"</OpenSearchDescription>",
|
"</OpenSearchDescription>",
|
||||||
title, title, sURL, sURL)
|
title, title, sURL, sURL)
|
||||||
w.Header().Set(contentType, "application/opensearchdescription+xml")
|
w.Header().Set(contentType, "application/opensearchdescription+xml")
|
||||||
_, _ = writeMinified(w, contentTypeXML, []byte(xml))
|
_, _ = a.min.Write(w, contenttype.XML, []byte(xml))
|
||||||
}
|
}
|
||||||
|
|
||||||
func openSearchUrl(b *configBlog) string {
|
func openSearchUrl(b *configBlog) string {
|
||||||
|
|
6
paths.go
6
paths.go
|
@ -43,3 +43,9 @@ func (cfg *configServer) getFullAddress(path string) string {
|
||||||
}
|
}
|
||||||
return pa + path
|
return pa + path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rendering funcs
|
||||||
|
|
||||||
|
func (blog *configBlog) RelativePath(path string) string {
|
||||||
|
return blog.getRelativePath(path)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
package contenttype
|
||||||
|
|
||||||
|
// This package contains constants for a few content types used in GoBlog
|
||||||
|
|
||||||
|
const (
|
||||||
|
CharsetUtf8Suffix = "; charset=utf-8"
|
||||||
|
|
||||||
|
AS = "application/activity+json"
|
||||||
|
ATOM = "application/atom+xml"
|
||||||
|
CSS = "text/css"
|
||||||
|
HTML = "text/html"
|
||||||
|
JS = "application/javascript"
|
||||||
|
JSON = "application/json"
|
||||||
|
JSONFeed = "application/feed+json"
|
||||||
|
LDJSON = "application/ld+json"
|
||||||
|
MultipartForm = "multipart/form-data"
|
||||||
|
RSS = "application/rss+xml"
|
||||||
|
WWWForm = "application/x-www-form-urlencoded"
|
||||||
|
XML = "text/xml"
|
||||||
|
|
||||||
|
ASUTF8 = AS + CharsetUtf8Suffix
|
||||||
|
HTMLUTF8 = HTML + CharsetUtf8Suffix
|
||||||
|
JSONUTF8 = JSON + CharsetUtf8Suffix
|
||||||
|
XMLUTF8 = XML + CharsetUtf8Suffix
|
||||||
|
)
|
|
@ -0,0 +1,45 @@
|
||||||
|
package minify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
|
||||||
|
"github.com/tdewolff/minify/v2"
|
||||||
|
mCss "github.com/tdewolff/minify/v2/css"
|
||||||
|
mHtml "github.com/tdewolff/minify/v2/html"
|
||||||
|
mJs "github.com/tdewolff/minify/v2/js"
|
||||||
|
mJson "github.com/tdewolff/minify/v2/json"
|
||||||
|
mXml "github.com/tdewolff/minify/v2/xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Minifier struct {
|
||||||
|
i sync.Once
|
||||||
|
m *minify.M
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Minifier) init() {
|
||||||
|
m.i.Do(func() {
|
||||||
|
m.m = minify.New()
|
||||||
|
m.m.AddFunc(contenttype.HTML, mHtml.Minify)
|
||||||
|
m.m.AddFunc(contenttype.CSS, mCss.Minify)
|
||||||
|
m.m.AddFunc(contenttype.XML, mXml.Minify)
|
||||||
|
m.m.AddFunc(contenttype.JS, mJs.Minify)
|
||||||
|
m.m.AddFunc(contenttype.RSS, mXml.Minify)
|
||||||
|
m.m.AddFunc(contenttype.ATOM, mXml.Minify)
|
||||||
|
m.m.AddFunc(contenttype.JSONFeed, mJson.Minify)
|
||||||
|
m.m.AddFunc(contenttype.AS, mJson.Minify)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Minifier) Get() *minify.M {
|
||||||
|
m.init()
|
||||||
|
return m.m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Minifier) Write(w io.Writer, mediatype string, b []byte) (int, error) {
|
||||||
|
m.init()
|
||||||
|
mw := m.m.Writer(mediatype, w)
|
||||||
|
defer func() { _ = mw.Close() }()
|
||||||
|
return mw.Write(b)
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package minify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_minify(t *testing.T) {
|
||||||
|
var min Minifier
|
||||||
|
assert.NotNil(t, min.Get())
|
||||||
|
}
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -122,8 +121,6 @@ type postCreationOptions struct {
|
||||||
oldStatus postStatus
|
oldStatus postStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
var postCreationMutex sync.Mutex
|
|
||||||
|
|
||||||
func (a *goBlog) createOrReplacePost(p *post, o *postCreationOptions) error {
|
func (a *goBlog) createOrReplacePost(p *post, o *postCreationOptions) error {
|
||||||
// Check post
|
// Check post
|
||||||
if err := a.checkPost(p); err != nil {
|
if err := a.checkPost(p); err != nil {
|
||||||
|
@ -148,8 +145,8 @@ func (a *goBlog) createOrReplacePost(p *post, o *postCreationOptions) error {
|
||||||
// Save check post to database
|
// Save check post to database
|
||||||
func (db *database) savePost(p *post, o *postCreationOptions) error {
|
func (db *database) savePost(p *post, o *postCreationOptions) error {
|
||||||
// Prevent bad things
|
// Prevent bad things
|
||||||
postCreationMutex.Lock()
|
db.pcm.Lock()
|
||||||
defer postCreationMutex.Unlock()
|
defer db.pcm.Unlock()
|
||||||
// Check if path is already in use
|
// Check if path is already in use
|
||||||
if o.new || (p.Path != o.oldPath) {
|
if o.new || (p.Path != o.oldPath) {
|
||||||
// Post is new or post path was changed
|
// Post is new or post path was changed
|
||||||
|
|
|
@ -53,7 +53,7 @@ func Test_postsDb(t *testing.T) {
|
||||||
is.Equal("en", p.Blog)
|
is.Equal("en", p.Blog)
|
||||||
is.Equal("test", p.Section)
|
is.Equal("test", p.Section)
|
||||||
is.Equal(statusDraft, p.Status)
|
is.Equal(statusDraft, p.Status)
|
||||||
is.Equal("Title", p.title())
|
is.Equal("Title", p.Title())
|
||||||
|
|
||||||
// Check number of post paths
|
// Check number of post paths
|
||||||
pp, err := app.db.allPostPaths(statusDraft)
|
pp, err := app.db.allPostPaths(statusDraft)
|
||||||
|
|
|
@ -4,8 +4,11 @@ import (
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
gogeouri "git.jlel.se/jlelse/go-geouri"
|
||||||
"github.com/PuerkitoBio/goquery"
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
"github.com/araddon/dateparse"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *goBlog) fullPostURL(p *post) string {
|
func (a *goBlog) fullPostURL(p *post) string {
|
||||||
|
@ -23,6 +26,14 @@ func (a *goBlog) shortPostURL(p *post) string {
|
||||||
return a.getFullAddress(s)
|
return a.getFullAddress(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func postParameter(p *post, parameter string) []string {
|
||||||
|
return p.Parameters[parameter]
|
||||||
|
}
|
||||||
|
|
||||||
|
func postHasParameter(p *post, parameter string) bool {
|
||||||
|
return len(p.Parameters[parameter]) > 0
|
||||||
|
}
|
||||||
|
|
||||||
func (p *post) firstParameter(parameter string) (result string) {
|
func (p *post) firstParameter(parameter string) (result string) {
|
||||||
if pp := p.Parameters[parameter]; len(pp) > 0 {
|
if pp := p.Parameters[parameter]; len(pp) > 0 {
|
||||||
result = pp[0]
|
result = pp[0]
|
||||||
|
@ -30,11 +41,11 @@ func (p *post) firstParameter(parameter string) (result string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *post) title() string {
|
func firstPostParameter(p *post, parameter string) string {
|
||||||
return p.firstParameter("title")
|
return p.firstParameter(parameter)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) html(p *post) template.HTML {
|
func (a *goBlog) postHtml(p *post) template.HTML {
|
||||||
if p.rendered != "" {
|
if p.rendered != "" {
|
||||||
return p.rendered
|
return p.rendered
|
||||||
}
|
}
|
||||||
|
@ -47,7 +58,7 @@ func (a *goBlog) html(p *post) template.HTML {
|
||||||
return p.rendered
|
return p.rendered
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) absoluteHTML(p *post) template.HTML {
|
func (a *goBlog) absolutePostHTML(p *post) template.HTML {
|
||||||
if p.absoluteRendered != "" {
|
if p.absoluteRendered != "" {
|
||||||
return p.absoluteRendered
|
return p.absoluteRendered
|
||||||
}
|
}
|
||||||
|
@ -62,12 +73,12 @@ func (a *goBlog) absoluteHTML(p *post) template.HTML {
|
||||||
|
|
||||||
const summaryDivider = "<!--more-->"
|
const summaryDivider = "<!--more-->"
|
||||||
|
|
||||||
func (a *goBlog) summary(p *post) (summary string) {
|
func (a *goBlog) postSummary(p *post) (summary string) {
|
||||||
summary = p.firstParameter("summary")
|
summary = p.firstParameter("summary")
|
||||||
if summary != "" {
|
if summary != "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
html := string(a.html(p))
|
html := string(a.postHtml(p))
|
||||||
if splitted := strings.Split(html, summaryDivider); len(splitted) > 1 {
|
if splitted := strings.Split(html, summaryDivider); len(splitted) > 1 {
|
||||||
doc, _ := goquery.NewDocumentFromReader(strings.NewReader(splitted[0]))
|
doc, _ := goquery.NewDocumentFromReader(strings.NewReader(splitted[0]))
|
||||||
summary = doc.Text()
|
summary = doc.Text()
|
||||||
|
@ -78,7 +89,7 @@ func (a *goBlog) summary(p *post) (summary string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) translations(p *post) []*post {
|
func (a *goBlog) postTranslations(p *post) []*post {
|
||||||
translationkey := p.firstParameter("translationkey")
|
translationkey := p.firstParameter("translationkey")
|
||||||
if translationkey == "" {
|
if translationkey == "" {
|
||||||
return nil
|
return nil
|
||||||
|
@ -105,3 +116,30 @@ func (a *goBlog) translations(p *post) []*post {
|
||||||
func (p *post) isPublishedSectionPost() bool {
|
func (p *post) isPublishedSectionPost() bool {
|
||||||
return p.Published != "" && p.Section != "" && p.Status == statusPublished
|
return p.Published != "" && p.Section != "" && p.Status == statusPublished
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Public because of rendering
|
||||||
|
|
||||||
|
func (p *post) Title() string {
|
||||||
|
return p.firstParameter("title")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *post) GeoURI() *gogeouri.Geo {
|
||||||
|
loc := p.firstParameter("location")
|
||||||
|
if loc == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
g, _ := gogeouri.Parse(loc)
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *post) Old() bool {
|
||||||
|
pub := p.Published
|
||||||
|
if pub == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
pubDate, err := dateparse.ParseLocal(pub)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return pubDate.AddDate(1, 0, 0).Before(time.Now())
|
||||||
|
}
|
||||||
|
|
174
render.go
174
render.go
|
@ -2,21 +2,16 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
gogeouri "git.jlel.se/jlelse/go-geouri"
|
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
|
||||||
"github.com/araddon/dateparse"
|
|
||||||
servertiming "github.com/mitchellh/go-server-timing"
|
servertiming "github.com/mitchellh/go-server-timing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -48,142 +43,32 @@ const (
|
||||||
func (a *goBlog) initRendering() error {
|
func (a *goBlog) initRendering() error {
|
||||||
a.templates = map[string]*template.Template{}
|
a.templates = map[string]*template.Template{}
|
||||||
templateFunctions := template.FuncMap{
|
templateFunctions := template.FuncMap{
|
||||||
"menu": func(blog *configBlog, id string) *menu {
|
"md": a.safeRenderMarkdownAsHTML,
|
||||||
return blog.Menus[id]
|
"html": wrapStringAsHTML,
|
||||||
},
|
|
||||||
"user": func() *configUser {
|
|
||||||
return a.cfg.User
|
|
||||||
},
|
|
||||||
"md": func(content string) template.HTML {
|
|
||||||
htmlContent, err := a.renderMarkdown(content, false)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return template.HTML(htmlContent)
|
|
||||||
},
|
|
||||||
"html": func(s string) template.HTML {
|
|
||||||
return template.HTML(s)
|
|
||||||
},
|
|
||||||
// Post specific
|
// Post specific
|
||||||
"p": func(p *post, parameter string) string {
|
"p": firstPostParameter,
|
||||||
return p.firstParameter(parameter)
|
"ps": postParameter,
|
||||||
},
|
"hasp": postHasParameter,
|
||||||
"ps": func(p *post, parameter string) []string {
|
"content": a.postHtml,
|
||||||
return p.Parameters[parameter]
|
"summary": a.postSummary,
|
||||||
},
|
"translations": a.postTranslations,
|
||||||
"hasp": func(p *post, parameter string) bool {
|
"shorturl": a.shortPostURL,
|
||||||
return len(p.Parameters[parameter]) > 0
|
|
||||||
},
|
|
||||||
"title": func(p *post) string {
|
|
||||||
return p.title()
|
|
||||||
},
|
|
||||||
"content": func(p *post) template.HTML {
|
|
||||||
return a.html(p)
|
|
||||||
},
|
|
||||||
"summary": func(p *post) string {
|
|
||||||
return a.summary(p)
|
|
||||||
},
|
|
||||||
"translations": func(p *post) []*post {
|
|
||||||
return a.translations(p)
|
|
||||||
},
|
|
||||||
"shorturl": func(p *post) string {
|
|
||||||
return a.shortPostURL(p)
|
|
||||||
},
|
|
||||||
// Others
|
// Others
|
||||||
"dateformat": dateFormat,
|
"dateformat": dateFormat,
|
||||||
"isodate": func(date string) string {
|
"isodate": isoDateFormat,
|
||||||
return dateFormat(date, "2006-01-02")
|
"unixtodate": unixToLocalDateString,
|
||||||
},
|
"now": localNowString,
|
||||||
"unixtodate": func(unix int64) string {
|
|
||||||
return time.Unix(unix, 0).Local().String()
|
|
||||||
},
|
|
||||||
"now": func() string {
|
|
||||||
return time.Now().Local().String()
|
|
||||||
},
|
|
||||||
"dateadd": func(date string, years, months, days int) string {
|
|
||||||
d, err := dateparse.ParseLocal(date)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return d.AddDate(years, months, days).Local().String()
|
|
||||||
},
|
|
||||||
"datebefore": func(date string, before string) bool {
|
|
||||||
d, err := dateparse.ParseLocal(date)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
b, err := dateparse.ParseLocal(before)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return d.Before(b)
|
|
||||||
},
|
|
||||||
"asset": a.assetFileName,
|
"asset": a.assetFileName,
|
||||||
"assetsri": a.assetSRI,
|
|
||||||
"string": a.ts.GetTemplateStringVariantFunc(),
|
"string": a.ts.GetTemplateStringVariantFunc(),
|
||||||
"include": func(templateName string, data ...interface{}) (template.HTML, error) {
|
"include": a.includeRenderedTemplate,
|
||||||
if len(data) == 0 || len(data) > 2 {
|
|
||||||
return "", errors.New("wrong argument count")
|
|
||||||
}
|
|
||||||
if rd, ok := data[0].(*renderData); ok {
|
|
||||||
if len(data) == 2 {
|
|
||||||
nrd := *rd
|
|
||||||
nrd.Data = data[1]
|
|
||||||
rd = &nrd
|
|
||||||
}
|
|
||||||
var buf bytes.Buffer
|
|
||||||
err := a.templates[templateName].ExecuteTemplate(&buf, templateName, rd)
|
|
||||||
return template.HTML(buf.String()), err
|
|
||||||
}
|
|
||||||
return "", errors.New("wrong arguments")
|
|
||||||
},
|
|
||||||
"urlize": urlize,
|
"urlize": urlize,
|
||||||
"sort": sortedStrings,
|
"sort": sortedStrings,
|
||||||
"absolute": func(path string) string {
|
"absolute": a.getFullAddress,
|
||||||
return a.getFullAddress(path)
|
"mentions": a.db.getWebmentionsByAddress,
|
||||||
},
|
|
||||||
"blogrelative": func(blog *configBlog, path string) string {
|
|
||||||
return blog.getRelativePath(path)
|
|
||||||
},
|
|
||||||
"jsonFile": func(filename string) *map[string]interface{} {
|
|
||||||
parsed := &map[string]interface{}{}
|
|
||||||
content, err := os.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err = json.Unmarshal(content, parsed)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err.Error())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return parsed
|
|
||||||
},
|
|
||||||
"mentions": func(absolute string) []*mention {
|
|
||||||
mentions, _ := a.db.getWebmentions(&webmentionsRequestConfig{
|
|
||||||
target: absolute,
|
|
||||||
status: webmentionStatusApproved,
|
|
||||||
asc: true,
|
|
||||||
})
|
|
||||||
return mentions
|
|
||||||
},
|
|
||||||
"urlToString": func(u url.URL) string {
|
|
||||||
return u.String()
|
|
||||||
},
|
|
||||||
"geouri": func(u string) *gogeouri.Geo {
|
|
||||||
g, _ := gogeouri.Parse(u)
|
|
||||||
return g
|
|
||||||
},
|
|
||||||
"geourip": func(g *gogeouri.Geo, parameter string) (s string) {
|
|
||||||
if gp := g.Parameters[parameter]; len(gp) > 0 {
|
|
||||||
return gp[0]
|
|
||||||
}
|
|
||||||
return
|
|
||||||
},
|
|
||||||
"geotitle": a.db.geoTitle,
|
"geotitle": a.db.geoTitle,
|
||||||
|
"geolink": geoOSMLink,
|
||||||
"opensearch": openSearchUrl,
|
"opensearch": openSearchUrl,
|
||||||
}
|
}
|
||||||
|
|
||||||
baseTemplate, err := template.New("base").Funcs(templateFunctions).ParseFiles(path.Join(templatesDir, templateBase+templatesExt))
|
baseTemplate, err := template.New("base").Funcs(templateFunctions).ParseFiles(path.Join(templatesDir, templateBase+templatesExt))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -212,6 +97,7 @@ type renderData struct {
|
||||||
Canonical string
|
Canonical string
|
||||||
TorAddress string
|
TorAddress string
|
||||||
Blog *configBlog
|
Blog *configBlog
|
||||||
|
User *configUser
|
||||||
Data interface{}
|
Data interface{}
|
||||||
LoggedIn bool
|
LoggedIn bool
|
||||||
CommentsEnabled bool
|
CommentsEnabled bool
|
||||||
|
@ -223,6 +109,9 @@ func (a *goBlog) render(w http.ResponseWriter, r *http.Request, template string,
|
||||||
// Server timing
|
// Server timing
|
||||||
t := servertiming.FromContext(r.Context()).NewMetric("r").Start()
|
t := servertiming.FromContext(r.Context()).NewMetric("r").Start()
|
||||||
// Check render data
|
// Check render data
|
||||||
|
if data.User == nil {
|
||||||
|
data.User = a.cfg.User
|
||||||
|
}
|
||||||
if data.Blog == nil {
|
if data.Blog == nil {
|
||||||
if len(data.BlogString) == 0 {
|
if len(data.BlogString) == 0 {
|
||||||
data.BlogString = a.cfg.DefaultBlog
|
data.BlogString = a.cfg.DefaultBlog
|
||||||
|
@ -256,7 +145,7 @@ func (a *goBlog) render(w http.ResponseWriter, r *http.Request, template string,
|
||||||
data.TorUsed = true
|
data.TorUsed = true
|
||||||
}
|
}
|
||||||
// Set content type
|
// Set content type
|
||||||
w.Header().Set(contentType, contentTypeHTMLUTF8)
|
w.Header().Set(contentType, contenttype.HTMLUTF8)
|
||||||
// Minify and write response
|
// Minify and write response
|
||||||
var tw bytes.Buffer
|
var tw bytes.Buffer
|
||||||
err := a.templates[template].ExecuteTemplate(&tw, template, data)
|
err := a.templates[template].ExecuteTemplate(&tw, template, data)
|
||||||
|
@ -264,7 +153,7 @@ func (a *goBlog) render(w http.ResponseWriter, r *http.Request, template string,
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, err = writeMinified(w, contentTypeHTML, tw.Bytes())
|
_, err = a.min.Write(w, contenttype.HTML, tw.Bytes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
@ -272,3 +161,20 @@ func (a *goBlog) render(w http.ResponseWriter, r *http.Request, template string,
|
||||||
// Server timing
|
// Server timing
|
||||||
t.Stop()
|
t.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *goBlog) includeRenderedTemplate(templateName string, data ...interface{}) (template.HTML, error) {
|
||||||
|
if l := len(data); l < 1 || l > 2 {
|
||||||
|
return "", errors.New("wrong argument count")
|
||||||
|
}
|
||||||
|
if rd, ok := data[0].(*renderData); ok {
|
||||||
|
if len(data) == 2 {
|
||||||
|
nrd := *rd
|
||||||
|
nrd.Data = data[1]
|
||||||
|
rd = &nrd
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := a.templates[templateName].ExecuteTemplate(&buf, templateName, rd)
|
||||||
|
return template.HTML(buf.String()), err
|
||||||
|
}
|
||||||
|
return "", errors.New("wrong arguments")
|
||||||
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ func (a *goBlog) initSessions() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
deleteExpiredSessions()
|
deleteExpiredSessions()
|
||||||
hourlyHooks = append(hourlyHooks, deleteExpiredSessions)
|
a.hourlyHooks = append(a.hourlyHooks, deleteExpiredSessions)
|
||||||
a.loginSessions = &dbSessionStore{
|
a.loginSessions = &dbSessionStore{
|
||||||
codecs: securecookie.CodecsFromPairs(a.jwtKey()),
|
codecs: securecookie.CodecsFromPairs(a.jwtKey()),
|
||||||
options: &sessions.Options{
|
options: &sessions.Options{
|
||||||
|
|
|
@ -16,7 +16,7 @@ const telegramBaseURL = "https://api.telegram.org/bot"
|
||||||
func (a *goBlog) initTelegram() {
|
func (a *goBlog) initTelegram() {
|
||||||
a.pPostHooks = append(a.pPostHooks, func(p *post) {
|
a.pPostHooks = append(a.pPostHooks, func(p *post) {
|
||||||
if tg := a.cfg.Blogs[p.Blog].Telegram; tg.enabled() && p.isPublishedSectionPost() {
|
if tg := a.cfg.Blogs[p.Blog].Telegram; tg.enabled() && p.isPublishedSectionPost() {
|
||||||
if html := tg.generateHTML(p.title(), a.fullPostURL(p), a.shortPostURL(p)); html != "" {
|
if html := tg.generateHTML(p.Title(), a.fullPostURL(p), a.shortPostURL(p)); html != "" {
|
||||||
if err := tg.send(html, "HTML"); err != nil {
|
if err := tg.send(html, "HTML"); err != nil {
|
||||||
log.Printf("Failed to send post to Telegram: %v", err)
|
log.Printf("Failed to send post to Telegram: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,23 +2,21 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"crypto/sha512"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
|
||||||
)
|
)
|
||||||
|
|
||||||
const assetsFolder = "templates/assets"
|
const assetsFolder = "templates/assets"
|
||||||
|
|
||||||
type assetFile struct {
|
type assetFile struct {
|
||||||
contentType string
|
contentType string
|
||||||
sri string
|
|
||||||
body []byte
|
body []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +48,7 @@ func (a *goBlog) compileAsset(name string) (string, error) {
|
||||||
}
|
}
|
||||||
ext := path.Ext(name)
|
ext := path.Ext(name)
|
||||||
compiledExt := ext
|
compiledExt := ext
|
||||||
m := getMinifier()
|
m := a.min.Get()
|
||||||
switch ext {
|
switch ext {
|
||||||
case ".js":
|
case ".js":
|
||||||
content, err = m.Bytes("application/javascript", content)
|
content, err = m.Bytes("application/javascript", content)
|
||||||
|
@ -67,18 +65,14 @@ func (a *goBlog) compileAsset(name string) (string, error) {
|
||||||
}
|
}
|
||||||
// Hashes
|
// Hashes
|
||||||
sha1Hash := sha1.New()
|
sha1Hash := sha1.New()
|
||||||
sha512Hash := sha512.New()
|
if _, err := sha1Hash.Write(content); err != nil {
|
||||||
if _, err := io.MultiWriter(sha1Hash, sha512Hash).Write(content); err != nil {
|
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
// File name
|
// File name
|
||||||
compiledFileName := fmt.Sprintf("%x", sha1Hash.Sum(nil)) + compiledExt
|
compiledFileName := fmt.Sprintf("%x", sha1Hash.Sum(nil)) + compiledExt
|
||||||
// SRI
|
|
||||||
sriHash := fmt.Sprintf("sha512-%s", base64.StdEncoding.EncodeToString(sha512Hash.Sum(nil)))
|
|
||||||
// Create struct
|
// Create struct
|
||||||
a.assetFiles[compiledFileName] = &assetFile{
|
a.assetFiles[compiledFileName] = &assetFile{
|
||||||
contentType: mime.TypeByExtension(compiledExt),
|
contentType: mime.TypeByExtension(compiledExt),
|
||||||
sri: sriHash,
|
|
||||||
body: content,
|
body: content,
|
||||||
}
|
}
|
||||||
return compiledFileName, err
|
return compiledFileName, err
|
||||||
|
@ -89,10 +83,6 @@ func (a *goBlog) assetFileName(fileName string) string {
|
||||||
return "/" + a.assetFileNames[fileName]
|
return "/" + a.assetFileNames[fileName]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) assetSRI(fileName string) string {
|
|
||||||
return a.assetFiles[a.assetFileNames[fileName]].sri
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *goBlog) allAssetPaths() []string {
|
func (a *goBlog) allAssetPaths() []string {
|
||||||
var paths []string
|
var paths []string
|
||||||
for _, name := range a.assetFileNames {
|
for _, name := range a.assetFileNames {
|
||||||
|
@ -109,6 +99,6 @@ func (a *goBlog) serveAsset(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Cache-Control", "public,max-age=31536000,immutable")
|
w.Header().Set("Cache-Control", "public,max-age=31536000,immutable")
|
||||||
w.Header().Set(contentType, af.contentType+charsetUtf8Suffix)
|
w.Header().Set(contentType, af.contentType+contenttype.CharsetUtf8Suffix)
|
||||||
_, _ = w.Write(af.body)
|
_, _ = w.Write(af.body)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{{ define "author" }}
|
{{ define "author" }}
|
||||||
{{ with user }}
|
{{ with .User }}
|
||||||
<div class="p-author h-card hide">
|
<div class="p-author h-card hide">
|
||||||
{{ with .Picture }}<data class="u-photo" value="{{ . }}"></data>{{ end }}
|
{{ with .Picture }}<data class="u-photo" value="{{ . }}"></data>{{ end }}
|
||||||
{{ if .Name }}
|
{{ if .Name }}
|
||||||
|
|
|
@ -15,11 +15,7 @@
|
||||||
<link rel="micropub" href="/micropub" />
|
<link rel="micropub" href="/micropub" />
|
||||||
<link rel="authorization_endpoint" href="/indieauth" />
|
<link rel="authorization_endpoint" href="/indieauth" />
|
||||||
<link rel="token_endpoint" href="/indieauth/token" />
|
<link rel="token_endpoint" href="/indieauth/token" />
|
||||||
{{ with user }}
|
{{ with .User }}{{ range .Identities }}<link rel="me" href="{{ . }}" />{{ end }}{{ end }}
|
||||||
{{ range .Identities }}
|
|
||||||
<link rel="me" href="{{ . }}" />
|
|
||||||
{{ end }}
|
|
||||||
{{ end }}
|
|
||||||
{{ $os := opensearch .Blog }}
|
{{ $os := opensearch .Blog }}
|
||||||
{{ if $os }}
|
{{ if $os }}
|
||||||
<link rel="search" type="application/opensearchdescription+xml" href="{{ $os }}" title="{{ .Blog.Title }}" />
|
<link rel="search" type="application/opensearchdescription+xml" href="{{ $os }}" title="{{ .Blog.Title }}" />
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<main>
|
<main>
|
||||||
{{ with .Data.Title }}<h1>{{ . }}</h1>{{ end }}
|
{{ with .Data.Title }}<h1>{{ . }}</h1>{{ end }}
|
||||||
{{ with .Data.Description }}{{ md . }}{{ end }}
|
{{ with .Data.Description }}{{ md . }}{{ end }}
|
||||||
<p><a href="{{ blogrelative .Blog .Data.Download }}" class="button" download>{{ string .Blog.Lang "download" }}</a></p>
|
<p><a href="{{ .Blog.RelativePath .Data.Download }}" class="button" download>{{ string .Blog.Lang "download" }}</a></p>
|
||||||
{{ $lang := .Blog.Lang }}
|
{{ $lang := .Blog.Lang }}
|
||||||
{{ range .Data.Outlines }}
|
{{ range .Data.Outlines }}
|
||||||
{{ $title := .Title }}
|
{{ $title := .Title }}
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
{{ range .Outlines }}
|
{{ range .Outlines }}
|
||||||
{{ $ct := .Title }}
|
{{ $ct := .Title }}
|
||||||
{{ if not $ct }}{{ $ct = .Text }}{{ end }}
|
{{ if not $ct }}{{ $ct = .Text }}{{ end }}
|
||||||
<li><a href="{{ urlToString .HTMLURL }}" target="_blank">{{ $ct }}</a> (<a href="{{ urlToString .XMLURL }}" target="_blank">{{ string $lang "feed" }}</a>)</li>
|
<li><a href="{{ .HTMLURL }}" target="_blank">{{ $ct }}</a> (<a href="{{ .XMLURL }}" target="_blank">{{ string $lang "feed" }}</a>)</li>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</ul>
|
</ul>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -55,7 +55,7 @@ tags:
|
||||||
<input type="hidden" name="editoraction" value="loadupdate">
|
<input type="hidden" name="editoraction" value="loadupdate">
|
||||||
<select name="url" class="fw">
|
<select name="url" class="fw">
|
||||||
{{ range $i, $draft := .Data.Drafts }}
|
{{ range $i, $draft := .Data.Drafts }}
|
||||||
<option value="{{ absolute $draft.Path }}">{{ with (title $draft) }}{{ . }}{{ else }}{{ $draft.Path }}{{ end }}</option>
|
<option value="{{ absolute $draft.Path }}">{{ with ($draft.Title) }}{{ . }}{{ else }}{{ $draft.Path }}{{ end }}</option>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</select>
|
</select>
|
||||||
<input class="fw" type="submit" value="{{ string .Blog.Lang "update" }}">
|
<input class="fw" type="submit" value="{{ string .Blog.Lang "update" }}">
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
{{ define "footer" }}
|
{{ define "footer" }}
|
||||||
<footer>
|
<footer>
|
||||||
{{ with menu .Blog "footer" }}
|
{{ with index .Blog.Menus "footer" }}
|
||||||
<p>
|
<p>
|
||||||
{{ range $i, $item := .Items }}
|
{{ range $i, $item := .Items }}
|
||||||
{{ if ne $i 0 }} • {{ end }}<a href="{{ $item.Link }}">{{ $item.Title }}</a>
|
{{ if ne $i 0 }} • {{ end }}<a href="{{ $item.Link }}">{{ $item.Title }}</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</p>
|
</p>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<p translate="no">© {{ dateformat now "2006" }} {{ with user.Name }}{{ . }}{{ else }}{{ .Blog.Title }}{{ end }}</p>
|
<p translate="no">© {{ dateformat now "2006" }} {{ with .User.Name }}{{ . }}{{ else }}{{ .Blog.Title }}{{ end }}</p>
|
||||||
{{ if .TorUsed }}
|
{{ if .TorUsed }}
|
||||||
<p>🔐 {{ string .Blog.Lang "connectedviator" }}</p>
|
<p>🔐 {{ string .Blog.Lang "connectedviator" }}</p>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
{{ define "header" }}
|
{{ define "header" }}
|
||||||
<header>
|
<header>
|
||||||
<h1><a href="{{ blogrelative .Blog "/" }}" rel="home" title="{{ .Blog.Title }}" translate="no">{{ .Blog.Title }}</a></h1>
|
<h1><a href="{{ .Blog.RelativePath "/" }}" rel="home" title="{{ .Blog.Title }}" translate="no">{{ .Blog.Title }}</a></h1>
|
||||||
{{ with .Blog.Description }}<p><i>{{ . }}</i></p>{{ end }}
|
{{ with .Blog.Description }}<p><i>{{ . }}</i></p>{{ end }}
|
||||||
<nav>
|
<nav>
|
||||||
{{ with menu .Blog "main" }}
|
{{ with index .Blog.Menus "main" }}
|
||||||
{{ range $i, $item := .Items }}{{ if ne $i 0 }} • {{ end }}<a href="{{ $item.Link }}">{{ $item.Title }}</a>{{ end }}
|
{{ range $i, $item := .Items }}{{ if ne $i 0 }} • {{ end }}<a href="{{ $item.Link }}">{{ $item.Title }}</a>{{ end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</nav>
|
</nav>
|
||||||
{{ if .LoggedIn }}
|
{{ if .LoggedIn }}
|
||||||
<nav>
|
<nav>
|
||||||
<a href="{{ blogrelative .Blog "/editor" }}">{{ string .Blog.Lang "editor" }}</a>
|
<a href="{{ .Blog.RelativePath "/editor" }}">{{ string .Blog.Lang "editor" }}</a>
|
||||||
• <a href="/notifications">{{ string .Blog.Lang "notifications" }}</a>
|
• <a href="/notifications">{{ string .Blog.Lang "notifications" }}</a>
|
||||||
{{ if .WebmentionReceivingEnabled }}• <a href="/webmention">{{ string .Blog.Lang "webmentions" }}</a>{{ end }}
|
{{ if .WebmentionReceivingEnabled }}• <a href="/webmention">{{ string .Blog.Lang "webmentions" }}</a>{{ end }}
|
||||||
{{ if .CommentsEnabled }}• <a href="{{ blogrelative .Blog "/comment" }}">{{ string .Blog.Lang "comments" }}</a>{{ end }}
|
{{ if .CommentsEnabled }}• <a href="{{ .Blog.RelativePath "/comment" }}">{{ string .Blog.Lang "comments" }}</a>{{ end }}
|
||||||
• <a href="/logout">{{ string .Blog.Lang "logout" }}</a>
|
• <a href="/logout">{{ string .Blog.Lang "logout" }}</a>
|
||||||
</nav>
|
</nav>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
<input type="hidden" name="target" value="{{ .Canonical }}">
|
<input type="hidden" name="target" value="{{ .Canonical }}">
|
||||||
<input class="fw" type="submit" value="{{ string .Blog.Lang "send" }}">
|
<input class="fw" type="submit" value="{{ string .Blog.Lang "send" }}">
|
||||||
</form>
|
</form>
|
||||||
<form class="fw-form p" method="post" action="{{ blogrelative .Blog "/comment" }}">
|
<form class="fw-form p" method="post" action="{{ .Blog.RelativePath "/comment" }}">
|
||||||
<input type="hidden" name="target" value="{{ .Canonical }}">
|
<input type="hidden" name="target" value="{{ .Canonical }}">
|
||||||
<input type="text" name="name" placeholder="{{ string .Blog.Lang "nameopt" }}">
|
<input type="text" name="name" placeholder="{{ string .Blog.Lang "nameopt" }}">
|
||||||
<input type="url" name="website" placeholder="{{ string .Blog.Lang "websiteopt" }}">
|
<input type="url" name="website" placeholder="{{ string .Blog.Lang "websiteopt" }}">
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
{{ define "oldcontentwarning" }}
|
{{ define "oldcontentwarning" }}
|
||||||
{{ if .Data.Published }}
|
{{ if .Data.Old }}
|
||||||
{{ if (datebefore (dateadd .Data.Published 1 0 0) now) }}
|
|
||||||
<strong class="p border-top border-bottom">{{ string .Blog.Lang "oldcontent" }}</strong>
|
<strong class="p border-top border-bottom">{{ string .Blog.Lang "oldcontent" }}</strong>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ end }}
|
|
||||||
{{ end }}
|
{{ end }}
|
|
@ -1,5 +1,5 @@
|
||||||
{{ define "title" }}
|
{{ define "title" }}
|
||||||
<title>{{ with title .Data }}{{ . }} - {{end}}{{ .Blog.Title }}</title>
|
<title>{{ with .Data.Title }}{{ . }} - {{end}}{{ .Blog.Title }}</title>
|
||||||
{{ include "postheadmeta" . }}
|
{{ include "postheadmeta" . }}
|
||||||
{{ with shorturl .Data }}<link rel="shortlink" href="{{ . }}">{{ end }}
|
{{ with shorturl .Data }}<link rel="shortlink" href="{{ . }}">{{ end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
@ -8,7 +8,7 @@
|
||||||
<main class=h-entry>
|
<main class=h-entry>
|
||||||
<article>
|
<article>
|
||||||
<data class="u-url hide" value="{{ absolute .Data.Path }}"></data>
|
<data class="u-url hide" value="{{ absolute .Data.Path }}"></data>
|
||||||
{{ with title .Data }}<h1 class=p-name>{{ . }}</h1>{{ end }}
|
{{ with .Data.Title }}<h1 class=p-name>{{ . }}</h1>{{ end }}
|
||||||
{{ include "postmeta" . }}
|
{{ include "postmeta" . }}
|
||||||
{{ include "postactions" . }}
|
{{ include "postactions" . }}
|
||||||
{{ if .Data.Content }}
|
{{ if .Data.Content }}
|
||||||
|
@ -29,12 +29,12 @@
|
||||||
</main>
|
</main>
|
||||||
{{ if .LoggedIn }}
|
{{ if .LoggedIn }}
|
||||||
<div class="p">
|
<div class="p">
|
||||||
<form class="in" method="post" action="{{ blogrelative .Blog "/editor" }}#update">
|
<form class="in" method="post" action="{{ .Blog.RelativePath "/editor" }}#update">
|
||||||
<input type="hidden" name="editoraction" value="loadupdate">
|
<input type="hidden" name="editoraction" value="loadupdate">
|
||||||
<input type="hidden" name="url" value="{{ .Canonical }}">
|
<input type="hidden" name="url" value="{{ .Canonical }}">
|
||||||
<input type="submit" value="{{ string .Blog.Lang "update" }}">
|
<input type="submit" value="{{ string .Blog.Lang "update" }}">
|
||||||
</form>
|
</form>
|
||||||
<form class="in" method="post" action="{{ blogrelative .Blog "/editor" }}#delete">
|
<form class="in" method="post" action="{{ .Blog.RelativePath "/editor" }}#delete">
|
||||||
<input type="hidden" name="editoraction" value="loaddelete">
|
<input type="hidden" name="editoraction" value="loaddelete">
|
||||||
<input type="hidden" name="url" value="{{ .Canonical }}">
|
<input type="hidden" name="url" value="{{ .Canonical }}">
|
||||||
<input type="submit" value="{{ string .Blog.Lang "delete" }}">
|
<input type="submit" value="{{ string .Blog.Lang "delete" }}">
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{{ define "postactions" }}
|
{{ define "postactions" }}
|
||||||
<div class="p flex" id="post-actions">
|
<div class="p flex" id="post-actions">
|
||||||
<a href="https://www.addtoany.com/share#url={{ absolute .Data.Path }}{{ with title .Data }}&title={{ . }}{{ end }}" target="_blank" rel="nofollow noopener noreferrer" class="button">{{ string .Blog.Lang "share" }}</a>
|
<a href="https://www.addtoany.com/share#url={{ absolute .Data.Path }}{{ with .Data.Title }}&title={{ . }}{{ end }}" target="_blank" rel="nofollow noopener noreferrer" class="button">{{ string .Blog.Lang "share" }}</a>
|
||||||
<a id="translateBtn" href="https://translate.google.com/translate?u={{ absolute .Data.Path }}" target="_blank" rel="nofollow noopener noreferrer" class="button">{{ string .Blog.Lang "translate" }}</a>
|
<a id="translateBtn" href="https://translate.google.com/translate?u={{ absolute .Data.Path }}" target="_blank" rel="nofollow noopener noreferrer" class="button">{{ string .Blog.Lang "translate" }}</a>
|
||||||
<script defer src="{{ asset "js/translate.js" }}"></script>
|
<script defer src="{{ asset "js/translate.js" }}"></script>
|
||||||
<button id="speakBtn" class="hide" data-speak="{{ string .Blog.Lang "speak" }}" data-stopspeak="{{ string .Blog.Lang "stopspeak" }}"></button>
|
<button id="speakBtn" class="hide" data-speak="{{ string .Blog.Lang "speak" }}" data-stopspeak="{{ string .Blog.Lang "stopspeak" }}"></button>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<meta property="og:url" content="{{ . }}">
|
<meta property="og:url" content="{{ . }}">
|
||||||
<meta property="twitter:url" content="{{ . }}">
|
<meta property="twitter:url" content="{{ . }}">
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ with title .Data }}
|
{{ with .Data.Title }}
|
||||||
<meta property="og:title" content="{{ . }}">
|
<meta property="og:title" content="{{ . }}">
|
||||||
<meta property="twitter:title" content="{{ . }}">
|
<meta property="twitter:title" content="{{ . }}">
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -2,19 +2,17 @@
|
||||||
<div class="p">
|
<div class="p">
|
||||||
{{ include "summaryandpostmeta" . }}
|
{{ include "summaryandpostmeta" . }}
|
||||||
{{ $bloglang := .Blog.Lang }}
|
{{ $bloglang := .Blog.Lang }}
|
||||||
{{ $loc := ( p .Data "location" ) }}
|
{{ $geo := .Data.GeoURI }}
|
||||||
{{ if $loc }}
|
{{ if $geo }}
|
||||||
{{ with geouri $loc }}
|
<div>📍 <a class="p-location h-geo" href="{{ geolink $geo }}" target="_blank" rel="nofollow noopener noreferrer">
|
||||||
<div>📍 <a class="p-location h-geo" href="https://www.openstreetmap.org/?mlat={{ .Latitude }}&mlon={{ .Longitude }}" target="_blank" rel="nofollow noopener noreferrer">
|
<span class="p-name">{{ geotitle $geo .Blog.Lang }}</span>
|
||||||
<span class="p-name">{{ with ( geourip . "name" ) }}{{ . }}{{ else }}{{ with ( geotitle .Latitude .Longitude $bloglang ) }}{{ . }}{{ else }}{{ .Latitude }}, {{ .Longitude }}{{ end }}{{ end }}</span>
|
<data class="p-longitude" value="{{ $geo.Longitude }}" />
|
||||||
<data class="p-longitude" value="{{ .Longitude }}" />
|
<data class="p-latitude" value="{{ $geo.Latitude }}" />
|
||||||
<data class="p-latitude" value="{{ .Latitude }}" />
|
|
||||||
</a></div>
|
</a></div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ end }}
|
|
||||||
{{ $translations := (translations .Data) }}
|
{{ $translations := (translations .Data) }}
|
||||||
{{ if gt (len $translations) 0 }}
|
{{ if gt (len $translations) 0 }}
|
||||||
<div>{{ string .Blog.Lang "translations" }}: {{ $delimiter := "" }}{{ range $i, $t := $translations }}{{ $delimiter }}<a href="{{ $t.Path }}" translate="no">{{ title $t }}</a>{{ $delimiter = ", " }}{{ end }}</div>
|
<div>{{ string .Blog.Lang "translations" }}: {{ $delimiter := "" }}{{ range $i, $t := $translations }}{{ $delimiter }}<a href="{{ $t.Path }}" translate="no">{{ $t.Title }}</a>{{ $delimiter = ", " }}{{ end }}</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ $short := shorturl .Data }}
|
{{ $short := shorturl .Data }}
|
||||||
{{ if $short }}<div>{{ string .Blog.Lang "shorturl" }} <a href="{{ $short }}" rel="shortlink">{{ $short }}</a></div>{{ end }}
|
{{ if $short }}<div>{{ string .Blog.Lang "shorturl" }} <a href="{{ $short }}" rel="shortlink">{{ $short }}</a></div>{{ end }}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
{{ if gt (len $tvs) 0 }}
|
{{ if gt (len $tvs) 0 }}
|
||||||
<p><b>{{ $tax.Title }}</b>:
|
<p><b>{{ $tax.Title }}</b>:
|
||||||
{{ range $j, $tv := $tvs }}
|
{{ range $j, $tv := $tvs }}
|
||||||
<a class="p-category" rel="tag" href="{{ blogrelative $blog ( printf "/%s/%s" $tax.Name (urlize $tv) ) }}">{{ $tv }}</a>
|
<a class="p-category" rel="tag" href="{{ $blog.RelativePath ( printf "/%s/%s" $tax.Name (urlize $tv) ) }}">{{ $tv }}</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</p>
|
</p>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{{ define "summaryandpostmeta" }}
|
{{ define "summaryandpostmeta" }}
|
||||||
{{ $section := (index .Blog.Sections .Data.Section) }}
|
{{ $section := (index .Blog.Sections .Data.Section) }}
|
||||||
{{ if .Data.Published }}<div>{{ string .Blog.Lang "publishedon" }} <time class="dt-published" datetime="{{ dateformat .Data.Published "2006-01-02T15:04:05Z07:00"}}">{{ isodate .Data.Published }}</time>{{ if $section }} in <a href="{{ blogrelative .Blog $section.Name }}">{{ $section.Title }}</a>{{ end }}</div>{{ end }}
|
{{ if .Data.Published }}<div>{{ string .Blog.Lang "publishedon" }} <time class="dt-published" datetime="{{ dateformat .Data.Published "2006-01-02T15:04:05Z07:00"}}">{{ isodate .Data.Published }}</time>{{ if $section }} in <a href="{{ .Blog.RelativePath $section.Name }}">{{ $section.Title }}</a>{{ end }}</div>{{ end }}
|
||||||
{{ if .Data.Updated }}<div>{{ string .Blog.Lang "updatedon" }} <time class="dt-updated" datetime="{{ dateformat .Data.Updated "2006-01-02T15:04:05Z07:00"}}">{{ isodate .Data.Updated }}</time></div>{{ end }}
|
{{ if .Data.Updated }}<div>{{ string .Blog.Lang "updatedon" }} <time class="dt-updated" datetime="{{ dateformat .Data.Updated "2006-01-02T15:04:05Z07:00"}}">{{ isodate .Data.Updated }}</time></div>{{ end }}
|
||||||
{{ if p .Data "replylink" }}
|
{{ if p .Data "replylink" }}
|
||||||
<div>{{ string .Blog.Lang "replyto" }}: <a class="u-in-reply-to" href="{{ p .Data "replylink" }}" target="_blank" rel="noopener">{{ with (p .Data "replytitle") }}{{ . }}{{ else }}{{ p .Data "replylink" }}{{ end }}</a></div>
|
<div>{{ string .Blog.Lang "replyto" }}: <a class="u-in-reply-to" href="{{ p .Data "replylink" }}" target="_blank" rel="noopener">{{ with (p .Data "replytitle") }}{{ . }}{{ else }}{{ p .Data "replylink" }}{{ end }}</a></div>
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<h2>{{ $valueGroup.Identifier }}</h2>
|
<h2>{{ $valueGroup.Identifier }}</h2>
|
||||||
<p>
|
<p>
|
||||||
{{ range $i, $value := $valueGroup.Strings }}
|
{{ range $i, $value := $valueGroup.Strings }}
|
||||||
{{ if ne $i 0 }} • {{ end }}<a href="{{ blogrelative $blog ( printf "/%s/%s" $taxonomy (urlize .) ) }}">{{ . }}</a>
|
{{ if ne $i 0 }} • {{ end }}<a href="{{ $blog.RelativePath ( printf "/%s/%s" $taxonomy (urlize .) ) }}">{{ . }}</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</p>
|
</p>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
2
tor.go
2
tor.go
|
@ -16,7 +16,7 @@ import (
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
var torUsedKey requestContextKey = "tor"
|
const torUsedKey requestContextKey = "tor"
|
||||||
|
|
||||||
func (a *goBlog) startOnionService(h http.Handler) error {
|
func (a *goBlog) startOnionService(h http.Handler) error {
|
||||||
torDataPath, err := filepath.Abs("data/tor")
|
torDataPath, err := filepath.Abs("data/tor")
|
||||||
|
|
18
utils.go
18
utils.go
|
@ -1,11 +1,13 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"github.com/PuerkitoBio/goquery"
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
@ -156,6 +158,18 @@ func dateFormat(date string, format string) string {
|
||||||
return d.Local().Format(format)
|
return d.Local().Format(format)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isoDateFormat(date string) string {
|
||||||
|
return dateFormat(date, "2006-01-02")
|
||||||
|
}
|
||||||
|
|
||||||
|
func unixToLocalDateString(unix int64) string {
|
||||||
|
return time.Unix(unix, 0).Local().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func localNowString() string {
|
||||||
|
return time.Now().Local().String()
|
||||||
|
}
|
||||||
|
|
||||||
type stringPair struct {
|
type stringPair struct {
|
||||||
First, Second string
|
First, Second string
|
||||||
}
|
}
|
||||||
|
@ -173,3 +187,7 @@ func charCount(s string) (count int) {
|
||||||
}
|
}
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func wrapStringAsHTML(s string) template.HTML {
|
||||||
|
return template.HTML(s)
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@ import (
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
|
||||||
)
|
)
|
||||||
|
|
||||||
type webmentionStatus string
|
type webmentionStatus string
|
||||||
|
@ -63,7 +65,7 @@ func (a *goBlog) handleWebmention(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractMention(r *http.Request) (*mention, error) {
|
func extractMention(r *http.Request) (*mention, error) {
|
||||||
if !strings.Contains(r.Header.Get(contentType), contentTypeWWWForm) {
|
if !strings.Contains(r.Header.Get(contentType), contenttype.WWWForm) {
|
||||||
return nil, errors.New("unsupported Content-Type")
|
return nil, errors.New("unsupported Content-Type")
|
||||||
}
|
}
|
||||||
err := r.ParseForm()
|
err := r.ParseForm()
|
||||||
|
@ -187,6 +189,15 @@ func (db *database) getWebmentions(config *webmentionsRequestConfig) ([]*mention
|
||||||
return mentions, nil
|
return mentions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *database) getWebmentionsByAddress(address string) []*mention {
|
||||||
|
mentions, _ := db.getWebmentions(&webmentionsRequestConfig{
|
||||||
|
target: address,
|
||||||
|
status: webmentionStatusApproved,
|
||||||
|
asc: true,
|
||||||
|
})
|
||||||
|
return mentions
|
||||||
|
}
|
||||||
|
|
||||||
func (db *database) countWebmentions(config *webmentionsRequestConfig) (count int, err error) {
|
func (db *database) countWebmentions(config *webmentionsRequestConfig) (count int, err error) {
|
||||||
query, params := buildWebmentionsQuery(config)
|
query, params := buildWebmentionsQuery(config)
|
||||||
query = "select count(*) from (" + query + ")"
|
query = "select count(*) from (" + query + ")"
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
|
||||||
"github.com/PuerkitoBio/goquery"
|
"github.com/PuerkitoBio/goquery"
|
||||||
"github.com/thoas/go-funk"
|
"github.com/thoas/go-funk"
|
||||||
"github.com/tomnomnom/linkheader"
|
"github.com/tomnomnom/linkheader"
|
||||||
|
@ -19,7 +20,7 @@ func (a *goBlog) sendWebmentions(p *post) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
links := []string{}
|
links := []string{}
|
||||||
contentLinks, err := allLinksFromHTML(strings.NewReader(string(a.html(p))), a.fullPostURL(p))
|
contentLinks, err := allLinksFromHTML(strings.NewReader(string(a.postHtml(p))), a.fullPostURL(p))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -67,7 +68,7 @@ func sendWebmention(endpoint, source, target string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
req.Header.Set(contentType, contentTypeWWWForm)
|
req.Header.Set(contentType, contenttype.WWWForm)
|
||||||
req.Header.Set(userAgent, appUserAgent)
|
req.Header.Set(userAgent, appUserAgent)
|
||||||
res, err := appHttpClient.Do(req)
|
res, err := appHttpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -84,6 +84,7 @@ func (a *goBlog) verifyMention(m *mention) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = m.verifyReader(resp.Body)
|
err = m.verifyReader(resp.Body)
|
||||||
|
_, _ = io.Copy(io.Discard, resp.Body)
|
||||||
_ = resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_, err := a.db.exec("delete from webmentions where source = @source and target = @target", sql.Named("source", m.Source), sql.Named("target", m.Target))
|
_, err := a.db.exec("delete from webmentions where source = @source and target = @target", sql.Named("source", m.Source), sql.Named("target", m.Target))
|
||||||
|
@ -162,12 +163,30 @@ func (m *mention) fill(mf *microformats.Microformat) bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Title
|
// Title
|
||||||
|
m.fillTitle(mf)
|
||||||
|
// Content
|
||||||
|
m.fillContent(mf)
|
||||||
|
// Author
|
||||||
|
m.fillAuthor(mf)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, mfc := range mf.Children {
|
||||||
|
if m.fill(mfc) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mention) fillTitle(mf *microformats.Microformat) {
|
||||||
if name, ok := mf.Properties["name"]; ok && len(name) > 0 {
|
if name, ok := mf.Properties["name"]; ok && len(name) > 0 {
|
||||||
if title, ok := name[0].(string); ok {
|
if title, ok := name[0].(string); ok {
|
||||||
m.Title = strings.TrimSpace(title)
|
m.Title = strings.TrimSpace(title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Content
|
}
|
||||||
|
|
||||||
|
func (m *mention) fillContent(mf *microformats.Microformat) {
|
||||||
if contents, ok := mf.Properties["content"]; ok && len(contents) > 0 {
|
if contents, ok := mf.Properties["content"]; ok && len(contents) > 0 {
|
||||||
if content, ok := contents[0].(map[string]string); ok {
|
if content, ok := contents[0].(map[string]string); ok {
|
||||||
if contentValue, ok := content["value"]; ok {
|
if contentValue, ok := content["value"]; ok {
|
||||||
|
@ -175,7 +194,9 @@ func (m *mention) fill(mf *microformats.Microformat) bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Author
|
}
|
||||||
|
|
||||||
|
func (m *mention) fillAuthor(mf *microformats.Microformat) {
|
||||||
if authors, ok := mf.Properties["author"]; ok && len(authors) > 0 {
|
if authors, ok := mf.Properties["author"]; ok && len(authors) > 0 {
|
||||||
if author, ok := authors[0].(*microformats.Microformat); ok {
|
if author, ok := authors[0].(*microformats.Microformat); ok {
|
||||||
if names, ok := author.Properties["name"]; ok && len(names) > 0 {
|
if names, ok := author.Properties["name"]; ok && len(names) > 0 {
|
||||||
|
@ -185,16 +206,6 @@ func (m *mention) fill(mf *microformats.Microformat) bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
|
||||||
}
|
|
||||||
if len(mf.Children) > 0 {
|
|
||||||
for _, mfc := range mf.Children {
|
|
||||||
if m.fill(mfc) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func mfHasType(mf *microformats.Microformat, typ string) bool {
|
func mfHasType(mf *microformats.Microformat, typ string) bool {
|
||||||
|
|
Loading…
Reference in New Issue