Add support for undeleting posts within 7 days (#3)

pull/10/head
Jan-Lukas Else 7 months ago
parent 86625cc8fe
commit 337392112b
  1. 21
      activityPub.go
  2. 9
      app.go
  3. 1
      blogstats.go
  4. 7
      config.go
  5. 2
      example-config.yml
  6. 16
      hooks.go
  7. 13
      http.go
  8. 3
      httpRouters.go
  9. 2
      indieAuth.go
  10. 1
      indieAuth_test.go
  11. 1
      main.go
  12. 32
      micropub.go
  13. 2
      ntfy_test.go
  14. 77
      posts.go
  15. 101
      postsDb.go
  16. 35
      postsDb_test.go
  17. 32
      postsDeleter.go
  18. 61
      postsDeleter_test.go
  19. 4
      postsFuncs.go
  20. 104
      posts_test.go
  21. 10
      telegram.go
  22. 1
      templates/editor.gohtml
  23. 7
      templates/post.gohtml
  24. 7
      templates/strings/de.yaml
  25. 7
      templates/strings/default.yaml
  26. 1
      tts.go
  27. 1
      webmention.go

@ -45,6 +45,9 @@ func (a *goBlog) initActivityPub() error {
a.pDeleteHooks = append(a.pDeleteHooks, func(p *post) {
a.apDelete(p)
})
a.pUndeleteHooks = append(a.pUndeleteHooks, func(p *post) {
a.apUndelete(p)
})
// Prepare webfinger
a.webfingerResources = map[string]*configBlog{}
a.webfingerAccts = map[string]string{}
@ -339,11 +342,21 @@ func (a *goBlog) apDelete(p *post) {
a.apSendToAllFollowers(p.Blog, map[string]interface{}{
"@context": []string{asContext},
"actor": a.apIri(a.cfg.Blogs[p.Blog]),
"id": a.fullPostURL(p) + "#delete",
"type": "Delete",
"object": map[string]string{
"id": a.fullPostURL(p),
"type": "Tombstone",
"object": a.fullPostURL(p),
})
}
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),
},
})
}

@ -43,10 +43,11 @@ type goBlog struct {
// Errors
errorCheckMediaTypes []ct.MediaType
// Hooks
pPostHooks []postHookFunc
pUpdateHooks []postHookFunc
pDeleteHooks []postHookFunc
hourlyHooks []hourlyHookFunc
pPostHooks []postHookFunc
pUpdateHooks []postHookFunc
pDeleteHooks []postHookFunc
pUndeleteHooks []postHookFunc
hourlyHooks []hourlyHookFunc
// HTTP Client
httpClient *http.Client
// HTTP Routers

@ -19,6 +19,7 @@ func (a *goBlog) initBlogStats() {
a.pPostHooks = append(a.pPostHooks, f)
a.pUpdateHooks = append(a.pUpdateHooks, f)
a.pDeleteHooks = append(a.pDeleteHooks, f)
a.pUndeleteHooks = append(a.pUndeleteHooks, f)
}
func (a *goBlog) serveBlogStats(w http.ResponseWriter, r *http.Request) {

@ -209,9 +209,10 @@ type configHooks struct {
Hourly []string `mapstructure:"hourly"`
PreStart []string `mapstructure:"prestart"`
// Can use template
PostPost []string `mapstructure:"postpost"`
PostUpdate []string `mapstructure:"postupdate"`
PostDelete []string `mapstructure:"postdelete"`
PostPost []string `mapstructure:"postpost"`
PostUpdate []string `mapstructure:"postupdate"`
PostDelete []string `mapstructure:"postdelete"`
PostUndelete []string `mapstructure:"postundelete"`
}
type configMicropub struct {

@ -70,6 +70,8 @@ hooks:
- echo Updated post at {{.URL}}
postdelete: # Commands to execute after deleting a post
- echo Deleted post at {{.URL}}
postundelete: # Commands to execute after undeleting a post
- echo Undeleted post at {{.URL}}
# ActivityPub
activityPub:

@ -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{}) {
cmdTmpl, err := template.New("cmd").Parse(tmpl)
if err != nil {

@ -265,7 +265,14 @@ func (a *goBlog) servePostsAliasesRedirects() http.HandlerFunc {
case statusPublished, statusUnlisted:
alicePrivate.Append(a.checkActivityStreamsRequest, a.cacheMiddleware).ThenFunc(a.servePost).ServeHTTP(w, r)
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)
return
}
@ -277,9 +284,7 @@ func (a *goBlog) servePostsAliasesRedirects() http.HandlerFunc {
return
case "deleted":
// Is deleted, serve 410
alicePrivate.Append(a.cacheMiddleware).ThenFunc(func(w http.ResponseWriter, r *http.Request) {
a.serve410(w, r)
}).ServeHTTP(w, r)
alicePrivate.Append(a.cacheMiddleware).ThenFunc(a.serve410).ServeHTTP(w, r)
return
}
}

@ -345,6 +345,9 @@ func (a *goBlog) blogEditorRouter(conf *configBlog) func(r chi.Router) {
r.Get("/scheduled", a.serveScheduled)
r.Get("/scheduled"+feedPath, 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)
}
}

@ -40,6 +40,6 @@ func (a *goBlog) checkIndieAuth(next http.Handler) http.Handler {
func addAllScopes(next http.Handler) http.Handler {
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, "update")
assert.Contains(t, scope, "delete")
assert.Contains(t, scope, "undelete")
assert.Contains(t, scope, "media")
checked = true
})).ServeHTTP(rec, req)

@ -181,6 +181,7 @@ func (app *goBlog) initComponents(logging bool) {
app.initSessions()
app.initIndieAuth()
app.startPostsScheduler()
app.initPostsDeleter()
// Log finish
if logging {
log.Println("Initialized components")

@ -102,6 +102,8 @@ func (a *goBlog) serveMicropubPost(w http.ResponseWriter, r *http.Request) {
switch action {
case actionDelete:
a.micropubDelete(w, r, r.Form.Get("url"))
case actionUndelete:
a.micropubUndelete(w, r, r.Form.Get("url"))
default:
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 {
case actionDelete:
a.micropubDelete(w, r, parsedMfItem.URL)
case actionUndelete:
a.micropubUndelete(w, r, parsedMfItem.URL)
case actionUpdate:
a.micropubUpdate(w, r, parsedMfItem.URL, parsedMfItem)
default:
@ -226,8 +230,9 @@ func (a *goBlog) micropubParseValuePostParamsValueMap(entry *post, values map[st
type micropubAction string
const (
actionUpdate micropubAction = "update"
actionDelete micropubAction = "delete"
actionUpdate micropubAction = "update"
actionDelete micropubAction = "delete"
actionUndelete micropubAction = "undelete"
)
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)
}
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) {
if !strings.Contains(r.Context().Value(indieAuthScope).(string), "update") {
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)
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
oldStatus := p.Status
a.micropubUpdateReplace(p, mf.Replace)

@ -26,7 +26,7 @@ func Test_ntfySending(t *testing.T) {
_ = app.initConfig()
_ = app.initDatabase(false)
app.initComponents(true)
app.initComponents(false)
app.sendNotification("Test notification")

@ -37,17 +37,24 @@ type post struct {
type postStatus string
const (
statusNil postStatus = ""
statusPublished postStatus = "published"
statusDraft postStatus = "draft"
statusPrivate postStatus = "private"
statusUnlisted postStatus = "unlisted"
statusScheduled postStatus = "scheduled"
statusDeletedSuffix string = "-deleted"
statusNil postStatus = ""
statusPublished postStatus = "published"
statusPublishedDeleted postStatus = "published-deleted"
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) {
p, err := a.getPost(r.URL.Path)
if err == errPostNotFound {
if errors.Is(err, errPostNotFound) {
a.serve404(w, r)
return
} else if err != nil {
@ -71,7 +78,11 @@ func (a *goBlog) servePost(w http.ResponseWriter, r *http.Request) {
template = templateStaticHome
}
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,
Canonical: canonical,
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) {
_, 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"),
status: statusDraft,
path: bc.getRelativePath("/editor/drafts"),
title: a.ts.GetTemplateStringVariant(bc.Lang, "drafts"),
description: a.ts.GetTemplateStringVariant(bc.Lang, "draftsdesc"),
status: 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"),
status: statusPrivate,
path: bc.getRelativePath("/editor/private"),
title: a.ts.GetTemplateStringVariant(bc.Lang, "privateposts"),
description: a.ts.GetTemplateStringVariant(bc.Lang, "privatepostsdesc"),
status: statusPrivate,
})))
}
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"),
status: statusUnlisted,
path: bc.getRelativePath("/editor/unlisted"),
title: a.ts.GetTemplateStringVariant(bc.Lang, "unlistedposts"),
description: a.ts.GetTemplateStringVariant(bc.Lang, "unlistedpostsdesc"),
status: statusUnlisted,
})))
}
func (a *goBlog) serveScheduled(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/scheduled"),
title: a.ts.GetTemplateStringVariant(bc.Lang, "scheduledposts"),
status: statusScheduled,
path: bc.getRelativePath("/editor/scheduled"),
title: a.ts.GetTemplateStringVariant(bc.Lang, "scheduledposts"),
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
summaryTemplate string
status postStatus
statusse []postStatus
}
const defaultPhotosPath = "/photos"
@ -239,9 +265,12 @@ func (a *goBlog) serveIndex(w http.ResponseWriter, r *http.Request) {
sections = append(sections, sectionKey)
}
}
status := ic.status
if status == statusNil {
status = statusPublished
statusse := ic.statusse
if ic.status != statusNil {
statusse = []postStatus{ic.status}
}
if len(statusse) == 0 {
statusse = []postStatus{statusPublished}
}
p := paginator.New(&postPaginationAdapter{config: &postsRequestConfig{
blog: blog,
@ -253,7 +282,7 @@ func (a *goBlog) serveIndex(w http.ResponseWriter, r *http.Request) {
publishedYear: ic.year,
publishedMonth: ic.month,
publishedDay: ic.day,
status: status,
statusse: statusse,
priorityOrder: true,
}, a: a}, bc.Pagination)
p.SetPage(pageNo)

@ -196,40 +196,86 @@ func (db *database) savePost(p *post, o *postCreationOptions) error {
}
func (a *goBlog) deletePost(path string) error {
p, err := a.deletePostFromDb(path)
if err != nil || p == nil {
if path == "" {
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
}
// Purge cache
a.cache.purge()
// Trigger hooks
a.postDeleteHooks(p)
// Post exists, check if it's already marked as deleted
if strings.HasSuffix(string(p.Status), statusDeletedSuffix) {
// Post is already marked as deleted, delete it from database
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
}
func (a *goBlog) deletePostFromDb(path string) (*post, error) {
func (a *goBlog) undeletePost(path string) error {
if path == "" {
return nil, 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 nil, err
return err
}
_, 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,
)
if err != nil {
return nil, err
// Post exists, update status and parameters
p.Status = postStatus(strings.TrimSuffix(string(p.Status), statusDeletedSuffix))
// Remove parameter
p.Parameters["deleted"] = nil
// Update database
if _, err = a.db.exec(
`begin; update posts set status = ? where path = ?; delete from post_parameters where path = ? and parameter = 'deleted'; commit;`,
dbNoCache, p.Status, p.Path, p.Path,
); err != nil {
return err
}
// Rebuild FTS index
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 {
@ -270,6 +316,7 @@ type postsRequestConfig struct {
offset int
sections []string
status postStatus
statusse []postStatus
taxonomy *configTaxonomy
taxonomyValue string
parameters []string // Ignores parameterValue
@ -307,6 +354,19 @@ func buildPostsQuery(c *postsRequestConfig, selection string) (query string, arg
queryBuilder.WriteString(" and status = @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 != "" {
queryBuilder.WriteString(" and blog = @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) {
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))
if err != nil {
return nil, err

@ -14,22 +14,19 @@ func Test_postsDb(t *testing.T) {
must := require.New(t)
app := &goBlog{
cfg: &config{
Db: &configDb{
File: filepath.Join(t.TempDir(), "test.db"),
},
Blogs: map[string]*configBlog{
"en": {
Sections: map[string]*configSection{
"test": {},
"micro": {},
},
},
cfg: createDefaultTestConfig(t),
}
app.cfg.Blogs = map[string]*configBlog{
"en": {
Sections: map[string]*configSection{
"test": {},
"micro": {},
},
},
}
_ = app.initConfig()
_ = app.initDatabase(false)
app.initMarkdown()
app.initComponents(false)
now := toLocalSafe(time.Now().String())
nowPlus1Hour := toLocalSafe(time.Now().Add(1 * time.Hour).String())
@ -95,7 +92,19 @@ func Test_postsDb(t *testing.T) {
is.Equal(1, count)
// 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)
// 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 {
return p.firstParameter(ttsParameter)
}
func (p *post) Deleted() bool {
return strings.HasSuffix(string(p.Status), statusDeletedSuffix)
}

@ -99,3 +99,107 @@ func Test_serveDate(t *testing.T) {
Client(client).Fetch(context.Background())
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>")
}

@ -102,8 +102,18 @@ func (a *goBlog) initTelegram() {
if err != nil {
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 {

@ -31,6 +31,7 @@
<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/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>
<form class="fw p" method="post" enctype="multipart/form-data">

@ -36,6 +36,13 @@
<input type="hidden" name="url" value="{{ .Canonical }}">
<input type="submit" value="{{ string .Blog.Lang "delete" }}" class="confirm" data-confirmmessage="{{ string .Blog.Lang "confirmdelete" }}">
</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 }}
<form class="in" method="post" action="{{ .Blog.RelativePath "/editor" }}">
<input type="hidden" name="editoraction" value="tts">

@ -10,9 +10,12 @@ contactagreesend: "Akzeptieren & Senden"
contactsend: "Senden"
create: "Erstellen"
delete: "Löschen"
deletedposts: "Gelöschte Posts"
deletedpostsdesc: "Gelöschte Posts, die nach 7 Tagen endgültig gelöscht werden."
docomment: "Kommentieren"
download: "Herunterladen"
drafts: "Entwürfe"
draftsdesc: "Posts mit dem Status `draft`."
editor: "Editor"
editorpostdesc: "💡 Leere Parameter werden automatisch entfernt. Mehr mögliche Parameter: %s. Mögliche Zustände für `%s`: %s."
emailopt: "E-Mail (optional)"
@ -41,9 +44,11 @@ pinned: "Angepinnt"
posts: "Posts"
prev: "Zurück"
privateposts: "Private Posts"
privatepostsdesc: "Posts mit dem Status `private`, die nur eingeloggt sichtbar sind."
publishedon: "Veröffentlicht am"
replyto: "Antwort an"
scheduledposts: "Geplante Posts"
scheduledpostsdesc: "Beiträge mit dem Status `scheduled`, die veröffentlicht werden, wenn das `published`-Datum erreicht ist."
search: "Suchen"
send: "Senden (zur Überprüfung)"
share: "Online teilen"
@ -55,7 +60,9 @@ submit: "Abschicken"
total: "Gesamt"
translate: "Übersetzen"
translations: "Übersetzungen"
undelete: "Wiederherstellen"
unlistedposts: "Ungelistete Posts"
unlistedpostsdesc: "Posts mit dem Status `unlisted`, die nicht in Archiven angezeigt werden."
update: "Aktualisieren"
updatedon: "Aktualisiert am"
upload: "Hochladen"

@ -13,9 +13,12 @@ contactagreesend: "Accept & Send"
contactsend: "Send"
create: "Create"
delete: "Delete"
deletedposts: "Deleted posts"
deletedpostsdesc: "Deleted posts that will be permanently deleted after 7 days."
docomment: "Comment"
download: "Download"
drafts: "Drafts"
draftsdesc: "Posts with status `draft`."
editor: "Editor"
editorpostdesc: "💡 Empty parameters are removed automatically. More possible parameters: %s. Possible states for `%s`: %s."
emailopt: "Email (optional)"
@ -51,10 +54,12 @@ pinned: "Pinned"
posts: "Posts"
prev: "Previous"
privateposts: "Private posts"
privatepostsdesc: "Posts with status `private` that are visible only when logged in."
publishedon: "Published on"
replyto: "Reply to"
reverify: "Reverify"
scheduledposts: "Scheduled posts"
scheduledpostsdesc: "Posts with status `scheduled` that are published when the `published` date is reached."
scopes: "Scopes"
search: "Search"
send: "Send (to review)"
@ -68,7 +73,9 @@ total: "Total"
totp: "TOTP"
translate: "Translate"
translations: "Translations"
undelete: "Undelete"
unlistedposts: "Unlisted posts"
unlistedpostsdesc: "Posts with status `unlisted` that are not displayed in archives."
update: "Update"
updatedon: "Updated on"
upload: "Upload"

@ -38,6 +38,7 @@ func (a *goBlog) initTTS() {
}
a.pPostHooks = append(a.pPostHooks, createOrUpdate)
a.pUpdateHooks = append(a.pUpdateHooks, createOrUpdate)
a.pUndeleteHooks = append(a.pUndeleteHooks, createOrUpdate)
a.pDeleteHooks = append(a.pDeleteHooks, func(p *post) {
// Try to delete the audio file
_ = a.deletePostTTSAudio(p)

@ -44,6 +44,7 @@ func (a *goBlog) initWebmention() {
a.pPostHooks = append(a.pPostHooks, hookFunc)
a.pUpdateHooks = append(a.pUpdateHooks, hookFunc)
a.pDeleteHooks = append(a.pDeleteHooks, hookFunc)
a.pUndeleteHooks = append(a.pUndeleteHooks, hookFunc)
// Start verifier
a.initWebmentionQueue()
}

Loading…
Cancel
Save