- 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
+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(
+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 }
+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;">
+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}}