]> Sergey Matveev's repositories - sgblog.git/commitdiff
Comments Atom feeds v0.6.0
authorSergey Matveev <stargrave@stargrave.org>
Thu, 21 May 2020 16:47:52 +0000 (19:47 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Thu, 21 May 2020 16:47:52 +0000 (19:47 +0300)
README.texi
cmd/sgblog/http.go
cmd/sgblog/main.go
common.go

index 37840b3db52257ed6e2dc954c3224ea5efb47a24..c2b5887c45acee02d80385d178c2ba9b72caed94 100644 (file)
@@ -27,6 +27,7 @@ Its main competitive features:
     server
 @item Supports on the fly generation of
     @url{https://en.wikipedia.org/wiki/Atom_(feed), Atom} feeds
+    for posts, comments and per-post comments
 @item Single binary for email-backed comments posting
 @item If access is granted, then everyone can easily create an offline
     copy of your blog/phlog!
index de92039e774d9399a19048c5935ccf467984951e..1c09ae0b5677771411154b5247e583e9eb5a8578 100644 (file)
@@ -20,6 +20,7 @@ package main
 import (
        "bytes"
        "compress/gzip"
+       "crypto/sha1"
        "encoding/hex"
        "encoding/json"
        "encoding/xml"
@@ -46,7 +47,8 @@ import (
 )
 
 const (
-       AtomFeed = "feed.atom"
+       AtomPostsFeed    = "feed.atom"
+       AtomCommentsFeed = "comments.atom"
 )
 
 var (
@@ -139,6 +141,13 @@ func checkETag(etag hash.Hash) {
        }
 }
 
+func bytes2uuid(b []byte) string {
+       raw := new([16]byte)
+       copy(raw[:], b)
+       raw[6] = (raw[6] & 0x0F) | uint8(4<<4) // version 4
+       return fmt.Sprintf("%x-%x-%x-%x-%x", raw[0:4], raw[4:6], raw[6:8], raw[8:10], raw[10:])
+}
+
 func serveHTTP() {
        cfgPath := os.Getenv("SGBLOG_CFG")
        if cfgPath == "" {
@@ -207,8 +216,8 @@ func serveHTTP() {
        }
 
        defaultLinks = append(defaultLinks, `<link rel="top" href="`+cfg.URLPrefix+`/" title="top">`)
-       atomURL := cfg.AtomBaseURL + cfg.URLPrefix + "/" + AtomFeed
-       defaultLinks = append(defaultLinks, `<link rel="alternate" title="Atom feed" href="`+atomURL+`" type="application/atom+xml">`)
+       atomPostsURL := cfg.AtomBaseURL + cfg.URLPrefix + "/" + AtomPostsFeed
+       atomCommentsURL := cfg.AtomBaseURL + cfg.URLPrefix + "/" + AtomCommentsFeed
 
        headHash, err := initRepo(cfg)
        if err != nil {
@@ -344,8 +353,11 @@ func serveHTTP() {
                table.WriteString("</table>")
 
                var href string
-               var links []string
+               links := []string{`<link rel="alternate" title="Posts feed" href="` + atomPostsURL + `" type="application/atom+xml">`}
                var refs bytes.Buffer
+               if commentsTree != nil {
+                       links = append(links, `<link rel="alternate" title="Comments feed" href="`+atomCommentsURL+`" type="application/atom+xml">`)
+               }
                if offset > 0 {
                        if offsetPrev := offset - PageEntries; offsetPrev > 0 {
                                href = cfg.URLPrefix + "/?offset=" + strconv.Itoa(offsetPrev)
@@ -373,23 +385,20 @@ func serveHTTP() {
                out.Write(table.Bytes())
                out.Write(refs.Bytes())
                out.Write([]byte("\n"))
-       } else if pathInfo == "/"+AtomFeed {
+       } else if pathInfo == "/"+AtomPostsFeed {
                commit, err := repo.CommitObject(*headHash)
                if err != nil {
                        makeErr(err)
                }
-               etagHash.Write([]byte("ATOM"))
+               etagHash.Write([]byte("ATOM POSTS"))
                etagHash.Write(commit.Hash[:])
                checkETag(etagHash)
                feed := atom.Feed{
                        Title:   cfg.Title,
                        ID:      cfg.AtomId,
                        Updated: atom.Time(commit.Author.When),
-                       Link: []atom.Link{{
-                               Rel:  "self",
-                               Href: atomURL,
-                       }},
-                       Author: &atom.Person{Name: cfg.AtomAuthor},
+                       Link:    []atom.Link{{Rel: "self", Href: atomPostsURL}},
+                       Author:  &atom.Person{Name: cfg.AtomAuthor},
                }
                repoLog, err := repo.Log(&git.LogOptions{From: *headHash})
                if err != nil {
@@ -400,33 +409,100 @@ func serveHTTP() {
                        if err != nil {
                                break
                        }
-
-                       feedIdRaw := new([16]byte)
-                       copy(feedIdRaw[:], commit.Hash[:])
-                       feedIdRaw[6] = (feedIdRaw[6] & 0x0F) | uint8(4<<4) // version 4
-                       feedId := fmt.Sprintf(
-                               "%x-%x-%x-%x-%x",
-                               feedIdRaw[0:4],
-                               feedIdRaw[4:6],
-                               feedIdRaw[6:8],
-                               feedIdRaw[8:10],
-                               feedIdRaw[10:],
-                       )
-
                        lines := msgSplit(commit.Message)
                        feed.Entry = append(feed.Entry, &atom.Entry{
                                Title: lines[0],
-                               ID:    "urn:uuid:" + feedId,
+                               ID:    "urn:uuid:" + bytes2uuid(commit.Hash[:]),
                                Link: []atom.Link{{
                                        Rel:  "alternate",
                                        Href: cfg.AtomBaseURL + cfg.URLPrefix + "/" + commit.Hash.String(),
                                }},
                                Published: atom.Time(commit.Author.When),
                                Updated:   atom.Time(commit.Author.When),
-                               Summary: &atom.Text{
+                               Summary:   &atom.Text{Type: "text", Body: lines[0]},
+                               Content: &atom.Text{
                                        Type: "text",
-                                       Body: lines[0],
+                                       Body: strings.Join(lines[2:], "\n"),
                                },
+                       })
+               }
+               data, err := xml.MarshalIndent(&feed, "", "  ")
+               if err != nil {
+                       makeErr(err)
+               }
+               out.Write(data)
+               goto AtomFinish
+       } else if pathInfo == "/"+AtomCommentsFeed {
+               commit, err := repo.CommitObject(commentsRef.Hash())
+               if err != nil {
+                       makeErr(err)
+               }
+               etagHash.Write([]byte("ATOM COMMENTS"))
+               etagHash.Write(commit.Hash[:])
+               checkETag(etagHash)
+               idHasher, err := blake2b.New256(nil)
+               if err != nil {
+                       panic(err)
+               }
+               idHasher.Write([]byte("ATOM COMMENTS"))
+               idHasher.Write([]byte(cfg.AtomId))
+               feed := atom.Feed{
+                       Title:   cfg.Title + " comments",
+                       ID:      "urn:uuid:" + bytes2uuid(idHasher.Sum(nil)),
+                       Updated: atom.Time(commit.Author.When),
+                       Link:    []atom.Link{{Rel: "self", Href: atomCommentsURL}},
+                       Author:  &atom.Person{Name: cfg.AtomAuthor},
+               }
+               repoLog, err := repo.Log(&git.LogOptions{From: commentsRef.Hash()})
+               if err != nil {
+                       makeErr(err)
+               }
+               for i := 0; i < PageEntries; i++ {
+                       commit, err = repoLog.Next()
+                       if err != nil {
+                               break
+                       }
+                       fileStats, err := commit.Stats()
+                       if err != nil {
+                               makeErr(err)
+                       }
+                       t, err := commit.Tree()
+                       if err != nil {
+                               makeErr(err)
+                       }
+                       commentedHash := plumbing.NewHash(strings.ReplaceAll(
+                               fileStats[0].Name, "/", "",
+                       ))
+                       comments := parseComments(getNote(t, commentedHash))
+                       commit, err = repo.CommitObject(commentedHash)
+                       if err != nil {
+                               makeErr(err)
+                       }
+                       commentN := strconv.Itoa(len(comments) - 1)
+                       lines := strings.Split(comments[len(comments)-1], "\n")
+                       from := strings.TrimPrefix(lines[0], "From: ")
+                       date := strings.TrimPrefix(lines[1], "Date: ")
+                       idHasher.Reset()
+                       idHasher.Write([]byte("COMMENT"))
+                       idHasher.Write(commit.Hash[:])
+                       idHasher.Write([]byte(commentN))
+                       feed.Entry = append(feed.Entry, &atom.Entry{
+                               Title: strings.Join([]string{
+                                       "Comment ", commentN,
+                                       " for \"", msgSplit(commit.Message)[0],
+                                       "\" by ", from,
+                               }, ""),
+                               Author: &atom.Person{Name: from},
+                               ID:     "urn:uuid:" + bytes2uuid(idHasher.Sum(nil)),
+                               Link: []atom.Link{{
+                                       Rel: "alternate",
+                                       Href: strings.Join([]string{
+                                               cfg.AtomBaseURL, cfg.URLPrefix, "/",
+                                               commit.Hash.String(), "#comment", commentN,
+                                       }, ""),
+                               }},
+                               Published: atom.TimeStr(date),
+                               Updated:   atom.TimeStr(date),
                                Content: &atom.Text{
                                        Type: "text",
                                        Body: strings.Join(lines[2:], "\n"),
@@ -438,17 +514,9 @@ func serveHTTP() {
                        makeErr(err)
                }
                out.Write(data)
-               os.Stdout.WriteString("Content-Type: text/xml; charset=UTF-8\n")
-               os.Stdout.WriteString("ETag: " + etagString(etagHash) + "\n")
-               if gzipWriter != nil {
-                       os.Stdout.WriteString("Content-Encoding: gzip\n")
-                       gzipWriter.Close()
-               }
-               os.Stdout.WriteString("\n")
-               os.Stdout.Write(outBuf.Bytes())
-               return
+               goto AtomFinish
        } else if sha1DigestRe.MatchString(pathInfo[1:]) {
-               commit, err := repo.CommitObject(plumbing.NewHash(pathInfo[1:]))
+               commit, err := repo.CommitObject(plumbing.NewHash(pathInfo[1 : 1+sha1.Size*2]))
                if err != nil {
                        makeErr(err)
                }
@@ -457,16 +525,99 @@ func serveHTTP() {
                }
                etagHash.Write([]byte("ENTRY"))
                etagHash.Write(commit.Hash[:])
-               notesRaw := getNote(notesTree, commit.Hash)
-               etagHash.Write(notesRaw)
+               atomCommentsURL = strings.Join([]string{
+                       cfg.AtomBaseURL, cfg.URLPrefix, "/",
+                       commit.Hash.String(), "/", AtomCommentsFeed,
+               }, "")
                commentsRaw := getNote(commentsTree, commit.Hash)
                etagHash.Write(commentsRaw)
+               if strings.HasSuffix(pathInfo, AtomCommentsFeed) {
+                       etagHash.Write([]byte("ATOM COMMENTS"))
+                       checkETag(etagHash)
+                       type Comment struct {
+                               n    string
+                               from string
+                               date string
+                               body []string
+                       }
+                       commentsRaw := parseComments(commentsRaw)
+                       var toSkip int
+                       if len(commentsRaw) > PageEntries {
+                               toSkip = len(commentsRaw) - PageEntries
+                       }
+                       comments := make([]Comment, 0, len(commentsRaw)-toSkip)
+                       for i := len(commentsRaw) - 1; i >= toSkip; i-- {
+                               lines := strings.Split(commentsRaw[i], "\n")
+                               from := strings.TrimPrefix(lines[0], "From: ")
+                               date := strings.TrimPrefix(lines[1], "Date: ")
+                               comments = append(comments, Comment{
+                                       n:    strconv.Itoa(i),
+                                       from: from,
+                                       date: strings.Replace(date, " ", "T", 1),
+                                       body: lines[3:],
+                               })
+                       }
+                       idHasher, err := blake2b.New256(nil)
+                       if err != nil {
+                               panic(err)
+                       }
+                       idHasher.Write([]byte("ATOM COMMENTS"))
+                       idHasher.Write(commit.Hash[:])
+                       feed := atom.Feed{
+                               Title:  fmt.Sprintf("\"%s\" comments", msgSplit(commit.Message)[0]),
+                               ID:     "urn:uuid:" + bytes2uuid(idHasher.Sum(nil)),
+                               Link:   []atom.Link{{Rel: "self", Href: atomCommentsURL}},
+                               Author: &atom.Person{Name: cfg.AtomAuthor},
+                       }
+                       if len(comments) > 0 {
+                               feed.Updated = atom.TimeStr(comments[0].date)
+                       } else {
+                               feed.Updated = atom.Time(commit.Author.When)
+                       }
+                       for _, comment := range comments {
+                               idHasher.Reset()
+                               idHasher.Write([]byte("COMMENT"))
+                               idHasher.Write(commit.Hash[:])
+                               idHasher.Write([]byte(comment.n))
+                               feed.Entry = append(feed.Entry, &atom.Entry{
+                                       Title: strings.Join([]string{
+                                               "Comment", comment.n,
+                                               "by", comment.from,
+                                       }, " "),
+                                       Author: &atom.Person{Name: comment.from},
+                                       ID:     "urn:uuid:" + bytes2uuid(idHasher.Sum(nil)),
+                                       Link: []atom.Link{{
+                                               Rel: "alternate",
+                                               Href: strings.Join([]string{
+                                                       cfg.AtomBaseURL,
+                                                       cfg.URLPrefix, "/",
+                                                       commit.Hash.String(),
+                                                       "#comment", comment.n,
+                                               }, ""),
+                                       }},
+                                       Published: atom.TimeStr(comment.date),
+                                       Updated:   atom.TimeStr(comment.date),
+                                       Content: &atom.Text{
+                                               Type: "text",
+                                               Body: strings.Join(comment.body, "\n"),
+                                       },
+                               })
+                       }
+                       data, err := xml.MarshalIndent(&feed, "", "  ")
+                       if err != nil {
+                               makeErr(err)
+                       }
+                       out.Write(data)
+                       goto AtomFinish
+               }
+               notesRaw := getNote(notesTree, commit.Hash)
+               etagHash.Write(notesRaw)
                checkETag(etagHash)
                lines := msgSplit(commit.Message)
                title := lines[0]
                when := commit.Author.When.Format(sgblog.WhenFmt)
                os.Stdout.Write([]byte(startHeader(etagHash, gzipWriter != nil)))
-               links := []string{}
+               links := []string{`<link rel="alternate" title="Comments feed" href="` + atomCommentsURL + `" type="application/atom+xml">`}
                var parent string
                if len(commit.ParentHashes) > 0 {
                        parent = commit.ParentHashes[0].String()
@@ -524,4 +675,15 @@ func serveHTTP() {
                gzipWriter.Close()
        }
        os.Stdout.Write(outBuf.Bytes())
+       return
+
+AtomFinish:
+       os.Stdout.WriteString("Content-Type: text/xml; charset=UTF-8\n")
+       os.Stdout.WriteString("ETag: " + etagString(etagHash) + "\n")
+       if gzipWriter != nil {
+               os.Stdout.WriteString("Content-Encoding: gzip\n")
+               gzipWriter.Close()
+       }
+       os.Stdout.WriteString("\n")
+       os.Stdout.Write(outBuf.Bytes())
 }
index 9b03e4cae1ab04b89a8b87a59c2f614165f6d0a1..05654c0760942625cf2c8fcd04be124c60379bba 100644 (file)
@@ -20,6 +20,8 @@ package main
 
 import (
        "bytes"
+       "crypto/sha1"
+       "fmt"
        "io/ioutil"
        "os"
        "regexp"
@@ -36,9 +38,10 @@ const (
 )
 
 var (
-       sha1DigestRe = regexp.MustCompilePOSIX("([0-9a-f]{40,40})")
+       sha1DigestRe = regexp.MustCompilePOSIX(fmt.Sprintf("([0-9a-f]{%d,%d})", sha1.Size*2, sha1.Size*2))
        repo         *git.Repository
        notesTree    *object.Tree
+       commentsRef  *plumbing.Reference
        commentsTree *object.Tree
 )
 
@@ -134,7 +137,6 @@ func initRepo(cfg *Cfg) (*plumbing.Hash, error) {
        headHash := head.Hash()
        if notes, err := repo.Notes(); err == nil {
                var notesRef *plumbing.Reference
-               var commentsRef *plumbing.Reference
                notes.ForEach(func(ref *plumbing.Reference) error {
                        switch string(ref.Name()) {
                        case "refs/notes/commits":
index 97b29f7bedf361a87f854e25c3b4a87aefef815c..ca7d04ff454a5944f575eb17d2c70be5341eb44d 100644 (file)
--- a/common.go
+++ b/common.go
@@ -2,6 +2,6 @@
 package sgblog
 
 const (
-       Version = "0.5.0"
+       Version = "0.6.0"
        WhenFmt = "2006-01-02 15:04:05Z07:00"
 )