repos / pico

pico services mono repo
git clone https://github.com/picosh/pico.git

pico / pkg / apps / feeds
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}