diff --git a/editor.go b/editor.go index bf92cef..23488e5 100644 --- a/editor.go +++ b/editor.go @@ -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) { diff --git a/go.mod b/go.mod index 8d2309d..44c89ba 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 8c5da69..0e93eb0 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/micropub.go b/micropub.go index e90f3ba..b26516f 100644 --- a/micropub.go +++ b/micropub.go @@ -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 diff --git a/micropub_test.go b/micropub_test.go index bf5e4e7..8cd6b8e 100644 --- a/micropub_test.go +++ b/micropub_test.go @@ -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, }, { diff --git a/postsDb.go b/postsDb.go index b6a50d3..b787dcc 100644 --- a/postsDb.go +++ b/postsDb.go @@ -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}}", diff --git a/postsFuncs.go b/postsFuncs.go index 1326ca3..b2269cb 100644 --- a/postsFuncs.go +++ b/postsFuncs.go @@ -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 { diff --git a/postsScheduler.go b/postsScheduler.go index 86efb71..b41de1b 100644 --- a/postsScheduler.go +++ b/postsScheduler.go @@ -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)