Antonio Mika
·
2026-10-25
1package feeds
2
3import (
4 "fmt"
5 "net/http"
6 "net/url"
7 "time"
8
9 "github.com/picosh/pico/pkg/db/postgres"
10 "github.com/picosh/pico/pkg/metrics"
11 "github.com/picosh/pico/pkg/shared"
12 "github.com/picosh/pico/pkg/shared/router"
13 "github.com/prometheus/client_golang/prometheus"
14 "github.com/prometheus/client_golang/prometheus/promhttp"
15)
16
17func (web *webRouter) keepAliveHandler(w http.ResponseWriter, r *http.Request) {
18 dbpool := web.db
19 logger := web.cfg.Logger
20 postID, _ := url.PathUnescape(r.PathValue("id"))
21
22 post, err := dbpool.FindPost(postID)
23 if err != nil {
24 logger.Error("post not found", "err", err)
25 http.Error(w, "post not found", http.StatusNotFound)
26 return
27 }
28
29 user, err := dbpool.FindUser(post.UserID)
30 if err != nil {
31 logger.Error("user not found", "err", err)
32 http.Error(w, "user not found", http.StatusNotFound)
33 return
34 }
35 logger = shared.LoggerWithUser(logger, user)
36 logger = logger.With("post", post.ID, "filename", post.Filename)
37
38 now := time.Now()
39 expiresAt := now.AddDate(0, 3, 0)
40 post.ExpiresAt = &expiresAt
41 _, err = dbpool.UpdatePost(post)
42 if err != nil {
43 logger.Error("could not update post", "err", err.Error())
44 http.Error(w, "server error", 500)
45 return
46 }
47
48 w.Header().Add("Content-Type", "text/plain")
49
50 logger.Info(
51 "Success! This feed will stay active until %s or by clicking the link in your digest email again",
52 "expiresAt", now,
53 )
54 txt := fmt.Sprintf(
55 "Success! This feed will stay active until %s or by clicking the link in your digest email again",
56 now,
57 )
58 _, err = w.Write([]byte(txt))
59 if err != nil {
60 logger.Error("could not write to writer", "err", err.Error())
61 http.Error(w, "server error", 500)
62 }
63}
64
65func (web *webRouter) unsubHandler(w http.ResponseWriter, r *http.Request) {
66 dbpool := web.db
67 logger := web.cfg.Logger
68 postID, _ := url.PathUnescape(r.PathValue("id"))
69
70 post, err := dbpool.FindPost(postID)
71 if err != nil {
72 logger.Error("post not found", "err", err)
73 http.Error(w, "post not found", http.StatusNotFound)
74 return
75 }
76
77 user, err := dbpool.FindUser(post.UserID)
78 if err != nil {
79 logger.Error("user not found", "err", err)
80 http.Error(w, "user not found", http.StatusNotFound)
81 return
82 }
83 logger = shared.LoggerWithUser(logger, user)
84 logger = logger.With("post", post.ID, "filename", post.Filename)
85
86 logger.Info("unsubscribe")
87 err = dbpool.RemovePosts([]string{post.ID})
88 if err != nil {
89 logger.Error("could not remove post", "err", err)
90 http.Error(w, "could not remove post", http.StatusInternalServerError)
91 return
92 }
93
94 txt := "Success! This feed digest post has been removed from our system."
95 _, err = w.Write([]byte(txt))
96 if err != nil {
97 logger.Error("could not write to writer", "err", err)
98 http.Error(w, "server error", 500)
99 }
100}
101
102type webRouter struct {
103 cfg *Config
104 db DB
105}
106
107func (web *webRouter) mux() *http.ServeMux {
108 cfg := web.cfg
109 files := cfg.Static()
110 mux := http.NewServeMux()
111 mux.HandleFunc("GET /{$}", files.Page("html/marketing.page.tmpl"))
112 mux.HandleFunc("GET /keep-alive/{id...}", web.keepAliveHandler)
113 mux.HandleFunc("GET /unsub/{id...}", web.unsubHandler)
114 mux.Handle("GET /_metrics", promhttp.Handler())
115 mux.HandleFunc("GET /main.css", files.File("main.css", "text/css"))
116 mux.HandleFunc("GET /card.png", files.File("card.png", "image/png"))
117 mux.HandleFunc("GET /favicon-16x16.png", files.File("favicon-16x16.png", "image/png"))
118 mux.HandleFunc("GET /apple-touch-icon.png", files.File("apple-touch-icon.png", "image/png"))
119 mux.HandleFunc("GET /favicon.ico", files.File("favicon.ico", "image/x-icon"))
120 mux.HandleFunc("GET /robots.txt", files.File("robots.txt", "text/plain"))
121 if cfg.Debug {
122 router.CreatePProfRoutesMux(mux)
123 }
124 return mux
125}
126
127func StartApiServer() {
128 cfg := NewConfig("feeds-web")
129 db := NewDB(postgres.NewDB(cfg.DbURL, cfg.Logger))
130 defer func() {
131 _ = db.Close()
132 }()
133 logger := cfg.Logger
134
135 // cron daily digest
136 fetcher := NewFetcher(db, cfg)
137 go fetcher.Loop()
138
139 web := &webRouter{cfg: cfg, db: db}
140
141 portStr := fmt.Sprintf(":%s", cfg.Port)
142 logger.Info(
143 "Starting server on port",
144 "port", cfg.Port,
145 "domain", cfg.Domain,
146 )
147
148 metrics.RegisterServerInfo(prometheus.DefaultRegisterer, "feeds")
149 logger.Error(http.ListenAndServe(portStr, web.mux()).Error())
150}