- commit
- d32eeb3
- parent
- 92e3a32
- author
- Eric Bower
- date
- 2025-08-08 21:43:58 -0400 EDT
refactor(feeds): deprecate `digest_interval`; use `cron` instead Reference: https://pico.sh/feeds#cron
5 files changed,
+64,
-83
M
Makefile
+10,
-10
1@@ -6,7 +6,7 @@ DB_CONTAINER?=pico-postgres-1
2 DOCKER_TAG?=$(shell git log --format="%H" -n 1)
3 DOCKER_PLATFORM?=linux/amd64,linux/arm64
4 DOCKER_CMD?=docker
5-DOCKER_BUILDX_BUILD?=$(DOCKER_CMD) buildx build --push --platform $(DOCKER_PLATFORM)
6+DOCKER_BUILDX_BUILD?=$(DOCKER_CMD) buildx build --push --platform $(DOCKER_PLATFORM) -t
7 WRITE?=0
8
9 smol:
10@@ -42,36 +42,36 @@ endif
11 .PHONY: bp-setup
12
13 bp-caddy: bp-setup
14- $(DOCKER_BUILDX_BUILD) -t ghcr.io/picosh/pico/caddy:$(DOCKER_TAG) ./caddy
15+ $(DOCKER_BUILDX_BUILD) ghcr.io/picosh/pico/caddy:$(DOCKER_TAG) ./caddy
16 .PHONY: bp-caddy
17
18 bp-auth: bp-setup
19- $(DOCKER_BUILDX_BUILD) -t ghcr.io/picosh/pico/auth-web:$(DOCKER_TAG) --build-arg APP=auth --target release-web .
20+ $(DOCKER_BUILDX_BUILD) ghcr.io/picosh/pico/auth-web:$(DOCKER_TAG) --build-arg APP=auth --target release-web .
21 .PHONY: bp-auth
22
23 bp-pgs-cdn: bp-setup
24- $(DOCKER_BUILDX_BUILD) -t ghcr.io/picosh/pico/pgs-cdn:$(DOCKER_TAG) --target release-web -f Dockerfile.cdn .
25+ $(DOCKER_BUILDX_BUILD) ghcr.io/picosh/pico/pgs-cdn:$(DOCKER_TAG) --target release-web -f Dockerfile.cdn .
26 .PHONY: bp-pgs-cdn
27
28 bp-pgs-standalone: bp-setup
29- $(DOCKER_BUILDX_BUILD) --manifest ghcr.io/picosh/pgs:$(DOCKER_TAG) --target release -f Dockerfile.standalone .
30+ $(DOCKER_BUILDX_BUILD) ghcr.io/picosh/pgs:$(DOCKER_TAG) --target release -f Dockerfile.standalone .
31 .PHONY: bp-pgs-standalone
32
33 bp-pico: bp-setup
34- $(DOCKER_BUILDX_BUILD) -t ghcr.io/picosh/pico/pico-ssh:$(DOCKER_TAG) --build-arg APP=pico --target release-ssh .
35+ $(DOCKER_BUILDX_BUILD) ghcr.io/picosh/pico/pico-ssh:$(DOCKER_TAG) --build-arg APP=pico --target release-ssh .
36 .PHONY: bp-auth
37
38 bp-bouncer: bp-setup
39- $(DOCKER_BUILDX_BUILD) -t ghcr.io/picosh/pico/bouncer:$(DOCKER_TAG) ./bouncer
40+ $(DOCKER_BUILDX_BUILD) ghcr.io/picosh/pico/bouncer:$(DOCKER_TAG) ./bouncer
41 .PHONY: bp-bouncer
42
43 bp-ssh-%: bp-setup
44- $(DOCKER_BUILDX_BUILD) --build-arg "APP=$*" -t "ghcr.io/picosh/pico/$*-ssh:$(DOCKER_TAG)" --target release-ssh .
45+ $(DOCKER_BUILDX_BUILD) "ghcr.io/picosh/pico/$*-ssh:$(DOCKER_TAG)" --build-arg "APP=$*" --target release-ssh .
46 .PHONY: pgs-ssh
47
48 bp-%: bp-setup
49- $(DOCKER_BUILDX_BUILD) --build-arg "APP=$*" -t "ghcr.io/picosh/pico/$*-ssh:$(DOCKER_TAG)" --target release-ssh .
50- $(DOCKER_BUILDX_BUILD) --build-arg "APP=$*" -t "ghcr.io/picosh/pico/$*-web:$(DOCKER_TAG)" --target release-web .
51+ $(DOCKER_BUILDX_BUILD) "ghcr.io/picosh/pico/$*-ssh:$(DOCKER_TAG)" --build-arg "APP=$*" --target release-ssh .
52+ $(DOCKER_BUILDX_BUILD) "ghcr.io/picosh/pico/$*-web:$(DOCKER_TAG)" --build-arg "APP=$*" --target release-web .
53 .PHONY: bp-%
54
55 bp-all: bp-prose bp-pastes bp-feeds bp-pgs bp-auth bp-bouncer bp-pipe bp-pgs-cdn
+8,
-7
1@@ -5,6 +5,7 @@ import (
2 "text/tabwriter"
3 "time"
4
5+ "github.com/adhocore/gronx"
6 "github.com/picosh/pico/pkg/db"
7 "github.com/picosh/pico/pkg/pssh"
8 "github.com/picosh/pico/pkg/shared"
9@@ -67,24 +68,24 @@ func Middleware(dbpool db.DB, cfg *shared.ConfigSite) pssh.SSHServerMiddleware {
10 }
11
12 writer := tabwriter.NewWriter(sesh, 0, 0, 1, ' ', tabwriter.TabIndent)
13- _, _ = fmt.Fprintln(writer, "Filename\tLast Digest\tNext Digest\tInterval\tFailed Attempts")
14+ _, _ = fmt.Fprintln(writer, "Filename\tLast Digest\tNext Digest\tCron\tFailed Attempts")
15 for _, post := range posts.Data {
16 parsed := shared.ListParseText(post.Text)
17
18 nextDigest := ""
19- if parsed.Cron != "" {
20- nextDigest = parsed.Cron
21- } else {
22- digestOption := DigestOptionToTime(*post.Data.LastDigest, parsed.DigestInterval)
23- nextDigest = digestOption.Format(time.RFC3339)
24+ cron := parsed.Cron
25+ if parsed.DigestInterval != "" {
26+ cron = DigestIntervalToCron(parsed.DigestInterval)
27 }
28+ nd, _ := gronx.NextTickAfter(cron, DateToMin(time.Now()), true)
29+ nextDigest = nd.Format(time.RFC3339)
30 _, _ = fmt.Fprintf(
31 writer,
32 "%s\t%s\t%s\t%s\t%d/10\r\n",
33 post.Filename,
34 post.Data.LastDigest.Format(time.RFC3339),
35 nextDigest,
36- parsed.DigestInterval,
37+ cron,
38 post.Data.Attempts,
39 )
40 }
+35,
-35
1@@ -84,25 +84,24 @@ func itemToTemplate(item *gofeed.Item) *FeedItemTmpl {
2 }
3 }
4
5-func DigestOptionToTime(lastDigest time.Time, interval string) time.Time {
6- day := 24 * time.Hour
7+func DigestIntervalToCron(interval string) string {
8 switch interval {
9 case "10min":
10- return lastDigest.Add(10 * time.Minute)
11+ return "*/10 * * * *"
12 case "1hour":
13- return lastDigest.Add(1 * time.Hour)
14+ return "0 * * * *"
15 case "6hour":
16- return lastDigest.Add(6 * time.Hour)
17+ return "0 */6 * * *"
18 case "12hour":
19- return lastDigest.Add(12 * time.Hour)
20+ return "0 */12 * * *"
21 case "1day", "":
22- return lastDigest.Add(1 * day)
23+ return "0 13 * * *"
24 case "7day":
25- return lastDigest.Add(7 * day)
26+ return "0 13 * * 0"
27 case "30day":
28- return lastDigest.Add(30 * day)
29+ return "0 13 1 * *"
30 default:
31- return lastDigest
32+ return "0 13 * * *"
33 }
34 }
35
36@@ -146,19 +145,16 @@ func NewFetcher(dbpool db.DB, cfg *shared.ConfigSite) *Fetcher {
37 }
38 }
39
40-func (f *Fetcher) Validate(post *db.Post, parsed *shared.ListParsedText, now time.Time) error {
41- lastDigest := post.Data.LastDigest
42- if lastDigest == nil {
43- return nil
44- }
45-
46- toTheMin := time.Date(
47+func DateToMin(now time.Time) time.Time {
48+ return time.Date(
49 now.Year(), now.Month(), now.Day(),
50 now.Hour(), now.Minute(),
51 0, 0, // zero out second and nano-second for cron
52 now.Location(),
53 )
54+}
55
56+func (f *Fetcher) Validate(post *db.Post, parsed *shared.ListParsedText, now time.Time) error {
57 expiresAt := post.ExpiresAt
58 if expiresAt != nil {
59 if post.ExpiresAt.Before(now) {
60@@ -166,24 +162,28 @@ func (f *Fetcher) Validate(post *db.Post, parsed *shared.ListParsedText, now tim
61 }
62 }
63
64- if parsed.Cron != "" {
65- isDue, err := f.gron.IsDue(parsed.Cron, toTheMin)
66- if err != nil {
67- return fmt.Errorf("cron error, skipping; err: %w", err)
68- }
69- if !isDue {
70- nextTime, _ := gronx.NextTick(parsed.Cron, true)
71- return fmt.Errorf(
72- "cron not time to digest, skipping; cur run: %s, next run: %s",
73- f.gron.C.GetRef(),
74- nextTime,
75- )
76- }
77- } else if parsed.DigestInterval != "" {
78- digestAt := DigestOptionToTime(*lastDigest, parsed.DigestInterval)
79- if digestAt.After(now) {
80- return fmt.Errorf("(%s) not time to digest, skipping", digestAt.Format(time.RFC3339))
81- }
82+ cron := parsed.Cron
83+ // support for posts with deprecated `digest_interval` property
84+ if parsed.DigestInterval != "" {
85+ cron = DigestIntervalToCron(parsed.DigestInterval)
86+ }
87+
88+ if !f.gron.IsValid(cron) {
89+ return fmt.Errorf("(%s) is invalid `cron`, skipping", cron)
90+ }
91+
92+ dt := DateToMin(now)
93+ isDue, err := f.gron.IsDue(cron, dt)
94+ if err != nil {
95+ return fmt.Errorf("cron error, skipping; err: %w", err)
96+ }
97+ if !isDue {
98+ nextTime, _ := gronx.NextTickAfter(cron, dt, true)
99+ return fmt.Errorf(
100+ "cron not time to digest, skipping; cur run: %s, next run: %s",
101+ f.gron.C.GetRef(),
102+ nextTime,
103+ )
104 }
105 return nil
106 }
+11,
-9
1@@ -4,10 +4,9 @@ import (
2 "errors"
3 "fmt"
4 "net/url"
5-
6 "strings"
7- "time"
8
9+ "github.com/adhocore/gronx"
10 "github.com/picosh/pico/pkg/db"
11 "github.com/picosh/pico/pkg/filehandlers"
12 "github.com/picosh/pico/pkg/pssh"
13@@ -51,6 +50,16 @@ func (p *FeedHooks) FileValidate(s *pssh.SSHServerConnSession, data *filehandler
14 return false, fmt.Errorf("ERROR: no email variable detected for %s, check the format of your file, skipping", data.Filename)
15 }
16
17+ if parsed.DigestInterval != "" {
18+ return false, fmt.Errorf("ERROR: `digest_interval` is deprecated; use `cron`: https://pico.sh/feeds#cron")
19+ }
20+
21+ if parsed.Cron != "" {
22+ if !gronx.IsValid(parsed.Cron) {
23+ return false, fmt.Errorf("ERROR: `cron` is invalid, reference: https://github.com/adhocore/gronx?tab=readme-ov-file#cron-expression")
24+ }
25+ }
26+
27 var allErr error
28 for _, txt := range parsed.Items {
29 u := ""
30@@ -74,12 +83,5 @@ func (p *FeedHooks) FileValidate(s *pssh.SSHServerConnSession, data *filehandler
31 }
32
33 func (p *FeedHooks) FileMeta(s *pssh.SSHServerConnSession, data *filehandlers.PostMetaData) error {
34- if data.Data.LastDigest == nil {
35- now := time.Now()
36- // let it run on the next loop
37- dd := now.AddDate(0, 0, -31)
38- data.Data.LastDigest = &dd
39- }
40-
41 return nil
42 }
1@@ -9,22 +9,10 @@ import (
2 "strings"
3 "time"
4
5- "slices"
6-
7- "github.com/adhocore/gronx"
8 "github.com/araddon/dateparse"
9 )
10
11 var reIndent = regexp.MustCompile(`^[[:blank:]]+`)
12-var DigestIntervalOpts = []string{
13- "10min",
14- "1hour",
15- "6hour",
16- "12hour",
17- "1day",
18- "7day",
19- "30day",
20-}
21
22 type ListParsedText struct {
23 Items []*ListItem
24@@ -124,18 +112,8 @@ func TokenToMetaField(meta *ListMetaData, token *SplitToken) error {
25 case "layout":
26 meta.Layout = token.Value
27 case "digest_interval":
28- if !slices.Contains(DigestIntervalOpts, token.Value) {
29- return fmt.Errorf(
30- "(%s) is not a valid option, choose from [%s]",
31- token.Value,
32- strings.Join(DigestIntervalOpts, ","),
33- )
34- }
35 meta.DigestInterval = token.Value
36 case "cron":
37- if !gronx.IsValid(token.Value) {
38- return fmt.Errorf("(%s) is not in a valid cron format: https://github.com/adhocore/gronx?tab=readme-ov-file#cron-expression", token.Value)
39- }
40 meta.Cron = token.Value
41 case "email":
42 meta.Email = token.Value