- commit
- bd6d79c
- parent
- 92b70e7
- author
- Eric Bower
- date
- 2025-02-06 11:32:28 -0500 EST
feat(feeds): unsubscribe link in email digests fix(feeds): show inline content by default
8 files changed,
+100,
-35
+1,
-0
1@@ -541,6 +541,7 @@ func AddPlusFeedForUser(dbpool db.DB, userID, email string) error {
2 href := fmt.Sprintf("https://auth.pico.sh/rss/%s", token)
3 text := fmt.Sprintf(`=: email %s
4 =: digest_interval 1day
5+=: inline_content true
6 => %s
7 => https://blog.pico.sh/rss`, email, href)
8 now := time.Now()
+3,
-1
1@@ -63,7 +63,9 @@ func TestPaymentWebhook(t *testing.T) {
2 }
3 expectedText := `=: email auth@pico.test
4 =: digest_interval 1day
5-=> https://auth.pico.sh/rss/123`
6+=: inline_content true
7+=> https://auth.pico.sh/rss/123
8+=> https://blog.pico.sh/rss`
9 if post.Text != expectedText {
10 t.Errorf("Want pico plus feed file %s, got %s", expectedText, post.Text)
11 }
+65,
-14
1@@ -10,18 +10,6 @@ import (
2 "github.com/picosh/pico/shared"
3 )
4
5-func createStaticRoutes() []shared.Route {
6- return []shared.Route{
7- shared.NewRoute("GET", "/main.css", shared.ServeFile("main.css", "text/css")),
8- shared.NewRoute("GET", "/card.png", shared.ServeFile("card.png", "image/png")),
9- shared.NewRoute("GET", "/favicon-16x16.png", shared.ServeFile("favicon-16x16.png", "image/png")),
10- shared.NewRoute("GET", "/favicon-32x32.png", shared.ServeFile("favicon-32x32.png", "image/png")),
11- shared.NewRoute("GET", "/apple-touch-icon.png", shared.ServeFile("apple-touch-icon.png", "image/png")),
12- shared.NewRoute("GET", "/favicon.ico", shared.ServeFile("favicon.ico", "image/x-icon")),
13- shared.NewRoute("GET", "/robots.txt", shared.ServeFile("robots.txt", "text/plain")),
14- }
15-}
16-
17 func keepAliveHandler(w http.ResponseWriter, r *http.Request) {
18 dbpool := shared.GetDB(r)
19 logger := shared.GetLogger(r)
20@@ -29,11 +17,20 @@ func keepAliveHandler(w http.ResponseWriter, r *http.Request) {
21
22 post, err := dbpool.FindPost(postID)
23 if err != nil {
24- logger.Info("post not found")
25+ logger.Error("post not found", "err", err)
26 http.Error(w, "post not found", http.StatusNotFound)
27 return
28 }
29
30+ user, err := dbpool.FindUser(post.UserID)
31+ if err != nil {
32+ logger.Error("user not found", "err", err)
33+ http.Error(w, "user not found", http.StatusNotFound)
34+ return
35+ }
36+ logger = shared.LoggerWithUser(logger, user)
37+ logger = logger.With("post", post.ID, "filename", post.Filename)
38+
39 now := time.Now()
40 expiresAt := now.AddDate(0, 3, 0)
41 post.ExpiresAt = &expiresAt
42@@ -46,9 +43,13 @@ func keepAliveHandler(w http.ResponseWriter, r *http.Request) {
43
44 w.Header().Add("Content-Type", "text/plain")
45
46+ logger.Info(
47+ "Success! This feed will stay active until %s or by clicking the link in your digest email again",
48+ "expiresAt", now,
49+ )
50 txt := fmt.Sprintf(
51 "Success! This feed will stay active until %s or by clicking the link in your digest email again",
52- time.Now(),
53+ now,
54 )
55 _, err = w.Write([]byte(txt))
56 if err != nil {
57@@ -57,10 +58,48 @@ func keepAliveHandler(w http.ResponseWriter, r *http.Request) {
58 }
59 }
60
61+func unsubHandler(w http.ResponseWriter, r *http.Request) {
62+ dbpool := shared.GetDB(r)
63+ logger := shared.GetLogger(r)
64+ postID, _ := url.PathUnescape(shared.GetField(r, 0))
65+
66+ post, err := dbpool.FindPost(postID)
67+ if err != nil {
68+ logger.Error("post not found", "err", err)
69+ http.Error(w, "post not found", http.StatusNotFound)
70+ return
71+ }
72+
73+ user, err := dbpool.FindUser(post.UserID)
74+ if err != nil {
75+ logger.Error("user not found", "err", err)
76+ http.Error(w, "user not found", http.StatusNotFound)
77+ return
78+ }
79+ logger = shared.LoggerWithUser(logger, user)
80+ logger = logger.With("post", post.ID, "filename", post.Filename)
81+
82+ logger.Info("unsubscribe")
83+ err = dbpool.RemovePosts([]string{post.ID})
84+ if err != nil {
85+ logger.Error("could not remove post", "err", err)
86+ http.Error(w, "could not remove post", http.StatusInternalServerError)
87+ return
88+ }
89+
90+ txt := "Success! This feed digest post has been removed from our system."
91+ _, err = w.Write([]byte(txt))
92+ if err != nil {
93+ logger.Error("could not write to writer", "err", err)
94+ http.Error(w, "server error", 500)
95+ }
96+}
97+
98 func createMainRoutes(staticRoutes []shared.Route) []shared.Route {
99 routes := []shared.Route{
100 shared.NewRoute("GET", "/", shared.CreatePageHandler("html/marketing.page.tmpl")),
101 shared.NewRoute("GET", "/keep-alive/(.+)", keepAliveHandler),
102+ shared.NewRoute("GET", "/unsub/(.+)", unsubHandler),
103 }
104
105 routes = append(
106@@ -71,6 +110,18 @@ func createMainRoutes(staticRoutes []shared.Route) []shared.Route {
107 return routes
108 }
109
110+func createStaticRoutes() []shared.Route {
111+ return []shared.Route{
112+ shared.NewRoute("GET", "/main.css", shared.ServeFile("main.css", "text/css")),
113+ shared.NewRoute("GET", "/card.png", shared.ServeFile("card.png", "image/png")),
114+ shared.NewRoute("GET", "/favicon-16x16.png", shared.ServeFile("favicon-16x16.png", "image/png")),
115+ shared.NewRoute("GET", "/favicon-32x32.png", shared.ServeFile("favicon-32x32.png", "image/png")),
116+ shared.NewRoute("GET", "/apple-touch-icon.png", shared.ServeFile("apple-touch-icon.png", "image/png")),
117+ shared.NewRoute("GET", "/favicon.ico", shared.ServeFile("favicon.ico", "image/x-icon")),
118+ shared.NewRoute("GET", "/robots.txt", shared.ServeFile("robots.txt", "text/plain")),
119+ }
120+}
121+
122 func StartApiServer() {
123 cfg := NewConfigSite()
124 db := postgres.NewDB(cfg.DbURL, cfg.Logger)
+4,
-0
1@@ -63,6 +63,7 @@ type DigestFeed struct {
2 Feeds []*Feed
3 Options DigestOptions
4 KeepAliveURL string
5+ UnsubURL string
6 DaysLeft string
7 ShowBanner bool
8 }
9@@ -200,6 +201,7 @@ func (f *Fetcher) RunPost(logger *slog.Logger, user *db.User, post *db.Post, ski
10 continue
11 }
12
13+ logger.Info("found rss feed url", "url", u)
14 urls = append(urls, u)
15 }
16
17@@ -418,6 +420,7 @@ type MsgBody struct {
18 }
19
20 func (f *Fetcher) FetchAll(logger *slog.Logger, urls []string, inlineContent bool, username string, post *db.Post) (*MsgBody, error) {
21+ logger.Info("fetching feeds", "inlineContent", inlineContent)
22 fp := gofeed.NewParser()
23 daysLeft := ""
24 showBanner := false
25@@ -431,6 +434,7 @@ func (f *Fetcher) FetchAll(logger *slog.Logger, urls []string, inlineContent boo
26 }
27 feeds := &DigestFeed{
28 KeepAliveURL: fmt.Sprintf("https://feeds.pico.sh/keep-alive/%s", post.ID),
29+ UnsubURL: fmt.Sprintf("https://feeds.pico.sh/unsub/%s", post.ID),
30 DaysLeft: daysLeft,
31 ShowBanner: showBanner,
32 Options: DigestOptions{InlineContent: inlineContent},
+19,
-15
1@@ -17,35 +17,39 @@ img {
2 {{end}}
3
4 <div class="feeds">
5-{{range .Feeds}}
6-<div style="margin-bottom: 10px;">
7+ {{range .Feeds}}
8+ <div style="margin-bottom: 10px;">
9 <h1 style="margin-bottom: 3px;"><a href="{{.Link}}">{{.Title}}</a></h1>
10 <div>{{.Description}}</div>
11-</div>
12+ </div>
13
14-<div class="summary">
15+ <div class="summary">
16 <h2>Summary</h2>
17 {{range .Items}}
18 <ul>
19- <li><a href="{{.Link}}">{{.Title}}</a></li>
20+ <li><a href="{{.Link}}">{{.Title}}</a></li>
21 </ul>
22 {{end}}
23 <hr />
24-</div>
25+ </div>
26
27-{{if $.Options.InlineContent}}
28-<div>
29+ {{if $.Options.InlineContent}}
30+ <div>
31 {{range .Items}}
32 <div>
33- <h1><a href="{{.Link}}">{{.Title}}</a></h1>
34- <div>{{.Description}}</div>
35- <div>{{.Content}}</div>
36+ <h1><a href="{{.Link}}">{{.Title}}</a></h1>
37+ <div>{{.Description}}</div>
38+ <div>{{.Content}}</div>
39 </div>
40 <hr />
41 {{end}}
42-</div>
43-{{end}}
44+ </div>
45+ {{end}}
46
47-<hr style="margin: 10px 0;" />
48-{{end}}
49+ <hr style="margin: 10px 0;" />
50+ {{end}}
51 </div>
52+
53+<blockquote>
54+ <a href="{{.UnsubURL}}">unsubscribe</a> to this digest.
55+</blockquote>
+2,
-0
1@@ -16,3 +16,5 @@
2 ---
3
4 {{end}}
5+
6+> {{.UnsubURL}} to unsubscribe to this digest.
+2,
-2
1@@ -76,8 +76,8 @@ func (p *FeedHooks) FileValidate(s ssh.Session, data *filehandlers.PostMetaData)
2 func (p *FeedHooks) FileMeta(s ssh.Session, data *filehandlers.PostMetaData) error {
3 if data.Data.LastDigest == nil {
4 now := time.Now()
5- // only fetch posts in the last week
6- dd := now.AddDate(0, 0, -7)
7+ // let it run on the next loop
8+ dd := now.AddDate(0, 0, -31)
9 data.Data.LastDigest = &dd
10 }
11
1@@ -220,9 +220,10 @@ func ListParseText(text string) *ListParsedText {
2 textItems := SplitByNewline(text)
3 items := []*ListItem{}
4 meta := ListMetaData{
5- ListType: "disc",
6- Tags: []string{},
7- Layout: "default",
8+ ListType: "disc",
9+ Tags: []string{},
10+ Layout: "default",
11+ InlineContent: true,
12 }
13 pre := false
14 skip := false