Add support for MicroPub channels and fix a few issues with post checking and the editor preview

This commit is contained in:
Jan-Lukas Else 2022-06-15 14:44:12 +02:00
parent d501855450
commit 1aa8b1b35f
8 changed files with 109 additions and 77 deletions

View File

@ -65,16 +65,20 @@ func (a *goBlog) createMarkdownPreview(w io.Writer, blog string, markdown io.Rea
Blog: blog,
Content: string(md),
}
if err = a.computeExtraPostParameters(p); err != nil {
if err = a.extractParamsFromContent(p); err != nil {
_, _ = io.WriteString(w, err.Error())
return
}
if err := a.checkPost(p); err != nil {
_, _ = io.WriteString(w, err.Error())
return
}
if t := p.Title(); t != "" {
p.RenderedTitle = a.renderMdTitle(t)
}
// Render post
// Render post (using post's blog config)
hb := newHtmlBuilder(w)
a.renderEditorPreview(hb, a.cfg.Blogs[blog], p)
a.renderEditorPreview(hb, a.cfg.Blogs[p.Blog], p)
}
func (a *goBlog) serveEditorPost(w http.ResponseWriter, r *http.Request) {

8
go.mod
View File

@ -28,7 +28,7 @@ require (
github.com/gorilla/sessions v1.2.1
github.com/gorilla/websocket v1.5.0
github.com/hacdias/indieauth/v2 v2.1.0
github.com/jlaffaye/ftp v0.0.0-20220524001917-dfa1e758f3af
github.com/jlaffaye/ftp v0.0.0-20220612151834-60a941566ce4
// master
github.com/jlelse/feeds v1.2.1-0.20210704161900-189f94254ad4
github.com/justinas/alice v1.2.0
@ -49,7 +49,7 @@ require (
github.com/spf13/cast v1.5.0
github.com/spf13/viper v1.12.0
github.com/stretchr/testify v1.7.2
github.com/tdewolff/minify/v2 v2.11.9
github.com/tdewolff/minify/v2 v2.11.10
// master
github.com/tkrajina/gpxgo v1.2.2-0.20220217201249-321f19554eec
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80
@ -58,7 +58,7 @@ require (
// master
github.com/yuin/goldmark-emoji v1.0.2-0.20210607094911-0487583eca38
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e
golang.org/x/net v0.0.0-20220607020251-c690dde0001d
golang.org/x/net v0.0.0-20220614195744-fb05da6f9022
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f
golang.org/x/text v0.3.7
gopkg.in/yaml.v3 v3.0.1
@ -137,7 +137,7 @@ require (
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 // indirect
github.com/tcnksm/go-httpstat v0.2.0 // indirect
github.com/tdewolff/parse/v2 v2.5.33 // indirect
github.com/tdewolff/parse/v2 v2.6.0 // indirect
github.com/u-root/uio v0.0.0-20210528151154-e40b768296a7 // indirect
github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54 // indirect
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect

16
go.sum
View File

@ -296,8 +296,8 @@ github.com/jackc/pgtype v1.10.0 h1:ILnBWrRMSXGczYvmkYD6PsYyVFUNLTnIUJHHDLmqk38=
github.com/jackc/pgx/v4 v4.15.0 h1:B7dTkXsdILD3MF987WGGCcg+tvLW6bZJdEcqVFeU//w=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jlaffaye/ftp v0.0.0-20220524001917-dfa1e758f3af h1:sh8vAWJ+vr9izhkDAMS3JRGDIjj0tNVwxfwd+2U2xMo=
github.com/jlaffaye/ftp v0.0.0-20220524001917-dfa1e758f3af/go.mod h1:oZaomI+9/et52UBjvNU9LCIqmgt816+7ljXCx0EIPzo=
github.com/jlaffaye/ftp v0.0.0-20220612151834-60a941566ce4 h1:gO/ufFrST8nt0py0FvlYHsSW81RCYeqflr8czF+UBys=
github.com/jlaffaye/ftp v0.0.0-20220612151834-60a941566ce4/go.mod h1:YFstjM4Y5zZdsON18Az8MNRgObXGJgor/UBMEQtZJes=
github.com/jlelse/feeds v1.2.1-0.20210704161900-189f94254ad4 h1:d2oKwfgLl3ef0PyYDkzjsVyYlBZzNpOpXitDraOnVXc=
github.com/jlelse/feeds v1.2.1-0.20210704161900-189f94254ad4/go.mod h1:vt0iOV52/wq97Ql/jp7mUkqyrlEiGQuhHic4bVoHy0c=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
@ -467,10 +467,10 @@ github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQ
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
github.com/tdewolff/minify/v2 v2.11.9 h1:1q5728c0QICKlp2X1n7OiaiiFFzCzsq7uxAkv+eykT8=
github.com/tdewolff/minify/v2 v2.11.9/go.mod h1:XHKhaRF/vTa3EP4JX8oZ2CO4crGEtVOiSoqUED953wM=
github.com/tdewolff/parse/v2 v2.5.33 h1:D75KlhAeCSQg4Na8cWKehJdPJoZxwdpRbTZw7lZFWNQ=
github.com/tdewolff/parse/v2 v2.5.33/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho=
github.com/tdewolff/minify/v2 v2.11.10 h1:2tk9nuKfc8YOTD8glZ7JF/VtE8W5HOgmepWdjcPtRro=
github.com/tdewolff/minify/v2 v2.11.10/go.mod h1:dHOS3dk+nJ0M3q3uM3VlNzTb70cou+ov0ki7C4PAFgM=
github.com/tdewolff/parse/v2 v2.6.0 h1:f2D7w32JtqjCv6SczWkfwK+m15et42qEtDnZXHoNY70=
github.com/tdewolff/parse/v2 v2.6.0/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho=
github.com/tdewolff/test v1.0.6 h1:76mzYJQ83Op284kMT+63iCNCI7NEERsIN8dLM+RiKr4=
github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
@ -613,8 +613,8 @@ golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d h1:4SFsTMi4UahlKoloni7L4eYzhFRifURQLw+yv0QDCx8=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220614195744-fb05da6f9022 h1:0qjDla5xICC2suMtyRH/QqX3B1btXTfNsIt/i4LFgO0=
golang.org/x/net v0.0.0-20220614195744-fb05da6f9022/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
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-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=

View File

@ -3,13 +3,13 @@ package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"mime"
"net/http"
"net/url"
"regexp"
"strings"
"time"
"github.com/samber/lo"
"github.com/spf13/cast"
@ -24,10 +24,23 @@ func (a *goBlog) serveMicropubQuery(w http.ResponseWriter, r *http.Request) {
var result any
switch query := r.URL.Query(); query.Get("q") {
case "config":
type micropubConfig struct {
MediaEndpoint string `json:"media-endpoint"`
channels := []map[string]any{}
for b, bc := range a.cfg.Blogs {
channels = append(channels, map[string]any{
"name": fmt.Sprintf("%s: %s", b, bc.Title),
"uid": b,
})
for s, sc := range bc.Sections {
channels = append(channels, map[string]any{
"name": fmt.Sprintf("%s/%s: %s", b, s, sc.Name),
"uid": fmt.Sprintf("%s/%s", b, s),
})
}
}
result = map[string]any{
"channels": channels,
"media-endpoint": a.getFullAddress(micropubPath + micropubMediaSubPath),
}
result = micropubConfig{MediaEndpoint: a.getFullAddress(micropubPath + micropubMediaSubPath)}
case "source":
if urlString := query.Get("url"); urlString != "" {
u, err := url.Parse(query.Get("url"))
@ -150,6 +163,10 @@ func (a *goBlog) micropubParseValuePostParamsValueMap(entry *post, values map[st
entry.Slug = slug[0]
delete(values, "mp-slug")
}
if channel, ok := values["mp-channel"]; ok && len(channel) > 0 {
entry.setChannel(channel[0])
delete(values, "mp-channel")
}
// Status
statusStr := ""
if status, ok := values["post-status"]; ok && len(status) > 0 {
@ -252,6 +269,7 @@ type microformatProperties struct {
MpSlug []string `json:"mp-slug,omitempty"`
Photo []any `json:"photo,omitempty"`
Audio []string `json:"audio,omitempty"`
MpChannel []string `json:"mp-channel,omitempty"`
}
func (a *goBlog) micropubParsePostParamsMfItem(entry *post, mf *microformatItem) error {
@ -275,6 +293,9 @@ func (a *goBlog) micropubParsePostParamsMfItem(entry *post, mf *microformatItem)
if len(mf.Properties.MpSlug) > 0 {
entry.Slug = mf.Properties.MpSlug[0]
}
if len(mf.Properties.MpChannel) > 0 {
entry.setChannel(mf.Properties.MpChannel[0])
}
// Status
status := ""
if len(mf.Properties.PostStatus) > 0 {
@ -320,7 +341,7 @@ func (a *goBlog) micropubParsePostParamsMfItem(entry *post, mf *microformatItem)
return nil
}
func (a *goBlog) computeExtraPostParameters(p *post) error {
func (a *goBlog) extractParamsFromContent(p *post) error {
if p.Parameters == nil {
p.Parameters = map[string][]string{}
}
@ -352,8 +373,6 @@ func (a *goBlog) computeExtraPostParameters(p *post) error {
if blog := p.Parameters["blog"]; len(blog) == 1 && blog[0] != "" {
p.Blog = blog[0]
delete(p.Parameters, "blog")
} else {
p.Blog = a.cfg.DefaultBlog
}
if path := p.Parameters["path"]; len(path) == 1 {
p.Path = path[0]
@ -383,14 +402,6 @@ func (a *goBlog) computeExtraPostParameters(p *post) error {
p.Priority = cast.ToInt(priority[0])
delete(p.Parameters, "priority")
}
if p.Path == "" && p.Section == "" {
// Has no path or section -> default section
p.Section = a.cfg.Blogs[p.Blog].DefaultSection
}
if p.Published == "" && p.Section != "" {
// Has no published date, but section -> published now
p.Published = time.Now().Local().Format(time.RFC3339)
}
// Add images not in content
images := p.Parameters[a.cfg.Micropub.PhotoParam]
imageAlts := p.Parameters[a.cfg.Micropub.PhotoDescriptionParam]
@ -404,12 +415,6 @@ func (a *goBlog) computeExtraPostParameters(p *post) error {
}
}
}
// Remove all parameters where there are just empty strings
for pk, pvs := range p.Parameters {
if len(pvs) == 0 || strings.Join(pvs, "") == "" {
delete(p.Parameters, pk)
}
}
return nil
}
@ -445,7 +450,7 @@ func (a *goBlog) micropubCreate(w http.ResponseWriter, r *http.Request, p *post)
if !a.micropubCheckScope(w, r, "create") {
return
}
if err := a.computeExtraPostParameters(p); err != nil {
if err := a.extractParamsFromContent(p); err != nil {
a.serveError(w, r, err.Error(), http.StatusBadRequest)
return
}
@ -518,7 +523,7 @@ func (a *goBlog) micropubUpdate(w http.ResponseWriter, r *http.Request, u string
a.micropubUpdateReplace(p, mf.Replace)
a.micropubUpdateAdd(p, mf.Add)
a.micropubUpdateDelete(p, mf.Delete)
err = a.computeExtraPostParameters(p)
err = a.extractParamsFromContent(p)
if err != nil {
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
return

View File

@ -38,17 +38,17 @@ func Test_micropubQuery(t *testing.T) {
testCases := []testCase{
{
query: "config",
want: "{\"media-endpoint\":\"http://localhost:8080/micropub/media\"}",
want: "{\"channels\":[{\"name\":\"default: My Blog\",\"uid\":\"default\"},{\"name\":\"default/posts: posts\",\"uid\":\"default/posts\"}],\"media-endpoint\":\"http://localhost:8080/micropub/media\"}",
wantStatus: http.StatusOK,
},
{
query: "source&url=http://localhost:8080/test/post",
want: "{\"type\":[\"h-entry\"],\"properties\":{\"published\":[\"\"],\"updated\":[\"\"],\"post-status\":[\"published\"],\"visibility\":[\"public\"],\"category\":[\"test\",\"test2\"],\"content\":[\"---\\nblog: default\\npath: /test/post\\npriority: 0\\npublished: \\\"\\\"\\nsection: \\\"\\\"\\nstatus: published\\ntags:\\n - test\\n - test2\\nupdated: \\\"\\\"\\n---\\nTest post\"],\"url\":[\"http://localhost:8080/test/post\"],\"mp-slug\":[\"\"]}}",
want: "{\"type\":[\"h-entry\"],\"properties\":{\"published\":[\"\"],\"updated\":[\"\"],\"post-status\":[\"published\"],\"visibility\":[\"public\"],\"category\":[\"test\",\"test2\"],\"content\":[\"---\\nblog: default\\npath: /test/post\\npriority: 0\\npublished: \\\"\\\"\\nsection: \\\"\\\"\\nstatus: published\\ntags:\\n - test\\n - test2\\nupdated: \\\"\\\"\\n---\\nTest post\"],\"url\":[\"http://localhost:8080/test/post\"],\"mp-slug\":[\"\"],\"mp-channel\":[\"default\"]}}",
wantStatus: http.StatusOK,
},
{
query: "source",
want: "{\"items\":[{\"type\":[\"h-entry\"],\"properties\":{\"published\":[\"\"],\"updated\":[\"\"],\"post-status\":[\"published\"],\"visibility\":[\"public\"],\"category\":[\"test\",\"test2\"],\"content\":[\"---\\nblog: default\\npath: /test/post\\npriority: 0\\npublished: \\\"\\\"\\nsection: \\\"\\\"\\nstatus: published\\ntags:\\n - test\\n - test2\\nupdated: \\\"\\\"\\n---\\nTest post\"],\"url\":[\"http://localhost:8080/test/post\"],\"mp-slug\":[\"\"]}}]}",
want: "{\"items\":[{\"type\":[\"h-entry\"],\"properties\":{\"published\":[\"\"],\"updated\":[\"\"],\"post-status\":[\"published\"],\"visibility\":[\"public\"],\"category\":[\"test\",\"test2\"],\"content\":[\"---\\nblog: default\\npath: /test/post\\npriority: 0\\npublished: \\\"\\\"\\nsection: \\\"\\\"\\nstatus: published\\ntags:\\n - test\\n - test2\\nupdated: \\\"\\\"\\n---\\nTest post\"],\"url\":[\"http://localhost:8080/test/post\"],\"mp-slug\":[\"\"],\"mp-channel\":[\"default\"]}}]}",
wantStatus: http.StatusOK,
},
{

View File

@ -18,10 +18,32 @@ func (a *goBlog) checkPost(p *post) (err error) {
if p == nil {
return errors.New("no post")
}
now := time.Now()
// Fix content
p.Content = strings.TrimSuffix(strings.TrimPrefix(p.Content, "\n"), "\n")
// Fix date strings
now := time.Now().Local()
// Maybe add blog
if p.Blog == "" {
p.Blog = a.cfg.DefaultBlog
}
// Check blog
if _, ok := a.cfg.Blogs[p.Blog]; !ok {
return errors.New("blog doesn't exist")
}
// Maybe add section
if p.Path == "" && p.Section == "" {
// Has no path or section -> default section
p.Section = a.cfg.Blogs[p.Blog].DefaultSection
}
// Check section
if p.Section != "" {
if _, ok := a.cfg.Blogs[p.Blog].Sections[p.Section]; !ok {
return errors.New("section doesn't exist")
}
}
// Maybe add published date
if p.Published == "" && p.Section != "" {
// Has no published date, but section -> published now
p.Published = now.Format(time.RFC3339)
}
// Fix and check date strings
if p.Published != "" {
p.Published, err = toLocal(p.Published)
if err != nil {
@ -34,6 +56,8 @@ func (a *goBlog) checkPost(p *post) (err error) {
return err
}
}
// Fix content
p.Content = strings.TrimSuffix(strings.TrimPrefix(p.Content, "\n"), "\n")
// Check status
if p.Status == "" {
p.Status = statusPublished
@ -43,52 +67,32 @@ func (a *goBlog) checkPost(p *post) (err error) {
if err != nil {
return err
}
if publishedTime.After(time.Now()) {
if publishedTime.After(now) {
p.Status = statusScheduled
}
}
}
// Cleanup params
for key, value := range p.Parameters {
if value == nil {
delete(p.Parameters, key)
for pk, pvs := range p.Parameters {
pvs = lo.Filter(pvs, func(s string, _ int) bool { return s != "" })
if len(pvs) == 0 {
delete(p.Parameters, pk)
continue
}
allValues := []string{}
for _, v := range value {
if v != "" {
allValues = append(allValues, v)
}
}
if len(allValues) >= 1 {
p.Parameters[key] = allValues
} else {
delete(p.Parameters, key)
}
}
// Check blog
if p.Blog == "" {
p.Blog = a.cfg.DefaultBlog
}
if _, ok := a.cfg.Blogs[p.Blog]; !ok {
return errors.New("blog doesn't exist")
}
// Check if section exists
if _, ok := a.cfg.Blogs[p.Blog].Sections[p.Section]; p.Section != "" && !ok {
return errors.New("section doesn't exist")
p.Parameters[pk] = pvs
}
// Check path
if p.Path != "/" {
p.Path = strings.TrimSuffix(p.Path, "/")
}
if p.Path == "" {
if p.Section == "" {
p.Section = a.cfg.Blogs[p.Blog].DefaultSection
published, err := dateparse.ParseLocal(p.Published)
if err != nil {
published, err = now, nil
}
if p.Slug == "" {
p.Slug = fmt.Sprintf("%v-%02d-%02d-%v", now.Year(), int(now.Month()), now.Day(), randomString(5))
p.Slug = fmt.Sprintf("%v-%02d-%02d-%v", published.Year(), int(published.Month()), published.Day(), randomString(5))
}
published := timeNoErr(dateparse.ParseLocal(p.Published))
pathTmplString := defaultIfEmpty(
a.cfg.Blogs[p.Blog].Sections[p.Section].PathTemplate,
"{{printf \""+a.getRelativePath(p.Blog, "/%v/%02d/%02d/%v")+"\" .Section .Year .Month .Slug}}",

View File

@ -168,6 +168,7 @@ func (a *goBlog) postToMfItem(p *post) *microformatItem {
BookmarkOf: p.Parameters[a.cfg.Micropub.BookmarkParam],
MpSlug: []string{p.Slug},
Audio: p.Parameters[a.cfg.Micropub.AudioParam],
MpChannel: []string{p.getChannel()},
// TODO: Photos
},
}
@ -235,6 +236,24 @@ func (p *post) contentWithParams() string {
return fmt.Sprintf("---\n%s---\n%s", string(pb), p.Content)
}
func (p *post) setChannel(channel string) {
if channel == "" {
return
}
channelParts := strings.SplitN(channel, "/", 2)
p.Blog = channelParts[0]
if len(channelParts) > 1 {
p.Section = channelParts[1]
}
}
func (p *post) getChannel() string {
if p.Section == "" {
return p.Blog
}
return p.Blog + "/" + p.Section
}
// Public because of rendering
func (p *post) Title() string {

View File

@ -27,7 +27,7 @@ func (a *goBlog) startPostsScheduler() {
func (a *goBlog) checkScheduledPosts() {
postsToPublish, err := a.getPosts(&postsRequestConfig{
status: "scheduled",
status: statusScheduled,
publishedBefore: time.Now(),
})
if err != nil {
@ -35,7 +35,7 @@ func (a *goBlog) checkScheduledPosts() {
return
}
for _, post := range postsToPublish {
post.Status = "published"
post.Status = statusPublished
err := a.replacePost(post, post.Path, statusScheduled)
if err != nil {
log.Println("Error publishing scheduled post:", err)