Eric Bower
·
2026-01-25
api.go
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 keepAliveHandler(w http.ResponseWriter, r *http.Request) {
16 dbpool := router.GetDB(r)
17 logger := router.GetLogger(r)
18 postID, _ := url.PathUnescape(router.GetField(r, 0))
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 unsubHandler(w http.ResponseWriter, r *http.Request) {
64 dbpool := router.GetDB(r)
65 logger := router.GetLogger(r)
66 postID, _ := url.PathUnescape(router.GetField(r, 0))
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
100func createMainRoutes(staticRoutes []router.Route) []router.Route {
101 routes := []router.Route{
102 router.NewRoute("GET", "/", router.CreatePageHandler("html/marketing.page.tmpl")),
103 router.NewRoute("GET", "/keep-alive/(.+)", keepAliveHandler),
104 router.NewRoute("GET", "/unsub/(.+)", unsubHandler),
105 router.NewRoute("GET", "/_metrics", promhttp.Handler().ServeHTTP),
106 }
107
108 routes = append(
109 routes,
110 staticRoutes...,
111 )
112
113 return routes
114}
115
116func createStaticRoutes() []router.Route {
117 return []router.Route{
118 router.NewRoute("GET", "/main.css", router.ServeFile("main.css", "text/css")),
119 router.NewRoute("GET", "/card.png", router.ServeFile("card.png", "image/png")),
120 router.NewRoute("GET", "/favicon-16x16.png", router.ServeFile("favicon-16x16.png", "image/png")),
121 router.NewRoute("GET", "/favicon-32x32.png", router.ServeFile("favicon-32x32.png", "image/png")),
122 router.NewRoute("GET", "/apple-touch-icon.png", router.ServeFile("apple-touch-icon.png", "image/png")),
123 router.NewRoute("GET", "/favicon.ico", router.ServeFile("favicon.ico", "image/x-icon")),
124 router.NewRoute("GET", "/robots.txt", router.ServeFile("robots.txt", "text/plain")),
125 }
126}
127
128func StartApiServer() {
129 cfg := NewConfigSite("feeds-web")
130 db := postgres.NewDB(cfg.DbURL, cfg.Logger)
131 defer func() {
132 _ = db.Close()
133 }()
134 logger := cfg.Logger
135
136 // cron daily digest
137 fetcher := NewFetcher(db, cfg)
138 go fetcher.Loop()
139
140 staticRoutes := createStaticRoutes()
141
142 if cfg.Debug {
143 staticRoutes = router.CreatePProfRoutes(staticRoutes)
144 }
145
146 mainRoutes := createMainRoutes(staticRoutes)
147
148 apiConfig := &router.ApiConfig{
149 Cfg: cfg,
150 Dbpool: db,
151 }
152 handler := router.CreateServe(mainRoutes, []router.Route{}, apiConfig)
153 router := http.HandlerFunc(handler)
154
155 portStr := fmt.Sprintf(":%s", cfg.Port)
156 logger.Info(
157 "Starting server on port",
158 "port", cfg.Port,
159 "domain", cfg.Domain,
160 )
161
162 logger.Error(http.ListenAndServe(portStr, router).Error())
163}