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