Support for scheduled posts

Closes #9
This commit is contained in:
Jan-Lukas Else 2021-12-11 19:43:40 +01:00
parent 63cb57ffaa
commit 9ea6104863
15 changed files with 136 additions and 3 deletions

View File

@ -55,5 +55,6 @@ Here's an (incomplete) list of features:
- [How to install and run GoBlog](./install.md) - [How to install and run GoBlog](./install.md)
- [How to build GoBlog](./build.md) - [How to build GoBlog](./build.md)
- [How to use GoBlog](./usage.md)
- [Administration paths](./admin-paths.md) - [Administration paths](./admin-paths.md)
- [GoBlog's storage system](./storage.md) - [GoBlog's storage system](./storage.md)

9
docs/usage.md Normal file
View File

@ -0,0 +1,9 @@
# How to use GoBlog
This section of the documentation is a work in progress!
## Posting
### Scheduling posts
To schedule a post, create a post with `status: scheduled` and set the `published` field to the desired date. A scheduler runs in the background and checks every 10 seconds if a scheduled post should be published. If there's a post to publish, the post status is changed to `published`. That will also trigger configured hooks. Scheduled posts are only visible when logged in.

View File

@ -227,6 +227,8 @@ func (a *goBlog) editorPostDesc(blog string) string {
t := a.ts.GetTemplateStringVariant(bc.Lang, "editorpostdesc") t := a.ts.GetTemplateStringVariant(bc.Lang, "editorpostdesc")
var paramBuilder, statusBuilder strings.Builder var paramBuilder, statusBuilder strings.Builder
for i, param := range []string{ for i, param := range []string{
"published",
"updated",
"summary", "summary",
"translationkey", "translationkey",
"original", "original",
@ -252,7 +254,7 @@ func (a *goBlog) editorPostDesc(blog string) string {
paramBuilder.WriteByte('`') paramBuilder.WriteByte('`')
} }
for i, status := range []postStatus{ for i, status := range []postStatus{
statusDraft, statusPublished, statusUnlisted, statusPrivate, statusDraft, statusPublished, statusUnlisted, statusScheduled, statusPrivate,
} { } {
if i > 0 { if i > 0 {
statusBuilder.WriteString(", ") statusBuilder.WriteString(", ")

View File

@ -265,7 +265,7 @@ 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
case statusDraft, statusPrivate: default: // private, draft, scheduled
alice.New(a.authMiddleware).ThenFunc(a.servePost).ServeHTTP(w, r) alice.New(a.authMiddleware).ThenFunc(a.servePost).ServeHTTP(w, r)
return return
} }

View File

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

View File

@ -42,6 +42,7 @@ const (
statusDraft postStatus = "draft" statusDraft postStatus = "draft"
statusPrivate postStatus = "private" statusPrivate postStatus = "private"
statusUnlisted postStatus = "unlisted" statusUnlisted postStatus = "unlisted"
statusScheduled postStatus = "scheduled"
) )
func (a *goBlog) servePost(w http.ResponseWriter, r *http.Request) { func (a *goBlog) servePost(w http.ResponseWriter, r *http.Request) {

View File

@ -276,6 +276,7 @@ type postsRequestConfig struct {
parameter string // Ignores parameters parameter string // Ignores parameters
parameterValue string parameterValue string
publishedYear, publishedMonth, publishedDay int publishedYear, publishedMonth, publishedDay int
publishedBefore time.Time
randomOrder bool randomOrder bool
priorityOrder bool priorityOrder bool
withoutParameters bool withoutParameters bool
@ -360,6 +361,10 @@ func buildPostsQuery(c *postsRequestConfig, selection string) (query string, arg
queryBuilder.WriteString(" and substr(tolocal(published), 9, 2) = @publishedday") queryBuilder.WriteString(" and substr(tolocal(published), 9, 2) = @publishedday")
args = append(args, sql.Named("publishedday", fmt.Sprintf("%02d", c.publishedDay))) args = append(args, sql.Named("publishedday", fmt.Sprintf("%02d", c.publishedDay)))
} }
if !c.publishedBefore.IsZero() {
queryBuilder.WriteString(" and toutc(published) < @publishedbefore")
args = append(args, sql.Named("publishedbefore", c.publishedBefore.UTC().Format(time.RFC3339)))
}
// Order // Order
queryBuilder.WriteString(" order by ") queryBuilder.WriteString(" order by ")
if c.randomOrder { if c.randomOrder {

View File

@ -137,7 +137,7 @@ func (a *goBlog) postToMfItem(p *post) *microformatItem {
switch p.Status { switch p.Status {
case statusDraft: case statusDraft:
mfStatus = "draft" mfStatus = "draft"
case statusPublished: case statusPublished, statusScheduled:
mfStatus = "published" mfStatus = "published"
mfVisibility = "public" mfVisibility = "public"
case statusUnlisted: case statusUnlisted:

46
postsScheduler.go Normal file
View File

@ -0,0 +1,46 @@
package main
import (
"log"
"time"
)
func (a *goBlog) startPostsScheduler() {
ticker := time.NewTicker(10 * time.Second)
done := make(chan bool)
go func() {
for {
select {
case <-done:
return
case <-ticker.C:
a.checkScheduledPosts()
}
}
}()
a.shutdown.Add(func() {
ticker.Stop()
done <- true
log.Println("Posts scheduler stopped")
})
}
func (a *goBlog) checkScheduledPosts() {
postsToPublish, err := a.getPosts(&postsRequestConfig{
status: "scheduled",
publishedBefore: time.Now(),
})
if err != nil {
log.Println("Error getting scheduled posts:", err)
return
}
for _, post := range postsToPublish {
post.Status = "published"
err := a.replacePost(post, post.Path, statusScheduled)
if err != nil {
log.Println("Error publishing scheduled post:", err)
continue
}
log.Println("Published scheduled post:", post.Path)
}
}

58
postsScheduler_test.go Normal file
View File

@ -0,0 +1,58 @@
package main
import (
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_postsScheduler(t *testing.T) {
app := &goBlog{
cfg: &config{
Db: &configDb{
File: filepath.Join(t.TempDir(), "test.db"),
},
Server: &configServer{
PublicAddress: "https://example.com",
},
DefaultBlog: "en",
Blogs: map[string]*configBlog{
"en": {
Sections: map[string]*configSection{
"test": {},
},
Lang: "en",
},
},
Micropub: &configMicropub{},
},
}
_ = app.initDatabase(false)
app.initComponents(false)
err := app.db.savePost(&post{
Path: "/test/abc",
Content: "ABC",
Published: toLocalSafe(time.Now().Add(-1 * time.Hour).String()),
Blog: "en",
Section: "test",
Status: statusScheduled,
}, &postCreationOptions{new: true})
require.NoError(t, err)
count, err := app.db.countPosts(&postsRequestConfig{status: statusPublished})
require.NoError(t, err)
assert.Equal(t, 0, count)
app.checkScheduledPosts()
count, err = app.db.countPosts(&postsRequestConfig{status: statusPublished})
require.NoError(t, err)
assert.Equal(t, 1, count)
}

View File

@ -13,6 +13,12 @@ import (
func (a *goBlog) initTelegram() { func (a *goBlog) initTelegram() {
a.pPostHooks = append(a.pPostHooks, func(p *post) { a.pPostHooks = append(a.pPostHooks, func(p *post) {
if tg := a.cfg.Blogs[p.Blog].Telegram; tg.enabled() && p.isPublishedSectionPost() { if tg := a.cfg.Blogs[p.Blog].Telegram; tg.enabled() && p.isPublishedSectionPost() {
tgChat := p.firstParameter("telegramchat")
tgMsg := p.firstParameter("telegrammsg")
if tgChat != "" && tgMsg != "" {
// Already posted
return
}
// Generate HTML // Generate HTML
html := tg.generateHTML(p.RenderedTitle, a.fullPostURL(p), a.shortPostURL(p)) html := tg.generateHTML(p.RenderedTitle, a.fullPostURL(p), a.shortPostURL(p))
if html == "" { if html == "" {

View File

@ -7,5 +7,6 @@
{{ end }} {{ end }}
{{ $short := shorturl .Data }} {{ $short := shorturl .Data }}
{{ if $short }}<div>{{ string .Blog.Lang "shorturl" }} <a href="{{ $short }}" rel="shortlink">{{ $short }}</a></div>{{ end }} {{ if $short }}<div>{{ string .Blog.Lang "shorturl" }} <a href="{{ $short }}" rel="shortlink">{{ $short }}</a></div>{{ end }}
{{ if ne .Data.Status "published" }}<div>{{ string .Blog.Lang "status" }}: {{ .Data.Status }}</div>{{ end }}
</div> </div>
{{ end }} {{ end }}

View File

@ -48,6 +48,7 @@ send: "Senden (zur Überprüfung)"
share: "Online teilen" share: "Online teilen"
shorturl: "Kurz-Link:" shorturl: "Kurz-Link:"
speak: "Vorlesen" speak: "Vorlesen"
status: "Status"
stopspeak: "Vorlesen stoppen" stopspeak: "Vorlesen stoppen"
submit: "Abschicken" submit: "Abschicken"
total: "Gesamt" total: "Gesamt"

View File

@ -60,6 +60,7 @@ send: "Send (to review)"
share: "Share online" share: "Share online"
shorturl: "Short link:" shorturl: "Short link:"
speak: "Read aloud" speak: "Read aloud"
status: "Status"
stopspeak: "Stop reading aloud" stopspeak: "Stop reading aloud"
submit: "Submit" submit: "Submit"
total: "Total" total: "Total"

View File

@ -60,6 +60,7 @@ send: "Enviar (para revisão)"
share: "Compartilhar online" share: "Compartilhar online"
shorturl: "Link curto:" shorturl: "Link curto:"
speak: "Leia" speak: "Leia"
status: "Status"
stopspeak: "Pare de ler" stopspeak: "Pare de ler"
submit: "Enviar" submit: "Enviar"
total: "Total" total: "Total"