Logs
Patchset ps-27
feat(prose): live journal
Eric Bower
db/db.go
+1
-0
go.mod
+1
-0
go.sum
+8
-0
prose/api.go
+24
-24
prose/scp_hooks.go
+5
-0
chore: diff in post
Eric Bower
prose/api.go
+11
-0
prose/scp_hooks.go
+28
-3
shared/mdparser.go
+7
-0
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.
db/db.go
link
+1
-0
+1
-0
1diff --git a/db/db.go b/db/db.go
2index 8cee29c..2c82dc4 100644
3--- a/db/db.go
4+++ b/db/db.go
5@@ -32,6 +32,7 @@ type User struct {
6 type PostData struct {
7 ImgPath string `json:"img_path"`
8 LastDigest *time.Time `json:"last_digest"`
9+ Diff string `json:"diff"`
10 }
11
12 // Make the Attrs struct implement the driver.Valuer interface. This method
go.mod
link
+1
-0
+1
-0
1diff --git a/go.mod b/go.mod
2index b7084ba..549e3ad 100644
3--- a/go.mod
4+++ b/go.mod
5@@ -37,6 +37,7 @@ require (
6 github.com/picosh/tunkit v0.0.0-20240709033345-8315d4f3cd0e
7 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
8 github.com/sendgrid/sendgrid-go v3.14.0+incompatible
9+ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
10 github.com/simplesurance/go-ip-anonymizer v0.0.0-20200429124537-35a880f8e87d
11 github.com/x-way/crawlerdetect v0.2.21
12 github.com/yuin/goldmark v1.7.1
go.sum
link
+8
-0
+8
-0
1diff --git a/go.sum b/go.sum
2index 1980154..2577975 100644
3--- a/go.sum
4+++ b/go.sum
5@@ -158,8 +158,11 @@ github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuV
6 github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
7 github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
8 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
9+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
10 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
11 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
12+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
13+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
14 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
15 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
16 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
17@@ -265,6 +268,8 @@ github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekuei
18 github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
19 github.com/sendgrid/sendgrid-go v3.14.0+incompatible h1:KDSasSTktAqMJCYClHVE94Fcif2i7P7wzISv1sU6DUA=
20 github.com/sendgrid/sendgrid-go v3.14.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
21+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
22+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
23 github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
24 github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
25 github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
26@@ -276,6 +281,7 @@ github.com/simplesurance/go-ip-anonymizer v0.0.0-20200429124537-35a880f8e87d/go.
27 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
28 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
29 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
30+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
31 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
32 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
33 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
34@@ -393,10 +399,12 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
35 google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
36 google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
37 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
38+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
39 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
40 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
41 gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
42 gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
43+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
44 gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
45 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
46 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
prose/api.go
link
+24
-24
+24
-24
1diff --git a/prose/api.go b/prose/api.go
2index 1d703d8..7643e0e 100644
3--- a/prose/api.go
4+++ b/prose/api.go
5@@ -160,7 +160,7 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
6 }
7
8 tag := r.URL.Query().Get("tag")
9- pager := &db.Pager{Num: 1000, Page: 0}
10+ pager := &db.Pager{Num: 250, Page: 0}
11 var posts []*db.Post
12 var p *db.Paginate[*db.Post]
13 if tag == "" {
14@@ -170,6 +170,13 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
15 }
16 posts = p.Data
17
18+ byUpdated := strings.Contains(r.URL.Path, "live")
19+ if byUpdated {
20+ slices.SortFunc(posts, func(a *db.Post, b *db.Post) int {
21+ return b.UpdatedAt.Compare(*a.UpdatedAt)
22+ })
23+ }
24+
25 if err != nil {
26 logger.Error(err.Error())
27 http.Error(w, "could not fetch posts for blog", http.StatusInternalServerError)
28@@ -653,6 +660,13 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
29 curl := shared.CreateURLFromRequest(cfg, r)
30 blogUrl := cfg.FullBlogURL(curl, username)
31
32+ byUpdated := strings.Contains(r.URL.Path, "live")
33+ if byUpdated {
34+ slices.SortFunc(posts, func(a *db.Post, b *db.Post) int {
35+ return b.UpdatedAt.Compare(*a.UpdatedAt)
36+ })
37+ }
38+
39 feed := &feeds.Feed{
40 Id: blogUrl,
41 Title: headerTxt.Title,
42@@ -691,12 +705,18 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
43 }
44
45 realUrl := cfg.FullPostURL(curl, post.Username, post.Slug)
46+ feedId := realUrl
47+
48+ if byUpdated {
49+ feedId = fmt.Sprintf("%s:%s", realUrl, post.UpdatedAt.Format(time.RFC3339))
50+ }
51
52 item := &feeds.Item{
53- Id: realUrl,
54+ Id: feedId,
55 Title: shared.FilenameToTitle(post.Filename, post.Title),
56 Link: &feeds.Link{Href: realUrl},
57 Content: tpl.String(),
58+ Updated: *post.UpdatedAt,
59 Created: *post.CreatedAt,
60 Description: post.Description,
61 }
62@@ -849,36 +869,16 @@ func createMainRoutes(staticRoutes []shared.Route) []shared.Route {
63 staticRoutes...,
64 )
65
66- routes = append(
67- routes,
68- shared.NewRoute("GET", "/rss", rssHandler),
69- shared.NewRoute("GET", "/rss.xml", rssHandler),
70- shared.NewRoute("GET", "/atom.xml", rssHandler),
71- shared.NewRoute("GET", "/feed.xml", rssHandler),
72-
73- shared.NewRoute("GET", "/([^/]+)", blogHandler),
74- shared.NewRoute("GET", "/([^/]+)/rss", rssBlogHandler),
75- shared.NewRoute("GET", "/([^/]+)/rss.xml", rssBlogHandler),
76- shared.NewRoute("GET", "/([^/]+)/atom.xml", rssBlogHandler),
77- shared.NewRoute("GET", "/([^/]+)/atom", rssBlogHandler),
78- shared.NewRoute("GET", "/([^/]+)/blog/index.xml", rssBlogHandler),
79- shared.NewRoute("GET", "/([^/]+)/feed.xml", rssBlogHandler),
80- shared.NewRoute("GET", "/([^/]+)/_styles.css", blogStyleHandler),
81- shared.NewRoute("GET", "/raw/([^/]+)/(.+)", postRawHandler),
82- shared.NewRoute("GET", "/([^/]+)/(.+)/(.+)", imgs.ImgRequest),
83- shared.NewRoute("GET", "/([^/]+)/(.+.(?:jpg|jpeg|png|gif|webp|svg))$", imgs.ImgRequest),
84- shared.NewRoute("GET", "/([^/]+)/i", imgs.ImgsListHandler),
85- shared.NewRoute("GET", "/([^/]+)/(.+)", postHandler),
86- )
87-
88 return routes
89 }
90
91 func createSubdomainRoutes(staticRoutes []shared.Route) []shared.Route {
92 routes := []shared.Route{
93 shared.NewRoute("GET", "/", blogHandler),
94+ shared.NewRoute("GET", "/live", blogHandler),
95 shared.NewRoute("GET", "/_styles.css", blogStyleHandler),
96 shared.NewRoute("GET", "/rss", rssBlogHandler),
97+ shared.NewRoute("GET", "/live/rss", rssBlogHandler),
98 shared.NewRoute("GET", "/rss.xml", rssBlogHandler),
99 shared.NewRoute("GET", "/atom.xml", rssBlogHandler),
100 shared.NewRoute("GET", "/feed.xml", rssBlogHandler),
prose/scp_hooks.go
link
+5
-0
+5
-0
1diff --git a/prose/scp_hooks.go b/prose/scp_hooks.go
2index 577790c..3cb64e6 100644
3--- a/prose/scp_hooks.go
4+++ b/prose/scp_hooks.go
5@@ -10,6 +10,7 @@ import (
6 "github.com/picosh/pico/db"
7 "github.com/picosh/pico/filehandlers"
8 "github.com/picosh/pico/shared"
9+ "github.com/sergi/go-diff/diffmatchpatch"
10 )
11
12 type MarkdownHooks struct {
13@@ -62,6 +63,10 @@ func (p *MarkdownHooks) FileMeta(s ssh.Session, data *filehandlers.PostMetaData)
14 data.Tags = parsedText.Tags
15 data.Description = parsedText.Description
16
17+ dmp := diffmatchpatch.New()
18+ diffs := dmp.DiffMain(data.Cur.Text, data.Text, false)
19+ data.Data.Diff = dmp.DiffPrettyText(diffs)
20+
21 if parsedText.PublishAt != nil && !parsedText.PublishAt.IsZero() {
22 data.PublishAt = parsedText.MetaData.PublishAt
23 }
chore: diff in post
filehandlers/post_handler.go
link
+1
-0
+1
-0
1diff --git a/filehandlers/post_handler.go b/filehandlers/post_handler.go
2index 4e10b66..30e00cc 100644
3--- a/filehandlers/post_handler.go
4+++ b/filehandlers/post_handler.go
5@@ -139,6 +139,7 @@ func (h *ScpUploadHandler) Write(s ssh.Session, entry *utils.FileEntry) (string,
6
7 if post != nil {
8 metadata.Cur = post
9+ metadata.Data = post.Data
10 metadata.Post.PublishAt = post.PublishAt
11 }
12
prose/api.go
link
+11
-0
+11
-0
1diff --git a/prose/api.go b/prose/api.go
2index 7643e0e..60aa52a 100644
3--- a/prose/api.go
4+++ b/prose/api.go
5@@ -83,6 +83,7 @@ type PostPageData struct {
6 Footer template.HTML
7 Favicon template.URL
8 Unlisted bool
9+ Diff template.HTML
10 }
11
12 type TransparencyPageData struct {
13@@ -399,6 +400,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
14 favicon = readmeParsed.Favicon
15 }
16
17+ diff := ""
18 post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
19 if err == nil {
20 parsedText, err := shared.ParseText(post.Text)
21@@ -414,6 +416,14 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
22 ogImageCard = parsedText.ImageCard
23 }
24
25+ // if parsedText.Live {
26+ if post.Data.Diff != "" {
27+ str := "```diff\n" + post.Data.Diff + "\n```"
28+ diffParsed, _ := shared.ParseText(str)
29+ diff = diffParsed.Html
30+ }
31+ // }
32+
33 // track visit
34 view, err := shared.AnalyticsVisitFromRequest(r, user.ID, cfg.Secret)
35 if err == nil {
36@@ -451,6 +461,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
37 Favicon: template.URL(favicon),
38 Footer: footerHTML,
39 Unlisted: unlisted,
40+ Diff: template.HTML(diff),
41 }
42 } else {
43 // TODO: HACK to support imgs slugs inside prose
prose/html/post.page.tmpl
link
+8
-1
+8
-1
1diff --git a/prose/html/post.page.tmpl b/prose/html/post.page.tmpl
2index 3811a4c..e667978 100644
3--- a/prose/html/post.page.tmpl
4+++ b/prose/html/post.page.tmpl
5@@ -62,9 +62,16 @@
6 </div>
7 </header>
8 <main>
9+ {{if .Diff}}
10+ <details>
11+ <summary>diff</summary>
12+ <div class="md">{{.Diff}}</div>
13+ </details>
14+ {{end}}
15+
16 <article class="md">
17 {{.Contents}}
18- <div id="post-footer">{{.Footer}}</div>
19+ <div id="post-footer" class="box">{{.Footer}}</div>
20 </article>
21 </main>
22 {{template "footer" .}}
prose/scp_hooks.go
link
+28
-3
+28
-3
1diff --git a/prose/scp_hooks.go b/prose/scp_hooks.go
2index 3cb64e6..707bb61 100644
3--- a/prose/scp_hooks.go
4+++ b/prose/scp_hooks.go
5@@ -1,6 +1,7 @@
6 package prose
7
8 import (
9+ "bytes"
10 "fmt"
11 "strings"
12
13@@ -47,6 +48,26 @@ func (p *MarkdownHooks) FileValidate(s ssh.Session, data *filehandlers.PostMetaD
14 return true, nil
15 }
16
17+func diffText(diffs []diffmatchpatch.Diff) string {
18+ var buff bytes.Buffer
19+ for _, diff := range diffs {
20+ text := diff.Text
21+
22+ switch diff.Type {
23+ case diffmatchpatch.DiffInsert:
24+ _, _ = buff.WriteString("+")
25+ _, _ = buff.WriteString(text)
26+ case diffmatchpatch.DiffDelete:
27+ _, _ = buff.WriteString("-")
28+ _, _ = buff.WriteString(text)
29+ case diffmatchpatch.DiffEqual:
30+ _, _ = buff.WriteString(text)
31+ }
32+ }
33+
34+ return buff.String()
35+}
36+
37 func (p *MarkdownHooks) FileMeta(s ssh.Session, data *filehandlers.PostMetaData) error {
38 parsedText, err := shared.ParseText(data.Text)
39 if err != nil {
40@@ -63,9 +84,13 @@ func (p *MarkdownHooks) FileMeta(s ssh.Session, data *filehandlers.PostMetaData)
41 data.Tags = parsedText.Tags
42 data.Description = parsedText.Description
43
44- dmp := diffmatchpatch.New()
45- diffs := dmp.DiffMain(data.Cur.Text, data.Text, false)
46- data.Data.Diff = dmp.DiffPrettyText(diffs)
47+ if data.Cur.Text == data.Text {
48+ } else {
49+ dmp := diffmatchpatch.New()
50+ diffs := dmp.DiffMain(data.Cur.Text, data.Text, false)
51+ fmt.Println(diffs)
52+ data.Data.Diff = diffText(diffs)
53+ }
54
55 if parsedText.PublishAt != nil && !parsedText.PublishAt.IsZero() {
56 data.PublishAt = parsedText.MetaData.PublishAt