repos / pico

pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh
git clone https://github.com/picosh/pico.git

commit
866976a
parent
0bd744d
author
Eric Bower
date
2024-08-01 14:24:47 +0000 UTC
feat(prose): live journal

The goal is to be able to use posts as live journals where reader can
receive updates to docs via rss.
5 files changed,  +39, -24
M go.mod
M go.sum
M db/db.go
+1, -0
1@@ -32,6 +32,7 @@ type User struct {
2 type PostData struct {
3 	ImgPath    string     `json:"img_path"`
4 	LastDigest *time.Time `json:"last_digest"`
5+	Diff       string     `json:"diff"`
6 }
7 
8 // Make the Attrs struct implement the driver.Valuer interface. This method
M go.mod
+1, -0
1@@ -37,6 +37,7 @@ require (
2 	github.com/picosh/tunkit v0.0.0-20240709033345-8315d4f3cd0e
3 	github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
4 	github.com/sendgrid/sendgrid-go v3.14.0+incompatible
5+	github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
6 	github.com/simplesurance/go-ip-anonymizer v0.0.0-20200429124537-35a880f8e87d
7 	github.com/x-way/crawlerdetect v0.2.21
8 	github.com/yuin/goldmark v1.7.1
M go.sum
+8, -0
 1@@ -158,8 +158,11 @@ github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuV
 2 github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
 3 github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
 4 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
 5+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 6 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 7 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 8+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 9+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
10 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
11 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
12 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
13@@ -265,6 +268,8 @@ github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekuei
14 github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
15 github.com/sendgrid/sendgrid-go v3.14.0+incompatible h1:KDSasSTktAqMJCYClHVE94Fcif2i7P7wzISv1sU6DUA=
16 github.com/sendgrid/sendgrid-go v3.14.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
17+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
18+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
19 github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
20 github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
21 github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
22@@ -276,6 +281,7 @@ github.com/simplesurance/go-ip-anonymizer v0.0.0-20200429124537-35a880f8e87d/go.
23 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
24 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
25 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
26+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
27 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
28 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
29 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
30@@ -393,10 +399,12 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
31 google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
32 google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
33 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
34+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
35 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
36 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
37 gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
38 gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
39+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
40 gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
41 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
42 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
M prose/api.go
+24, -24
 1@@ -160,7 +160,7 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
 2 	}
 3 
 4 	tag := r.URL.Query().Get("tag")
 5-	pager := &db.Pager{Num: 1000, Page: 0}
 6+	pager := &db.Pager{Num: 250, Page: 0}
 7 	var posts []*db.Post
 8 	var p *db.Paginate[*db.Post]
 9 	if tag == "" {
10@@ -170,6 +170,13 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
11 	}
12 	posts = p.Data
13 
14+	byUpdated := strings.Contains(r.URL.Path, "live")
15+	if byUpdated {
16+		slices.SortFunc(posts, func(a *db.Post, b *db.Post) int {
17+			return b.UpdatedAt.Compare(*a.UpdatedAt)
18+		})
19+	}
20+
21 	if err != nil {
22 		logger.Error(err.Error())
23 		http.Error(w, "could not fetch posts for blog", http.StatusInternalServerError)
24@@ -653,6 +660,13 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
25 	curl := shared.CreateURLFromRequest(cfg, r)
26 	blogUrl := cfg.FullBlogURL(curl, username)
27 
28+	byUpdated := strings.Contains(r.URL.Path, "live")
29+	if byUpdated {
30+		slices.SortFunc(posts, func(a *db.Post, b *db.Post) int {
31+			return b.UpdatedAt.Compare(*a.UpdatedAt)
32+		})
33+	}
34+
35 	feed := &feeds.Feed{
36 		Id:          blogUrl,
37 		Title:       headerTxt.Title,
38@@ -691,12 +705,18 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
39 		}
40 
41 		realUrl := cfg.FullPostURL(curl, post.Username, post.Slug)
42+		feedId := realUrl
43+
44+		if byUpdated {
45+			feedId = fmt.Sprintf("%s:%s", realUrl, post.UpdatedAt.Format(time.RFC3339))
46+		}
47 
48 		item := &feeds.Item{
49-			Id:          realUrl,
50+			Id:          feedId,
51 			Title:       shared.FilenameToTitle(post.Filename, post.Title),
52 			Link:        &feeds.Link{Href: realUrl},
53 			Content:     tpl.String(),
54+			Updated:     *post.UpdatedAt,
55 			Created:     *post.CreatedAt,
56 			Description: post.Description,
57 		}
58@@ -849,36 +869,16 @@ func createMainRoutes(staticRoutes []shared.Route) []shared.Route {
59 		staticRoutes...,
60 	)
61 
62-	routes = append(
63-		routes,
64-		shared.NewRoute("GET", "/rss", rssHandler),
65-		shared.NewRoute("GET", "/rss.xml", rssHandler),
66-		shared.NewRoute("GET", "/atom.xml", rssHandler),
67-		shared.NewRoute("GET", "/feed.xml", rssHandler),
68-
69-		shared.NewRoute("GET", "/([^/]+)", blogHandler),
70-		shared.NewRoute("GET", "/([^/]+)/rss", rssBlogHandler),
71-		shared.NewRoute("GET", "/([^/]+)/rss.xml", rssBlogHandler),
72-		shared.NewRoute("GET", "/([^/]+)/atom.xml", rssBlogHandler),
73-		shared.NewRoute("GET", "/([^/]+)/atom", rssBlogHandler),
74-		shared.NewRoute("GET", "/([^/]+)/blog/index.xml", rssBlogHandler),
75-		shared.NewRoute("GET", "/([^/]+)/feed.xml", rssBlogHandler),
76-		shared.NewRoute("GET", "/([^/]+)/_styles.css", blogStyleHandler),
77-		shared.NewRoute("GET", "/raw/([^/]+)/(.+)", postRawHandler),
78-		shared.NewRoute("GET", "/([^/]+)/(.+)/(.+)", imgs.ImgRequest),
79-		shared.NewRoute("GET", "/([^/]+)/(.+.(?:jpg|jpeg|png|gif|webp|svg))$", imgs.ImgRequest),
80-		shared.NewRoute("GET", "/([^/]+)/i", imgs.ImgsListHandler),
81-		shared.NewRoute("GET", "/([^/]+)/(.+)", postHandler),
82-	)
83-
84 	return routes
85 }
86 
87 func createSubdomainRoutes(staticRoutes []shared.Route) []shared.Route {
88 	routes := []shared.Route{
89 		shared.NewRoute("GET", "/", blogHandler),
90+		shared.NewRoute("GET", "/live", blogHandler),
91 		shared.NewRoute("GET", "/_styles.css", blogStyleHandler),
92 		shared.NewRoute("GET", "/rss", rssBlogHandler),
93+		shared.NewRoute("GET", "/live/rss", rssBlogHandler),
94 		shared.NewRoute("GET", "/rss.xml", rssBlogHandler),
95 		shared.NewRoute("GET", "/atom.xml", rssBlogHandler),
96 		shared.NewRoute("GET", "/feed.xml", rssBlogHandler),
M prose/scp_hooks.go
+5, -0
 1@@ -10,6 +10,7 @@ import (
 2 	"github.com/picosh/pico/db"
 3 	"github.com/picosh/pico/filehandlers"
 4 	"github.com/picosh/pico/shared"
 5+	"github.com/sergi/go-diff/diffmatchpatch"
 6 )
 7 
 8 type MarkdownHooks struct {
 9@@ -62,6 +63,10 @@ func (p *MarkdownHooks) FileMeta(s ssh.Session, data *filehandlers.PostMetaData)
10 	data.Tags = parsedText.Tags
11 	data.Description = parsedText.Description
12 
13+	dmp := diffmatchpatch.New()
14+	diffs := dmp.DiffMain(data.Cur.Text, data.Text, false)
15+	data.Data.Diff = dmp.DiffPrettyText(diffs)
16+
17 	if parsedText.PublishAt != nil && !parsedText.PublishAt.IsZero() {
18 		data.PublishAt = parsedText.MetaData.PublishAt
19 	}