GoBlog/posts.go

424 lines
12 KiB
Go
Raw Permalink Normal View History

2020-07-28 19:17:07 +00:00
package main
import (
"context"
2020-07-28 19:17:07 +00:00
"errors"
2020-08-05 17:54:04 +00:00
"fmt"
2020-07-28 19:17:07 +00:00
"net/http"
"net/url"
"path"
"reflect"
2020-09-24 15:13:03 +00:00
"strings"
2022-01-26 20:08:46 +00:00
"time"
2020-10-06 17:07:48 +00:00
2021-03-03 17:19:55 +00:00
"github.com/go-chi/chi/v5"
"github.com/samber/lo"
"github.com/vcraescu/go-paginator/v2"
"go.goblog.app/app/pkgs/bufferpool"
2020-07-28 19:17:07 +00:00
)
2020-07-30 19:18:13 +00:00
var errPostNotFound = errors.New("post not found")
2020-07-28 19:17:07 +00:00
2020-10-15 15:32:46 +00:00
type post struct {
Path string
Content string
Published string
Updated string
Parameters map[string][]string
Blog string
Section string
Status postStatus
Visibility postVisibility
Priority int
2020-10-06 17:07:48 +00:00
// Not persisted
2021-08-05 06:09:34 +00:00
Slug string
RenderedTitle string
2020-07-28 19:17:07 +00:00
}
2021-01-15 20:56:46 +00:00
type postStatus string
type postVisibility string
2021-01-15 20:56:46 +00:00
const (
statusDeletedSuffix postStatus = "-deleted"
statusNil postStatus = ""
statusPublished postStatus = "published"
2023-02-23 15:32:59 +00:00
statusPublishedDeleted = statusPublished + statusDeletedSuffix
statusDraft postStatus = "draft"
2023-02-23 15:32:59 +00:00
statusDraftDeleted = statusDraft + statusDeletedSuffix
statusScheduled postStatus = "scheduled"
2023-02-23 15:32:59 +00:00
statusScheduledDeleted = statusScheduled + statusDeletedSuffix
visibilityNil postVisibility = ""
visibilityPublic postVisibility = "public"
visibilityUnlisted postVisibility = "unlisted"
visibilityPrivate postVisibility = "private"
2021-01-15 20:56:46 +00:00
)
func validPostStatus(s postStatus) bool {
return s == statusPublished || s == statusPublishedDeleted ||
s == statusDraft || s == statusDraftDeleted ||
s == statusScheduled || s == statusScheduledDeleted
}
func validPostVisibility(v postVisibility) bool {
return v == visibilityPublic || v == visibilityUnlisted || v == visibilityPrivate
}
func (a *goBlog) servePost(w http.ResponseWriter, r *http.Request) {
2021-08-05 06:09:34 +00:00
p, err := a.getPost(r.URL.Path)
if errors.Is(err, errPostNotFound) {
a.serve404(w, r)
2020-07-29 14:41:36 +00:00
return
2020-07-28 19:17:07 +00:00
} else if err != nil {
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
2020-07-29 14:41:36 +00:00
return
2020-07-28 19:17:07 +00:00
}
status := http.StatusOK
if p.Deleted() {
status = http.StatusGone
}
2021-02-24 12:16:33 +00:00
if asRequest, ok := r.Context().Value(asRequestKey).(bool); ok && asRequest {
if r.URL.Path == a.getRelativePath(p.Blog, "") {
a.serveActivityStreams(w, r, status, p.Blog)
return
}
a.serveActivityStreamsPost(w, r, status, p)
return
}
2020-11-01 17:37:21 +00:00
canonical := p.firstParameter("original")
if canonical == "" {
canonical = a.fullPostURL(p)
2020-11-01 17:37:21 +00:00
}
renderMethod := a.renderPost
2021-06-11 06:24:41 +00:00
if p.Path == a.getRelativePath(p.Blog, "") {
renderMethod = a.renderStaticHome
2020-12-23 15:53:10 +00:00
}
if p.Visibility != visibilityPublic {
w.Header().Set("X-Robots-Tag", "noindex")
}
w.Header().Add("Link", fmt.Sprintf("<%s>; rel=shortlink", a.shortPostURL(p)))
a.renderWithStatusCode(w, r, status, renderMethod, &renderData{
2021-01-17 11:53:07 +00:00
BlogString: p.Blog,
2020-11-01 17:37:21 +00:00
Canonical: canonical,
2020-10-15 15:32:46 +00:00
Data: p,
2020-10-12 16:47:23 +00:00
})
2020-07-28 19:17:07 +00:00
}
2022-01-26 20:08:46 +00:00
const defaultRandomPath = "/random"
func (a *goBlog) redirectToRandomPost(rw http.ResponseWriter, r *http.Request) {
blog, _ := a.getBlog(r)
randomPath, err := a.getRandomPostPath(blog)
if err != nil {
a.serveError(rw, r, err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(rw, r, randomPath, http.StatusFound)
}
2022-01-26 20:08:46 +00:00
const defaultOnThisDayPath = "/onthisday"
func (a *goBlog) redirectToOnThisDay(w http.ResponseWriter, r *http.Request) {
_, bc := a.getBlog(r)
// Get current local month and day
now := time.Now()
month := now.Month()
day := now.Day()
// Build the path
targetPath := fmt.Sprintf("/x/%02d/%02d", month, day)
targetPath = bc.getRelativePath(targetPath)
// Redirect
http.Redirect(w, r, targetPath, http.StatusFound)
}
type postPaginationAdapter struct {
2020-10-19 18:25:30 +00:00
config *postsRequestConfig
2020-11-17 16:38:17 +00:00
nums int64
2021-08-05 06:09:34 +00:00
a *goBlog
}
2020-11-17 16:38:17 +00:00
func (p *postPaginationAdapter) Nums() (int64, error) {
if p.nums == 0 {
p.nums = int64(noError(p.a.db.countPosts(p.config)))
}
2020-11-17 16:38:17 +00:00
return p.nums, nil
}
2022-03-16 07:28:03 +00:00
func (p *postPaginationAdapter) Slice(offset, length int, data any) error {
2020-08-31 19:12:43 +00:00
modifiedConfig := *p.config
modifiedConfig.offset = offset
modifiedConfig.limit = length
2021-08-05 06:09:34 +00:00
posts, err := p.a.getPosts(&modifiedConfig)
reflect.ValueOf(data).Elem().Set(reflect.ValueOf(&posts).Elem())
return err
}
func (a *goBlog) serveHome(w http.ResponseWriter, r *http.Request) {
blog, bc := a.getBlog(r)
if asRequest, ok := r.Context().Value(asRequestKey).(bool); ok && asRequest {
a.serveActivityStreams(w, r, http.StatusOK, blog)
return
}
a.serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{
path: a.getRelativePath(blog, ""),
sections: lo.Filter(lo.Values(bc.Sections), func(s *configSection, _ int) bool { return !s.HideOnStart }),
})))
2020-08-25 18:55:32 +00:00
}
func (a *goBlog) serveDrafts(w http.ResponseWriter, r *http.Request) {
_, bc := a.getBlog(r)
a.serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{
path: bc.getRelativePath("/editor/drafts"),
title: a.ts.GetTemplateStringVariant(bc.Lang, "drafts"),
description: a.ts.GetTemplateStringVariant(bc.Lang, "draftsdesc"),
status: []postStatus{statusDraft},
})))
}
func (a *goBlog) servePrivate(w http.ResponseWriter, r *http.Request) {
_, bc := a.getBlog(r)
a.serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{
path: bc.getRelativePath("/editor/private"),
title: a.ts.GetTemplateStringVariant(bc.Lang, "privateposts"),
description: a.ts.GetTemplateStringVariant(bc.Lang, "privatepostsdesc"),
status: []postStatus{statusPublished},
visibility: []postVisibility{visibilityPrivate},
})))
}
func (a *goBlog) serveUnlisted(w http.ResponseWriter, r *http.Request) {
_, bc := a.getBlog(r)
a.serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{
path: bc.getRelativePath("/editor/unlisted"),
title: a.ts.GetTemplateStringVariant(bc.Lang, "unlistedposts"),
description: a.ts.GetTemplateStringVariant(bc.Lang, "unlistedpostsdesc"),
status: []postStatus{statusPublished},
visibility: []postVisibility{visibilityUnlisted},
})))
}
2021-12-11 18:56:40 +00:00
func (a *goBlog) serveScheduled(w http.ResponseWriter, r *http.Request) {
_, bc := a.getBlog(r)
2021-12-11 18:56:40 +00:00
a.serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{
path: bc.getRelativePath("/editor/scheduled"),
title: a.ts.GetTemplateStringVariant(bc.Lang, "scheduledposts"),
description: a.ts.GetTemplateStringVariant(bc.Lang, "scheduledpostsdesc"),
status: []postStatus{statusScheduled},
})))
}
func (a *goBlog) serveDeleted(w http.ResponseWriter, r *http.Request) {
_, bc := a.getBlog(r)
a.serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{
path: bc.getRelativePath("/editor/deleted"),
title: a.ts.GetTemplateStringVariant(bc.Lang, "deletedposts"),
description: a.ts.GetTemplateStringVariant(bc.Lang, "deletedpostsdesc"),
status: []postStatus{statusPublishedDeleted, statusDraftDeleted, statusScheduledDeleted},
2021-12-11 18:56:40 +00:00
})))
}
func (a *goBlog) serveDate(w http.ResponseWriter, r *http.Request) {
year, month, day, title, datePath := a.extractDate(r)
if year == 0 && month == 0 && day == 0 {
a.serve404(w, r)
return
}
var ic *indexConfig
if cv := r.Context().Value(indexConfigKey); cv != nil {
origIc := *(cv.(*indexConfig))
copyIc := origIc
ic = &copyIc
ic.path = path.Join(ic.path, datePath)
ic.titleSuffix = ": " + title
} else {
_, bc := a.getBlog(r)
ic = &indexConfig{
path: bc.getRelativePath(datePath),
title: title,
}
}
ic.year, ic.month, ic.day = year, month, day
a.serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, ic)))
}
func (a *goBlog) extractDate(r *http.Request) (year, month, day int, title, datePath string) {
if ys := chi.URLParam(r, "year"); ys != "" && ys != "x" {
2022-02-26 19:38:52 +00:00
year = stringToInt(ys)
}
if ms := chi.URLParam(r, "month"); ms != "" && ms != "x" {
2022-02-26 19:38:52 +00:00
month = stringToInt(ms)
}
if ds := chi.URLParam(r, "day"); ds != "" {
2022-02-26 19:38:52 +00:00
day = stringToInt(ds)
}
titleBuf, pathBuf := bufferpool.Get(), bufferpool.Get()
defer bufferpool.Put(titleBuf, pathBuf)
if year != 0 {
_, _ = fmt.Fprintf(titleBuf, "%0004d", year)
_, _ = fmt.Fprintf(pathBuf, "%0004d", year)
} else {
_, _ = titleBuf.WriteString("XXXX")
_, _ = pathBuf.WriteString("x")
}
if month != 0 {
_, _ = fmt.Fprintf(titleBuf, "-%02d", month)
_, _ = fmt.Fprintf(pathBuf, "/%02d", month)
} else if day != 0 {
_, _ = titleBuf.WriteString("-XX")
_, _ = pathBuf.WriteString("/x")
}
if day != 0 {
_, _ = fmt.Fprintf(titleBuf, "-%02d", day)
_, _ = fmt.Fprintf(pathBuf, "/%02d", day)
2020-12-13 14:16:47 +00:00
}
title = titleBuf.String()
datePath = pathBuf.String()
return
2020-12-13 14:16:47 +00:00
}
2020-09-21 16:03:05 +00:00
type indexConfig struct {
path string
section *configSection
sections []*configSection
tax *configTaxonomy
taxValue string
parameter string
year, month, day int
title string
titleSuffix string
description string
summaryTemplate summaryTyp
status []postStatus
visibility []postVisibility
2023-09-07 13:46:51 +00:00
search string
2020-09-21 16:03:05 +00:00
}
const defaultPhotosPath = "/photos"
const indexConfigKey contextKey = "indexConfig"
func (a *goBlog) serveIndex(w http.ResponseWriter, r *http.Request) {
ic := r.Context().Value(indexConfigKey).(*indexConfig)
blog, bc := a.getBlog(r)
sections := lo.Map(ic.sections, func(i *configSection, _ int) string { return i.Name })
if ic.section != nil {
sections = append(sections, ic.section.Name)
2020-08-05 17:14:10 +00:00
}
defaultStatus, defaultVisibility := a.getDefaultPostStates(r)
status := ic.status
if len(status) == 0 {
status = defaultStatus
}
visibility := ic.visibility
if len(visibility) == 0 {
visibility = defaultVisibility
}
// Parameter filter
params, paramValues := []string{}, []string{}
paramUrlValues := url.Values{}
for param, values := range r.URL.Query() {
if strings.HasPrefix(param, "p:") {
paramKey := strings.TrimPrefix(param, "p:")
for _, value := range values {
params, paramValues = append(params, paramKey), append(paramValues, value)
paramUrlValues.Add(param, value)
}
}
}
paramUrlQuery := ""
if len(paramUrlValues) > 0 {
paramUrlQuery += "?" + paramUrlValues.Encode()
}
// Create paginator
p := paginator.New(&postPaginationAdapter{config: &postsRequestConfig{
blog: blog,
sections: sections,
taxonomy: ic.tax,
taxonomyValue: ic.taxValue,
parameter: ic.parameter,
allParams: params,
allParamValues: paramValues,
2023-09-07 13:46:51 +00:00
search: ic.search,
publishedYear: ic.year,
publishedMonth: ic.month,
publishedDay: ic.day,
status: status,
visibility: visibility,
priorityOrder: true,
}, a: a}, bc.Pagination)
2022-02-26 19:38:52 +00:00
p.SetPage(stringToInt(chi.URLParam(r, "page")))
var posts []*post
err := p.Results(&posts)
if err != nil {
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
return
}
// Title
var title string
if ic.title != "" {
title = ic.title
} else if ic.section != nil {
title = ic.section.Title
} else if ic.tax != nil {
title = fmt.Sprintf("%s: %s", ic.tax.Title, ic.taxValue)
2023-09-07 13:46:51 +00:00
} else if ic.search != "" {
title = fmt.Sprintf("%s: %s", bc.Search.Title, ic.search)
}
title += ic.titleSuffix
// Description
var description string
if ic.description != "" {
description = ic.description
} else if ic.section != nil {
description = ic.section.Description
}
// Check if feed
if ft := feedType(chi.URLParam(r, "feed")); ft != noFeed {
a.generateFeed(blog, ft, w, r, posts, title, description, ic.path, paramUrlQuery)
return
}
// Navigation
var hasPrev, hasNext bool
var prevPage, nextPage int
var prevPath, nextPath string
hasPrev, _ = p.HasPrev()
if hasPrev {
prevPage, _ = p.PrevPage()
} else {
prevPage, _ = p.Page()
}
if prevPage < 2 {
prevPath = ic.path
} else {
prevPath = fmt.Sprintf("%s/page/%d", strings.TrimSuffix(ic.path, "/"), prevPage)
}
hasNext, _ = p.HasNext()
if hasNext {
nextPage, _ = p.NextPage()
} else {
nextPage, _ = p.Page()
}
nextPath = fmt.Sprintf("%s/page/%d", strings.TrimSuffix(ic.path, "/"), nextPage)
summaryTemplate := ic.summaryTemplate
if summaryTemplate == "" {
summaryTemplate = defaultSummary
}
a.render(w, r, a.renderIndex, &renderData{
Canonical: a.getFullAddress(ic.path) + paramUrlQuery,
Data: &indexRenderData{
title: title,
description: description,
posts: posts,
hasPrev: hasPrev,
hasNext: hasNext,
first: ic.path,
prev: prevPath,
next: nextPath,
summaryTemplate: summaryTemplate,
paramUrlQuery: paramUrlQuery,
},
})
2020-08-05 17:14:10 +00:00
}