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