diff --git a/docs/index.md b/docs/index.md index fdf0100..f63aabb 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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) \ No newline at end of file diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..2464074 --- /dev/null +++ b/docs/usage.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. \ No newline at end of file diff --git a/editor.go b/editor.go index 7fa91b3..555d303 100644 --- a/editor.go +++ b/editor.go @@ -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(", ") diff --git a/http.go b/http.go index 5c294b1..0047d6d 100644 --- a/http.go +++ b/http.go @@ -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 } diff --git a/main.go b/main.go index e22308f..ccefbde 100644 --- a/main.go +++ b/main.go @@ -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") diff --git a/posts.go b/posts.go index cd9f3f2..2381e09 100644 --- a/posts.go +++ b/posts.go @@ -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) { diff --git a/postsDb.go b/postsDb.go index 1d768f3..f19cfb5 100644 --- a/postsDb.go +++ b/postsDb.go @@ -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 { diff --git a/postsFuncs.go b/postsFuncs.go index 4f1427a..37b15fc 100644 --- a/postsFuncs.go +++ b/postsFuncs.go @@ -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: diff --git a/postsScheduler.go b/postsScheduler.go new file mode 100644 index 0000000..d2e4ff5 --- /dev/null +++ b/postsScheduler.go @@ -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) + } +} diff --git a/postsScheduler_test.go b/postsScheduler_test.go new file mode 100644 index 0000000..d11db43 --- /dev/null +++ b/postsScheduler_test.go @@ -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) + +} diff --git a/telegram.go b/telegram.go index 419cc31..af59af2 100644 --- a/telegram.go +++ b/telegram.go @@ -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 == "" { diff --git a/templates/postmeta.gohtml b/templates/postmeta.gohtml index ecb9318..8817205 100644 --- a/templates/postmeta.gohtml +++ b/templates/postmeta.gohtml @@ -7,5 +7,6 @@ {{ end }} {{ $short := shorturl .Data }} {{ if $short }}