mirror of https://github.com/jlelse/GoBlog
Add basic ActivityPub support and other things
This commit is contained in:
parent
b9856175b9
commit
d13b0a5394
|
@ -0,0 +1,327 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
"github.com/go-fed/httpsig"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
apPrivateKey *rsa.PrivateKey
|
||||||
|
apPostSigner httpsig.Signer
|
||||||
|
apPostSignMutex *sync.Mutex = &sync.Mutex{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func initActivityPub() error {
|
||||||
|
pkfile, err := ioutil.ReadFile(appConfig.ActivityPub.KeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
privateKeyDecoded, _ := pem.Decode(pkfile)
|
||||||
|
if privateKeyDecoded == nil {
|
||||||
|
return errors.New("failed to decode private key")
|
||||||
|
}
|
||||||
|
apPrivateKey, err = x509.ParsePKCS1PrivateKey(privateKeyDecoded.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
prefs := []httpsig.Algorithm{httpsig.RSA_SHA256}
|
||||||
|
digestAlgorithm := httpsig.DigestSha256
|
||||||
|
headersToSign := []string{httpsig.RequestTarget, "date", "host", "digest"}
|
||||||
|
apPostSigner, _, err = httpsig.NewSigner(prefs, digestAlgorithm, headersToSign, httpsig.Signature, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func apHandleWebfinger(w http.ResponseWriter, r *http.Request) {
|
||||||
|
re, err := regexp.Compile(`^acct:(.*)@` + regexp.QuoteMeta(appConfig.Server.Domain) + `$`)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name := re.ReplaceAllString(r.URL.Query().Get("resource"), "$1")
|
||||||
|
blog := appConfig.Blogs[name]
|
||||||
|
if blog == nil {
|
||||||
|
http.Error(w, "Not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set(contentType, "application/jrd+json"+charsetUtf8Suffix)
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"subject": "acct:" + name + "@" + appConfig.Server.Domain,
|
||||||
|
"links": []map[string]string{
|
||||||
|
{
|
||||||
|
"rel": "self",
|
||||||
|
"type": contentTypeAS,
|
||||||
|
"href": blog.apIri(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func apHandleInbox(w http.ResponseWriter, r *http.Request) {
|
||||||
|
blogName := chi.URLParam(r, "blog")
|
||||||
|
blog := appConfig.Blogs[blogName]
|
||||||
|
if blog == nil {
|
||||||
|
http.Error(w, "Inbox not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
activity := make(map[string]interface{})
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&activity)
|
||||||
|
_ = r.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to decode body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch activity["type"] {
|
||||||
|
case "Follow":
|
||||||
|
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 == activity["actor"] {
|
||||||
|
_ = apRemoveFollower(blogName, iri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "Create":
|
||||||
|
{
|
||||||
|
if object, ok := activity["object"].(map[string]interface{}); ok {
|
||||||
|
inReplyTo, hasReplyToString := object["inReplyTo"].(string)
|
||||||
|
id, hadID := object["id"].(string)
|
||||||
|
if hasReplyToString && hadID && len(inReplyTo) > 0 && len(id) > 0 && strings.Contains(inReplyTo, blog.apIri()) {
|
||||||
|
// It's an ActivityPub reply
|
||||||
|
// TODO: Save reply to database
|
||||||
|
} else if hadID && len(id) > 0 {
|
||||||
|
// May be a mention
|
||||||
|
// TODO: Save to database
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "Delete":
|
||||||
|
{
|
||||||
|
if object, ok := activity["object"].(string); ok && len(object) > 0 && activity["actor"] == object {
|
||||||
|
_ = apRemoveFollower(blogName, object)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "Like":
|
||||||
|
case "Announce":
|
||||||
|
{
|
||||||
|
// TODO: Save to database
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Return 201
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleWellKnownHostMeta(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set(contentType, "application/xrd+xml"+charsetUtf8Suffix)
|
||||||
|
w.Write([]byte(`<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" type="application/xrd+xml" template="https://` + r.Host + `/.well-known/webfinger?resource={uri}"/></XRD>`))
|
||||||
|
}
|
||||||
|
|
||||||
|
func apGetRemoteActor(iri string) (*asPerson, error) {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, iri, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Add("Accept", contentTypeAS)
|
||||||
|
req.Header.Add("User-Agent", "GoBlog")
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !apRequestIsSuccess(resp.StatusCode) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
actor := &asPerson{}
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(actor)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return actor, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func apGetAllFollowers(blog string) (map[string]string, error) {
|
||||||
|
rows, err := appDb.Query("select follower, inbox from activitypub_followers where blog = ?", blog)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
followers := map[string]string{}
|
||||||
|
for rows.Next() {
|
||||||
|
var follower, inbox string
|
||||||
|
err = rows.Scan(&follower, &inbox)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
followers[follower] = inbox
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func apAddFollower(blog, follower, inbox string) error {
|
||||||
|
startWritingToDb()
|
||||||
|
defer finishWritingToDb()
|
||||||
|
_, err := appDb.Exec("insert or replace into activitypub_followers (blog, follower, inbox) values (?, ?, ?)", blog, follower, inbox)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func apRemoveFollower(blog, follower string) error {
|
||||||
|
startWritingToDb()
|
||||||
|
defer finishWritingToDb()
|
||||||
|
_, err := appDb.Exec("delete from activitypub_followers where blog = ? and follower = ?", blog, follower)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func apPost(p *post) {
|
||||||
|
n := p.toASNote()
|
||||||
|
create := make(map[string]interface{})
|
||||||
|
create["@context"] = asContext
|
||||||
|
create["actor"] = appConfig.Blogs[p.Blog].apIri()
|
||||||
|
create["id"] = appConfig.Server.PublicAddress + p.Path
|
||||||
|
create["published"] = n.Published
|
||||||
|
create["type"] = "Create"
|
||||||
|
create["object"] = n
|
||||||
|
apSendToAllFollowers(p.Blog, create)
|
||||||
|
}
|
||||||
|
|
||||||
|
func apUpdate(p *post) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
func apDelete(p *post) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
func 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)
|
||||||
|
// check we aren't following ourselves
|
||||||
|
if newFollower == follow["object"] {
|
||||||
|
// actor and object are equal
|
||||||
|
return
|
||||||
|
}
|
||||||
|
follower, err := apGetRemoteActor(newFollower)
|
||||||
|
if err != nil {
|
||||||
|
// Couldn't retrieve remote actor info
|
||||||
|
log.Println("Failed to retrieve remote actor info:", newFollower)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Add or update follower
|
||||||
|
apAddFollower(blogName, follower.ID, follower.Inbox)
|
||||||
|
// remove @context from the inner activity
|
||||||
|
delete(follow, "@context")
|
||||||
|
accept := make(map[string]interface{})
|
||||||
|
accept["@context"] = asContext
|
||||||
|
accept["to"] = follow["actor"]
|
||||||
|
_, accept["id"] = apNewID(blog)
|
||||||
|
accept["actor"] = blog.apIri()
|
||||||
|
accept["object"] = follow
|
||||||
|
accept["type"] = "Accept"
|
||||||
|
err = apSendSigned(blog, accept, follower.Inbox)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to accept: %s\n%s\n", follower.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Println("Follower accepted:", follower.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func apSendToAllFollowers(blog string, activity interface{}) {
|
||||||
|
followers, err := apGetAllFollowers(blog)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to retrieve followers:", err.Error())
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
apSendTo(appConfig.Blogs[blog], activity, followers)
|
||||||
|
}
|
||||||
|
|
||||||
|
func apSendTo(blog *configBlog, activity interface{}, followers map[string]string) {
|
||||||
|
for _, i := range followers {
|
||||||
|
go func(inbox string) {
|
||||||
|
_ = apSendSigned(blog, activity, inbox)
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func apSendSigned(blog *configBlog, activity interface{}, to string) error {
|
||||||
|
// Marshal to json
|
||||||
|
body, err := json.Marshal(activity)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Copy body to sign it
|
||||||
|
bodyCopy := make([]byte, len(body))
|
||||||
|
copy(bodyCopy, body)
|
||||||
|
// Create request context with timeout
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
// Create request
|
||||||
|
r, err := http.NewRequestWithContext(ctx, http.MethodPost, to, bytes.NewBuffer(body))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
iri, err := url.Parse(to)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.Header.Add("Accept-Charset", "utf-8")
|
||||||
|
r.Header.Add("Date", time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
|
||||||
|
r.Header.Add("User-Agent", "GoBlog")
|
||||||
|
r.Header.Add("Accept", contentTypeASUTF8)
|
||||||
|
r.Header.Add(contentType, contentTypeASUTF8)
|
||||||
|
r.Header.Add("Host", iri.Host)
|
||||||
|
// Sign request
|
||||||
|
apPostSignMutex.Lock()
|
||||||
|
err = apPostSigner.SignRequest(apPrivateKey, blog.apIri()+"#main-key", r, bodyCopy)
|
||||||
|
apPostSignMutex.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Do request
|
||||||
|
resp, err := http.DefaultClient.Do(r)
|
||||||
|
if !apRequestIsSuccess(resp.StatusCode) {
|
||||||
|
body, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
return fmt.Errorf("signed request failed with status %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func apNewID(blog *configBlog) (hash string, url string) {
|
||||||
|
return hash, blog.apIri() + generateRandomString(16)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *configBlog) apIri() string {
|
||||||
|
return appConfig.Server.PublicAddress + b.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
func apRequestIsSuccess(code int) bool {
|
||||||
|
return code == http.StatusOK || code == http.StatusCreated || code == http.StatusAccepted || code == http.StatusNoContent
|
||||||
|
}
|
|
@ -0,0 +1,154 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/araddon/dateparse"
|
||||||
|
)
|
||||||
|
|
||||||
|
var asContext = []string{"https://www.w3.org/ns/activitystreams"}
|
||||||
|
|
||||||
|
func manipulateAsPath(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
if lowerAccept := strings.ToLower(r.Header.Get("Accept")); (strings.Contains(lowerAccept, contentTypeAS) || strings.Contains(lowerAccept, "application/ld+json")) && !strings.Contains(lowerAccept, contentTypeHTML) {
|
||||||
|
// Is ActivityStream, add ".as" to differentiate cache and also trigger as function
|
||||||
|
r.URL.Path += ".as"
|
||||||
|
}
|
||||||
|
next.ServeHTTP(rw, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type asNote struct {
|
||||||
|
Context interface{} `json:"@context,omitempty"`
|
||||||
|
To []string `json:"to,omitempty"`
|
||||||
|
InReplyTo string `json:"inReplyTo,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Content string `json:"content,omitempty"`
|
||||||
|
MediaType string `json:"mediaType,omitempty"`
|
||||||
|
Attachment []*asAttachment `json:"attachment,omitempty"`
|
||||||
|
Published string `json:"published,omitempty"`
|
||||||
|
Updated string `json:"updated,omitempty"`
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
AttributedTo string `json:"attributedTo,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type asPerson struct {
|
||||||
|
Context interface{} `json:"@context,omitempty"`
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Summary string `json:"summary,omitempty"`
|
||||||
|
PreferredUsername string `json:"preferredUsername,omitempty"`
|
||||||
|
Icon *asAttachment `json:"icon,omitempty"`
|
||||||
|
Inbox string `json:"inbox,omitempty"`
|
||||||
|
PublicKey *asPublicKey `json:"publicKey,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type asAttachment struct {
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type asPublicKey struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Owner string `json:"owner,omitempty"`
|
||||||
|
PublicKeyPem string `json:"publicKeyPem,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *post) serveActivityStreams(w http.ResponseWriter) {
|
||||||
|
// Send JSON
|
||||||
|
w.Header().Add(contentType, contentTypeASUTF8)
|
||||||
|
_ = json.NewEncoder(w).Encode(p.toASNote())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *post) toASNote() *asNote {
|
||||||
|
// Create a Note object
|
||||||
|
as := &asNote{
|
||||||
|
Context: asContext,
|
||||||
|
To: []string{"https://www.w3.org/ns/activitystreams#Public"},
|
||||||
|
MediaType: contentTypeHTML,
|
||||||
|
ID: appConfig.Server.PublicAddress + p.Path,
|
||||||
|
URL: appConfig.Server.PublicAddress + p.Path,
|
||||||
|
AttributedTo: appConfig.Blogs[p.Blog].apIri(),
|
||||||
|
}
|
||||||
|
// Name and Type
|
||||||
|
if title := p.title(); title != "" {
|
||||||
|
as.Name = title
|
||||||
|
as.Type = "Article"
|
||||||
|
} else {
|
||||||
|
as.Type = "Note"
|
||||||
|
}
|
||||||
|
// Content
|
||||||
|
as.Content = string(p.html())
|
||||||
|
// Attachments
|
||||||
|
if images := p.Parameters[appConfig.Blogs[p.Blog].ActivityStreams.ImagesParameter]; len(images) > 0 {
|
||||||
|
for _, image := range images {
|
||||||
|
as.Attachment = append(as.Attachment, &asAttachment{
|
||||||
|
Type: "Image",
|
||||||
|
URL: image,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Dates
|
||||||
|
dateFormat := "2006-01-02T15:04:05-07:00"
|
||||||
|
if p.Published != "" {
|
||||||
|
if t, err := dateparse.ParseIn(p.Published, time.Local); err == nil {
|
||||||
|
as.Published = t.Format(dateFormat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p.Updated != "" {
|
||||||
|
if t, err := dateparse.ParseIn(p.Updated, time.Local); err == nil {
|
||||||
|
as.Updated = t.Format(dateFormat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Reply
|
||||||
|
if replyLink := p.firstParameter(appConfig.Blogs[p.Blog].ActivityStreams.ReplyParameter); replyLink != "" {
|
||||||
|
as.InReplyTo = replyLink
|
||||||
|
}
|
||||||
|
return as
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *configBlog) serveActivityStreams(blog string, w http.ResponseWriter) {
|
||||||
|
publicKeyDer, err := x509.MarshalPKIXPublicKey(&apPrivateKey.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to marshal public key", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Send JSON
|
||||||
|
w.Header().Add(contentType, contentTypeASUTF8)
|
||||||
|
asBlog := &asPerson{
|
||||||
|
Context: asContext,
|
||||||
|
Type: "Person",
|
||||||
|
ID: b.apIri(),
|
||||||
|
URL: b.apIri(),
|
||||||
|
Name: b.Title,
|
||||||
|
Summary: b.Description,
|
||||||
|
PreferredUsername: blog,
|
||||||
|
Inbox: appConfig.Server.PublicAddress + "/activitypub/inbox/" + blog,
|
||||||
|
PublicKey: &asPublicKey{
|
||||||
|
Owner: appConfig.Server.PublicAddress + b.Path,
|
||||||
|
ID: appConfig.Server.PublicAddress + b.Path + "#main-key",
|
||||||
|
PublicKeyPem: string(pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "PUBLIC KEY",
|
||||||
|
Headers: nil,
|
||||||
|
Bytes: publicKeyDer,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// Add profile picture
|
||||||
|
if appConfig.User.Picture != "" {
|
||||||
|
asBlog.Icon = &asAttachment{
|
||||||
|
Type: "Image",
|
||||||
|
URL: appConfig.User.Picture,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(asBlog)
|
||||||
|
|
||||||
|
}
|
|
@ -1,124 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/araddon/dateparse"
|
|
||||||
)
|
|
||||||
|
|
||||||
func manipulateAsPath(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
||||||
if lowerAccept := strings.ToLower(r.Header.Get("Accept")); (strings.Contains(lowerAccept, "application/activity+json") || strings.Contains(lowerAccept, "application/ld+json")) && !strings.Contains(lowerAccept, "text/html") {
|
|
||||||
// Is ActivityStream, add ".as" to differentiate cache and also trigger as function
|
|
||||||
r.URL.Path += ".as"
|
|
||||||
}
|
|
||||||
next.ServeHTTP(rw, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type asPost struct {
|
|
||||||
Context []string `json:"@context"`
|
|
||||||
To []string `json:"to"`
|
|
||||||
InReplyTo string `json:"inReplyTo,omitempty"`
|
|
||||||
Name string `json:"name,omitempty"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Content string `json:"content"`
|
|
||||||
MediaType string `json:"mediaType"`
|
|
||||||
Attachment []*asAttachment `json:"attachment,omitempty"`
|
|
||||||
Published string `json:"published"`
|
|
||||||
Updated string `json:"updated,omitempty"`
|
|
||||||
ID string `json:"id"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
AttributedTo string `json:"attributedTo"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type asAttachment struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func servePostActivityStreams(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// Remove ".as" from path again
|
|
||||||
r.URL.Path = strings.TrimSuffix(r.URL.Path, ".as")
|
|
||||||
// Fetch post from db
|
|
||||||
p, err := getPost(slashTrimmedPath(r))
|
|
||||||
if err == errPostNotFound {
|
|
||||||
serve404(w, r)
|
|
||||||
return
|
|
||||||
} else if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Create a Note object
|
|
||||||
as := &asPost{
|
|
||||||
Context: []string{"https://www.w3.org/ns/activitystreams"},
|
|
||||||
To: []string{"https://www.w3.org/ns/activitystreams#Public"},
|
|
||||||
MediaType: "text/html",
|
|
||||||
ID: appConfig.Server.PublicAddress + p.Path,
|
|
||||||
URL: appConfig.Server.PublicAddress + p.Path,
|
|
||||||
AttributedTo: appConfig.Server.PublicAddress,
|
|
||||||
}
|
|
||||||
// Name and Type
|
|
||||||
if title := p.title(); title != "" {
|
|
||||||
as.Name = title
|
|
||||||
as.Type = "Article"
|
|
||||||
} else {
|
|
||||||
as.Type = "Note"
|
|
||||||
}
|
|
||||||
// Content
|
|
||||||
as.Content = string(p.html())
|
|
||||||
// Attachments
|
|
||||||
if images := p.Parameters[appConfig.Blogs[p.Blog].ActivityStreams.ImagesParameter]; len(images) > 0 {
|
|
||||||
for _, image := range images {
|
|
||||||
as.Attachment = append(as.Attachment, &asAttachment{
|
|
||||||
Type: "Image",
|
|
||||||
URL: image,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Dates
|
|
||||||
dateFormat := "2006-01-02T15:04:05-07:00"
|
|
||||||
if p.Published != "" {
|
|
||||||
if t, err := dateparse.ParseIn(p.Published, time.Local); err == nil {
|
|
||||||
as.Published = t.Format(dateFormat)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if p.Updated != "" {
|
|
||||||
if t, err := dateparse.ParseIn(p.Updated, time.Local); err == nil {
|
|
||||||
as.Published = t.Format(dateFormat)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Reply
|
|
||||||
if replyLink := p.firstParameter(appConfig.Blogs[p.Blog].ActivityStreams.ReplyParameter); replyLink != "" {
|
|
||||||
as.InReplyTo = replyLink
|
|
||||||
}
|
|
||||||
// Send JSON
|
|
||||||
w.Header().Add(contentType, contentTypeJSONUTF8)
|
|
||||||
_ = json.NewEncoder(w).Encode(as)
|
|
||||||
}
|
|
||||||
|
|
||||||
type asPerson struct {
|
|
||||||
Context []string `json:"@context"`
|
|
||||||
ID string `json:"id"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Summary string `json:"summary"`
|
|
||||||
Attachment []struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
} `json:"attachment"`
|
|
||||||
PreferredUsername string `json:"preferredUsername"`
|
|
||||||
Icon struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
} `json:"icon"`
|
|
||||||
Inbox string `json:"inbox"`
|
|
||||||
PublicKey struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Owner string `json:"owner"`
|
|
||||||
PublicKeyPem string `json:"publicKeyPem"`
|
|
||||||
} `json:"publicKey"`
|
|
||||||
}
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
func (blog *configBlog) getRelativePath(path string) string {
|
||||||
|
if !strings.HasPrefix(path, "/") {
|
||||||
|
path = "/" + path
|
||||||
|
}
|
||||||
|
if blog.Path != "/" {
|
||||||
|
return blog.Path + path
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ type config struct {
|
||||||
Hugo *configHugo `mapstructure:"hugo"`
|
Hugo *configHugo `mapstructure:"hugo"`
|
||||||
Micropub *configMicropub `mapstructure:"micropub"`
|
Micropub *configMicropub `mapstructure:"micropub"`
|
||||||
PathRedirects []*configRegexRedirect `mapstructure:"pathRedirects"`
|
PathRedirects []*configRegexRedirect `mapstructure:"pathRedirects"`
|
||||||
|
ActivityPub *configActivityPub `mapstructure:"activityPub"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type configServer struct {
|
type configServer struct {
|
||||||
|
@ -102,6 +103,7 @@ type configUser struct {
|
||||||
Nick string `mapstructure:"nick"`
|
Nick string `mapstructure:"nick"`
|
||||||
Name string `mapstructure:"name"`
|
Name string `mapstructure:"name"`
|
||||||
Password string `mapstructure:"password"`
|
Password string `mapstructure:"password"`
|
||||||
|
Picture string `mapstructure:"picture"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type configHooks struct {
|
type configHooks struct {
|
||||||
|
@ -145,6 +147,11 @@ type configRegexRedirect struct {
|
||||||
To string `mapstructure:"to"`
|
To string `mapstructure:"to"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type configActivityPub struct {
|
||||||
|
Enabled bool `mapstructure:"enabled"`
|
||||||
|
KeyPath string `mapstructure:"keyPath"`
|
||||||
|
}
|
||||||
|
|
||||||
var appConfig = &config{}
|
var appConfig = &config{}
|
||||||
|
|
||||||
func initConfig() error {
|
func initConfig() error {
|
||||||
|
@ -176,6 +183,7 @@ func initConfig() error {
|
||||||
viper.SetDefault("micropub.audioParam", "audio")
|
viper.SetDefault("micropub.audioParam", "audio")
|
||||||
viper.SetDefault("micropub.photoParam", "images")
|
viper.SetDefault("micropub.photoParam", "images")
|
||||||
viper.SetDefault("micropub.photoDescriptionParam", "imagealts")
|
viper.SetDefault("micropub.photoDescriptionParam", "imagealts")
|
||||||
|
viper.SetDefault("activityPub.keyPath", "data/private.pem")
|
||||||
// Unmarshal config
|
// Unmarshal config
|
||||||
err = viper.Unmarshal(appConfig)
|
err = viper.Unmarshal(appConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
27
database.go
27
database.go
|
@ -1,14 +1,10 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
"github.com/schollz/sqlite3dump"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var appDb *sql.DB
|
var appDb *sql.DB
|
||||||
|
@ -28,7 +24,6 @@ func startWritingToDb() {
|
||||||
|
|
||||||
func finishWritingToDb() {
|
func finishWritingToDb() {
|
||||||
appDbWriteMutex.Unlock()
|
appDbWriteMutex.Unlock()
|
||||||
dumpDb()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func closeDb() error {
|
func closeDb() error {
|
||||||
|
@ -41,25 +36,3 @@ func vacuumDb() {
|
||||||
defer finishWritingToDb()
|
defer finishWritingToDb()
|
||||||
_, _ = appDb.Exec("VACUUM;")
|
_, _ = appDb.Exec("VACUUM;")
|
||||||
}
|
}
|
||||||
|
|
||||||
func dumpDb() {
|
|
||||||
appDbWriteMutex.Lock()
|
|
||||||
defer appDbWriteMutex.Unlock()
|
|
||||||
f, err := os.OpenFile(appConfig.Db.File+".dump", os.O_RDWR|os.O_CREATE, 0644)
|
|
||||||
defer f.Close()
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to open dump file:", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w := bufio.NewWriter(f)
|
|
||||||
err = sqlite3dump.DumpDB(appDb, w)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to dump database:", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = w.Flush()
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to write dump:", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -22,15 +22,8 @@ func migrateDb() error {
|
||||||
CREATE TABLE indieauthauth (time text not null, code text not null, me text not null, client text not null, redirect text not null, scope text not null);
|
CREATE TABLE indieauthauth (time text not null, code text not null, me text not null, client text not null, redirect text not null, scope text not null);
|
||||||
CREATE TABLE indieauthtoken (time text not null, token text not null, me text not null, client text not null, scope text not null);
|
CREATE TABLE indieauthtoken (time text not null, token text not null, me text not null, client text not null, scope text not null);
|
||||||
CREATE INDEX index_iat_token on indieauthtoken (token);
|
CREATE INDEX index_iat_token on indieauthtoken (token);
|
||||||
`)
|
|
||||||
return err
|
|
||||||
},
|
|
||||||
},
|
|
||||||
&migrator.Migration{
|
|
||||||
Name: "00002",
|
|
||||||
Func: func(tx *sql.Tx) error {
|
|
||||||
_, err := tx.Exec(`
|
|
||||||
CREATE TABLE autocert (key text not null primary key, data blob not null, created text not null);
|
CREATE TABLE autocert (key text not null primary key, data blob not null, created text not null);
|
||||||
|
CREATE TABLE activitypub_followers (blog text not null, follower text not null, inbox text not null, primary key (blog, follower));
|
||||||
`)
|
`)
|
||||||
return err
|
return err
|
||||||
},
|
},
|
||||||
|
|
10
go.mod
10
go.mod
|
@ -8,6 +8,7 @@ require (
|
||||||
github.com/andybalholm/cascadia v1.2.0 // indirect
|
github.com/andybalholm/cascadia v1.2.0 // indirect
|
||||||
github.com/araddon/dateparse v0.0.0-20201001162425-8aadafed4dc4
|
github.com/araddon/dateparse v0.0.0-20201001162425-8aadafed4dc4
|
||||||
github.com/go-chi/chi v4.1.2+incompatible
|
github.com/go-chi/chi v4.1.2+incompatible
|
||||||
|
github.com/go-fed/httpsig v1.0.1-0.20200711113112-812070f75b67
|
||||||
github.com/goodsign/monday v1.0.1-0.20201007115131-c065b60ec611
|
github.com/goodsign/monday v1.0.1-0.20201007115131-c065b60ec611
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect
|
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect
|
||||||
github.com/gorilla/feeds v1.1.1
|
github.com/gorilla/feeds v1.1.1
|
||||||
|
@ -24,21 +25,20 @@ require (
|
||||||
github.com/mitchellh/mapstructure v1.3.3 // indirect
|
github.com/mitchellh/mapstructure v1.3.3 // indirect
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||||
github.com/pelletier/go-toml v1.8.1 // indirect
|
github.com/pelletier/go-toml v1.8.1 // indirect
|
||||||
github.com/schollz/sqlite3dump v1.2.4
|
|
||||||
github.com/smartystreets/assertions v1.2.0 // indirect
|
github.com/smartystreets/assertions v1.2.0 // indirect
|
||||||
github.com/snabb/sitemap v1.0.0
|
github.com/snabb/sitemap v1.0.0
|
||||||
github.com/spf13/afero v1.4.1 // indirect
|
github.com/spf13/afero v1.4.1 // indirect
|
||||||
github.com/spf13/cast v1.3.1
|
github.com/spf13/cast v1.3.1
|
||||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||||
github.com/spf13/viper v1.7.1
|
github.com/spf13/viper v1.7.1
|
||||||
github.com/tdewolff/minify/v2 v2.9.9
|
github.com/tdewolff/minify/v2 v2.9.10
|
||||||
github.com/vcraescu/go-paginator v0.0.0-20200923074551-426b20f3ae8a
|
github.com/vcraescu/go-paginator v0.0.0-20200923074551-426b20f3ae8a
|
||||||
github.com/yuin/goldmark v1.2.1
|
github.com/yuin/goldmark v1.2.1
|
||||||
github.com/yuin/goldmark-emoji v1.0.1
|
github.com/yuin/goldmark-emoji v1.0.1
|
||||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
|
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
|
||||||
golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0 // indirect
|
golang.org/x/net v0.0.0-20201026091529-146b70c837a4 // indirect
|
||||||
golang.org/x/sync v0.0.0-20201008141435-b3e1573b7520
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
|
||||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13 // indirect
|
golang.org/x/sys v0.0.0-20201026133411-418715ba6fdd // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
|
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
|
||||||
gopkg.in/ini.v1 v1.62.0 // indirect
|
gopkg.in/ini.v1 v1.62.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.3.0 // indirect
|
gopkg.in/yaml.v2 v2.3.0 // indirect
|
||||||
|
|
21
go.sum
21
go.sum
|
@ -63,6 +63,8 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
|
github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
|
||||||
github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||||
|
github.com/go-fed/httpsig v1.0.1-0.20200711113112-812070f75b67 h1:T4Tv75EmaqlHubh4+cK2eSySNvNA8O6gRB6qwuzfOCM=
|
||||||
|
github.com/go-fed/httpsig v1.0.1-0.20200711113112-812070f75b67/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
|
@ -179,7 +181,6 @@ github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O
|
||||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
|
||||||
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
|
github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
|
||||||
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||||
|
@ -230,8 +231,6 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T
|
||||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||||
github.com/schollz/sqlite3dump v1.2.4 h1:b3dgcKLsHZhF6OsB2EK+e/oA77vh4P/45TAh2R35OFI=
|
|
||||||
github.com/schollz/sqlite3dump v1.2.4/go.mod h1:SEajZA5udi52Taht5xQYlFfHwr7AIrqPrLDrAoFv17o=
|
|
||||||
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
|
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
|
||||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
|
@ -275,8 +274,8 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||||
github.com/tdewolff/minify/v2 v2.9.9 h1:5POLhoyTEWNNHADzlwH83AhvpKVAdpS7fOfaWIOWOTw=
|
github.com/tdewolff/minify/v2 v2.9.10 h1:p+ifTTl+JMFFLDYNAm7nxQ9XuCG10HTW00wlPAZ7aoE=
|
||||||
github.com/tdewolff/minify/v2 v2.9.9/go.mod h1:U1Nc+/YBSB0FPEarqcgkYH3Ep4DNyyIbOyl5P4eWMuo=
|
github.com/tdewolff/minify/v2 v2.9.10/go.mod h1:U1Nc+/YBSB0FPEarqcgkYH3Ep4DNyyIbOyl5P4eWMuo=
|
||||||
github.com/tdewolff/parse/v2 v2.5.5 h1:b7ICJa4I/54JQGEGgTte8DiyJPKcC5g8V773QMzkeUM=
|
github.com/tdewolff/parse/v2 v2.5.5 h1:b7ICJa4I/54JQGEGgTte8DiyJPKcC5g8V773QMzkeUM=
|
||||||
github.com/tdewolff/parse/v2 v2.5.5/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho=
|
github.com/tdewolff/parse/v2 v2.5.5/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho=
|
||||||
github.com/tdewolff/test v1.0.6 h1:76mzYJQ83Op284kMT+63iCNCI7NEERsIN8dLM+RiKr4=
|
github.com/tdewolff/test v1.0.6 h1:76mzYJQ83Op284kMT+63iCNCI7NEERsIN8dLM+RiKr4=
|
||||||
|
@ -343,8 +342,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
|
||||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0 h1:5kGOVHlq0euqwzgTC9Vu15p6fV1Wi0ArVi8da2urnVg=
|
golang.org/x/net v0.0.0-20201026091529-146b70c837a4 h1:awiuzyrRjJDb+OXi9ceHO3SDxVoN3JER57mhtqkdQBs=
|
||||||
golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201026091529-146b70c837a4/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
@ -354,8 +353,8 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
|
||||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201008141435-b3e1573b7520 h1:Bx6FllMpG4NWDOfhMBz1VR2QYNp/SAOHPIAsaVmxfPo=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
|
||||||
golang.org/x/sync v0.0.0-20201008141435-b3e1573b7520/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
@ -375,8 +374,8 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||||
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c h1:UIcGWL6/wpCfyGuJnRFJRurA+yj8RrW7Q6x2YMCXt6c=
|
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c h1:UIcGWL6/wpCfyGuJnRFJRurA+yj8RrW7Q6x2YMCXt6c=
|
||||||
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13 h1:5jaG59Zhd+8ZXe8C+lgiAGqkOaZBruqrWclLkgAww34=
|
golang.org/x/sys v0.0.0-20201026133411-418715ba6fdd h1:+7OQgGrJBd80e8ASl94G3xIpokulXXzB/dikfre4ho0=
|
||||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201026133411-418715ba6fdd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||||
|
|
75
http.go
75
http.go
|
@ -7,31 +7,39 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
"github.com/go-chi/chi/middleware"
|
"github.com/go-chi/chi/middleware"
|
||||||
"golang.org/x/crypto/acme/autocert"
|
"golang.org/x/crypto/acme/autocert"
|
||||||
)
|
)
|
||||||
|
|
||||||
const contentType = "Content-Type"
|
const (
|
||||||
const charsetUtf8Suffix = "; charset=utf-8"
|
contentType = "Content-Type"
|
||||||
const contentTypeHTML = "text/html"
|
|
||||||
const contentTypeHTMLUTF8 = contentTypeHTML + charsetUtf8Suffix
|
|
||||||
const contentTypeJSON = "application/json"
|
|
||||||
const contentTypeJSONUTF8 = contentTypeJSON + charsetUtf8Suffix
|
|
||||||
const contentTypeWWWForm = "application/x-www-form-urlencoded"
|
|
||||||
const contentTypeMultipartForm = "multipart/form-data"
|
|
||||||
|
|
||||||
var d *dynamicHandler
|
charsetUtf8Suffix = "; charset=utf-8"
|
||||||
|
|
||||||
|
contentTypeHTML = "text/html"
|
||||||
|
contentTypeJSON = "application/json"
|
||||||
|
contentTypeWWWForm = "application/x-www-form-urlencoded"
|
||||||
|
contentTypeMultipartForm = "multipart/form-data"
|
||||||
|
contentTypeAS = "application/activity+json"
|
||||||
|
|
||||||
|
contentTypeHTMLUTF8 = contentTypeHTML + charsetUtf8Suffix
|
||||||
|
contentTypeJSONUTF8 = contentTypeJSON + charsetUtf8Suffix
|
||||||
|
contentTypeASUTF8 = contentTypeAS + charsetUtf8Suffix
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
d *dynamicHandler
|
||||||
|
)
|
||||||
|
|
||||||
func startServer() (err error) {
|
func startServer() (err error) {
|
||||||
d = newDynamicHandler()
|
d = &dynamicHandler{}
|
||||||
h, err := buildHandler()
|
err = reloadRouter()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
d.swapHandler(h)
|
|
||||||
localAddress := ":" + strconv.Itoa(appConfig.Server.Port)
|
localAddress := ":" + strconv.Itoa(appConfig.Server.Port)
|
||||||
if appConfig.Server.PublicHTTPS {
|
if appConfig.Server.PublicHTTPS {
|
||||||
cache, err := newAutocertCache()
|
cache, err := newAutocertCache()
|
||||||
|
@ -83,6 +91,9 @@ func buildHandler() (http.Handler, error) {
|
||||||
r.Use(middleware.Compress(flate.DefaultCompression))
|
r.Use(middleware.Compress(flate.DefaultCompression))
|
||||||
r.Use(middleware.StripSlashes)
|
r.Use(middleware.StripSlashes)
|
||||||
r.Use(middleware.GetHead)
|
r.Use(middleware.GetHead)
|
||||||
|
if !appConfig.Cache.Enable {
|
||||||
|
r.Use(middleware.NoCache)
|
||||||
|
}
|
||||||
|
|
||||||
// Profiler
|
// Profiler
|
||||||
if appConfig.Server.Debug {
|
if appConfig.Server.Debug {
|
||||||
|
@ -119,14 +130,27 @@ func buildHandler() (http.Handler, error) {
|
||||||
indieauthRouter.Post("/token", indieAuthToken)
|
indieauthRouter.Post("/token", indieAuthToken)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ActivityPub and stuff
|
||||||
|
if appConfig.ActivityPub.Enabled {
|
||||||
|
r.Post("/activitypub/inbox/{blog}", apHandleInbox)
|
||||||
|
r.Get("/.well-known/webfinger", apHandleWebfinger)
|
||||||
|
r.Get("/.well-known/host-meta", handleWellKnownHostMeta)
|
||||||
|
}
|
||||||
|
|
||||||
// Posts
|
// Posts
|
||||||
allPostPaths, err := allPostPaths()
|
allPostPaths, err := allPostPaths()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
var postMW []func(http.Handler) http.Handler
|
||||||
|
if appConfig.ActivityPub.Enabled {
|
||||||
|
postMW = []func(http.Handler) http.Handler{manipulateAsPath, cacheMiddleware, minifier.Middleware}
|
||||||
|
} else {
|
||||||
|
postMW = []func(http.Handler) http.Handler{cacheMiddleware, minifier.Middleware}
|
||||||
|
}
|
||||||
for _, path := range allPostPaths {
|
for _, path := range allPostPaths {
|
||||||
if path != "" {
|
if path != "" {
|
||||||
r.With(manipulateAsPath, cacheMiddleware, minifier.Middleware).Get(path, servePost)
|
r.With(postMW...).Get(path, servePost)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,7 +215,13 @@ func buildHandler() (http.Handler, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blog
|
// Blog
|
||||||
r.With(cacheMiddleware, minifier.Middleware).Get(fullBlogPath, serveHome(blog, blogPath))
|
var mw []func(http.Handler) http.Handler
|
||||||
|
if appConfig.ActivityPub.Enabled {
|
||||||
|
mw = []func(http.Handler) http.Handler{manipulateAsPath, cacheMiddleware, minifier.Middleware}
|
||||||
|
} else {
|
||||||
|
mw = []func(http.Handler) http.Handler{cacheMiddleware, minifier.Middleware}
|
||||||
|
}
|
||||||
|
r.With(mw...).Get(fullBlogPath, serveHome(blog, blogPath))
|
||||||
r.With(cacheMiddleware, minifier.Middleware).Get(fullBlogPath+feedPath, serveHome(blog, blogPath))
|
r.With(cacheMiddleware, minifier.Middleware).Get(fullBlogPath+feedPath, serveHome(blog, blogPath))
|
||||||
r.With(cacheMiddleware, minifier.Middleware).Get(blogPath+paginationPath, serveHome(blog, blogPath))
|
r.With(cacheMiddleware, minifier.Middleware).Get(blogPath+paginationPath, serveHome(blog, blogPath))
|
||||||
|
|
||||||
|
@ -228,24 +258,15 @@ func securityHeaders(next http.Handler) http.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
type dynamicHandler struct {
|
type dynamicHandler struct {
|
||||||
realHandler http.Handler
|
realHandler atomic.Value
|
||||||
changeMutex *sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func newDynamicHandler() *dynamicHandler {
|
|
||||||
return &dynamicHandler{
|
|
||||||
changeMutex: &sync.Mutex{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *dynamicHandler) swapHandler(h http.Handler) {
|
func (d *dynamicHandler) swapHandler(h http.Handler) {
|
||||||
d.changeMutex.Lock()
|
d.realHandler.Store(h)
|
||||||
d.realHandler = h
|
|
||||||
d.changeMutex.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *dynamicHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (d *dynamicHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
d.realHandler.ServeHTTP(w, r)
|
d.realHandler.Load().(http.Handler).ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func slashTrimmedPath(r *http.Request) string {
|
func slashTrimmedPath(r *http.Request) string {
|
||||||
|
|
7
main.go
7
main.go
|
@ -50,6 +50,13 @@ func main() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
initCache()
|
initCache()
|
||||||
|
if appConfig.ActivityPub.Enabled {
|
||||||
|
err = initActivityPub()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Start cron hooks
|
// Start cron hooks
|
||||||
startHourlyHooks()
|
startHourlyHooks()
|
||||||
|
|
25
posts.go
25
posts.go
|
@ -29,9 +29,9 @@ type post struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func servePost(w http.ResponseWriter, r *http.Request) {
|
func servePost(w http.ResponseWriter, r *http.Request) {
|
||||||
if strings.HasSuffix(r.URL.Path, ".as") {
|
as := strings.HasSuffix(r.URL.Path, ".as")
|
||||||
servePostActivityStreams(w, r)
|
if as {
|
||||||
return
|
r.URL.Path = strings.TrimSuffix(r.URL.Path, ".as")
|
||||||
}
|
}
|
||||||
path := slashTrimmedPath(r)
|
path := slashTrimmedPath(r)
|
||||||
p, err := getPost(path)
|
p, err := getPost(path)
|
||||||
|
@ -42,6 +42,10 @@ func servePost(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if as {
|
||||||
|
p.serveActivityStreams(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
render(w, templatePost, &renderData{
|
render(w, templatePost, &renderData{
|
||||||
blogString: p.Blog,
|
blogString: p.Blog,
|
||||||
Data: p,
|
Data: p,
|
||||||
|
@ -87,10 +91,17 @@ func (p *postPaginationAdapter) Slice(offset, length int, data interface{}) erro
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveHome(blog string, path string) func(w http.ResponseWriter, r *http.Request) {
|
func serveHome(blog string, path string) func(w http.ResponseWriter, r *http.Request) {
|
||||||
return serveIndex(&indexConfig{
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
blog: blog,
|
as := strings.HasSuffix(r.URL.Path, ".as")
|
||||||
path: path,
|
if as {
|
||||||
})
|
appConfig.Blogs[blog].serveActivityStreams(blog, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serveIndex(&indexConfig{
|
||||||
|
blog: blog,
|
||||||
|
path: path,
|
||||||
|
})(w, r)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveSection(blog string, path string, section *section) func(w http.ResponseWriter, r *http.Request) {
|
func serveSection(blog string, path string, section *section) func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
|
@ -62,7 +62,7 @@ func (p *post) checkPost() error {
|
||||||
p.Section = appConfig.Blogs[p.Blog].DefaultSection
|
p.Section = appConfig.Blogs[p.Blog].DefaultSection
|
||||||
}
|
}
|
||||||
if p.Slug == "" {
|
if p.Slug == "" {
|
||||||
random := generateRandomString(now, 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.ParseIn(p.Published, time.Local)
|
published, _ := dateparse.ParseIn(p.Published, time.Local)
|
||||||
|
|
|
@ -98,13 +98,7 @@ func initRendering() error {
|
||||||
"urlize": urlize,
|
"urlize": urlize,
|
||||||
"sort": sortedStrings,
|
"sort": sortedStrings,
|
||||||
"blogRelative": func(blog *configBlog, path string) string {
|
"blogRelative": func(blog *configBlog, path string) string {
|
||||||
if !strings.HasPrefix(path, "/") {
|
return blog.getRelativePath(path)
|
||||||
path = "/" + path
|
|
||||||
}
|
|
||||||
if blog.Path != "/" {
|
|
||||||
return blog.Path + path
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
},
|
},
|
||||||
"jsonFile": func(filename string) *map[string]interface{} {
|
"jsonFile": func(filename string) *map[string]interface{} {
|
||||||
parsed := &map[string]interface{}{}
|
parsed := &map[string]interface{}{}
|
||||||
|
|
6
utils.go
6
utils.go
|
@ -26,10 +26,10 @@ func sortedStrings(s []string) []string {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateRandomString(now time.Time, n int) string {
|
func generateRandomString(chars int) string {
|
||||||
rand.Seed(now.UnixNano())
|
rand.Seed(time.Now().UnixNano())
|
||||||
letters := []rune("abcdefghijklmnopqrstuvwxyz")
|
letters := []rune("abcdefghijklmnopqrstuvwxyz")
|
||||||
b := make([]rune, n)
|
b := make([]rune, chars)
|
||||||
for i := range b {
|
for i := range b {
|
||||||
b[i] = letters[rand.Intn(len(letters))]
|
b[i] = letters[rand.Intn(len(letters))]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue