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.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),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
1
app.go
1
app.go
|
@ -46,6 +46,7 @@ type goBlog struct {
|
|||
pPostHooks []postHookFunc
|
||||
pUpdateHooks []postHookFunc
|
||||
pDeleteHooks []postHookFunc
|
||||
pUndeleteHooks []postHookFunc
|
||||
hourlyHooks []hourlyHookFunc
|
||||
// HTTP Client
|
||||
httpClient *http.Client
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -212,6 +212,7 @@ type configHooks struct {
|
|||
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:
|
||||
|
|
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{}) {
|
||||
cmdTmpl, err := template.New("cmd").Parse(tmpl)
|
||||
if err != nil {
|
||||
|
|
13
http.go
13
http.go
|
@ -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)
|
||||
|
|
1
main.go
1
main.go
|
@ -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")
|
||||
|
|
28
micropub.go
28
micropub.go
|
@ -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:
|
||||
|
@ -228,6 +232,7 @@ type micropubAction string
|
|||
const (
|
||||
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")
|
||||
|
||||
|
|
41
posts.go
41
posts.go
|
@ -37,17 +37,24 @@ type post struct {
|
|||
type postStatus string
|
||||
|
||||
const (
|
||||
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,
|
||||
|
@ -128,6 +139,7 @@ func (a *goBlog) serveDrafts(w http.ResponseWriter, r *http.Request) {
|
|||
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: statusDraft,
|
||||
})))
|
||||
}
|
||||
|
@ -137,6 +149,7 @@ func (a *goBlog) servePrivate(w http.ResponseWriter, r *http.Request) {
|
|||
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: statusPrivate,
|
||||
})))
|
||||
}
|
||||
|
@ -146,6 +159,7 @@ func (a *goBlog) serveUnlisted(w http.ResponseWriter, r *http.Request) {
|
|||
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: statusUnlisted,
|
||||
})))
|
||||
}
|
||||
|
@ -155,10 +169,21 @@ func (a *goBlog) serveScheduled(w http.ResponseWriter, r *http.Request) {
|
|||
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: 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},
|
||||
})))
|
||||
}
|
||||
|
||||
func (a *goBlog) serveDate(w http.ResponseWriter, r *http.Request) {
|
||||
var year, month, day int
|
||||
if ys := chi.URLParam(r, "year"); ys != "" && ys != "x" {
|
||||
|
@ -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)
|
||||
|
|
93
postsDb.go
93
postsDb.go
|
@ -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
|
||||
}
|
||||
// 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{
|
||||
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)
|
||||
}
|
||||
|
|
104
posts_test.go
104
posts_test.go
|
@ -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>")
|
||||
|
||||
}
|
||||
|
|
10
telegram.go
10
telegram.go
|
@ -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"
|
||||
|
|
1
tts.go
1
tts.go
|
@ -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…
Reference in New Issue