Support for scheduled posts

Closes #9
pull/10/head
Jan-Lukas Else 8 months ago
parent 63cb57ffaa
commit 9ea6104863
  1. 1
      docs/index.md
  2. 9
      docs/usage.md
  3. 4
      editor.go
  4. 2
      http.go
  5. 1
      main.go
  6. 1
      posts.go
  7. 5
      postsDb.go
  8. 2
      postsFuncs.go
  9. 46
      postsScheduler.go
  10. 58
      postsScheduler_test.go
  11. 6
      telegram.go
  12. 1
      templates/postmeta.gohtml
  13. 1
      templates/strings/de.yaml
  14. 1
      templates/strings/default.yaml
  15. 1
      templates/strings/pt-br.yaml

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

@ -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.

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

@ -265,7 +265,7 @@ func (a *goBlog) servePostsAliasesRedirects() http.HandlerFunc {
case statusPublished, statusUnlisted:
alicePrivate.Append(a.checkActivityStreamsRequest, a.cacheMiddleware).ThenFunc(a.servePost).ServeHTTP(w, r)
return
case statusDraft, statusPrivate:
default: // private, draft, scheduled
alice.New(a.authMiddleware).ThenFunc(a.servePost).ServeHTTP(w, r)
return
}

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

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

@ -276,6 +276,7 @@ type postsRequestConfig struct {
parameter string // Ignores parameters
parameterValue string
publishedYear, publishedMonth, publishedDay int
publishedBefore time.Time
randomOrder bool
priorityOrder 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")
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
queryBuilder.WriteString(" order by ")
if c.randomOrder {

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

@ -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)
}
}

@ -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)
}

@ -13,6 +13,12 @@ import (
func (a *goBlog) initTelegram() {
a.pPostHooks = append(a.pPostHooks, func(p *post) {
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
html := tg.generateHTML(p.RenderedTitle, a.fullPostURL(p), a.shortPostURL(p))
if html == "" {

@ -7,5 +7,6 @@
{{ end }}
{{ $short := shorturl .Data }}
{{ 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>
{{ end }}

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

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

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

Loading…
Cancel
Save