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",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Build",
|
||||
"type": "shell",
|
||||
"command": "go build --tags \"libsqlite3 linux sqlite_fts5\"",
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Build",
|
||||
"type": "shell",
|
||||
"command": "go build",
|
||||
"options": {
|
||||
"env": {
|
||||
"GOFLAGS": "-tags=linux,libsqlite3,sqlite_fts5"
|
||||
}
|
||||
},
|
||||
"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
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
|
@ -14,50 +13,41 @@ import (
|
|||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-fed/httpsig"
|
||||
)
|
||||
|
||||
var (
|
||||
apPrivateKey *rsa.PrivateKey
|
||||
apPostSigner httpsig.Signer
|
||||
apPostSignMutex *sync.Mutex = &sync.Mutex{}
|
||||
webfingerResources map[string]*configBlog
|
||||
webfingerAccts map[string]string
|
||||
)
|
||||
|
||||
func initActivityPub() error {
|
||||
if !appConfig.ActivityPub.Enabled {
|
||||
func (a *goBlog) initActivityPub() error {
|
||||
if !a.cfg.ActivityPub.Enabled {
|
||||
return nil
|
||||
}
|
||||
// Add hooks
|
||||
postPostHooks = append(postPostHooks, func(p *post) {
|
||||
a.pPostHooks = append(a.pPostHooks, func(p *post) {
|
||||
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() {
|
||||
p.apUpdate()
|
||||
a.apUpdate(p)
|
||||
}
|
||||
})
|
||||
postDeleteHooks = append(postDeleteHooks, func(p *post) {
|
||||
p.apDelete()
|
||||
a.pDeleteHooks = append(a.pDeleteHooks, func(p *post) {
|
||||
a.apDelete(p)
|
||||
})
|
||||
// Prepare webfinger
|
||||
webfingerResources = map[string]*configBlog{}
|
||||
webfingerAccts = map[string]string{}
|
||||
for name, blog := range appConfig.Blogs {
|
||||
acct := "acct:" + name + "@" + appConfig.Server.publicHostname
|
||||
webfingerResources[acct] = blog
|
||||
webfingerResources[blog.apIri()] = blog
|
||||
webfingerAccts[blog.apIri()] = acct
|
||||
a.webfingerResources = map[string]*configBlog{}
|
||||
a.webfingerAccts = map[string]string{}
|
||||
for name, blog := range a.cfg.Blogs {
|
||||
acct := "acct:" + name + "@" + a.cfg.Server.publicHostname
|
||||
a.webfingerResources[acct] = blog
|
||||
a.webfingerResources[a.apIri(blog)] = blog
|
||||
a.webfingerAccts[a.apIri(blog)] = acct
|
||||
}
|
||||
// Read key and prepare signing
|
||||
pkfile, err := os.ReadFile(appConfig.ActivityPub.KeyPath)
|
||||
pkfile, err := os.ReadFile(a.cfg.ActivityPub.KeyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -65,11 +55,11 @@ func initActivityPub() error {
|
|||
if privateKeyDecoded == nil {
|
||||
return errors.New("failed to decode private key")
|
||||
}
|
||||
apPrivateKey, err = x509.ParsePKCS1PrivateKey(privateKeyDecoded.Bytes)
|
||||
a.apPrivateKey, err = x509.ParsePKCS1PrivateKey(privateKeyDecoded.Bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apPostSigner, _, err = httpsig.NewSigner(
|
||||
a.apPostSigner, _, err = httpsig.NewSigner(
|
||||
[]httpsig.Algorithm{httpsig.RSA_SHA256},
|
||||
httpsig.DigestSha256,
|
||||
[]string{httpsig.RequestTarget, "date", "host", "digest"},
|
||||
|
@ -80,32 +70,32 @@ func initActivityPub() error {
|
|||
return err
|
||||
}
|
||||
// Init send queue
|
||||
initAPSendQueue()
|
||||
a.initAPSendQueue()
|
||||
return nil
|
||||
}
|
||||
|
||||
func apHandleWebfinger(w http.ResponseWriter, r *http.Request) {
|
||||
blog, ok := webfingerResources[r.URL.Query().Get("resource")]
|
||||
func (a *goBlog) apHandleWebfinger(w http.ResponseWriter, r *http.Request) {
|
||||
blog, ok := a.webfingerResources[r.URL.Query().Get("resource")]
|
||||
if !ok {
|
||||
serveError(w, r, "Resource not found", http.StatusNotFound)
|
||||
a.serveError(w, r, "Resource not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
b, _ := json.Marshal(map[string]interface{}{
|
||||
"subject": webfingerAccts[blog.apIri()],
|
||||
"subject": a.webfingerAccts[a.apIri(blog)],
|
||||
"aliases": []string{
|
||||
webfingerAccts[blog.apIri()],
|
||||
blog.apIri(),
|
||||
a.webfingerAccts[a.apIri(blog)],
|
||||
a.apIri(blog),
|
||||
},
|
||||
"links": []map[string]string{
|
||||
{
|
||||
"rel": "self",
|
||||
"type": contentTypeAS,
|
||||
"href": blog.apIri(),
|
||||
"href": a.apIri(blog),
|
||||
},
|
||||
{
|
||||
"rel": "http://webfinger.net/rel/profile-page",
|
||||
"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)
|
||||
}
|
||||
|
||||
func apHandleInbox(w http.ResponseWriter, r *http.Request) {
|
||||
func (a *goBlog) apHandleInbox(w http.ResponseWriter, r *http.Request) {
|
||||
blogName := chi.URLParam(r, "blog")
|
||||
blog := appConfig.Blogs[blogName]
|
||||
blog := a.cfg.Blogs[blogName]
|
||||
if blog == nil {
|
||||
serveError(w, r, "Inbox not found", http.StatusNotFound)
|
||||
a.serveError(w, r, "Inbox not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
blogIri := blog.apIri()
|
||||
blogIri := a.apIri(blog)
|
||||
// Verify request
|
||||
requestActor, requestKey, requestActorStatus, err := apVerifySignature(r)
|
||||
if err != nil {
|
||||
// Send 401 because signature could not be verified
|
||||
serveError(w, r, err.Error(), http.StatusUnauthorized)
|
||||
a.serveError(w, r, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
if requestActorStatus != 0 {
|
||||
|
@ -134,12 +124,12 @@ func apHandleInbox(w http.ResponseWriter, r *http.Request) {
|
|||
if err == nil {
|
||||
u.Fragment = ""
|
||||
u.RawFragment = ""
|
||||
_ = apRemoveFollower(blogName, u.String())
|
||||
_ = a.db.apRemoveFollower(blogName, u.String())
|
||||
w.WriteHeader(http.StatusOK)
|
||||
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
|
||||
}
|
||||
// Parse activity
|
||||
|
@ -147,29 +137,29 @@ func apHandleInbox(w http.ResponseWriter, r *http.Request) {
|
|||
err = json.NewDecoder(r.Body).Decode(&activity)
|
||||
_ = r.Body.Close()
|
||||
if err != nil {
|
||||
serveError(w, r, "Failed to decode body", http.StatusBadRequest)
|
||||
a.serveError(w, r, "Failed to decode body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Get and check activity actor
|
||||
activityActor, ok := activity["actor"].(string)
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
// Do
|
||||
switch activity["type"] {
|
||||
case "Follow":
|
||||
apAccept(blogName, blog, activity)
|
||||
a.apAccept(blogName, blog, activity)
|
||||
case "Undo":
|
||||
{
|
||||
if object, ok := activity["object"].(map[string]interface{}); ok {
|
||||
if objectType, ok := object["type"].(string); ok && objectType == "Follow" {
|
||||
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)
|
||||
if hasReplyToString && hasID && len(inReplyTo) > 0 && len(id) > 0 && strings.Contains(inReplyTo, blogIri) {
|
||||
// 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 {
|
||||
// May be a mention; find links to blog and save them as webmentions
|
||||
if links, err := allLinksFromHTML(strings.NewReader(content), id); err == nil {
|
||||
for _, link := range links {
|
||||
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":
|
||||
{
|
||||
if object, ok := activity["object"].(string); ok && len(object) > 0 && object == activityActor {
|
||||
_ = apRemoveFollower(blogName, activityActor)
|
||||
_ = a.db.apRemoveFollower(blogName, activityActor)
|
||||
}
|
||||
}
|
||||
case "Like":
|
||||
{
|
||||
likeObject, likeObjectOk := activity["object"].(string)
|
||||
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":
|
||||
{
|
||||
announceObject, announceObjectOk := activity["object"].(string)
|
||||
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
|
||||
}
|
||||
|
||||
func apGetAllInboxes(blog string) ([]string, error) {
|
||||
rows, err := appDb.query("select distinct inbox from activitypub_followers where blog = @blog", sql.Named("blog", blog))
|
||||
func (db *database) apGetAllInboxes(blog string) ([]string, error) {
|
||||
rows, err := db.query("select distinct inbox from activitypub_followers where blog = @blog", sql.Named("blog", blog))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -294,27 +284,27 @@ func apGetAllInboxes(blog string) ([]string, error) {
|
|||
return inboxes, nil
|
||||
}
|
||||
|
||||
func 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))
|
||||
func (db *database) apAddFollower(blog, follower, inbox string) error {
|
||||
_, 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
|
||||
}
|
||||
|
||||
func 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))
|
||||
func (db *database) apRemoveFollower(blog, follower string) error {
|
||||
_, err := db.exec("delete from activitypub_followers where blog = @blog and follower = @follower", sql.Named("blog", blog), sql.Named("follower", follower))
|
||||
return err
|
||||
}
|
||||
|
||||
func apRemoveInbox(inbox string) error {
|
||||
_, err := appDb.exec("delete from activitypub_followers where inbox = @inbox", sql.Named("inbox", inbox))
|
||||
func (db *database) apRemoveInbox(inbox string) error {
|
||||
_, err := db.exec("delete from activitypub_followers where inbox = @inbox", sql.Named("inbox", inbox))
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *post) apPost() {
|
||||
n := p.toASNote()
|
||||
apSendToAllFollowers(p.Blog, map[string]interface{}{
|
||||
func (a *goBlog) apPost(p *post) {
|
||||
n := a.toASNote(p)
|
||||
a.apSendToAllFollowers(p.Blog, map[string]interface{}{
|
||||
"@context": asContext,
|
||||
"actor": appConfig.Blogs[p.Blog].apIri(),
|
||||
"id": p.fullURL(),
|
||||
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
|
||||
"id": a.fullPostURL(p),
|
||||
"published": n.Published,
|
||||
"type": "Create",
|
||||
"object": n,
|
||||
|
@ -322,46 +312,46 @@ func (p *post) apPost() {
|
|||
if n.InReplyTo != "" {
|
||||
// Is reply, so announce it
|
||||
time.Sleep(30 * time.Second)
|
||||
p.apAnnounce()
|
||||
a.apAnnounce(p)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *post) apUpdate() {
|
||||
apSendToAllFollowers(p.Blog, map[string]interface{}{
|
||||
func (a *goBlog) apUpdate(p *post) {
|
||||
a.apSendToAllFollowers(p.Blog, map[string]interface{}{
|
||||
"@context": asContext,
|
||||
"actor": appConfig.Blogs[p.Blog].apIri(),
|
||||
"id": p.fullURL(),
|
||||
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
|
||||
"id": a.fullPostURL(p),
|
||||
"published": time.Now().Format("2006-01-02T15:04:05-07:00"),
|
||||
"type": "Update",
|
||||
"object": p.toASNote(),
|
||||
"object": a.toASNote(p),
|
||||
})
|
||||
}
|
||||
|
||||
func (p *post) apAnnounce() {
|
||||
apSendToAllFollowers(p.Blog, map[string]interface{}{
|
||||
func (a *goBlog) apAnnounce(p *post) {
|
||||
a.apSendToAllFollowers(p.Blog, map[string]interface{}{
|
||||
"@context": asContext,
|
||||
"actor": appConfig.Blogs[p.Blog].apIri(),
|
||||
"id": p.fullURL() + "#announce",
|
||||
"published": p.toASNote().Published,
|
||||
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
|
||||
"id": a.fullPostURL(p) + "#announce",
|
||||
"published": a.toASNote(p).Published,
|
||||
"type": "Announce",
|
||||
"object": p.fullURL(),
|
||||
"object": a.fullPostURL(p),
|
||||
})
|
||||
}
|
||||
|
||||
func (p *post) apDelete() {
|
||||
apSendToAllFollowers(p.Blog, map[string]interface{}{
|
||||
func (a *goBlog) apDelete(p *post) {
|
||||
a.apSendToAllFollowers(p.Blog, map[string]interface{}{
|
||||
"@context": asContext,
|
||||
"actor": appConfig.Blogs[p.Blog].apIri(),
|
||||
"id": p.fullURL() + "#delete",
|
||||
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
|
||||
"id": a.fullPostURL(p) + "#delete",
|
||||
"type": "Delete",
|
||||
"object": map[string]string{
|
||||
"id": p.fullURL(),
|
||||
"id": a.fullPostURL(p),
|
||||
"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
|
||||
newFollower := follow["actor"].(string)
|
||||
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 != "" {
|
||||
inbox = endpoints.SharedInbox
|
||||
}
|
||||
if err = apAddFollower(blogName, follower.ID, inbox); err != nil {
|
||||
if err = a.db.apAddFollower(blogName, follower.ID, inbox); err != nil {
|
||||
return
|
||||
}
|
||||
// remove @context from the inner activity
|
||||
|
@ -389,37 +379,37 @@ func apAccept(blogName string, blog *configBlog, follow map[string]interface{})
|
|||
accept := map[string]interface{}{
|
||||
"@context": asContext,
|
||||
"to": follow["actor"],
|
||||
"actor": blog.apIri(),
|
||||
"actor": a.apIri(blog),
|
||||
"object": follow,
|
||||
"type": "Accept",
|
||||
}
|
||||
_, accept["id"] = apNewID(blog)
|
||||
_ = apQueueSendSigned(blog.apIri(), follower.Inbox, accept)
|
||||
_, accept["id"] = a.apNewID(blog)
|
||||
_ = a.db.apQueueSendSigned(a.apIri(blog), follower.Inbox, accept)
|
||||
}
|
||||
|
||||
func apSendToAllFollowers(blog string, activity interface{}) {
|
||||
inboxes, err := apGetAllInboxes(blog)
|
||||
func (a *goBlog) apSendToAllFollowers(blog string, activity interface{}) {
|
||||
inboxes, err := a.db.apGetAllInboxes(blog)
|
||||
if err != nil {
|
||||
log.Println("Failed to retrieve inboxes:", err.Error())
|
||||
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 {
|
||||
go func(inbox string) {
|
||||
_ = apQueueSendSigned(blogIri, inbox, activity)
|
||||
_ = db.apQueueSendSigned(blogIri, inbox, activity)
|
||||
}(i)
|
||||
}
|
||||
}
|
||||
|
||||
func apNewID(blog *configBlog) (hash string, url string) {
|
||||
return hash, blog.apIri() + generateRandomString(16)
|
||||
func (a *goBlog) apNewID(blog *configBlog) (hash string, url string) {
|
||||
return hash, a.apIri(blog) + generateRandomString(16)
|
||||
}
|
||||
|
||||
func (b *configBlog) apIri() string {
|
||||
return appConfig.Server.PublicAddress + b.Path
|
||||
func (a *goBlog) apIri(b *configBlog) string {
|
||||
return a.cfg.Server.PublicAddress + b.Path
|
||||
}
|
||||
|
||||
func apRequestIsSuccess(code int) bool {
|
||||
|
|
|
@ -19,10 +19,10 @@ type apRequest struct {
|
|||
Try int
|
||||
}
|
||||
|
||||
func initAPSendQueue() {
|
||||
func (a *goBlog) initAPSendQueue() {
|
||||
go func() {
|
||||
for {
|
||||
qi, err := peekQueue("ap")
|
||||
qi, err := a.db.peekQueue("ap")
|
||||
if err != nil {
|
||||
log.Println(err.Error())
|
||||
continue
|
||||
|
@ -31,22 +31,22 @@ func initAPSendQueue() {
|
|||
err = gob.NewDecoder(bytes.NewReader(qi.content)).Decode(&r)
|
||||
if err != nil {
|
||||
log.Println(err.Error())
|
||||
_ = qi.dequeue()
|
||||
_ = a.db.dequeue(qi)
|
||||
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 {
|
||||
// Try it again
|
||||
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
|
||||
} else {
|
||||
log.Printf("Request to %s failed for the 20th time", r.To)
|
||||
log.Println()
|
||||
_ = apRemoveInbox(r.To)
|
||||
_ = a.db.apRemoveInbox(r.To)
|
||||
}
|
||||
}
|
||||
err = qi.dequeue()
|
||||
err = a.db.dequeue(qi)
|
||||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -71,7 +71,7 @@ func apQueueSendSigned(blogIri, to string, activity interface{}) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return enqueue("ap", b, time.Now())
|
||||
return db.enqueue("ap", b, time.Now())
|
||||
}
|
||||
|
||||
func (r *apRequest) encode() ([]byte, error) {
|
||||
|
@ -83,7 +83,7 @@ func (r *apRequest) encode() ([]byte, error) {
|
|||
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
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
@ -105,9 +105,9 @@ func apSendSigned(blogIri, to string, activity []byte) error {
|
|||
r.Header.Set(contentType, contentTypeASUTF8)
|
||||
r.Header.Set("Host", iri.Host)
|
||||
// Sign request
|
||||
apPostSignMutex.Lock()
|
||||
err = apPostSigner.SignRequest(apPrivateKey, blogIri+"#main-key", r, activity)
|
||||
apPostSignMutex.Unlock()
|
||||
a.apPostSignMutex.Lock()
|
||||
err = a.apPostSigner.SignRequest(a.apPrivateKey, blogIri+"#main-key", r, activity)
|
||||
a.apPostSignMutex.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -22,9 +22,9 @@ var asCheckMediaTypes = []contenttype.MediaType{
|
|||
|
||||
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) {
|
||||
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
|
||||
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)))
|
||||
|
@ -87,21 +87,21 @@ type asEndpoints struct {
|
|||
SharedInbox string `json:"sharedInbox,omitempty"`
|
||||
}
|
||||
|
||||
func (p *post) serveActivityStreams(w http.ResponseWriter) {
|
||||
b, _ := json.Marshal(p.toASNote())
|
||||
func (a *goBlog) serveActivityStreamsPost(p *post, w http.ResponseWriter) {
|
||||
b, _ := json.Marshal(a.toASNote(p))
|
||||
w.Header().Set(contentType, contentTypeASUTF8)
|
||||
_, _ = writeMinified(w, contentTypeAS, b)
|
||||
}
|
||||
|
||||
func (p *post) toASNote() *asNote {
|
||||
func (a *goBlog) toASNote(p *post) *asNote {
|
||||
// Create a Note object
|
||||
as := &asNote{
|
||||
Context: asContext,
|
||||
To: []string{"https://www.w3.org/ns/activitystreams#Public"},
|
||||
MediaType: contentTypeHTML,
|
||||
ID: p.fullURL(),
|
||||
URL: p.fullURL(),
|
||||
AttributedTo: appConfig.Blogs[p.Blog].apIri(),
|
||||
ID: a.fullPostURL(p),
|
||||
URL: a.fullPostURL(p),
|
||||
AttributedTo: a.apIri(a.cfg.Blogs[p.Blog]),
|
||||
}
|
||||
// Name and Type
|
||||
if title := p.title(); title != "" {
|
||||
|
@ -111,9 +111,9 @@ func (p *post) toASNote() *asNote {
|
|||
as.Type = "Note"
|
||||
}
|
||||
// Content
|
||||
as.Content = string(p.absoluteHTML())
|
||||
as.Content = string(a.absoluteHTML(p))
|
||||
// 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 {
|
||||
as.Attachment = append(as.Attachment, &asAttachment{
|
||||
Type: "Image",
|
||||
|
@ -122,12 +122,12 @@ func (p *post) toASNote() *asNote {
|
|||
}
|
||||
}
|
||||
// Tags
|
||||
for _, tagTax := range appConfig.ActivityPub.TagsTaxonomies {
|
||||
for _, tagTax := range a.cfg.ActivityPub.TagsTaxonomies {
|
||||
for _, tag := range p.Parameters[tagTax] {
|
||||
as.Tag = append(as.Tag, &asTag{
|
||||
Type: "Hashtag",
|
||||
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
|
||||
if replyLink := p.firstParameter(appConfig.Micropub.ReplyParam); replyLink != "" {
|
||||
if replyLink := p.firstParameter(a.cfg.Micropub.ReplyParam); replyLink != "" {
|
||||
as.InReplyTo = replyLink
|
||||
}
|
||||
return as
|
||||
}
|
||||
|
||||
func (b *configBlog) serveActivityStreams(blog string, w http.ResponseWriter, r *http.Request) {
|
||||
publicKeyDer, err := x509.MarshalPKIXPublicKey(&apPrivateKey.PublicKey)
|
||||
func (a *goBlog) serveActivityStreams(blog string, w http.ResponseWriter, r *http.Request) {
|
||||
b := a.cfg.Blogs[blog]
|
||||
publicKeyDer, err := x509.MarshalPKIXPublicKey(&(a.apPrivateKey.PublicKey))
|
||||
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
|
||||
}
|
||||
asBlog := &asPerson{
|
||||
Context: asContext,
|
||||
Type: "Person",
|
||||
ID: b.apIri(),
|
||||
URL: b.apIri(),
|
||||
ID: a.apIri(b),
|
||||
URL: a.apIri(b),
|
||||
Name: b.Title,
|
||||
Summary: b.Description,
|
||||
PreferredUsername: blog,
|
||||
Inbox: appConfig.Server.PublicAddress + "/activitypub/inbox/" + blog,
|
||||
Inbox: a.cfg.Server.PublicAddress + "/activitypub/inbox/" + blog,
|
||||
PublicKey: &asPublicKey{
|
||||
Owner: b.apIri(),
|
||||
ID: b.apIri() + "#main-key",
|
||||
Owner: a.apIri(b),
|
||||
ID: a.apIri(b) + "#main-key",
|
||||
PublicKeyPem: string(pem.EncodeToMemory(&pem.Block{
|
||||
Type: "PUBLIC KEY",
|
||||
Headers: nil,
|
||||
|
@ -176,10 +177,10 @@ func (b *configBlog) serveActivityStreams(blog string, w http.ResponseWriter, r
|
|||
},
|
||||
}
|
||||
// Add profile picture
|
||||
if appConfig.User.Picture != "" {
|
||||
if a.cfg.User.Picture != "" {
|
||||
asBlog.Icon = &asAttachment{
|
||||
Type: "Image",
|
||||
URL: appConfig.User.Picture,
|
||||
URL: a.cfg.User.Picture,
|
||||
}
|
||||
}
|
||||
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"
|
||||
)
|
||||
|
||||
func checkCredentials(username, password, totpPasscode string) bool {
|
||||
return username == appConfig.User.Nick &&
|
||||
password == appConfig.User.Password &&
|
||||
(appConfig.User.TOTP == "" || totp.Validate(totpPasscode, appConfig.User.TOTP))
|
||||
func (a *goBlog) checkCredentials(username, password, totpPasscode string) bool {
|
||||
return username == a.cfg.User.Nick &&
|
||||
password == a.cfg.User.Password &&
|
||||
(a.cfg.User.TOTP == "" || totp.Validate(totpPasscode, a.cfg.User.TOTP))
|
||||
}
|
||||
|
||||
func checkAppPasswords(username, password string) bool {
|
||||
for _, apw := range appConfig.User.AppPasswords {
|
||||
func (a *goBlog) checkAppPasswords(username, password string) bool {
|
||||
for _, apw := range a.cfg.User.AppPasswords {
|
||||
if apw.Username == username && apw.Password == password {
|
||||
return true
|
||||
}
|
||||
|
@ -26,11 +26,11 @@ func checkAppPasswords(username, password string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func jwtKey() []byte {
|
||||
return []byte(appConfig.Server.JWTSecret)
|
||||
func (a *goBlog) jwtKey() []byte {
|
||||
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) {
|
||||
// 1. Check if already logged in
|
||||
if loggedIn, ok := r.Context().Value(loggedInKey).(bool); ok && loggedIn {
|
||||
|
@ -38,12 +38,12 @@ func authMiddleware(next http.Handler) http.Handler {
|
|||
return
|
||||
}
|
||||
// 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)
|
||||
return
|
||||
}
|
||||
// 3. Check login cookie
|
||||
if checkLoginCookie(r) {
|
||||
if a.checkLoginCookie(r) {
|
||||
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), loggedInKey, true)))
|
||||
return
|
||||
}
|
||||
|
@ -57,12 +57,12 @@ func authMiddleware(next http.Handler) http.Handler {
|
|||
_ = r.ParseForm()
|
||||
b = []byte(r.PostForm.Encode())
|
||||
}
|
||||
render(w, r, templateLogin, &renderData{
|
||||
a.render(w, r, templateLogin, &renderData{
|
||||
Data: map[string]interface{}{
|
||||
"loginmethod": r.Method,
|
||||
"loginheaders": base64.StdEncoding.EncodeToString(h),
|
||||
"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"
|
||||
|
||||
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) {
|
||||
if checkLoginCookie(r) {
|
||||
if a.checkLoginCookie(r) {
|
||||
next.ServeHTTP(rw, r.WithContext(context.WithValue(r.Context(), loggedInKey, true)))
|
||||
return
|
||||
}
|
||||
|
@ -80,8 +80,8 @@ func checkLoggedIn(next http.Handler) http.Handler {
|
|||
})
|
||||
}
|
||||
|
||||
func checkLoginCookie(r *http.Request) bool {
|
||||
ses, err := loginSessionsStore.Get(r, "l")
|
||||
func (a *goBlog) checkLoginCookie(r *http.Request) bool {
|
||||
ses, err := a.loginSessions.Get(r, "l")
|
||||
if err == nil && ses != nil {
|
||||
if login, ok := ses.Values["login"]; ok && login.(bool) {
|
||||
return true
|
||||
|
@ -90,15 +90,15 @@ func checkLoginCookie(r *http.Request) bool {
|
|||
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) {
|
||||
if !checkLogin(rw, r) {
|
||||
if !a.checkLogin(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 {
|
||||
return false
|
||||
}
|
||||
|
@ -109,8 +109,8 @@ func checkLogin(w http.ResponseWriter, r *http.Request) bool {
|
|||
return false
|
||||
}
|
||||
// Check credential
|
||||
if !checkCredentials(r.FormValue("username"), r.FormValue("password"), r.FormValue("token")) {
|
||||
serveError(w, r, "Incorrect credentials", http.StatusUnauthorized)
|
||||
if !a.checkCredentials(r.FormValue("username"), r.FormValue("password"), r.FormValue("token")) {
|
||||
a.serveError(w, r, "Incorrect credentials", http.StatusUnauthorized)
|
||||
return true
|
||||
}
|
||||
// Prepare original request
|
||||
|
@ -124,20 +124,20 @@ func checkLogin(w http.ResponseWriter, r *http.Request) bool {
|
|||
req.Header[k] = v
|
||||
}
|
||||
// Cookie
|
||||
ses, err := loginSessionsStore.Get(r, "l")
|
||||
ses, err := a.loginSessions.Get(r, "l")
|
||||
if err != nil {
|
||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||
return true
|
||||
}
|
||||
ses.Values["login"] = true
|
||||
cookie, err := loginSessionsStore.SaveGetCookie(r, w, ses)
|
||||
cookie, err := a.loginSessions.SaveGetCookie(r, w, ses)
|
||||
if err != nil {
|
||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||
return true
|
||||
}
|
||||
req.AddCookie(cookie)
|
||||
// Serve original request
|
||||
d.ServeHTTP(w, req)
|
||||
a.d.ServeHTTP(w, req)
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -146,9 +146,9 @@ func serveLogin(w http.ResponseWriter, r *http.Request) {
|
|||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
}
|
||||
|
||||
func serveLogout(w http.ResponseWriter, r *http.Request) {
|
||||
if ses, err := loginSessionsStore.Get(r, "l"); err == nil && ses != nil {
|
||||
_ = loginSessionsStore.Delete(r, w, ses)
|
||||
func (a *goBlog) serveLogout(w http.ResponseWriter, r *http.Request) {
|
||||
if ses, err := a.loginSessions.Get(r, "l"); err == nil && ses != nil {
|
||||
_ = a.loginSessions.Delete(r, w, ses)
|
||||
}
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
}
|
||||
|
|
59
blogroll.go
59
blogroll.go
|
@ -13,28 +13,25 @@ import (
|
|||
"github.com/kaorimatz/go-opml"
|
||||
servertiming "github.com/mitchellh/go-server-timing"
|
||||
"github.com/thoas/go-funk"
|
||||
"golang.org/x/sync/singleflight"
|
||||
)
|
||||
|
||||
var blogrollCacheGroup singleflight.Group
|
||||
|
||||
func serveBlogroll(w http.ResponseWriter, r *http.Request) {
|
||||
func (a *goBlog) serveBlogroll(w http.ResponseWriter, r *http.Request) {
|
||||
blog := r.Context().Value(blogContextKey).(string)
|
||||
t := servertiming.FromContext(r.Context()).NewMetric("bg").Start()
|
||||
outlines, err, _ := blogrollCacheGroup.Do(blog, func() (interface{}, error) {
|
||||
return getBlogrollOutlines(blog)
|
||||
outlines, err, _ := a.blogrollCacheGroup.Do(blog, func() (interface{}, error) {
|
||||
return a.getBlogrollOutlines(blog)
|
||||
})
|
||||
t.Stop()
|
||||
if err != nil {
|
||||
log.Println("Failed to get outlines:", err.Error())
|
||||
serveError(w, r, "", http.StatusInternalServerError)
|
||||
log.Printf("Failed to get outlines: %v", err)
|
||||
a.serveError(w, r, "", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if appConfig.Cache != nil && appConfig.Cache.Enable {
|
||||
setInternalCacheExpirationHeader(w, r, int(appConfig.Cache.Expiration))
|
||||
if a.cfg.Cache != nil && a.cfg.Cache.Enable {
|
||||
setInternalCacheExpirationHeader(w, r, int(a.cfg.Cache.Expiration))
|
||||
}
|
||||
c := appConfig.Blogs[blog].Blogroll
|
||||
render(w, r, templateBlogroll, &renderData{
|
||||
c := a.cfg.Blogs[blog].Blogroll
|
||||
a.render(w, r, templateBlogroll, &renderData{
|
||||
BlogString: blog,
|
||||
Data: map[string]interface{}{
|
||||
"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)
|
||||
outlines, err, _ := blogrollCacheGroup.Do(blog, func() (interface{}, error) {
|
||||
return getBlogrollOutlines(blog)
|
||||
outlines, err, _ := a.blogrollCacheGroup.Do(blog, func() (interface{}, error) {
|
||||
return a.getBlogrollOutlines(blog)
|
||||
})
|
||||
if err != nil {
|
||||
log.Println("Failed to get outlines:", err.Error())
|
||||
serveError(w, r, "", http.StatusInternalServerError)
|
||||
log.Printf("Failed to get outlines: %v", err)
|
||||
a.serveError(w, r, "", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if appConfig.Cache != nil && appConfig.Cache.Enable {
|
||||
setInternalCacheExpirationHeader(w, r, int(appConfig.Cache.Expiration))
|
||||
if a.cfg.Cache != nil && a.cfg.Cache.Enable {
|
||||
setInternalCacheExpirationHeader(w, r, int(a.cfg.Cache.Expiration))
|
||||
}
|
||||
w.Header().Set(contentType, contentTypeXMLUTF8)
|
||||
mw := minifier.Writer(contentTypeXML, w)
|
||||
defer func() {
|
||||
_ = mw.Close()
|
||||
}()
|
||||
_ = opml.Render(mw, &opml.OPML{
|
||||
var opmlBytes bytes.Buffer
|
||||
_ = opml.Render(&opmlBytes, &opml.OPML{
|
||||
Version: "2.0",
|
||||
DateCreated: time.Now().UTC(),
|
||||
Outlines: outlines.([]*opml.Outline),
|
||||
})
|
||||
_, _ = writeMinified(w, contentTypeXML, opmlBytes.Bytes())
|
||||
}
|
||||
|
||||
func getBlogrollOutlines(blog string) ([]*opml.Outline, error) {
|
||||
config := appConfig.Blogs[blog].Blogroll
|
||||
if cache := loadOutlineCache(blog); cache != nil {
|
||||
func (a *goBlog) getBlogrollOutlines(blog string) ([]*opml.Outline, error) {
|
||||
config := a.cfg.Blogs[blog].Blogroll
|
||||
if cache := a.db.loadOutlineCache(blog); cache != nil {
|
||||
return cache, nil
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodGet, config.Opml, nil)
|
||||
|
@ -112,22 +107,22 @@ func getBlogrollOutlines(blog string) ([]*opml.Outline, error) {
|
|||
} else {
|
||||
outlines = sortOutlines(outlines)
|
||||
}
|
||||
cacheOutlines(blog, outlines)
|
||||
a.db.cacheOutlines(blog, outlines)
|
||||
return outlines, nil
|
||||
}
|
||||
|
||||
func cacheOutlines(blog string, outlines []*opml.Outline) {
|
||||
func (db *database) cacheOutlines(blog string, outlines []*opml.Outline) {
|
||||
var opmlBuffer bytes.Buffer
|
||||
_ = opml.Render(&opmlBuffer, &opml.OPML{
|
||||
Version: "2.0",
|
||||
DateCreated: time.Now().UTC(),
|
||||
Outlines: outlines,
|
||||
})
|
||||
_ = cachePersistently("blogroll_"+blog, opmlBuffer.Bytes())
|
||||
_ = db.cachePersistently("blogroll_"+blog, opmlBuffer.Bytes())
|
||||
}
|
||||
|
||||
func loadOutlineCache(blog string) []*opml.Outline {
|
||||
data, err := retrievePersistentCache("blogroll_" + blog)
|
||||
func (db *database) loadOutlineCache(blog string) []*opml.Outline {
|
||||
data, err := db.retrievePersistentCache("blogroll_" + blog)
|
||||
if err != nil || data == nil {
|
||||
return nil
|
||||
}
|
||||
|
|
50
blogstats.go
50
blogstats.go
|
@ -9,19 +9,19 @@ import (
|
|||
"golang.org/x/sync/singleflight"
|
||||
)
|
||||
|
||||
func initBlogStats() {
|
||||
func (a *goBlog) initBlogStats() {
|
||||
f := func(p *post) {
|
||||
resetBlogStats(p.Blog)
|
||||
a.db.resetBlogStats(p.Blog)
|
||||
}
|
||||
postPostHooks = append(postPostHooks, f)
|
||||
postUpdateHooks = append(postUpdateHooks, f)
|
||||
postDeleteHooks = append(postDeleteHooks, f)
|
||||
a.pPostHooks = append(a.pPostHooks, f)
|
||||
a.pUpdateHooks = append(a.pUpdateHooks, 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)
|
||||
canonical := blogPath(blog) + appConfig.Blogs[blog].BlogStats.Path
|
||||
render(w, r, templateBlogStats, &renderData{
|
||||
canonical := a.blogPath(blog) + a.cfg.Blogs[blog].BlogStats.Path
|
||||
a.render(w, r, templateBlogStats, &renderData{
|
||||
BlogString: blog,
|
||||
Canonical: canonical,
|
||||
Data: map[string]interface{}{
|
||||
|
@ -32,24 +32,24 @@ func serveBlogStats(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
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)
|
||||
data, err, _ := blogStatsCacheGroup.Do(blog, func() (interface{}, error) {
|
||||
return getBlogStats(blog)
|
||||
return a.db.getBlogStats(blog)
|
||||
})
|
||||
if err != nil {
|
||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Render
|
||||
render(w, r, templateBlogStatsTable, &renderData{
|
||||
a.render(w, r, templateBlogStatsTable, &renderData{
|
||||
BlogString: blog,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
func getBlogStats(blog string) (data map[string]interface{}, err error) {
|
||||
if stats := loadBlogStatsCache(blog); stats != nil {
|
||||
func (db *database) getBlogStats(blog string) (data map[string]interface{}, err error) {
|
||||
if stats := db.loadBlogStatsCache(blog); stats != nil {
|
||||
return stats, nil
|
||||
}
|
||||
// Build query
|
||||
|
@ -67,7 +67,7 @@ func getBlogStats(blog string) (data map[string]interface{}, err error) {
|
|||
Name, Posts, Chars, Words, WordsPerPost string
|
||||
}
|
||||
// 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 {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ func getBlogStats(blog string) (data map[string]interface{}, err error) {
|
|||
return nil, err
|
||||
}
|
||||
// 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 {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -90,7 +90,7 @@ func getBlogStats(blog string) (data map[string]interface{}, err error) {
|
|||
}
|
||||
}
|
||||
// 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 {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -102,7 +102,7 @@ func getBlogStats(blog string) (data map[string]interface{}, err error) {
|
|||
months := map[string][]statsTableType{}
|
||||
month := statsTableType{}
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -120,17 +120,17 @@ func getBlogStats(blog string) (data map[string]interface{}, err error) {
|
|||
"withoutdate": noDate,
|
||||
"months": months,
|
||||
}
|
||||
cacheBlogStats(blog, data)
|
||||
db.cacheBlogStats(blog, data)
|
||||
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)
|
||||
_ = cachePersistently("blogstats_"+blog, jb)
|
||||
_ = db.cachePersistently("blogstats_"+blog, jb)
|
||||
}
|
||||
|
||||
func loadBlogStatsCache(blog string) (stats map[string]interface{}) {
|
||||
data, err := retrievePersistentCache("blogstats_" + blog)
|
||||
func (db *database) loadBlogStatsCache(blog string) (stats map[string]interface{}) {
|
||||
data, err := db.retrievePersistentCache("blogstats_" + blog)
|
||||
if err != nil || data == nil {
|
||||
return nil
|
||||
}
|
||||
|
@ -141,6 +141,6 @@ func loadBlogStatsCache(blog string) (stats map[string]interface{}) {
|
|||
return stats
|
||||
}
|
||||
|
||||
func resetBlogStats(blog string) {
|
||||
_ = clearPersistentCache("blogstats_" + blog)
|
||||
func (db *database) resetBlogStats(blog string) {
|
||||
_ = db.clearPersistentCache("blogstats_" + blog)
|
||||
}
|
||||
|
|
64
cache.go
64
cache.go
|
@ -20,17 +20,22 @@ import (
|
|||
"golang.org/x/sync/singleflight"
|
||||
)
|
||||
|
||||
const (
|
||||
cacheInternalExpirationHeader = "Goblog-Expire"
|
||||
)
|
||||
const cacheInternalExpirationHeader = "Goblog-Expire"
|
||||
|
||||
var (
|
||||
cacheGroup singleflight.Group
|
||||
cacheR *ristretto.Cache
|
||||
)
|
||||
type cache struct {
|
||||
g singleflight.Group
|
||||
c *ristretto.Cache
|
||||
cfg *configCache
|
||||
}
|
||||
|
||||
func initCache() (err error) {
|
||||
cacheR, err = ristretto.NewCache(&ristretto.Config{
|
||||
func (a *goBlog) initCache() (err error) {
|
||||
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,
|
||||
MaxCost: 20000000, // 20 MB
|
||||
BufferItems: 16,
|
||||
|
@ -52,13 +57,14 @@ func initCache() (err error) {
|
|||
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) {
|
||||
// Do checks
|
||||
if !appConfig.Cache.Enable {
|
||||
if c.c == nil {
|
||||
// No cache configured
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
// Do checks
|
||||
if !(r.Method == http.MethodGet || r.Method == http.MethodHead) {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
|
@ -74,32 +80,32 @@ func cacheMiddleware(next http.Handler) http.Handler {
|
|||
// Search and serve cache
|
||||
key := cacheKey(r)
|
||||
// Get cache or render it
|
||||
cacheInterface, _, _ := cacheGroup.Do(key, func() (interface{}, error) {
|
||||
return getCache(key, next, r), nil
|
||||
cacheInterface, _, _ := c.g.Do(key, func() (interface{}, error) {
|
||||
return c.getCache(key, next, r), nil
|
||||
})
|
||||
cache := cacheInterface.(*cacheItem)
|
||||
ci := cacheInterface.(*cacheItem)
|
||||
// copy cached headers
|
||||
for k, v := range cache.header {
|
||||
for k, v := range ci.header {
|
||||
w.Header()[k] = v
|
||||
}
|
||||
setCacheHeaders(w, cache)
|
||||
c.setCacheHeaders(w, ci)
|
||||
// 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
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
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
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
}
|
||||
// set status code
|
||||
w.WriteHeader(cache.code)
|
||||
w.WriteHeader(ci.code)
|
||||
// write cached body
|
||||
_, _ = w.Write(cache.body)
|
||||
_, _ = w.Write(ci.body)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -125,14 +131,14 @@ func cacheURLString(u *url.URL) 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("Last-Modified", cache.creationTime.UTC().Format(http.TimeFormat))
|
||||
if w.Header().Get("Cache-Control") == "" {
|
||||
if cache.expiration != 0 {
|
||||
w.Header().Set("Cache-Control", fmt.Sprintf("public,max-age=%d,stale-while-revalidate=%d", cache.expiration, cache.expiration))
|
||||
} 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))
|
||||