repos / pico

pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh
git clone https://github.com/picosh/pico.git

commit
a533a40
parent
ea7ac00
author
Eric Bower
date
2024-08-29 17:42:43 +0000 UTC
feat(feeds): require human interaction to continue sending emails

BREAKING CHANGE: we require users to click a link in their feed emails
once every 90 days
4 files changed,  +85, -13
M feeds/api.go
+38, -0
 1@@ -3,6 +3,8 @@ package feeds
 2 import (
 3 	"fmt"
 4 	"net/http"
 5+	"net/url"
 6+	"time"
 7 
 8 	"github.com/picosh/pico/db/postgres"
 9 	"github.com/picosh/pico/shared"
10@@ -21,9 +23,45 @@ func createStaticRoutes() []shared.Route {
11 	}
12 }
13 
14+func keepAliveHandler(w http.ResponseWriter, r *http.Request) {
15+	dbpool := shared.GetDB(r)
16+	logger := shared.GetLogger(r)
17+	postID, _ := url.PathUnescape(shared.GetField(r, 0))
18+
19+	post, err := dbpool.FindPost(postID)
20+	if err != nil {
21+		logger.Info("post not found")
22+		http.Error(w, "post not found", http.StatusNotFound)
23+		return
24+	}
25+
26+	now := time.Now()
27+	expiresAt := now.AddDate(0, 3, 0)
28+	post.ExpiresAt = &expiresAt
29+	_, err = dbpool.UpdatePost(post)
30+	if err != nil {
31+		logger.Error("could not update post", "err", err.Error())
32+		http.Error(w, "server error", 500)
33+		return
34+	}
35+
36+	w.Header().Add("Content-Type", "text/plain")
37+
38+	txt := fmt.Sprintf(
39+		"Success! This feed will stay active until %s or by clicking the link in your digest email again",
40+		time.Now(),
41+	)
42+	_, err = w.Write([]byte(txt))
43+	if err != nil {
44+		logger.Error("could not write to writer", "err", err.Error())
45+		http.Error(w, "server error", 500)
46+	}
47+}
48+
49 func createMainRoutes(staticRoutes []shared.Route) []shared.Route {
50 	routes := []shared.Route{
51 		shared.NewRoute("GET", "/", shared.CreatePageHandler("html/marketing.page.tmpl")),
52+		shared.NewRoute("GET", "/keep-alive/(.+)", keepAliveHandler),
53 	}
54 
55 	routes = append(
M feeds/cron.go
+39, -13
  1@@ -7,6 +7,7 @@ import (
  2 	html "html/template"
  3 	"io"
  4 	"log/slog"
  5+	"math"
  6 	"net/http"
  7 	"strings"
  8 	"text/template"
  9@@ -58,8 +59,10 @@ type Feed struct {
 10 }
 11 
 12 type DigestFeed struct {
 13-	Feeds   []*Feed
 14-	Options DigestOptions
 15+	Feeds        []*Feed
 16+	Options      DigestOptions
 17+	KeepAliveURL string
 18+	DaysLeft     string
 19 }
 20 
 21 type DigestOptions struct {
 22@@ -120,26 +123,36 @@ func NewFetcher(dbpool db.DB, cfg *shared.ConfigSite) *Fetcher {
 23 	}
 24 }
 25 
 26-func (f *Fetcher) Validate(lastDigest *time.Time, parsed *shared.ListParsedText) error {
 27+func (f *Fetcher) Validate(post *db.Post, parsed *shared.ListParsedText) error {
 28+	lastDigest := post.Data.LastDigest
 29 	if lastDigest == nil {
 30 		return nil
 31 	}
 32 
 33-	digestAt := digestOptionToTime(*lastDigest, parsed.DigestInterval)
 34 	now := time.Now().UTC()
 35+
 36+	expiresAt := post.ExpiresAt
 37+	if expiresAt != nil {
 38+		if post.ExpiresAt.After(now) {
 39+			return fmt.Errorf("(%s) post has expired, skipping", post.ExpiresAt.Format(time.RFC3339))
 40+		}
 41+	}
 42+
 43+	digestAt := digestOptionToTime(*lastDigest, parsed.DigestInterval)
 44 	if digestAt.After(now) {
 45-		return fmt.Errorf("(%s) not time to digest, skipping", digestAt)
 46+		return fmt.Errorf("(%s) not time to digest, skipping", digestAt.Format(time.RFC3339))
 47 	}
 48 	return nil
 49 }
 50 
 51 func (f *Fetcher) RunPost(logger *slog.Logger, user *db.User, post *db.Post) error {
 52-	logger.Info("running feed post", "filename", post.Filename)
 53+	logger = logger.With("filename", post.Filename)
 54+	logger.Info("running feed post")
 55 
 56 	parsed := shared.ListParseText(post.Text)
 57 
 58 	logger.Info("last digest at", "lastDigest", post.Data.LastDigest)
 59-	err := f.Validate(post.Data.LastDigest, parsed)
 60+	err := f.Validate(post, parsed)
 61 	if err != nil {
 62 		logger.Info("validation failed", "err", err.Error())
 63 		return nil
 64@@ -161,7 +174,7 @@ func (f *Fetcher) RunPost(logger *slog.Logger, user *db.User, post *db.Post) err
 65 		urls = append(urls, url)
 66 	}
 67 
 68-	msgBody, err := f.FetchAll(logger, urls, parsed.InlineContent, post.ID, user.Name)
 69+	msgBody, err := f.FetchAll(logger, urls, parsed.InlineContent, user.Name, post)
 70 	if err != nil {
 71 		return err
 72 	}
 73@@ -173,6 +186,10 @@ func (f *Fetcher) RunPost(logger *slog.Logger, user *db.User, post *db.Post) err
 74 	}
 75 
 76 	now := time.Now().UTC()
 77+	if post.ExpiresAt == nil {
 78+		expiresAt := time.Now().AddDate(0, 3, 0)
 79+		post.ExpiresAt = &expiresAt
 80+	}
 81 	post.Data.LastDigest = &now
 82 	_, err = f.db.UpdatePost(post)
 83 	return err
 84@@ -313,10 +330,19 @@ type MsgBody struct {
 85 	Text string
 86 }
 87 
 88-func (f *Fetcher) FetchAll(logger *slog.Logger, urls []string, inlineContent bool, postID string, username string) (*MsgBody, error) {
 89+func (f *Fetcher) FetchAll(logger *slog.Logger, urls []string, inlineContent bool, username string, post *db.Post) (*MsgBody, error) {
 90 	fp := gofeed.NewParser()
 91-	feeds := &DigestFeed{Options: DigestOptions{InlineContent: inlineContent}}
 92-	feedItems, err := f.db.FindFeedItemsByPostID(postID)
 93+	daysLeft := "90"
 94+	if post.ExpiresAt != nil {
 95+		diff := time.Since(*post.ExpiresAt)
 96+		daysLeft = fmt.Sprintf("%f", math.Ceil(diff.Hours()/24))
 97+	}
 98+	feeds := &DigestFeed{
 99+		KeepAliveURL: fmt.Sprintf("https://feeds.pico.sh/keep-alive/%s", post.ID),
100+		DaysLeft:     daysLeft,
101+		Options:      DigestOptions{InlineContent: inlineContent},
102+	}
103+	feedItems, err := f.db.FindFeedItemsByPostID(post.ID)
104 	if err != nil {
105 		return nil, err
106 	}
107@@ -342,7 +368,7 @@ func (f *Fetcher) FetchAll(logger *slog.Logger, urls []string, inlineContent boo
108 	for _, feed := range feeds.Feeds {
109 		for _, item := range feed.FeedItems {
110 			fdi = append(fdi, &db.FeedItem{
111-				PostID: postID,
112+				PostID: post.ID,
113 				GUID:   item.GUID,
114 				Data: db.FeedItemData{
115 					Title:       item.Title,
116@@ -354,7 +380,7 @@ func (f *Fetcher) FetchAll(logger *slog.Logger, urls []string, inlineContent boo
117 			})
118 		}
119 	}
120-	err = f.db.InsertFeedItems(postID, fdi)
121+	err = f.db.InsertFeedItems(post.ID, fdi)
122 	if err != nil {
123 		return nil, err
124 	}
M feeds/html/digest.page.tmpl
+5, -0
 1@@ -9,6 +9,11 @@ img {
 2 }
 3 </style>
 4 
 5+<blockquote>
 6+  In order to keep this digest email active you must <a href="{{.KeepAliveURL}}">click this link</a>.
 7+  You have {{.DaysLeft}} days left.
 8+</blockquote>
 9+
10 <div class="feeds">
11 {{range .Feeds}}
12 <div style="margin-bottom: 10px;">
M feeds/html/digest_text.page.tmpl
+3, -0
1@@ -1,3 +1,6 @@
2+> In order to keep this digest email active you must click the link below. You have {{.DaysLeft}} days left.
3+> {{.KeepAliveURL}}
4+
5 {{range .Feeds}}
6     {{.Title}}
7     {{.Link}}