pico

created pr with pico/forms-rss.1 on 2026-11-08T03:18:36Z · by c8ef7d19
cmds
checkout latest patchset:
ssh pr.pico.sh pull pico:forms-rss | git am -3
checkout specific patchset revision:
ssh pr.pico.sh pull pico:forms-rss [rev] | git am -3
add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pico:forms-rss
add comment to patch request:
ssh pr.pico.sh comment pico:forms-rss "lgtm!"
+72 -0 pkg/apps/pgs/auto_form.go #
......@@ -1,8 +1,11 @@
11 package pgs
22
33 import (
4+ "fmt"
45 "net/http"
6+ "time"
57
8+ "github.com/gorilla/feeds"
69 "github.com/picosh/pico/pkg/shared/router"
710 )
811
......@@ -87,3 +90,72 @@ func serveAutoFormSubmitted(w http.ResponseWriter, r *http.Request, cfg *Config)
8790 http.Error(w, "Server error", http.StatusInternalServerError)
8891 }
8992 }
93+
94+func handleAutoFormRss(w http.ResponseWriter, r *http.Request, cfg *Config) {
95+ logger := cfg.Logger
96+ token := r.PathValue("token")
97+ user, err := cfg.DB.FindUserByToken(token)
98+ if err != nil {
99+ cfg.Logger.Error("invalid token", "err", err.Error())
100+ http.Error(w, "invalid token", http.StatusUnprocessableEntity)
101+ return
102+ }
103+
104+ formNames, err := cfg.DB.FindFormNamesByUser(user.ID)
105+ if err != nil {
106+ cfg.Logger.Error("could not get form names", "err", err.Error())
107+ http.Error(w, "could not get form names", http.StatusInternalServerError)
108+ return
109+ }
110+
111+ feed := &feeds.Feed{
112+ Title: fmt.Sprintf("%s form entries", user.Name),
113+ Link: &feeds.Link{Href: "https://pgs.sh"},
114+ Description: fmt.Sprintf("%s form entries", user.Name),
115+ Author: &feeds.Author{Name: user.Name},
116+ Created: time.Now(),
117+ }
118+
119+ var feedItems []*feeds.Item
120+ for _, name := range formNames {
121+ entries, err := cfg.DB.FindFormEntriesByUserAndName(user.ID, name)
122+ if err != nil {
123+ continue
124+ }
125+
126+ for _, entry := range entries {
127+ content := "<ul>"
128+ for key, val := range entry.Data {
129+ content += fmt.Sprintf("<li>%s: %s</li>\n", key, val)
130+ }
131+ content += "</ul>"
132+
133+ realUrl := fmt.Sprintf("https://pgs.sh/forms/%s/%s", name, entry.ID)
134+ item := &feeds.Item{
135+ Id: entry.ID,
136+ Title: entry.Name,
137+ Link: &feeds.Link{Href: realUrl},
138+ Content: content,
139+ Created: *entry.CreatedAt,
140+ Updated: *entry.CreatedAt,
141+ Description: "",
142+ Author: &feeds.Author{Name: user.Name},
143+ }
144+ feedItems = append(feedItems, item)
145+ }
146+
147+ }
148+ feed.Items = feedItems
149+
150+ rss, err := feed.ToAtom()
151+ if err != nil {
152+ logger.Error("could not convert feed to atom", "err", err.Error())
153+ http.Error(w, "Could not generate atom rss feed", http.StatusInternalServerError)
154+ }
155+
156+ w.Header().Add("Content-Type", "application/atom+xml")
157+ _, err = w.Write([]byte(rss))
158+ if err != nil {
159+ logger.Error("http write failed", "err", err.Error())
160+ }
161+}
+1 -0 pkg/apps/pgs/db/db.go #
......@@ -8,6 +8,7 @@ import (
88
99 type PgsDB interface {
1010 db.AuthDB
11+ db.TokenDB
1112 FindUsers() ([]*db.User, error)
1213
1314 InsertProject(userID, name, projectDir string) (string, error)
+16 -0 pkg/apps/pgs/db/memory.go #
......@@ -277,3 +277,19 @@ func (me *MemoryDB) RemoveFormEntriesByUserAndName(userID, name string) error {
277277 me.FormEntries = filtered
278278 return nil
279279 }
280+
281+func (me *MemoryDB) FindUserByToken(token string) (*db.User, error) {
282+ return nil, fmt.Errorf("not implemented")
283+}
284+func (me *MemoryDB) FindTokensByUser(userID string) ([]*db.Token, error) {
285+ return []*db.Token{}, fmt.Errorf("not implemented")
286+}
287+func (me *MemoryDB) InsertToken(userID, name string) (string, error) {
288+ return "", fmt.Errorf("not implemented")
289+}
290+func (me *MemoryDB) UpsertToken(userID, name string) (string, error) {
291+ return "", fmt.Errorf("not implemented")
292+}
293+func (me *MemoryDB) RemoveToken(tokenID string) error {
294+ return fmt.Errorf("not implemented")
295+}
+26 -0 pkg/apps/pgs/db/postgres.go #
......@@ -338,3 +338,29 @@ func (me *PgsPsqlDB) RemoveFormEntriesByUserAndName(userID, name string) error {
338338 _, err := me.Db.Exec("DELETE FROM form_entries WHERE user_id=$1 AND name=$2", userID, name)
339339 return err
340340 }
341+
342+func (me *PgsPsqlDB) FindUserByToken(token string) (*db.User, error) {
343+ user := &db.User{}
344+ err := me.Db.Get(user, `
345+ SELECT app_users.id, app_users.name, app_users.created_at
346+ FROM app_users
347+ LEFT JOIN tokens ON tokens.user_id = app_users.id
348+ WHERE tokens.token = $1 AND tokens.expires_at > NOW()`, token)
349+ if err != nil {
350+ return nil, err
351+ }
352+ return user, nil
353+}
354+
355+func (me *PgsPsqlDB) FindTokensByUser(userID string) ([]*db.Token, error) {
356+ return []*db.Token{}, fmt.Errorf("not implemented")
357+}
358+func (me *PgsPsqlDB) InsertToken(userID, name string) (string, error) {
359+ return "", fmt.Errorf("not implemented")
360+}
361+func (me *PgsPsqlDB) UpsertToken(userID, name string) (string, error) {
362+ return "", fmt.Errorf("not implemented")
363+}
364+func (me *PgsPsqlDB) RemoveToken(tokenID string) error {
365+ return fmt.Errorf("not implemented")
366+}
+5 -0 pkg/apps/pgs/web.go #
......@@ -289,6 +289,7 @@ func (web *WebRouter) initRouters() {
289289
290290 rootRouter.Handle("GET /rss/updated", web.createRssHandler("updated_at"))
291291 rootRouter.Handle("GET /rss", web.createRssHandler("created_at"))
292+ rootRouter.HandleFunc("GET /rss/forms/{token}", web.handleAutoFormRss)
292293 rootRouter.Handle("GET /{$}", files.Page("html/marketing.page.tmpl"))
293294 web.RootRouter = rootRouter
294295
......@@ -595,3 +596,7 @@ func (web *WebRouter) handleLogin(w http.ResponseWriter, r *http.Request) {
595596 func (web *WebRouter) handleAutoForm(w http.ResponseWriter, r *http.Request) {
596597 handleAutoForm(w, r, web.Cfg)
597598 }
599+
600+func (web *WebRouter) handleAutoFormRss(w http.ResponseWriter, r *http.Request) {
601+ handleAutoFormRss(w, r, web.Cfg)
602+}
+14 -0 sql/teardown.sql #
......@@ -9,3 +9,17 @@ DROP TABLE posts CASCADE;
99 DROP TABLE projects CASCADE;
1010 DROP TABLE public_keys CASCADE;
1111 DROP TABLE tokens CASCADE;
12+DROP TABLE access_logs;
13+DROP TABLE analytics_monthly_top_referers;
14+DROP TABLE analytics_monthly_top_urls;
15+DROP TABLE analytics_monthly_visits;
16+DROP TABLE analytics_user_sites;
17+DROP TABLE block_signups;
18+DROP TABLE caddy_storage;
19+DROP TABLE certmagic_data;
20+DROP TABLE certmagic_locks;
21+DROP TABLE form_entries;
22+DROP TABLE invites;
23+DROP TABLE pipe_monitors;
24+DROP TABLE pipe_monitors_history;
25+DROP TABLE tuns_event_logs;
Back to top