Bug fixes, refactoring and other improvements

This commit is contained in:
Jan-Lukas Else 2021-06-18 14:32:03 +02:00
parent 20df90bf4e
commit 67f2b1fbdb
58 changed files with 445 additions and 400 deletions

View File

@ -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
ADD *.go go.mod go.sum /app/
ENV GOFLAGS="-tags=linux,libsqlite3,sqlite_fts5"
RUN go test -cover
RUN go test -cover ./...
RUN go build -ldflags '-w -s' -o GoBlog
FROM alpine:3.13

View File

@ -15,6 +15,7 @@ import (
"strings"
"time"
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
"github.com/go-chi/chi/v5"
"github.com/go-fed/httpsig"
"github.com/spf13/cast"
@ -90,7 +91,7 @@ func (a *goBlog) apHandleWebfinger(w http.ResponseWriter, r *http.Request) {
"links": []map[string]string{
{
"rel": "self",
"type": contentTypeAS,
"type": contenttype.AS,
"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)
_, _ = writeMinified(w, contentTypeJSON, b)
w.Header().Set(contentType, "application/jrd+json"+contenttype.CharsetUtf8Suffix)
_, _ = a.min.Write(w, contenttype.JSON, b)
}
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) {
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>`))
}
@ -253,7 +254,7 @@ func apGetRemoteActor(iri string) (*asPerson, int, error) {
if err != nil {
return nil, 0, err
}
req.Header.Set("Accept", contentTypeAS)
req.Header.Set("Accept", contenttype.AS)
req.Header.Set(userAgent, appUserAgent)
resp, err := appHttpClient.Do(req)
if err != nil {
@ -308,7 +309,7 @@ func (db *database) apRemoveInbox(inbox string) error {
func (a *goBlog) apPost(p *post) {
n := a.toASNote(p)
a.apSendToAllFollowers(p.Blog, map[string]interface{}{
"@context": asContext,
"@context": []string{asContext},
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
"id": a.fullPostURL(p),
"published": n.Published,
@ -324,7 +325,7 @@ func (a *goBlog) apPost(p *post) {
func (a *goBlog) apUpdate(p *post) {
a.apSendToAllFollowers(p.Blog, map[string]interface{}{
"@context": asContext,
"@context": []string{asContext},
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
"id": a.fullPostURL(p),
"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) {
a.apSendToAllFollowers(p.Blog, map[string]interface{}{
"@context": asContext,
"@context": []string{asContext},
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
"id": a.fullPostURL(p) + "#announce",
"published": a.toASNote(p).Published,
@ -346,7 +347,7 @@ func (a *goBlog) apAnnounce(p *post) {
func (a *goBlog) apDelete(p *post) {
a.apSendToAllFollowers(p.Blog, map[string]interface{}{
"@context": asContext,
"@context": []string{asContext},
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
"id": a.fullPostURL(p) + "#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
delete(follow, "@context")
accept := map[string]interface{}{
"@context": asContext,
"@context": []string{asContext},
"to": follow["actor"],
"actor": a.apIri(blog),
"object": follow,

View File

@ -11,6 +11,8 @@ import (
"net/http"
"net/url"
"time"
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
)
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("Date", time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
r.Header.Set(userAgent, appUserAgent)
r.Header.Set("Accept", contentTypeASUTF8)
r.Header.Set(contentType, contentTypeASUTF8)
r.Header.Set("Accept", contenttype.ASUTF8)
r.Header.Set(contentType, contenttype.ASUTF8)
r.Header.Set("Host", iri.Host)
// Sign request
a.apPostSignMutex.Lock()

View File

@ -8,25 +8,26 @@ import (
"fmt"
"net/http"
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
"github.com/araddon/dateparse"
"github.com/elnormous/contenttype"
ct "github.com/elnormous/contenttype"
)
var asContext = []string{"https://www.w3.org/ns/activitystreams"}
var asCheckMediaTypes = []contenttype.MediaType{
contenttype.NewMediaType(contentTypeHTML),
contenttype.NewMediaType(contentTypeAS),
contenttype.NewMediaType("application/ld+json"),
}
const asContext = "https://www.w3.org/ns/activitystreams"
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 {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if ap := a.cfg.ActivityPub; ap != nil && ap.Enabled {
// 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)))
return
}
@ -89,29 +90,29 @@ type asEndpoints struct {
func (a *goBlog) serveActivityStreamsPost(p *post, w http.ResponseWriter) {
b, _ := json.Marshal(a.toASNote(p))
w.Header().Set(contentType, contentTypeASUTF8)
_, _ = writeMinified(w, contentTypeAS, b)
w.Header().Set(contentType, contenttype.ASUTF8)
_, _ = a.min.Write(w, contenttype.AS, b)
}
func (a *goBlog) toASNote(p *post) *asNote {
// Create a Note object
as := &asNote{
Context: asContext,
Context: []string{asContext},
To: []string{"https://www.w3.org/ns/activitystreams#Public"},
MediaType: contentTypeHTML,
MediaType: contenttype.HTML,
ID: a.fullPostURL(p),
URL: a.fullPostURL(p),
AttributedTo: a.apIri(a.cfg.Blogs[p.Blog]),
}
// Name and Type
if title := p.title(); title != "" {
if title := p.Title(); title != "" {
as.Name = title
as.Type = "Article"
} else {
as.Type = "Note"
}
// Content
as.Content = string(a.absoluteHTML(p))
as.Content = string(a.absolutePostHTML(p))
// Attachments
if images := p.Parameters[a.cfg.Micropub.PhotoParam]; len(images) > 0 {
for _, image := range images {
@ -158,7 +159,7 @@ func (a *goBlog) serveActivityStreams(blog string, w http.ResponseWriter, r *htt
return
}
asBlog := &asPerson{
Context: asContext,
Context: []string{asContext},
Type: "Person",
ID: 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)
w.Header().Set(contentType, contentTypeASUTF8)
_, _ = writeMinified(w, contentTypeAS, jb)
w.Header().Set(contentType, contenttype.ASUTF8)
_, _ = a.min.Write(w, contenttype.AS, jb)
}

9
app.go
View File

@ -6,6 +6,7 @@ import (
"net/http"
"sync"
"git.jlel.se/jlelse/GoBlog/pkgs/minify"
shutdowner "git.jlel.se/jlelse/go-shutdowner"
ts "git.jlel.se/jlelse/template-strings"
"github.com/go-chi/chi/v5"
@ -27,6 +28,8 @@ type goBlog struct {
assetFiles map[string]*assetFile
// Blogroll
blogrollCacheGroup singleflight.Group
// Blogstats
blogStatsCacheGroup singleflight.Group
// Cache
cache *cache
// Config
@ -37,6 +40,9 @@ type goBlog struct {
pPostHooks []postHookFunc
pUpdateHooks []postHookFunc
pDeleteHooks []postHookFunc
hourlyHooks []hourlyHookFunc
// HTTP
cspDomains string
// HTTP Routers
d *dynamicHandler
privateMode bool
@ -53,6 +59,7 @@ type goBlog struct {
setBlogMiddlewares map[string]func(http.Handler) http.Handler
sectionMiddlewares 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
searchMiddlewares 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
// Markdown
md, absoluteMd goldmark.Markdown
// Minify
min minify.Minifier
// Regex Redirects
regexRedirects []*regexRedirect
// Rendering

View File

@ -8,6 +8,7 @@ import (
"io"
"net/http"
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
"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 {
return false
}
if r.Header.Get(contentType) != contentTypeWWWForm {
if r.Header.Get(contentType) != contenttype.WWWForm {
return false
}
if r.FormValue("loginaction") != "login" {

View File

@ -10,6 +10,7 @@ import (
"strings"
"time"
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
"github.com/kaorimatz/go-opml"
servertiming "github.com/mitchellh/go-server-timing"
"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 {
setInternalCacheExpirationHeader(w, r, int(a.cfg.Cache.Expiration))
}
w.Header().Set(contentType, contentTypeXMLUTF8)
w.Header().Set(contentType, contenttype.XMLUTF8)
var opmlBytes bytes.Buffer
_ = opml.Render(&opmlBytes, &opml.OPML{
Version: "2.0",
DateCreated: time.Now().UTC(),
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) {

View File

@ -5,8 +5,6 @@ import (
"encoding/json"
"log"
"net/http"
"golang.org/x/sync/singleflight"
)
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) {
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)
})
if err != nil {

View File

@ -7,6 +7,7 @@ import (
"io"
"net/http"
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
"github.com/dchest/captcha"
)
@ -53,7 +54,7 @@ func (a *goBlog) checkCaptcha(w http.ResponseWriter, r *http.Request) bool {
if r.Method != http.MethodPost {
return false
}
if r.Header.Get(contentType) != contentTypeWWWForm {
if r.Header.Get(contentType) != contenttype.WWWForm {
return false
}
if r.FormValue("captchaaction") != "captcha" {

View File

@ -90,7 +90,7 @@ func (a *goBlog) getExternalLinks(posts []*post, linkChan chan<- stringPair) err
wg.Add(1)
go func(p *post) {
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 {
linkChan <- stringPair{a.fullPostURL(p), link}
}

View File

@ -6,6 +6,7 @@ import (
"errors"
"log"
"os"
"sync"
sqlite "github.com/mattn/go-sqlite3"
"github.com/schollz/sqlite3dump"
@ -19,6 +20,7 @@ type database struct {
stmts map[string]*sql.Stmt
g singleflight.Group
pc singleflight.Group
pcm sync.Mutex
}
func (a *goBlog) initDatabase() (err error) {
@ -38,7 +40,7 @@ func (a *goBlog) initDatabase() (err error) {
}
})
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)

View File

@ -7,6 +7,8 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
)
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)
return
}
req.Header.Set(contentType, contentTypeJSON)
req.Header.Set(contentType, contenttype.JSON)
a.editorMicropubPost(w, req, false)
case "upload":
a.editorMicropubPost(w, r, true)

View File

@ -4,7 +4,8 @@ import (
"fmt"
"net/http"
"github.com/elnormous/contenttype"
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
ct "github.com/elnormous/contenttype"
)
type errorData struct {
@ -20,12 +21,12 @@ func (a *goBlog) serveNotAllowed(w http.ResponseWriter, r *http.Request) {
a.serveError(w, r, "", http.StatusMethodNotAllowed)
}
var errorCheckMediaTypes = []contenttype.MediaType{
contenttype.NewMediaType(contentTypeHTML),
var errorCheckMediaTypes = []ct.MediaType{
ct.NewMediaType(contenttype.HTML),
}
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
http.Error(w, message, status)
return

View File

@ -5,6 +5,7 @@ import (
"strings"
"time"
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
"github.com/araddon/dateparse"
"github.com/gorilla/feeds"
)
@ -55,11 +56,11 @@ func (a *goBlog) generateFeed(blog string, f feedType, w http.ResponseWriter, r
}
}
feed.Add(&feeds.Item{
Title: p.title(),
Title: p.Title(),
Link: &feeds.Link{Href: a.fullPostURL(p)},
Description: a.summary(p),
Description: a.postSummary(p),
Id: p.Path,
Content: string(a.absoluteHTML(p)),
Content: string(a.absolutePostHTML(p)),
Created: created,
Updated: updated,
Enclosure: enc,
@ -69,13 +70,13 @@ func (a *goBlog) generateFeed(blog string, f feedType, w http.ResponseWriter, r
var feedString, feedMediaType string
switch f {
case rssFeed:
feedMediaType = contentTypeRSS
feedMediaType = contenttype.RSS
feedString, err = feed.ToRss()
case atomFeed:
feedMediaType = contentTypeATOM
feedMediaType = contenttype.ATOM
feedString, err = feed.ToAtom()
case jsonFeed:
feedMediaType = contentTypeJSONFeed
feedMediaType = contenttype.JSONFeed
feedString, err = feed.ToJSON()
default:
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)
return
}
w.Header().Set(contentType, feedMediaType+charsetUtf8Suffix)
_, _ = writeMinified(w, feedMediaType, []byte(feedString))
w.Header().Set(contentType, feedMediaType+contenttype.CharsetUtf8Suffix)
_, _ = a.min.Write(w, feedMediaType, []byte(feedString))
}

View File

@ -7,12 +7,16 @@ import (
"net/url"
"strings"
gogeouri "git.jlel.se/jlelse/go-geouri"
geojson "github.com/paulmach/go.geojson"
"github.com/thoas/go-funk"
)
func (db *database) geoTitle(lat, lon float64, lang string) string {
ba, err := db.photonReverse(lat, lon, lang)
func (db *database) geoTitle(g *gogeouri.Geo, lang string) string {
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 {
return ""
}
@ -65,3 +69,7 @@ func (db *database) photonReverse(lat, lon float64, lang string) ([]byte, error)
_ = db.cachePersistently(cacheKey, ba)
return ba, nil
}
func geoOSMLink(g *gogeouri.Geo) string {
return fmt.Sprintf("https://www.openstreetmap.org/?mlat=%v&mlon=%v", g.Latitude, g.Longitude)
}

View File

@ -78,7 +78,7 @@ func (cfg *configHooks) executeTemplateCommand(hookType string, tmpl string, dat
executeHookCommand(hookType, cfg.Shell, cmd)
}
var hourlyHooks = []func(){}
type hourlyHookFunc func()
func (a *goBlog) startHourlyHooks() {
cfg := a.cfg.Hooks
@ -88,14 +88,14 @@ func (a *goBlog) startHourlyHooks() {
f := func() {
executeHookCommand("hourly", cfg.Shell, c)
}
hourlyHooks = append(hourlyHooks, f)
a.hourlyHooks = append(a.hourlyHooks, f)
}
// When there are hooks, start ticker
if len(hourlyHooks) > 0 {
if len(a.hourlyHooks) > 0 {
// Wait for next full hour
tr := time.AfterFunc(time.Until(time.Now().Truncate(time.Hour).Add(time.Hour)), func() {
// Execute once
for _, f := range hourlyHooks {
for _, f := range a.hourlyHooks {
go f()
}
// Start ticker and execute regularly
@ -105,7 +105,7 @@ func (a *goBlog) startHourlyHooks() {
log.Println("Stopped hourly hooks")
})
for range ticker.C {
for _, f := range hourlyHooks {
for _, f := range a.hourlyHooks {
go f()
}
}

46
http.go
View File

@ -22,24 +22,6 @@ import (
const (
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"
appUserAgent = "GoBlog"
)
@ -241,6 +223,7 @@ func (a *goBlog) buildStaticHandlersRouters() error {
a.setBlogMiddlewares = 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.taxValueMiddlewares = 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.customPagesMiddlewares = map[string]func(http.Handler) http.Handler{}
@ -293,10 +276,6 @@ func (a *goBlog) buildStaticHandlersRouters() error {
return nil
}
var (
taxValueMiddlewares = map[string]func(http.Handler) http.Handler{}
)
func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
r := chi.NewRouter()
@ -436,14 +415,14 @@ func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
for _, tv := range taxValues {
r.Group(func(r chi.Router) {
vPath := taxPath + "/" + urlize(tv)
if _, ok := taxValueMiddlewares[vPath]; !ok {
taxValueMiddlewares[vPath] = middleware.WithValue(indexConfigKey, &indexConfig{
if _, ok := a.taxValueMiddlewares[vPath]; !ok {
a.taxValueMiddlewares[vPath] = middleware.WithValue(indexConfigKey, &indexConfig{
path: vPath,
tax: taxonomy,
taxValue: tv,
})
}
r.Use(taxValueMiddlewares[vPath])
r.Use(a.taxValueMiddlewares[vPath])
r.Get(vPath, a.serveIndex)
r.Get(vPath+feedPath, 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.Use(a.privateModeHandler...)
r.Use(sbm)
blogBasePath := blogConfig.getRelativePath("")
r.With(a.checkActivityStreamsRequest, a.cache.cacheMiddleware).Get(blogBasePath, a.serveHome)
r.With(a.cache.cacheMiddleware).Get(blogBasePath+feedPath, a.serveHome)
r.With(a.cache.cacheMiddleware).Get(blogBasePath+paginationPath, a.serveHome)
r.With(a.checkActivityStreamsRequest, a.cache.cacheMiddleware).Get(blogConfig.getRelativePath(""), a.serveHome)
r.With(a.cache.cacheMiddleware).Get(blogConfig.getRelativePath("")+feedPath, a.serveHome)
r.With(a.cache.cacheMiddleware).Get(blogConfig.getRelativePath(paginationPath), a.serveHome)
})
}
@ -579,17 +557,15 @@ func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
const blogContextKey requestContextKey = "blog"
const pathContextKey requestContextKey = "httpPath"
var cspDomains = ""
func (a *goBlog) refreshCSPDomains() {
cspDomains = ""
a.cspDomains = ""
if mp := a.cfg.Micropub.MediaStorage; mp != nil && mp.MediaURL != "" {
if u, err := url.Parse(mp.MediaURL); err == nil {
cspDomains += " " + u.Hostname()
a.cspDomains += " " + u.Hostname()
}
}
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-Frame-Options", "SAMEORIGIN")
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 != "" {
w.Header().Set("Onion-Location", fmt.Sprintf("http://%v%v", a.torAddress, r.RequestURI))
}

View File

@ -11,6 +11,7 @@ import (
"strings"
"time"
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
"github.com/spf13/cast"
)
@ -137,8 +138,8 @@ func (a *goBlog) indieAuthVerification(w http.ResponseWriter, r *http.Request) {
b, _ := json.Marshal(tokenResponse{
Me: a.cfg.Server.PublicAddress,
})
w.Header().Set(contentType, contentTypeJSONUTF8)
_, _ = writeMinified(w, contentTypeJSON, b)
w.Header().Set(contentType, contenttype.JSONUTF8)
_, _ = a.min.Write(w, contenttype.JSON, b)
}
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,
}
b, _ := json.Marshal(res)
w.Header().Set(contentType, contentTypeJSONUTF8)
_, _ = writeMinified(w, contentTypeJSON, b)
w.Header().Set(contentType, contenttype.JSONUTF8)
_, _ = a.min.Write(w, contenttype.JSON, b)
return
} else if r.Method == http.MethodPost {
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,
}
b, _ := json.Marshal(res)
w.Header().Set(contentType, contentTypeJSONUTF8)
_, _ = writeMinified(w, contentTypeJSON, b)
w.Header().Set(contentType, contenttype.JSONUTF8)
_, _ = a.min.Write(w, contenttype.JSON, b)
return
}
a.serveError(w, r, "", http.StatusBadRequest)

View File

@ -10,13 +10,12 @@ import (
"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() {
var err error
// 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()
if *cpuprofile != "" {
f, err := os.Create(*cpuprofile)

View File

@ -2,6 +2,7 @@ package main
import (
"bytes"
"html/template"
"strings"
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
}
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 {
h, err := a.renderMarkdown(s, false)
if err != nil {

View File

@ -4,6 +4,8 @@ import (
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_markdown(t *testing.T) {
@ -65,6 +67,11 @@ func Test_markdown(t *testing.T) {
if renderedText != "This is text" {
t.Errorf("Wrong result, got \"%v\"", renderedText)
}
// Template func
renderedText = string(app.safeRenderMarkdownAsHTML("[Relative](/relative)"))
assert.Contains(t, renderedText, `href="/relative"`)
})
}

View File

@ -8,6 +8,8 @@ import (
"io"
"net/http"
"os"
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
)
const defaultCompressionWidth = 2000
@ -34,7 +36,7 @@ func (a *goBlog) tinify(url string, config *configMicropubMedia) (location strin
return "", err
}
req.SetBasicAuth("api", config.TinifyKey)
req.Header.Set(contentType, contentTypeJSON)
req.Header.Set(contentType, contenttype.JSON)
resp, err := appHttpClient.Do(req)
if err != nil {
return "", err
@ -61,7 +63,7 @@ func (a *goBlog) tinify(url string, config *configMicropubMedia) (location strin
return "", err
}
downloadReq.SetBasicAuth("api", config.TinifyKey)
downloadReq.Header.Set(contentType, contentTypeJSON)
downloadReq.Header.Set(contentType, contenttype.JSON)
downloadResp, err := appHttpClient.Do(downloadReq)
if err != nil {
return "", err

View File

@ -12,6 +12,7 @@ import (
"strings"
"time"
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
"github.com/spf13/cast"
"gopkg.in/yaml.v3"
)
@ -25,12 +26,12 @@ type micropubConfig struct {
func (a *goBlog) serveMicropubQuery(w http.ResponseWriter, r *http.Request) {
switch r.URL.Query().Get("q") {
case "config":
w.Header().Set(contentType, contentTypeJSONUTF8)
w.Header().Set(contentType, contenttype.JSONUTF8)
w.WriteHeader(http.StatusOK)
b, _ := json.Marshal(&micropubConfig{
MediaEndpoint: a.getFullAddress(micropubPath + micropubMediaSubPath),
})
_, _ = writeMinified(w, contentTypeJSON, b)
_, _ = a.min.Write(w, contenttype.JSON, b)
case "source":
var mf interface{}
if urlString := r.URL.Query().Get("url"); urlString != "" {
@ -62,10 +63,10 @@ func (a *goBlog) serveMicropubQuery(w http.ResponseWriter, r *http.Request) {
}
mf = list
}
w.Header().Set(contentType, contentTypeJSONUTF8)
w.Header().Set(contentType, contenttype.JSONUTF8)
w.WriteHeader(http.StatusOK)
b, _ := json.Marshal(mf)
_, _ = writeMinified(w, contentTypeJSON, b)
_, _ = a.min.Write(w, contenttype.JSON, b)
case "category":
allCategories := []string{}
for blog := range a.cfg.Blogs {
@ -76,12 +77,12 @@ func (a *goBlog) serveMicropubQuery(w http.ResponseWriter, r *http.Request) {
}
allCategories = append(allCategories, values...)
}
w.Header().Set(contentType, contentTypeJSONUTF8)
w.Header().Set(contentType, contenttype.JSONUTF8)
w.WriteHeader(http.StatusOK)
b, _ := json.Marshal(map[string]interface{}{
"categories": allCategories,
})
_, _ = writeMinified(w, contentTypeJSON, b)
_, _ = a.min.Write(w, contenttype.JSON, b)
default:
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) {
defer r.Body.Close()
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
if strings.Contains(ct, contentTypeMultipartForm) {
if strings.Contains(ct, contenttype.MultipartForm) {
err = r.ParseMultipartForm(0)
} else {
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)
return
}
} else if strings.Contains(ct, contentTypeJSON) {
} else if strings.Contains(ct, contenttype.JSON) {
parsedMfItem := &microformatItem{}
err := json.NewDecoder(r.Body).Decode(parsedMfItem)
if err != nil {

View File

@ -11,6 +11,8 @@ import (
"path/filepath"
"sort"
"strings"
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
)
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)
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)
return
}

View File

@ -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)
}

View File

@ -3,6 +3,8 @@ package main
import (
"encoding/json"
"net/http"
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
)
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)
_, _ = writeMinified(w, contentTypeJSON, b)
w.Header().Set(contentType, contenttype.JSONUTF8)
_, _ = a.min.Write(w, contenttype.JSON, b)
}
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{}{},
})
w.Header().Set(contentType, contentTypeJSONUTF8)
_, _ = writeMinified(w, contentTypeJSON, b)
w.Header().Set(contentType, contenttype.JSONUTF8)
_, _ = a.min.Write(w, contenttype.JSON, b)
}

View File

@ -3,6 +3,8 @@ package main
import (
"fmt"
"net/http"
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
)
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>",
title, title, sURL, sURL)
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 {

View File

@ -43,3 +43,9 @@ func (cfg *configServer) getFullAddress(path string) string {
}
return pa + path
}
// Rendering funcs
func (blog *configBlog) RelativePath(path string) string {
return blog.getRelativePath(path)
}

View File

@ -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
)

45
pkgs/minify/minify.go Normal file
View File

@ -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)
}

View File

@ -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())
}

View File

@ -6,7 +6,6 @@ import (
"errors"
"fmt"
"strings"
"sync"
"text/template"
"time"
@ -122,8 +121,6 @@ type postCreationOptions struct {
oldStatus postStatus
}
var postCreationMutex sync.Mutex
func (a *goBlog) createOrReplacePost(p *post, o *postCreationOptions) error {
// Check post
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
func (db *database) savePost(p *post, o *postCreationOptions) error {
// Prevent bad things
postCreationMutex.Lock()
defer postCreationMutex.Unlock()
db.pcm.Lock()
defer db.pcm.Unlock()
// Check if path is already in use
if o.new || (p.Path != o.oldPath) {
// Post is new or post path was changed

View File

@ -53,7 +53,7 @@ func Test_postsDb(t *testing.T) {
is.Equal("en", p.Blog)
is.Equal("test", p.Section)
is.Equal(statusDraft, p.Status)
is.Equal("Title", p.title())
is.Equal("Title", p.Title())
// Check number of post paths
pp, err := app.db.allPostPaths(statusDraft)

View File

@ -4,8 +4,11 @@ import (
"html/template"
"log"
"strings"
"time"
gogeouri "git.jlel.se/jlelse/go-geouri"
"github.com/PuerkitoBio/goquery"
"github.com/araddon/dateparse"
)
func (a *goBlog) fullPostURL(p *post) string {
@ -23,6 +26,14 @@ func (a *goBlog) shortPostURL(p *post) string {
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) {
if pp := p.Parameters[parameter]; len(pp) > 0 {
result = pp[0]
@ -30,11 +41,11 @@ func (p *post) firstParameter(parameter string) (result string) {
return
}
func (p *post) title() string {
return p.firstParameter("title")
func firstPostParameter(p *post, parameter string) string {
return p.firstParameter(parameter)
}
func (a *goBlog) html(p *post) template.HTML {
func (a *goBlog) postHtml(p *post) template.HTML {
if p.rendered != "" {
return p.rendered
}
@ -47,7 +58,7 @@ func (a *goBlog) html(p *post) template.HTML {
return p.rendered
}
func (a *goBlog) absoluteHTML(p *post) template.HTML {
func (a *goBlog) absolutePostHTML(p *post) template.HTML {
if p.absoluteRendered != "" {
return p.absoluteRendered
}
@ -62,12 +73,12 @@ func (a *goBlog) absoluteHTML(p *post) template.HTML {
const summaryDivider = "<!--more-->"
func (a *goBlog) summary(p *post) (summary string) {
func (a *goBlog) postSummary(p *post) (summary string) {
summary = p.firstParameter("summary")
if summary != "" {
return
}
html := string(a.html(p))
html := string(a.postHtml(p))
if splitted := strings.Split(html, summaryDivider); len(splitted) > 1 {
doc, _ := goquery.NewDocumentFromReader(strings.NewReader(splitted[0]))
summary = doc.Text()
@ -78,7 +89,7 @@ func (a *goBlog) summary(p *post) (summary string) {
return
}
func (a *goBlog) translations(p *post) []*post {
func (a *goBlog) postTranslations(p *post) []*post {
translationkey := p.firstParameter("translationkey")
if translationkey == "" {
return nil
@ -105,3 +116,30 @@ func (a *goBlog) translations(p *post) []*post {
func (p *post) isPublishedSectionPost() bool {
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
View File

@ -2,21 +2,16 @@ package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"html/template"
"log"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"time"
gogeouri "git.jlel.se/jlelse/go-geouri"
"github.com/araddon/dateparse"
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
servertiming "github.com/mitchellh/go-server-timing"
)
@ -48,142 +43,32 @@ const (
func (a *goBlog) initRendering() error {
a.templates = map[string]*template.Template{}
templateFunctions := template.FuncMap{
"menu": func(blog *configBlog, id string) *menu {
return blog.Menus[id]
},
"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)
},
"md": a.safeRenderMarkdownAsHTML,
"html": wrapStringAsHTML,
// Post specific
"p": func(p *post, parameter string) string {
return p.firstParameter(parameter)
},
"ps": func(p *post, parameter string) []string {
return p.Parameters[parameter]
},
"hasp": func(p *post, parameter string) bool {
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)
},
"p": firstPostParameter,
"ps": postParameter,
"hasp": postHasParameter,
"content": a.postHtml,
"summary": a.postSummary,
"translations": a.postTranslations,
"shorturl": a.shortPostURL,
// Others
"dateformat": dateFormat,
"isodate": func(date string) string {
return dateFormat(date, "2006-01-02")
},
"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)
},
"isodate": isoDateFormat,
"unixtodate": unixToLocalDateString,
"now": localNowString,
"asset": a.assetFileName,
"assetsri": a.assetSRI,
"string": a.ts.GetTemplateStringVariantFunc(),
"include": func(templateName string, data ...interface{}) (template.HTML, error) {
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")
},
"include": a.includeRenderedTemplate,
"urlize": urlize,
"sort": sortedStrings,
"absolute": func(path string) string {
return a.getFullAddress(path)
},
"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
},
"absolute": a.getFullAddress,
"mentions": a.db.getWebmentionsByAddress,
"geotitle": a.db.geoTitle,
"geolink": geoOSMLink,
"opensearch": openSearchUrl,
}
baseTemplate, err := template.New("base").Funcs(templateFunctions).ParseFiles(path.Join(templatesDir, templateBase+templatesExt))
if err != nil {
return err
@ -212,6 +97,7 @@ type renderData struct {
Canonical string
TorAddress string
Blog *configBlog
User *configUser
Data interface{}
LoggedIn bool
CommentsEnabled bool
@ -223,6 +109,9 @@ func (a *goBlog) render(w http.ResponseWriter, r *http.Request, template string,
// Server timing
t := servertiming.FromContext(r.Context()).NewMetric("r").Start()
// Check render data
if data.User == nil {
data.User = a.cfg.User
}
if data.Blog == nil {
if len(data.BlogString) == 0 {
data.BlogString = a.cfg.DefaultBlog
@ -256,7 +145,7 @@ func (a *goBlog) render(w http.ResponseWriter, r *http.Request, template string,
data.TorUsed = true
}
// Set content type
w.Header().Set(contentType, contentTypeHTMLUTF8)
w.Header().Set(contentType, contenttype.HTMLUTF8)
// Minify and write response
var tw bytes.Buffer
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)
return
}
_, err = writeMinified(w, contentTypeHTML, tw.Bytes())
_, err = a.min.Write(w, contenttype.HTML, tw.Bytes())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -272,3 +161,20 @@ func (a *goBlog) render(w http.ResponseWriter, r *http.Request, template string,
// Server timing
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")
}

View File

@ -27,7 +27,7 @@ func (a *goBlog) initSessions() {
}
}
deleteExpiredSessions()
hourlyHooks = append(hourlyHooks, deleteExpiredSessions)
a.hourlyHooks = append(a.hourlyHooks, deleteExpiredSessions)
a.loginSessions = &dbSessionStore{
codecs: securecookie.CodecsFromPairs(a.jwtKey()),
options: &sessions.Options{

View File

@ -16,7 +16,7 @@ const telegramBaseURL = "https://api.telegram.org/bot"
func (a *goBlog) initTelegram() {
a.pPostHooks = append(a.pPostHooks, func(p *post) {
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 {
log.Printf("Failed to send post to Telegram: %v", err)
}

View File

@ -2,23 +2,21 @@ package main
import (
"crypto/sha1"
"crypto/sha512"
"encoding/base64"
"fmt"
"io"
"mime"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
)
const assetsFolder = "templates/assets"
type assetFile struct {
contentType string
sri string
body []byte
}
@ -50,7 +48,7 @@ func (a *goBlog) compileAsset(name string) (string, error) {
}
ext := path.Ext(name)
compiledExt := ext
m := getMinifier()
m := a.min.Get()
switch ext {
case ".js":
content, err = m.Bytes("application/javascript", content)
@ -67,18 +65,14 @@ func (a *goBlog) compileAsset(name string) (string, error) {
}
// Hashes
sha1Hash := sha1.New()
sha512Hash := sha512.New()
if _, err := io.MultiWriter(sha1Hash, sha512Hash).Write(content); err != nil {
if _, err := sha1Hash.Write(content); err != nil {
return "", err
}
// File name
compiledFileName := fmt.Sprintf("%x", sha1Hash.Sum(nil)) + compiledExt
// SRI
sriHash := fmt.Sprintf("sha512-%s", base64.StdEncoding.EncodeToString(sha512Hash.Sum(nil)))
// Create struct
a.assetFiles[compiledFileName] = &assetFile{
contentType: mime.TypeByExtension(compiledExt),
sri: sriHash,
body: content,
}
return compiledFileName, err
@ -89,10 +83,6 @@ func (a *goBlog) assetFileName(fileName string) string {
return "/" + a.assetFileNames[fileName]
}
func (a *goBlog) assetSRI(fileName string) string {
return a.assetFiles[a.assetFileNames[fileName]].sri
}
func (a *goBlog) allAssetPaths() []string {
var paths []string
for _, name := range a.assetFileNames {
@ -109,6 +99,6 @@ func (a *goBlog) serveAsset(w http.ResponseWriter, r *http.Request) {
return
}
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)
}

View File

@ -1,5 +1,5 @@
{{ define "author" }}
{{ with user }}
{{ with .User }}
<div class="p-author h-card hide">
{{ with .Picture }}<data class="u-photo" value="{{ . }}"></data>{{ end }}
{{ if .Name }}

View File

@ -15,11 +15,7 @@
<link rel="micropub" href="/micropub" />
<link rel="authorization_endpoint" href="/indieauth" />
<link rel="token_endpoint" href="/indieauth/token" />
{{ with user }}
{{ range .Identities }}
<link rel="me" href="{{ . }}" />
{{ end }}
{{ end }}
{{ with .User }}{{ range .Identities }}<link rel="me" href="{{ . }}" />{{ end }}{{ end }}
{{ $os := opensearch .Blog }}
{{ if $os }}
<link rel="search" type="application/opensearchdescription+xml" href="{{ $os }}" title="{{ .Blog.Title }}" />

View File

@ -6,7 +6,7 @@
<main>
{{ with .Data.Title }}<h1>{{ . }}</h1>{{ 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 }}
{{ range .Data.Outlines }}
{{ $title := .Title }}
@ -16,7 +16,7 @@
{{ range .Outlines }}
{{ $ct := .Title }}
{{ 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 }}
</ul>
{{ end }}

View File

@ -55,7 +55,7 @@ tags:
<input type="hidden" name="editoraction" value="loadupdate">
<select name="url" class="fw">
{{ 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 }}
</select>
<input class="fw" type="submit" value="{{ string .Blog.Lang "update" }}">

View File

@ -1,13 +1,13 @@
{{ define "footer" }}
<footer>
{{ with menu .Blog "footer" }}
{{ with index .Blog.Menus "footer" }}
<p>
{{ range $i, $item := .Items }}
{{ if ne $i 0 }} &bull; {{ end }}<a href="{{ $item.Link }}">{{ $item.Title }}</a>
{{ end }}
</p>
{{ end }}
<p translate="no">&copy; {{ dateformat now "2006" }} {{ with user.Name }}{{ . }}{{ else }}{{ .Blog.Title }}{{ end }}</p>
<p translate="no">&copy; {{ dateformat now "2006" }} {{ with .User.Name }}{{ . }}{{ else }}{{ .Blog.Title }}{{ end }}</p>
{{ if .TorUsed }}
<p>🔐 {{ string .Blog.Lang "connectedviator" }}</p>
{{ else }}

View File

@ -1,18 +1,18 @@
{{ define "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 }}
<nav>
{{ with menu .Blog "main" }}
{{ with index .Blog.Menus "main" }}
{{ range $i, $item := .Items }}{{ if ne $i 0 }} &bull; {{ end }}<a href="{{ $item.Link }}">{{ $item.Title }}</a>{{ end }}
{{ end }}
</nav>
{{ if .LoggedIn }}
<nav>
<a href="{{ blogrelative .Blog "/editor" }}">{{ string .Blog.Lang "editor" }}</a>
<a href="{{ .Blog.RelativePath "/editor" }}">{{ string .Blog.Lang "editor" }}</a>
&bull; <a href="/notifications">{{ string .Blog.Lang "notifications" }}</a>
{{ if .WebmentionReceivingEnabled }}&bull; <a href="/webmention">{{ string .Blog.Lang "webmentions" }}</a>{{ end }}
{{ if .CommentsEnabled }}&bull; <a href="{{ blogrelative .Blog "/comment" }}">{{ string .Blog.Lang "comments" }}</a>{{ end }}
{{ if .CommentsEnabled }}&bull; <a href="{{ .Blog.RelativePath "/comment" }}">{{ string .Blog.Lang "comments" }}</a>{{ end }}
&bull; <a href="/logout">{{ string .Blog.Lang "logout" }}</a>
</nav>
{{ end }}

View File

@ -20,7 +20,7 @@
<input type="hidden" name="target" value="{{ .Canonical }}">
<input class="fw" type="submit" value="{{ string .Blog.Lang "send" }}">
</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="text" name="name" placeholder="{{ string .Blog.Lang "nameopt" }}">
<input type="url" name="website" placeholder="{{ string .Blog.Lang "websiteopt" }}">

View File

@ -1,7 +1,5 @@
{{ define "oldcontentwarning" }}
{{ if .Data.Published }}
{{ if (datebefore (dateadd .Data.Published 1 0 0) now) }}
{{ if .Data.Old }}
<strong class="p border-top border-bottom">{{ string .Blog.Lang "oldcontent" }}</strong>
{{ end }}
{{ end }}
{{ end }}

View File

@ -1,5 +1,5 @@
{{ define "title" }}
<title>{{ with title .Data }}{{ . }} - {{end}}{{ .Blog.Title }}</title>
<title>{{ with .Data.Title }}{{ . }} - {{end}}{{ .Blog.Title }}</title>
{{ include "postheadmeta" . }}
{{ with shorturl .Data }}<link rel="shortlink" href="{{ . }}">{{ end }}
{{ end }}
@ -8,7 +8,7 @@
<main class=h-entry>
<article>
<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 "postactions" . }}
{{ if .Data.Content }}
@ -29,12 +29,12 @@
</main>
{{ if .LoggedIn }}
<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="url" value="{{ .Canonical }}">
<input type="submit" value="{{ string .Blog.Lang "update" }}">
</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="url" value="{{ .Canonical }}">
<input type="submit" value="{{ string .Blog.Lang "delete" }}">

View File

@ -1,6 +1,6 @@
{{ define "postactions" }}
<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>&nbsp;
<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>&nbsp;
<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>&nbsp;
<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>

View File

@ -3,7 +3,7 @@
<meta property="og:url" content="{{ . }}">
<meta property="twitter:url" content="{{ . }}">
{{ end }}
{{ with title .Data }}
{{ with .Data.Title }}
<meta property="og:title" content="{{ . }}">
<meta property="twitter:title" content="{{ . }}">
{{ end }}

View File

@ -2,19 +2,17 @@
<div class="p">
{{ include "summaryandpostmeta" . }}
{{ $bloglang := .Blog.Lang }}
{{ $loc := ( p .Data "location" ) }}
{{ if $loc }}
{{ with geouri $loc }}
<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">{{ with ( geourip . "name" ) }}{{ . }}{{ else }}{{ with ( geotitle .Latitude .Longitude $bloglang ) }}{{ . }}{{ else }}{{ .Latitude }}, {{ .Longitude }}{{ end }}{{ end }}</span>
<data class="p-longitude" value="{{ .Longitude }}" />
<data class="p-latitude" value="{{ .Latitude }}" />
{{ $geo := .Data.GeoURI }}
{{ if $geo }}
<div>📍 <a class="p-location h-geo" href="{{ geolink $geo }}" target="_blank" rel="nofollow noopener noreferrer">
<span class="p-name">{{ geotitle $geo .Blog.Lang }}</span>
<data class="p-longitude" value="{{ $geo.Longitude }}" />
<data class="p-latitude" value="{{ $geo.Latitude }}" />
</a></div>
{{ end }}
{{ end }}
{{ $translations := (translations .Data) }}
{{ 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 }}
{{ $short := shorturl .Data }}
{{ if $short }}<div>{{ string .Blog.Lang "shorturl" }} <a href="{{ $short }}" rel="shortlink">{{ $short }}</a></div>{{ end }}

View File

@ -6,7 +6,7 @@
{{ if gt (len $tvs) 0 }}
<p><b>{{ $tax.Title }}</b>:
{{ 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 }}
</p>
{{ end }}

View File

@ -1,6 +1,6 @@
{{ define "summaryandpostmeta" }}
{{ $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 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>

View File

@ -12,7 +12,7 @@
<h2>{{ $valueGroup.Identifier }}</h2>
<p>
{{ range $i, $value := $valueGroup.Strings }}
{{ if ne $i 0 }} &bull; {{ end }}<a href="{{ blogrelative $blog ( printf "/%s/%s" $taxonomy (urlize .) ) }}">{{ . }}</a>
{{ if ne $i 0 }} &bull; {{ end }}<a href="{{ $blog.RelativePath ( printf "/%s/%s" $taxonomy (urlize .) ) }}">{{ . }}</a>
{{ end }}
</p>
{{ end }}

2
tor.go
View File

@ -16,7 +16,7 @@ import (
"github.com/go-chi/chi/v5/middleware"
)
var torUsedKey requestContextKey = "tor"
const torUsedKey requestContextKey = "tor"
func (a *goBlog) startOnionService(h http.Handler) error {
torDataPath, err := filepath.Abs("data/tor")

View File

@ -1,11 +1,13 @@
package main
import (
"html/template"
"io"
"net/http"
"net/url"
"sort"
"strings"
"time"
"unicode"
"github.com/PuerkitoBio/goquery"
@ -156,6 +158,18 @@ func dateFormat(date string, format string) string {
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 {
First, Second string
}
@ -173,3 +187,7 @@ func charCount(s string) (count int) {
}
return count
}
func wrapStringAsHTML(s string) template.HTML {
return template.HTML(s)
}

View File

@ -8,6 +8,8 @@ import (
"net/http/httptest"
"strings"
"time"
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
)
type webmentionStatus string
@ -63,7 +65,7 @@ func (a *goBlog) handleWebmention(w http.ResponseWriter, r *http.Request) {
}
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")
}
err := r.ParseForm()
@ -187,6 +189,15 @@ func (db *database) getWebmentions(config *webmentionsRequestConfig) ([]*mention
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) {
query, params := buildWebmentionsQuery(config)
query = "select count(*) from (" + query + ")"

View File

@ -8,6 +8,7 @@ import (
"net/url"
"strings"
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
"github.com/PuerkitoBio/goquery"
"github.com/thoas/go-funk"
"github.com/tomnomnom/linkheader"
@ -19,7 +20,7 @@ func (a *goBlog) sendWebmentions(p *post) error {
return nil
}
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 {
return err
}
@ -67,7 +68,7 @@ func sendWebmention(endpoint, source, target string) error {
if err != nil {
return err
}
req.Header.Set(contentType, contentTypeWWWForm)
req.Header.Set(contentType, contenttype.WWWForm)
req.Header.Set(userAgent, appUserAgent)
res, err := appHttpClient.Do(req)
if err != nil {

View File

@ -84,6 +84,7 @@ func (a *goBlog) verifyMention(m *mention) error {
}
}
err = m.verifyReader(resp.Body)
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
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))
@ -162,12 +163,30 @@ func (m *mention) fill(mf *microformats.Microformat) bool {
}
}
// 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 title, ok := name[0].(string); ok {
m.Title = strings.TrimSpace(title)
}
}
// Content
}
func (m *mention) fillContent(mf *microformats.Microformat) {
if contents, ok := mf.Properties["content"]; ok && len(contents) > 0 {
if content, ok := contents[0].(map[string]string); 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 author, ok := authors[0].(*microformats.Microformat); ok {
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 {