mirror of https://github.com/jlelse/GoBlog
Big refactoring: Avoid global vars almost everywhere
This commit is contained in:
parent
9f9ff58a0d
commit
9714d65679
|
@ -1,15 +1,33 @@
|
||||||
{
|
{
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"label": "Build",
|
"label": "Build",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "go build --tags \"libsqlite3 linux sqlite_fts5\"",
|
"command": "go build",
|
||||||
"problemMatcher": [],
|
"options": {
|
||||||
"group": {
|
"env": {
|
||||||
"kind": "build",
|
"GOFLAGS": "-tags=linux,libsqlite3,sqlite_fts5"
|
||||||
"isDefault": true
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
}
|
}
|
||||||
]
|
},
|
||||||
}
|
{
|
||||||
|
"label": "Test",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "go test",
|
||||||
|
"options": {
|
||||||
|
"env": {
|
||||||
|
"GOFLAGS": "-tags=linux,libsqlite3,sqlite_fts5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"group": {
|
||||||
|
"kind": "test",
|
||||||
|
"isDefault": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
186
activityPub.go
186
activityPub.go
|
@ -1,7 +1,6 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
@ -14,50 +13,41 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-fed/httpsig"
|
"github.com/go-fed/httpsig"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
func (a *goBlog) initActivityPub() error {
|
||||||
apPrivateKey *rsa.PrivateKey
|
if !a.cfg.ActivityPub.Enabled {
|
||||||
apPostSigner httpsig.Signer
|
|
||||||
apPostSignMutex *sync.Mutex = &sync.Mutex{}
|
|
||||||
webfingerResources map[string]*configBlog
|
|
||||||
webfingerAccts map[string]string
|
|
||||||
)
|
|
||||||
|
|
||||||
func initActivityPub() error {
|
|
||||||
if !appConfig.ActivityPub.Enabled {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// Add hooks
|
// Add hooks
|
||||||
postPostHooks = append(postPostHooks, func(p *post) {
|
a.pPostHooks = append(a.pPostHooks, func(p *post) {
|
||||||
if p.isPublishedSectionPost() {
|
if p.isPublishedSectionPost() {
|
||||||
p.apPost()
|
a.apPost(p)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
postUpdateHooks = append(postUpdateHooks, func(p *post) {
|
a.pUpdateHooks = append(a.pUpdateHooks, func(p *post) {
|
||||||
if p.isPublishedSectionPost() {
|
if p.isPublishedSectionPost() {
|
||||||
p.apUpdate()
|
a.apUpdate(p)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
postDeleteHooks = append(postDeleteHooks, func(p *post) {
|
a.pDeleteHooks = append(a.pDeleteHooks, func(p *post) {
|
||||||
p.apDelete()
|
a.apDelete(p)
|
||||||
})
|
})
|
||||||
// Prepare webfinger
|
// Prepare webfinger
|
||||||
webfingerResources = map[string]*configBlog{}
|
a.webfingerResources = map[string]*configBlog{}
|
||||||
webfingerAccts = map[string]string{}
|
a.webfingerAccts = map[string]string{}
|
||||||
for name, blog := range appConfig.Blogs {
|
for name, blog := range a.cfg.Blogs {
|
||||||
acct := "acct:" + name + "@" + appConfig.Server.publicHostname
|
acct := "acct:" + name + "@" + a.cfg.Server.publicHostname
|
||||||
webfingerResources[acct] = blog
|
a.webfingerResources[acct] = blog
|
||||||
webfingerResources[blog.apIri()] = blog
|
a.webfingerResources[a.apIri(blog)] = blog
|
||||||
webfingerAccts[blog.apIri()] = acct
|
a.webfingerAccts[a.apIri(blog)] = acct
|
||||||
}
|
}
|
||||||
// Read key and prepare signing
|
// Read key and prepare signing
|
||||||
pkfile, err := os.ReadFile(appConfig.ActivityPub.KeyPath)
|
pkfile, err := os.ReadFile(a.cfg.ActivityPub.KeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -65,11 +55,11 @@ func initActivityPub() error {
|
||||||
if privateKeyDecoded == nil {
|
if privateKeyDecoded == nil {
|
||||||
return errors.New("failed to decode private key")
|
return errors.New("failed to decode private key")
|
||||||
}
|
}
|
||||||
apPrivateKey, err = x509.ParsePKCS1PrivateKey(privateKeyDecoded.Bytes)
|
a.apPrivateKey, err = x509.ParsePKCS1PrivateKey(privateKeyDecoded.Bytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
apPostSigner, _, err = httpsig.NewSigner(
|
a.apPostSigner, _, err = httpsig.NewSigner(
|
||||||
[]httpsig.Algorithm{httpsig.RSA_SHA256},
|
[]httpsig.Algorithm{httpsig.RSA_SHA256},
|
||||||
httpsig.DigestSha256,
|
httpsig.DigestSha256,
|
||||||
[]string{httpsig.RequestTarget, "date", "host", "digest"},
|
[]string{httpsig.RequestTarget, "date", "host", "digest"},
|
||||||
|
@ -80,32 +70,32 @@ func initActivityPub() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Init send queue
|
// Init send queue
|
||||||
initAPSendQueue()
|
a.initAPSendQueue()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func apHandleWebfinger(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) apHandleWebfinger(w http.ResponseWriter, r *http.Request) {
|
||||||
blog, ok := webfingerResources[r.URL.Query().Get("resource")]
|
blog, ok := a.webfingerResources[r.URL.Query().Get("resource")]
|
||||||
if !ok {
|
if !ok {
|
||||||
serveError(w, r, "Resource not found", http.StatusNotFound)
|
a.serveError(w, r, "Resource not found", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
b, _ := json.Marshal(map[string]interface{}{
|
b, _ := json.Marshal(map[string]interface{}{
|
||||||
"subject": webfingerAccts[blog.apIri()],
|
"subject": a.webfingerAccts[a.apIri(blog)],
|
||||||
"aliases": []string{
|
"aliases": []string{
|
||||||
webfingerAccts[blog.apIri()],
|
a.webfingerAccts[a.apIri(blog)],
|
||||||
blog.apIri(),
|
a.apIri(blog),
|
||||||
},
|
},
|
||||||
"links": []map[string]string{
|
"links": []map[string]string{
|
||||||
{
|
{
|
||||||
"rel": "self",
|
"rel": "self",
|
||||||
"type": contentTypeAS,
|
"type": contentTypeAS,
|
||||||
"href": blog.apIri(),
|
"href": a.apIri(blog),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rel": "http://webfinger.net/rel/profile-page",
|
"rel": "http://webfinger.net/rel/profile-page",
|
||||||
"type": "text/html",
|
"type": "text/html",
|
||||||
"href": blog.apIri(),
|
"href": a.apIri(blog),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -113,19 +103,19 @@ func apHandleWebfinger(w http.ResponseWriter, r *http.Request) {
|
||||||
_, _ = writeMinified(w, contentTypeJSON, b)
|
_, _ = writeMinified(w, contentTypeJSON, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func apHandleInbox(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) apHandleInbox(w http.ResponseWriter, r *http.Request) {
|
||||||
blogName := chi.URLParam(r, "blog")
|
blogName := chi.URLParam(r, "blog")
|
||||||
blog := appConfig.Blogs[blogName]
|
blog := a.cfg.Blogs[blogName]
|
||||||
if blog == nil {
|
if blog == nil {
|
||||||
serveError(w, r, "Inbox not found", http.StatusNotFound)
|
a.serveError(w, r, "Inbox not found", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
blogIri := blog.apIri()
|
blogIri := a.apIri(blog)
|
||||||
// Verify request
|
// Verify request
|
||||||
requestActor, requestKey, requestActorStatus, err := apVerifySignature(r)
|
requestActor, requestKey, requestActorStatus, err := apVerifySignature(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Send 401 because signature could not be verified
|
// Send 401 because signature could not be verified
|
||||||
serveError(w, r, err.Error(), http.StatusUnauthorized)
|
a.serveError(w, r, err.Error(), http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if requestActorStatus != 0 {
|
if requestActorStatus != 0 {
|
||||||
|
@ -134,12 +124,12 @@ func apHandleInbox(w http.ResponseWriter, r *http.Request) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
u.Fragment = ""
|
u.Fragment = ""
|
||||||
u.RawFragment = ""
|
u.RawFragment = ""
|
||||||
_ = apRemoveFollower(blogName, u.String())
|
_ = a.db.apRemoveFollower(blogName, u.String())
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
serveError(w, r, "Error when trying to get request actor", http.StatusBadRequest)
|
a.serveError(w, r, "Error when trying to get request actor", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Parse activity
|
// Parse activity
|
||||||
|
@ -147,29 +137,29 @@ func apHandleInbox(w http.ResponseWriter, r *http.Request) {
|
||||||
err = json.NewDecoder(r.Body).Decode(&activity)
|
err = json.NewDecoder(r.Body).Decode(&activity)
|
||||||
_ = r.Body.Close()
|
_ = r.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, "Failed to decode body", http.StatusBadRequest)
|
a.serveError(w, r, "Failed to decode body", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Get and check activity actor
|
// Get and check activity actor
|
||||||
activityActor, ok := activity["actor"].(string)
|
activityActor, ok := activity["actor"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
serveError(w, r, "actor in activity is no string", http.StatusBadRequest)
|
a.serveError(w, r, "actor in activity is no string", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if activityActor != requestActor.ID {
|
if activityActor != requestActor.ID {
|
||||||
serveError(w, r, "Request actor isn't activity actor", http.StatusForbidden)
|
a.serveError(w, r, "Request actor isn't activity actor", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Do
|
// Do
|
||||||
switch activity["type"] {
|
switch activity["type"] {
|
||||||
case "Follow":
|
case "Follow":
|
||||||
apAccept(blogName, blog, activity)
|
a.apAccept(blogName, blog, activity)
|
||||||
case "Undo":
|
case "Undo":
|
||||||
{
|
{
|
||||||
if object, ok := activity["object"].(map[string]interface{}); ok {
|
if object, ok := activity["object"].(map[string]interface{}); ok {
|
||||||
if objectType, ok := object["type"].(string); ok && objectType == "Follow" {
|
if objectType, ok := object["type"].(string); ok && objectType == "Follow" {
|
||||||
if iri, ok := object["actor"].(string); ok && iri == activityActor {
|
if iri, ok := object["actor"].(string); ok && iri == activityActor {
|
||||||
_ = apRemoveFollower(blogName, activityActor)
|
_ = a.db.apRemoveFollower(blogName, activityActor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -181,13 +171,13 @@ func apHandleInbox(w http.ResponseWriter, r *http.Request) {
|
||||||
id, hasID := object["id"].(string)
|
id, hasID := object["id"].(string)
|
||||||
if hasReplyToString && hasID && len(inReplyTo) > 0 && len(id) > 0 && strings.Contains(inReplyTo, blogIri) {
|
if hasReplyToString && hasID && len(inReplyTo) > 0 && len(id) > 0 && strings.Contains(inReplyTo, blogIri) {
|
||||||
// It's an ActivityPub reply; save reply as webmention
|
// It's an ActivityPub reply; save reply as webmention
|
||||||
_ = createWebmention(id, inReplyTo)
|
_ = a.createWebmention(id, inReplyTo)
|
||||||
} else if content, hasContent := object["content"].(string); hasContent && hasID && len(id) > 0 {
|
} else if content, hasContent := object["content"].(string); hasContent && hasID && len(id) > 0 {
|
||||||
// May be a mention; find links to blog and save them as webmentions
|
// May be a mention; find links to blog and save them as webmentions
|
||||||
if links, err := allLinksFromHTML(strings.NewReader(content), id); err == nil {
|
if links, err := allLinksFromHTML(strings.NewReader(content), id); err == nil {
|
||||||
for _, link := range links {
|
for _, link := range links {
|
||||||
if strings.Contains(link, blogIri) {
|
if strings.Contains(link, blogIri) {
|
||||||
_ = createWebmention(id, link)
|
_ = a.createWebmention(id, link)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -198,21 +188,21 @@ func apHandleInbox(w http.ResponseWriter, r *http.Request) {
|
||||||
case "Block":
|
case "Block":
|
||||||
{
|
{
|
||||||
if object, ok := activity["object"].(string); ok && len(object) > 0 && object == activityActor {
|
if object, ok := activity["object"].(string); ok && len(object) > 0 && object == activityActor {
|
||||||
_ = apRemoveFollower(blogName, activityActor)
|
_ = a.db.apRemoveFollower(blogName, activityActor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "Like":
|
case "Like":
|
||||||
{
|
{
|
||||||
likeObject, likeObjectOk := activity["object"].(string)
|
likeObject, likeObjectOk := activity["object"].(string)
|
||||||
if likeObjectOk && len(likeObject) > 0 && strings.Contains(likeObject, blogIri) {
|
if likeObjectOk && len(likeObject) > 0 && strings.Contains(likeObject, blogIri) {
|
||||||
sendNotification(fmt.Sprintf("%s liked %s", activityActor, likeObject))
|
a.sendNotification(fmt.Sprintf("%s liked %s", activityActor, likeObject))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "Announce":
|
case "Announce":
|
||||||
{
|
{
|
||||||
announceObject, announceObjectOk := activity["object"].(string)
|
announceObject, announceObjectOk := activity["object"].(string)
|
||||||
if announceObjectOk && len(announceObject) > 0 && strings.Contains(announceObject, blogIri) {
|
if announceObjectOk && len(announceObject) > 0 && strings.Contains(announceObject, blogIri) {
|
||||||
sendNotification(fmt.Sprintf("%s announced %s", activityActor, announceObject))
|
a.sendNotification(fmt.Sprintf("%s announced %s", activityActor, announceObject))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -277,8 +267,8 @@ func apGetRemoteActor(iri string) (*asPerson, int, error) {
|
||||||
return actor, 0, nil
|
return actor, 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func apGetAllInboxes(blog string) ([]string, error) {
|
func (db *database) apGetAllInboxes(blog string) ([]string, error) {
|
||||||
rows, err := appDb.query("select distinct inbox from activitypub_followers where blog = @blog", sql.Named("blog", blog))
|
rows, err := db.query("select distinct inbox from activitypub_followers where blog = @blog", sql.Named("blog", blog))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -294,27 +284,27 @@ func apGetAllInboxes(blog string) ([]string, error) {
|
||||||
return inboxes, nil
|
return inboxes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func apAddFollower(blog, follower, inbox string) error {
|
func (db *database) apAddFollower(blog, follower, inbox string) error {
|
||||||
_, err := appDb.exec("insert or replace into activitypub_followers (blog, follower, inbox) values (@blog, @follower, @inbox)", sql.Named("blog", blog), sql.Named("follower", follower), sql.Named("inbox", inbox))
|
_, err := db.exec("insert or replace into activitypub_followers (blog, follower, inbox) values (@blog, @follower, @inbox)", sql.Named("blog", blog), sql.Named("follower", follower), sql.Named("inbox", inbox))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func apRemoveFollower(blog, follower string) error {
|
func (db *database) apRemoveFollower(blog, follower string) error {
|
||||||
_, err := appDb.exec("delete from activitypub_followers where blog = @blog and follower = @follower", sql.Named("blog", blog), sql.Named("follower", follower))
|
_, err := db.exec("delete from activitypub_followers where blog = @blog and follower = @follower", sql.Named("blog", blog), sql.Named("follower", follower))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func apRemoveInbox(inbox string) error {
|
func (db *database) apRemoveInbox(inbox string) error {
|
||||||
_, err := appDb.exec("delete from activitypub_followers where inbox = @inbox", sql.Named("inbox", inbox))
|
_, err := db.exec("delete from activitypub_followers where inbox = @inbox", sql.Named("inbox", inbox))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *post) apPost() {
|
func (a *goBlog) apPost(p *post) {
|
||||||
n := p.toASNote()
|
n := a.toASNote(p)
|
||||||
apSendToAllFollowers(p.Blog, map[string]interface{}{
|
a.apSendToAllFollowers(p.Blog, map[string]interface{}{
|
||||||
"@context": asContext,
|
"@context": asContext,
|
||||||
"actor": appConfig.Blogs[p.Blog].apIri(),
|
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
|
||||||
"id": p.fullURL(),
|
"id": a.fullPostURL(p),
|
||||||
"published": n.Published,
|
"published": n.Published,
|
||||||
"type": "Create",
|
"type": "Create",
|
||||||
"object": n,
|
"object": n,
|
||||||
|
@ -322,46 +312,46 @@ func (p *post) apPost() {
|
||||||
if n.InReplyTo != "" {
|
if n.InReplyTo != "" {
|
||||||
// Is reply, so announce it
|
// Is reply, so announce it
|
||||||
time.Sleep(30 * time.Second)
|
time.Sleep(30 * time.Second)
|
||||||
p.apAnnounce()
|
a.apAnnounce(p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *post) apUpdate() {
|
func (a *goBlog) apUpdate(p *post) {
|
||||||
apSendToAllFollowers(p.Blog, map[string]interface{}{
|
a.apSendToAllFollowers(p.Blog, map[string]interface{}{
|
||||||
"@context": asContext,
|
"@context": asContext,
|
||||||
"actor": appConfig.Blogs[p.Blog].apIri(),
|
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
|
||||||
"id": p.fullURL(),
|
"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"),
|
||||||
"type": "Update",
|
"type": "Update",
|
||||||
"object": p.toASNote(),
|
"object": a.toASNote(p),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *post) apAnnounce() {
|
func (a *goBlog) apAnnounce(p *post) {
|
||||||
apSendToAllFollowers(p.Blog, map[string]interface{}{
|
a.apSendToAllFollowers(p.Blog, map[string]interface{}{
|
||||||
"@context": asContext,
|
"@context": asContext,
|
||||||
"actor": appConfig.Blogs[p.Blog].apIri(),
|
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
|
||||||
"id": p.fullURL() + "#announce",
|
"id": a.fullPostURL(p) + "#announce",
|
||||||
"published": p.toASNote().Published,
|
"published": a.toASNote(p).Published,
|
||||||
"type": "Announce",
|
"type": "Announce",
|
||||||
"object": p.fullURL(),
|
"object": a.fullPostURL(p),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *post) apDelete() {
|
func (a *goBlog) apDelete(p *post) {
|
||||||
apSendToAllFollowers(p.Blog, map[string]interface{}{
|
a.apSendToAllFollowers(p.Blog, map[string]interface{}{
|
||||||
"@context": asContext,
|
"@context": asContext,
|
||||||
"actor": appConfig.Blogs[p.Blog].apIri(),
|
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
|
||||||
"id": p.fullURL() + "#delete",
|
"id": a.fullPostURL(p) + "#delete",
|
||||||
"type": "Delete",
|
"type": "Delete",
|
||||||
"object": map[string]string{
|
"object": map[string]string{
|
||||||
"id": p.fullURL(),
|
"id": a.fullPostURL(p),
|
||||||
"type": "Tombstone",
|
"type": "Tombstone",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func apAccept(blogName string, blog *configBlog, follow map[string]interface{}) {
|
func (a *goBlog) apAccept(blogName string, blog *configBlog, follow map[string]interface{}) {
|
||||||
// it's a follow, write it down
|
// it's a follow, write it down
|
||||||
newFollower := follow["actor"].(string)
|
newFollower := follow["actor"].(string)
|
||||||
log.Println("New follow request:", newFollower)
|
log.Println("New follow request:", newFollower)
|
||||||
|
@ -381,7 +371,7 @@ func apAccept(blogName string, blog *configBlog, follow map[string]interface{})
|
||||||
if endpoints := follower.Endpoints; endpoints != nil && endpoints.SharedInbox != "" {
|
if endpoints := follower.Endpoints; endpoints != nil && endpoints.SharedInbox != "" {
|
||||||
inbox = endpoints.SharedInbox
|
inbox = endpoints.SharedInbox
|
||||||
}
|
}
|
||||||
if err = apAddFollower(blogName, follower.ID, inbox); err != nil {
|
if err = a.db.apAddFollower(blogName, follower.ID, inbox); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// remove @context from the inner activity
|
// remove @context from the inner activity
|
||||||
|
@ -389,37 +379,37 @@ func apAccept(blogName string, blog *configBlog, follow map[string]interface{})
|
||||||
accept := map[string]interface{}{
|
accept := map[string]interface{}{
|
||||||
"@context": asContext,
|
"@context": asContext,
|
||||||
"to": follow["actor"],
|
"to": follow["actor"],
|
||||||
"actor": blog.apIri(),
|
"actor": a.apIri(blog),
|
||||||
"object": follow,
|
"object": follow,
|
||||||
"type": "Accept",
|
"type": "Accept",
|
||||||
}
|
}
|
||||||
_, accept["id"] = apNewID(blog)
|
_, accept["id"] = a.apNewID(blog)
|
||||||
_ = apQueueSendSigned(blog.apIri(), follower.Inbox, accept)
|
_ = a.db.apQueueSendSigned(a.apIri(blog), follower.Inbox, accept)
|
||||||
}
|
}
|
||||||
|
|
||||||
func apSendToAllFollowers(blog string, activity interface{}) {
|
func (a *goBlog) apSendToAllFollowers(blog string, activity interface{}) {
|
||||||
inboxes, err := apGetAllInboxes(blog)
|
inboxes, err := a.db.apGetAllInboxes(blog)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Failed to retrieve inboxes:", err.Error())
|
log.Println("Failed to retrieve inboxes:", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
apSendTo(appConfig.Blogs[blog].apIri(), activity, inboxes)
|
a.db.apSendTo(a.apIri(a.cfg.Blogs[blog]), activity, inboxes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func apSendTo(blogIri string, activity interface{}, inboxes []string) {
|
func (db *database) apSendTo(blogIri string, activity interface{}, inboxes []string) {
|
||||||
for _, i := range inboxes {
|
for _, i := range inboxes {
|
||||||
go func(inbox string) {
|
go func(inbox string) {
|
||||||
_ = apQueueSendSigned(blogIri, inbox, activity)
|
_ = db.apQueueSendSigned(blogIri, inbox, activity)
|
||||||
}(i)
|
}(i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func apNewID(blog *configBlog) (hash string, url string) {
|
func (a *goBlog) apNewID(blog *configBlog) (hash string, url string) {
|
||||||
return hash, blog.apIri() + generateRandomString(16)
|
return hash, a.apIri(blog) + generateRandomString(16)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *configBlog) apIri() string {
|
func (a *goBlog) apIri(b *configBlog) string {
|
||||||
return appConfig.Server.PublicAddress + b.Path
|
return a.cfg.Server.PublicAddress + b.Path
|
||||||
}
|
}
|
||||||
|
|
||||||
func apRequestIsSuccess(code int) bool {
|
func apRequestIsSuccess(code int) bool {
|
||||||
|
|
|
@ -19,10 +19,10 @@ type apRequest struct {
|
||||||
Try int
|
Try int
|
||||||
}
|
}
|
||||||
|
|
||||||
func initAPSendQueue() {
|
func (a *goBlog) initAPSendQueue() {
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
qi, err := peekQueue("ap")
|
qi, err := a.db.peekQueue("ap")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err.Error())
|
log.Println(err.Error())
|
||||||
continue
|
continue
|
||||||
|
@ -31,22 +31,22 @@ func initAPSendQueue() {
|
||||||
err = gob.NewDecoder(bytes.NewReader(qi.content)).Decode(&r)
|
err = gob.NewDecoder(bytes.NewReader(qi.content)).Decode(&r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err.Error())
|
log.Println(err.Error())
|
||||||
_ = qi.dequeue()
|
_ = a.db.dequeue(qi)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := apSendSigned(r.BlogIri, r.To, r.Activity); err != nil {
|
if err := a.apSendSigned(r.BlogIri, r.To, r.Activity); err != nil {
|
||||||
if r.Try++; r.Try < 20 {
|
if r.Try++; r.Try < 20 {
|
||||||
// Try it again
|
// Try it again
|
||||||
qi.content, _ = r.encode()
|
qi.content, _ = r.encode()
|
||||||
_ = qi.reschedule(time.Duration(r.Try) * 10 * time.Minute)
|
_ = a.db.reschedule(qi, time.Duration(r.Try)*10*time.Minute)
|
||||||
continue
|
continue
|
||||||
} else {
|
} else {
|
||||||
log.Printf("Request to %s failed for the 20th time", r.To)
|
log.Printf("Request to %s failed for the 20th time", r.To)
|
||||||
log.Println()
|
log.Println()
|
||||||
_ = apRemoveInbox(r.To)
|
_ = a.db.apRemoveInbox(r.To)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = qi.dequeue()
|
err = a.db.dequeue(qi)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err.Error())
|
log.Println(err.Error())
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ func initAPSendQueue() {
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func apQueueSendSigned(blogIri, to string, activity interface{}) error {
|
func (db *database) apQueueSendSigned(blogIri, to string, activity interface{}) error {
|
||||||
body, err := json.Marshal(activity)
|
body, err := json.Marshal(activity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -71,7 +71,7 @@ func apQueueSendSigned(blogIri, to string, activity interface{}) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return enqueue("ap", b, time.Now())
|
return db.enqueue("ap", b, time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *apRequest) encode() ([]byte, error) {
|
func (r *apRequest) encode() ([]byte, error) {
|
||||||
|
@ -83,7 +83,7 @@ func (r *apRequest) encode() ([]byte, error) {
|
||||||
return buf.Bytes(), nil
|
return buf.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func apSendSigned(blogIri, to string, activity []byte) error {
|
func (a *goBlog) apSendSigned(blogIri, to string, activity []byte) error {
|
||||||
// Create request context with timeout
|
// Create request context with timeout
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
@ -105,9 +105,9 @@ func apSendSigned(blogIri, to string, activity []byte) error {
|
||||||
r.Header.Set(contentType, contentTypeASUTF8)
|
r.Header.Set(contentType, contentTypeASUTF8)
|
||||||
r.Header.Set("Host", iri.Host)
|
r.Header.Set("Host", iri.Host)
|
||||||
// Sign request
|
// Sign request
|
||||||
apPostSignMutex.Lock()
|
a.apPostSignMutex.Lock()
|
||||||
err = apPostSigner.SignRequest(apPrivateKey, blogIri+"#main-key", r, activity)
|
err = a.apPostSigner.SignRequest(a.apPrivateKey, blogIri+"#main-key", r, activity)
|
||||||
apPostSignMutex.Unlock()
|
a.apPostSignMutex.Unlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,9 +22,9 @@ var asCheckMediaTypes = []contenttype.MediaType{
|
||||||
|
|
||||||
const asRequestKey requestContextKey = "asRequest"
|
const asRequestKey requestContextKey = "asRequest"
|
||||||
|
|
||||||
func 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 := appConfig.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 := contenttype.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)))
|
||||||
|
@ -87,21 +87,21 @@ type asEndpoints struct {
|
||||||
SharedInbox string `json:"sharedInbox,omitempty"`
|
SharedInbox string `json:"sharedInbox,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *post) serveActivityStreams(w http.ResponseWriter) {
|
func (a *goBlog) serveActivityStreamsPost(p *post, w http.ResponseWriter) {
|
||||||
b, _ := json.Marshal(p.toASNote())
|
b, _ := json.Marshal(a.toASNote(p))
|
||||||
w.Header().Set(contentType, contentTypeASUTF8)
|
w.Header().Set(contentType, contentTypeASUTF8)
|
||||||
_, _ = writeMinified(w, contentTypeAS, b)
|
_, _ = writeMinified(w, contentTypeAS, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *post) toASNote() *asNote {
|
func (a *goBlog) toASNote(p *post) *asNote {
|
||||||
// Create a Note object
|
// Create a Note object
|
||||||
as := &asNote{
|
as := &asNote{
|
||||||
Context: asContext,
|
Context: asContext,
|
||||||
To: []string{"https://www.w3.org/ns/activitystreams#Public"},
|
To: []string{"https://www.w3.org/ns/activitystreams#Public"},
|
||||||
MediaType: contentTypeHTML,
|
MediaType: contentTypeHTML,
|
||||||
ID: p.fullURL(),
|
ID: a.fullPostURL(p),
|
||||||
URL: p.fullURL(),
|
URL: a.fullPostURL(p),
|
||||||
AttributedTo: appConfig.Blogs[p.Blog].apIri(),
|
AttributedTo: a.apIri(a.cfg.Blogs[p.Blog]),
|
||||||
}
|
}
|
||||||
// Name and Type
|
// Name and Type
|
||||||
if title := p.title(); title != "" {
|
if title := p.title(); title != "" {
|
||||||
|
@ -111,9 +111,9 @@ func (p *post) toASNote() *asNote {
|
||||||
as.Type = "Note"
|
as.Type = "Note"
|
||||||
}
|
}
|
||||||
// Content
|
// Content
|
||||||
as.Content = string(p.absoluteHTML())
|
as.Content = string(a.absoluteHTML(p))
|
||||||
// Attachments
|
// Attachments
|
||||||
if images := p.Parameters[appConfig.Micropub.PhotoParam]; len(images) > 0 {
|
if images := p.Parameters[a.cfg.Micropub.PhotoParam]; len(images) > 0 {
|
||||||
for _, image := range images {
|
for _, image := range images {
|
||||||
as.Attachment = append(as.Attachment, &asAttachment{
|
as.Attachment = append(as.Attachment, &asAttachment{
|
||||||
Type: "Image",
|
Type: "Image",
|
||||||
|
@ -122,12 +122,12 @@ func (p *post) toASNote() *asNote {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Tags
|
// Tags
|
||||||
for _, tagTax := range appConfig.ActivityPub.TagsTaxonomies {
|
for _, tagTax := range a.cfg.ActivityPub.TagsTaxonomies {
|
||||||
for _, tag := range p.Parameters[tagTax] {
|
for _, tag := range p.Parameters[tagTax] {
|
||||||
as.Tag = append(as.Tag, &asTag{
|
as.Tag = append(as.Tag, &asTag{
|
||||||
Type: "Hashtag",
|
Type: "Hashtag",
|
||||||
Name: tag,
|
Name: tag,
|
||||||
Href: appConfig.Server.PublicAddress + appConfig.Blogs[p.Blog].getRelativePath(fmt.Sprintf("/%s/%s", tagTax, urlize(tag))),
|
Href: a.cfg.Server.PublicAddress + a.cfg.Blogs[p.Blog].getRelativePath(fmt.Sprintf("/%s/%s", tagTax, urlize(tag))),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -144,30 +144,31 @@ func (p *post) toASNote() *asNote {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Reply
|
// Reply
|
||||||
if replyLink := p.firstParameter(appConfig.Micropub.ReplyParam); replyLink != "" {
|
if replyLink := p.firstParameter(a.cfg.Micropub.ReplyParam); replyLink != "" {
|
||||||
as.InReplyTo = replyLink
|
as.InReplyTo = replyLink
|
||||||
}
|
}
|
||||||
return as
|
return as
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *configBlog) serveActivityStreams(blog string, w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveActivityStreams(blog string, w http.ResponseWriter, r *http.Request) {
|
||||||
publicKeyDer, err := x509.MarshalPKIXPublicKey(&apPrivateKey.PublicKey)
|
b := a.cfg.Blogs[blog]
|
||||||
|
publicKeyDer, err := x509.MarshalPKIXPublicKey(&(a.apPrivateKey.PublicKey))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, "Failed to marshal public key", http.StatusInternalServerError)
|
a.serveError(w, r, "Failed to marshal public key", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
asBlog := &asPerson{
|
asBlog := &asPerson{
|
||||||
Context: asContext,
|
Context: asContext,
|
||||||
Type: "Person",
|
Type: "Person",
|
||||||
ID: b.apIri(),
|
ID: a.apIri(b),
|
||||||
URL: b.apIri(),
|
URL: a.apIri(b),
|
||||||
Name: b.Title,
|
Name: b.Title,
|
||||||
Summary: b.Description,
|
Summary: b.Description,
|
||||||
PreferredUsername: blog,
|
PreferredUsername: blog,
|
||||||
Inbox: appConfig.Server.PublicAddress + "/activitypub/inbox/" + blog,
|
Inbox: a.cfg.Server.PublicAddress + "/activitypub/inbox/" + blog,
|
||||||
PublicKey: &asPublicKey{
|
PublicKey: &asPublicKey{
|
||||||
Owner: b.apIri(),
|
Owner: a.apIri(b),
|
||||||
ID: b.apIri() + "#main-key",
|
ID: a.apIri(b) + "#main-key",
|
||||||
PublicKeyPem: string(pem.EncodeToMemory(&pem.Block{
|
PublicKeyPem: string(pem.EncodeToMemory(&pem.Block{
|
||||||
Type: "PUBLIC KEY",
|
Type: "PUBLIC KEY",
|
||||||
Headers: nil,
|
Headers: nil,
|
||||||
|
@ -176,10 +177,10 @@ func (b *configBlog) serveActivityStreams(blog string, w http.ResponseWriter, r
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
// Add profile picture
|
// Add profile picture
|
||||||
if appConfig.User.Picture != "" {
|
if a.cfg.User.Picture != "" {
|
||||||
asBlog.Icon = &asAttachment{
|
asBlog.Icon = &asAttachment{
|
||||||
Type: "Image",
|
Type: "Image",
|
||||||
URL: appConfig.User.Picture,
|
URL: a.cfg.User.Picture,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jb, _ := json.Marshal(asBlog)
|
jb, _ := json.Marshal(asBlog)
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rsa"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
ts "git.jlel.se/jlelse/template-strings"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/go-fed/httpsig"
|
||||||
|
rotatelogs "github.com/lestrrat-go/file-rotatelogs"
|
||||||
|
"github.com/yuin/goldmark"
|
||||||
|
"golang.org/x/sync/singleflight"
|
||||||
|
)
|
||||||
|
|
||||||
|
type goBlog struct {
|
||||||
|
// ActivityPub
|
||||||
|
apPrivateKey *rsa.PrivateKey
|
||||||
|
apPostSigner httpsig.Signer
|
||||||
|
apPostSignMutex sync.Mutex
|
||||||
|
webfingerResources map[string]*configBlog
|
||||||
|
webfingerAccts map[string]string
|
||||||
|
// Assets
|
||||||
|
assetFileNames map[string]string
|
||||||
|
assetFiles map[string]*assetFile
|
||||||
|
// Blogroll
|
||||||
|
blogrollCacheGroup singleflight.Group
|
||||||
|
// Cache
|
||||||
|
cache *cache
|
||||||
|
// Config
|
||||||
|
cfg *config
|
||||||
|
// Database
|
||||||
|
db *database
|
||||||
|
// Hooks
|
||||||
|
pPostHooks []postHookFunc
|
||||||
|
pUpdateHooks []postHookFunc
|
||||||
|
pDeleteHooks []postHookFunc
|
||||||
|
// HTTP
|
||||||
|
d *dynamicHandler
|
||||||
|
privateMode bool
|
||||||
|
privateModeHandler []func(http.Handler) http.Handler
|
||||||
|
captchaHandler http.Handler
|
||||||
|
micropubRouter *chi.Mux
|
||||||
|
indieAuthRouter *chi.Mux
|
||||||
|
webmentionsRouter *chi.Mux
|
||||||
|
notificationsRouter *chi.Mux
|
||||||
|
activitypubRouter *chi.Mux
|
||||||
|
editorRouter *chi.Mux
|
||||||
|
commentsRouter *chi.Mux
|
||||||
|
searchRouter *chi.Mux
|
||||||
|
setBlogMiddlewares map[string]func(http.Handler) http.Handler
|
||||||
|
sectionMiddlewares map[string]func(http.Handler) http.Handler
|
||||||
|
taxonomyMiddlewares 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
|
||||||
|
commentsMiddlewares map[string]func(http.Handler) http.Handler
|
||||||
|
// Logs
|
||||||
|
logf *rotatelogs.RotateLogs
|
||||||
|
// Markdown
|
||||||
|
md, absoluteMd goldmark.Markdown
|
||||||
|
// Regex Redirects
|
||||||
|
regexRedirects []*regexRedirect
|
||||||
|
// Rendering
|
||||||
|
templates map[string]*template.Template
|
||||||
|
// Sessions
|
||||||
|
loginSessions, captchaSessions *dbSessionStore
|
||||||
|
// Template strings
|
||||||
|
ts *ts.TemplateStrings
|
||||||
|
// Tor
|
||||||
|
torAddress string
|
||||||
|
}
|
|
@ -11,14 +11,14 @@ import (
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func checkCredentials(username, password, totpPasscode string) bool {
|
func (a *goBlog) checkCredentials(username, password, totpPasscode string) bool {
|
||||||
return username == appConfig.User.Nick &&
|
return username == a.cfg.User.Nick &&
|
||||||
password == appConfig.User.Password &&
|
password == a.cfg.User.Password &&
|
||||||
(appConfig.User.TOTP == "" || totp.Validate(totpPasscode, appConfig.User.TOTP))
|
(a.cfg.User.TOTP == "" || totp.Validate(totpPasscode, a.cfg.User.TOTP))
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkAppPasswords(username, password string) bool {
|
func (a *goBlog) checkAppPasswords(username, password string) bool {
|
||||||
for _, apw := range appConfig.User.AppPasswords {
|
for _, apw := range a.cfg.User.AppPasswords {
|
||||||
if apw.Username == username && apw.Password == password {
|
if apw.Username == username && apw.Password == password {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -26,11 +26,11 @@ func checkAppPasswords(username, password string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func jwtKey() []byte {
|
func (a *goBlog) jwtKey() []byte {
|
||||||
return []byte(appConfig.Server.JWTSecret)
|
return []byte(a.cfg.Server.JWTSecret)
|
||||||
}
|
}
|
||||||
|
|
||||||
func authMiddleware(next http.Handler) http.Handler {
|
func (a *goBlog) authMiddleware(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// 1. Check if already logged in
|
// 1. Check if already logged in
|
||||||
if loggedIn, ok := r.Context().Value(loggedInKey).(bool); ok && loggedIn {
|
if loggedIn, ok := r.Context().Value(loggedInKey).(bool); ok && loggedIn {
|
||||||
|
@ -38,12 +38,12 @@ func authMiddleware(next http.Handler) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 2. Check BasicAuth (just for app passwords)
|
// 2. Check BasicAuth (just for app passwords)
|
||||||
if username, password, ok := r.BasicAuth(); ok && checkAppPasswords(username, password) {
|
if username, password, ok := r.BasicAuth(); ok && a.checkAppPasswords(username, password) {
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 3. Check login cookie
|
// 3. Check login cookie
|
||||||
if checkLoginCookie(r) {
|
if a.checkLoginCookie(r) {
|
||||||
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), loggedInKey, true)))
|
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), loggedInKey, true)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -57,12 +57,12 @@ func authMiddleware(next http.Handler) http.Handler {
|
||||||
_ = r.ParseForm()
|
_ = r.ParseForm()
|
||||||
b = []byte(r.PostForm.Encode())
|
b = []byte(r.PostForm.Encode())
|
||||||
}
|
}
|
||||||
render(w, r, templateLogin, &renderData{
|
a.render(w, r, templateLogin, &renderData{
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"loginmethod": r.Method,
|
"loginmethod": r.Method,
|
||||||
"loginheaders": base64.StdEncoding.EncodeToString(h),
|
"loginheaders": base64.StdEncoding.EncodeToString(h),
|
||||||
"loginbody": base64.StdEncoding.EncodeToString(b),
|
"loginbody": base64.StdEncoding.EncodeToString(b),
|
||||||
"totp": appConfig.User.TOTP != "",
|
"totp": a.cfg.User.TOTP != "",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -70,9 +70,9 @@ func authMiddleware(next http.Handler) http.Handler {
|
||||||
|
|
||||||
const loggedInKey requestContextKey = "loggedIn"
|
const loggedInKey requestContextKey = "loggedIn"
|
||||||
|
|
||||||
func checkLoggedIn(next http.Handler) http.Handler {
|
func (a *goBlog) checkLoggedIn(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 checkLoginCookie(r) {
|
if a.checkLoginCookie(r) {
|
||||||
next.ServeHTTP(rw, r.WithContext(context.WithValue(r.Context(), loggedInKey, true)))
|
next.ServeHTTP(rw, r.WithContext(context.WithValue(r.Context(), loggedInKey, true)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -80,8 +80,8 @@ func checkLoggedIn(next http.Handler) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkLoginCookie(r *http.Request) bool {
|
func (a *goBlog) checkLoginCookie(r *http.Request) bool {
|
||||||
ses, err := loginSessionsStore.Get(r, "l")
|
ses, err := a.loginSessions.Get(r, "l")
|
||||||
if err == nil && ses != nil {
|
if err == nil && ses != nil {
|
||||||
if login, ok := ses.Values["login"]; ok && login.(bool) {
|
if login, ok := ses.Values["login"]; ok && login.(bool) {
|
||||||
return true
|
return true
|
||||||
|
@ -90,15 +90,15 @@ func checkLoginCookie(r *http.Request) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkIsLogin(next http.Handler) http.Handler {
|
func (a *goBlog) checkIsLogin(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 !checkLogin(rw, r) {
|
if !a.checkLogin(rw, r) {
|
||||||
next.ServeHTTP(rw, r)
|
next.ServeHTTP(rw, r)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkLogin(w http.ResponseWriter, r *http.Request) bool {
|
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
|
||||||
}
|
}
|
||||||
|
@ -109,8 +109,8 @@ func checkLogin(w http.ResponseWriter, r *http.Request) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// Check credential
|
// Check credential
|
||||||
if !checkCredentials(r.FormValue("username"), r.FormValue("password"), r.FormValue("token")) {
|
if !a.checkCredentials(r.FormValue("username"), r.FormValue("password"), r.FormValue("token")) {
|
||||||
serveError(w, r, "Incorrect credentials", http.StatusUnauthorized)
|
a.serveError(w, r, "Incorrect credentials", http.StatusUnauthorized)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// Prepare original request
|
// Prepare original request
|
||||||
|
@ -124,20 +124,20 @@ func checkLogin(w http.ResponseWriter, r *http.Request) bool {
|
||||||
req.Header[k] = v
|
req.Header[k] = v
|
||||||
}
|
}
|
||||||
// Cookie
|
// Cookie
|
||||||
ses, err := loginSessionsStore.Get(r, "l")
|
ses, err := a.loginSessions.Get(r, "l")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
ses.Values["login"] = true
|
ses.Values["login"] = true
|
||||||
cookie, err := loginSessionsStore.SaveGetCookie(r, w, ses)
|
cookie, err := a.loginSessions.SaveGetCookie(r, w, ses)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
req.AddCookie(cookie)
|
req.AddCookie(cookie)
|
||||||
// Serve original request
|
// Serve original request
|
||||||
d.ServeHTTP(w, req)
|
a.d.ServeHTTP(w, req)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,9 +146,9 @@ func serveLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Redirect(w, r, "/", http.StatusFound)
|
http.Redirect(w, r, "/", http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveLogout(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveLogout(w http.ResponseWriter, r *http.Request) {
|
||||||
if ses, err := loginSessionsStore.Get(r, "l"); err == nil && ses != nil {
|
if ses, err := a.loginSessions.Get(r, "l"); err == nil && ses != nil {
|
||||||
_ = loginSessionsStore.Delete(r, w, ses)
|
_ = a.loginSessions.Delete(r, w, ses)
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, "/", http.StatusFound)
|
http.Redirect(w, r, "/", http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
59
blogroll.go
59
blogroll.go
|
@ -13,28 +13,25 @@ import (
|
||||||
"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"
|
||||||
"golang.org/x/sync/singleflight"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var blogrollCacheGroup singleflight.Group
|
func (a *goBlog) serveBlogroll(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
func serveBlogroll(w http.ResponseWriter, r *http.Request) {
|
|
||||||
blog := r.Context().Value(blogContextKey).(string)
|
blog := r.Context().Value(blogContextKey).(string)
|
||||||
t := servertiming.FromContext(r.Context()).NewMetric("bg").Start()
|
t := servertiming.FromContext(r.Context()).NewMetric("bg").Start()
|
||||||
outlines, err, _ := blogrollCacheGroup.Do(blog, func() (interface{}, error) {
|
outlines, err, _ := a.blogrollCacheGroup.Do(blog, func() (interface{}, error) {
|
||||||
return getBlogrollOutlines(blog)
|
return a.getBlogrollOutlines(blog)
|
||||||
})
|
})
|
||||||
t.Stop()
|
t.Stop()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Failed to get outlines:", err.Error())
|
log.Printf("Failed to get outlines: %v", err)
|
||||||
serveError(w, r, "", http.StatusInternalServerError)
|
a.serveError(w, r, "", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if appConfig.Cache != nil && appConfig.Cache.Enable {
|
if a.cfg.Cache != nil && a.cfg.Cache.Enable {
|
||||||
setInternalCacheExpirationHeader(w, r, int(appConfig.Cache.Expiration))
|
setInternalCacheExpirationHeader(w, r, int(a.cfg.Cache.Expiration))
|
||||||
}
|
}
|
||||||
c := appConfig.Blogs[blog].Blogroll
|
c := a.cfg.Blogs[blog].Blogroll
|
||||||
render(w, r, templateBlogroll, &renderData{
|
a.render(w, r, templateBlogroll, &renderData{
|
||||||
BlogString: blog,
|
BlogString: blog,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"Title": c.Title,
|
"Title": c.Title,
|
||||||
|
@ -45,34 +42,32 @@ func serveBlogroll(w http.ResponseWriter, r *http.Request) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveBlogrollExport(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveBlogrollExport(w http.ResponseWriter, r *http.Request) {
|
||||||
blog := r.Context().Value(blogContextKey).(string)
|
blog := r.Context().Value(blogContextKey).(string)
|
||||||
outlines, err, _ := blogrollCacheGroup.Do(blog, func() (interface{}, error) {
|
outlines, err, _ := a.blogrollCacheGroup.Do(blog, func() (interface{}, error) {
|
||||||
return getBlogrollOutlines(blog)
|
return a.getBlogrollOutlines(blog)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Failed to get outlines:", err.Error())
|
log.Printf("Failed to get outlines: %v", err)
|
||||||
serveError(w, r, "", http.StatusInternalServerError)
|
a.serveError(w, r, "", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if appConfig.Cache != nil && appConfig.Cache.Enable {
|
if a.cfg.Cache != nil && a.cfg.Cache.Enable {
|
||||||
setInternalCacheExpirationHeader(w, r, int(appConfig.Cache.Expiration))
|
setInternalCacheExpirationHeader(w, r, int(a.cfg.Cache.Expiration))
|
||||||
}
|
}
|
||||||
w.Header().Set(contentType, contentTypeXMLUTF8)
|
w.Header().Set(contentType, contentTypeXMLUTF8)
|
||||||
mw := minifier.Writer(contentTypeXML, w)
|
var opmlBytes bytes.Buffer
|
||||||
defer func() {
|
_ = opml.Render(&opmlBytes, &opml.OPML{
|
||||||
_ = mw.Close()
|
|
||||||
}()
|
|
||||||
_ = opml.Render(mw, &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())
|
||||||
}
|
}
|
||||||
|
|
||||||
func getBlogrollOutlines(blog string) ([]*opml.Outline, error) {
|
func (a *goBlog) getBlogrollOutlines(blog string) ([]*opml.Outline, error) {
|
||||||
config := appConfig.Blogs[blog].Blogroll
|
config := a.cfg.Blogs[blog].Blogroll
|
||||||
if cache := loadOutlineCache(blog); cache != nil {
|
if cache := a.db.loadOutlineCache(blog); cache != nil {
|
||||||
return cache, nil
|
return cache, nil
|
||||||
}
|
}
|
||||||
req, err := http.NewRequest(http.MethodGet, config.Opml, nil)
|
req, err := http.NewRequest(http.MethodGet, config.Opml, nil)
|
||||||
|
@ -112,22 +107,22 @@ func getBlogrollOutlines(blog string) ([]*opml.Outline, error) {
|
||||||
} else {
|
} else {
|
||||||
outlines = sortOutlines(outlines)
|
outlines = sortOutlines(outlines)
|
||||||
}
|
}
|
||||||
cacheOutlines(blog, outlines)
|
a.db.cacheOutlines(blog, outlines)
|
||||||
return outlines, nil
|
return outlines, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func cacheOutlines(blog string, outlines []*opml.Outline) {
|
func (db *database) cacheOutlines(blog string, outlines []*opml.Outline) {
|
||||||
var opmlBuffer bytes.Buffer
|
var opmlBuffer bytes.Buffer
|
||||||
_ = opml.Render(&opmlBuffer, &opml.OPML{
|
_ = opml.Render(&opmlBuffer, &opml.OPML{
|
||||||
Version: "2.0",
|
Version: "2.0",
|
||||||
DateCreated: time.Now().UTC(),
|
DateCreated: time.Now().UTC(),
|
||||||
Outlines: outlines,
|
Outlines: outlines,
|
||||||
})
|
})
|
||||||
_ = cachePersistently("blogroll_"+blog, opmlBuffer.Bytes())
|
_ = db.cachePersistently("blogroll_"+blog, opmlBuffer.Bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadOutlineCache(blog string) []*opml.Outline {
|
func (db *database) loadOutlineCache(blog string) []*opml.Outline {
|
||||||
data, err := retrievePersistentCache("blogroll_" + blog)
|
data, err := db.retrievePersistentCache("blogroll_" + blog)
|
||||||
if err != nil || data == nil {
|
if err != nil || data == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
50
blogstats.go
50
blogstats.go
|
@ -9,19 +9,19 @@ import (
|
||||||
"golang.org/x/sync/singleflight"
|
"golang.org/x/sync/singleflight"
|
||||||
)
|
)
|
||||||
|
|
||||||
func initBlogStats() {
|
func (a *goBlog) initBlogStats() {
|
||||||
f := func(p *post) {
|
f := func(p *post) {
|
||||||
resetBlogStats(p.Blog)
|
a.db.resetBlogStats(p.Blog)
|
||||||
}
|
}
|
||||||
postPostHooks = append(postPostHooks, f)
|
a.pPostHooks = append(a.pPostHooks, f)
|
||||||
postUpdateHooks = append(postUpdateHooks, f)
|
a.pUpdateHooks = append(a.pUpdateHooks, f)
|
||||||
postDeleteHooks = append(postDeleteHooks, f)
|
a.pDeleteHooks = append(a.pDeleteHooks, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveBlogStats(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveBlogStats(w http.ResponseWriter, r *http.Request) {
|
||||||
blog := r.Context().Value(blogContextKey).(string)
|
blog := r.Context().Value(blogContextKey).(string)
|
||||||
canonical := blogPath(blog) + appConfig.Blogs[blog].BlogStats.Path
|
canonical := a.blogPath(blog) + a.cfg.Blogs[blog].BlogStats.Path
|
||||||
render(w, r, templateBlogStats, &renderData{
|
a.render(w, r, templateBlogStats, &renderData{
|
||||||
BlogString: blog,
|
BlogString: blog,
|
||||||
Canonical: canonical,
|
Canonical: canonical,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
|
@ -32,24 +32,24 @@ func serveBlogStats(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
var blogStatsCacheGroup singleflight.Group
|
var blogStatsCacheGroup singleflight.Group
|
||||||
|
|
||||||
func 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, _ := blogStatsCacheGroup.Do(blog, func() (interface{}, error) {
|
||||||
return getBlogStats(blog)
|
return a.db.getBlogStats(blog)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Render
|
// Render
|
||||||
render(w, r, templateBlogStatsTable, &renderData{
|
a.render(w, r, templateBlogStatsTable, &renderData{
|
||||||
BlogString: blog,
|
BlogString: blog,
|
||||||
Data: data,
|
Data: data,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func getBlogStats(blog string) (data map[string]interface{}, err error) {
|
func (db *database) getBlogStats(blog string) (data map[string]interface{}, err error) {
|
||||||
if stats := loadBlogStatsCache(blog); stats != nil {
|
if stats := db.loadBlogStatsCache(blog); stats != nil {
|
||||||
return stats, nil
|
return stats, nil
|
||||||
}
|
}
|
||||||
// Build query
|
// Build query
|
||||||
|
@ -67,7 +67,7 @@ func getBlogStats(blog string) (data map[string]interface{}, err error) {
|
||||||
Name, Posts, Chars, Words, WordsPerPost string
|
Name, Posts, Chars, Words, WordsPerPost string
|
||||||
}
|
}
|
||||||
// Count total posts
|
// Count total posts
|
||||||
row, err := appDb.queryRow("select *, "+wordsPerPost+" from (select "+postCount+", "+charCount+", "+wordCount+" from ("+query+"))", params...)
|
row, err := db.queryRow("select *, "+wordsPerPost+" from (select "+postCount+", "+charCount+", "+wordCount+" from ("+query+"))", params...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@ func getBlogStats(blog string) (data map[string]interface{}, err error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// Count posts per year
|
// Count posts per year
|
||||||
rows, err := appDb.query("select *, "+wordsPerPost+" from (select year, "+postCount+", "+charCount+", "+wordCount+" from ("+query+") where published != '' group by year order by year desc)", params...)
|
rows, err := db.query("select *, "+wordsPerPost+" from (select year, "+postCount+", "+charCount+", "+wordCount+" from ("+query+") where published != '' group by year order by year desc)", params...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -90,7 +90,7 @@ func getBlogStats(blog string) (data map[string]interface{}, err error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Count posts without date
|
// Count posts without date
|
||||||
row, err = appDb.queryRow("select *, "+wordsPerPost+" from (select "+postCount+", "+charCount+", "+wordCount+" from ("+query+") where published = '')", params...)
|
row, err = db.queryRow("select *, "+wordsPerPost+" from (select "+postCount+", "+charCount+", "+wordCount+" from ("+query+") where published = '')", params...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -102,7 +102,7 @@ func getBlogStats(blog string) (data map[string]interface{}, err error) {
|
||||||
months := map[string][]statsTableType{}
|
months := map[string][]statsTableType{}
|
||||||
month := statsTableType{}
|
month := statsTableType{}
|
||||||
for _, year := range years {
|
for _, year := range years {
|
||||||
rows, err = appDb.query("select *, "+wordsPerPost+" from (select month, "+postCount+", "+charCount+", "+wordCount+" from ("+query+") where published != '' and year = @year group by month order by month desc)", append(params, sql.Named("year", year.Name))...)
|
rows, err = db.query("select *, "+wordsPerPost+" from (select month, "+postCount+", "+charCount+", "+wordCount+" from ("+query+") where published != '' and year = @year group by month order by month desc)", append(params, sql.Named("year", year.Name))...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -120,17 +120,17 @@ func getBlogStats(blog string) (data map[string]interface{}, err error) {
|
||||||
"withoutdate": noDate,
|
"withoutdate": noDate,
|
||||||
"months": months,
|
"months": months,
|
||||||
}
|
}
|
||||||
cacheBlogStats(blog, data)
|
db.cacheBlogStats(blog, data)
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func cacheBlogStats(blog string, stats map[string]interface{}) {
|
func (db *database) cacheBlogStats(blog string, stats map[string]interface{}) {
|
||||||
jb, _ := json.Marshal(stats)
|
jb, _ := json.Marshal(stats)
|
||||||
_ = cachePersistently("blogstats_"+blog, jb)
|
_ = db.cachePersistently("blogstats_"+blog, jb)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadBlogStatsCache(blog string) (stats map[string]interface{}) {
|
func (db *database) loadBlogStatsCache(blog string) (stats map[string]interface{}) {
|
||||||
data, err := retrievePersistentCache("blogstats_" + blog)
|
data, err := db.retrievePersistentCache("blogstats_" + blog)
|
||||||
if err != nil || data == nil {
|
if err != nil || data == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -141,6 +141,6 @@ func loadBlogStatsCache(blog string) (stats map[string]interface{}) {
|
||||||
return stats
|
return stats
|
||||||
}
|
}
|
||||||
|
|
||||||
func resetBlogStats(blog string) {
|
func (db *database) resetBlogStats(blog string) {
|
||||||
_ = clearPersistentCache("blogstats_" + blog)
|
_ = db.clearPersistentCache("blogstats_" + blog)
|
||||||
}
|
}
|
||||||
|
|
64
cache.go
64
cache.go
|
@ -20,17 +20,22 @@ import (
|
||||||
"golang.org/x/sync/singleflight"
|
"golang.org/x/sync/singleflight"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const cacheInternalExpirationHeader = "Goblog-Expire"
|
||||||
cacheInternalExpirationHeader = "Goblog-Expire"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
type cache struct {
|
||||||
cacheGroup singleflight.Group
|
g singleflight.Group
|
||||||
cacheR *ristretto.Cache
|
c *ristretto.Cache
|
||||||
)
|
cfg *configCache
|
||||||
|
}
|
||||||
|
|
||||||
func initCache() (err error) {
|
func (a *goBlog) initCache() (err error) {
|
||||||
cacheR, err = ristretto.NewCache(&ristretto.Config{
|
a.cache = &cache{
|
||||||
|
cfg: a.cfg.Cache,
|
||||||
|
}
|
||||||
|
if a.cache.cfg != nil && !a.cache.cfg.Enable {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
a.cache.c, err = ristretto.NewCache(&ristretto.Config{
|
||||||
NumCounters: 5000,
|
NumCounters: 5000,
|
||||||
MaxCost: 20000000, // 20 MB
|
MaxCost: 20000000, // 20 MB
|
||||||
BufferItems: 16,
|
BufferItems: 16,
|
||||||
|
@ -52,13 +57,14 @@ func initCache() (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func cacheMiddleware(next http.Handler) http.Handler {
|
func (c *cache) cacheMiddleware(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// Do checks
|
if c.c == nil {
|
||||||
if !appConfig.Cache.Enable {
|
// No cache configured
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Do checks
|
||||||
if !(r.Method == http.MethodGet || r.Method == http.MethodHead) {
|
if !(r.Method == http.MethodGet || r.Method == http.MethodHead) {
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
|
@ -74,32 +80,32 @@ func cacheMiddleware(next http.Handler) http.Handler {
|
||||||
// Search and serve cache
|
// Search and serve cache
|
||||||
key := cacheKey(r)
|
key := cacheKey(r)
|
||||||
// Get cache or render it
|
// Get cache or render it
|
||||||
cacheInterface, _, _ := cacheGroup.Do(key, func() (interface{}, error) {
|
cacheInterface, _, _ := c.g.Do(key, func() (interface{}, error) {
|
||||||
return getCache(key, next, r), nil
|
return c.getCache(key, next, r), nil
|
||||||
})
|
})
|
||||||
cache := cacheInterface.(*cacheItem)
|
ci := cacheInterface.(*cacheItem)
|
||||||
// copy cached headers
|
// copy cached headers
|
||||||
for k, v := range cache.header {
|
for k, v := range ci.header {
|
||||||
w.Header()[k] = v
|
w.Header()[k] = v
|
||||||
}
|
}
|
||||||
setCacheHeaders(w, cache)
|
c.setCacheHeaders(w, ci)
|
||||||
// check conditional request
|
// check conditional request
|
||||||
if ifNoneMatchHeader := r.Header.Get("If-None-Match"); ifNoneMatchHeader != "" && ifNoneMatchHeader == cache.eTag {
|
if ifNoneMatchHeader := r.Header.Get("If-None-Match"); ifNoneMatchHeader != "" && ifNoneMatchHeader == ci.eTag {
|
||||||
// send 304
|
// send 304
|
||||||
w.WriteHeader(http.StatusNotModified)
|
w.WriteHeader(http.StatusNotModified)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if ifModifiedSinceHeader := r.Header.Get("If-Modified-Since"); ifModifiedSinceHeader != "" {
|
if ifModifiedSinceHeader := r.Header.Get("If-Modified-Since"); ifModifiedSinceHeader != "" {
|
||||||
if t, err := dateparse.ParseAny(ifModifiedSinceHeader); err == nil && t.After(cache.creationTime) {
|
if t, err := dateparse.ParseAny(ifModifiedSinceHeader); err == nil && t.After(ci.creationTime) {
|
||||||
// send 304
|
// send 304
|
||||||
w.WriteHeader(http.StatusNotModified)
|
w.WriteHeader(http.StatusNotModified)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// set status code
|
// set status code
|
||||||
w.WriteHeader(cache.code)
|
w.WriteHeader(ci.code)
|
||||||
// write cached body
|
// write cached body
|
||||||
_, _ = w.Write(cache.body)
|
_, _ = w.Write(ci.body)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,14 +131,14 @@ func cacheURLString(u *url.URL) string {
|
||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func setCacheHeaders(w http.ResponseWriter, cache *cacheItem) {
|
func (c *cache) setCacheHeaders(w http.ResponseWriter, cache *cacheItem) {
|
||||||
w.Header().Set("ETag", cache.eTag)
|
w.Header().Set("ETag", cache.eTag)
|
||||||
w.Header().Set("Last-Modified", cache.creationTime.UTC().Format(http.TimeFormat))
|
w.Header().Set("Last-Modified", cache.creationTime.UTC().Format(http.TimeFormat))
|
||||||
if w.Header().Get("Cache-Control") == "" {
|
if w.Header().Get("Cache-Control") == "" {
|
||||||
if cache.expiration != 0 {
|
if cache.expiration != 0 {
|
||||||
w.Header().Set("Cache-Control", fmt.Sprintf("public,max-age=%d,stale-while-revalidate=%d", cache.expiration, cache.expiration))
|
w.Header().Set("Cache-Control", fmt.Sprintf("public,max-age=%d,stale-while-revalidate=%d", cache.expiration, cache.expiration))
|
||||||
} else {
|
} else {
|
||||||
w.Header().Set("Cache-Control", fmt.Sprintf("public,max-age=%d,s-max-age=%d,stale-while-revalidate=%d", appConfig.Cache.Expiration, appConfig.Cache.Expiration/3, appConfig.Cache.Expiration))
|
w.Header().Set("Cache-Control", fmt.Sprintf("public,max-age=%d,s-max-age=%d,stale-while-revalidate=%d", c.cfg.Expiration, c.cfg.Expiration/3, c.cfg.Expiration))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -146,8 +152,8 @@ type cacheItem struct {
|
||||||
body []byte
|
body []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCache(key string, next http.Handler, r *http.Request) (item *cacheItem) {
|
func (c *cache) getCache(key string, next http.Handler, r *http.Request) (item *cacheItem) {
|
||||||
if rItem, ok := cacheR.Get(key); ok {
|
if rItem, ok := c.c.Get(key); ok {
|
||||||
item = rItem.(*cacheItem)
|
item = rItem.(*cacheItem)
|
||||||
}
|
}
|
||||||
if item == nil {
|
if item == nil {
|
||||||
|
@ -198,10 +204,10 @@ func getCache(key string, next http.Handler, r *http.Request) (item *cacheItem)
|
||||||
// Save cache
|
// Save cache
|
||||||
if cch := item.header.Get("Cache-Control"); !strings.Contains(cch, "no-store") && !strings.Contains(cch, "private") && !strings.Contains(cch, "no-cache") {
|
if cch := item.header.Get("Cache-Control"); !strings.Contains(cch, "no-store") && !strings.Contains(cch, "private") && !strings.Contains(cch, "no-cache") {
|
||||||
if exp == 0 {
|
if exp == 0 {
|
||||||
cacheR.Set(key, item, 0)
|
c.c.Set(key, item, 0)
|
||||||
} else {
|
} else {
|
||||||
ttl := time.Duration(exp) * time.Second
|
ttl := time.Duration(exp) * time.Second
|
||||||
cacheR.SetWithTTL(key, item, 0, ttl)
|
c.c.SetWithTTL(key, item, 0, ttl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -210,8 +216,8 @@ func getCache(key string, next http.Handler, r *http.Request) (item *cacheItem)
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
func purgeCache() {
|
func (c *cache) purge() {
|
||||||
cacheR.Clear()
|
c.c.Clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
func setInternalCacheExpirationHeader(w http.ResponseWriter, r *http.Request, expiration int) {
|
func setInternalCacheExpirationHeader(w http.ResponseWriter, r *http.Request, expiration int) {
|
||||||
|
|
22
captcha.go
22
captcha.go
|
@ -10,10 +10,10 @@ import (
|
||||||
"github.com/dchest/captcha"
|
"github.com/dchest/captcha"
|
||||||
)
|
)
|
||||||
|
|
||||||
func captchaMiddleware(next http.Handler) http.Handler {
|
func (a *goBlog) captchaMiddleware(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// 1. Check Cookie
|
// 1. Check Cookie
|
||||||
ses, err := captchaSessionsStore.Get(r, "c")
|
ses, err := a.captchaSessions.Get(r, "c")
|
||||||
if err == nil && ses != nil {
|
if err == nil && ses != nil {
|
||||||
if captcha, ok := ses.Values["captcha"]; ok && captcha.(bool) {
|
if captcha, ok := ses.Values["captcha"]; ok && captcha.(bool) {
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
|
@ -30,7 +30,7 @@ func captchaMiddleware(next http.Handler) http.Handler {
|
||||||
_ = r.ParseForm()
|
_ = r.ParseForm()
|
||||||
b = []byte(r.PostForm.Encode())
|
b = []byte(r.PostForm.Encode())
|
||||||
}
|
}
|
||||||
render(w, r, templateCaptcha, &renderData{
|
a.render(w, r, templateCaptcha, &renderData{
|
||||||
Data: map[string]string{
|
Data: map[string]string{
|
||||||
"captchamethod": r.Method,
|
"captchamethod": r.Method,
|
||||||
"captchaheaders": base64.StdEncoding.EncodeToString(h),
|
"captchaheaders": base64.StdEncoding.EncodeToString(h),
|
||||||
|
@ -41,15 +41,15 @@ func captchaMiddleware(next http.Handler) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkIsCaptcha(next http.Handler) http.Handler {
|
func (a *goBlog) checkIsCaptcha(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 !checkCaptcha(rw, r) {
|
if !a.checkCaptcha(rw, r) {
|
||||||
next.ServeHTTP(rw, r)
|
next.ServeHTTP(rw, r)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkCaptcha(w http.ResponseWriter, r *http.Request) bool {
|
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
|
||||||
}
|
}
|
||||||
|
@ -71,20 +71,20 @@ func checkCaptcha(w http.ResponseWriter, r *http.Request) bool {
|
||||||
}
|
}
|
||||||
// Check captcha and create cookie
|
// Check captcha and create cookie
|
||||||
if captcha.VerifyString(r.FormValue("captchaid"), r.FormValue("digits")) {
|
if captcha.VerifyString(r.FormValue("captchaid"), r.FormValue("digits")) {
|
||||||
ses, err := captchaSessionsStore.Get(r, "c")
|
ses, err := a.captchaSessions.Get(r, "c")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
ses.Values["captcha"] = true
|
ses.Values["captcha"] = true
|
||||||
cookie, err := captchaSessionsStore.SaveGetCookie(r, w, ses)
|
cookie, err := a.captchaSessions.SaveGetCookie(r, w, ses)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
req.AddCookie(cookie)
|
req.AddCookie(cookie)
|
||||||
}
|
}
|
||||||
// Serve original request
|
// Serve original request
|
||||||
d.ServeHTTP(w, req)
|
a.d.ServeHTTP(w, req)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
90
check.go
90
check.go
|
@ -11,8 +11,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func checkAllExternalLinks() {
|
func (a *goBlog) checkAllExternalLinks() {
|
||||||
allPosts, err := getPosts(&postsRequestConfig{status: statusPublished})
|
allPosts, err := a.db.getPosts(&postsRequestConfig{status: statusPublished})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err.Error())
|
log.Println(err.Error())
|
||||||
return
|
return
|
||||||
|
@ -30,45 +30,49 @@ func checkAllExternalLinks() {
|
||||||
}
|
}
|
||||||
responses := map[string]int{}
|
responses := map[string]int{}
|
||||||
rm := sync.RWMutex{}
|
rm := sync.RWMutex{}
|
||||||
for i := 0; i < 20; i++ {
|
processFunc := func() {
|
||||||
go func() {
|
defer wg.Done()
|
||||||
defer wg.Done()
|
wg.Add(1)
|
||||||
wg.Add(1)
|
for postLinkPair := range linkChan {
|
||||||
for postLinkPair := range linkChan {
|
if strings.HasPrefix(postLinkPair.Second, a.cfg.Server.PublicAddress) {
|
||||||
rm.RLock()
|
continue
|
||||||
_, ok := responses[postLinkPair.Second]
|
|
||||||
rm.RUnlock()
|
|
||||||
if !ok {
|
|
||||||
req, err := http.NewRequest(http.MethodGet, postLinkPair.Second, nil)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// User-Agent from Tor
|
|
||||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 6.1; rv:60.0) Gecko/20100101 Firefox/60.0")
|
|
||||||
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
|
||||||
req.Header.Set("Accept-Language", "en-US,en;q=0.5")
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(postLinkPair.Second+" ("+postLinkPair.First+"):", err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
status := resp.StatusCode
|
|
||||||
_, _ = io.Copy(io.Discard, resp.Body)
|
|
||||||
resp.Body.Close()
|
|
||||||
rm.Lock()
|
|
||||||
responses[postLinkPair.Second] = status
|
|
||||||
rm.Unlock()
|
|
||||||
}
|
|
||||||
rm.RLock()
|
|
||||||
if response, ok := responses[postLinkPair.Second]; ok && !checkSuccessStatus(response) {
|
|
||||||
fmt.Println(postLinkPair.Second+" ("+postLinkPair.First+"):", response)
|
|
||||||
}
|
|
||||||
rm.RUnlock()
|
|
||||||
}
|
}
|
||||||
}()
|
rm.RLock()
|
||||||
|
_, ok := responses[postLinkPair.Second]
|
||||||
|
rm.RUnlock()
|
||||||
|
if !ok {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, postLinkPair.Second, nil)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// User-Agent from Tor
|
||||||
|
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 6.1; rv:60.0) Gecko/20100101 Firefox/60.0")
|
||||||
|
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
||||||
|
req.Header.Set("Accept-Language", "en-US,en;q=0.5")
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(postLinkPair.Second+" ("+postLinkPair.First+"):", err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
status := resp.StatusCode
|
||||||
|
_, _ = io.Copy(io.Discard, resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
rm.Lock()
|
||||||
|
responses[postLinkPair.Second] = status
|
||||||
|
rm.Unlock()
|
||||||
|
}
|
||||||
|
rm.RLock()
|
||||||
|
if response, ok := responses[postLinkPair.Second]; ok && !checkSuccessStatus(response) {
|
||||||
|
fmt.Println(postLinkPair.Second+" ("+postLinkPair.First+"):", response)
|
||||||
|
}
|
||||||
|
rm.RUnlock()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
err = getExternalLinks(allPosts, linkChan)
|
for i := 0; i < 20; i++ {
|
||||||
|
go processFunc()
|
||||||
|
}
|
||||||
|
err = a.getExternalLinks(allPosts, linkChan)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err.Error())
|
log.Println(err.Error())
|
||||||
return
|
return
|
||||||
|
@ -80,17 +84,15 @@ func checkSuccessStatus(status int) bool {
|
||||||
return status >= 200 && status < 400
|
return status >= 200 && status < 400
|
||||||
}
|
}
|
||||||
|
|
||||||
func getExternalLinks(posts []*post, linkChan chan<- stringPair) error {
|
func (a *goBlog) getExternalLinks(posts []*post, linkChan chan<- stringPair) error {
|
||||||
wg := new(sync.WaitGroup)
|
wg := new(sync.WaitGroup)
|
||||||
for _, p := range posts {
|
for _, p := range posts {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(p *post) {
|
go func(p *post) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
links, _ := allLinksFromHTML(strings.NewReader(string(p.absoluteHTML())), p.fullURL())
|
links, _ := allLinksFromHTML(strings.NewReader(string(a.absoluteHTML(p))), a.fullPostURL(p))
|
||||||
for _, link := range links {
|
for _, link := range links {
|
||||||
if !strings.HasPrefix(link, appConfig.Server.PublicAddress) {
|
linkChan <- stringPair{a.fullPostURL(p), link}
|
||||||
linkChan <- stringPair{p.fullURL(), link}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}(p)
|
}(p)
|
||||||
}
|
}
|
||||||
|
|
54
comments.go
54
comments.go
|
@ -20,36 +20,36 @@ type comment struct {
|
||||||
Comment string
|
Comment string
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveComment(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveComment(w http.ResponseWriter, r *http.Request) {
|
||||||
id, err := strconv.Atoi(chi.URLParam(r, "id"))
|
id, err := strconv.Atoi(chi.URLParam(r, "id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusBadRequest)
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
row, err := appDb.queryRow("select id, target, name, website, comment from comments where id = @id", sql.Named("id", id))
|
row, err := a.db.queryRow("select id, target, name, website, comment from comments where id = @id", sql.Named("id", id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
comment := &comment{}
|
comment := &comment{}
|
||||||
if err = row.Scan(&comment.ID, &comment.Target, &comment.Name, &comment.Website, &comment.Comment); err == sql.ErrNoRows {
|
if err = row.Scan(&comment.ID, &comment.Target, &comment.Name, &comment.Website, &comment.Comment); err == sql.ErrNoRows {
|
||||||
serve404(w, r)
|
a.serve404(w, r)
|
||||||
return
|
return
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
blog := r.Context().Value(blogContextKey).(string)
|
blog := r.Context().Value(blogContextKey).(string)
|
||||||
render(w, r, templateComment, &renderData{
|
a.render(w, r, templateComment, &renderData{
|
||||||
BlogString: blog,
|
BlogString: blog,
|
||||||
Canonical: appConfig.Server.PublicAddress + appConfig.Blogs[blog].getRelativePath(fmt.Sprintf("/comment/%d", id)),
|
Canonical: a.cfg.Server.PublicAddress + a.cfg.Blogs[blog].getRelativePath(fmt.Sprintf("/comment/%d", id)),
|
||||||
Data: comment,
|
Data: comment,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func createComment(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) createComment(w http.ResponseWriter, r *http.Request) {
|
||||||
// Check target
|
// Check target
|
||||||
target := checkCommentTarget(w, r)
|
target := a.checkCommentTarget(w, r)
|
||||||
if target == "" {
|
if target == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,7 @@ func createComment(w http.ResponseWriter, r *http.Request) {
|
||||||
strict := bluemonday.StrictPolicy()
|
strict := bluemonday.StrictPolicy()
|
||||||
comment := strings.TrimSpace(strict.Sanitize(r.FormValue("comment")))
|
comment := strings.TrimSpace(strict.Sanitize(r.FormValue("comment")))
|
||||||
if comment == "" {
|
if comment == "" {
|
||||||
serveError(w, r, "Comment is empty", http.StatusBadRequest)
|
a.serveError(w, r, "Comment is empty", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
name := strings.TrimSpace(strict.Sanitize(r.FormValue("name")))
|
name := strings.TrimSpace(strict.Sanitize(r.FormValue("name")))
|
||||||
|
@ -66,35 +66,35 @@ func createComment(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
website := strings.TrimSpace(strict.Sanitize(r.FormValue("website")))
|
website := strings.TrimSpace(strict.Sanitize(r.FormValue("website")))
|
||||||
// Insert
|
// Insert
|
||||||
result, err := appDb.exec("insert into comments (target, comment, name, website) values (@target, @comment, @name, @website)", sql.Named("target", target), sql.Named("comment", comment), sql.Named("name", name), sql.Named("website", website))
|
result, err := a.db.exec("insert into comments (target, comment, name, website) values (@target, @comment, @name, @website)", sql.Named("target", target), sql.Named("comment", comment), sql.Named("name", name), sql.Named("website", website))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if commentID, err := result.LastInsertId(); err != nil {
|
if commentID, err := result.LastInsertId(); err != nil {
|
||||||
// Serve error
|
// Serve error
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
} else {
|
} else {
|
||||||
commentAddress := fmt.Sprintf("%s/%d", blogPath(r.Context().Value(blogContextKey).(string))+"/comment", commentID)
|
commentAddress := fmt.Sprintf("%s/%d", a.blogPath(r.Context().Value(blogContextKey).(string))+"/comment", commentID)
|
||||||
// Send webmention
|
// Send webmention
|
||||||
_ = createWebmention(appConfig.Server.PublicAddress+commentAddress, appConfig.Server.PublicAddress+target)
|
_ = a.createWebmention(a.cfg.Server.PublicAddress+commentAddress, a.cfg.Server.PublicAddress+target)
|
||||||
// Redirect to comment
|
// Redirect to comment
|
||||||
http.Redirect(w, r, commentAddress, http.StatusFound)
|
http.Redirect(w, r, commentAddress, http.StatusFound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkCommentTarget(w http.ResponseWriter, r *http.Request) string {
|
func (a *goBlog) checkCommentTarget(w http.ResponseWriter, r *http.Request) string {
|
||||||
target := r.FormValue("target")
|
target := r.FormValue("target")
|
||||||
if target == "" {
|
if target == "" {
|
||||||
serveError(w, r, "No target specified", http.StatusBadRequest)
|
a.serveError(w, r, "No target specified", http.StatusBadRequest)
|
||||||
return ""
|
return ""
|
||||||
} else if !strings.HasPrefix(target, appConfig.Server.PublicAddress) {
|
} else if !strings.HasPrefix(target, a.cfg.Server.PublicAddress) {
|
||||||
serveError(w, r, "Bad target", http.StatusBadRequest)
|
a.serveError(w, r, "Bad target", http.StatusBadRequest)
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
targetURL, err := url.Parse(target)
|
targetURL, err := url.Parse(target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusBadRequest)
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return targetURL.Path
|
return targetURL.Path
|
||||||
|
@ -114,10 +114,10 @@ func buildCommentsQuery(config *commentsRequestConfig) (query string, args []int
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func getComments(config *commentsRequestConfig) ([]*comment, error) {
|
func (db *database) getComments(config *commentsRequestConfig) ([]*comment, error) {
|
||||||
comments := []*comment{}
|
comments := []*comment{}
|
||||||
query, args := buildCommentsQuery(config)
|
query, args := buildCommentsQuery(config)
|
||||||
rows, err := appDb.query(query, args...)
|
rows, err := db.query(query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -132,10 +132,10 @@ func getComments(config *commentsRequestConfig) ([]*comment, error) {
|
||||||
return comments, nil
|
return comments, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func countComments(config *commentsRequestConfig) (count int, err error) {
|
func (db *database) countComments(config *commentsRequestConfig) (count int, err error) {
|
||||||
query, params := buildCommentsQuery(config)
|
query, params := buildCommentsQuery(config)
|
||||||
query = "select count(*) from (" + query + ")"
|
query = "select count(*) from (" + query + ")"
|
||||||
row, err := appDb.queryRow(query, params...)
|
row, err := db.queryRow(query, params...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -143,7 +143,7 @@ func countComments(config *commentsRequestConfig) (count int, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteComment(id int) error {
|
func (db *database) deleteComment(id int) error {
|
||||||
_, err := appDb.exec("delete from comments where id = @id", sql.Named("id", id))
|
_, err := db.exec("delete from comments where id = @id", sql.Named("id", id))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,11 +13,12 @@ import (
|
||||||
type commentsPaginationAdapter struct {
|
type commentsPaginationAdapter struct {
|
||||||
config *commentsRequestConfig
|
config *commentsRequestConfig
|
||||||
nums int64
|
nums int64
|
||||||
|
db *database
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *commentsPaginationAdapter) Nums() (int64, error) {
|
func (p *commentsPaginationAdapter) Nums() (int64, error) {
|
||||||
if p.nums == 0 {
|
if p.nums == 0 {
|
||||||
nums, _ := countComments(p.config)
|
nums, _ := p.db.countComments(p.config)
|
||||||
p.nums = int64(nums)
|
p.nums = int64(nums)
|
||||||
}
|
}
|
||||||
return p.nums, nil
|
return p.nums, nil
|
||||||
|
@ -28,23 +29,23 @@ func (p *commentsPaginationAdapter) Slice(offset, length int, data interface{})
|
||||||
modifiedConfig.offset = offset
|
modifiedConfig.offset = offset
|
||||||
modifiedConfig.limit = length
|
modifiedConfig.limit = length
|
||||||
|
|
||||||
comments, err := getComments(&modifiedConfig)
|
comments, err := p.db.getComments(&modifiedConfig)
|
||||||
reflect.ValueOf(data).Elem().Set(reflect.ValueOf(&comments).Elem())
|
reflect.ValueOf(data).Elem().Set(reflect.ValueOf(&comments).Elem())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func commentsAdmin(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) commentsAdmin(w http.ResponseWriter, r *http.Request) {
|
||||||
blog := r.Context().Value(blogContextKey).(string)
|
blog := r.Context().Value(blogContextKey).(string)
|
||||||
commentsPath := r.Context().Value(pathContextKey).(string)
|
commentsPath := r.Context().Value(pathContextKey).(string)
|
||||||
// Adapter
|
// Adapter
|
||||||
pageNoString := chi.URLParam(r, "page")
|
pageNoString := chi.URLParam(r, "page")
|
||||||
pageNo, _ := strconv.Atoi(pageNoString)
|
pageNo, _ := strconv.Atoi(pageNoString)
|
||||||
p := paginator.New(&commentsPaginationAdapter{config: &commentsRequestConfig{}}, 5)
|
p := paginator.New(&commentsPaginationAdapter{config: &commentsRequestConfig{}, db: a.db}, 5)
|
||||||
p.SetPage(pageNo)
|
p.SetPage(pageNo)
|
||||||
var comments []*comment
|
var comments []*comment
|
||||||
err := p.Results(&comments)
|
err := p.Results(&comments)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Navigation
|
// Navigation
|
||||||
|
@ -70,7 +71,7 @@ func commentsAdmin(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
nextPath = fmt.Sprintf("%s/page/%d", commentsPath, nextPage)
|
nextPath = fmt.Sprintf("%s/page/%d", commentsPath, nextPage)
|
||||||
// Render
|
// Render
|
||||||
render(w, r, templateCommentsAdmin, &renderData{
|
a.render(w, r, templateCommentsAdmin, &renderData{
|
||||||
BlogString: blog,
|
BlogString: blog,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"Comments": comments,
|
"Comments": comments,
|
||||||
|
@ -82,17 +83,17 @@ func commentsAdmin(w http.ResponseWriter, r *http.Request) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func commentsAdminDelete(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) commentsAdminDelete(w http.ResponseWriter, r *http.Request) {
|
||||||
id, err := strconv.Atoi(r.FormValue("commentid"))
|
id, err := strconv.Atoi(r.FormValue("commentid"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusBadRequest)
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = deleteComment(id)
|
err = a.db.deleteComment(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
purgeCache()
|
a.cache.purge()
|
||||||
http.Redirect(w, r, ".", http.StatusFound)
|
http.Redirect(w, r, ".", http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
51
config.go
51
config.go
|
@ -224,9 +224,7 @@ type configWebmention struct {
|
||||||
DisableReceiving bool `mapstructure:"disableReceiving"`
|
DisableReceiving bool `mapstructure:"disableReceiving"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var appConfig = &config{}
|
func (a *goBlog) initConfig() error {
|
||||||
|
|
||||||
func initConfig() error {
|
|
||||||
viper.SetConfigName("config")
|
viper.SetConfigName("config")
|
||||||
viper.AddConfigPath("./config/")
|
viper.AddConfigPath("./config/")
|
||||||
err := viper.ReadInConfig()
|
err := viper.ReadInConfig()
|
||||||
|
@ -258,52 +256,53 @@ func initConfig() error {
|
||||||
viper.SetDefault("webmention.disableSending", false)
|
viper.SetDefault("webmention.disableSending", false)
|
||||||
viper.SetDefault("webmention.disableReceiving", false)
|
viper.SetDefault("webmention.disableReceiving", false)
|
||||||
// Unmarshal config
|
// Unmarshal config
|
||||||
err = viper.Unmarshal(appConfig)
|
a.cfg = &config{}
|
||||||
|
err = viper.Unmarshal(a.cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Check config
|
// Check config
|
||||||
publicURL, err := url.Parse(appConfig.Server.PublicAddress)
|
publicURL, err := url.Parse(a.cfg.Server.PublicAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
appConfig.Server.publicHostname = publicURL.Hostname()
|
a.cfg.Server.publicHostname = publicURL.Hostname()
|
||||||
if appConfig.Server.ShortPublicAddress != "" {
|
if a.cfg.Server.ShortPublicAddress != "" {
|
||||||
shortPublicURL, err := url.Parse(appConfig.Server.ShortPublicAddress)
|
shortPublicURL, err := url.Parse(a.cfg.Server.ShortPublicAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
appConfig.Server.shortPublicHostname = shortPublicURL.Hostname()
|
a.cfg.Server.shortPublicHostname = shortPublicURL.Hostname()
|
||||||
}
|
}
|
||||||
if appConfig.Server.JWTSecret == "" {
|
if a.cfg.Server.JWTSecret == "" {
|
||||||
return errors.New("no JWT secret configured")
|
return errors.New("no JWT secret configured")
|
||||||
}
|
}
|
||||||
if len(appConfig.Blogs) == 0 {
|
if len(a.cfg.Blogs) == 0 {
|
||||||
return errors.New("no blog configured")
|
return errors.New("no blog configured")
|
||||||
}
|
}
|
||||||
if len(appConfig.DefaultBlog) == 0 || appConfig.Blogs[appConfig.DefaultBlog] == nil {
|
if len(a.cfg.DefaultBlog) == 0 || a.cfg.Blogs[a.cfg.DefaultBlog] == nil {
|
||||||
return errors.New("no default blog or default blog not present")
|
return errors.New("no default blog or default blog not present")
|
||||||
}
|
}
|
||||||
if appConfig.Micropub.MediaStorage != nil {
|
if a.cfg.Micropub.MediaStorage != nil {
|
||||||
if appConfig.Micropub.MediaStorage.MediaURL == "" ||
|
if a.cfg.Micropub.MediaStorage.MediaURL == "" ||
|
||||||
appConfig.Micropub.MediaStorage.BunnyStorageKey == "" ||
|
a.cfg.Micropub.MediaStorage.BunnyStorageKey == "" ||
|
||||||
appConfig.Micropub.MediaStorage.BunnyStorageName == "" {
|
a.cfg.Micropub.MediaStorage.BunnyStorageName == "" {
|
||||||
appConfig.Micropub.MediaStorage.BunnyStorageKey = ""
|
a.cfg.Micropub.MediaStorage.BunnyStorageKey = ""
|
||||||
appConfig.Micropub.MediaStorage.BunnyStorageName = ""
|
a.cfg.Micropub.MediaStorage.BunnyStorageName = ""
|
||||||
}
|
}
|
||||||
appConfig.Micropub.MediaStorage.MediaURL = strings.TrimSuffix(appConfig.Micropub.MediaStorage.MediaURL, "/")
|
a.cfg.Micropub.MediaStorage.MediaURL = strings.TrimSuffix(a.cfg.Micropub.MediaStorage.MediaURL, "/")
|
||||||
}
|
}
|
||||||
if pm := appConfig.PrivateMode; pm != nil && pm.Enabled {
|
if pm := a.cfg.PrivateMode; pm != nil && pm.Enabled {
|
||||||
appConfig.ActivityPub = &configActivityPub{Enabled: false}
|
a.cfg.ActivityPub = &configActivityPub{Enabled: false}
|
||||||
}
|
}
|
||||||
if wm := appConfig.Webmention; wm != nil && wm.DisableReceiving {
|
if wm := a.cfg.Webmention; wm != nil && wm.DisableReceiving {
|
||||||
// Disable comments for all blogs
|
// Disable comments for all blogs
|
||||||
for _, b := range appConfig.Blogs {
|
for _, b := range a.cfg.Blogs {
|
||||||
b.Comments = &comments{Enabled: false}
|
b.Comments = &comments{Enabled: false}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Check config for each blog
|
// Check config for each blog
|
||||||
for _, blog := range appConfig.Blogs {
|
for _, blog := range a.cfg.Blogs {
|
||||||
if br := blog.Blogroll; br != nil && br.Enabled && br.Opml == "" {
|
if br := blog.Blogroll; br != nil && br.Enabled && br.Opml == "" {
|
||||||
br.Enabled = false
|
br.Enabled = false
|
||||||
}
|
}
|
||||||
|
@ -311,6 +310,6 @@ func initConfig() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func httpsConfigured() bool {
|
func (a *goBlog) httpsConfigured() bool {
|
||||||
return appConfig.Server.PublicHTTPS || appConfig.Server.SecurityHeaders || strings.HasPrefix(appConfig.Server.PublicAddress, "https")
|
return a.cfg.Server.PublicHTTPS || a.cfg.Server.SecurityHeaders || strings.HasPrefix(a.cfg.Server.PublicAddress, "https")
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,18 +4,18 @@ import "net/http"
|
||||||
|
|
||||||
const customPageContextKey = "custompage"
|
const customPageContextKey = "custompage"
|
||||||
|
|
||||||
func serveCustomPage(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveCustomPage(w http.ResponseWriter, r *http.Request) {
|
||||||
page := r.Context().Value(customPageContextKey).(*customPage)
|
page := r.Context().Value(customPageContextKey).(*customPage)
|
||||||
if appConfig.Cache != nil && appConfig.Cache.Enable && page.Cache {
|
if a.cfg.Cache != nil && a.cfg.Cache.Enable && page.Cache {
|
||||||
if page.CacheExpiration != 0 {
|
if page.CacheExpiration != 0 {
|
||||||
setInternalCacheExpirationHeader(w, r, page.CacheExpiration)
|
setInternalCacheExpirationHeader(w, r, page.CacheExpiration)
|
||||||
} else {
|
} else {
|
||||||
setInternalCacheExpirationHeader(w, r, int(appConfig.Cache.Expiration))
|
setInternalCacheExpirationHeader(w, r, int(a.cfg.Cache.Expiration))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
render(w, r, page.Template, &renderData{
|
a.render(w, r, page.Template, &renderData{
|
||||||
BlogString: r.Context().Value(blogContextKey).(string),
|
BlogString: r.Context().Value(blogContextKey).(string),
|
||||||
Canonical: appConfig.Server.PublicAddress + page.Path,
|
Canonical: a.cfg.Server.PublicAddress + page.Path,
|
||||||
Data: page.Data,
|
Data: page.Data,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
108
database.go
108
database.go
|
@ -8,18 +8,42 @@ import (
|
||||||
|
|
||||||
sqlite "github.com/mattn/go-sqlite3"
|
sqlite "github.com/mattn/go-sqlite3"
|
||||||
"github.com/schollz/sqlite3dump"
|
"github.com/schollz/sqlite3dump"
|
||||||
|
"golang.org/x/sync/singleflight"
|
||||||
)
|
)
|
||||||
|
|
||||||
var appDb *goblogDb
|
type database struct {
|
||||||
|
db *sql.DB
|
||||||
type goblogDb struct {
|
stmts map[string]*sql.Stmt
|
||||||
db *sql.DB
|
g singleflight.Group
|
||||||
statementCache map[string]*sql.Stmt
|
persistentCacheGroup singleflight.Group
|
||||||
}
|
}
|
||||||
|
|
||||||
func initDatabase() (err error) {
|
func (a *goBlog) initDatabase() (err error) {
|
||||||
// Setup db
|
// Setup db
|
||||||
sql.Register("goblog_db", &sqlite.SQLiteDriver{
|
db, err := a.openDatabase(a.cfg.Db.File)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Create appDB
|
||||||
|
a.db = db
|
||||||
|
db.vacuum()
|
||||||
|
addShutdownFunc(func() {
|
||||||
|
_ = db.close()
|
||||||
|
log.Println("Closed database")
|
||||||
|
})
|
||||||
|
if a.cfg.Db.DumpFile != "" {
|
||||||
|
hourlyHooks = append(hourlyHooks, func() {
|
||||||
|
db.dump(a.cfg.Db.DumpFile)
|
||||||
|
})
|
||||||
|
db.dump(a.cfg.Db.DumpFile)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *goBlog) openDatabase(file string) (*database, error) {
|
||||||
|
// Register driver
|
||||||
|
dbDriverName := generateRandomString(15)
|
||||||
|
sql.Register("goblog_db_"+dbDriverName, &sqlite.SQLiteDriver{
|
||||||
ConnectHook: func(c *sqlite.SQLiteConn) error {
|
ConnectHook: func(c *sqlite.SQLiteConn) error {
|
||||||
if err := c.RegisterFunc("tolocal", toLocalSafe, true); err != nil {
|
if err := c.RegisterFunc("tolocal", toLocalSafe, true); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -27,64 +51,54 @@ func initDatabase() (err error) {
|
||||||
if err := c.RegisterFunc("wordcount", wordCount, true); err != nil {
|
if err := c.RegisterFunc("wordcount", wordCount, true); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := c.RegisterFunc("mdtext", renderText, true); err != nil {
|
if err := c.RegisterFunc("mdtext", a.renderText, true); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
db, err := sql.Open("goblog_db", appConfig.Db.File+"?cache=shared&mode=rwc&_journal_mode=WAL")
|
// Open db
|
||||||
|
db, err := sql.Open("goblog_db_"+dbDriverName, file+"?cache=shared&mode=rwc&_journal_mode=WAL")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
db.SetMaxOpenConns(1)
|
db.SetMaxOpenConns(1)
|
||||||
err = db.Ping()
|
err = db.Ping()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
// Check available SQLite features
|
// Check available SQLite features
|
||||||
rows, err := db.Query("pragma compile_options")
|
rows, err := db.Query("pragma compile_options")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
cos := map[string]bool{}
|
cos := map[string]bool{}
|
||||||
var co string
|
var co string
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
err = rows.Scan(&co)
|
err = rows.Scan(&co)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
cos[co] = true
|
cos[co] = true
|
||||||
}
|
}
|
||||||
if _, ok := cos["ENABLE_FTS5"]; !ok {
|
if _, ok := cos["ENABLE_FTS5"]; !ok {
|
||||||
return errors.New("sqlite not compiled with FTS5")
|
return nil, errors.New("sqlite not compiled with FTS5")
|
||||||
}
|
}
|
||||||
// Migrate DB
|
// Migrate DB
|
||||||
err = migrateDb(db)
|
err = migrateDb(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
// Create appDB
|
return &database{
|
||||||
appDb = &goblogDb{
|
db: db,
|
||||||
db: db,
|
stmts: map[string]*sql.Stmt{},
|
||||||
statementCache: map[string]*sql.Stmt{},
|
}, nil
|
||||||
}
|
|
||||||
appDb.vacuum()
|
|
||||||
addShutdownFunc(func() {
|
|
||||||
_ = appDb.close()
|
|
||||||
log.Println("Closed database")
|
|
||||||
})
|
|
||||||
if appConfig.Db.DumpFile != "" {
|
|
||||||
hourlyHooks = append(hourlyHooks, func() {
|
|
||||||
appDb.dump()
|
|
||||||
})
|
|
||||||
appDb.dump()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *goblogDb) dump() {
|
// Main features
|
||||||
f, err := os.Create(appConfig.Db.DumpFile)
|
|
||||||
|
func (db *database) dump(file string) {
|
||||||
|
f, err := os.Create(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Error while dump db:", err.Error())
|
log.Println("Error while dump db:", err.Error())
|
||||||
return
|
return
|
||||||
|
@ -94,18 +108,18 @@ func (db *goblogDb) dump() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *goblogDb) close() error {
|
func (db *database) close() error {
|
||||||
db.vacuum()
|
db.vacuum()
|
||||||
return db.db.Close()
|
return db.db.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *goblogDb) vacuum() {
|
func (db *database) vacuum() {
|
||||||
_, _ = db.exec("VACUUM")
|
_, _ = db.exec("VACUUM")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *goblogDb) prepare(query string) (*sql.Stmt, error) {
|
func (db *database) prepare(query string) (*sql.Stmt, error) {
|
||||||
stmt, err, _ := cacheGroup.Do(query, func() (interface{}, error) {
|
stmt, err, _ := db.g.Do(query, func() (interface{}, error) {
|
||||||
stmt, ok := db.statementCache[query]
|
stmt, ok := db.stmts[query]
|
||||||
if ok && stmt != nil {
|
if ok && stmt != nil {
|
||||||
return stmt, nil
|
return stmt, nil
|
||||||
}
|
}
|
||||||
|
@ -113,7 +127,7 @@ func (db *goblogDb) prepare(query string) (*sql.Stmt, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
db.statementCache[query] = stmt
|
db.stmts[query] = stmt
|
||||||
return stmt, nil
|
return stmt, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -122,7 +136,7 @@ func (db *goblogDb) prepare(query string) (*sql.Stmt, error) {
|
||||||
return stmt.(*sql.Stmt), nil
|
return stmt.(*sql.Stmt), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *goblogDb) exec(query string, args ...interface{}) (sql.Result, error) {
|
func (db *database) exec(query string, args ...interface{}) (sql.Result, error) {
|
||||||
stmt, err := db.prepare(query)
|
stmt, err := db.prepare(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -130,12 +144,12 @@ func (db *goblogDb) exec(query string, args ...interface{}) (sql.Result, error)
|
||||||
return stmt.Exec(args...)
|
return stmt.Exec(args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *goblogDb) execMulti(query string, args ...interface{}) (sql.Result, error) {
|
func (db *database) execMulti(query string, args ...interface{}) (sql.Result, error) {
|
||||||
// Can't prepare the statement
|
// Can't prepare the statement
|
||||||
return db.db.Exec(query, args...)
|
return db.db.Exec(query, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *goblogDb) query(query string, args ...interface{}) (*sql.Rows, error) {
|
func (db *database) query(query string, args ...interface{}) (*sql.Rows, error) {
|
||||||
stmt, err := db.prepare(query)
|
stmt, err := db.prepare(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -143,10 +157,16 @@ func (db *goblogDb) query(query string, args ...interface{}) (*sql.Rows, error)
|
||||||
return stmt.Query(args...)
|
return stmt.Query(args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *goblogDb) queryRow(query string, args ...interface{}) (*sql.Row, error) {
|
func (db *database) queryRow(query string, args ...interface{}) (*sql.Row, error) {
|
||||||
stmt, err := db.prepare(query)
|
stmt, err := db.prepare(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return stmt.QueryRow(args...), nil
|
return stmt.QueryRow(args...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Other things
|
||||||
|
|
||||||
|
func (d *database) rebuildFTSIndex() {
|
||||||
|
_, _ = d.exec("insert into posts_fts(posts_fts) values ('rebuild')")
|
||||||
|
}
|
||||||
|
|
|
@ -2,12 +2,16 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"log"
|
||||||
|
|
||||||
"github.com/lopezator/migrator"
|
"github.com/lopezator/migrator"
|
||||||
)
|
)
|
||||||
|
|
||||||
func migrateDb(db *sql.DB) error {
|
func migrateDb(db *sql.DB) error {
|
||||||
m, err := migrator.New(
|
m, err := migrator.New(
|
||||||
|
migrator.WithLogger(migrator.LoggerFunc(func(s string, i ...interface{}) {
|
||||||
|
log.Printf(s, i)
|
||||||
|
})),
|
||||||
migrator.Migrations(
|
migrator.Migrations(
|
||||||
&migrator.Migration{
|
&migrator.Migration{
|
||||||
Name: "00001",
|
Name: "00001",
|
||||||
|
|
47
editor.go
47
editor.go
|
@ -11,45 +11,45 @@ import (
|
||||||
|
|
||||||
const editorPath = "/editor"
|
const editorPath = "/editor"
|
||||||
|
|
||||||
func serveEditor(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveEditor(w http.ResponseWriter, r *http.Request) {
|
||||||
blog := r.Context().Value(blogContextKey).(string)
|
blog := r.Context().Value(blogContextKey).(string)
|
||||||
render(w, r, templateEditor, &renderData{
|
a.render(w, r, templateEditor, &renderData{
|
||||||
BlogString: blog,
|
BlogString: blog,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"Drafts": loadDrafts(blog),
|
"Drafts": a.db.getDrafts(blog),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveEditorPost(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveEditorPost(w http.ResponseWriter, r *http.Request) {
|
||||||
blog := r.Context().Value(blogContextKey).(string)
|
blog := r.Context().Value(blogContextKey).(string)
|
||||||
if action := r.FormValue("editoraction"); action != "" {
|
if action := r.FormValue("editoraction"); action != "" {
|
||||||
switch action {
|
switch action {
|
||||||
case "loaddelete":
|
case "loaddelete":
|
||||||
render(w, r, templateEditor, &renderData{
|
a.render(w, r, templateEditor, &renderData{
|
||||||
BlogString: blog,
|
BlogString: blog,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"DeleteURL": r.FormValue("url"),
|
"DeleteURL": r.FormValue("url"),
|
||||||
"Drafts": loadDrafts(blog),
|
"Drafts": a.db.getDrafts(blog),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
case "loadupdate":
|
case "loadupdate":
|
||||||
parsedURL, err := url.Parse(r.FormValue("url"))
|
parsedURL, err := url.Parse(r.FormValue("url"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusBadRequest)
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
post, err := getPost(parsedURL.Path)
|
post, err := a.db.getPost(parsedURL.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusBadRequest)
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
render(w, r, templateEditor, &renderData{
|
a.render(w, r, templateEditor, &renderData{
|
||||||
BlogString: blog,
|
BlogString: blog,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"UpdatePostURL": parsedURL.String(),
|
"UpdatePostURL": parsedURL.String(),
|
||||||
"UpdatePostContent": post.toMfItem().Properties.Content[0],
|
"UpdatePostContent": a.toMfItem(post).Properties.Content[0],
|
||||||
"Drafts": loadDrafts(blog),
|
"Drafts": a.db.getDrafts(blog),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
case "updatepost":
|
case "updatepost":
|
||||||
|
@ -63,37 +63,32 @@ func serveEditorPost(w http.ResponseWriter, r *http.Request) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
req, err := http.NewRequest(http.MethodPost, "", bytes.NewReader(jsonBytes))
|
req, err := http.NewRequest(http.MethodPost, "", bytes.NewReader(jsonBytes))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
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, contentTypeJSON)
|
||||||
editorMicropubPost(w, req, false)
|
a.editorMicropubPost(w, req, false)
|
||||||
case "upload":
|
case "upload":
|
||||||
editorMicropubPost(w, r, true)
|
a.editorMicropubPost(w, r, true)
|
||||||
default:
|
default:
|
||||||
serveError(w, r, "Unknown editoraction", http.StatusBadRequest)
|
a.serveError(w, r, "Unknown editoraction", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
editorMicropubPost(w, r, false)
|
a.editorMicropubPost(w, r, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadDrafts(blog string) []*post {
|
func (a *goBlog) editorMicropubPost(w http.ResponseWriter, r *http.Request, media bool) {
|
||||||
ps, _ := getPosts(&postsRequestConfig{status: statusDraft, blog: blog})
|
|
||||||
return ps
|
|
||||||
}
|
|
||||||
|
|
||||||
func editorMicropubPost(w http.ResponseWriter, r *http.Request, media bool) {
|
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
if media {
|
if media {
|
||||||
addAllScopes(http.HandlerFunc(serveMicropubMedia)).ServeHTTP(recorder, r)
|
addAllScopes(http.HandlerFunc(a.serveMicropubMedia)).ServeHTTP(recorder, r)
|
||||||
} else {
|
} else {
|
||||||
addAllScopes(http.HandlerFunc(serveMicropubPost)).ServeHTTP(recorder, r)
|
addAllScopes(http.HandlerFunc(a.serveMicropubPost)).ServeHTTP(recorder, r)
|
||||||
}
|
}
|
||||||
result := recorder.Result()
|
result := recorder.Result()
|
||||||
if location := result.Header.Get("Location"); location != "" {
|
if location := result.Header.Get("Location"); location != "" {
|
||||||
|
|
12
errors.go
12
errors.go
|
@ -12,19 +12,19 @@ type errorData struct {
|
||||||
Message string
|
Message string
|
||||||
}
|
}
|
||||||
|
|
||||||
func serve404(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serve404(w http.ResponseWriter, r *http.Request) {
|
||||||
serveError(w, r, fmt.Sprintf("%s was not found", r.RequestURI), http.StatusNotFound)
|
a.serveError(w, r, fmt.Sprintf("%s was not found", r.RequestURI), http.StatusNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveNotAllowed(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveNotAllowed(w http.ResponseWriter, r *http.Request) {
|
||||||
serveError(w, r, "", http.StatusMethodNotAllowed)
|
a.serveError(w, r, "", http.StatusMethodNotAllowed)
|
||||||
}
|
}
|
||||||
|
|
||||||
var errorCheckMediaTypes = []contenttype.MediaType{
|
var errorCheckMediaTypes = []contenttype.MediaType{
|
||||||
contenttype.NewMediaType(contentTypeHTML),
|
contenttype.NewMediaType(contentTypeHTML),
|
||||||
}
|
}
|
||||||
|
|
||||||
func 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 := contenttype.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)
|
||||||
|
@ -35,7 +35,7 @@ func serveError(w http.ResponseWriter, r *http.Request, message string, status i
|
||||||
message = http.StatusText(status)
|
message = http.StatusText(status)
|
||||||
}
|
}
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
render(w, r, templateError, &renderData{
|
a.render(w, r, templateError, &renderData{
|
||||||
Data: &errorData{
|
Data: &errorData{
|
||||||
Title: title,
|
Title: title,
|
||||||
Message: message,
|
Message: message,
|
||||||
|
|
22
feeds.go
22
feeds.go
|
@ -22,25 +22,25 @@ const (
|
||||||
feedAudioLength = "audiolength"
|
feedAudioLength = "audiolength"
|
||||||
)
|
)
|
||||||
|
|
||||||
func generateFeed(blog string, f feedType, w http.ResponseWriter, r *http.Request, posts []*post, title string, description string) {
|
func (a *goBlog) generateFeed(blog string, f feedType, w http.ResponseWriter, r *http.Request, posts []*post, title string, description string) {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
if title == "" {
|
if title == "" {
|
||||||
title = appConfig.Blogs[blog].Title
|
title = a.cfg.Blogs[blog].Title
|
||||||
}
|
}
|
||||||
if description == "" {
|
if description == "" {
|
||||||
description = appConfig.Blogs[blog].Description
|
description = a.cfg.Blogs[blog].Description
|
||||||
}
|
}
|
||||||
feed := &feeds.Feed{
|
feed := &feeds.Feed{
|
||||||
Title: title,
|
Title: title,
|
||||||
Description: description,
|
Description: description,
|
||||||
Link: &feeds.Link{Href: appConfig.Server.PublicAddress + strings.TrimSuffix(r.URL.Path, "."+string(f))},
|
Link: &feeds.Link{Href: a.cfg.Server.PublicAddress + strings.TrimSuffix(r.URL.Path, "."+string(f))},
|
||||||
Created: now,
|
Created: now,
|
||||||
Author: &feeds.Author{
|
Author: &feeds.Author{
|
||||||
Name: appConfig.User.Name,
|
Name: a.cfg.User.Name,
|
||||||
Email: appConfig.User.Email,
|
Email: a.cfg.User.Email,
|
||||||
},
|
},
|
||||||
Image: &feeds.Image{
|
Image: &feeds.Image{
|
||||||
Url: appConfig.User.Picture,
|
Url: a.cfg.User.Picture,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, p := range posts {
|
for _, p := range posts {
|
||||||
|
@ -56,10 +56,10 @@ func generateFeed(blog string, f feedType, w http.ResponseWriter, r *http.Reques
|
||||||
}
|
}
|
||||||
feed.Add(&feeds.Item{
|
feed.Add(&feeds.Item{
|
||||||
Title: p.title(),
|
Title: p.title(),
|
||||||
Link: &feeds.Link{Href: p.fullURL()},
|
Link: &feeds.Link{Href: a.fullPostURL(p)},
|
||||||
Description: p.summary(),
|
Description: a.summary(p),
|
||||||
Id: p.Path,
|
Id: p.Path,
|
||||||
Content: string(p.absoluteHTML()),
|
Content: string(a.absoluteHTML(p)),
|
||||||
Created: created,
|
Created: created,
|
||||||
Updated: updated,
|
Updated: updated,
|
||||||
Enclosure: enc,
|
Enclosure: enc,
|
||||||
|
@ -82,7 +82,7 @@ func generateFeed(blog string, f feedType, w http.ResponseWriter, r *http.Reques
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.Header().Del(contentType)
|
w.Header().Del(contentType)
|
||||||
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+charsetUtf8Suffix)
|
||||||
|
|
12
go.mod
12
go.mod
|
@ -14,11 +14,9 @@ require (
|
||||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
|
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
|
||||||
github.com/boombuler/barcode v1.0.1 // indirect
|
github.com/boombuler/barcode v1.0.1 // indirect
|
||||||
github.com/caddyserver/certmagic v0.13.1
|
github.com/caddyserver/certmagic v0.13.1
|
||||||
// master
|
github.com/cretz/bine v0.2.0
|
||||||
github.com/cretz/bine v0.1.1-0.20200124154328-f9f678b84cca
|
|
||||||
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
|
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
|
||||||
// master
|
github.com/dgraph-io/ristretto v0.1.0
|
||||||
github.com/dgraph-io/ristretto v0.0.4-0.20210504190834-0bf2acd73aa3
|
|
||||||
github.com/elnormous/contenttype v1.0.0
|
github.com/elnormous/contenttype v1.0.0
|
||||||
github.com/felixge/httpsnoop v1.0.2 // indirect
|
github.com/felixge/httpsnoop v1.0.2 // indirect
|
||||||
github.com/go-chi/chi/v5 v5.0.3
|
github.com/go-chi/chi/v5 v5.0.3
|
||||||
|
@ -38,6 +36,7 @@ require (
|
||||||
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
|
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
|
||||||
github.com/lestrrat-go/strftime v1.0.4 // indirect
|
github.com/lestrrat-go/strftime v1.0.4 // indirect
|
||||||
github.com/lib/pq v1.9.0 // indirect
|
github.com/lib/pq v1.9.0 // indirect
|
||||||
|
github.com/libdns/libdns v0.2.1 // indirect
|
||||||
github.com/lopezator/migrator v0.3.0
|
github.com/lopezator/migrator v0.3.0
|
||||||
github.com/magiconair/properties v1.8.5 // indirect
|
github.com/magiconair/properties v1.8.5 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.7
|
github.com/mattn/go-sqlite3 v1.14.7
|
||||||
|
@ -46,7 +45,7 @@ require (
|
||||||
github.com/mitchellh/go-server-timing v1.0.1
|
github.com/mitchellh/go-server-timing v1.0.1
|
||||||
github.com/mitchellh/mapstructure v1.4.1 // indirect
|
github.com/mitchellh/mapstructure v1.4.1 // indirect
|
||||||
github.com/paulmach/go.geojson v1.4.0
|
github.com/paulmach/go.geojson v1.4.0
|
||||||
github.com/pelletier/go-toml v1.9.1 // indirect
|
github.com/pelletier/go-toml v1.9.2 // indirect
|
||||||
github.com/pquerna/otp v1.3.0
|
github.com/pquerna/otp v1.3.0
|
||||||
github.com/schollz/sqlite3dump v1.2.4
|
github.com/schollz/sqlite3dump v1.2.4
|
||||||
github.com/smartystreets/assertions v1.2.0 // indirect
|
github.com/smartystreets/assertions v1.2.0 // indirect
|
||||||
|
@ -63,10 +62,9 @@ require (
|
||||||
github.com/yuin/goldmark-emoji v1.0.1
|
github.com/yuin/goldmark-emoji v1.0.1
|
||||||
go.uber.org/multierr v1.7.0 // indirect
|
go.uber.org/multierr v1.7.0 // indirect
|
||||||
go.uber.org/zap v1.17.0 // indirect
|
go.uber.org/zap v1.17.0 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect
|
|
||||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5
|
golang.org/x/net v0.0.0-20210525063256-abc453219eb5
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||||
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea // indirect
|
golang.org/x/sys v0.0.0-20210603125802-9665404d3644 // indirect
|
||||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect
|
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||||
gopkg.in/ini.v1 v1.62.0 // indirect
|
gopkg.in/ini.v1 v1.62.0 // indirect
|
||||||
|
|
19
go.sum
19
go.sum
|
@ -59,15 +59,15 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
|
||||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/cretz/bine v0.1.1-0.20200124154328-f9f678b84cca h1:Q2r7AxHdJwWfLtBZwvW621M3sPqxPc6ITv2j1FGsYpw=
|
github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo=
|
||||||
github.com/cretz/bine v0.1.1-0.20200124154328-f9f678b84cca/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw=
|
github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f h1:q/DpyjJjZs94bziQ7YkBmIlpqbVP7yw179rnzoNVX1M=
|
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f h1:q/DpyjJjZs94bziQ7YkBmIlpqbVP7yw179rnzoNVX1M=
|
||||||
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f/go.mod h1:QGrK8vMWWHQYQ3QU9bw9Y9OPNfxccGzfb41qjvVeXtY=
|
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f/go.mod h1:QGrK8vMWWHQYQ3QU9bw9Y9OPNfxccGzfb41qjvVeXtY=
|
||||||
github.com/dgraph-io/ristretto v0.0.4-0.20210504190834-0bf2acd73aa3 h1:jU/wpYsEL+8JPLf/QcjkQKI5g0dOjSuwcMjkThxt5x0=
|
github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI=
|
||||||
github.com/dgraph-io/ristretto v0.0.4-0.20210504190834-0bf2acd73aa3/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=
|
github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
|
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
|
||||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||||
|
@ -214,8 +214,9 @@ github.com/lestrrat-go/strftime v1.0.4/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR7
|
||||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8=
|
github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8=
|
||||||
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/libdns/libdns v0.2.0 h1:ewg3ByWrdUrxrje8ChPVMBNcotg7H9LQYg+u5De2RzI=
|
|
||||||
github.com/libdns/libdns v0.2.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
|
github.com/libdns/libdns v0.2.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
|
||||||
|
github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
|
||||||
|
github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
|
||||||
github.com/lopezator/migrator v0.3.0 h1:VW/rR+J8NYwPdkBxjrFdjwejpgvP59LbmANJxXuNbuk=
|
github.com/lopezator/migrator v0.3.0 h1:VW/rR+J8NYwPdkBxjrFdjwejpgvP59LbmANJxXuNbuk=
|
||||||
github.com/lopezator/migrator v0.3.0/go.mod h1:bpVAVPkWSvTw8ya2Pk7E/KiNAyDWNImgivQY79o8/8I=
|
github.com/lopezator/migrator v0.3.0/go.mod h1:bpVAVPkWSvTw8ya2Pk7E/KiNAyDWNImgivQY79o8/8I=
|
||||||
github.com/magiconair/properties v1.7.4-0.20170902060319-8d7837e64d3c/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
github.com/magiconair/properties v1.7.4-0.20170902060319-8d7837e64d3c/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
|
@ -262,8 +263,8 @@ github.com/paulmach/go.geojson v1.4.0 h1:5x5moCkCtDo5x8af62P9IOAYGQcYHtxz2QJ3x1D
|
||||||
github.com/paulmach/go.geojson v1.4.0/go.mod h1:YaKx1hKpWF+T2oj2lFJPsW/t1Q5e1jQI61eoQSTwpIs=
|
github.com/paulmach/go.geojson v1.4.0/go.mod h1:YaKx1hKpWF+T2oj2lFJPsW/t1Q5e1jQI61eoQSTwpIs=
|
||||||
github.com/pelletier/go-toml v1.0.1-0.20170904195809-1d6b12b7cb29/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
github.com/pelletier/go-toml v1.0.1-0.20170904195809-1d6b12b7cb29/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
github.com/pelletier/go-toml v1.9.1 h1:a6qW1EVNZWH9WGI6CsYdD8WAylkoXBS5yv0XHlh17Tc=
|
github.com/pelletier/go-toml v1.9.2 h1:7NiByeVF4jKSG1lDF3X8LTIkq2/bu+1uYbIm1eS5tzk=
|
||||||
github.com/pelletier/go-toml v1.9.1/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
github.com/pelletier/go-toml v1.9.2/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
@ -450,8 +451,8 @@ golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea h1:+WiDlPBBaO+h9vPNZi8uJ3k4BkKQB7Iow3aqwHVA5hI=
|
golang.org/x/sys v0.0.0-20210603125802-9665404d3644 h1:CA1DEQ4NdKphKeL70tvsWNdT5oFh1lOjihRcEDROi0I=
|
||||||
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
|
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
|
||||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
|
|
@ -6,8 +6,8 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func healthcheck() bool {
|
func (a *goBlog) healthcheck() bool {
|
||||||
req, err := http.NewRequest(http.MethodGet, appConfig.Server.PublicAddress+"/ping", nil)
|
req, err := http.NewRequest(http.MethodGet, a.cfg.Server.PublicAddress+"/ping", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
fmt.Println(err.Error())
|
||||||
return false
|
return false
|
||||||
|
@ -22,8 +22,8 @@ func healthcheck() bool {
|
||||||
return resp.StatusCode == 200
|
return resp.StatusCode == 200
|
||||||
}
|
}
|
||||||
|
|
||||||
func healthcheckExitCode() int {
|
func (a *goBlog) healthcheckExitCode() int {
|
||||||
if healthcheck() {
|
if a.healthcheck() {
|
||||||
return 0
|
return 0
|
||||||
} else {
|
} else {
|
||||||
return 1
|
return 1
|
||||||
|
|
56
hooks.go
56
hooks.go
|
@ -8,68 +8,62 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func preStartHooks() {
|
func (a *goBlog) preStartHooks() {
|
||||||
for _, cmd := range appConfig.Hooks.PreStart {
|
for _, cmd := range a.cfg.Hooks.PreStart {
|
||||||
func(cmd string) {
|
func(cmd string) {
|
||||||
log.Println("Executing pre-start hook:", cmd)
|
log.Println("Executing pre-start hook:", cmd)
|
||||||
executeCommand(cmd)
|
a.cfg.Hooks.executeCommand(cmd)
|
||||||
}(cmd)
|
}(cmd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type postHookFunc func(*post)
|
type postHookFunc func(*post)
|
||||||
|
|
||||||
var (
|
func (a *goBlog) postPostHooks(p *post) {
|
||||||
postPostHooks []postHookFunc
|
|
||||||
postUpdateHooks []postHookFunc
|
|
||||||
postDeleteHooks []postHookFunc
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p *post) postPostHooks() {
|
|
||||||
// Hooks after post published
|
// Hooks after post published
|
||||||
for _, cmdTmplString := range appConfig.Hooks.PostPost {
|
for _, cmdTmplString := range a.cfg.Hooks.PostPost {
|
||||||
go func(p *post, cmdTmplString string) {
|
go func(p *post, cmdTmplString string) {
|
||||||
executeTemplateCommand("post-post", cmdTmplString, map[string]interface{}{
|
a.cfg.Hooks.executeTemplateCommand("post-post", cmdTmplString, map[string]interface{}{
|
||||||
"URL": p.fullURL(),
|
"URL": a.fullPostURL(p),
|
||||||
"Post": p,
|
"Post": p,
|
||||||
})
|
})
|
||||||
}(p, cmdTmplString)
|
}(p, cmdTmplString)
|
||||||
}
|
}
|
||||||
for _, f := range postPostHooks {
|
for _, f := range a.pPostHooks {
|
||||||
go f(p)
|
go f(p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *post) postUpdateHooks() {
|
func (a *goBlog) postUpdateHooks(p *post) {
|
||||||
// Hooks after post updated
|
// Hooks after post updated
|
||||||
for _, cmdTmplString := range appConfig.Hooks.PostUpdate {
|
for _, cmdTmplString := range a.cfg.Hooks.PostUpdate {
|
||||||
go func(p *post, cmdTmplString string) {
|
go func(p *post, cmdTmplString string) {
|
||||||
executeTemplateCommand("post-update", cmdTmplString, map[string]interface{}{
|
a.cfg.Hooks.executeTemplateCommand("post-update", cmdTmplString, map[string]interface{}{
|
||||||
"URL": p.fullURL(),
|
"URL": a.fullPostURL(p),
|
||||||
"Post": p,
|
"Post": p,
|
||||||
})
|
})
|
||||||
}(p, cmdTmplString)
|
}(p, cmdTmplString)
|
||||||
}
|
}
|
||||||
for _, f := range postUpdateHooks {
|
for _, f := range a.pUpdateHooks {
|
||||||
go f(p)
|
go f(p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *post) postDeleteHooks() {
|
func (a *goBlog) postDeleteHooks(p *post) {
|
||||||
for _, cmdTmplString := range appConfig.Hooks.PostDelete {
|
for _, cmdTmplString := range a.cfg.Hooks.PostDelete {
|
||||||
go func(p *post, cmdTmplString string) {
|
go func(p *post, cmdTmplString string) {
|
||||||
executeTemplateCommand("post-delete", cmdTmplString, map[string]interface{}{
|
a.cfg.Hooks.executeTemplateCommand("post-delete", cmdTmplString, map[string]interface{}{
|
||||||
"URL": p.fullURL(),
|
"URL": a.fullPostURL(p),
|
||||||
"Post": p,
|
"Post": p,
|
||||||
})
|
})
|
||||||
}(p, cmdTmplString)
|
}(p, cmdTmplString)
|
||||||
}
|
}
|
||||||
for _, f := range postDeleteHooks {
|
for _, f := range a.pDeleteHooks {
|
||||||
go f(p)
|
go f(p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func executeTemplateCommand(hookType string, tmpl string, data map[string]interface{}) {
|
func (cfg *configHooks) executeTemplateCommand(hookType string, tmpl string, data map[string]interface{}) {
|
||||||
cmdTmpl, err := template.New("cmd").Parse(tmpl)
|
cmdTmpl, err := template.New("cmd").Parse(tmpl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Failed to parse cmd template:", err.Error())
|
log.Println("Failed to parse cmd template:", err.Error())
|
||||||
|
@ -82,18 +76,18 @@ func executeTemplateCommand(hookType string, tmpl string, data map[string]interf
|
||||||
}
|
}
|
||||||
cmd := cmdBuf.String()
|
cmd := cmdBuf.String()
|
||||||
log.Println("Executing "+hookType+" hook:", cmd)
|
log.Println("Executing "+hookType+" hook:", cmd)
|
||||||
executeCommand(cmd)
|
cfg.executeCommand(cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
var hourlyHooks = []func(){}
|
var hourlyHooks = []func(){}
|
||||||
|
|
||||||
func startHourlyHooks() {
|
func (a *goBlog) startHourlyHooks() {
|
||||||
// Add configured hourly hooks
|
// Add configured hourly hooks
|
||||||
for _, cmd := range appConfig.Hooks.Hourly {
|
for _, cmd := range a.cfg.Hooks.Hourly {
|
||||||
c := cmd
|
c := cmd
|
||||||
f := func() {
|
f := func() {
|
||||||
log.Println("Executing hourly hook:", c)
|
log.Println("Executing hourly hook:", c)
|
||||||
executeCommand(c)
|
a.cfg.Hooks.executeCommand(c)
|
||||||
}
|
}
|
||||||
hourlyHooks = append(hourlyHooks, f)
|
hourlyHooks = append(hourlyHooks, f)
|
||||||
}
|
}
|
||||||
|
@ -121,8 +115,8 @@ func startHourlyHooks() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func executeCommand(cmd string) {
|
func (cfg *configHooks) executeCommand(cmd string) {
|
||||||
out, err := exec.Command(appConfig.Hooks.Shell, "-c", cmd).CombinedOutput()
|
out, err := exec.Command(cfg.Shell, "-c", cmd).CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Failed to execute command:", err.Error())
|
log.Println("Failed to execute command:", err.Error())
|
||||||
}
|
}
|
||||||
|
|
406
http.go
406
http.go
|
@ -44,35 +44,33 @@ const (
|
||||||
appUserAgent = "GoBlog"
|
appUserAgent = "GoBlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
var d *dynamicHandler
|
func (a *goBlog) startServer() (err error) {
|
||||||
|
|
||||||
func startServer() (err error) {
|
|
||||||
// Start
|
// Start
|
||||||
d = &dynamicHandler{}
|
a.d = &dynamicHandler{}
|
||||||
// Set basic middlewares
|
// Set basic middlewares
|
||||||
var finalHandler http.Handler = d
|
var finalHandler http.Handler = a.d
|
||||||
if appConfig.Server.PublicHTTPS || appConfig.Server.SecurityHeaders {
|
if a.cfg.Server.PublicHTTPS || a.cfg.Server.SecurityHeaders {
|
||||||
finalHandler = securityHeaders(finalHandler)
|
finalHandler = a.securityHeaders(finalHandler)
|
||||||
}
|
}
|
||||||
finalHandler = servertiming.Middleware(finalHandler, nil)
|
finalHandler = servertiming.Middleware(finalHandler, nil)
|
||||||
finalHandler = middleware.Heartbeat("/ping")(finalHandler)
|
finalHandler = middleware.Heartbeat("/ping")(finalHandler)
|
||||||
finalHandler = middleware.Compress(flate.DefaultCompression)(finalHandler)
|
finalHandler = middleware.Compress(flate.DefaultCompression)(finalHandler)
|
||||||
finalHandler = middleware.Recoverer(finalHandler)
|
finalHandler = middleware.Recoverer(finalHandler)
|
||||||
if appConfig.Server.Logging {
|
if a.cfg.Server.Logging {
|
||||||
finalHandler = logMiddleware(finalHandler)
|
finalHandler = a.logMiddleware(finalHandler)
|
||||||
}
|
}
|
||||||
// Create routers that don't change
|
// Create routers that don't change
|
||||||
if err = buildStaticHandlersRouters(); err != nil {
|
if err = a.buildStaticHandlersRouters(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Load router
|
// Load router
|
||||||
if err = reloadRouter(); err != nil {
|
if err = a.reloadRouter(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Start Onion service
|
// Start Onion service
|
||||||
if appConfig.Server.Tor {
|
if a.cfg.Server.Tor {
|
||||||
go func() {
|
go func() {
|
||||||
if err := startOnionService(finalHandler); err != nil {
|
if err := a.startOnionService(finalHandler); err != nil {
|
||||||
log.Println("Tor failed:", err.Error())
|
log.Println("Tor failed:", err.Error())
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@ -84,10 +82,10 @@ func startServer() (err error) {
|
||||||
WriteTimeout: 5 * time.Minute,
|
WriteTimeout: 5 * time.Minute,
|
||||||
}
|
}
|
||||||
addShutdownFunc(shutdownServer(s, "main server"))
|
addShutdownFunc(shutdownServer(s, "main server"))
|
||||||
if appConfig.Server.PublicHTTPS {
|
if a.cfg.Server.PublicHTTPS {
|
||||||
// Configure
|
// Configure
|
||||||
certmagic.Default.Storage = &certmagic.FileStorage{Path: "data/https"}
|
certmagic.Default.Storage = &certmagic.FileStorage{Path: "data/https"}
|
||||||
certmagic.DefaultACME.Email = appConfig.Server.LetsEncryptMail
|
certmagic.DefaultACME.Email = a.cfg.Server.LetsEncryptMail
|
||||||
certmagic.DefaultACME.CA = certmagic.LetsEncryptProductionCA
|
certmagic.DefaultACME.CA = certmagic.LetsEncryptProductionCA
|
||||||
// Start HTTP server for redirects
|
// Start HTTP server for redirects
|
||||||
httpServer := &http.Server{
|
httpServer := &http.Server{
|
||||||
|
@ -104,9 +102,9 @@ func startServer() (err error) {
|
||||||
}()
|
}()
|
||||||
// Start HTTPS
|
// Start HTTPS
|
||||||
s.Addr = ":https"
|
s.Addr = ":https"
|
||||||
hosts := []string{appConfig.Server.publicHostname}
|
hosts := []string{a.cfg.Server.publicHostname}
|
||||||
if appConfig.Server.shortPublicHostname != "" {
|
if a.cfg.Server.shortPublicHostname != "" {
|
||||||
hosts = append(hosts, appConfig.Server.shortPublicHostname)
|
hosts = append(hosts, a.cfg.Server.shortPublicHostname)
|
||||||
}
|
}
|
||||||
listener, e := certmagic.Listen(hosts)
|
listener, e := certmagic.Listen(hosts)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
|
@ -116,7 +114,7 @@ func startServer() (err error) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
s.Addr = ":" + strconv.Itoa(appConfig.Server.Port)
|
s.Addr = ":" + strconv.Itoa(a.cfg.Server.Port)
|
||||||
if err = s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
if err = s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -142,13 +140,13 @@ func redirectToHttps(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Redirect(w, r, fmt.Sprintf("https://%s%s", requestHost, r.URL.RequestURI()), http.StatusMovedPermanently)
|
http.Redirect(w, r, fmt.Sprintf("https://%s%s", requestHost, r.URL.RequestURI()), http.StatusMovedPermanently)
|
||||||
}
|
}
|
||||||
|
|
||||||
func reloadRouter() error {
|
func (a *goBlog) reloadRouter() error {
|
||||||
h, err := buildDynamicRouter()
|
h, err := a.buildDynamicRouter()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
d.swapHandler(h)
|
a.d.swapHandler(h)
|
||||||
purgeCache()
|
a.cache.purge()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,107 +155,101 @@ const (
|
||||||
feedPath = ".{feed:rss|json|atom}"
|
feedPath = ".{feed:rss|json|atom}"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
func (a *goBlog) buildStaticHandlersRouters() error {
|
||||||
privateMode = false
|
if pm := a.cfg.PrivateMode; pm != nil && pm.Enabled {
|
||||||
privateModeHandler = []func(http.Handler) http.Handler{}
|
a.privateMode = true
|
||||||
|
a.privateModeHandler = append(a.privateModeHandler, a.authMiddleware)
|
||||||
captchaHandler http.Handler
|
} else {
|
||||||
|
a.privateMode = false
|
||||||
micropubRouter, indieAuthRouter, webmentionsRouter, notificationsRouter, activitypubRouter, editorRouter, commentsRouter, searchRouter *chi.Mux
|
a.privateModeHandler = []func(http.Handler) http.Handler{}
|
||||||
|
|
||||||
setBlogMiddlewares = map[string]func(http.Handler) http.Handler{}
|
|
||||||
sectionMiddlewares = map[string]func(http.Handler) http.Handler{}
|
|
||||||
taxonomyMiddlewares = 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{}
|
|
||||||
commentsMiddlewares = map[string]func(http.Handler) http.Handler{}
|
|
||||||
)
|
|
||||||
|
|
||||||
func buildStaticHandlersRouters() error {
|
|
||||||
if pm := appConfig.PrivateMode; pm != nil && pm.Enabled {
|
|
||||||
privateMode = true
|
|
||||||
privateModeHandler = append(privateModeHandler, authMiddleware)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
captchaHandler = captcha.Server(500, 250)
|
a.captchaHandler = captcha.Server(500, 250)
|
||||||
|
|
||||||
micropubRouter = chi.NewRouter()
|
a.micropubRouter = chi.NewRouter()
|
||||||
micropubRouter.Use(checkIndieAuth)
|
a.micropubRouter.Use(a.checkIndieAuth)
|
||||||
micropubRouter.Get("/", serveMicropubQuery)
|
a.micropubRouter.Get("/", a.serveMicropubQuery)
|
||||||
micropubRouter.Post("/", serveMicropubPost)
|
a.micropubRouter.Post("/", a.serveMicropubPost)
|
||||||
micropubRouter.Post(micropubMediaSubPath, serveMicropubMedia)
|
a.micropubRouter.Post(micropubMediaSubPath, a.serveMicropubMedia)
|
||||||
|
|
||||||
indieAuthRouter = chi.NewRouter()
|
a.indieAuthRouter = chi.NewRouter()
|
||||||
indieAuthRouter.Get("/", indieAuthRequest)
|
a.indieAuthRouter.Get("/", a.indieAuthRequest)
|
||||||
indieAuthRouter.With(authMiddleware).Post("/accept", indieAuthAccept)
|
a.indieAuthRouter.With(a.authMiddleware).Post("/accept", a.indieAuthAccept)
|
||||||
indieAuthRouter.Post("/", indieAuthVerification)
|
a.indieAuthRouter.Post("/", a.indieAuthVerification)
|
||||||
indieAuthRouter.Get("/token", indieAuthToken)
|
a.indieAuthRouter.Get("/token", a.indieAuthToken)
|
||||||
indieAuthRouter.Post("/token", indieAuthToken)
|
a.indieAuthRouter.Post("/token", a.indieAuthToken)
|
||||||
|
|
||||||
webmentionsRouter = chi.NewRouter()
|
a.webmentionsRouter = chi.NewRouter()
|
||||||
if wm := appConfig.Webmention; wm != nil && !wm.DisableReceiving {
|
if wm := a.cfg.Webmention; wm != nil && !wm.DisableReceiving {
|
||||||
webmentionsRouter.Post("/", handleWebmention)
|
a.webmentionsRouter.Post("/", a.handleWebmention)
|
||||||
webmentionsRouter.Group(func(r chi.Router) {
|
a.webmentionsRouter.Group(func(r chi.Router) {
|
||||||
// Authenticated routes
|
// Authenticated routes
|
||||||
r.Use(authMiddleware)
|
r.Use(a.authMiddleware)
|
||||||
r.Get("/", webmentionAdmin)
|
r.Get("/", a.webmentionAdmin)
|
||||||
r.Get(paginationPath, webmentionAdmin)
|
r.Get(paginationPath, a.webmentionAdmin)
|
||||||
r.Post("/delete", webmentionAdminDelete)
|
r.Post("/delete", a.webmentionAdminDelete)
|
||||||
r.Post("/approve", webmentionAdminApprove)
|
r.Post("/approve", a.webmentionAdminApprove)
|
||||||
r.Post("/reverify", webmentionAdminReverify)
|
r.Post("/reverify", a.webmentionAdminReverify)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationsRouter = chi.NewRouter()
|
a.notificationsRouter = chi.NewRouter()
|
||||||
notificationsRouter.Use(authMiddleware)
|
a.notificationsRouter.Use(a.authMiddleware)
|
||||||
notificationsRouter.Get("/", notificationsAdmin)
|
a.notificationsRouter.Get("/", a.notificationsAdmin)
|
||||||
notificationsRouter.Get(paginationPath, notificationsAdmin)
|
a.notificationsRouter.Get(paginationPath, a.notificationsAdmin)
|
||||||
notificationsRouter.Post("/delete", notificationsAdminDelete)
|
a.notificationsRouter.Post("/delete", a.notificationsAdminDelete)
|
||||||
|
|
||||||
if ap := appConfig.ActivityPub; ap != nil && ap.Enabled {
|
if ap := a.cfg.ActivityPub; ap != nil && ap.Enabled {
|
||||||
activitypubRouter = chi.NewRouter()
|
a.activitypubRouter = chi.NewRouter()
|
||||||
activitypubRouter.Post("/inbox/{blog}", apHandleInbox)
|
a.activitypubRouter.Post("/inbox/{blog}", a.apHandleInbox)
|
||||||
activitypubRouter.Post("/{blog}/inbox", apHandleInbox)
|
a.activitypubRouter.Post("/{blog}/inbox", a.apHandleInbox)
|
||||||
}
|
}
|
||||||
|
|
||||||
editorRouter = chi.NewRouter()
|
a.editorRouter = chi.NewRouter()
|
||||||
editorRouter.Use(authMiddleware)
|
a.editorRouter.Use(a.authMiddleware)
|
||||||
editorRouter.Get("/", serveEditor)
|
a.editorRouter.Get("/", a.serveEditor)
|
||||||
editorRouter.Post("/", serveEditorPost)
|
a.editorRouter.Post("/", a.serveEditorPost)
|
||||||
|
|
||||||
commentsRouter = chi.NewRouter()
|
a.commentsRouter = chi.NewRouter()
|
||||||
commentsRouter.Use(privateModeHandler...)
|
a.commentsRouter.Use(a.privateModeHandler...)
|
||||||
commentsRouter.With(cacheMiddleware, noIndexHeader).Get("/{id:[0-9]+}", serveComment)
|
a.commentsRouter.With(a.cache.cacheMiddleware, noIndexHeader).Get("/{id:[0-9]+}", a.serveComment)
|
||||||
commentsRouter.With(captchaMiddleware).Post("/", createComment)
|
a.commentsRouter.With(a.captchaMiddleware).Post("/", a.createComment)
|
||||||
commentsRouter.Group(func(r chi.Router) {
|
a.commentsRouter.Group(func(r chi.Router) {
|
||||||
// Admin
|
// Admin
|
||||||
r.Use(authMiddleware)
|
r.Use(a.authMiddleware)
|
||||||
r.Get("/", commentsAdmin)
|
r.Get("/", a.commentsAdmin)
|
||||||
r.Get(paginationPath, commentsAdmin)
|
r.Get(paginationPath, a.commentsAdmin)
|
||||||
r.Post("/delete", commentsAdminDelete)
|
r.Post("/delete", a.commentsAdminDelete)
|
||||||
})
|
})
|
||||||
|
|
||||||
searchRouter = chi.NewRouter()
|
a.searchRouter = chi.NewRouter()
|
||||||
searchRouter.Use(privateModeHandler...)
|
a.searchRouter.Use(a.privateModeHandler...)
|
||||||
searchRouter.Use(cacheMiddleware)
|
a.searchRouter.Use(a.cache.cacheMiddleware)
|
||||||
searchRouter.Get("/", serveSearch)
|
a.searchRouter.Get("/", a.serveSearch)
|
||||||
searchRouter.Post("/", serveSearch)
|
a.searchRouter.Post("/", a.serveSearch)
|
||||||
searchResultPath := "/" + searchPlaceholder
|
searchResultPath := "/" + searchPlaceholder
|
||||||
searchRouter.Get(searchResultPath, serveSearchResult)
|
a.searchRouter.Get(searchResultPath, a.serveSearchResult)
|
||||||
searchRouter.Get(searchResultPath+feedPath, serveSearchResult)
|
a.searchRouter.Get(searchResultPath+feedPath, a.serveSearchResult)
|
||||||
searchRouter.Get(searchResultPath+paginationPath, serveSearchResult)
|
a.searchRouter.Get(searchResultPath+paginationPath, a.serveSearchResult)
|
||||||
|
|
||||||
for blog, blogConfig := range appConfig.Blogs {
|
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.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{}
|
||||||
|
a.commentsMiddlewares = map[string]func(http.Handler) http.Handler{}
|
||||||
|
|
||||||
|
for blog, blogConfig := range a.cfg.Blogs {
|
||||||
sbm := middleware.WithValue(blogContextKey, blog)
|
sbm := middleware.WithValue(blogContextKey, blog)
|
||||||
setBlogMiddlewares[blog] = sbm
|
a.setBlogMiddlewares[blog] = sbm
|
||||||
|
|
||||||
blogPath := blogPath(blog)
|
blogPath := a.blogPath(blog)
|
||||||
|
|
||||||
for _, section := range blogConfig.Sections {
|
for _, section := range blogConfig.Sections {
|
||||||
if section.Name != "" {
|
if section.Name != "" {
|
||||||
secPath := blogPath + "/" + section.Name
|
secPath := blogPath + "/" + section.Name
|
||||||
sectionMiddlewares[secPath] = middleware.WithValue(indexConfigKey, &indexConfig{
|
a.sectionMiddlewares[secPath] = middleware.WithValue(indexConfigKey, &indexConfig{
|
||||||
path: secPath,
|
path: secPath,
|
||||||
section: section,
|
section: section,
|
||||||
})
|
})
|
||||||
|
@ -267,12 +259,12 @@ func buildStaticHandlersRouters() error {
|
||||||
for _, taxonomy := range blogConfig.Taxonomies {
|
for _, taxonomy := range blogConfig.Taxonomies {
|
||||||
if taxonomy.Name != "" {
|
if taxonomy.Name != "" {
|
||||||
taxPath := blogPath + "/" + taxonomy.Name
|
taxPath := blogPath + "/" + taxonomy.Name
|
||||||
taxonomyMiddlewares[taxPath] = middleware.WithValue(taxonomyContextKey, taxonomy)
|
a.taxonomyMiddlewares[taxPath] = middleware.WithValue(taxonomyContextKey, taxonomy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if blogConfig.Photos != nil && blogConfig.Photos.Enabled {
|
if blogConfig.Photos != nil && blogConfig.Photos.Enabled {
|
||||||
photosMiddlewares[blog] = middleware.WithValue(indexConfigKey, &indexConfig{
|
a.photosMiddlewares[blog] = middleware.WithValue(indexConfigKey, &indexConfig{
|
||||||
path: blogPath + blogConfig.Photos.Path,
|
path: blogPath + blogConfig.Photos.Path,
|
||||||
parameter: blogConfig.Photos.Parameter,
|
parameter: blogConfig.Photos.Parameter,
|
||||||
title: blogConfig.Photos.Title,
|
title: blogConfig.Photos.Title,
|
||||||
|
@ -282,15 +274,15 @@ func buildStaticHandlersRouters() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if blogConfig.Search != nil && blogConfig.Search.Enabled {
|
if blogConfig.Search != nil && blogConfig.Search.Enabled {
|
||||||
searchMiddlewares[blog] = middleware.WithValue(pathContextKey, blogPath+blogConfig.Search.Path)
|
a.searchMiddlewares[blog] = middleware.WithValue(pathContextKey, blogPath+blogConfig.Search.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cp := range blogConfig.CustomPages {
|
for _, cp := range blogConfig.CustomPages {
|
||||||
customPagesMiddlewares[cp.Path] = middleware.WithValue(customPageContextKey, cp)
|
a.customPagesMiddlewares[cp.Path] = middleware.WithValue(customPageContextKey, cp)
|
||||||
}
|
}
|
||||||
|
|
||||||
if commentsConfig := blogConfig.Comments; commentsConfig != nil && commentsConfig.Enabled {
|
if commentsConfig := blogConfig.Comments; commentsConfig != nil && commentsConfig.Enabled {
|
||||||
commentsMiddlewares[blog] = middleware.WithValue(pathContextKey, blogPath+"/comment")
|
a.commentsMiddlewares[blog] = middleware.WithValue(pathContextKey, blogPath+"/comment")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -301,127 +293,127 @@ var (
|
||||||
taxValueMiddlewares = map[string]func(http.Handler) http.Handler{}
|
taxValueMiddlewares = map[string]func(http.Handler) http.Handler{}
|
||||||
)
|
)
|
||||||
|
|
||||||
func buildDynamicRouter() (*chi.Mux, error) {
|
func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
|
|
||||||
// Basic middleware
|
// Basic middleware
|
||||||
r.Use(redirectShortDomain)
|
r.Use(a.redirectShortDomain)
|
||||||
r.Use(middleware.RedirectSlashes)
|
r.Use(middleware.RedirectSlashes)
|
||||||
r.Use(middleware.CleanPath)
|
r.Use(middleware.CleanPath)
|
||||||
r.Use(middleware.GetHead)
|
r.Use(middleware.GetHead)
|
||||||
if !appConfig.Cache.Enable {
|
if !a.cfg.Cache.Enable {
|
||||||
r.Use(middleware.NoCache)
|
r.Use(middleware.NoCache)
|
||||||
}
|
}
|
||||||
|
|
||||||
// No Index Header
|
// No Index Header
|
||||||
if privateMode {
|
if a.privateMode {
|
||||||
r.Use(noIndexHeader)
|
r.Use(noIndexHeader)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login middleware etc.
|
// Login middleware etc.
|
||||||
r.Use(checkIsLogin)
|
r.Use(a.checkIsLogin)
|
||||||
r.Use(checkIsCaptcha)
|
r.Use(a.checkIsCaptcha)
|
||||||
r.Use(checkLoggedIn)
|
r.Use(a.checkLoggedIn)
|
||||||
|
|
||||||
// Logout
|
// Logout
|
||||||
r.With(authMiddleware).Get("/login", serveLogin)
|
r.With(a.authMiddleware).Get("/login", serveLogin)
|
||||||
r.With(authMiddleware).Get("/logout", serveLogout)
|
r.With(a.authMiddleware).Get("/logout", a.serveLogout)
|
||||||
|
|
||||||
// Micropub
|
// Micropub
|
||||||
r.Mount(micropubPath, micropubRouter)
|
r.Mount(micropubPath, a.micropubRouter)
|
||||||
|
|
||||||
// IndieAuth
|
// IndieAuth
|
||||||
r.Mount("/indieauth", indieAuthRouter)
|
r.Mount("/indieauth", a.indieAuthRouter)
|
||||||
|
|
||||||
// ActivityPub and stuff
|
// ActivityPub and stuff
|
||||||
if ap := appConfig.ActivityPub; ap != nil && ap.Enabled {
|
if ap := a.cfg.ActivityPub; ap != nil && ap.Enabled {
|
||||||
r.Mount("/activitypub", activitypubRouter)
|
r.Mount("/activitypub", a.activitypubRouter)
|
||||||
r.With(cacheMiddleware).Get("/.well-known/webfinger", apHandleWebfinger)
|
r.With(a.cache.cacheMiddleware).Get("/.well-known/webfinger", a.apHandleWebfinger)
|
||||||
r.With(cacheMiddleware).Get("/.well-known/host-meta", handleWellKnownHostMeta)
|
r.With(a.cache.cacheMiddleware).Get("/.well-known/host-meta", handleWellKnownHostMeta)
|
||||||
r.With(cacheMiddleware).Get("/.well-known/nodeinfo", serveNodeInfoDiscover)
|
r.With(a.cache.cacheMiddleware).Get("/.well-known/nodeinfo", a.serveNodeInfoDiscover)
|
||||||
r.With(cacheMiddleware).Get("/nodeinfo", serveNodeInfo)
|
r.With(a.cache.cacheMiddleware).Get("/nodeinfo", a.serveNodeInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Webmentions
|
// Webmentions
|
||||||
r.Mount(webmentionPath, webmentionsRouter)
|
r.Mount(webmentionPath, a.webmentionsRouter)
|
||||||
|
|
||||||
// Notifications
|
// Notifications
|
||||||
r.Mount(notificationsPath, notificationsRouter)
|
r.Mount(notificationsPath, a.notificationsRouter)
|
||||||
|
|
||||||
// Posts
|
// Posts
|
||||||
pp, err := allPostPaths(statusPublished)
|
pp, err := a.db.allPostPaths(statusPublished)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(privateModeHandler...)
|
r.Use(a.privateModeHandler...)
|
||||||
r.Use(checkActivityStreamsRequest, cacheMiddleware)
|
r.Use(a.checkActivityStreamsRequest, a.cache.cacheMiddleware)
|
||||||
for _, path := range pp {
|
for _, path := range pp {
|
||||||
r.Get(path, servePost)
|
r.Get(path, a.servePost)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Drafts
|
// Drafts
|
||||||
dp, err := allPostPaths(statusDraft)
|
dp, err := a.db.allPostPaths(statusDraft)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(authMiddleware)
|
r.Use(a.authMiddleware)
|
||||||
for _, path := range dp {
|
for _, path := range dp {
|
||||||
r.Get(path, servePost)
|
r.Get(path, a.servePost)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Post aliases
|
// Post aliases
|
||||||
allPostAliases, err := allPostAliases()
|
allPostAliases, err := a.db.allPostAliases()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(privateModeHandler...)
|
r.Use(a.privateModeHandler...)
|
||||||
r.Use(cacheMiddleware)
|
r.Use(a.cache.cacheMiddleware)
|
||||||
for _, path := range allPostAliases {
|
for _, path := range allPostAliases {
|
||||||
r.Get(path, servePostAlias)
|
r.Get(path, a.servePostAlias)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Assets
|
// Assets
|
||||||
for _, path := range allAssetPaths() {
|
for _, path := range a.allAssetPaths() {
|
||||||
r.Get(path, serveAsset)
|
r.Get(path, a.serveAsset)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Static files
|
// Static files
|
||||||
for _, path := range allStaticPaths() {
|
for _, path := range allStaticPaths() {
|
||||||
r.Get(path, serveStaticFile)
|
r.Get(path, a.serveStaticFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Media files
|
// Media files
|
||||||
r.With(privateModeHandler...).Get(`/m/{file:[0-9a-fA-F]+(\.[0-9a-zA-Z]+)?}`, serveMediaFile)
|
r.With(a.privateModeHandler...).Get(`/m/{file:[0-9a-fA-F]+(\.[0-9a-zA-Z]+)?}`, a.serveMediaFile)
|
||||||
|
|
||||||
// Captcha
|
// Captcha
|
||||||
r.Handle("/captcha/*", captchaHandler)
|
r.Handle("/captcha/*", a.captchaHandler)
|
||||||
|
|
||||||
// Short paths
|
// Short paths
|
||||||
r.With(privateModeHandler...).With(cacheMiddleware).Get("/s/{id:[0-9a-fA-F]+}", redirectToLongPath)
|
r.With(a.privateModeHandler...).With(a.cache.cacheMiddleware).Get("/s/{id:[0-9a-fA-F]+}", a.redirectToLongPath)
|
||||||
|
|
||||||
for blog, blogConfig := range appConfig.Blogs {
|
for blog, blogConfig := range a.cfg.Blogs {
|
||||||
blogPath := blogPath(blog)
|
blogPath := a.blogPath(blog)
|
||||||
|
|
||||||
sbm := setBlogMiddlewares[blog]
|
sbm := a.setBlogMiddlewares[blog]
|
||||||
|
|
||||||
// Sections
|
// Sections
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(privateModeHandler...)
|
r.Use(a.privateModeHandler...)
|
||||||
r.Use(cacheMiddleware, sbm)
|
r.Use(a.cache.cacheMiddleware, sbm)
|
||||||
for _, section := range blogConfig.Sections {
|
for _, section := range blogConfig.Sections {
|
||||||
if section.Name != "" {
|
if section.Name != "" {
|
||||||
secPath := blogPath + "/" + section.Name
|
secPath := blogPath + "/" + section.Name
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(sectionMiddlewares[secPath])
|
r.Use(a.sectionMiddlewares[secPath])
|
||||||
r.Get(secPath, serveIndex)
|
r.Get(secPath, a.serveIndex)
|
||||||
r.Get(secPath+feedPath, serveIndex)
|
r.Get(secPath+feedPath, a.serveIndex)
|
||||||
r.Get(secPath+paginationPath, serveIndex)
|
r.Get(secPath+paginationPath, a.serveIndex)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -431,14 +423,14 @@ func buildDynamicRouter() (*chi.Mux, error) {
|
||||||
for _, taxonomy := range blogConfig.Taxonomies {
|
for _, taxonomy := range blogConfig.Taxonomies {
|
||||||
if taxonomy.Name != "" {
|
if taxonomy.Name != "" {
|
||||||
taxPath := blogPath + "/" + taxonomy.Name
|
taxPath := blogPath + "/" + taxonomy.Name
|
||||||
taxValues, err := allTaxonomyValues(blog, taxonomy.Name)
|
taxValues, err := a.db.allTaxonomyValues(blog, taxonomy.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(privateModeHandler...)
|
r.Use(a.privateModeHandler...)
|
||||||
r.Use(cacheMiddleware, sbm)
|
r.Use(a.cache.cacheMiddleware, sbm)
|
||||||
r.With(taxonomyMiddlewares[taxPath]).Get(taxPath, serveTaxonomy)
|
r.With(a.taxonomyMiddlewares[taxPath]).Get(taxPath, a.serveTaxonomy)
|
||||||
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)
|
||||||
|
@ -450,9 +442,9 @@ func buildDynamicRouter() (*chi.Mux, error) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
r.Use(taxValueMiddlewares[vPath])
|
r.Use(taxValueMiddlewares[vPath])
|
||||||
r.Get(vPath, serveIndex)
|
r.Get(vPath, a.serveIndex)
|
||||||
r.Get(vPath+feedPath, serveIndex)
|
r.Get(vPath+feedPath, a.serveIndex)
|
||||||
r.Get(vPath+paginationPath, serveIndex)
|
r.Get(vPath+paginationPath, a.serveIndex)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -462,75 +454,75 @@ func buildDynamicRouter() (*chi.Mux, error) {
|
||||||
// Photos
|
// Photos
|
||||||
if blogConfig.Photos != nil && blogConfig.Photos.Enabled {
|
if blogConfig.Photos != nil && blogConfig.Photos.Enabled {
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(privateModeHandler...)
|
r.Use(a.privateModeHandler...)
|
||||||
r.Use(cacheMiddleware, sbm, photosMiddlewares[blog])
|
r.Use(a.cache.cacheMiddleware, sbm, a.photosMiddlewares[blog])
|
||||||
photoPath := blogPath + blogConfig.Photos.Path
|
photoPath := blogPath + blogConfig.Photos.Path
|
||||||
r.Get(photoPath, serveIndex)
|
r.Get(photoPath, a.serveIndex)
|
||||||
r.Get(photoPath+feedPath, serveIndex)
|
r.Get(photoPath+feedPath, a.serveIndex)
|
||||||
r.Get(photoPath+paginationPath, serveIndex)
|
r.Get(photoPath+paginationPath, a.serveIndex)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
if blogConfig.Search != nil && blogConfig.Search.Enabled {
|
if blogConfig.Search != nil && blogConfig.Search.Enabled {
|
||||||
searchPath := blogPath + blogConfig.Search.Path
|
searchPath := blogPath + blogConfig.Search.Path
|
||||||
r.With(sbm, searchMiddlewares[blog]).Mount(searchPath, searchRouter)
|
r.With(sbm, a.searchMiddlewares[blog]).Mount(searchPath, a.searchRouter)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stats
|
// Stats
|
||||||
if blogConfig.BlogStats != nil && blogConfig.BlogStats.Enabled {
|
if blogConfig.BlogStats != nil && blogConfig.BlogStats.Enabled {
|
||||||
statsPath := blogPath + blogConfig.BlogStats.Path
|
statsPath := blogPath + blogConfig.BlogStats.Path
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(privateModeHandler...)
|
r.Use(a.privateModeHandler...)
|
||||||
r.Use(cacheMiddleware, sbm)
|
r.Use(a.cache.cacheMiddleware, sbm)
|
||||||
r.Get(statsPath, serveBlogStats)
|
r.Get(statsPath, a.serveBlogStats)
|
||||||
r.Get(statsPath+".table.html", serveBlogStatsTable)
|
r.Get(statsPath+".table.html", a.serveBlogStatsTable)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Date archives
|
// Date archives
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(privateModeHandler...)
|
r.Use(a.privateModeHandler...)
|
||||||
r.Use(cacheMiddleware, sbm)
|
r.Use(a.cache.cacheMiddleware, sbm)
|
||||||
|
|
||||||
yearRegex := `/{year:x|\d\d\d\d}`
|
yearRegex := `/{year:x|\d\d\d\d}`
|
||||||
monthRegex := `/{month:x|\d\d}`
|
monthRegex := `/{month:x|\d\d}`
|
||||||
dayRegex := `/{day:\d\d}`
|
dayRegex := `/{day:\d\d}`
|
||||||
|
|
||||||
yearPath := blogPath + yearRegex
|
yearPath := blogPath + yearRegex
|
||||||
r.Get(yearPath, serveDate)
|
r.Get(yearPath, a.serveDate)
|
||||||
r.Get(yearPath+feedPath, serveDate)
|
r.Get(yearPath+feedPath, a.serveDate)
|
||||||
r.Get(yearPath+paginationPath, serveDate)
|
r.Get(yearPath+paginationPath, a.serveDate)
|
||||||
|
|
||||||
monthPath := yearPath + monthRegex
|
monthPath := yearPath + monthRegex
|
||||||
r.Get(monthPath, serveDate)
|
r.Get(monthPath, a.serveDate)
|
||||||
r.Get(monthPath+feedPath, serveDate)
|
r.Get(monthPath+feedPath, a.serveDate)
|
||||||
r.Get(monthPath+paginationPath, serveDate)
|
r.Get(monthPath+paginationPath, a.serveDate)
|
||||||
|
|
||||||
dayPath := monthPath + dayRegex
|
dayPath := monthPath + dayRegex
|
||||||
r.Get(dayPath, serveDate)
|
r.Get(dayPath, a.serveDate)
|
||||||
r.Get(dayPath+feedPath, serveDate)
|
r.Get(dayPath+feedPath, a.serveDate)
|
||||||
r.Get(dayPath+paginationPath, serveDate)
|
r.Get(dayPath+paginationPath, a.serveDate)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Blog
|
// Blog
|
||||||
if !blogConfig.PostAsHome {
|
if !blogConfig.PostAsHome {
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(privateModeHandler...)
|
r.Use(a.privateModeHandler...)
|
||||||
r.Use(sbm)
|
r.Use(sbm)
|
||||||
r.With(checkActivityStreamsRequest, cacheMiddleware).Get(blogConfig.Path, serveHome)
|
r.With(a.checkActivityStreamsRequest, a.cache.cacheMiddleware).Get(blogConfig.Path, a.serveHome)
|
||||||
r.With(cacheMiddleware).Get(blogConfig.Path+feedPath, serveHome)
|
r.With(a.cache.cacheMiddleware).Get(blogConfig.Path+feedPath, a.serveHome)
|
||||||
r.With(cacheMiddleware).Get(blogPath+paginationPath, serveHome)
|
r.With(a.cache.cacheMiddleware).Get(blogPath+paginationPath, a.serveHome)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom pages
|
// Custom pages
|
||||||
for _, cp := range blogConfig.CustomPages {
|
for _, cp := range blogConfig.CustomPages {
|
||||||
scp := customPagesMiddlewares[cp.Path]
|
scp := a.customPagesMiddlewares[cp.Path]
|
||||||
if cp.Cache {
|
if cp.Cache {
|
||||||
r.With(privateModeHandler...).With(cacheMiddleware, sbm, scp).Get(cp.Path, serveCustomPage)
|
r.With(a.privateModeHandler...).With(a.cache.cacheMiddleware, sbm, scp).Get(cp.Path, a.serveCustomPage)
|
||||||
} else {
|
} else {
|
||||||
r.With(privateModeHandler...).With(sbm, scp).Get(cp.Path, serveCustomPage)
|
r.With(a.privateModeHandler...).With(sbm, scp).Get(cp.Path, a.serveCustomPage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -540,50 +532,50 @@ func buildDynamicRouter() (*chi.Mux, error) {
|
||||||
if randomPath == "" {
|
if randomPath == "" {
|
||||||
randomPath = "/random"
|
randomPath = "/random"
|
||||||
}
|
}
|
||||||
r.With(privateModeHandler...).With(sbm).Get(blogPath+randomPath, redirectToRandomPost)
|
r.With(a.privateModeHandler...).With(sbm).Get(blogPath+randomPath, a.redirectToRandomPost)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Editor
|
// Editor
|
||||||
r.With(sbm).Mount(blogPath+"/editor", editorRouter)
|
r.With(sbm).Mount(blogPath+"/editor", a.editorRouter)
|
||||||
|
|
||||||
// Comments
|
// Comments
|
||||||
if commentsConfig := blogConfig.Comments; commentsConfig != nil && commentsConfig.Enabled {
|
if commentsConfig := blogConfig.Comments; commentsConfig != nil && commentsConfig.Enabled {
|
||||||
commentsPath := blogPath + "/comment"
|
commentsPath := blogPath + "/comment"
|
||||||
r.With(sbm, commentsMiddlewares[blog]).Mount(commentsPath, commentsRouter)
|
r.With(sbm, a.commentsMiddlewares[blog]).Mount(commentsPath, a.commentsRouter)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blogroll
|
// Blogroll
|
||||||
if brConfig := blogConfig.Blogroll; brConfig != nil && brConfig.Enabled {
|
if brConfig := blogConfig.Blogroll; brConfig != nil && brConfig.Enabled {
|
||||||
brPath := blogPath + brConfig.Path
|
brPath := blogPath + brConfig.Path
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(privateModeHandler...)
|
r.Use(a.privateModeHandler...)
|
||||||
r.Use(cacheMiddleware, sbm)
|
r.Use(a.cache.cacheMiddleware, sbm)
|
||||||
r.Get(brPath, serveBlogroll)
|
r.Get(brPath, a.serveBlogroll)
|
||||||
r.Get(brPath+".opml", serveBlogrollExport)
|
r.Get(brPath+".opml", a.serveBlogrollExport)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sitemap
|
// Sitemap
|
||||||
r.With(privateModeHandler...).With(cacheMiddleware).Get(sitemapPath, serveSitemap)
|
r.With(a.privateModeHandler...).With(a.cache.cacheMiddleware).Get(sitemapPath, a.serveSitemap)
|
||||||
|
|
||||||
// Robots.txt - doesn't need cache, because it's too simple
|
// Robots.txt - doesn't need cache, because it's too simple
|
||||||
if !privateMode {
|
if !a.privateMode {
|
||||||
r.Get("/robots.txt", serveRobotsTXT)
|
r.Get("/robots.txt", a.serveRobotsTXT)
|
||||||
} else {
|
} else {
|
||||||
r.Get("/robots.txt", servePrivateRobotsTXT)
|
r.Get("/robots.txt", servePrivateRobotsTXT)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check redirects, then serve 404
|
// Check redirects, then serve 404
|
||||||
r.With(cacheMiddleware, checkRegexRedirects).NotFound(serve404)
|
r.With(a.cache.cacheMiddleware, a.checkRegexRedirects).NotFound(a.serve404)
|
||||||
|
|
||||||
r.MethodNotAllowed(serveNotAllowed)
|
r.MethodNotAllowed(a.serveNotAllowed)
|
||||||
|
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func blogPath(blog string) string {
|
func (a *goBlog) blogPath(blog string) string {
|
||||||
blogPath := appConfig.Blogs[blog].Path
|
blogPath := a.cfg.Blogs[blog].Path
|
||||||
if blogPath == "/" {
|
if blogPath == "/" {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -595,20 +587,20 @@ const pathContextKey requestContextKey = "httpPath"
|
||||||
|
|
||||||
var cspDomains = ""
|
var cspDomains = ""
|
||||||
|
|
||||||
func refreshCSPDomains() {
|
func (a *goBlog) refreshCSPDomains() {
|
||||||
cspDomains = ""
|
cspDomains = ""
|
||||||
if mp := appConfig.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()
|
cspDomains += " " + u.Hostname()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(appConfig.Server.CSPDomains) > 0 {
|
if len(a.cfg.Server.CSPDomains) > 0 {
|
||||||
cspDomains += " " + strings.Join(appConfig.Server.CSPDomains, " ")
|
cspDomains += " " + strings.Join(a.cfg.Server.CSPDomains, " ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func securityHeaders(next http.Handler) http.Handler {
|
func (a *goBlog) securityHeaders(next http.Handler) http.Handler {
|
||||||
refreshCSPDomains()
|
a.refreshCSPDomains()
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Strict-Transport-Security", "max-age=31536000;")
|
w.Header().Set("Strict-Transport-Security", "max-age=31536000;")
|
||||||
w.Header().Set("Referrer-Policy", "no-referrer")
|
w.Header().Set("Referrer-Policy", "no-referrer")
|
||||||
|
@ -616,8 +608,8 @@ func securityHeaders(next http.Handler) http.Handler {
|
||||||
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'"+cspDomains)
|
||||||
if appConfig.Server.Tor && torAddress != "" {
|
if a.cfg.Server.Tor && a.torAddress != "" {
|
||||||
w.Header().Set("Onion-Location", fmt.Sprintf("http://%v%v", torAddress, r.RequestURI))
|
w.Header().Set("Onion-Location", fmt.Sprintf("http://%v%v", a.torAddress, r.RequestURI))
|
||||||
}
|
}
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
|
|
16
httpLogs.go
16
httpLogs.go
|
@ -8,15 +8,13 @@ import (
|
||||||
rotatelogs "github.com/lestrrat-go/file-rotatelogs"
|
rotatelogs "github.com/lestrrat-go/file-rotatelogs"
|
||||||
)
|
)
|
||||||
|
|
||||||
var logf *rotatelogs.RotateLogs
|
func (a *goBlog) initHTTPLog() (err error) {
|
||||||
|
if !a.cfg.Server.Logging {
|
||||||
func initHTTPLog() (err error) {
|
|
||||||
if !appConfig.Server.Logging {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
logf, err = rotatelogs.New(
|
a.logf, err = rotatelogs.New(
|
||||||
appConfig.Server.LogFile+".%Y%m%d",
|
a.cfg.Server.LogFile+".%Y%m%d",
|
||||||
rotatelogs.WithLinkName(appConfig.Server.LogFile),
|
rotatelogs.WithLinkName(a.cfg.Server.LogFile),
|
||||||
rotatelogs.WithClock(rotatelogs.UTC),
|
rotatelogs.WithClock(rotatelogs.UTC),
|
||||||
rotatelogs.WithMaxAge(30*24*time.Hour),
|
rotatelogs.WithMaxAge(30*24*time.Hour),
|
||||||
rotatelogs.WithRotationTime(24*time.Hour),
|
rotatelogs.WithRotationTime(24*time.Hour),
|
||||||
|
@ -24,8 +22,8 @@ func initHTTPLog() (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func logMiddleware(next http.Handler) http.Handler {
|
func (a *goBlog) logMiddleware(next http.Handler) http.Handler {
|
||||||
h := handlers.CombinedLoggingHandler(logf, next)
|
h := handlers.CombinedLoggingHandler(a.logf, next)
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// Remove remote address for privacy
|
// Remove remote address for privacy
|
||||||
r.RemoteAddr = ""
|
r.RemoteAddr = ""
|
||||||
|
|
|
@ -8,15 +8,15 @@ import (
|
||||||
|
|
||||||
const indieAuthScope requestContextKey = "scope"
|
const indieAuthScope requestContextKey = "scope"
|
||||||
|
|
||||||
func checkIndieAuth(next http.Handler) http.Handler {
|
func (a *goBlog) checkIndieAuth(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
bearerToken := r.Header.Get("Authorization")
|
bearerToken := r.Header.Get("Authorization")
|
||||||
if len(bearerToken) == 0 {
|
if len(bearerToken) == 0 {
|
||||||
bearerToken = r.URL.Query().Get("access_token")
|
bearerToken = r.URL.Query().Get("access_token")
|
||||||
}
|
}
|
||||||
tokenData, err := verifyIndieAuthToken(bearerToken)
|
tokenData, err := a.db.verifyIndieAuthToken(bearerToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusUnauthorized)
|
a.serveError(w, r, err.Error(), http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), indieAuthScope, strings.Join(tokenData.Scopes, " "))))
|
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), indieAuthScope, strings.Join(tokenData.Scopes, " "))))
|
||||||
|
|
|
@ -27,10 +27,10 @@ type indieAuthData struct {
|
||||||
time time.Time
|
time time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func indieAuthRequest(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) indieAuthRequest(w http.ResponseWriter, r *http.Request) {
|
||||||
// Authorization request
|
// Authorization request
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusBadRequest)
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data := &indieAuthData{
|
data := &indieAuthData{
|
||||||
|
@ -39,21 +39,21 @@ func indieAuthRequest(w http.ResponseWriter, r *http.Request) {
|
||||||
State: r.Form.Get("state"),
|
State: r.Form.Get("state"),
|
||||||
}
|
}
|
||||||
if rt := r.Form.Get("response_type"); rt != "code" && rt != "id" && rt != "" {
|
if rt := r.Form.Get("response_type"); rt != "code" && rt != "id" && rt != "" {
|
||||||
serveError(w, r, "response_type must be code", http.StatusBadRequest)
|
a.serveError(w, r, "response_type must be code", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if scope := r.Form.Get("scope"); scope != "" {
|
if scope := r.Form.Get("scope"); scope != "" {
|
||||||
data.Scopes = strings.Split(scope, " ")
|
data.Scopes = strings.Split(scope, " ")
|
||||||
}
|
}
|
||||||
if !isValidProfileURL(data.ClientID) || !isValidProfileURL(data.RedirectURI) {
|
if !isValidProfileURL(data.ClientID) || !isValidProfileURL(data.RedirectURI) {
|
||||||
serveError(w, r, "client_id and redirect_uri need to by valid URLs", http.StatusBadRequest)
|
a.serveError(w, r, "client_id and redirect_uri need to by valid URLs", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if data.State == "" {
|
if data.State == "" {
|
||||||
serveError(w, r, "state must not be empty", http.StatusBadRequest)
|
a.serveError(w, r, "state must not be empty", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
render(w, r, "indieauth", &renderData{
|
a.render(w, r, "indieauth", &renderData{
|
||||||
Data: data,
|
Data: data,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -79,10 +79,10 @@ func isValidProfileURL(profileURL string) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func indieAuthAccept(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) indieAuthAccept(w http.ResponseWriter, r *http.Request) {
|
||||||
// Authentication flow
|
// Authentication flow
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusBadRequest)
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data := &indieAuthData{
|
data := &indieAuthData{
|
||||||
|
@ -94,13 +94,13 @@ func indieAuthAccept(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
sha := sha1.New()
|
sha := sha1.New()
|
||||||
if _, err := sha.Write([]byte(data.time.String() + data.ClientID)); err != nil {
|
if _, err := sha.Write([]byte(data.time.String() + data.ClientID)); err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data.code = fmt.Sprintf("%x", sha.Sum(nil))
|
data.code = fmt.Sprintf("%x", sha.Sum(nil))
|
||||||
err := data.saveAuthorization()
|
err := a.db.saveAuthorization(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, data.RedirectURI+"?code="+data.code+"&state="+data.State, http.StatusFound)
|
http.Redirect(w, r, data.RedirectURI+"?code="+data.code+"&state="+data.State, http.StatusFound)
|
||||||
|
@ -114,10 +114,10 @@ type tokenResponse struct {
|
||||||
ClientID string `json:"client_id,omitempty"`
|
ClientID string `json:"client_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func indieAuthVerification(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) indieAuthVerification(w http.ResponseWriter, r *http.Request) {
|
||||||
// Authorization verification
|
// Authorization verification
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusBadRequest)
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data := &indieAuthData{
|
data := &indieAuthData{
|
||||||
|
@ -125,33 +125,33 @@ func indieAuthVerification(w http.ResponseWriter, r *http.Request) {
|
||||||
ClientID: r.Form.Get("client_id"),
|
ClientID: r.Form.Get("client_id"),
|
||||||
RedirectURI: r.Form.Get("redirect_uri"),
|
RedirectURI: r.Form.Get("redirect_uri"),
|
||||||
}
|
}
|
||||||
valid, err := data.verifyAuthorization()
|
valid, err := a.db.verifyAuthorization(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !valid {
|
if !valid {
|
||||||
serveError(w, r, "Authentication not valid", http.StatusForbidden)
|
a.serveError(w, r, "Authentication not valid", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
b, _ := json.Marshal(tokenResponse{
|
b, _ := json.Marshal(tokenResponse{
|
||||||
Me: appConfig.Server.PublicAddress,
|
Me: a.cfg.Server.PublicAddress,
|
||||||
})
|
})
|
||||||
w.Header().Set(contentType, contentTypeJSONUTF8)
|
w.Header().Set(contentType, contentTypeJSONUTF8)
|
||||||
_, _ = writeMinified(w, contentTypeJSON, b)
|
_, _ = writeMinified(w, contentTypeJSON, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func indieAuthToken(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) indieAuthToken(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method == http.MethodGet {
|
if r.Method == http.MethodGet {
|
||||||
// Token verification
|
// Token verification
|
||||||
data, err := verifyIndieAuthToken(r.Header.Get("Authorization"))
|
data, err := a.db.verifyIndieAuthToken(r.Header.Get("Authorization"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, "Invalid token or token not found", http.StatusUnauthorized)
|
a.serveError(w, r, "Invalid token or token not found", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
res := &tokenResponse{
|
res := &tokenResponse{
|
||||||
Scope: strings.Join(data.Scopes, " "),
|
Scope: strings.Join(data.Scopes, " "),
|
||||||
Me: appConfig.Server.PublicAddress,
|
Me: a.cfg.Server.PublicAddress,
|
||||||
ClientID: data.ClientID,
|
ClientID: data.ClientID,
|
||||||
}
|
}
|
||||||
b, _ := json.Marshal(res)
|
b, _ := json.Marshal(res)
|
||||||
|
@ -160,12 +160,12 @@ func indieAuthToken(w http.ResponseWriter, r *http.Request) {
|
||||||
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 {
|
||||||
serveError(w, r, err.Error(), http.StatusBadRequest)
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Token Revocation
|
// Token Revocation
|
||||||
if r.Form.Get("action") == "revoke" {
|
if r.Form.Get("action") == "revoke" {
|
||||||
revokeIndieAuthToken(r.Form.Get("token"))
|
a.db.revokeIndieAuthToken(r.Form.Get("token"))
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -176,55 +176,55 @@ func indieAuthToken(w http.ResponseWriter, r *http.Request) {
|
||||||
ClientID: r.Form.Get("client_id"),
|
ClientID: r.Form.Get("client_id"),
|
||||||
RedirectURI: r.Form.Get("redirect_uri"),
|
RedirectURI: r.Form.Get("redirect_uri"),
|
||||||
}
|
}
|
||||||
valid, err := data.verifyAuthorization()
|
valid, err := a.db.verifyAuthorization(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !valid {
|
if !valid {
|
||||||
serveError(w, r, "Authentication not valid", http.StatusForbidden)
|
a.serveError(w, r, "Authentication not valid", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(data.Scopes) < 1 {
|
if len(data.Scopes) < 1 {
|
||||||
serveError(w, r, "No scope", http.StatusBadRequest)
|
a.serveError(w, r, "No scope", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data.time = time.Now()
|
data.time = time.Now()
|
||||||
sha := sha1.New()
|
sha := sha1.New()
|
||||||
if _, err := sha.Write([]byte(data.time.String() + data.ClientID)); err != nil {
|
if _, err := sha.Write([]byte(data.time.String() + data.ClientID)); err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data.token = fmt.Sprintf("%x", sha.Sum(nil))
|
data.token = fmt.Sprintf("%x", sha.Sum(nil))
|
||||||
err = data.saveToken()
|
err = a.db.saveToken(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
res := &tokenResponse{
|
res := &tokenResponse{
|
||||||
TokenType: "Bearer",
|
TokenType: "Bearer",
|
||||||
AccessToken: data.token,
|
AccessToken: data.token,
|
||||||
Scope: strings.Join(data.Scopes, " "),
|
Scope: strings.Join(data.Scopes, " "),
|
||||||
Me: appConfig.Server.PublicAddress,
|
Me: a.cfg.Server.PublicAddress,
|
||||||
}
|
}
|
||||||
b, _ := json.Marshal(res)
|
b, _ := json.Marshal(res)
|
||||||
w.Header().Set(contentType, contentTypeJSONUTF8)
|
w.Header().Set(contentType, contentTypeJSONUTF8)
|
||||||
_, _ = writeMinified(w, contentTypeJSON, b)
|
_, _ = writeMinified(w, contentTypeJSON, b)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
serveError(w, r, "", http.StatusBadRequest)
|
a.serveError(w, r, "", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (data *indieAuthData) saveAuthorization() (err error) {
|
func (db *database) saveAuthorization(data *indieAuthData) (err error) {
|
||||||
_, err = appDb.exec("insert into indieauthauth (time, code, client, redirect, scope) values (?, ?, ?, ?, ?)", data.time.Unix(), data.code, data.ClientID, data.RedirectURI, strings.Join(data.Scopes, " "))
|
_, err = db.exec("insert into indieauthauth (time, code, client, redirect, scope) values (?, ?, ?, ?, ?)", data.time.Unix(), data.code, data.ClientID, data.RedirectURI, strings.Join(data.Scopes, " "))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (data *indieAuthData) verifyAuthorization() (valid bool, err error) {
|
func (db *database) verifyAuthorization(data *indieAuthData) (valid bool, err error) {
|
||||||
// code valid for 600 seconds
|
// code valid for 600 seconds
|
||||||
row, err := appDb.queryRow("select code, client, redirect, scope from indieauthauth where time >= ? and code = ? and client = ? and redirect = ?", time.Now().Unix()-600, data.code, data.ClientID, data.RedirectURI)
|
row, err := db.queryRow("select code, client, redirect, scope from indieauthauth where time >= ? and code = ? and client = ? and redirect = ?", time.Now().Unix()-600, data.code, data.ClientID, data.RedirectURI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
@ -239,22 +239,22 @@ func (data *indieAuthData) verifyAuthorization() (valid bool, err error) {
|
||||||
data.Scopes = strings.Split(scope, " ")
|
data.Scopes = strings.Split(scope, " ")
|
||||||
}
|
}
|
||||||
valid = true
|
valid = true
|
||||||
_, err = appDb.exec("delete from indieauthauth where code = ? or time < ?", data.code, time.Now().Unix()-600)
|
_, err = db.exec("delete from indieauthauth where code = ? or time < ?", data.code, time.Now().Unix()-600)
|
||||||
data.code = ""
|
data.code = ""
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (data *indieAuthData) saveToken() (err error) {
|
func (db *database) saveToken(data *indieAuthData) (err error) {
|
||||||
_, err = appDb.exec("insert into indieauthtoken (time, token, client, scope) values (?, ?, ?, ?)", data.time.Unix(), data.token, data.ClientID, strings.Join(data.Scopes, " "))
|
_, err = db.exec("insert into indieauthtoken (time, token, client, scope) values (?, ?, ?, ?)", data.time.Unix(), data.token, data.ClientID, strings.Join(data.Scopes, " "))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyIndieAuthToken(token string) (data *indieAuthData, err error) {
|
func (db *database) verifyIndieAuthToken(token string) (data *indieAuthData, err error) {
|
||||||
token = strings.ReplaceAll(token, "Bearer ", "")
|
token = strings.ReplaceAll(token, "Bearer ", "")
|
||||||
data = &indieAuthData{
|
data = &indieAuthData{
|
||||||
Scopes: []string{},
|
Scopes: []string{},
|
||||||
}
|
}
|
||||||
row, err := appDb.queryRow("select time, token, client, scope from indieauthtoken where token = @token", sql.Named("token", token))
|
row, err := db.queryRow("select time, token, client, scope from indieauthtoken where token = @token", sql.Named("token", token))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -273,8 +273,8 @@ func verifyIndieAuthToken(token string) (data *indieAuthData, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func revokeIndieAuthToken(token string) {
|
func (db *database) revokeIndieAuthToken(token string) {
|
||||||
if token != "" {
|
if token != "" {
|
||||||
_, _ = appDb.exec("delete from indieauthtoken where token=?", token)
|
_, _ = db.exec("delete from indieauthtoken where token=?", token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
45
main.go
45
main.go
|
@ -47,9 +47,11 @@ func main() {
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app := &goBlog{}
|
||||||
|
|
||||||
// Initialize config
|
// Initialize config
|
||||||
log.Println("Initialize configuration...")
|
log.Println("Initialize configuration...")
|
||||||
if err = initConfig(); err != nil {
|
if err = app.initConfig(); err != nil {
|
||||||
logErrAndQuit("Failed to init config:", err.Error())
|
logErrAndQuit("Failed to init config:", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -57,7 +59,7 @@ func main() {
|
||||||
// Healthcheck tool
|
// Healthcheck tool
|
||||||
if len(os.Args) >= 2 && os.Args[1] == "healthcheck" {
|
if len(os.Args) >= 2 && os.Args[1] == "healthcheck" {
|
||||||
// Connect to public address + "/ping" and exit with 0 when successful
|
// Connect to public address + "/ping" and exit with 0 when successful
|
||||||
health := healthcheckExitCode()
|
health := app.healthcheckExitCode()
|
||||||
shutdown()
|
shutdown()
|
||||||
os.Exit(health)
|
os.Exit(health)
|
||||||
return
|
return
|
||||||
|
@ -66,8 +68,8 @@ func main() {
|
||||||
// Tool to generate TOTP secret
|
// Tool to generate TOTP secret
|
||||||
if len(os.Args) >= 2 && os.Args[1] == "totp-secret" {
|
if len(os.Args) >= 2 && os.Args[1] == "totp-secret" {
|
||||||
key, err := totp.Generate(totp.GenerateOpts{
|
key, err := totp.Generate(totp.GenerateOpts{
|
||||||
Issuer: appConfig.Server.PublicAddress,
|
Issuer: app.cfg.Server.PublicAddress,
|
||||||
AccountName: appConfig.User.Nick,
|
AccountName: app.cfg.User.Nick,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logErrAndQuit(err.Error())
|
logErrAndQuit(err.Error())
|
||||||
|
@ -82,65 +84,64 @@ func main() {
|
||||||
initGC()
|
initGC()
|
||||||
|
|
||||||
// Execute pre-start hooks
|
// Execute pre-start hooks
|
||||||
preStartHooks()
|
app.preStartHooks()
|
||||||
|
|
||||||
// Initialize database and markdown
|
// Initialize database and markdown
|
||||||
log.Println("Initialize database...")
|
log.Println("Initialize database...")
|
||||||
if err = initDatabase(); err != nil {
|
if err = app.initDatabase(); err != nil {
|
||||||
logErrAndQuit("Failed to init database:", err.Error())
|
logErrAndQuit("Failed to init database:", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Println("Initialize server components...")
|
log.Println("Initialize server components...")
|
||||||
initMarkdown()
|
app.initMarkdown()
|
||||||
|
|
||||||
// Link check tool after init of markdown
|
// Link check tool after init of markdown
|
||||||
if len(os.Args) >= 2 && os.Args[1] == "check" {
|
if len(os.Args) >= 2 && os.Args[1] == "check" {
|
||||||
checkAllExternalLinks()
|
app.checkAllExternalLinks()
|
||||||
shutdown()
|
shutdown()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// More initializations
|
// More initializations
|
||||||
initMinify()
|
if err = app.initTemplateAssets(); err != nil { // Needs minify
|
||||||
if err = initTemplateAssets(); err != nil { // Needs minify
|
|
||||||
logErrAndQuit("Failed to init template assets:", err.Error())
|
logErrAndQuit("Failed to init template assets:", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = initTemplateStrings(); err != nil {
|
if err = app.initTemplateStrings(); err != nil {
|
||||||
logErrAndQuit("Failed to init template translations:", err.Error())
|
logErrAndQuit("Failed to init template translations:", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = initRendering(); err != nil { // Needs assets and minify
|
if err = app.initRendering(); err != nil { // Needs assets and minify
|
||||||
logErrAndQuit("Failed to init HTML rendering:", err.Error())
|
logErrAndQuit("Failed to init HTML rendering:", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = initCache(); err != nil {
|
if err = app.initCache(); err != nil {
|
||||||
logErrAndQuit("Failed to init HTTP cache:", err.Error())
|
logErrAndQuit("Failed to init HTTP cache:", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = initRegexRedirects(); err != nil {
|
if err = app.initRegexRedirects(); err != nil {
|
||||||
logErrAndQuit("Failed to init redirects:", err.Error())
|
logErrAndQuit("Failed to init redirects:", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = initHTTPLog(); err != nil {
|
if err = app.initHTTPLog(); err != nil {
|
||||||
logErrAndQuit("Failed to init HTTP logging:", err.Error())
|
logErrAndQuit("Failed to init HTTP logging:", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = initActivityPub(); err != nil {
|
if err = app.initActivityPub(); err != nil {
|
||||||
logErrAndQuit("Failed to init ActivityPub:", err.Error())
|
logErrAndQuit("Failed to init ActivityPub:", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
initWebmention()
|
app.initWebmention()
|
||||||
initTelegram()
|
app.initTelegram()
|
||||||
initBlogStats()
|
app.initBlogStats()
|
||||||
initSessions()
|
app.initSessions()
|
||||||
|
|
||||||
// Start cron hooks
|
// Start cron hooks
|
||||||
startHourlyHooks()
|
app.startHourlyHooks()
|
||||||
|
|
||||||
// Start the server
|
// Start the server
|
||||||
log.Println("Starting server(s)...")
|
log.Println("Starting server(s)...")
|
||||||
err = startServer()
|
err = app.startServer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logErrAndQuit("Failed to start server(s):", err.Error())
|
logErrAndQuit("Failed to start server(s):", err.Error())
|
||||||
return
|
return
|
||||||
|
|
35
markdown.go
35
markdown.go
|
@ -15,9 +15,7 @@ import (
|
||||||
"github.com/yuin/goldmark/util"
|
"github.com/yuin/goldmark/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var defaultMarkdown, absoluteMarkdown goldmark.Markdown
|
func (a *goBlog) initMarkdown() {
|
||||||
|
|
||||||
func initMarkdown() {
|
|
||||||
defaultGoldmarkOptions := []goldmark.Option{
|
defaultGoldmarkOptions := []goldmark.Option{
|
||||||
goldmark.WithRendererOptions(
|
goldmark.WithRendererOptions(
|
||||||
html.WithUnsafe(),
|
html.WithUnsafe(),
|
||||||
|
@ -35,22 +33,28 @@ func initMarkdown() {
|
||||||
emoji.Emoji,
|
emoji.Emoji,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
defaultMarkdown = goldmark.New(append(defaultGoldmarkOptions, goldmark.WithExtensions(&customExtension{absoluteLinks: false}))...)
|
a.md = goldmark.New(append(defaultGoldmarkOptions, goldmark.WithExtensions(&customExtension{
|
||||||
absoluteMarkdown = goldmark.New(append(defaultGoldmarkOptions, goldmark.WithExtensions(&customExtension{absoluteLinks: true}))...)
|
absoluteLinks: false,
|
||||||
|
publicAddress: a.cfg.Server.PublicAddress,
|
||||||
|
}))...)
|
||||||
|
a.absoluteMd = goldmark.New(append(defaultGoldmarkOptions, goldmark.WithExtensions(&customExtension{
|
||||||
|
absoluteLinks: true,
|
||||||
|
publicAddress: a.cfg.Server.PublicAddress,
|
||||||
|
}))...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderMarkdown(source string, absoluteLinks bool) (rendered []byte, err error) {
|
func (a *goBlog) renderMarkdown(source string, absoluteLinks bool) (rendered []byte, err error) {
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
if absoluteLinks {
|
if absoluteLinks {
|
||||||
err = absoluteMarkdown.Convert([]byte(source), &buffer)
|
err = a.absoluteMd.Convert([]byte(source), &buffer)
|
||||||
} else {
|
} else {
|
||||||
err = defaultMarkdown.Convert([]byte(source), &buffer)
|
err = a.md.Convert([]byte(source), &buffer)
|
||||||
}
|
}
|
||||||
return buffer.Bytes(), err
|
return buffer.Bytes(), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderText(s string) string {
|
func (a *goBlog) renderText(s string) string {
|
||||||
h, err := renderMarkdown(s, false)
|
h, err := a.renderMarkdown(s, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -66,18 +70,21 @@ func renderText(s string) string {
|
||||||
// Links
|
// Links
|
||||||
type customExtension struct {
|
type customExtension struct {
|
||||||
absoluteLinks bool
|
absoluteLinks bool
|
||||||
|
publicAddress string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *customExtension) Extend(m goldmark.Markdown) {
|
func (l *customExtension) Extend(m goldmark.Markdown) {
|
||||||
m.Renderer().AddOptions(renderer.WithNodeRenderers(
|
m.Renderer().AddOptions(renderer.WithNodeRenderers(
|
||||||
util.Prioritized(&customRenderer{
|
util.Prioritized(&customRenderer{
|
||||||
absoluteLinks: l.absoluteLinks,
|
absoluteLinks: l.absoluteLinks,
|
||||||
|
publicAddress: l.publicAddress,
|
||||||
}, 500),
|
}, 500),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
type customRenderer struct {
|
type customRenderer struct {
|
||||||
absoluteLinks bool
|
absoluteLinks bool
|
||||||
|
publicAddress string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *customRenderer) RegisterFuncs(r renderer.NodeRendererFuncRegisterer) {
|
func (c *customRenderer) RegisterFuncs(r renderer.NodeRendererFuncRegisterer) {
|
||||||
|
@ -91,8 +98,8 @@ func (c *customRenderer) renderLink(w util.BufWriter, _ []byte, node ast.Node, e
|
||||||
_, _ = w.WriteString("<a href=\"")
|
_, _ = w.WriteString("<a href=\"")
|
||||||
// Make URL absolute if it's relative
|
// Make URL absolute if it's relative
|
||||||
newDestination := util.URLEscape(n.Destination, true)
|
newDestination := util.URLEscape(n.Destination, true)
|
||||||
if c.absoluteLinks && bytes.HasPrefix(newDestination, []byte("/")) {
|
if c.absoluteLinks && c.publicAddress != "" && bytes.HasPrefix(newDestination, []byte("/")) {
|
||||||
_, _ = w.Write(util.EscapeHTML([]byte(appConfig.Server.PublicAddress)))
|
_, _ = w.Write(util.EscapeHTML([]byte(c.publicAddress)))
|
||||||
}
|
}
|
||||||
_, _ = w.Write(util.EscapeHTML(newDestination))
|
_, _ = w.Write(util.EscapeHTML(newDestination))
|
||||||
_, _ = w.WriteRune('"')
|
_, _ = w.WriteRune('"')
|
||||||
|
@ -120,8 +127,8 @@ func (c *customRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
|
||||||
n := node.(*ast.Image)
|
n := node.(*ast.Image)
|
||||||
// Make URL absolute if it's relative
|
// Make URL absolute if it's relative
|
||||||
destination := util.URLEscape(n.Destination, true)
|
destination := util.URLEscape(n.Destination, true)
|
||||||
if bytes.HasPrefix(destination, []byte("/")) {
|
if c.publicAddress != "" && bytes.HasPrefix(destination, []byte("/")) {
|
||||||
destination = util.EscapeHTML(append([]byte(appConfig.Server.PublicAddress), destination...))
|
destination = util.EscapeHTML(append([]byte(c.publicAddress), destination...))
|
||||||
} else {
|
} else {
|
||||||
destination = util.EscapeHTML(destination)
|
destination = util.EscapeHTML(destination)
|
||||||
}
|
}
|
||||||
|
|
4
media.go
4
media.go
|
@ -27,11 +27,11 @@ func saveMediaFile(filename string, mediaFile io.Reader) (string, error) {
|
||||||
return "/m/" + filename, nil
|
return "/m/" + filename, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveMediaFile(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveMediaFile(w http.ResponseWriter, r *http.Request) {
|
||||||
f := filepath.Join(mediaFilePath, chi.URLParam(r, "file"))
|
f := filepath.Join(mediaFilePath, chi.URLParam(r, "file"))
|
||||||
_, err := os.Stat(f)
|
_, err := os.Stat(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serve404(w, r)
|
a.serve404(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Add("Cache-Control", "public,max-age=31536000,immutable")
|
w.Header().Add("Cache-Control", "public,max-age=31536000,immutable")
|
||||||
|
|
|
@ -13,7 +13,7 @@ import (
|
||||||
const defaultCompressionWidth = 2000
|
const defaultCompressionWidth = 2000
|
||||||
const defaultCompressionHeight = 3000
|
const defaultCompressionHeight = 3000
|
||||||
|
|
||||||
func tinify(url string, config *configMicropubMedia) (location string, err error) {
|
func (a *goBlog) tinify(url string, config *configMicropubMedia) (location string, err error) {
|
||||||
// Check config
|
// Check config
|
||||||
if config == nil || config.TinifyKey == "" {
|
if config == nil || config.TinifyKey == "" {
|
||||||
return "", errors.New("service Tinify not configured")
|
return "", errors.New("service Tinify not configured")
|
||||||
|
@ -89,11 +89,11 @@ func tinify(url string, config *configMicropubMedia) (location string, err error
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
// Upload compressed file
|
// Upload compressed file
|
||||||
location, err = uploadFile(fileName+"."+fileExtension, tmpFile)
|
location, err = a.uploadFile(fileName+"."+fileExtension, tmpFile)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func shortPixel(url string, config *configMicropubMedia) (location string, err error) {
|
func (a *goBlog) shortPixel(url string, config *configMicropubMedia) (location string, err error) {
|
||||||
// Check config
|
// Check config
|
||||||
if config == nil || config.ShortPixelKey == "" {
|
if config == nil || config.ShortPixelKey == "" {
|
||||||
return "", errors.New("service ShortPixel not configured")
|
return "", errors.New("service ShortPixel not configured")
|
||||||
|
@ -146,11 +146,11 @@ func shortPixel(url string, config *configMicropubMedia) (location string, err e
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
// Upload compressed file
|
// Upload compressed file
|
||||||
location, err = uploadFile(fileName+"."+fileExtension, tmpFile)
|
location, err = a.uploadFile(fileName+"."+fileExtension, tmpFile)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func cloudflare(url string) (location string, err error) {
|
func (a *goBlog) cloudflare(url string) (location string, err error) {
|
||||||
// Check url
|
// Check url
|
||||||
_, allowed := compressionIsSupported(url, "jpg", "jpeg", "png")
|
_, allowed := compressionIsSupported(url, "jpg", "jpeg", "png")
|
||||||
if !allowed {
|
if !allowed {
|
||||||
|
@ -190,6 +190,6 @@ func cloudflare(url string) (location string, err error) {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
// Upload compressed file
|
// Upload compressed file
|
||||||
location, err = uploadFile(fileName+"."+fileExtension, tmpFile)
|
location, err = a.uploadFile(fileName+"."+fileExtension, tmpFile)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
208
micropub.go
208
micropub.go
|
@ -22,13 +22,13 @@ type micropubConfig struct {
|
||||||
MediaEndpoint string `json:"media-endpoint,omitempty"`
|
MediaEndpoint string `json:"media-endpoint,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func 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, contentTypeJSONUTF8)
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
b, _ := json.Marshal(µpubConfig{
|
b, _ := json.Marshal(µpubConfig{
|
||||||
MediaEndpoint: appConfig.Server.PublicAddress + micropubPath + micropubMediaSubPath,
|
MediaEndpoint: a.cfg.Server.PublicAddress + micropubPath + micropubMediaSubPath,
|
||||||
})
|
})
|
||||||
_, _ = writeMinified(w, contentTypeJSON, b)
|
_, _ = writeMinified(w, contentTypeJSON, b)
|
||||||
case "source":
|
case "source":
|
||||||
|
@ -36,29 +36,29 @@ func serveMicropubQuery(w http.ResponseWriter, r *http.Request) {
|
||||||
if urlString := r.URL.Query().Get("url"); urlString != "" {
|
if urlString := r.URL.Query().Get("url"); urlString != "" {
|
||||||
u, err := url.Parse(r.URL.Query().Get("url"))
|
u, err := url.Parse(r.URL.Query().Get("url"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusBadRequest)
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
p, err := getPost(u.Path)
|
p, err := a.db.getPost(u.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusBadRequest)
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
mf = p.toMfItem()
|
mf = a.toMfItem(p)
|
||||||
} else {
|
} else {
|
||||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||||
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
|
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
|
||||||
posts, err := getPosts(&postsRequestConfig{
|
posts, err := a.db.getPosts(&postsRequestConfig{
|
||||||
limit: limit,
|
limit: limit,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
list := map[string][]*microformatItem{}
|
list := map[string][]*microformatItem{}
|
||||||
for _, p := range posts {
|
for _, p := range posts {
|
||||||
list["items"] = append(list["items"], p.toMfItem())
|
list["items"] = append(list["items"], a.toMfItem(p))
|
||||||
}
|
}
|
||||||
mf = list
|
mf = list
|
||||||
}
|
}
|
||||||
|
@ -68,10 +68,10 @@ func serveMicropubQuery(w http.ResponseWriter, r *http.Request) {
|
||||||
_, _ = writeMinified(w, contentTypeJSON, b)
|
_, _ = writeMinified(w, contentTypeJSON, b)
|
||||||
case "category":
|
case "category":
|
||||||
allCategories := []string{}
|
allCategories := []string{}
|
||||||
for blog := range appConfig.Blogs {
|
for blog := range a.cfg.Blogs {
|
||||||
values, err := allTaxonomyValues(blog, appConfig.Micropub.CategoryParam)
|
values, err := a.db.allTaxonomyValues(blog, a.cfg.Micropub.CategoryParam)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
allCategories = append(allCategories, values...)
|
allCategories = append(allCategories, values...)
|
||||||
|
@ -83,11 +83,11 @@ func serveMicropubQuery(w http.ResponseWriter, r *http.Request) {
|
||||||
})
|
})
|
||||||
_, _ = writeMinified(w, contentTypeJSON, b)
|
_, _ = writeMinified(w, contentTypeJSON, b)
|
||||||
default:
|
default:
|
||||||
serve404(w, r)
|
a.serve404(w, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *post) toMfItem() *microformatItem {
|
func (a *goBlog) toMfItem(p *post) *microformatItem {
|
||||||
params := p.Parameters
|
params := p.Parameters
|
||||||
params["path"] = []string{p.Path}
|
params["path"] = []string{p.Path}
|
||||||
params["section"] = []string{p.Section}
|
params["section"] = []string{p.Section}
|
||||||
|
@ -104,20 +104,20 @@ func (p *post) toMfItem() *microformatItem {
|
||||||
Published: []string{p.Published},
|
Published: []string{p.Published},
|
||||||
Updated: []string{p.Updated},
|
Updated: []string{p.Updated},
|
||||||
PostStatus: []string{string(p.Status)},
|
PostStatus: []string{string(p.Status)},
|
||||||
Category: p.Parameters[appConfig.Micropub.CategoryParam],
|
Category: p.Parameters[a.cfg.Micropub.CategoryParam],
|
||||||
Content: []string{content},
|
Content: []string{content},
|
||||||
URL: []string{p.fullURL()},
|
URL: []string{a.fullPostURL(p)},
|
||||||
InReplyTo: p.Parameters[appConfig.Micropub.ReplyParam],
|
InReplyTo: p.Parameters[a.cfg.Micropub.ReplyParam],
|
||||||
LikeOf: p.Parameters[appConfig.Micropub.LikeParam],
|
LikeOf: p.Parameters[a.cfg.Micropub.LikeParam],
|
||||||
BookmarkOf: p.Parameters[appConfig.Micropub.BookmarkParam],
|
BookmarkOf: p.Parameters[a.cfg.Micropub.BookmarkParam],
|
||||||
MpSlug: []string{p.Slug},
|
MpSlug: []string{p.Slug},
|
||||||
Audio: p.Parameters[appConfig.Micropub.AudioParam],
|
Audio: p.Parameters[a.cfg.Micropub.AudioParam],
|
||||||
// TODO: Photos
|
// TODO: Photos
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func 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, contentTypeWWWForm) || strings.Contains(ct, contentTypeMultipartForm) {
|
||||||
|
@ -128,73 +128,73 @@ func serveMicropubPost(w http.ResponseWriter, r *http.Request) {
|
||||||
err = r.ParseForm()
|
err = r.ParseForm()
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusBadRequest)
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if action := micropubAction(r.Form.Get("action")); action != "" {
|
if action := micropubAction(r.Form.Get("action")); action != "" {
|
||||||
u, err := url.Parse(r.Form.Get("url"))
|
u, err := url.Parse(r.Form.Get("url"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusBadRequest)
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if action == actionDelete {
|
if action == actionDelete {
|
||||||
micropubDelete(w, r, u)
|
a.micropubDelete(w, r, u)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
serveError(w, r, "Action not supported", http.StatusNotImplemented)
|
a.serveError(w, r, "Action not supported", http.StatusNotImplemented)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
p, err = convertMPValueMapToPost(r.Form)
|
p, err = a.convertMPValueMapToPost(r.Form)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
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, contentTypeJSON) {
|
||||||
parsedMfItem := µformatItem{}
|
parsedMfItem := µformatItem{}
|
||||||
err := json.NewDecoder(r.Body).Decode(parsedMfItem)
|
err := json.NewDecoder(r.Body).Decode(parsedMfItem)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if parsedMfItem.Action != "" {
|
if parsedMfItem.Action != "" {
|
||||||
u, err := url.Parse(parsedMfItem.URL)
|
u, err := url.Parse(parsedMfItem.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusBadRequest)
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if parsedMfItem.Action == actionDelete {
|
if parsedMfItem.Action == actionDelete {
|
||||||
micropubDelete(w, r, u)
|
a.micropubDelete(w, r, u)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if parsedMfItem.Action == actionUpdate {
|
if parsedMfItem.Action == actionUpdate {
|
||||||
micropubUpdate(w, r, u, parsedMfItem)
|
a.micropubUpdate(w, r, u, parsedMfItem)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
serveError(w, r, "Action not supported", http.StatusNotImplemented)
|
a.serveError(w, r, "Action not supported", http.StatusNotImplemented)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
p, err = convertMPMfToPost(parsedMfItem)
|
p, err = a.convertMPMfToPost(parsedMfItem)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
serveError(w, r, "wrong content type", http.StatusBadRequest)
|
a.serveError(w, r, "wrong content type", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !strings.Contains(r.Context().Value(indieAuthScope).(string), "create") {
|
if !strings.Contains(r.Context().Value(indieAuthScope).(string), "create") {
|
||||||
serveError(w, r, "create scope missing", http.StatusForbidden)
|
a.serveError(w, r, "create scope missing", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := p.create()
|
err := a.createPost(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, p.fullURL(), http.StatusAccepted)
|
http.Redirect(w, r, a.fullPostURL(p), http.StatusAccepted)
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertMPValueMapToPost(values map[string][]string) (*post, error) {
|
func (a *goBlog) convertMPValueMapToPost(values map[string][]string) (*post, error) {
|
||||||
if h, ok := values["h"]; ok && (len(h) != 1 || h[0] != "entry") {
|
if h, ok := values["h"]; ok && (len(h) != 1 || h[0] != "entry") {
|
||||||
return nil, errors.New("only entry type is supported so far")
|
return nil, errors.New("only entry type is supported so far")
|
||||||
}
|
}
|
||||||
|
@ -228,53 +228,53 @@ func convertMPValueMapToPost(values map[string][]string) (*post, error) {
|
||||||
delete(values, "name")
|
delete(values, "name")
|
||||||
}
|
}
|
||||||
if category, ok := values["category"]; ok {
|
if category, ok := values["category"]; ok {
|
||||||
entry.Parameters[appConfig.Micropub.CategoryParam] = category
|
entry.Parameters[a.cfg.Micropub.CategoryParam] = category
|
||||||
delete(values, "category")
|
delete(values, "category")
|
||||||
} else if categories, ok := values["category[]"]; ok {
|
} else if categories, ok := values["category[]"]; ok {
|
||||||
entry.Parameters[appConfig.Micropub.CategoryParam] = categories
|
entry.Parameters[a.cfg.Micropub.CategoryParam] = categories
|
||||||
delete(values, "category[]")
|
delete(values, "category[]")
|
||||||
}
|
}
|
||||||
if inReplyTo, ok := values["in-reply-to"]; ok {
|
if inReplyTo, ok := values["in-reply-to"]; ok {
|
||||||
entry.Parameters[appConfig.Micropub.ReplyParam] = inReplyTo
|
entry.Parameters[a.cfg.Micropub.ReplyParam] = inReplyTo
|
||||||
delete(values, "in-reply-to")
|
delete(values, "in-reply-to")
|
||||||
}
|
}
|
||||||
if likeOf, ok := values["like-of"]; ok {
|
if likeOf, ok := values["like-of"]; ok {
|
||||||
entry.Parameters[appConfig.Micropub.LikeParam] = likeOf
|
entry.Parameters[a.cfg.Micropub.LikeParam] = likeOf
|
||||||
delete(values, "like-of")
|
delete(values, "like-of")
|
||||||
}
|
}
|
||||||
if bookmarkOf, ok := values["bookmark-of"]; ok {
|
if bookmarkOf, ok := values["bookmark-of"]; ok {
|
||||||
entry.Parameters[appConfig.Micropub.BookmarkParam] = bookmarkOf
|
entry.Parameters[a.cfg.Micropub.BookmarkParam] = bookmarkOf
|
||||||
delete(values, "bookmark-of")
|
delete(values, "bookmark-of")
|
||||||
}
|
}
|
||||||
if audio, ok := values["audio"]; ok {
|
if audio, ok := values["audio"]; ok {
|
||||||
entry.Parameters[appConfig.Micropub.AudioParam] = audio
|
entry.Parameters[a.cfg.Micropub.AudioParam] = audio
|
||||||
delete(values, "audio")
|
delete(values, "audio")
|
||||||
} else if audio, ok := values["audio[]"]; ok {
|
} else if audio, ok := values["audio[]"]; ok {
|
||||||
entry.Parameters[appConfig.Micropub.AudioParam] = audio
|
entry.Parameters[a.cfg.Micropub.AudioParam] = audio
|
||||||
delete(values, "audio[]")
|
delete(values, "audio[]")
|
||||||
}
|
}
|
||||||
if photo, ok := values["photo"]; ok {
|
if photo, ok := values["photo"]; ok {
|
||||||
entry.Parameters[appConfig.Micropub.PhotoParam] = photo
|
entry.Parameters[a.cfg.Micropub.PhotoParam] = photo
|
||||||
delete(values, "photo")
|
delete(values, "photo")
|
||||||
} else if photos, ok := values["photo[]"]; ok {
|
} else if photos, ok := values["photo[]"]; ok {
|
||||||
entry.Parameters[appConfig.Micropub.PhotoParam] = photos
|
entry.Parameters[a.cfg.Micropub.PhotoParam] = photos
|
||||||
delete(values, "photo[]")
|
delete(values, "photo[]")
|
||||||
}
|
}
|
||||||
if photoAlt, ok := values["mp-photo-alt"]; ok {
|
if photoAlt, ok := values["mp-photo-alt"]; ok {
|
||||||
entry.Parameters[appConfig.Micropub.PhotoDescriptionParam] = photoAlt
|
entry.Parameters[a.cfg.Micropub.PhotoDescriptionParam] = photoAlt
|
||||||
delete(values, "mp-photo-alt")
|
delete(values, "mp-photo-alt")
|
||||||
} else if photoAlts, ok := values["mp-photo-alt[]"]; ok {
|
} else if photoAlts, ok := values["mp-photo-alt[]"]; ok {
|
||||||
entry.Parameters[appConfig.Micropub.PhotoDescriptionParam] = photoAlts
|
entry.Parameters[a.cfg.Micropub.PhotoDescriptionParam] = photoAlts
|
||||||
delete(values, "mp-photo-alt[]")
|
delete(values, "mp-photo-alt[]")
|
||||||
}
|
}
|
||||||
if location, ok := values["location"]; ok {
|
if location, ok := values["location"]; ok {
|
||||||
entry.Parameters[appConfig.Micropub.LocationParam] = location
|
entry.Parameters[a.cfg.Micropub.LocationParam] = location
|
||||||
delete(values, "location")
|
delete(values, "location")
|
||||||
}
|
}
|
||||||
for n, p := range values {
|
for n, p := range values {
|
||||||
entry.Parameters[n] = append(entry.Parameters[n], p...)
|
entry.Parameters[n] = append(entry.Parameters[n], p...)
|
||||||
}
|
}
|
||||||
err := entry.computeExtraPostParameters()
|
err := a.computeExtraPostParameters(entry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -314,7 +314,7 @@ type microformatProperties struct {
|
||||||
Audio []string `json:"audio,omitempty"`
|
Audio []string `json:"audio,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertMPMfToPost(mf *microformatItem) (*post, error) {
|
func (a *goBlog) convertMPMfToPost(mf *microformatItem) (*post, error) {
|
||||||
if len(mf.Type) != 1 || mf.Type[0] != "h-entry" {
|
if len(mf.Type) != 1 || mf.Type[0] != "h-entry" {
|
||||||
return nil, errors.New("only entry type is supported so far")
|
return nil, errors.New("only entry type is supported so far")
|
||||||
}
|
}
|
||||||
|
@ -339,35 +339,35 @@ func convertMPMfToPost(mf *microformatItem) (*post, error) {
|
||||||
entry.Parameters["title"] = mf.Properties.Name
|
entry.Parameters["title"] = mf.Properties.Name
|
||||||
}
|
}
|
||||||
if len(mf.Properties.Category) > 0 {
|
if len(mf.Properties.Category) > 0 {
|
||||||
entry.Parameters[appConfig.Micropub.CategoryParam] = mf.Properties.Category
|
entry.Parameters[a.cfg.Micropub.CategoryParam] = mf.Properties.Category
|
||||||
}
|
}
|
||||||
if len(mf.Properties.InReplyTo) == 1 {
|
if len(mf.Properties.InReplyTo) == 1 {
|
||||||
entry.Parameters[appConfig.Micropub.ReplyParam] = mf.Properties.InReplyTo
|
entry.Parameters[a.cfg.Micropub.ReplyParam] = mf.Properties.InReplyTo
|
||||||
}
|
}
|
||||||
if len(mf.Properties.LikeOf) == 1 {
|
if len(mf.Properties.LikeOf) == 1 {
|
||||||
entry.Parameters[appConfig.Micropub.LikeParam] = mf.Properties.LikeOf
|
entry.Parameters[a.cfg.Micropub.LikeParam] = mf.Properties.LikeOf
|
||||||
}
|
}
|
||||||
if len(mf.Properties.BookmarkOf) == 1 {
|
if len(mf.Properties.BookmarkOf) == 1 {
|
||||||
entry.Parameters[appConfig.Micropub.BookmarkParam] = mf.Properties.BookmarkOf
|
entry.Parameters[a.cfg.Micropub.BookmarkParam] = mf.Properties.BookmarkOf
|
||||||
}
|
}
|
||||||
if len(mf.Properties.Audio) > 0 {
|
if len(mf.Properties.Audio) > 0 {
|
||||||
entry.Parameters[appConfig.Micropub.AudioParam] = mf.Properties.Audio
|
entry.Parameters[a.cfg.Micropub.AudioParam] = mf.Properties.Audio
|
||||||
}
|
}
|
||||||
if len(mf.Properties.Photo) > 0 {
|
if len(mf.Properties.Photo) > 0 {
|
||||||
for _, photo := range mf.Properties.Photo {
|
for _, photo := range mf.Properties.Photo {
|
||||||
if theString, justString := photo.(string); justString {
|
if theString, justString := photo.(string); justString {
|
||||||
entry.Parameters[appConfig.Micropub.PhotoParam] = append(entry.Parameters[appConfig.Micropub.PhotoParam], theString)
|
entry.Parameters[a.cfg.Micropub.PhotoParam] = append(entry.Parameters[a.cfg.Micropub.PhotoParam], theString)
|
||||||
entry.Parameters[appConfig.Micropub.PhotoDescriptionParam] = append(entry.Parameters[appConfig.Micropub.PhotoDescriptionParam], "")
|
entry.Parameters[a.cfg.Micropub.PhotoDescriptionParam] = append(entry.Parameters[a.cfg.Micropub.PhotoDescriptionParam], "")
|
||||||
} else if thePhoto, isPhoto := photo.(map[string]interface{}); isPhoto {
|
} else if thePhoto, isPhoto := photo.(map[string]interface{}); isPhoto {
|
||||||
entry.Parameters[appConfig.Micropub.PhotoParam] = append(entry.Parameters[appConfig.Micropub.PhotoParam], cast.ToString(thePhoto["value"]))
|
entry.Parameters[a.cfg.Micropub.PhotoParam] = append(entry.Parameters[a.cfg.Micropub.PhotoParam], cast.ToString(thePhoto["value"]))
|
||||||
entry.Parameters[appConfig.Micropub.PhotoDescriptionParam] = append(entry.Parameters[appConfig.Micropub.PhotoDescriptionParam], cast.ToString(thePhoto["alt"]))
|
entry.Parameters[a.cfg.Micropub.PhotoDescriptionParam] = append(entry.Parameters[a.cfg.Micropub.PhotoDescriptionParam], cast.ToString(thePhoto["alt"]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(mf.Properties.MpSlug) == 1 {
|
if len(mf.Properties.MpSlug) == 1 {
|
||||||
entry.Slug = mf.Properties.MpSlug[0]
|
entry.Slug = mf.Properties.MpSlug[0]
|
||||||
}
|
}
|
||||||
err := entry.computeExtraPostParameters()
|
err := a.computeExtraPostParameters(entry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -375,7 +375,7 @@ func convertMPMfToPost(mf *microformatItem) (*post, error) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *post) computeExtraPostParameters() error {
|
func (a *goBlog) computeExtraPostParameters(p *post) error {
|
||||||
p.Content = regexp.MustCompile("\r\n").ReplaceAllString(p.Content, "\n")
|
p.Content = regexp.MustCompile("\r\n").ReplaceAllString(p.Content, "\n")
|
||||||
if split := strings.Split(p.Content, "---\n"); len(split) >= 3 && len(strings.TrimSpace(split[0])) == 0 {
|
if split := strings.Split(p.Content, "---\n"); len(split) >= 3 && len(strings.TrimSpace(split[0])) == 0 {
|
||||||
// Contains frontmatter
|
// Contains frontmatter
|
||||||
|
@ -405,7 +405,7 @@ func (p *post) computeExtraPostParameters() error {
|
||||||
p.Blog = blog[0]
|
p.Blog = blog[0]
|
||||||
delete(p.Parameters, "blog")
|
delete(p.Parameters, "blog")
|
||||||
} else {
|
} else {
|
||||||
p.Blog = appConfig.DefaultBlog
|
p.Blog = a.cfg.DefaultBlog
|
||||||
}
|
}
|
||||||
if path := p.Parameters["path"]; len(path) == 1 {
|
if path := p.Parameters["path"]; len(path) == 1 {
|
||||||
p.Path = path[0]
|
p.Path = path[0]
|
||||||
|
@ -433,15 +433,15 @@ func (p *post) computeExtraPostParameters() error {
|
||||||
}
|
}
|
||||||
if p.Path == "" && p.Section == "" {
|
if p.Path == "" && p.Section == "" {
|
||||||
// Has no path or section -> default section
|
// Has no path or section -> default section
|
||||||
p.Section = appConfig.Blogs[p.Blog].DefaultSection
|
p.Section = a.cfg.Blogs[p.Blog].DefaultSection
|
||||||
}
|
}
|
||||||
if p.Published == "" && p.Section != "" {
|
if p.Published == "" && p.Section != "" {
|
||||||
// Has no published date, but section -> published now
|
// Has no published date, but section -> published now
|
||||||
p.Published = time.Now().Local().String()
|
p.Published = time.Now().Local().String()
|
||||||
}
|
}
|
||||||
// Add images not in content
|
// Add images not in content
|
||||||
images := p.Parameters[appConfig.Micropub.PhotoParam]
|
images := p.Parameters[a.cfg.Micropub.PhotoParam]
|
||||||
imageAlts := p.Parameters[appConfig.Micropub.PhotoDescriptionParam]
|
imageAlts := p.Parameters[a.cfg.Micropub.PhotoDescriptionParam]
|
||||||
useAlts := len(images) == len(imageAlts)
|
useAlts := len(images) == len(imageAlts)
|
||||||
for i, image := range images {
|
for i, image := range images {
|
||||||
if !strings.Contains(p.Content, image) {
|
if !strings.Contains(p.Content, image) {
|
||||||
|
@ -455,26 +455,26 @@ func (p *post) computeExtraPostParameters() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func micropubDelete(w http.ResponseWriter, r *http.Request, u *url.URL) {
|
func (a *goBlog) micropubDelete(w http.ResponseWriter, r *http.Request, u *url.URL) {
|
||||||
if !strings.Contains(r.Context().Value(indieAuthScope).(string), "delete") {
|
if !strings.Contains(r.Context().Value(indieAuthScope).(string), "delete") {
|
||||||
serveError(w, r, "delete scope missing", http.StatusForbidden)
|
a.serveError(w, r, "delete scope missing", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := deletePost(u.Path); err != nil {
|
if err := a.deletePost(u.Path); err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusBadRequest)
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, u.String(), http.StatusNoContent)
|
http.Redirect(w, r, u.String(), http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
func micropubUpdate(w http.ResponseWriter, r *http.Request, u *url.URL, mf *microformatItem) {
|
func (a *goBlog) micropubUpdate(w http.ResponseWriter, r *http.Request, u *url.URL, mf *microformatItem) {
|
||||||
if !strings.Contains(r.Context().Value(indieAuthScope).(string), "update") {
|
if !strings.Contains(r.Context().Value(indieAuthScope).(string), "update") {
|
||||||
serveError(w, r, "update scope missing", http.StatusForbidden)
|
a.serveError(w, r, "update scope missing", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
p, err := getPost(u.Path)
|
p, err := a.db.getPost(u.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusBadRequest)
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
oldPath := p.Path
|
oldPath := p.Path
|
||||||
|
@ -491,15 +491,15 @@ func micropubUpdate(w http.ResponseWriter, r *http.Request, u *url.URL, mf *micr
|
||||||
case "name":
|
case "name":
|
||||||
p.Parameters["title"] = cast.ToStringSlice(value)
|
p.Parameters["title"] = cast.ToStringSlice(value)
|
||||||
case "category":
|
case "category":
|
||||||
p.Parameters[appConfig.Micropub.CategoryParam] = cast.ToStringSlice(value)
|
p.Parameters[a.cfg.Micropub.CategoryParam] = cast.ToStringSlice(value)
|
||||||
case "in-reply-to":
|
case "in-reply-to":
|
||||||
p.Parameters[appConfig.Micropub.ReplyParam] = cast.ToStringSlice(value)
|
p.Parameters[a.cfg.Micropub.ReplyParam] = cast.ToStringSlice(value)
|
||||||
case "like-of":
|
case "like-of":
|
||||||
p.Parameters[appConfig.Micropub.LikeParam] = cast.ToStringSlice(value)
|
p.Parameters[a.cfg.Micropub.LikeParam] = cast.ToStringSlice(value)
|
||||||
case "bookmark-of":
|
case "bookmark-of":
|
||||||
p.Parameters[appConfig.Micropub.BookmarkParam] = cast.ToStringSlice(value)
|
p.Parameters[a.cfg.Micropub.BookmarkParam] = cast.ToStringSlice(value)
|
||||||
case "audio":
|
case "audio":
|
||||||
p.Parameters[appConfig.Micropub.AudioParam] = cast.ToStringSlice(value)
|
p.Parameters[a.cfg.Micropub.AudioParam] = cast.ToStringSlice(value)
|
||||||
// TODO: photo
|
// TODO: photo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -514,23 +514,23 @@ func micropubUpdate(w http.ResponseWriter, r *http.Request, u *url.URL, mf *micr
|
||||||
case "updated":
|
case "updated":
|
||||||
p.Updated = strings.TrimSpace(strings.Join(cast.ToStringSlice(value), " "))
|
p.Updated = strings.TrimSpace(strings.Join(cast.ToStringSlice(value), " "))
|
||||||
case "category":
|
case "category":
|
||||||
category := p.Parameters[appConfig.Micropub.CategoryParam]
|
category := p.Parameters[a.cfg.Micropub.CategoryParam]
|
||||||
if category == nil {
|
if category == nil {
|
||||||
category = []string{}
|
category = []string{}
|
||||||
}
|
}
|
||||||
p.Parameters[appConfig.Micropub.CategoryParam] = append(category, cast.ToStringSlice(value)...)
|
p.Parameters[a.cfg.Micropub.CategoryParam] = append(category, cast.ToStringSlice(value)...)
|
||||||
case "in-reply-to":
|
case "in-reply-to":
|
||||||
p.Parameters[appConfig.Micropub.ReplyParam] = cast.ToStringSlice(value)
|
p.Parameters[a.cfg.Micropub.ReplyParam] = cast.ToStringSlice(value)
|
||||||
case "like-of":
|
case "like-of":
|
||||||
p.Parameters[appConfig.Micropub.LikeParam] = cast.ToStringSlice(value)
|
p.Parameters[a.cfg.Micropub.LikeParam] = cast.ToStringSlice(value)
|
||||||
case "bookmark-of":
|
case "bookmark-of":
|
||||||
p.Parameters[appConfig.Micropub.BookmarkParam] = cast.ToStringSlice(value)
|
p.Parameters[a.cfg.Micropub.BookmarkParam] = cast.ToStringSlice(value)
|
||||||
case "audio":
|
case "audio":
|
||||||
audio := p.Parameters[appConfig.Micropub.CategoryParam]
|
audio := p.Parameters[a.cfg.Micropub.CategoryParam]
|
||||||
if audio == nil {
|
if audio == nil {
|
||||||
audio = []string{}
|
audio = []string{}
|
||||||
}
|
}
|
||||||
p.Parameters[appConfig.Micropub.AudioParam] = append(audio, cast.ToStringSlice(value)...)
|
p.Parameters[a.cfg.Micropub.AudioParam] = append(audio, cast.ToStringSlice(value)...)
|
||||||
// TODO: photo
|
// TODO: photo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -548,18 +548,18 @@ func micropubUpdate(w http.ResponseWriter, r *http.Request, u *url.URL, mf *micr
|
||||||
case "updated":
|
case "updated":
|
||||||
p.Updated = ""
|
p.Updated = ""
|
||||||
case "category":
|
case "category":
|
||||||
delete(p.Parameters, appConfig.Micropub.CategoryParam)
|
delete(p.Parameters, a.cfg.Micropub.CategoryParam)
|
||||||
case "in-reply-to":
|
case "in-reply-to":
|
||||||
delete(p.Parameters, appConfig.Micropub.ReplyParam)
|
delete(p.Parameters, a.cfg.Micropub.ReplyParam)
|
||||||
case "like-of":
|
case "like-of":
|
||||||
delete(p.Parameters, appConfig.Micropub.LikeParam)
|
delete(p.Parameters, a.cfg.Micropub.LikeParam)
|
||||||
case "bookmark-of":
|
case "bookmark-of":
|
||||||
delete(p.Parameters, appConfig.Micropub.BookmarkParam)
|
delete(p.Parameters, a.cfg.Micropub.BookmarkParam)
|
||||||
case "audio":
|
case "audio":
|
||||||
delete(p.Parameters, appConfig.Micropub.AudioParam)
|
delete(p.Parameters, a.cfg.Micropub.AudioParam)
|
||||||
case "photo":
|
case "photo":
|
||||||
delete(p.Parameters, appConfig.Micropub.PhotoParam)
|
delete(p.Parameters, a.cfg.Micropub.PhotoParam)
|
||||||
delete(p.Parameters, appConfig.Micropub.PhotoDescriptionParam)
|
delete(p.Parameters, a.cfg.Micropub.PhotoDescriptionParam)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -576,11 +576,11 @@ func micropubUpdate(w http.ResponseWriter, r *http.Request, u *url.URL, mf *micr
|
||||||
case "updated":
|
case "updated":
|
||||||
p.Updated = ""
|
p.Updated = ""
|
||||||
case "in-reply-to":
|
case "in-reply-to":
|
||||||
delete(p.Parameters, appConfig.Micropub.ReplyParam)
|
delete(p.Parameters, a.cfg.Micropub.ReplyParam)
|
||||||
case "like-of":
|
case "like-of":
|
||||||
delete(p.Parameters, appConfig.Micropub.LikeParam)
|
delete(p.Parameters, a.cfg.Micropub.LikeParam)
|
||||||
case "bookmark-of":
|
case "bookmark-of":
|
||||||
delete(p.Parameters, appConfig.Micropub.BookmarkParam)
|
delete(p.Parameters, a.cfg.Micropub.BookmarkParam)
|
||||||
// Use content to edit other parameters
|
// Use content to edit other parameters
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -588,15 +588,15 @@ func micropubUpdate(w http.ResponseWriter, r *http.Request, u *url.URL, mf *micr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = p.computeExtraPostParameters()
|
err = a.computeExtraPostParameters(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = p.replace(oldPath, oldStatus)
|
err = a.replacePost(p, oldPath, oldStatus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, p.fullURL(), http.StatusNoContent)
|
http.Redirect(w, r, a.fullPostURL(p), http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,23 +15,23 @@ import (
|
||||||
|
|
||||||
const micropubMediaSubPath = "/media"
|
const micropubMediaSubPath = "/media"
|
||||||
|
|
||||||
func serveMicropubMedia(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveMicropubMedia(w http.ResponseWriter, r *http.Request) {
|
||||||
if !strings.Contains(r.Context().Value(indieAuthScope).(string), "media") {
|
if !strings.Contains(r.Context().Value(indieAuthScope).(string), "media") {
|
||||||
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, contentTypeMultipartForm) {
|
||||||
serveError(w, r, "wrong content-type", http.StatusBadRequest)
|
a.serveError(w, r, "wrong content-type", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := r.ParseMultipartForm(0)
|
err := r.ParseMultipartForm(0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusBadRequest)
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
file, header, err := r.FormFile("file")
|
file, header, err := r.FormFile("file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusBadRequest)
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer func() { _ = file.Close() }()
|
defer func() { _ = file.Close() }()
|
||||||
|
@ -39,7 +39,7 @@ func serveMicropubMedia(w http.ResponseWriter, r *http.Request) {
|
||||||
defer func() { _ = hashFile.Close() }()
|
defer func() { _ = hashFile.Close() }()
|
||||||
fileName, err := getSHA256(hashFile)
|
fileName, err := getSHA256(hashFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fileExtension := filepath.Ext(header.Filename)
|
fileExtension := filepath.Ext(header.Filename)
|
||||||
|
@ -55,22 +55,22 @@ func serveMicropubMedia(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
fileName += strings.ToLower(fileExtension)
|
fileName += strings.ToLower(fileExtension)
|
||||||
// Save file
|
// Save file
|
||||||
location, err := uploadFile(fileName, file)
|
location, err := a.uploadFile(fileName, file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, "failed to save original file: "+err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, "failed to save original file: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Try to compress file (only when not in private mode)
|
// Try to compress file (only when not in private mode)
|
||||||
if pm := appConfig.PrivateMode; !(pm != nil && pm.Enabled) {
|
if pm := a.cfg.PrivateMode; !(pm != nil && pm.Enabled) {
|
||||||
serveCompressionError := func(ce error) {
|
serveCompressionError := func(ce error) {
|
||||||
serveError(w, r, "failed to compress file: "+ce.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, "failed to compress file: "+ce.Error(), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
var compressedLocation string
|
var compressedLocation string
|
||||||
var compressionErr error
|
var compressionErr error
|
||||||
if ms := appConfig.Micropub.MediaStorage; ms != nil {
|
if ms := a.cfg.Micropub.MediaStorage; ms != nil {
|
||||||
// Default ShortPixel
|
// Default ShortPixel
|
||||||
if ms.ShortPixelKey != "" {
|
if ms.ShortPixelKey != "" {
|
||||||
compressedLocation, compressionErr = shortPixel(location, ms)
|
compressedLocation, compressionErr = a.shortPixel(location, ms)
|
||||||
}
|
}
|
||||||
if compressionErr != nil {
|
if compressionErr != nil {
|
||||||
serveCompressionError(compressionErr)
|
serveCompressionError(compressionErr)
|
||||||
|
@ -78,7 +78,7 @@ func serveMicropubMedia(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
// Fallback Tinify
|
// Fallback Tinify
|
||||||
if compressedLocation == "" && ms.TinifyKey != "" {
|
if compressedLocation == "" && ms.TinifyKey != "" {
|
||||||
compressedLocation, compressionErr = tinify(location, ms)
|
compressedLocation, compressionErr = a.tinify(location, ms)
|
||||||
}
|
}
|
||||||
if compressionErr != nil {
|
if compressionErr != nil {
|
||||||
serveCompressionError(compressionErr)
|
serveCompressionError(compressionErr)
|
||||||
|
@ -86,7 +86,7 @@ func serveMicropubMedia(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
// Fallback Cloudflare
|
// Fallback Cloudflare
|
||||||
if compressedLocation == "" && ms.CloudflareCompressionEnabled {
|
if compressedLocation == "" && ms.CloudflareCompressionEnabled {
|
||||||
compressedLocation, compressionErr = cloudflare(location)
|
compressedLocation, compressionErr = a.cloudflare(location)
|
||||||
}
|
}
|
||||||
if compressionErr != nil {
|
if compressionErr != nil {
|
||||||
serveCompressionError(compressionErr)
|
serveCompressionError(compressionErr)
|
||||||
|
@ -101,10 +101,10 @@ func serveMicropubMedia(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Redirect(w, r, location, http.StatusCreated)
|
http.Redirect(w, r, location, http.StatusCreated)
|
||||||
}
|
}
|
||||||
|
|
||||||
func uploadFile(filename string, f io.Reader) (string, error) {
|
func (a *goBlog) uploadFile(filename string, f io.Reader) (string, error) {
|
||||||
ms := appConfig.Micropub.MediaStorage
|
ms := a.cfg.Micropub.MediaStorage
|
||||||
if ms != nil && ms.BunnyStorageKey != "" && ms.BunnyStorageName != "" {
|
if ms != nil && ms.BunnyStorageKey != "" && ms.BunnyStorageName != "" {
|
||||||
return uploadToBunny(filename, f, ms)
|
return ms.uploadToBunny(filename, f)
|
||||||
}
|
}
|
||||||
loc, err := saveMediaFile(filename, f)
|
loc, err := saveMediaFile(filename, f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -113,10 +113,10 @@ func uploadFile(filename string, f io.Reader) (string, error) {
|
||||||
if ms != nil && ms.MediaURL != "" {
|
if ms != nil && ms.MediaURL != "" {
|
||||||
return ms.MediaURL + loc, nil
|
return ms.MediaURL + loc, nil
|
||||||
}
|
}
|
||||||
return appConfig.Server.PublicAddress + loc, nil
|
return a.cfg.Server.PublicAddress + loc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func uploadToBunny(filename string, f io.Reader, config *configMicropubMedia) (location string, err error) {
|
func (config *configMicropubMedia) uploadToBunny(filename string, f io.Reader) (location string, err error) {
|
||||||
if config == nil || config.BunnyStorageName == "" || config.BunnyStorageKey == "" || config.MediaURL == "" {
|
if config == nil || config.BunnyStorageName == "" || config.BunnyStorageKey == "" || config.MediaURL == "" {
|
||||||
return "", errors.New("Bunny storage not completely configured")
|
return "", errors.New("Bunny storage not completely configured")
|
||||||
}
|
}
|
||||||
|
|
33
minify.go
33
minify.go
|
@ -2,6 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/tdewolff/minify/v2"
|
"github.com/tdewolff/minify/v2"
|
||||||
mCss "github.com/tdewolff/minify/v2/css"
|
mCss "github.com/tdewolff/minify/v2/css"
|
||||||
|
@ -11,22 +12,28 @@ import (
|
||||||
mXml "github.com/tdewolff/minify/v2/xml"
|
mXml "github.com/tdewolff/minify/v2/xml"
|
||||||
)
|
)
|
||||||
|
|
||||||
var minifier *minify.M
|
var (
|
||||||
|
initMinify sync.Once
|
||||||
|
minifier *minify.M
|
||||||
|
)
|
||||||
|
|
||||||
func initMinify() {
|
func getMinifier() *minify.M {
|
||||||
minifier = minify.New()
|
initMinify.Do(func() {
|
||||||
minifier.AddFunc(contentTypeHTML, mHtml.Minify)
|
minifier = minify.New()
|
||||||
minifier.AddFunc("text/css", mCss.Minify)
|
minifier.AddFunc(contentTypeHTML, mHtml.Minify)
|
||||||
minifier.AddFunc(contentTypeXML, mXml.Minify)
|
minifier.AddFunc("text/css", mCss.Minify)
|
||||||
minifier.AddFunc("application/javascript", mJs.Minify)
|
minifier.AddFunc(contentTypeXML, mXml.Minify)
|
||||||
minifier.AddFunc(contentTypeRSS, mXml.Minify)
|
minifier.AddFunc("application/javascript", mJs.Minify)
|
||||||
minifier.AddFunc(contentTypeATOM, mXml.Minify)
|
minifier.AddFunc(contentTypeRSS, mXml.Minify)
|
||||||
minifier.AddFunc(contentTypeJSONFeed, mJson.Minify)
|
minifier.AddFunc(contentTypeATOM, mXml.Minify)
|
||||||
minifier.AddFunc(contentTypeAS, mJson.Minify)
|
minifier.AddFunc(contentTypeJSONFeed, mJson.Minify)
|
||||||
|
minifier.AddFunc(contentTypeAS, mJson.Minify)
|
||||||
|
})
|
||||||
|
return minifier
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeMinified(w io.Writer, mediatype string, b []byte) (int, error) {
|
func writeMinified(w io.Writer, mediatype string, b []byte) (int, error) {
|
||||||
mw := minifier.Writer(mediatype, w)
|
mw := getMinifier().Writer(mediatype, w)
|
||||||
defer func() { mw.Close() }()
|
defer func() { _ = mw.Close() }()
|
||||||
return mw.Write(b)
|
return mw.Write(b)
|
||||||
}
|
}
|
||||||
|
|
10
nodeinfo.go
10
nodeinfo.go
|
@ -5,11 +5,11 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func serveNodeInfoDiscover(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveNodeInfoDiscover(w http.ResponseWriter, r *http.Request) {
|
||||||
b, _ := json.Marshal(map[string]interface{}{
|
b, _ := json.Marshal(map[string]interface{}{
|
||||||
"links": []map[string]interface{}{
|
"links": []map[string]interface{}{
|
||||||
{
|
{
|
||||||
"href": appConfig.Server.PublicAddress + "/nodeinfo",
|
"href": a.cfg.Server.PublicAddress + "/nodeinfo",
|
||||||
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.1",
|
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -18,8 +18,8 @@ func serveNodeInfoDiscover(w http.ResponseWriter, r *http.Request) {
|
||||||
_, _ = writeMinified(w, contentTypeJSON, b)
|
_, _ = writeMinified(w, contentTypeJSON, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveNodeInfo(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveNodeInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
localPosts, _ := countPosts(&postsRequestConfig{
|
localPosts, _ := a.db.countPosts(&postsRequestConfig{
|
||||||
status: statusPublished,
|
status: statusPublished,
|
||||||
})
|
})
|
||||||
b, _ := json.Marshal(map[string]interface{}{
|
b, _ := json.Marshal(map[string]interface{}{
|
||||||
|
@ -30,7 +30,7 @@ func serveNodeInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
},
|
},
|
||||||
"usage": map[string]interface{}{
|
"usage": map[string]interface{}{
|
||||||
"users": map[string]interface{}{
|
"users": map[string]interface{}{
|
||||||
"total": len(appConfig.Blogs),
|
"total": len(a.cfg.Blogs),
|
||||||
},
|
},
|
||||||
"localPosts": localPosts,
|
"localPosts": localPosts,
|
||||||
},
|
},
|
||||||
|
|
|
@ -21,15 +21,15 @@ type notification struct {
|
||||||
Text string
|
Text string
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendNotification(text string) {
|
func (a *goBlog) sendNotification(text string) {
|
||||||
n := ¬ification{
|
n := ¬ification{
|
||||||
Time: time.Now().Unix(),
|
Time: time.Now().Unix(),
|
||||||
Text: text,
|
Text: text,
|
||||||
}
|
}
|
||||||
if err := saveNotification(n); err != nil {
|
if err := a.db.saveNotification(n); err != nil {
|
||||||
log.Println("Failed to save notification:", err.Error())
|
log.Println("Failed to save notification:", err.Error())
|
||||||
}
|
}
|
||||||
if an := appConfig.Notifications; an != nil {
|
if an := a.cfg.Notifications; an != nil {
|
||||||
if tg := an.Telegram; tg != nil && tg.Enabled {
|
if tg := an.Telegram; tg != nil && tg.Enabled {
|
||||||
err := sendTelegramMessage(n.Text, "", tg.BotToken, tg.ChatID)
|
err := sendTelegramMessage(n.Text, "", tg.BotToken, tg.ChatID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -39,15 +39,15 @@ func sendNotification(text string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveNotification(n *notification) error {
|
func (db *database) saveNotification(n *notification) error {
|
||||||
if _, err := appDb.exec("insert into notifications (time, text) values (@time, @text)", sql.Named("time", n.Time), sql.Named("text", n.Text)); err != nil {
|
if _, err := db.exec("insert into notifications (time, text) values (@time, @text)", sql.Named("time", n.Time), sql.Named("text", n.Text)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteNotification(id int) error {
|
func (db *database) deleteNotification(id int) error {
|
||||||
_, err := appDb.exec("delete from notifications where id = @id", sql.Named("id", id))
|
_, err := db.exec("delete from notifications where id = @id", sql.Named("id", id))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,10 +65,10 @@ func buildNotificationsQuery(config *notificationsRequestConfig) (query string,
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func getNotifications(config *notificationsRequestConfig) ([]*notification, error) {
|
func (db *database) getNotifications(config *notificationsRequestConfig) ([]*notification, error) {
|
||||||
notifications := []*notification{}
|
notifications := []*notification{}
|
||||||
query, args := buildNotificationsQuery(config)
|
query, args := buildNotificationsQuery(config)
|
||||||
rows, err := appDb.query(query, args...)
|
rows, err := db.query(query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -83,10 +83,10 @@ func getNotifications(config *notificationsRequestConfig) ([]*notification, erro
|
||||||
return notifications, nil
|
return notifications, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func countNotifications(config *notificationsRequestConfig) (count int, err error) {
|
func (db *database) countNotifications(config *notificationsRequestConfig) (count int, err error) {
|
||||||
query, params := buildNotificationsQuery(config)
|
query, params := buildNotificationsQuery(config)
|
||||||
query = "select count(*) from (" + query + ")"
|
query = "select count(*) from (" + query + ")"
|
||||||
row, err := appDb.queryRow(query, params...)
|
row, err := db.queryRow(query, params...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -97,11 +97,12 @@ func countNotifications(config *notificationsRequestConfig) (count int, err erro
|
||||||
type notificationsPaginationAdapter struct {
|
type notificationsPaginationAdapter struct {
|
||||||
config *notificationsRequestConfig
|
config *notificationsRequestConfig
|
||||||
nums int64
|
nums int64
|
||||||
|
db *database
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *notificationsPaginationAdapter) Nums() (int64, error) {
|
func (p *notificationsPaginationAdapter) Nums() (int64, error) {
|
||||||
if p.nums == 0 {
|
if p.nums == 0 {
|
||||||
nums, _ := countNotifications(p.config)
|
nums, _ := p.db.countNotifications(p.config)
|
||||||
p.nums = int64(nums)
|
p.nums = int64(nums)
|
||||||
}
|
}
|
||||||
return p.nums, nil
|
return p.nums, nil
|
||||||
|
@ -112,21 +113,21 @@ func (p *notificationsPaginationAdapter) Slice(offset, length int, data interfac
|
||||||
modifiedConfig.offset = offset
|
modifiedConfig.offset = offset
|
||||||
modifiedConfig.limit = length
|
modifiedConfig.limit = length
|
||||||
|
|
||||||
notifications, err := getNotifications(&modifiedConfig)
|
notifications, err := p.db.getNotifications(&modifiedConfig)
|
||||||
reflect.ValueOf(data).Elem().Set(reflect.ValueOf(¬ifications).Elem())
|
reflect.ValueOf(data).Elem().Set(reflect.ValueOf(¬ifications).Elem())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func notificationsAdmin(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) notificationsAdmin(w http.ResponseWriter, r *http.Request) {
|
||||||
// Adapter
|
// Adapter
|
||||||
pageNoString := chi.URLParam(r, "page")
|
pageNoString := chi.URLParam(r, "page")
|
||||||
pageNo, _ := strconv.Atoi(pageNoString)
|
pageNo, _ := strconv.Atoi(pageNoString)
|
||||||
p := paginator.New(¬ificationsPaginationAdapter{config: ¬ificationsRequestConfig{}}, 10)
|
p := paginator.New(¬ificationsPaginationAdapter{config: ¬ificationsRequestConfig{}, db: a.db}, 10)
|
||||||
p.SetPage(pageNo)
|
p.SetPage(pageNo)
|
||||||
var notifications []*notification
|
var notifications []*notification
|
||||||
err := p.Results(¬ifications)
|
err := p.Results(¬ifications)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Navigation
|
// Navigation
|
||||||
|
@ -152,7 +153,7 @@ func notificationsAdmin(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
nextPath = fmt.Sprintf("%s/page/%d", notificationsPath, nextPage)
|
nextPath = fmt.Sprintf("%s/page/%d", notificationsPath, nextPage)
|
||||||
// Render
|
// Render
|
||||||
render(w, r, templateNotificationsAdmin, &renderData{
|
a.render(w, r, templateNotificationsAdmin, &renderData{
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"Notifications": notifications,
|
"Notifications": notifications,
|
||||||
"HasPrev": hasPrev,
|
"HasPrev": hasPrev,
|
||||||
|
@ -163,15 +164,15 @@ func notificationsAdmin(w http.ResponseWriter, r *http.Request) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func notificationsAdminDelete(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) notificationsAdminDelete(w http.ResponseWriter, r *http.Request) {
|
||||||
id, err := strconv.Atoi(r.FormValue("notificationid"))
|
id, err := strconv.Atoi(r.FormValue("notificationid"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusBadRequest)
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = deleteNotification(id)
|
err = a.db.deleteNotification(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, ".", http.StatusFound)
|
http.Redirect(w, r, ".", http.StatusFound)
|
||||||
|
|
|
@ -3,21 +3,17 @@ package main
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/sync/singleflight"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func cachePersistently(key string, data []byte) error {
|
func (db *database) cachePersistently(key string, data []byte) error {
|
||||||
date, _ := toLocal(time.Now().String())
|
date, _ := toLocal(time.Now().String())
|
||||||
_, err := appDb.exec("insert or replace into persistent_cache(key, data, date) values(@key, @data, @date)", sql.Named("key", key), sql.Named("data", data), sql.Named("date", date))
|
_, err := db.exec("insert or replace into persistent_cache(key, data, date) values(@key, @data, @date)", sql.Named("key", key), sql.Named("data", data), sql.Named("date", date))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var persistentCacheGroup singleflight.Group
|
func (db *database) retrievePersistentCache(key string) (data []byte, err error) {
|
||||||
|
d, err, _ := db.persistentCacheGroup.Do(key, func() (interface{}, error) {
|
||||||
func retrievePersistentCache(key string) (data []byte, err error) {
|
if row, err := db.queryRow("select data from persistent_cache where key = @key", sql.Named("key", key)); err == sql.ErrNoRows {
|
||||||
d, err, _ := persistentCacheGroup.Do(key, func() (interface{}, error) {
|
|
||||||
if row, err := appDb.queryRow("select data from persistent_cache where key = @key", sql.Named("key", key)); err == sql.ErrNoRows {
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -32,7 +28,7 @@ func retrievePersistentCache(key string) (data []byte, err error) {
|
||||||
return d.([]byte), nil
|
return d.([]byte), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func clearPersistentCache(pattern string) error {
|
func (db *database) clearPersistentCache(pattern string) error {
|
||||||
_, err := appDb.exec("delete from persistent_cache where key like @pattern", sql.Named("pattern", pattern))
|
_, err := db.exec("delete from persistent_cache where key like @pattern", sql.Named("pattern", pattern))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,9 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func allPostAliases() ([]string, error) {
|
func (db *database) allPostAliases() ([]string, error) {
|
||||||
var aliases []string
|
var aliases []string
|
||||||
rows, err := appDb.query("select distinct value from post_parameters where parameter = 'aliases' and value != path")
|
rows, err := db.query("select distinct value from post_parameters where parameter = 'aliases' and value != path")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -21,18 +21,18 @@ func allPostAliases() ([]string, error) {
|
||||||
return aliases, nil
|
return aliases, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func servePostAlias(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) servePostAlias(w http.ResponseWriter, r *http.Request) {
|
||||||
row, err := appDb.queryRow("select path from post_parameters where parameter = 'aliases' and value = @alias", sql.Named("alias", r.URL.Path))
|
row, err := a.db.queryRow("select path from post_parameters where parameter = 'aliases' and value = @alias", sql.Named("alias", r.URL.Path))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var path string
|
var path string
|
||||||
if err := row.Scan(&path); err == sql.ErrNoRows {
|
if err := row.Scan(&path); err == sql.ErrNoRows {
|
||||||
serve404(w, r)
|
a.serve404(w, r)
|
||||||
return
|
return
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, path, http.StatusFound)
|
http.Redirect(w, r, path, http.StatusFound)
|
||||||
|
|
65
posts.go
65
posts.go
|
@ -41,45 +41,45 @@ const (
|
||||||
statusDraft postStatus = "draft"
|
statusDraft postStatus = "draft"
|
||||||
)
|
)
|
||||||
|
|
||||||
func servePost(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) servePost(w http.ResponseWriter, r *http.Request) {
|
||||||
t := servertiming.FromContext(r.Context()).NewMetric("gp").Start()
|
t := servertiming.FromContext(r.Context()).NewMetric("gp").Start()
|
||||||
p, err := getPost(r.URL.Path)
|
p, err := a.db.getPost(r.URL.Path)
|
||||||
t.Stop()
|
t.Stop()
|
||||||
if err == errPostNotFound {
|
if err == errPostNotFound {
|
||||||
serve404(w, r)
|
a.serve404(w, r)
|
||||||
return
|
return
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if asRequest, ok := r.Context().Value(asRequestKey).(bool); ok && asRequest {
|
if asRequest, ok := r.Context().Value(asRequestKey).(bool); ok && asRequest {
|
||||||
if r.URL.Path == blogPath(p.Blog) {
|
if r.URL.Path == a.blogPath(p.Blog) {
|
||||||
appConfig.Blogs[p.Blog].serveActivityStreams(p.Blog, w, r)
|
a.serveActivityStreams(p.Blog, w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
p.serveActivityStreams(w)
|
a.serveActivityStreamsPost(p, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
canonical := p.firstParameter("original")
|
canonical := p.firstParameter("original")
|
||||||
if canonical == "" {
|
if canonical == "" {
|
||||||
canonical = p.fullURL()
|
canonical = a.fullPostURL(p)
|
||||||
}
|
}
|
||||||
template := templatePost
|
template := templatePost
|
||||||
if p.Path == appConfig.Blogs[p.Blog].Path {
|
if p.Path == a.cfg.Blogs[p.Blog].Path {
|
||||||
template = templateStaticHome
|
template = templateStaticHome
|
||||||
}
|
}
|
||||||
w.Header().Add("Link", fmt.Sprintf("<%s>; rel=shortlink", p.shortURL()))
|
w.Header().Add("Link", fmt.Sprintf("<%s>; rel=shortlink", a.shortPostURL(p)))
|
||||||
render(w, r, template, &renderData{
|
a.render(w, r, template, &renderData{
|
||||||
BlogString: p.Blog,
|
BlogString: p.Blog,
|
||||||
Canonical: canonical,
|
Canonical: canonical,
|
||||||
Data: p,
|
Data: p,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func redirectToRandomPost(rw http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) redirectToRandomPost(rw http.ResponseWriter, r *http.Request) {
|
||||||
randomPath, err := getRandomPostPath(r.Context().Value(blogContextKey).(string))
|
randomPath, err := a.getRandomPostPath(r.Context().Value(blogContextKey).(string))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(rw, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(rw, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Redirect(rw, r, randomPath, http.StatusFound)
|
http.Redirect(rw, r, randomPath, http.StatusFound)
|
||||||
|
@ -88,11 +88,12 @@ func redirectToRandomPost(rw http.ResponseWriter, r *http.Request) {
|
||||||
type postPaginationAdapter struct {
|
type postPaginationAdapter struct {
|
||||||
config *postsRequestConfig
|
config *postsRequestConfig
|
||||||
nums int64
|
nums int64
|
||||||
|
db *database
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *postPaginationAdapter) Nums() (int64, error) {
|
func (p *postPaginationAdapter) Nums() (int64, error) {
|
||||||
if p.nums == 0 {
|
if p.nums == 0 {
|
||||||
nums, _ := countPosts(p.config)
|
nums, _ := p.db.countPosts(p.config)
|
||||||
p.nums = int64(nums)
|
p.nums = int64(nums)
|
||||||
}
|
}
|
||||||
return p.nums, nil
|
return p.nums, nil
|
||||||
|
@ -103,23 +104,23 @@ func (p *postPaginationAdapter) Slice(offset, length int, data interface{}) erro
|
||||||
modifiedConfig.offset = offset
|
modifiedConfig.offset = offset
|
||||||
modifiedConfig.limit = length
|
modifiedConfig.limit = length
|
||||||
|
|
||||||
posts, err := getPosts(&modifiedConfig)
|
posts, err := p.db.getPosts(&modifiedConfig)
|
||||||
reflect.ValueOf(data).Elem().Set(reflect.ValueOf(&posts).Elem())
|
reflect.ValueOf(data).Elem().Set(reflect.ValueOf(&posts).Elem())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveHome(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveHome(w http.ResponseWriter, r *http.Request) {
|
||||||
blog := r.Context().Value(blogContextKey).(string)
|
blog := r.Context().Value(blogContextKey).(string)
|
||||||
if asRequest, ok := r.Context().Value(asRequestKey).(bool); ok && asRequest {
|
if asRequest, ok := r.Context().Value(asRequestKey).(bool); ok && asRequest {
|
||||||
appConfig.Blogs[blog].serveActivityStreams(blog, w, r)
|
a.serveActivityStreams(blog, w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{
|
a.serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{
|
||||||
path: blogPath(blog),
|
path: a.blogPath(blog),
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveDate(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveDate(w http.ResponseWriter, r *http.Request) {
|
||||||
var year, month, day int
|
var year, month, day int
|
||||||
if ys := chi.URLParam(r, "year"); ys != "" && ys != "x" {
|
if ys := chi.URLParam(r, "year"); ys != "" && ys != "x" {
|
||||||
year, _ = strconv.Atoi(ys)
|
year, _ = strconv.Atoi(ys)
|
||||||
|
@ -131,11 +132,11 @@ func serveDate(w http.ResponseWriter, r *http.Request) {
|
||||||
day, _ = strconv.Atoi(ds)
|
day, _ = strconv.Atoi(ds)
|
||||||
}
|
}
|
||||||
if year == 0 && month == 0 && day == 0 {
|
if year == 0 && month == 0 && day == 0 {
|
||||||
serve404(w, r)
|
a.serve404(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var title, dPath strings.Builder
|
var title, dPath strings.Builder
|
||||||
dPath.WriteString(blogPath(r.Context().Value(blogContextKey).(string)) + "/")
|
dPath.WriteString(a.blogPath(r.Context().Value(blogContextKey).(string)) + "/")
|
||||||
if year != 0 {
|
if year != 0 {
|
||||||
ys := fmt.Sprintf("%0004d", year)
|
ys := fmt.Sprintf("%0004d", year)
|
||||||
title.WriteString(ys)
|
title.WriteString(ys)
|
||||||
|
@ -155,7 +156,7 @@ func serveDate(w http.ResponseWriter, r *http.Request) {
|
||||||
title.WriteString(fmt.Sprintf("-%02d", day))
|
title.WriteString(fmt.Sprintf("-%02d", day))
|
||||||
dPath.WriteString(fmt.Sprintf("/%02d", day))
|
dPath.WriteString(fmt.Sprintf("/%02d", day))
|
||||||
}
|
}
|
||||||
serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{
|
a.serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{
|
||||||
path: dPath.String(),
|
path: dPath.String(),
|
||||||
year: year,
|
year: year,
|
||||||
month: month,
|
month: month,
|
||||||
|
@ -179,7 +180,7 @@ type indexConfig struct {
|
||||||
|
|
||||||
const indexConfigKey requestContextKey = "indexConfig"
|
const indexConfigKey requestContextKey = "indexConfig"
|
||||||
|
|
||||||
func serveIndex(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveIndex(w http.ResponseWriter, r *http.Request) {
|
||||||
ic := r.Context().Value(indexConfigKey).(*indexConfig)
|
ic := r.Context().Value(indexConfigKey).(*indexConfig)
|
||||||
blog := ic.blog
|
blog := ic.blog
|
||||||
if blog == "" {
|
if blog == "" {
|
||||||
|
@ -195,7 +196,7 @@ func serveIndex(w http.ResponseWriter, r *http.Request) {
|
||||||
if ic.section != nil {
|
if ic.section != nil {
|
||||||
sections = []string{ic.section.Name}
|
sections = []string{ic.section.Name}
|
||||||
} else {
|
} else {
|
||||||
for sectionKey := range appConfig.Blogs[blog].Sections {
|
for sectionKey := range a.cfg.Blogs[blog].Sections {
|
||||||
sections = append(sections, sectionKey)
|
sections = append(sections, sectionKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -210,14 +211,14 @@ func serveIndex(w http.ResponseWriter, r *http.Request) {
|
||||||
publishedMonth: ic.month,
|
publishedMonth: ic.month,
|
||||||
publishedDay: ic.day,
|
publishedDay: ic.day,
|
||||||
status: statusPublished,
|
status: statusPublished,
|
||||||
}}, appConfig.Blogs[blog].Pagination)
|
}, db: a.db}, a.cfg.Blogs[blog].Pagination)
|
||||||
p.SetPage(pageNo)
|
p.SetPage(pageNo)
|
||||||
var posts []*post
|
var posts []*post
|
||||||
t := servertiming.FromContext(r.Context()).NewMetric("gp").Start()
|
t := servertiming.FromContext(r.Context()).NewMetric("gp").Start()
|
||||||
err := p.Results(&posts)
|
err := p.Results(&posts)
|
||||||
t.Stop()
|
t.Stop()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Meta
|
// Meta
|
||||||
|
@ -229,13 +230,13 @@ func serveIndex(w http.ResponseWriter, r *http.Request) {
|
||||||
title = ic.section.Title
|
title = ic.section.Title
|
||||||
description = ic.section.Description
|
description = ic.section.Description
|
||||||
} else if search != "" {
|
} else if search != "" {
|
||||||
title = fmt.Sprintf("%s: %s", appConfig.Blogs[blog].Search.Title, search)
|
title = fmt.Sprintf("%s: %s", a.cfg.Blogs[blog].Search.Title, search)
|
||||||
}
|
}
|
||||||
// Clean title
|
// Clean title
|
||||||
title = bluemonday.StrictPolicy().Sanitize(title)
|
title = bluemonday.StrictPolicy().Sanitize(title)
|
||||||
// Check if feed
|
// Check if feed
|
||||||
if ft := feedType(chi.URLParam(r, "feed")); ft != noFeed {
|
if ft := feedType(chi.URLParam(r, "feed")); ft != noFeed {
|
||||||
generateFeed(blog, ft, w, r, posts, title, description)
|
a.generateFeed(blog, ft, w, r, posts, title, description)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Path
|
// Path
|
||||||
|
@ -269,9 +270,9 @@ func serveIndex(w http.ResponseWriter, r *http.Request) {
|
||||||
if summaryTemplate == "" {
|
if summaryTemplate == "" {
|
||||||
summaryTemplate = templateSummary
|
summaryTemplate = templateSummary
|
||||||
}
|
}
|
||||||
render(w, r, templateIndex, &renderData{
|
a.render(w, r, templateIndex, &renderData{
|
||||||
BlogString: blog,
|
BlogString: blog,
|
||||||
Canonical: appConfig.Server.PublicAddress + path,
|
Canonical: a.cfg.Server.PublicAddress + path,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"Title": title,
|
"Title": title,
|
||||||
"Description": description,
|
"Description": description,
|
||||||
|
|
179
postsDb.go
179
postsDb.go
|
@ -13,7 +13,7 @@ import (
|
||||||
"github.com/araddon/dateparse"
|
"github.com/araddon/dateparse"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *post) checkPost() (err error) {
|
func (a *goBlog) checkPost(p *post) (err error) {
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return errors.New("no post")
|
return errors.New("no post")
|
||||||
}
|
}
|
||||||
|
@ -57,13 +57,13 @@ func (p *post) checkPost() (err error) {
|
||||||
}
|
}
|
||||||
// Check blog
|
// Check blog
|
||||||
if p.Blog == "" {
|
if p.Blog == "" {
|
||||||
p.Blog = appConfig.DefaultBlog
|
p.Blog = a.cfg.DefaultBlog
|
||||||
}
|
}
|
||||||
if _, ok := appConfig.Blogs[p.Blog]; !ok {
|
if _, ok := a.cfg.Blogs[p.Blog]; !ok {
|
||||||
return errors.New("blog doesn't exist")
|
return errors.New("blog doesn't exist")
|
||||||
}
|
}
|
||||||
// Check if section exists
|
// Check if section exists
|
||||||
if _, ok := appConfig.Blogs[p.Blog].Sections[p.Section]; p.Section != "" && !ok {
|
if _, ok := a.cfg.Blogs[p.Blog].Sections[p.Section]; p.Section != "" && !ok {
|
||||||
return errors.New("section doesn't exist")
|
return errors.New("section doesn't exist")
|
||||||
}
|
}
|
||||||
// Check path
|
// Check path
|
||||||
|
@ -72,14 +72,14 @@ func (p *post) checkPost() (err error) {
|
||||||
}
|
}
|
||||||
if p.Path == "" {
|
if p.Path == "" {
|
||||||
if p.Section == "" {
|
if p.Section == "" {
|
||||||
p.Section = appConfig.Blogs[p.Blog].DefaultSection
|
p.Section = a.cfg.Blogs[p.Blog].DefaultSection
|
||||||
}
|
}
|
||||||
if p.Slug == "" {
|
if p.Slug == "" {
|
||||||
random := generateRandomString(5)
|
random := generateRandomString(5)
|
||||||
p.Slug = fmt.Sprintf("%v-%02d-%02d-%v", now.Year(), int(now.Month()), now.Day(), random)
|
p.Slug = fmt.Sprintf("%v-%02d-%02d-%v", now.Year(), int(now.Month()), now.Day(), random)
|
||||||
}
|
}
|
||||||
published, _ := dateparse.ParseLocal(p.Published)
|
published, _ := dateparse.ParseLocal(p.Published)
|
||||||
pathTmplString := appConfig.Blogs[p.Blog].Sections[p.Section].PathTemplate
|
pathTmplString := a.cfg.Blogs[p.Blog].Sections[p.Section].PathTemplate
|
||||||
if pathTmplString == "" {
|
if pathTmplString == "" {
|
||||||
return errors.New("path template empty")
|
return errors.New("path template empty")
|
||||||
}
|
}
|
||||||
|
@ -89,7 +89,7 @@ func (p *post) checkPost() (err error) {
|
||||||
}
|
}
|
||||||
var pathBuffer bytes.Buffer
|
var pathBuffer bytes.Buffer
|
||||||
err = pathTmpl.Execute(&pathBuffer, map[string]interface{}{
|
err = pathTmpl.Execute(&pathBuffer, map[string]interface{}{
|
||||||
"BlogPath": appConfig.Blogs[p.Blog].Path,
|
"BlogPath": a.cfg.Blogs[p.Blog].Path,
|
||||||
"Year": published.Year(),
|
"Year": published.Year(),
|
||||||
"Month": int(published.Month()),
|
"Month": int(published.Month()),
|
||||||
"Day": published.Day(),
|
"Day": published.Day(),
|
||||||
|
@ -107,12 +107,12 @@ func (p *post) checkPost() (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *post) create() error {
|
func (a *goBlog) createPost(p *post) error {
|
||||||
return p.createOrReplace(&postCreationOptions{new: true})
|
return a.createOrReplacePost(p, &postCreationOptions{new: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *post) replace(oldPath string, oldStatus postStatus) error {
|
func (a *goBlog) replacePost(p *post, oldPath string, oldStatus postStatus) error {
|
||||||
return p.createOrReplace(&postCreationOptions{new: false, oldPath: oldPath, oldStatus: oldStatus})
|
return a.createOrReplacePost(p, &postCreationOptions{new: false, oldPath: oldPath, oldStatus: oldStatus})
|
||||||
}
|
}
|
||||||
|
|
||||||
type postCreationOptions struct {
|
type postCreationOptions struct {
|
||||||
|
@ -123,8 +123,8 @@ type postCreationOptions struct {
|
||||||
|
|
||||||
var postCreationMutex sync.Mutex
|
var postCreationMutex sync.Mutex
|
||||||
|
|
||||||
func (p *post) createOrReplace(o *postCreationOptions) error {
|
func (a *goBlog) createOrReplacePost(p *post, o *postCreationOptions) error {
|
||||||
err := p.checkPost()
|
err := a.checkPost(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -135,7 +135,7 @@ func (p *post) createOrReplace(o *postCreationOptions) error {
|
||||||
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
|
||||||
newPathExists := false
|
newPathExists := false
|
||||||
row, err := appDb.queryRow("select exists(select 1 from posts where path = @path)", sql.Named("path", p.Path))
|
row, err := a.db.queryRow("select exists(select 1 from posts where path = @path)", sql.Named("path", p.Path))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -169,65 +169,37 @@ func (p *post) createOrReplace(o *postCreationOptions) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Execute
|
// Execute
|
||||||
_, err = appDb.execMulti(sqlBuilder.String(), sqlArgs...)
|
_, err = a.db.execMulti(sqlBuilder.String(), sqlArgs...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Update FTS index, trigger hooks and reload router
|
// Update FTS index, trigger hooks and reload router
|
||||||
rebuildFTSIndex()
|
a.db.rebuildFTSIndex()
|
||||||
if p.Status == statusPublished {
|
if p.Status == statusPublished {
|
||||||
if o.new || o.oldStatus == statusDraft {
|
if o.new || o.oldStatus == statusDraft {
|
||||||
defer p.postPostHooks()
|
defer a.postPostHooks(p)
|
||||||
} else {
|
} else {
|
||||||
defer p.postUpdateHooks()
|
defer a.postUpdateHooks(p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return reloadRouter()
|
return a.reloadRouter()
|
||||||
}
|
}
|
||||||
|
|
||||||
func deletePost(path string) error {
|
func (a *goBlog) deletePost(path string) error {
|
||||||
if path == "" {
|
if path == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
p, err := getPost(path)
|
p, err := a.db.getPost(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = appDb.exec("delete from posts where path = @path", sql.Named("path", p.Path))
|
_, err = a.db.exec("delete from posts where path = @path", sql.Named("path", p.Path))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
rebuildFTSIndex()
|
a.db.rebuildFTSIndex()
|
||||||
defer p.postDeleteHooks()
|
defer a.postDeleteHooks(p)
|
||||||
return reloadRouter()
|
return a.reloadRouter()
|
||||||
}
|
|
||||||
|
|
||||||
func rebuildFTSIndex() {
|
|
||||||
_, _ = appDb.exec("insert into posts_fts(posts_fts) values ('rebuild')")
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPost(path string) (*post, error) {
|
|
||||||
posts, err := getPosts(&postsRequestConfig{path: path})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if len(posts) == 0 {
|
|
||||||
return nil, errPostNotFound
|
|
||||||
}
|
|
||||||
return posts[0], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getRandomPostPath(blog string) (string, error) {
|
|
||||||
var sections []string
|
|
||||||
for sectionKey := range appConfig.Blogs[blog].Sections {
|
|
||||||
sections = append(sections, sectionKey)
|
|
||||||
}
|
|
||||||
posts, err := getPosts(&postsRequestConfig{randomOrder: true, limit: 1, blog: blog, sections: sections})
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
} else if len(posts) == 0 {
|
|
||||||
return "", errPostNotFound
|
|
||||||
}
|
|
||||||
return posts[0].Path, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type postsRequestConfig struct {
|
type postsRequestConfig struct {
|
||||||
|
@ -246,39 +218,39 @@ type postsRequestConfig struct {
|
||||||
randomOrder bool
|
randomOrder bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildPostsQuery(config *postsRequestConfig) (query string, args []interface{}) {
|
func buildPostsQuery(c *postsRequestConfig) (query string, args []interface{}) {
|
||||||
args = []interface{}{}
|
args = []interface{}{}
|
||||||
defaultSelection := "select p.path as path, coalesce(content, '') as content, coalesce(published, '') as published, coalesce(updated, '') as updated, coalesce(blog, '') as blog, coalesce(section, '') as section, coalesce(status, '') as status, coalesce(parameter, '') as parameter, coalesce(value, '') as value "
|
defaultSelection := "select p.path as path, coalesce(content, '') as content, coalesce(published, '') as published, coalesce(updated, '') as updated, coalesce(blog, '') as blog, coalesce(section, '') as section, coalesce(status, '') as status, coalesce(parameter, '') as parameter, coalesce(value, '') as value "
|
||||||
postsTable := "posts"
|
postsTable := "posts"
|
||||||
if config.search != "" {
|
if c.search != "" {
|
||||||
postsTable = "posts_fts(@search)"
|
postsTable = "posts_fts(@search)"
|
||||||
args = append(args, sql.Named("search", config.search))
|
args = append(args, sql.Named("search", c.search))
|
||||||
}
|
}
|
||||||
if config.status != "" && config.status != statusNil {
|
if c.status != "" && c.status != statusNil {
|
||||||
postsTable = "(select * from " + postsTable + " where status = @status)"
|
postsTable = "(select * from " + postsTable + " where status = @status)"
|
||||||
args = append(args, sql.Named("status", config.status))
|
args = append(args, sql.Named("status", c.status))
|
||||||
}
|
}
|
||||||
if config.blog != "" {
|
if c.blog != "" {
|
||||||
postsTable = "(select * from " + postsTable + " where blog = @blog)"
|
postsTable = "(select * from " + postsTable + " where blog = @blog)"
|
||||||
args = append(args, sql.Named("blog", config.blog))
|
args = append(args, sql.Named("blog", c.blog))
|
||||||
}
|
}
|
||||||
if config.parameter != "" {
|
if c.parameter != "" {
|
||||||
postsTable = "(select distinct p.* from " + postsTable + " p left outer join post_parameters pp on p.path = pp.path where pp.parameter = @param "
|
postsTable = "(select distinct p.* from " + postsTable + " p left outer join post_parameters pp on p.path = pp.path where pp.parameter = @param "
|
||||||
args = append(args, sql.Named("param", config.parameter))
|
args = append(args, sql.Named("param", c.parameter))
|
||||||
if config.parameterValue != "" {
|
if c.parameterValue != "" {
|
||||||
postsTable += "and pp.value = @paramval)"
|
postsTable += "and pp.value = @paramval)"
|
||||||
args = append(args, sql.Named("paramval", config.parameterValue))
|
args = append(args, sql.Named("paramval", c.parameterValue))
|
||||||
} else {
|
} else {
|
||||||
postsTable += "and length(coalesce(pp.value, '')) > 1)"
|
postsTable += "and length(coalesce(pp.value, '')) > 1)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if config.taxonomy != nil && len(config.taxonomyValue) > 0 {
|
if c.taxonomy != nil && len(c.taxonomyValue) > 0 {
|
||||||
postsTable = "(select distinct p.* from " + postsTable + " p left outer join post_parameters pp on p.path = pp.path where pp.parameter = @taxname and lower(pp.value) = lower(@taxval))"
|
postsTable = "(select distinct p.* from " + postsTable + " p left outer join post_parameters pp on p.path = pp.path where pp.parameter = @taxname and lower(pp.value) = lower(@taxval))"
|
||||||
args = append(args, sql.Named("taxname", config.taxonomy.Name), sql.Named("taxval", config.taxonomyValue))
|
args = append(args, sql.Named("taxname", c.taxonomy.Name), sql.Named("taxval", c.taxonomyValue))
|
||||||
}
|
}
|
||||||
if len(config.sections) > 0 {
|
if len(c.sections) > 0 {
|
||||||
postsTable = "(select * from " + postsTable + " where"
|
postsTable = "(select * from " + postsTable + " where"
|
||||||
for i, section := range config.sections {
|
for i, section := range c.sections {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
postsTable += " or"
|
postsTable += " or"
|
||||||
}
|
}
|
||||||
|
@ -288,38 +260,38 @@ func buildPostsQuery(config *postsRequestConfig) (query string, args []interface
|
||||||
}
|
}
|
||||||
postsTable += ")"
|
postsTable += ")"
|
||||||
}
|
}
|
||||||
if config.publishedYear != 0 {
|
if c.publishedYear != 0 {
|
||||||
postsTable = "(select * from " + postsTable + " p where substr(p.published, 1, 4) = @publishedyear)"
|
postsTable = "(select * from " + postsTable + " p where substr(p.published, 1, 4) = @publishedyear)"
|
||||||
args = append(args, sql.Named("publishedyear", fmt.Sprintf("%0004d", config.publishedYear)))
|
args = append(args, sql.Named("publishedyear", fmt.Sprintf("%0004d", c.publishedYear)))
|
||||||
}
|
}
|
||||||
if config.publishedMonth != 0 {
|
if c.publishedMonth != 0 {
|
||||||
postsTable = "(select * from " + postsTable + " p where substr(p.published, 6, 2) = @publishedmonth)"
|
postsTable = "(select * from " + postsTable + " p where substr(p.published, 6, 2) = @publishedmonth)"
|
||||||
args = append(args, sql.Named("publishedmonth", fmt.Sprintf("%02d", config.publishedMonth)))
|
args = append(args, sql.Named("publishedmonth", fmt.Sprintf("%02d", c.publishedMonth)))
|
||||||
}
|
}
|
||||||
if config.publishedDay != 0 {
|
if c.publishedDay != 0 {
|
||||||
postsTable = "(select * from " + postsTable + " p where substr(p.published, 9, 2) = @publishedday)"
|
postsTable = "(select * from " + postsTable + " p where substr(p.published, 9, 2) = @publishedday)"
|
||||||
args = append(args, sql.Named("publishedday", fmt.Sprintf("%02d", config.publishedDay)))
|
args = append(args, sql.Named("publishedday", fmt.Sprintf("%02d", c.publishedDay)))
|
||||||
}
|
}
|
||||||
defaultTables := " from " + postsTable + " p left outer join post_parameters pp on p.path = pp.path "
|
defaultTables := " from " + postsTable + " p left outer join post_parameters pp on p.path = pp.path "
|
||||||
defaultSorting := " order by p.published desc "
|
defaultSorting := " order by p.published desc "
|
||||||
if config.randomOrder {
|
if c.randomOrder {
|
||||||
defaultSorting = " order by random() "
|
defaultSorting = " order by random() "
|
||||||
}
|
}
|
||||||
if config.path != "" {
|
if c.path != "" {
|
||||||
query = defaultSelection + defaultTables + " where p.path = @path" + defaultSorting
|
query = defaultSelection + defaultTables + " where p.path = @path" + defaultSorting
|
||||||
args = append(args, sql.Named("path", config.path))
|
args = append(args, sql.Named("path", c.path))
|
||||||
} else if config.limit != 0 || config.offset != 0 {
|
} else if c.limit != 0 || c.offset != 0 {
|
||||||
query = defaultSelection + " from (select * from " + postsTable + " p " + defaultSorting + " limit @limit offset @offset) p left outer join post_parameters pp on p.path = pp.path "
|
query = defaultSelection + " from (select * from " + postsTable + " p " + defaultSorting + " limit @limit offset @offset) p left outer join post_parameters pp on p.path = pp.path "
|
||||||
args = append(args, sql.Named("limit", config.limit), sql.Named("offset", config.offset))
|
args = append(args, sql.Named("limit", c.limit), sql.Named("offset", c.offset))
|
||||||
} else {
|
} else {
|
||||||
query = defaultSelection + defaultTables + defaultSorting
|
query = defaultSelection + defaultTables + defaultSorting
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPosts(config *postsRequestConfig) (posts []*post, err error) {
|
func (d *database) getPosts(config *postsRequestConfig) (posts []*post, err error) {
|
||||||
query, queryParams := buildPostsQuery(config)
|
query, queryParams := buildPostsQuery(config)
|
||||||
rows, err := appDb.query(query, queryParams...)
|
rows, err := d.query(query, queryParams...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -351,10 +323,25 @@ func getPosts(config *postsRequestConfig) (posts []*post, err error) {
|
||||||
return posts, nil
|
return posts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func countPosts(config *postsRequestConfig) (count int, err error) {
|
func (d *database) getPost(path string) (*post, error) {
|
||||||
|
posts, err := d.getPosts(&postsRequestConfig{path: path})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if len(posts) == 0 {
|
||||||
|
return nil, errPostNotFound
|
||||||
|
}
|
||||||
|
return posts[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *database) getDrafts(blog string) []*post {
|
||||||
|
ps, _ := d.getPosts(&postsRequestConfig{status: statusDraft, blog: blog})
|
||||||
|
return ps
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *database) countPosts(config *postsRequestConfig) (count int, err error) {
|
||||||
query, params := buildPostsQuery(config)
|
query, params := buildPostsQuery(config)
|
||||||
query = "select count(distinct path) from (" + query + ")"
|
query = "select count(distinct path) from (" + query + ")"
|
||||||
row, err := appDb.queryRow(query, params...)
|
row, err := d.queryRow(query, params...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -362,9 +349,9 @@ func countPosts(config *postsRequestConfig) (count int, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func allPostPaths(status postStatus) ([]string, error) {
|
func (d *database) allPostPaths(status postStatus) ([]string, error) {
|
||||||
var postPaths []string
|
var postPaths []string
|
||||||
rows, err := appDb.query("select path from posts where status = @status", sql.Named("status", status))
|
rows, err := d.query("select path from posts where status = @status", sql.Named("status", status))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -378,9 +365,23 @@ func allPostPaths(status postStatus) ([]string, error) {
|
||||||
return postPaths, nil
|
return postPaths, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func allTaxonomyValues(blog string, taxonomy string) ([]string, error) {
|
func (a *goBlog) getRandomPostPath(blog string) (string, error) {
|
||||||
|
var sections []string
|
||||||
|
for sectionKey := range a.cfg.Blogs[blog].Sections {
|
||||||
|
sections = append(sections, sectionKey)
|
||||||
|
}
|
||||||
|
posts, err := a.db.getPosts(&postsRequestConfig{randomOrder: true, limit: 1, blog: blog, sections: sections})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
} else if len(posts) == 0 {
|
||||||
|
return "", errPostNotFound
|
||||||
|
}
|
||||||
|
return posts[0].Path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *database) allTaxonomyValues(blog string, taxonomy string) ([]string, error) {
|
||||||
var values []string
|
var values []string
|
||||||
rows, err := appDb.query("select distinct pp.value from posts p left outer join post_parameters pp on p.path = pp.path where pp.parameter = @tax and length(coalesce(pp.value, '')) > 1 and blog = @blog and status = @status", sql.Named("tax", taxonomy), sql.Named("blog", blog), sql.Named("status", statusPublished))
|
rows, err := d.query("select distinct pp.value from posts p left outer join post_parameters pp on p.path = pp.path where pp.parameter = @tax and length(coalesce(pp.value, '')) > 1 and blog = @blog and status = @status", sql.Named("tax", taxonomy), sql.Named("blog", blog), sql.Named("status", statusPublished))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -396,8 +397,8 @@ type publishedDate struct {
|
||||||
year, month, day int
|
year, month, day int
|
||||||
}
|
}
|
||||||
|
|
||||||
func allPublishedDates(blog string) (dates []publishedDate, err error) {
|
func (d *database) allPublishedDates(blog string) (dates []publishedDate, err error) {
|
||||||
rows, err := appDb.query("select distinct substr(published, 1, 4) as year, substr(published, 6, 2) as month, substr(published, 9, 2) as day from posts where blog = @blog and status = @status and year != '' and month != '' and day != ''", sql.Named("blog", blog), sql.Named("status", statusPublished))
|
rows, err := d.query("select distinct substr(published, 1, 4) as year, substr(published, 6, 2) as month, substr(published, 9, 2) as day from posts where blog = @blog and status = @status and year != '' and month != '' and day != ''", sql.Named("blog", blog), sql.Named("status", statusPublished))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,19 +8,19 @@ import (
|
||||||
"github.com/PuerkitoBio/goquery"
|
"github.com/PuerkitoBio/goquery"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *post) fullURL() string {
|
func (a *goBlog) fullPostURL(p *post) string {
|
||||||
return appConfig.Server.PublicAddress + p.Path
|
return a.cfg.Server.PublicAddress + p.Path
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *post) shortURL() string {
|
func (a *goBlog) shortPostURL(p *post) string {
|
||||||
s, err := shortenPath(p.Path)
|
s, err := a.db.shortenPath(p.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
if appConfig.Server.ShortPublicAddress != "" {
|
if a.cfg.Server.ShortPublicAddress != "" {
|
||||||
return appConfig.Server.ShortPublicAddress + s
|
return a.cfg.Server.ShortPublicAddress + s
|
||||||
}
|
}
|
||||||
return appConfig.Server.PublicAddress + s
|
return a.cfg.Server.PublicAddress + s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *post) firstParameter(parameter string) (result string) {
|
func (p *post) firstParameter(parameter string) (result string) {
|
||||||
|
@ -34,11 +34,11 @@ func (p *post) title() string {
|
||||||
return p.firstParameter("title")
|
return p.firstParameter("title")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *post) html() template.HTML {
|
func (a *goBlog) html(p *post) template.HTML {
|
||||||
if p.rendered != "" {
|
if p.rendered != "" {
|
||||||
return p.rendered
|
return p.rendered
|
||||||
}
|
}
|
||||||
htmlContent, err := renderMarkdown(p.Content, false)
|
htmlContent, err := a.renderMarkdown(p.Content, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
return ""
|
return ""
|
||||||
|
@ -47,11 +47,11 @@ func (p *post) html() template.HTML {
|
||||||
return p.rendered
|
return p.rendered
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *post) absoluteHTML() template.HTML {
|
func (a *goBlog) absoluteHTML(p *post) template.HTML {
|
||||||
if p.absoluteRendered != "" {
|
if p.absoluteRendered != "" {
|
||||||
return p.absoluteRendered
|
return p.absoluteRendered
|
||||||
}
|
}
|
||||||
htmlContent, err := renderMarkdown(p.Content, true)
|
htmlContent, err := a.renderMarkdown(p.Content, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
return ""
|
return ""
|
||||||
|
@ -62,12 +62,12 @@ func (p *post) absoluteHTML() template.HTML {
|
||||||
|
|
||||||
const summaryDivider = "<!--more-->"
|
const summaryDivider = "<!--more-->"
|
||||||
|
|
||||||
func (p *post) summary() (summary string) {
|
func (a *goBlog) summary(p *post) (summary string) {
|
||||||
summary = p.firstParameter("summary")
|
summary = p.firstParameter("summary")
|
||||||
if summary != "" {
|
if summary != "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
html := string(p.html())
|
html := string(a.html(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,12 +78,12 @@ func (p *post) summary() (summary string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *post) translations() []*post {
|
func (a *goBlog) translations(p *post) []*post {
|
||||||
translationkey := p.firstParameter("translationkey")
|
translationkey := p.firstParameter("translationkey")
|
||||||
if translationkey == "" {
|
if translationkey == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
posts, err := getPosts(&postsRequestConfig{
|
posts, err := a.db.getPosts(&postsRequestConfig{
|
||||||
parameter: "translationkey",
|
parameter: "translationkey",
|
||||||
parameterValue: translationkey,
|
parameterValue: translationkey,
|
||||||
})
|
})
|
||||||
|
|
16
queue.go
16
queue.go
|
@ -8,11 +8,11 @@ import (
|
||||||
"github.com/araddon/dateparse"
|
"github.com/araddon/dateparse"
|
||||||
)
|
)
|
||||||
|
|
||||||
func enqueue(name string, content []byte, schedule time.Time) error {
|
func (db *database) enqueue(name string, content []byte, schedule time.Time) error {
|
||||||
if len(content) == 0 {
|
if len(content) == 0 {
|
||||||
return errors.New("empty content")
|
return errors.New("empty content")
|
||||||
}
|
}
|
||||||
_, err := appDb.exec("insert into queue (name, content, schedule) values (@name, @content, @schedule)",
|
_, err := db.exec("insert into queue (name, content, schedule) values (@name, @content, @schedule)",
|
||||||
sql.Named("name", name), sql.Named("content", content), sql.Named("schedule", schedule.UTC().String()))
|
sql.Named("name", name), sql.Named("content", content), sql.Named("schedule", schedule.UTC().String()))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -24,18 +24,18 @@ type queueItem struct {
|
||||||
schedule *time.Time
|
schedule *time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qi *queueItem) reschedule(dur time.Duration) error {
|
func (db *database) reschedule(qi *queueItem, dur time.Duration) error {
|
||||||
_, err := appDb.exec("update queue set schedule = @schedule, content = @content where id = @id", sql.Named("schedule", qi.schedule.Add(dur).UTC().String()), sql.Named("content", qi.content), sql.Named("id", qi.id))
|
_, err := db.exec("update queue set schedule = @schedule, content = @content where id = @id", sql.Named("schedule", qi.schedule.Add(dur).UTC().String()), sql.Named("content", qi.content), sql.Named("id", qi.id))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qi *queueItem) dequeue() error {
|
func (db *database) dequeue(qi *queueItem) error {
|
||||||
_, err := appDb.exec("delete from queue where id = @id", sql.Named("id", qi.id))
|
_, err := db.exec("delete from queue where id = @id", sql.Named("id", qi.id))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func peekQueue(name string) (*queueItem, error) {
|
func (db *database) peekQueue(name string) (*queueItem, error) {
|
||||||
row, err := appDb.queryRow("select id, name, content, schedule from queue where schedule <= @schedule and name = @name order by schedule asc limit 1", sql.Named("name", name), sql.Named("schedule", time.Now().UTC().String()))
|
row, err := db.queryRow("select id, name, content, schedule from queue where schedule <= @schedule and name = @name order by schedule asc limit 1", sql.Named("name", name), sql.Named("schedule", time.Now().UTC().String()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,16 +5,14 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
)
|
)
|
||||||
|
|
||||||
var regexRedirects []*regexRedirect
|
|
||||||
|
|
||||||
type regexRedirect struct {
|
type regexRedirect struct {
|
||||||
From *regexp.Regexp
|
From *regexp.Regexp
|
||||||
To string
|
To string
|
||||||
Type int
|
Type int
|
||||||
}
|
}
|
||||||
|
|
||||||
func initRegexRedirects() error {
|
func (a *goBlog) initRegexRedirects() error {
|
||||||
for _, cr := range appConfig.PathRedirects {
|
for _, cr := range a.cfg.PathRedirects {
|
||||||
re, err := regexp.Compile(cr.From)
|
re, err := regexp.Compile(cr.From)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -27,14 +25,14 @@ func initRegexRedirects() error {
|
||||||
if r.Type == 0 {
|
if r.Type == 0 {
|
||||||
r.Type = http.StatusFound
|
r.Type = http.StatusFound
|
||||||
}
|
}
|
||||||
regexRedirects = append(regexRedirects, r)
|
a.regexRedirects = append(a.regexRedirects, r)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkRegexRedirects(next http.Handler) http.Handler {
|
func (a *goBlog) checkRegexRedirects(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
for _, re := range regexRedirects {
|
for _, re := range a.regexRedirects {
|
||||||
if newPath := re.From.ReplaceAllString(r.URL.Path, re.To); r.URL.Path != newPath {
|
if newPath := re.From.ReplaceAllString(r.URL.Path, re.To); r.URL.Path != newPath {
|
||||||
r.URL.Path = newPath
|
r.URL.Path = newPath
|
||||||
http.Redirect(w, r, r.URL.String(), re.Type)
|
http.Redirect(w, r, r.URL.String(), re.Type)
|
||||||
|
|
63
render.go
63
render.go
|
@ -45,18 +45,17 @@ const (
|
||||||
templateBlogroll = "blogroll"
|
templateBlogroll = "blogroll"
|
||||||
)
|
)
|
||||||
|
|
||||||
var templates map[string]*template.Template = map[string]*template.Template{}
|
func (a *goBlog) initRendering() error {
|
||||||
|
a.templates = map[string]*template.Template{}
|
||||||
func initRendering() error {
|
|
||||||
templateFunctions := template.FuncMap{
|
templateFunctions := template.FuncMap{
|
||||||
"menu": func(blog *configBlog, id string) *menu {
|
"menu": func(blog *configBlog, id string) *menu {
|
||||||
return blog.Menus[id]
|
return blog.Menus[id]
|
||||||
},
|
},
|
||||||
"user": func() *configUser {
|
"user": func() *configUser {
|
||||||
return appConfig.User
|
return a.cfg.User
|
||||||
},
|
},
|
||||||
"md": func(content string) template.HTML {
|
"md": func(content string) template.HTML {
|
||||||
htmlContent, err := renderMarkdown(content, false)
|
htmlContent, err := a.renderMarkdown(content, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
return ""
|
return ""
|
||||||
|
@ -80,16 +79,16 @@ func initRendering() error {
|
||||||
return p.title()
|
return p.title()
|
||||||
},
|
},
|
||||||
"content": func(p *post) template.HTML {
|
"content": func(p *post) template.HTML {
|
||||||
return p.html()
|
return a.html(p)
|
||||||
},
|
},
|
||||||
"summary": func(p *post) string {
|
"summary": func(p *post) string {
|
||||||
return p.summary()
|
return a.summary(p)
|
||||||
},
|
},
|
||||||
"translations": func(p *post) []*post {
|
"translations": func(p *post) []*post {
|
||||||
return p.translations()
|
return a.translations(p)
|
||||||
},
|
},
|
||||||
"shorturl": func(p *post) string {
|
"shorturl": func(p *post) string {
|
||||||
return p.shortURL()
|
return a.shortPostURL(p)
|
||||||
},
|
},
|
||||||
// Others
|
// Others
|
||||||
"dateformat": dateFormat,
|
"dateformat": dateFormat,
|
||||||
|
@ -120,9 +119,9 @@ func initRendering() error {
|
||||||
}
|
}
|
||||||
return d.Before(b)
|
return d.Before(b)
|
||||||
},
|
},
|
||||||
"asset": assetFileName,
|
"asset": a.assetFileName,
|
||||||
"assetsri": assetSRI,
|
"assetsri": a.assetSRI,
|
||||||
"string": appTs.GetTemplateStringVariantFunc(),
|
"string": a.ts.GetTemplateStringVariantFunc(),
|
||||||
"include": func(templateName string, data ...interface{}) (template.HTML, error) {
|
"include": func(templateName string, data ...interface{}) (template.HTML, error) {
|
||||||
if len(data) == 0 || len(data) > 2 {
|
if len(data) == 0 || len(data) > 2 {
|
||||||
return "", errors.New("wrong argument count")
|
return "", errors.New("wrong argument count")
|
||||||
|
@ -134,7 +133,7 @@ func initRendering() error {
|
||||||
rd = &nrd
|
rd = &nrd
|
||||||
}
|
}
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
err := templates[templateName].ExecuteTemplate(&buf, templateName, rd)
|
err := a.templates[templateName].ExecuteTemplate(&buf, templateName, rd)
|
||||||
return template.HTML(buf.String()), err
|
return template.HTML(buf.String()), err
|
||||||
}
|
}
|
||||||
return "", errors.New("wrong arguments")
|
return "", errors.New("wrong arguments")
|
||||||
|
@ -142,7 +141,7 @@ func initRendering() error {
|
||||||
"urlize": urlize,
|
"urlize": urlize,
|
||||||
"sort": sortedStrings,
|
"sort": sortedStrings,
|
||||||
"absolute": func(path string) string {
|
"absolute": func(path string) string {
|
||||||
return appConfig.Server.PublicAddress + path
|
return a.cfg.Server.PublicAddress + path
|
||||||
},
|
},
|
||||||
"blogrelative": func(blog *configBlog, path string) string {
|
"blogrelative": func(blog *configBlog, path string) string {
|
||||||
return blog.getRelativePath(path)
|
return blog.getRelativePath(path)
|
||||||
|
@ -161,7 +160,7 @@ func initRendering() error {
|
||||||
return parsed
|
return parsed
|
||||||
},
|
},
|
||||||
"mentions": func(absolute string) []*mention {
|
"mentions": func(absolute string) []*mention {
|
||||||
mentions, _ := getWebmentions(&webmentionsRequestConfig{
|
mentions, _ := a.db.getWebmentions(&webmentionsRequestConfig{
|
||||||
target: absolute,
|
target: absolute,
|
||||||
status: webmentionStatusApproved,
|
status: webmentionStatusApproved,
|
||||||
asc: true,
|
asc: true,
|
||||||
|
@ -181,7 +180,7 @@ func initRendering() error {
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
},
|
},
|
||||||
"geotitle": geoTitle,
|
"geotitle": a.db.geoTitle,
|
||||||
}
|
}
|
||||||
|
|
||||||
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))
|
||||||
|
@ -194,7 +193,7 @@ func initRendering() error {
|
||||||
}
|
}
|
||||||
if info.Mode().IsRegular() && path.Ext(p) == templatesExt {
|
if info.Mode().IsRegular() && path.Ext(p) == templatesExt {
|
||||||
if name := strings.TrimSuffix(path.Base(p), templatesExt); name != templateBase {
|
if name := strings.TrimSuffix(path.Base(p), templatesExt); name != templateBase {
|
||||||
if templates[name], err = template.Must(baseTemplate.Clone()).New(name).ParseFiles(p); err != nil {
|
if a.templates[name], err = template.Must(baseTemplate.Clone()).New(name).ParseFiles(p); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -219,26 +218,26 @@ type renderData struct {
|
||||||
TorUsed bool
|
TorUsed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func render(w http.ResponseWriter, r *http.Request, template string, data *renderData) {
|
func (a *goBlog) render(w http.ResponseWriter, r *http.Request, template string, data *renderData) {
|
||||||
// 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.Blog == nil {
|
if data.Blog == nil {
|
||||||
if len(data.BlogString) == 0 {
|
if len(data.BlogString) == 0 {
|
||||||
data.BlogString = appConfig.DefaultBlog
|
data.BlogString = a.cfg.DefaultBlog
|
||||||
}
|
}
|
||||||
data.Blog = appConfig.Blogs[data.BlogString]
|
data.Blog = a.cfg.Blogs[data.BlogString]
|
||||||
}
|
}
|
||||||
if data.BlogString == "" {
|
if data.BlogString == "" {
|
||||||
for s, b := range appConfig.Blogs {
|
for s, b := range a.cfg.Blogs {
|
||||||
if b == data.Blog {
|
if b == data.Blog {
|
||||||
data.BlogString = s
|
data.BlogString = s
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if appConfig.Server.Tor && torAddress != "" {
|
if a.cfg.Server.Tor && a.torAddress != "" {
|
||||||
data.TorAddress = fmt.Sprintf("http://%v%v", torAddress, r.RequestURI)
|
data.TorAddress = fmt.Sprintf("http://%v%v", a.torAddress, r.RequestURI)
|
||||||
}
|
}
|
||||||
if data.Data == nil {
|
if data.Data == nil {
|
||||||
data.Data = map[string]interface{}{}
|
data.Data = map[string]interface{}{}
|
||||||
|
@ -250,23 +249,25 @@ func render(w http.ResponseWriter, r *http.Request, template string, data *rende
|
||||||
// Check if comments enabled
|
// Check if comments enabled
|
||||||
data.CommentsEnabled = data.Blog.Comments != nil && data.Blog.Comments.Enabled
|
data.CommentsEnabled = data.Blog.Comments != nil && data.Blog.Comments.Enabled
|
||||||
// Check if able to receive webmentions
|
// Check if able to receive webmentions
|
||||||
data.WebmentionReceivingEnabled = appConfig.Webmention == nil || !appConfig.Webmention.DisableReceiving
|
data.WebmentionReceivingEnabled = a.cfg.Webmention == nil || !a.cfg.Webmention.DisableReceiving
|
||||||
// Check if Tor request
|
// Check if Tor request
|
||||||
if torUsed, ok := r.Context().Value(torUsedKey).(bool); ok && torUsed {
|
if torUsed, ok := r.Context().Value(torUsedKey).(bool); ok && torUsed {
|
||||||
data.TorUsed = true
|
data.TorUsed = true
|
||||||
}
|
}
|
||||||
|
// Set content type
|
||||||
|
w.Header().Set(contentType, contentTypeHTMLUTF8)
|
||||||
// Minify and write response
|
// Minify and write response
|
||||||
mw := minifier.Writer(contentTypeHTML, w)
|
var tw bytes.Buffer
|
||||||
defer func() {
|
err := a.templates[template].ExecuteTemplate(&tw, template, data)
|
||||||
_ = mw.Close()
|
if err != nil {
|
||||||
}()
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
err := templates[template].ExecuteTemplate(mw, template, data)
|
return
|
||||||
|
}
|
||||||
|
_, err = writeMinified(w, contentTypeHTML, tw.Bytes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Set content type
|
|
||||||
w.Header().Set(contentType, contentTypeHTMLUTF8)
|
|
||||||
// Server timing
|
// Server timing
|
||||||
t.Stop()
|
t.Stop()
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,8 @@ import (
|
||||||
"github.com/thoas/go-funk"
|
"github.com/thoas/go-funk"
|
||||||
)
|
)
|
||||||
|
|
||||||
func geoTitle(lat, lon float64, lang string) string {
|
func (db *database) geoTitle(lat, lon float64, lang string) string {
|
||||||
ba, err := photonReverse(lat, lon, lang)
|
ba, err := db.photonReverse(lat, lon, lang)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -29,9 +29,9 @@ func geoTitle(lat, lon float64, lang string) string {
|
||||||
return strings.Join(funk.FilterString([]string{name, city, state, country}, func(s string) bool { return s != "" }), ", ")
|
return strings.Join(funk.FilterString([]string{name, city, state, country}, func(s string) bool { return s != "" }), ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func photonReverse(lat, lon float64, lang string) ([]byte, error) {
|
func (db *database) photonReverse(lat, lon float64, lang string) ([]byte, error) {
|
||||||
cacheKey := fmt.Sprintf("photon-%v-%v-%v", lat, lon, lang)
|
cacheKey := fmt.Sprintf("photon-%v-%v-%v", lat, lon, lang)
|
||||||
cache, _ := retrievePersistentCache(cacheKey)
|
cache, _ := db.retrievePersistentCache(cacheKey)
|
||||||
if cache != nil {
|
if cache != nil {
|
||||||
return cache, nil
|
return cache, nil
|
||||||
}
|
}
|
||||||
|
@ -63,6 +63,6 @@ func photonReverse(lat, lon float64, lang string) ([]byte, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
_ = cachePersistently(cacheKey, ba)
|
_ = db.cachePersistently(cacheKey, ba)
|
||||||
return ba, nil
|
return ba, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,8 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func serveRobotsTXT(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveRobotsTXT(w http.ResponseWriter, r *http.Request) {
|
||||||
_, _ = w.Write([]byte(fmt.Sprintf("User-agent: *\nSitemap: %v", appConfig.Server.PublicAddress+sitemapPath)))
|
_, _ = w.Write([]byte(fmt.Sprintf("User-agent: *\nSitemap: %v", a.cfg.Server.PublicAddress+sitemapPath)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func servePrivateRobotsTXT(w http.ResponseWriter, r *http.Request) {
|
func servePrivateRobotsTXT(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
12
search.go
12
search.go
|
@ -11,26 +11,26 @@ import (
|
||||||
|
|
||||||
const searchPlaceholder = "{search}"
|
const searchPlaceholder = "{search}"
|
||||||
|
|
||||||
func serveSearch(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveSearch(w http.ResponseWriter, r *http.Request) {
|
||||||
blog := r.Context().Value(blogContextKey).(string)
|
blog := r.Context().Value(blogContextKey).(string)
|
||||||
servePath := r.Context().Value(pathContextKey).(string)
|
servePath := r.Context().Value(pathContextKey).(string)
|
||||||
err := r.ParseForm()
|
err := r.ParseForm()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusBadRequest)
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if q := r.Form.Get("q"); q != "" {
|
if q := r.Form.Get("q"); q != "" {
|
||||||
http.Redirect(w, r, path.Join(servePath, searchEncode(q)), http.StatusFound)
|
http.Redirect(w, r, path.Join(servePath, searchEncode(q)), http.StatusFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
render(w, r, templateSearch, &renderData{
|
a.render(w, r, templateSearch, &renderData{
|
||||||
BlogString: blog,
|
BlogString: blog,
|
||||||
Canonical: appConfig.Server.PublicAddress + servePath,
|
Canonical: a.cfg.Server.PublicAddress + servePath,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveSearchResult(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveSearchResult(w http.ResponseWriter, r *http.Request) {
|
||||||
serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{
|
a.serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{
|
||||||
path: r.Context().Value(pathContextKey).(string) + "/" + searchPlaceholder,
|
path: r.Context().Value(pathContextKey).(string) + "/" + searchPlaceholder,
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
29
sessions.go
29
sessions.go
|
@ -13,46 +13,47 @@ import (
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
)
|
)
|
||||||
|
|
||||||
var loginSessionsStore, captchaSessionsStore *dbSessionStore
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
sessionCreatedOn = "created"
|
sessionCreatedOn = "created"
|
||||||
sessionModifiedOn = "modified"
|
sessionModifiedOn = "modified"
|
||||||
sessionExpiresOn = "expires"
|
sessionExpiresOn = "expires"
|
||||||
)
|
)
|
||||||
|
|
||||||
func initSessions() {
|
func (a *goBlog) initSessions() {
|
||||||
deleteExpiredSessions := func() {
|
deleteExpiredSessions := func() {
|
||||||
if _, err := appDb.exec("delete from sessions where expires < @now",
|
if _, err := a.db.exec("delete from sessions where expires < @now",
|
||||||
sql.Named("now", time.Now().Local().String())); err != nil {
|
sql.Named("now", time.Now().Local().String())); err != nil {
|
||||||
log.Println("Failed to delete expired sessions:", err.Error())
|
log.Println("Failed to delete expired sessions:", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
deleteExpiredSessions()
|
deleteExpiredSessions()
|
||||||
hourlyHooks = append(hourlyHooks, deleteExpiredSessions)
|
hourlyHooks = append(hourlyHooks, deleteExpiredSessions)
|
||||||
loginSessionsStore = &dbSessionStore{
|
a.loginSessions = &dbSessionStore{
|
||||||
codecs: securecookie.CodecsFromPairs(jwtKey()),
|
codecs: securecookie.CodecsFromPairs(a.jwtKey()),
|
||||||
options: &sessions.Options{
|
options: &sessions.Options{
|
||||||
Secure: httpsConfigured(),
|
Secure: a.httpsConfigured(),
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteLaxMode,
|
||||||
MaxAge: int((7 * 24 * time.Hour).Seconds()),
|
MaxAge: int((7 * 24 * time.Hour).Seconds()),
|
||||||
},
|
},
|
||||||
|
db: a.db,
|
||||||
}
|
}
|
||||||
captchaSessionsStore = &dbSessionStore{
|
a.captchaSessions = &dbSessionStore{
|
||||||
codecs: securecookie.CodecsFromPairs(jwtKey()),
|
codecs: securecookie.CodecsFromPairs(a.jwtKey()),
|
||||||
options: &sessions.Options{
|
options: &sessions.Options{
|
||||||
Secure: httpsConfigured(),
|
Secure: a.httpsConfigured(),
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteLaxMode,
|
||||||
MaxAge: int((24 * time.Hour).Seconds()),
|
MaxAge: int((24 * time.Hour).Seconds()),
|
||||||
},
|
},
|
||||||
|
db: a.db,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type dbSessionStore struct {
|
type dbSessionStore struct {
|
||||||
options *sessions.Options
|
options *sessions.Options
|
||||||
codecs []securecookie.Codec
|
codecs []securecookie.Codec
|
||||||
|
db *database
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *dbSessionStore) Get(r *http.Request, name string) (*sessions.Session, error) {
|
func (s *dbSessionStore) Get(r *http.Request, name string) (*sessions.Session, error) {
|
||||||
|
@ -101,14 +102,14 @@ func (s *dbSessionStore) Delete(r *http.Request, w http.ResponseWriter, session
|
||||||
for k := range session.Values {
|
for k := range session.Values {
|
||||||
delete(session.Values, k)
|
delete(session.Values, k)
|
||||||
}
|
}
|
||||||
if _, err := appDb.exec("delete from sessions where id = @id", sql.Named("id", session.ID)); err != nil {
|
if _, err := s.db.exec("delete from sessions where id = @id", sql.Named("id", session.ID)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *dbSessionStore) load(session *sessions.Session) (err error) {
|
func (s *dbSessionStore) load(session *sessions.Session) (err error) {
|
||||||
row, err := appDb.queryRow("select data, created, modified, expires from sessions where id = @id", sql.Named("id", session.ID))
|
row, err := s.db.queryRow("select data, created, modified, expires from sessions where id = @id", sql.Named("id", session.ID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -144,7 +145,7 @@ func (s *dbSessionStore) insert(session *sessions.Session) (err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
res, err := appDb.exec("insert into sessions(data, created, modified, expires) values(@data, @created, @modified, @expires)",
|
res, err := s.db.exec("insert into sessions(data, created, modified, expires) values(@data, @created, @modified, @expires)",
|
||||||
sql.Named("data", encoded), sql.Named("created", created.Local().String()), sql.Named("modified", modified.Local().String()), sql.Named("expires", expires.Local().String()))
|
sql.Named("data", encoded), sql.Named("created", created.Local().String()), sql.Named("modified", modified.Local().String()), sql.Named("expires", expires.Local().String()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -168,7 +169,7 @@ func (s *dbSessionStore) save(session *sessions.Session) (err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = appDb.exec("update sessions set data = @data, modified = @modified where id = @id",
|
_, err = s.db.exec("update sessions set data = @data, modified = @modified where id = @id",
|
||||||
sql.Named("data", encoded), sql.Named("modified", time.Now().Local().String()), sql.Named("id", session.ID))
|
sql.Named("data", encoded), sql.Named("modified", time.Now().Local().String()), sql.Named("id", session.ID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -4,10 +4,10 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func redirectShortDomain(next http.Handler) http.Handler {
|
func (a *goBlog) redirectShortDomain(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 appConfig.Server.shortPublicHostname != "" && r.Host == appConfig.Server.shortPublicHostname {
|
if a.cfg.Server.shortPublicHostname != "" && r.Host == a.cfg.Server.shortPublicHostname {
|
||||||
http.Redirect(rw, r, appConfig.Server.PublicAddress+r.RequestURI, http.StatusMovedPermanently)
|
http.Redirect(rw, r, a.cfg.Server.PublicAddress+r.RequestURI, http.StatusMovedPermanently)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
next.ServeHTTP(rw, r)
|
next.ServeHTTP(rw, r)
|
||||||
|
|
22
shortPath.go
22
shortPath.go
|
@ -10,17 +10,17 @@ import (
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
func shortenPath(p string) (string, error) {
|
func (db *database) shortenPath(p string) (string, error) {
|
||||||
if p == "" {
|
if p == "" {
|
||||||
return "", errors.New("empty path")
|
return "", errors.New("empty path")
|
||||||
}
|
}
|
||||||
id := getShortPathID(p)
|
id := db.getShortPathID(p)
|
||||||
if id == -1 {
|
if id == -1 {
|
||||||
_, err := appDb.exec("insert or ignore into shortpath (path) values (@path)", sql.Named("path", p))
|
_, err := db.exec("insert or ignore into shortpath (path) values (@path)", sql.Named("path", p))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
id = getShortPathID(p)
|
id = db.getShortPathID(p)
|
||||||
}
|
}
|
||||||
if id == -1 {
|
if id == -1 {
|
||||||
return "", errors.New("failed to retrieve short path for " + p)
|
return "", errors.New("failed to retrieve short path for " + p)
|
||||||
|
@ -28,11 +28,11 @@ func shortenPath(p string) (string, error) {
|
||||||
return fmt.Sprintf("/s/%x", id), nil
|
return fmt.Sprintf("/s/%x", id), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getShortPathID(p string) (id int) {
|
func (db *database) getShortPathID(p string) (id int) {
|
||||||
if p == "" {
|
if p == "" {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
row, err := appDb.queryRow("select id from shortpath where path = @path", sql.Named("path", p))
|
row, err := db.queryRow("select id from shortpath where path = @path", sql.Named("path", p))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
@ -43,21 +43,21 @@ func getShortPathID(p string) (id int) {
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
func redirectToLongPath(rw http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) redirectToLongPath(rw http.ResponseWriter, r *http.Request) {
|
||||||
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 16, 64)
|
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 16, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serve404(rw, r)
|
a.serve404(rw, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
row, err := appDb.queryRow("select path from shortpath where id = @id", sql.Named("id", id))
|
row, err := a.db.queryRow("select path from shortpath where id = @id", sql.Named("id", id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serve404(rw, r)
|
a.serve404(rw, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var path string
|
var path string
|
||||||
err = row.Scan(&path)
|
err = row.Scan(&path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serve404(rw, r)
|
a.serve404(rw, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Redirect(rw, r, path, http.StatusMovedPermanently)
|
http.Redirect(rw, r, path, http.StatusMovedPermanently)
|
||||||
|
|
40
sitemap.go
40
sitemap.go
|
@ -11,24 +11,24 @@ import (
|
||||||
|
|
||||||
const sitemapPath = "/sitemap.xml"
|
const sitemapPath = "/sitemap.xml"
|
||||||
|
|
||||||
func serveSitemap(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveSitemap(w http.ResponseWriter, r *http.Request) {
|
||||||
sm := sitemap.New()
|
sm := sitemap.New()
|
||||||
sm.Minify = true
|
sm.Minify = true
|
||||||
// Blogs
|
// Blogs
|
||||||
for b, bc := range appConfig.Blogs {
|
for b, bc := range a.cfg.Blogs {
|
||||||
// Blog
|
// Blog
|
||||||
blogPath := bc.Path
|
blogPath := bc.Path
|
||||||
if blogPath == "/" {
|
if blogPath == "/" {
|
||||||
blogPath = ""
|
blogPath = ""
|
||||||
}
|
}
|
||||||
sm.Add(&sitemap.URL{
|
sm.Add(&sitemap.URL{
|
||||||
Loc: appConfig.Server.PublicAddress + blogPath,
|
Loc: a.cfg.Server.PublicAddress + blogPath,
|
||||||
})
|
})
|
||||||
// Sections
|
// Sections
|
||||||
for _, section := range bc.Sections {
|
for _, section := range bc.Sections {
|
||||||
if section.Name != "" {
|
if section.Name != "" {
|
||||||
sm.Add(&sitemap.URL{
|
sm.Add(&sitemap.URL{
|
||||||
Loc: appConfig.Server.PublicAddress + bc.getRelativePath("/"+section.Name),
|
Loc: a.cfg.Server.PublicAddress + bc.getRelativePath("/"+section.Name),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,27 +38,27 @@ func serveSitemap(w http.ResponseWriter, r *http.Request) {
|
||||||
// Taxonomy
|
// Taxonomy
|
||||||
taxPath := bc.getRelativePath("/" + taxonomy.Name)
|
taxPath := bc.getRelativePath("/" + taxonomy.Name)
|
||||||
sm.Add(&sitemap.URL{
|
sm.Add(&sitemap.URL{
|
||||||
Loc: appConfig.Server.PublicAddress + taxPath,
|
Loc: a.cfg.Server.PublicAddress + taxPath,
|
||||||
})
|
})
|
||||||
// Values
|
// Values
|
||||||
if taxValues, err := allTaxonomyValues(b, taxonomy.Name); err == nil {
|
if taxValues, err := a.db.allTaxonomyValues(b, taxonomy.Name); err == nil {
|
||||||
for _, tv := range taxValues {
|
for _, tv := range taxValues {
|
||||||
sm.Add(&sitemap.URL{
|
sm.Add(&sitemap.URL{
|
||||||
Loc: appConfig.Server.PublicAddress + taxPath + "/" + urlize(tv),
|
Loc: a.cfg.Server.PublicAddress + taxPath + "/" + urlize(tv),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Year / month archives
|
// Year / month archives
|
||||||
if dates, err := allPublishedDates(b); err == nil {
|
if dates, err := a.db.allPublishedDates(b); err == nil {
|
||||||
already := map[string]bool{}
|
already := map[string]bool{}
|
||||||
for _, d := range dates {
|
for _, d := range dates {
|
||||||
// Year
|
// Year
|
||||||
yearPath := bc.getRelativePath("/" + fmt.Sprintf("%0004d", d.year))
|
yearPath := bc.getRelativePath("/" + fmt.Sprintf("%0004d", d.year))
|
||||||
if !already[yearPath] {
|
if !already[yearPath] {
|
||||||
sm.Add(&sitemap.URL{
|
sm.Add(&sitemap.URL{
|
||||||
Loc: appConfig.Server.PublicAddress + yearPath,
|
Loc: a.cfg.Server.PublicAddress + yearPath,
|
||||||
})
|
})
|
||||||
already[yearPath] = true
|
already[yearPath] = true
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,7 @@ func serveSitemap(w http.ResponseWriter, r *http.Request) {
|
||||||
monthPath := yearPath + "/" + fmt.Sprintf("%02d", d.month)
|
monthPath := yearPath + "/" + fmt.Sprintf("%02d", d.month)
|
||||||
if !already[monthPath] {
|
if !already[monthPath] {
|
||||||
sm.Add(&sitemap.URL{
|
sm.Add(&sitemap.URL{
|
||||||
Loc: appConfig.Server.PublicAddress + monthPath,
|
Loc: a.cfg.Server.PublicAddress + monthPath,
|
||||||
})
|
})
|
||||||
already[monthPath] = true
|
already[monthPath] = true
|
||||||
}
|
}
|
||||||
|
@ -74,7 +74,7 @@ func serveSitemap(w http.ResponseWriter, r *http.Request) {
|
||||||
dayPath := monthPath + "/" + fmt.Sprintf("%02d", d.day)
|
dayPath := monthPath + "/" + fmt.Sprintf("%02d", d.day)
|
||||||
if !already[dayPath] {
|
if !already[dayPath] {
|
||||||
sm.Add(&sitemap.URL{
|
sm.Add(&sitemap.URL{
|
||||||
Loc: appConfig.Server.PublicAddress + dayPath,
|
Loc: a.cfg.Server.PublicAddress + dayPath,
|
||||||
})
|
})
|
||||||
already[dayPath] = true
|
already[dayPath] = true
|
||||||
}
|
}
|
||||||
|
@ -82,7 +82,7 @@ func serveSitemap(w http.ResponseWriter, r *http.Request) {
|
||||||
genericMonthPath := blogPath + "/x/" + fmt.Sprintf("%02d", d.month)
|
genericMonthPath := blogPath + "/x/" + fmt.Sprintf("%02d", d.month)
|
||||||
if !already[genericMonthPath] {
|
if !already[genericMonthPath] {
|
||||||
sm.Add(&sitemap.URL{
|
sm.Add(&sitemap.URL{
|
||||||
Loc: appConfig.Server.PublicAddress + genericMonthPath,
|
Loc: a.cfg.Server.PublicAddress + genericMonthPath,
|
||||||
})
|
})
|
||||||
already[genericMonthPath] = true
|
already[genericMonthPath] = true
|
||||||
}
|
}
|
||||||
|
@ -90,7 +90,7 @@ func serveSitemap(w http.ResponseWriter, r *http.Request) {
|
||||||
genericMonthDayPath := genericMonthPath + "/" + fmt.Sprintf("%02d", d.day)
|
genericMonthDayPath := genericMonthPath + "/" + fmt.Sprintf("%02d", d.day)
|
||||||
if !already[genericMonthDayPath] {
|
if !already[genericMonthDayPath] {
|
||||||
sm.Add(&sitemap.URL{
|
sm.Add(&sitemap.URL{
|
||||||
Loc: appConfig.Server.PublicAddress + genericMonthDayPath,
|
Loc: a.cfg.Server.PublicAddress + genericMonthDayPath,
|
||||||
})
|
})
|
||||||
already[genericMonthDayPath] = true
|
already[genericMonthDayPath] = true
|
||||||
}
|
}
|
||||||
|
@ -99,38 +99,38 @@ func serveSitemap(w http.ResponseWriter, r *http.Request) {
|
||||||
// Photos
|
// Photos
|
||||||
if bc.Photos != nil && bc.Photos.Enabled {
|
if bc.Photos != nil && bc.Photos.Enabled {
|
||||||
sm.Add(&sitemap.URL{
|
sm.Add(&sitemap.URL{
|
||||||
Loc: appConfig.Server.PublicAddress + bc.getRelativePath(bc.Photos.Path),
|
Loc: a.cfg.Server.PublicAddress + bc.getRelativePath(bc.Photos.Path),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Search
|
// Search
|
||||||
if bc.Search != nil && bc.Search.Enabled {
|
if bc.Search != nil && bc.Search.Enabled {
|
||||||
sm.Add(&sitemap.URL{
|
sm.Add(&sitemap.URL{
|
||||||
Loc: appConfig.Server.PublicAddress + bc.getRelativePath(bc.Search.Path),
|
Loc: a.cfg.Server.PublicAddress + bc.getRelativePath(bc.Search.Path),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Stats
|
// Stats
|
||||||
if bc.BlogStats != nil && bc.BlogStats.Enabled {
|
if bc.BlogStats != nil && bc.BlogStats.Enabled {
|
||||||
sm.Add(&sitemap.URL{
|
sm.Add(&sitemap.URL{
|
||||||
Loc: appConfig.Server.PublicAddress + bc.getRelativePath(bc.BlogStats.Path),
|
Loc: a.cfg.Server.PublicAddress + bc.getRelativePath(bc.BlogStats.Path),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Blogroll
|
// Blogroll
|
||||||
if bc.Blogroll != nil && bc.Blogroll.Enabled {
|
if bc.Blogroll != nil && bc.Blogroll.Enabled {
|
||||||
sm.Add(&sitemap.URL{
|
sm.Add(&sitemap.URL{
|
||||||
Loc: appConfig.Server.PublicAddress + bc.getRelativePath(bc.Blogroll.Path),
|
Loc: a.cfg.Server.PublicAddress + bc.getRelativePath(bc.Blogroll.Path),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Custom pages
|
// Custom pages
|
||||||
for _, cp := range bc.CustomPages {
|
for _, cp := range bc.CustomPages {
|
||||||
sm.Add(&sitemap.URL{
|
sm.Add(&sitemap.URL{
|
||||||
Loc: appConfig.Server.PublicAddress + cp.Path,
|
Loc: a.cfg.Server.PublicAddress + cp.Path,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Posts
|
// Posts
|
||||||
if posts, err := getPosts(&postsRequestConfig{status: statusPublished}); err == nil {
|
if posts, err := a.db.getPosts(&postsRequestConfig{status: statusPublished}); err == nil {
|
||||||
for _, p := range posts {
|
for _, p := range posts {
|
||||||
item := &sitemap.URL{Loc: p.fullURL()}
|
item := &sitemap.URL{Loc: a.fullPostURL(p)}
|
||||||
var lastMod time.Time
|
var lastMod time.Time
|
||||||
if p.Updated != "" {
|
if p.Updated != "" {
|
||||||
lastMod, _ = dateparse.ParseLocal(p.Updated)
|
lastMod, _ = dateparse.ParseLocal(p.Updated)
|
||||||
|
|
|
@ -28,7 +28,7 @@ func allStaticPaths() (paths []string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets only called by registered paths
|
// Gets only called by registered paths
|
||||||
func serveStaticFile(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveStaticFile(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Cache-Control", fmt.Sprintf("public,max-age=%d,s-max-age=%d,stale-while-revalidate=%d", appConfig.Cache.Expiration, appConfig.Cache.Expiration/3, appConfig.Cache.Expiration))
|
w.Header().Set("Cache-Control", fmt.Sprintf("public,max-age=%d,s-max-age=%d,stale-while-revalidate=%d", a.cfg.Cache.Expiration, a.cfg.Cache.Expiration/3, a.cfg.Cache.Expiration))
|
||||||
http.ServeFile(w, r, filepath.Join(staticFolder, r.URL.Path))
|
http.ServeFile(w, r, filepath.Join(staticFolder, r.URL.Path))
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,17 +4,17 @@ import "net/http"
|
||||||
|
|
||||||
const taxonomyContextKey = "taxonomy"
|
const taxonomyContextKey = "taxonomy"
|
||||||
|
|
||||||
func serveTaxonomy(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveTaxonomy(w http.ResponseWriter, r *http.Request) {
|
||||||
blog := r.Context().Value(blogContextKey).(string)
|
blog := r.Context().Value(blogContextKey).(string)
|
||||||
tax := r.Context().Value(taxonomyContextKey).(*taxonomy)
|
tax := r.Context().Value(taxonomyContextKey).(*taxonomy)
|
||||||
allValues, err := allTaxonomyValues(blog, tax.Name)
|
allValues, err := a.db.allTaxonomyValues(blog, tax.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
render(w, r, templateTaxonomy, &renderData{
|
a.render(w, r, templateTaxonomy, &renderData{
|
||||||
BlogString: blog,
|
BlogString: blog,
|
||||||
Canonical: appConfig.Server.PublicAddress + r.URL.Path,
|
Canonical: a.cfg.Server.PublicAddress + r.URL.Path,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"Taxonomy": tax,
|
"Taxonomy": tax,
|
||||||
"ValueGroups": groupStrings(allValues),
|
"ValueGroups": groupStrings(allValues),
|
||||||
|
|
21
telegram.go
21
telegram.go
|
@ -13,40 +13,39 @@ import (
|
||||||
|
|
||||||
const telegramBaseURL = "https://api.telegram.org/bot"
|
const telegramBaseURL = "https://api.telegram.org/bot"
|
||||||
|
|
||||||
func initTelegram() {
|
func (a *goBlog) initTelegram() {
|
||||||
enable := false
|
enable := false
|
||||||
for _, b := range appConfig.Blogs {
|
for _, b := range a.cfg.Blogs {
|
||||||
if tg := b.Telegram; tg != nil && tg.Enabled && tg.BotToken != "" && tg.ChatID != "" {
|
if tg := b.Telegram; tg != nil && tg.Enabled && tg.BotToken != "" && tg.ChatID != "" {
|
||||||
enable = true
|
enable = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if enable {
|
if enable {
|
||||||
postPostHooks = append(postPostHooks, func(p *post) {
|
a.pPostHooks = append(a.pPostHooks, func(p *post) {
|
||||||
if p.isPublishedSectionPost() {
|
if p.isPublishedSectionPost() {
|
||||||
p.tgPost()
|
tgPost(a.cfg.Blogs[p.Blog].Telegram, p.title(), a.fullPostURL(p), a.shortPostURL(p))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *post) tgPost() {
|
func tgPost(tg *configTelegram, title, fullURL, shortURL string) {
|
||||||
tg := appConfig.Blogs[p.Blog].Telegram
|
|
||||||
if tg == nil || !tg.Enabled || tg.BotToken == "" || tg.ChatID == "" {
|
if tg == nil || !tg.Enabled || tg.BotToken == "" || tg.ChatID == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
replacer := strings.NewReplacer("<", "<", ">", ">", "&", "&")
|
replacer := strings.NewReplacer("<", "<", ">", ">", "&", "&")
|
||||||
var message bytes.Buffer
|
var message bytes.Buffer
|
||||||
if title := p.title(); title != "" {
|
if title != "" {
|
||||||
message.WriteString(replacer.Replace(title))
|
message.WriteString(replacer.Replace(title))
|
||||||
message.WriteString("\n\n")
|
message.WriteString("\n\n")
|
||||||
}
|
}
|
||||||
if tg.InstantViewHash != "" {
|
if tg.InstantViewHash != "" {
|
||||||
message.WriteString("<a href=\"https://t.me/iv?rhash=" + tg.InstantViewHash + "&url=" + url.QueryEscape(p.fullURL()) + "\">")
|
message.WriteString("<a href=\"https://t.me/iv?rhash=" + tg.InstantViewHash + "&url=" + url.QueryEscape(fullURL) + "\">")
|
||||||
message.WriteString(replacer.Replace(p.shortURL()))
|
message.WriteString(replacer.Replace(shortURL))
|
||||||
message.WriteString("</a>")
|
message.WriteString("</a>")
|
||||||
} else {
|
} else {
|
||||||
message.WriteString("<a href=\"" + p.shortURL() + "\">")
|
message.WriteString("<a href=\"" + shortURL + "\">")
|
||||||
message.WriteString(replacer.Replace(p.shortURL()))
|
message.WriteString(replacer.Replace(shortURL))
|
||||||
message.WriteString("</a>")
|
message.WriteString("</a>")
|
||||||
}
|
}
|
||||||
if err := sendTelegramMessage(message.String(), "HTML", tg.BotToken, tg.ChatID); err != nil {
|
if err := sendTelegramMessage(message.String(), "HTML", tg.BotToken, tg.ChatID); err != nil {
|
||||||
|
|
|
@ -16,24 +16,23 @@ import (
|
||||||
|
|
||||||
const assetsFolder = "templates/assets"
|
const assetsFolder = "templates/assets"
|
||||||
|
|
||||||
var assetFileNames map[string]string = map[string]string{}
|
|
||||||
var assetFiles map[string]*assetFile = map[string]*assetFile{}
|
|
||||||
|
|
||||||
type assetFile struct {
|
type assetFile struct {
|
||||||
contentType string
|
contentType string
|
||||||
sri string
|
sri string
|
||||||
body []byte
|
body []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func initTemplateAssets() (err error) {
|
func (a *goBlog) initTemplateAssets() (err error) {
|
||||||
|
a.assetFileNames = map[string]string{}
|
||||||
|
a.assetFiles = map[string]*assetFile{}
|
||||||
err = filepath.Walk(assetsFolder, func(path string, info os.FileInfo, err error) error {
|
err = filepath.Walk(assetsFolder, func(path string, info os.FileInfo, err error) error {
|
||||||
if info.Mode().IsRegular() {
|
if info.Mode().IsRegular() {
|
||||||
compiled, err := compileAsset(path)
|
compiled, err := a.compileAsset(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if compiled != "" {
|
if compiled != "" {
|
||||||
assetFileNames[strings.TrimPrefix(path, assetsFolder+"/")] = compiled
|
a.assetFileNames[strings.TrimPrefix(path, assetsFolder+"/")] = compiled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -44,21 +43,22 @@ func initTemplateAssets() (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func compileAsset(name string) (string, error) {
|
func (a *goBlog) compileAsset(name string) (string, error) {
|
||||||
content, err := os.ReadFile(name)
|
content, err := os.ReadFile(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
ext := path.Ext(name)
|
ext := path.Ext(name)
|
||||||
compiledExt := ext
|
compiledExt := ext
|
||||||
|
m := getMinifier()
|
||||||
switch ext {
|
switch ext {
|
||||||
case ".js":
|
case ".js":
|
||||||
content, err = minifier.Bytes("application/javascript", content)
|
content, err = m.Bytes("application/javascript", content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
case ".css":
|
case ".css":
|
||||||
content, err = minifier.Bytes("text/css", content)
|
content, err = m.Bytes("text/css", content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@ func compileAsset(name string) (string, error) {
|
||||||
// SRI
|
// SRI
|
||||||
sriHash := fmt.Sprintf("sha512-%s", base64.StdEncoding.EncodeToString(sha512Hash.Sum(nil)))
|
sriHash := fmt.Sprintf("sha512-%s", base64.StdEncoding.EncodeToString(sha512Hash.Sum(nil)))
|
||||||
// Create struct
|
// Create struct
|
||||||
assetFiles[compiledFileName] = &assetFile{
|
a.assetFiles[compiledFileName] = &assetFile{
|
||||||
contentType: mime.TypeByExtension(compiledExt),
|
contentType: mime.TypeByExtension(compiledExt),
|
||||||
sri: sriHash,
|
sri: sriHash,
|
||||||
body: content,
|
body: content,
|
||||||
|
@ -85,27 +85,27 @@ func compileAsset(name string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function for templates
|
// Function for templates
|
||||||
func assetFileName(fileName string) string {
|
func (a *goBlog) assetFileName(fileName string) string {
|
||||||
return "/" + assetFileNames[fileName]
|
return "/" + a.assetFileNames[fileName]
|
||||||
}
|
}
|
||||||
|
|
||||||
func assetSRI(fileName string) string {
|
func (a *goBlog) assetSRI(fileName string) string {
|
||||||
return assetFiles[assetFileNames[fileName]].sri
|
return a.assetFiles[a.assetFileNames[fileName]].sri
|
||||||
}
|
}
|
||||||
|
|
||||||
func allAssetPaths() []string {
|
func (a *goBlog) allAssetPaths() []string {
|
||||||
var paths []string
|
var paths []string
|
||||||
for _, name := range assetFileNames {
|
for _, name := range a.assetFileNames {
|
||||||
paths = append(paths, "/"+name)
|
paths = append(paths, "/"+name)
|
||||||
}
|
}
|
||||||
return paths
|
return paths
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets only called by registered paths
|
// Gets only called by registered paths
|
||||||
func serveAsset(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveAsset(w http.ResponseWriter, r *http.Request) {
|
||||||
af, ok := assetFiles[strings.TrimPrefix(r.URL.Path, "/")]
|
af, ok := a.assetFiles[strings.TrimPrefix(r.URL.Path, "/")]
|
||||||
if !ok {
|
if !ok {
|
||||||
serve404(w, r)
|
a.serve404(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Cache-Control", "public,max-age=31536000,immutable")
|
w.Header().Set("Cache-Control", "public,max-age=31536000,immutable")
|
||||||
|
|
|
@ -4,13 +4,11 @@ import (
|
||||||
ts "git.jlel.se/jlelse/template-strings"
|
ts "git.jlel.se/jlelse/template-strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var appTs *ts.TemplateStrings
|
func (a *goBlog) initTemplateStrings() (err error) {
|
||||||
|
|
||||||
func initTemplateStrings() (err error) {
|
|
||||||
var blogLangs []string
|
var blogLangs []string
|
||||||
for _, b := range appConfig.Blogs {
|
for _, b := range a.cfg.Blogs {
|
||||||
blogLangs = append(blogLangs, b.Lang)
|
blogLangs = append(blogLangs, b.Lang)
|
||||||
}
|
}
|
||||||
appTs, err = ts.InitTemplateStrings("templates/strings", ".yaml", "default", blogLangs...)
|
a.ts, err = ts.InitTemplateStrings("templates/strings", ".yaml", "default", blogLangs...)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
12
tor.go
12
tor.go
|
@ -16,13 +16,9 @@ import (
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
torAddress string
|
|
||||||
)
|
|
||||||
|
|
||||||
var torUsedKey requestContextKey = "tor"
|
var torUsedKey requestContextKey = "tor"
|
||||||
|
|
||||||
func 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")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -76,10 +72,10 @@ func startOnionService(h http.Handler) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer onion.Close()
|
defer onion.Close()
|
||||||
torAddress = onion.String()
|
a.torAddress = onion.String()
|
||||||
log.Println("Onion service published on http://" + torAddress)
|
log.Println("Onion service published on http://" + a.torAddress)
|
||||||
// Clear cache
|
// Clear cache
|
||||||
purgeCache()
|
a.cache.purge()
|
||||||
// Serve handler
|
// Serve handler
|
||||||
s := &http.Server{
|
s := &http.Server{
|
||||||
Handler: middleware.WithValue(torUsedKey, true)(h),
|
Handler: middleware.WithValue(torUsedKey, true)(h),
|
||||||
|
|
|
@ -30,32 +30,32 @@ type mention struct {
|
||||||
Status webmentionStatus
|
Status webmentionStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
func initWebmention() {
|
func (a *goBlog) initWebmention() {
|
||||||
// Add hooks
|
// Add hooks
|
||||||
hookFunc := func(p *post) {
|
hookFunc := func(p *post) {
|
||||||
if p.Status == statusPublished {
|
if p.Status == statusPublished {
|
||||||
_ = p.sendWebmentions()
|
_ = a.sendWebmentions(p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
postPostHooks = append(postPostHooks, hookFunc)
|
a.pPostHooks = append(a.pPostHooks, hookFunc)
|
||||||
postUpdateHooks = append(postUpdateHooks, hookFunc)
|
a.pUpdateHooks = append(a.pUpdateHooks, hookFunc)
|
||||||
postDeleteHooks = append(postDeleteHooks, hookFunc)
|
a.pDeleteHooks = append(a.pDeleteHooks, hookFunc)
|
||||||
// Start verifier
|
// Start verifier
|
||||||
initWebmentionQueue()
|
a.initWebmentionQueue()
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleWebmention(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) handleWebmention(w http.ResponseWriter, r *http.Request) {
|
||||||
m, err := extractMention(r)
|
m, err := extractMention(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusBadRequest)
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !isAllowedHost(httptest.NewRequest(http.MethodGet, m.Target, nil), appConfig.Server.publicHostname) {
|
if !isAllowedHost(httptest.NewRequest(http.MethodGet, m.Target, nil), a.cfg.Server.publicHostname) {
|
||||||
serveError(w, r, "target not allowed", http.StatusBadRequest)
|
a.serveError(w, r, "target not allowed", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = queueMention(m); err != nil {
|
if err = a.queueMention(m); err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusAccepted)
|
w.WriteHeader(http.StatusAccepted)
|
||||||
|
@ -82,9 +82,9 @@ func extractMention(r *http.Request) (*mention, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func webmentionExists(source, target string) bool {
|
func (db *database) webmentionExists(source, target string) bool {
|
||||||
result := 0
|
result := 0
|
||||||
row, err := appDb.queryRow("select exists(select 1 from webmentions where source = ? and target = ?)", source, target)
|
row, err := db.queryRow("select exists(select 1 from webmentions where source = ? and target = ?)", source, target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -94,26 +94,26 @@ func webmentionExists(source, target string) bool {
|
||||||
return result == 1
|
return result == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
func createWebmention(source, target string) (err error) {
|
func (a *goBlog) createWebmention(source, target string) (err error) {
|
||||||
return queueMention(&mention{
|
return a.queueMention(&mention{
|
||||||
Source: source,
|
Source: source,
|
||||||
Target: unescapedPath(target),
|
Target: unescapedPath(target),
|
||||||
Created: time.Now().Unix(),
|
Created: time.Now().Unix(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteWebmention(id int) error {
|
func (db *database) deleteWebmention(id int) error {
|
||||||
_, err := appDb.exec("delete from webmentions where id = @id", sql.Named("id", id))
|
_, err := db.exec("delete from webmentions where id = @id", sql.Named("id", id))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func approveWebmention(id int) error {
|
func (db *database) approveWebmention(id int) error {
|
||||||
_, err := appDb.exec("update webmentions set status = ? where id = ?", webmentionStatusApproved, id)
|
_, err := db.exec("update webmentions set status = ? where id = ?", webmentionStatusApproved, id)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func reverifyWebmention(id int) error {
|
func (a *goBlog) reverifyWebmention(id int) error {
|
||||||
m, err := getWebmentions(&webmentionsRequestConfig{
|
m, err := a.db.getWebmentions(&webmentionsRequestConfig{
|
||||||
id: id,
|
id: id,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
})
|
})
|
||||||
|
@ -121,7 +121,7 @@ func reverifyWebmention(id int) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(m) > 0 {
|
if len(m) > 0 {
|
||||||
err = queueMention(m[0])
|
err = a.queueMention(m[0])
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -169,10 +169,10 @@ func buildWebmentionsQuery(config *webmentionsRequestConfig) (query string, args
|
||||||
return query, args
|
return query, args
|
||||||
}
|
}
|
||||||
|
|
||||||
func getWebmentions(config *webmentionsRequestConfig) ([]*mention, error) {
|
func (db *database) getWebmentions(config *webmentionsRequestConfig) ([]*mention, error) {
|
||||||
mentions := []*mention{}
|
mentions := []*mention{}
|
||||||
query, args := buildWebmentionsQuery(config)
|
query, args := buildWebmentionsQuery(config)
|
||||||
rows, err := appDb.query(query, args...)
|
rows, err := db.query(query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -187,10 +187,10 @@ func getWebmentions(config *webmentionsRequestConfig) ([]*mention, error) {
|
||||||
return mentions, nil
|
return mentions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func 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 + ")"
|
||||||
row, err := appDb.queryRow(query, params...)
|
row, err := db.queryRow(query, params...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,11 +14,12 @@ import (
|
||||||
type webmentionPaginationAdapter struct {
|
type webmentionPaginationAdapter struct {
|
||||||
config *webmentionsRequestConfig
|
config *webmentionsRequestConfig
|
||||||
nums int64
|
nums int64
|
||||||
|
db *database
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *webmentionPaginationAdapter) Nums() (int64, error) {
|
func (p *webmentionPaginationAdapter) Nums() (int64, error) {
|
||||||
if p.nums == 0 {
|
if p.nums == 0 {
|
||||||
nums, _ := countWebmentions(p.config)
|
nums, _ := p.db.countWebmentions(p.config)
|
||||||
p.nums = int64(nums)
|
p.nums = int64(nums)
|
||||||
}
|
}
|
||||||
return p.nums, nil
|
return p.nums, nil
|
||||||
|
@ -29,12 +30,12 @@ func (p *webmentionPaginationAdapter) Slice(offset, length int, data interface{}
|
||||||
modifiedConfig.offset = offset
|
modifiedConfig.offset = offset
|
||||||
modifiedConfig.limit = length
|
modifiedConfig.limit = length
|
||||||
|
|
||||||
wms, err := getWebmentions(&modifiedConfig)
|
wms, err := p.db.getWebmentions(&modifiedConfig)
|
||||||
reflect.ValueOf(data).Elem().Set(reflect.ValueOf(&wms).Elem())
|
reflect.ValueOf(data).Elem().Set(reflect.ValueOf(&wms).Elem())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func webmentionAdmin(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) webmentionAdmin(w http.ResponseWriter, r *http.Request) {
|
||||||
pageNoString := chi.URLParam(r, "page")
|
pageNoString := chi.URLParam(r, "page")
|
||||||
pageNo, _ := strconv.Atoi(pageNoString)
|
pageNo, _ := strconv.Atoi(pageNoString)
|
||||||
var status webmentionStatus = ""
|
var status webmentionStatus = ""
|
||||||
|
@ -48,12 +49,12 @@ func webmentionAdmin(w http.ResponseWriter, r *http.Request) {
|
||||||
p := paginator.New(&webmentionPaginationAdapter{config: &webmentionsRequestConfig{
|
p := paginator.New(&webmentionPaginationAdapter{config: &webmentionsRequestConfig{
|
||||||
status: status,
|
status: status,
|
||||||
sourcelike: sourcelike,
|
sourcelike: sourcelike,
|
||||||
}}, 10)
|
}, db: a.db}, 10)
|
||||||
p.SetPage(pageNo)
|
p.SetPage(pageNo)
|
||||||
var mentions []*mention
|
var mentions []*mention
|
||||||
err := p.Results(&mentions)
|
err := p.Results(&mentions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Navigation
|
// Navigation
|
||||||
|
@ -91,7 +92,7 @@ func webmentionAdmin(w http.ResponseWriter, r *http.Request) {
|
||||||
query = "?" + params.Encode()
|
query = "?" + params.Encode()
|
||||||
}
|
}
|
||||||
// Render
|
// Render
|
||||||
render(w, r, templateWebmentionAdmin, &renderData{
|
a.render(w, r, templateWebmentionAdmin, &renderData{
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"Mentions": mentions,
|
"Mentions": mentions,
|
||||||
"HasPrev": hasPrev,
|
"HasPrev": hasPrev,
|
||||||
|
@ -102,45 +103,45 @@ func webmentionAdmin(w http.ResponseWriter, r *http.Request) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func webmentionAdminDelete(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) webmentionAdminDelete(w http.ResponseWriter, r *http.Request) {
|
||||||
id, err := strconv.Atoi(r.FormValue("mentionid"))
|
id, err := strconv.Atoi(r.FormValue("mentionid"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusBadRequest)
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = deleteWebmention(id)
|
err = a.db.deleteWebmention(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
purgeCache()
|
a.cache.purge()
|
||||||
http.Redirect(w, r, ".", http.StatusFound)
|
http.Redirect(w, r, ".", http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func webmentionAdminApprove(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) webmentionAdminApprove(w http.ResponseWriter, r *http.Request) {
|
||||||
id, err := strconv.Atoi(r.FormValue("mentionid"))
|
id, err := strconv.Atoi(r.FormValue("mentionid"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusBadRequest)
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = approveWebmention(id)
|
err = a.db.approveWebmention(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
purgeCache()
|
a.cache.purge()
|
||||||
http.Redirect(w, r, ".", http.StatusFound)
|
http.Redirect(w, r, ".", http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func webmentionAdminReverify(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) webmentionAdminReverify(w http.ResponseWriter, r *http.Request) {
|
||||||
id, err := strconv.Atoi(r.FormValue("mentionid"))
|
id, err := strconv.Atoi(r.FormValue("mentionid"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusBadRequest)
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = reverifyWebmention(id)
|
err = a.reverifyWebmention(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, ".", http.StatusFound)
|
http.Redirect(w, r, ".", http.StatusFound)
|
||||||
|
|
|
@ -13,40 +13,44 @@ import (
|
||||||
"github.com/tomnomnom/linkheader"
|
"github.com/tomnomnom/linkheader"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *post) sendWebmentions() error {
|
func (a *goBlog) sendWebmentions(p *post) error {
|
||||||
if wm := appConfig.Webmention; wm != nil && wm.DisableSending {
|
if wm := a.cfg.Webmention; wm != nil && wm.DisableSending {
|
||||||
// Just ignore the mentions
|
// Just ignore the mentions
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
links := []string{}
|
links := []string{}
|
||||||
contentLinks, err := allLinksFromHTML(strings.NewReader(string(p.html())), p.fullURL())
|
contentLinks, err := allLinksFromHTML(strings.NewReader(string(a.html(p))), a.fullPostURL(p))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
links = append(links, contentLinks...)
|
links = append(links, contentLinks...)
|
||||||
links = append(links, p.firstParameter("link"), p.firstParameter(appConfig.Micropub.LikeParam), p.firstParameter(appConfig.Micropub.ReplyParam), p.firstParameter(appConfig.Micropub.BookmarkParam))
|
links = append(links, p.firstParameter("link"), p.firstParameter(a.cfg.Micropub.LikeParam), p.firstParameter(a.cfg.Micropub.ReplyParam), p.firstParameter(a.cfg.Micropub.BookmarkParam))
|
||||||
for _, link := range funk.UniqString(links) {
|
for _, link := range funk.UniqString(links) {
|
||||||
if link == "" {
|
if link == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Internal mention
|
// Internal mention
|
||||||
if strings.HasPrefix(link, appConfig.Server.PublicAddress) {
|
if strings.HasPrefix(link, a.cfg.Server.PublicAddress) {
|
||||||
// Save mention directly
|
// Save mention directly
|
||||||
if err := createWebmention(p.fullURL(), link); err != nil {
|
if err := a.createWebmention(a.fullPostURL(p), link); err != nil {
|
||||||
log.Println("Failed to create webmention:", err.Error())
|
log.Println("Failed to create webmention:", err.Error())
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// External mention
|
// External mention
|
||||||
if pm := appConfig.PrivateMode; pm != nil && pm.Enabled {
|
if pm := a.cfg.PrivateMode; pm != nil && pm.Enabled {
|
||||||
// Private mode, don't send external mentions
|
// Private mode, don't send external mentions
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if wm := a.cfg.Webmention; wm != nil && wm.DisableSending {
|
||||||
|
// Just ignore the mention
|
||||||
|
continue
|
||||||
|
}
|
||||||
endpoint := discoverEndpoint(link)
|
endpoint := discoverEndpoint(link)
|
||||||
if endpoint == "" {
|
if endpoint == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err = sendWebmention(endpoint, p.fullURL(), link); err != nil {
|
if err = sendWebmention(endpoint, a.fullPostURL(p), link); err != nil {
|
||||||
log.Println("Sending webmention to " + link + " failed")
|
log.Println("Sending webmention to " + link + " failed")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -56,10 +60,6 @@ func (p *post) sendWebmentions() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendWebmention(endpoint, source, target string) error {
|
func sendWebmention(endpoint, source, target string) error {
|
||||||
if wm := appConfig.Webmention; wm != nil && wm.DisableSending {
|
|
||||||
// Just ignore the mention
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
req, err := http.NewRequest(http.MethodPost, endpoint, strings.NewReader(url.Values{
|
req, err := http.NewRequest(http.MethodPost, endpoint, strings.NewReader(url.Values{
|
||||||
"source": []string{source},
|
"source": []string{source},
|
||||||
"target": []string{target},
|
"target": []string{target},
|
||||||
|
|
|
@ -20,10 +20,10 @@ import (
|
||||||
"willnorris.com/go/microformats"
|
"willnorris.com/go/microformats"
|
||||||
)
|
)
|
||||||
|
|
||||||
func initWebmentionQueue() {
|
func (a *goBlog) initWebmentionQueue() {
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
qi, err := peekQueue("wm")
|
qi, err := a.db.peekQueue("wm")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err.Error())
|
log.Println(err.Error())
|
||||||
continue
|
continue
|
||||||
|
@ -32,14 +32,14 @@ func initWebmentionQueue() {
|
||||||
err = gob.NewDecoder(bytes.NewReader(qi.content)).Decode(&m)
|
err = gob.NewDecoder(bytes.NewReader(qi.content)).Decode(&m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err.Error())
|
log.Println(err.Error())
|
||||||
_ = qi.dequeue()
|
_ = a.db.dequeue(qi)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
err = m.verifyMention()
|
err = a.verifyMention(&m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(fmt.Sprintf("Failed to verify webmention from %s to %s: %s", m.Source, m.Target, err.Error()))
|
log.Println(fmt.Sprintf("Failed to verify webmention from %s to %s: %s", m.Source, m.Target, err.Error()))
|
||||||
}
|
}
|
||||||
err = qi.dequeue()
|
err = a.db.dequeue(qi)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err.Error())
|
log.Println(err.Error())
|
||||||
}
|
}
|
||||||
|
@ -51,26 +51,30 @@ func initWebmentionQueue() {
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func queueMention(m *mention) error {
|
func (a *goBlog) queueMention(m *mention) error {
|
||||||
if wm := appConfig.Webmention; wm != nil && wm.DisableReceiving {
|
if wm := a.cfg.Webmention; wm != nil && wm.DisableReceiving {
|
||||||
return errors.New("webmention receiving disabled")
|
return errors.New("webmention receiving disabled")
|
||||||
}
|
}
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := gob.NewEncoder(&buf).Encode(m); err != nil {
|
if err := gob.NewEncoder(&buf).Encode(m); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return enqueue("wm", buf.Bytes(), time.Now())
|
return a.db.enqueue("wm", buf.Bytes(), time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mention) verifyMention() error {
|
func (a *goBlog) verifyMention(m *mention) error {
|
||||||
req, err := http.NewRequest(http.MethodGet, m.Source, nil)
|
req, err := http.NewRequest(http.MethodGet, m.Source, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
if strings.HasPrefix(m.Source, appConfig.Server.PublicAddress) {
|
if strings.HasPrefix(m.Source, a.cfg.Server.PublicAddress) {
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
d.ServeHTTP(rec, req.WithContext(context.WithValue(req.Context(), loggedInKey, true)))
|
for a.d == nil {
|
||||||
|
// Server not yet started
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
}
|
||||||
|
a.d.ServeHTTP(rec, req.WithContext(context.WithValue(req.Context(), loggedInKey, true)))
|
||||||
resp = rec.Result()
|
resp = rec.Result()
|
||||||
} else {
|
} else {
|
||||||
req.Header.Set(userAgent, appUserAgent)
|
req.Header.Set(userAgent, appUserAgent)
|
||||||
|
@ -82,7 +86,7 @@ func (m *mention) verifyMention() error {
|
||||||
err = m.verifyReader(resp.Body)
|
err = m.verifyReader(resp.Body)
|
||||||
_ = resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_, err := appDb.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))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(m.Content) > 500 {
|
if len(m.Content) > 500 {
|
||||||
|
@ -92,13 +96,13 @@ func (m *mention) verifyMention() error {
|
||||||
m.Title = m.Title[0:57] + "…"
|
m.Title = m.Title[0:57] + "…"
|
||||||
}
|
}
|
||||||
newStatus := webmentionStatusVerified
|
newStatus := webmentionStatusVerified
|
||||||
if webmentionExists(m.Source, m.Target) {
|
if a.db.webmentionExists(m.Source, m.Target) {
|
||||||
_, err = appDb.exec("update webmentions set status = @status, title = @title, content = @content, author = @author where source = @source and target = @target",
|
_, err = a.db.exec("update webmentions set status = @status, title = @title, content = @content, author = @author where source = @source and target = @target",
|
||||||
sql.Named("status", newStatus), sql.Named("title", m.Title), sql.Named("content", m.Content), sql.Named("author", m.Author), sql.Named("source", m.Source), sql.Named("target", m.Target))
|
sql.Named("status", newStatus), sql.Named("title", m.Title), sql.Named("content", m.Content), sql.Named("author", m.Author), sql.Named("source", m.Source), sql.Named("target", m.Target))
|
||||||
} else {
|
} else {
|
||||||
_, err = appDb.exec("insert into webmentions (source, target, created, status, title, content, author) values (@source, @target, @created, @status, @title, @content, @author)",
|
_, err = a.db.exec("insert into webmentions (source, target, created, status, title, content, author) values (@source, @target, @created, @status, @title, @content, @author)",
|
||||||
sql.Named("source", m.Source), sql.Named("target", m.Target), sql.Named("created", m.Created), sql.Named("status", newStatus), sql.Named("title", m.Title), sql.Named("content", m.Content), sql.Named("author", m.Author))
|
sql.Named("source", m.Source), sql.Named("target", m.Target), sql.Named("created", m.Created), sql.Named("status", newStatus), sql.Named("title", m.Title), sql.Named("content", m.Content), sql.Named("author", m.Author))
|
||||||
sendNotification(fmt.Sprintf("New webmention from %s to %s", m.Source, m.Target))
|
a.sendNotification(fmt.Sprintf("New webmention from %s to %s", m.Source, m.Target))
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue