mirror of https://github.com/jlelse/GoBlog
Add support for undeleting posts within 7 days (#3)
This commit is contained in:
parent
86625cc8fe
commit
337392112b
|
@ -45,6 +45,9 @@ func (a *goBlog) initActivityPub() error {
|
||||||
a.pDeleteHooks = append(a.pDeleteHooks, func(p *post) {
|
a.pDeleteHooks = append(a.pDeleteHooks, func(p *post) {
|
||||||
a.apDelete(p)
|
a.apDelete(p)
|
||||||
})
|
})
|
||||||
|
a.pUndeleteHooks = append(a.pUndeleteHooks, func(p *post) {
|
||||||
|
a.apUndelete(p)
|
||||||
|
})
|
||||||
// Prepare webfinger
|
// Prepare webfinger
|
||||||
a.webfingerResources = map[string]*configBlog{}
|
a.webfingerResources = map[string]*configBlog{}
|
||||||
a.webfingerAccts = map[string]string{}
|
a.webfingerAccts = map[string]string{}
|
||||||
|
@ -339,11 +342,21 @@ func (a *goBlog) apDelete(p *post) {
|
||||||
a.apSendToAllFollowers(p.Blog, map[string]interface{}{
|
a.apSendToAllFollowers(p.Blog, map[string]interface{}{
|
||||||
"@context": []string{asContext},
|
"@context": []string{asContext},
|
||||||
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
|
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
|
||||||
"id": a.fullPostURL(p) + "#delete",
|
|
||||||
"type": "Delete",
|
"type": "Delete",
|
||||||
"object": map[string]string{
|
"object": a.fullPostURL(p),
|
||||||
"id": a.fullPostURL(p),
|
})
|
||||||
"type": "Tombstone",
|
}
|
||||||
|
|
||||||
|
func (a *goBlog) apUndelete(p *post) {
|
||||||
|
a.apSendToAllFollowers(p.Blog, map[string]interface{}{
|
||||||
|
"@context": []string{asContext},
|
||||||
|
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
|
||||||
|
"type": "Undo",
|
||||||
|
"object": map[string]interface{}{
|
||||||
|
"@context": []string{asContext},
|
||||||
|
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
|
||||||
|
"type": "Delete",
|
||||||
|
"object": a.fullPostURL(p),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
9
app.go
9
app.go
|
@ -43,10 +43,11 @@ type goBlog struct {
|
||||||
// Errors
|
// Errors
|
||||||
errorCheckMediaTypes []ct.MediaType
|
errorCheckMediaTypes []ct.MediaType
|
||||||
// Hooks
|
// Hooks
|
||||||
pPostHooks []postHookFunc
|
pPostHooks []postHookFunc
|
||||||
pUpdateHooks []postHookFunc
|
pUpdateHooks []postHookFunc
|
||||||
pDeleteHooks []postHookFunc
|
pDeleteHooks []postHookFunc
|
||||||
hourlyHooks []hourlyHookFunc
|
pUndeleteHooks []postHookFunc
|
||||||
|
hourlyHooks []hourlyHookFunc
|
||||||
// HTTP Client
|
// HTTP Client
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
// HTTP Routers
|
// HTTP Routers
|
||||||
|
|
|
@ -19,6 +19,7 @@ func (a *goBlog) initBlogStats() {
|
||||||
a.pPostHooks = append(a.pPostHooks, f)
|
a.pPostHooks = append(a.pPostHooks, f)
|
||||||
a.pUpdateHooks = append(a.pUpdateHooks, f)
|
a.pUpdateHooks = append(a.pUpdateHooks, f)
|
||||||
a.pDeleteHooks = append(a.pDeleteHooks, f)
|
a.pDeleteHooks = append(a.pDeleteHooks, f)
|
||||||
|
a.pUndeleteHooks = append(a.pUndeleteHooks, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) serveBlogStats(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveBlogStats(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
|
@ -209,9 +209,10 @@ type configHooks struct {
|
||||||
Hourly []string `mapstructure:"hourly"`
|
Hourly []string `mapstructure:"hourly"`
|
||||||
PreStart []string `mapstructure:"prestart"`
|
PreStart []string `mapstructure:"prestart"`
|
||||||
// Can use template
|
// Can use template
|
||||||
PostPost []string `mapstructure:"postpost"`
|
PostPost []string `mapstructure:"postpost"`
|
||||||
PostUpdate []string `mapstructure:"postupdate"`
|
PostUpdate []string `mapstructure:"postupdate"`
|
||||||
PostDelete []string `mapstructure:"postdelete"`
|
PostDelete []string `mapstructure:"postdelete"`
|
||||||
|
PostUndelete []string `mapstructure:"postundelete"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type configMicropub struct {
|
type configMicropub struct {
|
||||||
|
|
|
@ -70,6 +70,8 @@ hooks:
|
||||||
- echo Updated post at {{.URL}}
|
- echo Updated post at {{.URL}}
|
||||||
postdelete: # Commands to execute after deleting a post
|
postdelete: # Commands to execute after deleting a post
|
||||||
- echo Deleted post at {{.URL}}
|
- echo Deleted post at {{.URL}}
|
||||||
|
postundelete: # Commands to execute after undeleting a post
|
||||||
|
- echo Undeleted post at {{.URL}}
|
||||||
|
|
||||||
# ActivityPub
|
# ActivityPub
|
||||||
activityPub:
|
activityPub:
|
||||||
|
|
16
hooks.go
16
hooks.go
|
@ -69,6 +69,22 @@ func (a *goBlog) postDeleteHooks(p *post) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *goBlog) postUndeleteHooks(p *post) {
|
||||||
|
if hc := a.cfg.Hooks; hc != nil {
|
||||||
|
for _, cmdTmplString := range hc.PostUndelete {
|
||||||
|
go func(p *post, cmdTmplString string) {
|
||||||
|
a.cfg.Hooks.executeTemplateCommand("post-undelete", cmdTmplString, map[string]interface{}{
|
||||||
|
"URL": a.fullPostURL(p),
|
||||||
|
"Post": p,
|
||||||
|
})
|
||||||
|
}(p, cmdTmplString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, f := range a.pUndeleteHooks {
|
||||||
|
go f(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (cfg *configHooks) executeTemplateCommand(hookType string, tmpl string, data map[string]interface{}) {
|
func (cfg *configHooks) executeTemplateCommand(hookType string, tmpl string, data map[string]interface{}) {
|
||||||
cmdTmpl, err := template.New("cmd").Parse(tmpl)
|
cmdTmpl, err := template.New("cmd").Parse(tmpl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
13
http.go
13
http.go
|
@ -265,7 +265,14 @@ func (a *goBlog) servePostsAliasesRedirects() http.HandlerFunc {
|
||||||
case statusPublished, statusUnlisted:
|
case statusPublished, statusUnlisted:
|
||||||
alicePrivate.Append(a.checkActivityStreamsRequest, a.cacheMiddleware).ThenFunc(a.servePost).ServeHTTP(w, r)
|
alicePrivate.Append(a.checkActivityStreamsRequest, a.cacheMiddleware).ThenFunc(a.servePost).ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
default: // private, draft, scheduled
|
case statusPublishedDeleted, statusUnlistedDeleted:
|
||||||
|
if a.isLoggedIn(r) {
|
||||||
|
a.servePost(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
alicePrivate.Append(a.cacheMiddleware).ThenFunc(a.serve410).ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
default: // private, draft, scheduled, etc.
|
||||||
alice.New(a.authMiddleware).ThenFunc(a.servePost).ServeHTTP(w, r)
|
alice.New(a.authMiddleware).ThenFunc(a.servePost).ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -277,9 +284,7 @@ func (a *goBlog) servePostsAliasesRedirects() http.HandlerFunc {
|
||||||
return
|
return
|
||||||
case "deleted":
|
case "deleted":
|
||||||
// Is deleted, serve 410
|
// Is deleted, serve 410
|
||||||
alicePrivate.Append(a.cacheMiddleware).ThenFunc(func(w http.ResponseWriter, r *http.Request) {
|
alicePrivate.Append(a.cacheMiddleware).ThenFunc(a.serve410).ServeHTTP(w, r)
|
||||||
a.serve410(w, r)
|
|
||||||
}).ServeHTTP(w, r)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -345,6 +345,9 @@ func (a *goBlog) blogEditorRouter(conf *configBlog) func(r chi.Router) {
|
||||||
r.Get("/scheduled", a.serveScheduled)
|
r.Get("/scheduled", a.serveScheduled)
|
||||||
r.Get("/scheduled"+feedPath, a.serveScheduled)
|
r.Get("/scheduled"+feedPath, a.serveScheduled)
|
||||||
r.Get("/scheduled"+paginationPath, a.serveScheduled)
|
r.Get("/scheduled"+paginationPath, a.serveScheduled)
|
||||||
|
r.Get("/deleted", a.serveDeleted)
|
||||||
|
r.Get("/deleted"+feedPath, a.serveDeleted)
|
||||||
|
r.Get("/deleted"+paginationPath, a.serveDeleted)
|
||||||
r.HandleFunc("/preview", a.serveEditorPreview)
|
r.HandleFunc("/preview", a.serveEditorPreview)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,6 @@ func (a *goBlog) checkIndieAuth(next http.Handler) http.Handler {
|
||||||
|
|
||||||
func addAllScopes(next http.Handler) http.Handler {
|
func addAllScopes(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
next.ServeHTTP(rw, r.WithContext(context.WithValue(r.Context(), indieAuthScope, "create update delete media")))
|
next.ServeHTTP(rw, r.WithContext(context.WithValue(r.Context(), indieAuthScope, "create update delete undelete media")))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,6 +70,7 @@ func Test_addAllScopes(t *testing.T) {
|
||||||
assert.Contains(t, scope, "create")
|
assert.Contains(t, scope, "create")
|
||||||
assert.Contains(t, scope, "update")
|
assert.Contains(t, scope, "update")
|
||||||
assert.Contains(t, scope, "delete")
|
assert.Contains(t, scope, "delete")
|
||||||
|
assert.Contains(t, scope, "undelete")
|
||||||
assert.Contains(t, scope, "media")
|
assert.Contains(t, scope, "media")
|
||||||
checked = true
|
checked = true
|
||||||
})).ServeHTTP(rec, req)
|
})).ServeHTTP(rec, req)
|
||||||
|
|
1
main.go
1
main.go
|
@ -181,6 +181,7 @@ func (app *goBlog) initComponents(logging bool) {
|
||||||
app.initSessions()
|
app.initSessions()
|
||||||
app.initIndieAuth()
|
app.initIndieAuth()
|
||||||
app.startPostsScheduler()
|
app.startPostsScheduler()
|
||||||
|
app.initPostsDeleter()
|
||||||
// Log finish
|
// Log finish
|
||||||
if logging {
|
if logging {
|
||||||
log.Println("Initialized components")
|
log.Println("Initialized components")
|
||||||
|
|
32
micropub.go
32
micropub.go
|
@ -102,6 +102,8 @@ func (a *goBlog) serveMicropubPost(w http.ResponseWriter, r *http.Request) {
|
||||||
switch action {
|
switch action {
|
||||||
case actionDelete:
|
case actionDelete:
|
||||||
a.micropubDelete(w, r, r.Form.Get("url"))
|
a.micropubDelete(w, r, r.Form.Get("url"))
|
||||||
|
case actionUndelete:
|
||||||
|
a.micropubUndelete(w, r, r.Form.Get("url"))
|
||||||
default:
|
default:
|
||||||
a.serveError(w, r, "Action not supported", http.StatusNotImplemented)
|
a.serveError(w, r, "Action not supported", http.StatusNotImplemented)
|
||||||
}
|
}
|
||||||
|
@ -119,6 +121,8 @@ func (a *goBlog) serveMicropubPost(w http.ResponseWriter, r *http.Request) {
|
||||||
switch parsedMfItem.Action {
|
switch parsedMfItem.Action {
|
||||||
case actionDelete:
|
case actionDelete:
|
||||||
a.micropubDelete(w, r, parsedMfItem.URL)
|
a.micropubDelete(w, r, parsedMfItem.URL)
|
||||||
|
case actionUndelete:
|
||||||
|
a.micropubUndelete(w, r, parsedMfItem.URL)
|
||||||
case actionUpdate:
|
case actionUpdate:
|
||||||
a.micropubUpdate(w, r, parsedMfItem.URL, parsedMfItem)
|
a.micropubUpdate(w, r, parsedMfItem.URL, parsedMfItem)
|
||||||
default:
|
default:
|
||||||
|
@ -226,8 +230,9 @@ func (a *goBlog) micropubParseValuePostParamsValueMap(entry *post, values map[st
|
||||||
type micropubAction string
|
type micropubAction string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
actionUpdate micropubAction = "update"
|
actionUpdate micropubAction = "update"
|
||||||
actionDelete micropubAction = "delete"
|
actionDelete micropubAction = "delete"
|
||||||
|
actionUndelete micropubAction = "undelete"
|
||||||
)
|
)
|
||||||
|
|
||||||
type microformatItem struct {
|
type microformatItem struct {
|
||||||
|
@ -469,6 +474,23 @@ func (a *goBlog) micropubDelete(w http.ResponseWriter, r *http.Request, u string
|
||||||
http.Redirect(w, r, uu.String(), http.StatusNoContent)
|
http.Redirect(w, r, uu.String(), http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *goBlog) micropubUndelete(w http.ResponseWriter, r *http.Request, u string) {
|
||||||
|
if !strings.Contains(r.Context().Value(indieAuthScope).(string), "undelete") {
|
||||||
|
a.serveError(w, r, "undelete scope missing", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uu, err := url.Parse(u)
|
||||||
|
if err != nil {
|
||||||
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := a.undeletePost(uu.Path); err != nil {
|
||||||
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, uu.String(), http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *goBlog) micropubUpdate(w http.ResponseWriter, r *http.Request, u string, mf *microformatItem) {
|
func (a *goBlog) micropubUpdate(w http.ResponseWriter, r *http.Request, u string, mf *microformatItem) {
|
||||||
if !strings.Contains(r.Context().Value(indieAuthScope).(string), "update") {
|
if !strings.Contains(r.Context().Value(indieAuthScope).(string), "update") {
|
||||||
a.serveError(w, r, "update scope missing", http.StatusForbidden)
|
a.serveError(w, r, "update scope missing", http.StatusForbidden)
|
||||||
|
@ -489,6 +511,12 @@ func (a *goBlog) micropubUpdate(w http.ResponseWriter, r *http.Request, u string
|
||||||
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Check if post is marked as deleted
|
||||||
|
if strings.HasSuffix(string(p.Status), statusDeletedSuffix) {
|
||||||
|
a.serveError(w, r, "post is marked as deleted, undelete it first", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Update post
|
||||||
oldPath := p.Path
|
oldPath := p.Path
|
||||||
oldStatus := p.Status
|
oldStatus := p.Status
|
||||||
a.micropubUpdateReplace(p, mf.Replace)
|
a.micropubUpdateReplace(p, mf.Replace)
|
||||||
|
|
|
@ -26,7 +26,7 @@ func Test_ntfySending(t *testing.T) {
|
||||||
|
|
||||||
_ = app.initConfig()
|
_ = app.initConfig()
|
||||||
_ = app.initDatabase(false)
|
_ = app.initDatabase(false)
|
||||||
app.initComponents(true)
|
app.initComponents(false)
|
||||||
|
|
||||||
app.sendNotification("Test notification")
|
app.sendNotification("Test notification")
|
||||||
|
|
||||||
|
|
77
posts.go
77
posts.go
|
@ -37,17 +37,24 @@ type post struct {
|
||||||
type postStatus string
|
type postStatus string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
statusNil postStatus = ""
|
statusDeletedSuffix string = "-deleted"
|
||||||
statusPublished postStatus = "published"
|
|
||||||
statusDraft postStatus = "draft"
|
statusNil postStatus = ""
|
||||||
statusPrivate postStatus = "private"
|
statusPublished postStatus = "published"
|
||||||
statusUnlisted postStatus = "unlisted"
|
statusPublishedDeleted postStatus = "published-deleted"
|
||||||
statusScheduled postStatus = "scheduled"
|
statusDraft postStatus = "draft"
|
||||||
|
statusDraftDeleted postStatus = "draft-deleted"
|
||||||
|
statusPrivate postStatus = "private"
|
||||||
|
statusPrivateDeleted postStatus = "private-deleted"
|
||||||
|
statusUnlisted postStatus = "unlisted"
|
||||||
|
statusUnlistedDeleted postStatus = "unlisted-deleted"
|
||||||
|
statusScheduled postStatus = "scheduled"
|
||||||
|
statusScheduledDeleted postStatus = "scheduled-deleted"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *goBlog) servePost(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) servePost(w http.ResponseWriter, r *http.Request) {
|
||||||
p, err := a.getPost(r.URL.Path)
|
p, err := a.getPost(r.URL.Path)
|
||||||
if err == errPostNotFound {
|
if errors.Is(err, errPostNotFound) {
|
||||||
a.serve404(w, r)
|
a.serve404(w, r)
|
||||||
return
|
return
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
|
@ -71,7 +78,11 @@ func (a *goBlog) servePost(w http.ResponseWriter, r *http.Request) {
|
||||||
template = templateStaticHome
|
template = templateStaticHome
|
||||||
}
|
}
|
||||||
w.Header().Add("Link", fmt.Sprintf("<%s>; rel=shortlink", a.shortPostURL(p)))
|
w.Header().Add("Link", fmt.Sprintf("<%s>; rel=shortlink", a.shortPostURL(p)))
|
||||||
a.render(w, r, template, &renderData{
|
status := http.StatusOK
|
||||||
|
if strings.HasSuffix(string(p.Status), statusDeletedSuffix) {
|
||||||
|
status = http.StatusGone
|
||||||
|
}
|
||||||
|
a.renderWithStatusCode(w, r, status, template, &renderData{
|
||||||
BlogString: p.Blog,
|
BlogString: p.Blog,
|
||||||
Canonical: canonical,
|
Canonical: canonical,
|
||||||
Data: p,
|
Data: p,
|
||||||
|
@ -126,36 +137,50 @@ func (a *goBlog) serveHome(w http.ResponseWriter, r *http.Request) {
|
||||||
func (a *goBlog) serveDrafts(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveDrafts(w http.ResponseWriter, r *http.Request) {
|
||||||
_, bc := a.getBlog(r)
|
_, bc := a.getBlog(r)
|
||||||
a.serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{
|
a.serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{
|
||||||
path: bc.getRelativePath("/editor/drafts"),
|
path: bc.getRelativePath("/editor/drafts"),
|
||||||
title: a.ts.GetTemplateStringVariant(bc.Lang, "drafts"),
|
title: a.ts.GetTemplateStringVariant(bc.Lang, "drafts"),
|
||||||
status: statusDraft,
|
description: a.ts.GetTemplateStringVariant(bc.Lang, "draftsdesc"),
|
||||||
|
status: statusDraft,
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) servePrivate(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) servePrivate(w http.ResponseWriter, r *http.Request) {
|
||||||
_, bc := a.getBlog(r)
|
_, bc := a.getBlog(r)
|
||||||
a.serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{
|
a.serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{
|
||||||
path: bc.getRelativePath("/editor/private"),
|
path: bc.getRelativePath("/editor/private"),
|
||||||
title: a.ts.GetTemplateStringVariant(bc.Lang, "privateposts"),
|
title: a.ts.GetTemplateStringVariant(bc.Lang, "privateposts"),
|
||||||
status: statusPrivate,
|
description: a.ts.GetTemplateStringVariant(bc.Lang, "privatepostsdesc"),
|
||||||
|
status: statusPrivate,
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) serveUnlisted(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveUnlisted(w http.ResponseWriter, r *http.Request) {
|
||||||
_, bc := a.getBlog(r)
|
_, bc := a.getBlog(r)
|
||||||
a.serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{
|
a.serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{
|
||||||
path: bc.getRelativePath("/editor/unlisted"),
|
path: bc.getRelativePath("/editor/unlisted"),
|
||||||
title: a.ts.GetTemplateStringVariant(bc.Lang, "unlistedposts"),
|
title: a.ts.GetTemplateStringVariant(bc.Lang, "unlistedposts"),
|
||||||
status: statusUnlisted,
|
description: a.ts.GetTemplateStringVariant(bc.Lang, "unlistedpostsdesc"),
|
||||||
|
status: statusUnlisted,
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) serveScheduled(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) serveScheduled(w http.ResponseWriter, r *http.Request) {
|
||||||
_, bc := a.getBlog(r)
|
_, bc := a.getBlog(r)
|
||||||
a.serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{
|
a.serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{
|
||||||
path: bc.getRelativePath("/editor/scheduled"),
|
path: bc.getRelativePath("/editor/scheduled"),
|
||||||
title: a.ts.GetTemplateStringVariant(bc.Lang, "scheduledposts"),
|
title: a.ts.GetTemplateStringVariant(bc.Lang, "scheduledposts"),
|
||||||
status: statusScheduled,
|
description: a.ts.GetTemplateStringVariant(bc.Lang, "scheduledpostsdesc"),
|
||||||
|
status: 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"),
|
||||||
|
statusse: []postStatus{statusPublishedDeleted, statusDraftDeleted, statusScheduledDeleted, statusPrivateDeleted, statusUnlistedDeleted},
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,6 +240,7 @@ type indexConfig struct {
|
||||||
description string
|
description string
|
||||||
summaryTemplate string
|
summaryTemplate string
|
||||||
status postStatus
|
status postStatus
|
||||||
|
statusse []postStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultPhotosPath = "/photos"
|
const defaultPhotosPath = "/photos"
|
||||||
|
@ -239,9 +265,12 @@ func (a *goBlog) serveIndex(w http.ResponseWriter, r *http.Request) {
|
||||||
sections = append(sections, sectionKey)
|
sections = append(sections, sectionKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
status := ic.status
|
statusse := ic.statusse
|
||||||
if status == statusNil {
|
if ic.status != statusNil {
|
||||||
status = statusPublished
|
statusse = []postStatus{ic.status}
|
||||||
|
}
|
||||||
|
if len(statusse) == 0 {
|
||||||
|
statusse = []postStatus{statusPublished}
|
||||||
}
|
}
|
||||||
p := paginator.New(&postPaginationAdapter{config: &postsRequestConfig{
|
p := paginator.New(&postPaginationAdapter{config: &postsRequestConfig{
|
||||||
blog: blog,
|
blog: blog,
|
||||||
|
@ -253,7 +282,7 @@ func (a *goBlog) serveIndex(w http.ResponseWriter, r *http.Request) {
|
||||||
publishedYear: ic.year,
|
publishedYear: ic.year,
|
||||||
publishedMonth: ic.month,
|
publishedMonth: ic.month,
|
||||||
publishedDay: ic.day,
|
publishedDay: ic.day,
|
||||||
status: status,
|
statusse: statusse,
|
||||||
priorityOrder: true,
|
priorityOrder: true,
|
||||||
}, a: a}, bc.Pagination)
|
}, a: a}, bc.Pagination)
|
||||||
p.SetPage(pageNo)
|
p.SetPage(pageNo)
|
||||||
|
|
101
postsDb.go
101
postsDb.go
|
@ -196,40 +196,86 @@ func (db *database) savePost(p *post, o *postCreationOptions) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) deletePost(path string) error {
|
func (a *goBlog) deletePost(path string) error {
|
||||||
p, err := a.deletePostFromDb(path)
|
if path == "" {
|
||||||
if err != nil || p == nil {
|
return errors.New("path required")
|
||||||
|
}
|
||||||
|
// Lock post creation
|
||||||
|
a.db.pcm.Lock()
|
||||||
|
defer a.db.pcm.Unlock()
|
||||||
|
// Check if post exists
|
||||||
|
p, err := a.getPost(path)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Purge cache
|
// Post exists, check if it's already marked as deleted
|
||||||
a.cache.purge()
|
if strings.HasSuffix(string(p.Status), statusDeletedSuffix) {
|
||||||
// Trigger hooks
|
// Post is already marked as deleted, delete it from database
|
||||||
a.postDeleteHooks(p)
|
if _, err = a.db.exec(
|
||||||
|
`begin; delete from posts where path = ?; delete from post_parameters where path = ?; insert or ignore into deleted (path) values (?); commit;`,
|
||||||
|
dbNoCache, p.Path, p.Path, p.Path,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Rebuild FTS index
|
||||||
|
a.db.rebuildFTSIndex()
|
||||||
|
// Purge cache
|
||||||
|
a.cache.purge()
|
||||||
|
} else {
|
||||||
|
// Update post status
|
||||||
|
p.Status = postStatus(string(p.Status) + statusDeletedSuffix)
|
||||||
|
// Add parameter
|
||||||
|
deletedTime := utcNowString()
|
||||||
|
if p.Parameters == nil {
|
||||||
|
p.Parameters = map[string][]string{}
|
||||||
|
}
|
||||||
|
p.Parameters["deleted"] = []string{deletedTime}
|
||||||
|
// Mark post as deleted
|
||||||
|
if _, err = a.db.exec(
|
||||||
|
`begin; update posts set status = ? where path = ?; delete from post_parameters where path = ? and value = 'deleted'; insert into post_parameters (path, parameter, value) values (?, 'deleted', ?); commit;`,
|
||||||
|
dbNoCache, p.Status, p.Path, p.Path, p.Path, deletedTime,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Rebuild FTS index
|
||||||
|
a.db.rebuildFTSIndex()
|
||||||
|
// Purge cache
|
||||||
|
a.cache.purge()
|
||||||
|
// Trigger hooks
|
||||||
|
a.postDeleteHooks(p)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) deletePostFromDb(path string) (*post, error) {
|
func (a *goBlog) undeletePost(path string) error {
|
||||||
if path == "" {
|
if path == "" {
|
||||||
return nil, nil
|
return errors.New("path required")
|
||||||
}
|
}
|
||||||
|
// Lock post creation
|
||||||
a.db.pcm.Lock()
|
a.db.pcm.Lock()
|
||||||
defer a.db.pcm.Unlock()
|
defer a.db.pcm.Unlock()
|
||||||
|
// Check if post exists
|
||||||
p, err := a.getPost(path)
|
p, err := a.getPost(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
_, err = a.db.exec(
|
// Post exists, update status and parameters
|
||||||
`begin;
|
p.Status = postStatus(strings.TrimSuffix(string(p.Status), statusDeletedSuffix))
|
||||||
delete from posts where path = ?;
|
// Remove parameter
|
||||||
delete from post_parameters where path = ?;
|
p.Parameters["deleted"] = nil
|
||||||
insert or ignore into deleted (path) values (?);
|
// Update database
|
||||||
commit;`,
|
if _, err = a.db.exec(
|
||||||
dbNoCache, p.Path, p.Path, p.Path,
|
`begin; update posts set status = ? where path = ?; delete from post_parameters where path = ? and parameter = 'deleted'; commit;`,
|
||||||
)
|
dbNoCache, p.Status, p.Path, p.Path,
|
||||||
if err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
// Rebuild FTS index
|
||||||
a.db.rebuildFTSIndex()
|
a.db.rebuildFTSIndex()
|
||||||
return p, nil
|
// Purge cache
|
||||||
|
a.cache.purge()
|
||||||
|
// Trigger hooks
|
||||||
|
a.postUndeleteHooks(p)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *database) replacePostParam(path, param string, values []string) error {
|
func (db *database) replacePostParam(path, param string, values []string) error {
|
||||||
|
@ -270,6 +316,7 @@ type postsRequestConfig struct {
|
||||||
offset int
|
offset int
|
||||||
sections []string
|
sections []string
|
||||||
status postStatus
|
status postStatus
|
||||||
|
statusse []postStatus
|
||||||
taxonomy *configTaxonomy
|
taxonomy *configTaxonomy
|
||||||
taxonomyValue string
|
taxonomyValue string
|
||||||
parameters []string // Ignores parameterValue
|
parameters []string // Ignores parameterValue
|
||||||
|
@ -307,6 +354,19 @@ func buildPostsQuery(c *postsRequestConfig, selection string) (query string, arg
|
||||||
queryBuilder.WriteString(" and status = @status")
|
queryBuilder.WriteString(" and status = @status")
|
||||||
args = append(args, sql.Named("status", c.status))
|
args = append(args, sql.Named("status", c.status))
|
||||||
}
|
}
|
||||||
|
if c.statusse != nil && len(c.statusse) > 0 {
|
||||||
|
queryBuilder.WriteString(" and status in (")
|
||||||
|
for i, status := range c.statusse {
|
||||||
|
if i > 0 {
|
||||||
|
queryBuilder.WriteString(", ")
|
||||||
|
}
|
||||||
|
named := "status" + strconv.Itoa(i)
|
||||||
|
queryBuilder.WriteByte('@')
|
||||||
|
queryBuilder.WriteString(named)
|
||||||
|
args = append(args, sql.Named(named, status))
|
||||||
|
}
|
||||||
|
queryBuilder.WriteByte(')')
|
||||||
|
}
|
||||||
if c.blog != "" {
|
if c.blog != "" {
|
||||||
queryBuilder.WriteString(" and blog = @blog")
|
queryBuilder.WriteString(" and blog = @blog")
|
||||||
args = append(args, sql.Named("blog", c.blog))
|
args = append(args, sql.Named("blog", c.blog))
|
||||||
|
@ -529,6 +589,7 @@ func (a *goBlog) getRandomPostPath(blog string) (path string, err error) {
|
||||||
|
|
||||||
func (d *database) allTaxonomyValues(blog string, taxonomy string) ([]string, error) {
|
func (d *database) allTaxonomyValues(blog string, taxonomy string) ([]string, error) {
|
||||||
var values []string
|
var values []string
|
||||||
|
// TODO: Query posts the normal way
|
||||||
rows, err := d.query("select distinct value from post_parameters where parameter = @tax and length(coalesce(value, '')) > 0 and path in (select path from posts where blog = @blog and status = @status) order by value", sql.Named("tax", taxonomy), sql.Named("blog", blog), sql.Named("status", statusPublished))
|
rows, err := d.query("select distinct value from post_parameters where parameter = @tax and length(coalesce(value, '')) > 0 and path in (select path from posts where blog = @blog and status = @status) order by value", sql.Named("tax", taxonomy), sql.Named("blog", blog), sql.Named("status", statusPublished))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -14,22 +14,19 @@ func Test_postsDb(t *testing.T) {
|
||||||
must := require.New(t)
|
must := require.New(t)
|
||||||
|
|
||||||
app := &goBlog{
|
app := &goBlog{
|
||||||
cfg: &config{
|
cfg: createDefaultTestConfig(t),
|
||||||
Db: &configDb{
|
}
|
||||||
File: filepath.Join(t.TempDir(), "test.db"),
|
app.cfg.Blogs = map[string]*configBlog{
|
||||||
},
|
"en": {
|
||||||
Blogs: map[string]*configBlog{
|
Sections: map[string]*configSection{
|
||||||
"en": {
|
"test": {},
|
||||||
Sections: map[string]*configSection{
|
"micro": {},
|
||||||
"test": {},
|
|
||||||
"micro": {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
_ = app.initConfig()
|
||||||
_ = app.initDatabase(false)
|
_ = app.initDatabase(false)
|
||||||
app.initMarkdown()
|
app.initComponents(false)
|
||||||
|
|
||||||
now := toLocalSafe(time.Now().String())
|
now := toLocalSafe(time.Now().String())
|
||||||
nowPlus1Hour := toLocalSafe(time.Now().Add(1 * time.Hour).String())
|
nowPlus1Hour := toLocalSafe(time.Now().Add(1 * time.Hour).String())
|
||||||
|
@ -95,7 +92,19 @@ func Test_postsDb(t *testing.T) {
|
||||||
is.Equal(1, count)
|
is.Equal(1, count)
|
||||||
|
|
||||||
// Delete post
|
// Delete post
|
||||||
_, err = app.deletePostFromDb("/test/abc")
|
err = app.deletePost("/test/abc")
|
||||||
|
must.NoError(err)
|
||||||
|
|
||||||
|
// Check if post is marked as deleted
|
||||||
|
count, err = app.db.countPosts(&postsRequestConfig{status: statusDraft})
|
||||||
|
must.NoError(err)
|
||||||
|
is.Equal(0, count)
|
||||||
|
count, err = app.db.countPosts(&postsRequestConfig{status: statusDraftDeleted})
|
||||||
|
must.NoError(err)
|
||||||
|
is.Equal(1, count)
|
||||||
|
|
||||||
|
// Delete post again
|
||||||
|
err = app.deletePost("/test/abc")
|
||||||
must.NoError(err)
|
must.NoError(err)
|
||||||
|
|
||||||
// Check that there is no post
|
// Check that there is no post
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/araddon/dateparse"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *goBlog) initPostsDeleter() {
|
||||||
|
a.hourlyHooks = append(a.hourlyHooks, func() {
|
||||||
|
a.checkDeletedPosts()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *goBlog) checkDeletedPosts() {
|
||||||
|
// Get all posts with `deleted` parameter and a deleted status
|
||||||
|
postsToDelete, err := a.getPosts(&postsRequestConfig{
|
||||||
|
statusse: []postStatus{statusPublishedDeleted, statusDraftDeleted, statusPrivateDeleted, statusUnlistedDeleted, statusScheduledDeleted},
|
||||||
|
parameter: "deleted",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error getting deleted posts:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, post := range postsToDelete {
|
||||||
|
// Check if post is deleted for more than 7 days
|
||||||
|
if deleted, err := dateparse.ParseLocal(post.firstParameter("deleted")); err == nil && deleted.Add(time.Hour*24*7).Before(time.Now()) {
|
||||||
|
a.deletePost(post.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_checkDeletedPosts(t *testing.T) {
|
||||||
|
app := &goBlog{
|
||||||
|
cfg: createDefaultTestConfig(t),
|
||||||
|
}
|
||||||
|
_ = app.initConfig()
|
||||||
|
_ = app.initDatabase(false)
|
||||||
|
app.initComponents(false)
|
||||||
|
|
||||||
|
// Create a post
|
||||||
|
app.createPost(&post{
|
||||||
|
Content: "Test",
|
||||||
|
Status: statusPublished,
|
||||||
|
Path: "/testpost",
|
||||||
|
Section: "posts",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check if post count is 1
|
||||||
|
count, err := app.db.countPosts(&postsRequestConfig{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, count)
|
||||||
|
|
||||||
|
// Run deleter
|
||||||
|
app.checkDeletedPosts()
|
||||||
|
|
||||||
|
// Check if post count is still 1
|
||||||
|
count, err = app.db.countPosts(&postsRequestConfig{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, count)
|
||||||
|
|
||||||
|
// Delete the post
|
||||||
|
err = app.deletePost("/testpost")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Run deleter
|
||||||
|
app.checkDeletedPosts()
|
||||||
|
|
||||||
|
// Check if post count is still 1
|
||||||
|
count, err = app.db.countPosts(&postsRequestConfig{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, count)
|
||||||
|
|
||||||
|
// Set deleted time to more than 7 days ago
|
||||||
|
app.db.replacePostParam("/testpost", "deleted", []string{time.Now().Add(-time.Hour * 24 * 8).Format(time.RFC3339)})
|
||||||
|
|
||||||
|
// Run deleter
|
||||||
|
app.checkDeletedPosts()
|
||||||
|
|
||||||
|
// Check if post count is 0
|
||||||
|
count, err = app.db.countPosts(&postsRequestConfig{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 0, count)
|
||||||
|
}
|
|
@ -264,3 +264,7 @@ func (p *post) Old() bool {
|
||||||
func (p *post) TTS() string {
|
func (p *post) TTS() string {
|
||||||
return p.firstParameter(ttsParameter)
|
return p.firstParameter(ttsParameter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *post) Deleted() bool {
|
||||||
|
return strings.HasSuffix(string(p.Status), statusDeletedSuffix)
|
||||||
|
}
|
||||||
|
|
104
posts_test.go
104
posts_test.go
|
@ -99,3 +99,107 @@ func Test_serveDate(t *testing.T) {
|
||||||
Client(client).Fetch(context.Background())
|
Client(client).Fetch(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_servePost(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
app := &goBlog{
|
||||||
|
cfg: createDefaultTestConfig(t),
|
||||||
|
}
|
||||||
|
app.cfg.User.AppPasswords = append(app.cfg.User.AppPasswords, &configAppPassword{
|
||||||
|
Username: "test",
|
||||||
|
Password: "test",
|
||||||
|
})
|
||||||
|
_ = app.initConfig()
|
||||||
|
_ = app.initDatabase(false)
|
||||||
|
app.initComponents(false)
|
||||||
|
|
||||||
|
app.d, err = app.buildRouter()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create a post
|
||||||
|
err = app.createPost(&post{
|
||||||
|
Path: "/testpost",
|
||||||
|
Section: "posts",
|
||||||
|
Status: "published",
|
||||||
|
Published: "2020-10-15T10:00:00Z",
|
||||||
|
Parameters: map[string][]string{"title": {"Test Post"}},
|
||||||
|
Content: "Test Content",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: &handlerRoundTripper{
|
||||||
|
handler: app.d,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var resString string
|
||||||
|
|
||||||
|
// Check if the post is served
|
||||||
|
err = requests.
|
||||||
|
URL("http://localhost:8080/testpost").
|
||||||
|
CheckStatus(http.StatusOK).
|
||||||
|
ToString(&resString).
|
||||||
|
Client(client).Fetch(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Contains(t, resString, "<h1 class=p-name>Test Post</h1>")
|
||||||
|
|
||||||
|
// Delete the post
|
||||||
|
err = app.deletePost("/testpost")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Check if the post is no longer served
|
||||||
|
err = requests.
|
||||||
|
URL("http://localhost:8080/testpost").
|
||||||
|
CheckStatus(http.StatusGone).
|
||||||
|
ToString(&resString).
|
||||||
|
Client(client).Fetch(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Contains(t, resString, "410 Gone")
|
||||||
|
|
||||||
|
// Check if the post is still served for logged in user
|
||||||
|
err = requests.
|
||||||
|
URL("http://localhost:8080/testpost").
|
||||||
|
BasicAuth("test", "test").
|
||||||
|
CheckStatus(http.StatusGone).
|
||||||
|
ToString(&resString).
|
||||||
|
Client(client).Fetch(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Contains(t, resString, "<h1 class=p-name>Test Post</h1>")
|
||||||
|
|
||||||
|
// Undelete the post
|
||||||
|
err = app.undeletePost("/testpost")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Check if the post is served
|
||||||
|
err = requests.
|
||||||
|
URL("http://localhost:8080/testpost").
|
||||||
|
CheckStatus(http.StatusOK).
|
||||||
|
ToString(&resString).
|
||||||
|
Client(client).Fetch(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Contains(t, resString, "<h1 class=p-name>Test Post</h1>")
|
||||||
|
|
||||||
|
// Delete the post completely
|
||||||
|
err = app.deletePost("/testpost")
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = app.deletePost("/testpost")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Check if the post is no longer served
|
||||||
|
err = requests.
|
||||||
|
URL("http://localhost:8080/testpost").
|
||||||
|
BasicAuth("test", "test").
|
||||||
|
CheckStatus(http.StatusGone).
|
||||||
|
ToString(&resString).
|
||||||
|
Client(client).Fetch(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.NotContains(t, resString, "<h1 class=p-name>Test Post</h1>")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
10
telegram.go
10
telegram.go
|
@ -102,8 +102,18 @@ func (a *goBlog) initTelegram() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to delete Telegram message: %v", err)
|
log.Printf("Failed to delete Telegram message: %v", err)
|
||||||
}
|
}
|
||||||
|
// Delete chat and message id from post
|
||||||
|
err = a.db.replacePostParam(p.Path, "telegramchat", []string{})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to remove Telegram chat id: %v", err)
|
||||||
|
}
|
||||||
|
err = a.db.replacePostParam(p.Path, "telegrammsg", []string{})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to remove Telegram message id: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
// TODO: Handle undelete
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tg *configTelegram) enabled() bool {
|
func (tg *configTelegram) enabled() bool {
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
<p><a href="{{ .Blog.RelativePath "/editor/private" }}">{{ string .Blog.Lang "privateposts" }}</a></p>
|
<p><a href="{{ .Blog.RelativePath "/editor/private" }}">{{ string .Blog.Lang "privateposts" }}</a></p>
|
||||||
<p><a href="{{ .Blog.RelativePath "/editor/unlisted" }}">{{ string .Blog.Lang "unlistedposts" }}</a></p>
|
<p><a href="{{ .Blog.RelativePath "/editor/unlisted" }}">{{ string .Blog.Lang "unlistedposts" }}</a></p>
|
||||||
<p><a href="{{ .Blog.RelativePath "/editor/scheduled" }}">{{ string .Blog.Lang "scheduledposts" }}</a></p>
|
<p><a href="{{ .Blog.RelativePath "/editor/scheduled" }}">{{ string .Blog.Lang "scheduledposts" }}</a></p>
|
||||||
|
<p><a href="{{ .Blog.RelativePath "/editor/deleted" }}">{{ string .Blog.Lang "deletedposts" }}</a></p>
|
||||||
|
|
||||||
<h2>{{ string .Blog.Lang "upload" }}</h2>
|
<h2>{{ string .Blog.Lang "upload" }}</h2>
|
||||||
<form class="fw p" method="post" enctype="multipart/form-data">
|
<form class="fw p" method="post" enctype="multipart/form-data">
|
||||||
|
|
|
@ -36,6 +36,13 @@
|
||||||
<input type="hidden" name="url" value="{{ .Canonical }}">
|
<input type="hidden" name="url" value="{{ .Canonical }}">
|
||||||
<input type="submit" value="{{ string .Blog.Lang "delete" }}" class="confirm" data-confirmmessage="{{ string .Blog.Lang "confirmdelete" }}">
|
<input type="submit" value="{{ string .Blog.Lang "delete" }}" class="confirm" data-confirmmessage="{{ string .Blog.Lang "confirmdelete" }}">
|
||||||
</form>
|
</form>
|
||||||
|
{{ if .Data.Deleted }}
|
||||||
|
<form class="in" method="post" action="{{ .Blog.RelativePath "/editor" }}">
|
||||||
|
<input type="hidden" name="action" value="undelete">
|
||||||
|
<input type="hidden" name="url" value="{{ .Canonical }}">
|
||||||
|
<input type="submit" value="{{ string .Blog.Lang "undelete" }}">
|
||||||
|
</form>
|
||||||
|
{{ end }}
|
||||||
{{ if ttsenabled }}
|
{{ if ttsenabled }}
|
||||||
<form class="in" method="post" action="{{ .Blog.RelativePath "/editor" }}">
|
<form class="in" method="post" action="{{ .Blog.RelativePath "/editor" }}">
|
||||||
<input type="hidden" name="editoraction" value="tts">
|
<input type="hidden" name="editoraction" value="tts">
|
||||||
|
|
|
@ -10,9 +10,12 @@ contactagreesend: "Akzeptieren & Senden"
|
||||||
contactsend: "Senden"
|
contactsend: "Senden"
|
||||||
create: "Erstellen"
|
create: "Erstellen"
|
||||||
delete: "Löschen"
|
delete: "Löschen"
|
||||||
|
deletedposts: "Gelöschte Posts"
|
||||||
|
deletedpostsdesc: "Gelöschte Posts, die nach 7 Tagen endgültig gelöscht werden."
|
||||||
docomment: "Kommentieren"
|
docomment: "Kommentieren"
|
||||||
download: "Herunterladen"
|
download: "Herunterladen"
|
||||||
drafts: "Entwürfe"
|
drafts: "Entwürfe"
|
||||||
|
draftsdesc: "Posts mit dem Status `draft`."
|
||||||
editor: "Editor"
|
editor: "Editor"
|
||||||
editorpostdesc: "💡 Leere Parameter werden automatisch entfernt. Mehr mögliche Parameter: %s. Mögliche Zustände für `%s`: %s."
|
editorpostdesc: "💡 Leere Parameter werden automatisch entfernt. Mehr mögliche Parameter: %s. Mögliche Zustände für `%s`: %s."
|
||||||
emailopt: "E-Mail (optional)"
|
emailopt: "E-Mail (optional)"
|
||||||
|
@ -41,9 +44,11 @@ pinned: "Angepinnt"
|
||||||
posts: "Posts"
|
posts: "Posts"
|
||||||
prev: "Zurück"
|
prev: "Zurück"
|
||||||
privateposts: "Private Posts"
|
privateposts: "Private Posts"
|
||||||
|
privatepostsdesc: "Posts mit dem Status `private`, die nur eingeloggt sichtbar sind."
|
||||||
publishedon: "Veröffentlicht am"
|
publishedon: "Veröffentlicht am"
|
||||||
replyto: "Antwort an"
|
replyto: "Antwort an"
|
||||||
scheduledposts: "Geplante Posts"
|
scheduledposts: "Geplante Posts"
|
||||||
|
scheduledpostsdesc: "Beiträge mit dem Status `scheduled`, die veröffentlicht werden, wenn das `published`-Datum erreicht ist."
|
||||||
search: "Suchen"
|
search: "Suchen"
|
||||||
send: "Senden (zur Überprüfung)"
|
send: "Senden (zur Überprüfung)"
|
||||||
share: "Online teilen"
|
share: "Online teilen"
|
||||||
|
@ -55,7 +60,9 @@ submit: "Abschicken"
|
||||||
total: "Gesamt"
|
total: "Gesamt"
|
||||||
translate: "Übersetzen"
|
translate: "Übersetzen"
|
||||||
translations: "Übersetzungen"
|
translations: "Übersetzungen"
|
||||||
|
undelete: "Wiederherstellen"
|
||||||
unlistedposts: "Ungelistete Posts"
|
unlistedposts: "Ungelistete Posts"
|
||||||
|
unlistedpostsdesc: "Posts mit dem Status `unlisted`, die nicht in Archiven angezeigt werden."
|
||||||
update: "Aktualisieren"
|
update: "Aktualisieren"
|
||||||
updatedon: "Aktualisiert am"
|
updatedon: "Aktualisiert am"
|
||||||
upload: "Hochladen"
|
upload: "Hochladen"
|
||||||
|
|
|
@ -13,9 +13,12 @@ contactagreesend: "Accept & Send"
|
||||||
contactsend: "Send"
|
contactsend: "Send"
|
||||||
create: "Create"
|
create: "Create"
|
||||||
delete: "Delete"
|
delete: "Delete"
|
||||||
|
deletedposts: "Deleted posts"
|
||||||
|
deletedpostsdesc: "Deleted posts that will be permanently deleted after 7 days."
|
||||||
docomment: "Comment"
|
docomment: "Comment"
|
||||||
download: "Download"
|
download: "Download"
|
||||||
drafts: "Drafts"
|
drafts: "Drafts"
|
||||||
|
draftsdesc: "Posts with status `draft`."
|
||||||
editor: "Editor"
|
editor: "Editor"
|
||||||
editorpostdesc: "💡 Empty parameters are removed automatically. More possible parameters: %s. Possible states for `%s`: %s."
|
editorpostdesc: "💡 Empty parameters are removed automatically. More possible parameters: %s. Possible states for `%s`: %s."
|
||||||
emailopt: "Email (optional)"
|
emailopt: "Email (optional)"
|
||||||
|
@ -51,10 +54,12 @@ pinned: "Pinned"
|
||||||
posts: "Posts"
|
posts: "Posts"
|
||||||
prev: "Previous"
|
prev: "Previous"
|
||||||
privateposts: "Private posts"
|
privateposts: "Private posts"
|
||||||
|
privatepostsdesc: "Posts with status `private` that are visible only when logged in."
|
||||||
publishedon: "Published on"
|
publishedon: "Published on"
|
||||||
replyto: "Reply to"
|
replyto: "Reply to"
|
||||||
reverify: "Reverify"
|
reverify: "Reverify"
|
||||||
scheduledposts: "Scheduled posts"
|
scheduledposts: "Scheduled posts"
|
||||||
|
scheduledpostsdesc: "Posts with status `scheduled` that are published when the `published` date is reached."
|
||||||
scopes: "Scopes"
|
scopes: "Scopes"
|
||||||
search: "Search"
|
search: "Search"
|
||||||
send: "Send (to review)"
|
send: "Send (to review)"
|
||||||
|
@ -68,7 +73,9 @@ total: "Total"
|
||||||
totp: "TOTP"
|
totp: "TOTP"
|
||||||
translate: "Translate"
|
translate: "Translate"
|
||||||
translations: "Translations"
|
translations: "Translations"
|
||||||
|
undelete: "Undelete"
|
||||||
unlistedposts: "Unlisted posts"
|
unlistedposts: "Unlisted posts"
|
||||||
|
unlistedpostsdesc: "Posts with status `unlisted` that are not displayed in archives."
|
||||||
update: "Update"
|
update: "Update"
|
||||||
updatedon: "Updated on"
|
updatedon: "Updated on"
|
||||||
upload: "Upload"
|
upload: "Upload"
|
||||||
|
|
1
tts.go
1
tts.go
|
@ -38,6 +38,7 @@ func (a *goBlog) initTTS() {
|
||||||
}
|
}
|
||||||
a.pPostHooks = append(a.pPostHooks, createOrUpdate)
|
a.pPostHooks = append(a.pPostHooks, createOrUpdate)
|
||||||
a.pUpdateHooks = append(a.pUpdateHooks, createOrUpdate)
|
a.pUpdateHooks = append(a.pUpdateHooks, createOrUpdate)
|
||||||
|
a.pUndeleteHooks = append(a.pUndeleteHooks, createOrUpdate)
|
||||||
a.pDeleteHooks = append(a.pDeleteHooks, func(p *post) {
|
a.pDeleteHooks = append(a.pDeleteHooks, func(p *post) {
|
||||||
// Try to delete the audio file
|
// Try to delete the audio file
|
||||||
_ = a.deletePostTTSAudio(p)
|
_ = a.deletePostTTSAudio(p)
|
||||||
|
|
|
@ -44,6 +44,7 @@ func (a *goBlog) initWebmention() {
|
||||||
a.pPostHooks = append(a.pPostHooks, hookFunc)
|
a.pPostHooks = append(a.pPostHooks, hookFunc)
|
||||||
a.pUpdateHooks = append(a.pUpdateHooks, hookFunc)
|
a.pUpdateHooks = append(a.pUpdateHooks, hookFunc)
|
||||||
a.pDeleteHooks = append(a.pDeleteHooks, hookFunc)
|
a.pDeleteHooks = append(a.pDeleteHooks, hookFunc)
|
||||||
|
a.pUndeleteHooks = append(a.pUndeleteHooks, hookFunc)
|
||||||
// Start verifier
|
// Start verifier
|
||||||
a.initWebmentionQueue()
|
a.initWebmentionQueue()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue