repos / pico

pico services mono repo
git clone https://github.com/picosh/pico.git

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
M pkg/apps/feeds/cli.go
+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 				}
M pkg/apps/feeds/cron.go
+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 }
M pkg/apps/feeds/scp_hooks.go
+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 }
M pkg/shared/listparser.go
+0, -22
 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