]> Sergey Matveev's repositories - sgblog.git/commitdiff
Experimental gemini support v0.21.0
authorSergey Matveev <stargrave@stargrave.org>
Tue, 14 Sep 2021 19:28:55 +0000 (22:28 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Sat, 18 Sep 2021 08:18:58 +0000 (11:18 +0300)
README.texi
cmd/sgblog-comment-add/mail.go
cmd/sgblog-comment-add/main.go
cmd/sgblog-topics/main.go
cmd/sgblog/gemini.go [new file with mode: 0644]
cmd/sgblog/gopher.go
cmd/sgblog/http.go
cmd/sgblog/main.go
cmd/sgblog/topics.go
common.go

index adc60739a1a2e92c91f45883214eb0e599381c4a..9409b09a938bbb3096e13d7946b15110a10f4741 100644 (file)
@@ -9,15 +9,16 @@ Copyright @copyright{} 2020 @email{stargrave@@stargrave.org, Sergey Matveev}
 @node Top
 @top SGBlog
 
-SGBlog is minimalistic and simple Git-backed CGI/inetd
-@url{https://en.wikipedia.org/wiki/Blog, blogging} and
-@url{https://en.wikipedia.org/wiki/Phlog, phlogging} engine
+SGBlog is minimalistic and simple Git-backed CGI/UCSPI
+@url{https://en.wikipedia.org/wiki/Blog, blogging} (@code{http://}/@code{https://}),
+@url{https://en.wikipedia.org/wiki/Phlog, phlogging} (@code{gopher://}) and
+gemlogging (@code{gemini://}) engine
 with email-backed comments support, written on @url{https://golang.org/, Go}.
 
 Its main competitive features:
 
 @itemize
-@item Single binary, responsible for both blog and phlog
+@item Single binary, responsible for both blog, phlog and gemlog
 @item @url{https://git-scm.com/, Git} DVCS as a storage for posts and comments
 @item Single small @url{https://hjson.github.io/, Hjson} configuration file
 @item Uses @url{https://en.wikipedia.org/wiki/Common_Gateway_Interface, CGI}
@@ -32,10 +33,10 @@ Its main competitive features:
     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!
+    copy of your blog/phlog/gemlog!
 @end itemize
 
-All of that, except for comments, topics and phlog, could be achieved
+All of that, except for comments, topics and phlog/gemlog, could be achieved
 with some Git viewer like @url{https://git.zx2c4.com/cgit/about/, cgit}.
 But SGBlog also is able to:
 
@@ -183,6 +184,15 @@ EOF
 # mv /var/service/.phlog-ipv6 /var/service/phlog-ipv6
 @end example
 
+Gemlog uses Gemini protocol that requires TLS usage, that can be
+achieved with @url{go.cypherpunks.ru/ucspi} tools:
+
+@example
+exec tcpserver -DRH -u $uid -g $gid -l 0 ::0 1965 \
+  tlss -key gemlog.key.pem -cert gemlog.pem \
+  sgblog -gemini /home/sgblog/gemlog.hjson 2>&1
+@end example
+
 For comments workability you have to configure your SMTP server to feed
 incoming messages to @command{sgblog-comment-add} utility. For example,
 Postfix'es @file{/etc/aliases} can contain:
index 7b804c19d61d63ae28a29ca09f76f18c3e3cbc95..3ffc1e84ac83df06c6cf52445984ff4a8953b325 100644 (file)
@@ -1,5 +1,5 @@
 /*
-SGBlog -- Git-backed CGI/inetd blogging/phlogging engine
+SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine
 Copyright (C) 2020-2021 Sergey Matveev <stargrave@stargrave.org>
 
 This program is free software: you can redistribute it and/or modify
index e632ca68cf7f1cad29f746793d410cd1ea2802d0..a1e72077009824d956754fc81cf3c7883e5018c5 100644 (file)
@@ -1,5 +1,5 @@
 /*
-SGBlog -- Git-backed CGI/inetd blogging/phlogging engine
+SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine
 Copyright (C) 2020-2021 Sergey Matveev <stargrave@stargrave.org>
 
 This program is free software: you can redistribute it and/or modify
@@ -15,7 +15,7 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-// Git-backed CGI/inetd blogging/phlogging engine
+// Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine
 package main
 
 import (
index 121380ffc9884a1f0fa51f92d7f079837923fd86..f936461f247bd9dd0023bf1ddf72c098f5bf8faa 100644 (file)
@@ -1,5 +1,5 @@
 /*
-SGBlog -- Git-backed CGI/inetd blogging/phlogging engine
+SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine
 Copyright (C) 2020-2021 Sergey Matveev <stargrave@stargrave.org>
 
 This program is free software: you can redistribute it and/or modify
@@ -15,7 +15,7 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-// Git-backed CGI/inetd blogging/phlogging engine
+// Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine
 package main
 
 import (
diff --git a/cmd/sgblog/gemini.go b/cmd/sgblog/gemini.go
new file mode 100644 (file)
index 0000000..af95e67
--- /dev/null
@@ -0,0 +1,241 @@
+/*
+SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine
+Copyright (C) 2020-2021 Sergey Matveev <stargrave@stargrave.org>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, version 3 of the License.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+package main
+
+import (
+       "bufio"
+       "errors"
+       "fmt"
+       "io"
+       "log"
+       "net/url"
+       "os"
+       "strconv"
+       "text/template"
+
+       "github.com/go-git/go-git/v5"
+       "github.com/go-git/go-git/v5/plumbing"
+       "github.com/go-git/go-git/v5/plumbing/object"
+       "go.stargrave.org/sgblog"
+)
+
+const (
+       TmplGemMenu = `{{$CR := printf "\r"}}20 text/gemini{{$CR}}
+# {{.Cfg.Title}} {{if .Topic}}(topic: {{.Topic}}) {{end}}({{.Offset}}-{{.OffsetNext}})
+{{if .Cfg.AboutURL}}=> {{.Cfg.AboutURL}} About{{end}}
+{{if .Offset}}=> /?offset={{.OffsetPrev}}{{if .Topic}}&topic={{.Topic}}{{end}} Prev{{end}}
+{{if not .LogEnded}}=> /?offset={{.OffsetNext}}{{if .Topic}}&topic={{.Topic}}{{end}} Next{{end}}
+
+{{$datePrev := "0001-01-01" -}}
+{{- range .Entries -}}
+{{- $dateCur := .Commit.Author.When.Format "2006-01-02" -}}
+{{- if ne $dateCur $datePrev}}{{$datePrev = $dateCur}}## {{$dateCur}}
+{{end -}}
+=> /{{.Commit.Hash.String}} [{{.Commit.Author.When.Format "15:04"}}] {{.Title}} ({{.LinesNum}}L){{with .CommentsNum}} ({{.}}C){{end}}{{if .Topics}}{{range .Topics}} {{.}}{{end}}{{end}}
+{{end}}
+
+{{range .Topics -}}=> /?topic={{.}} Topic: {{.}}
+{{end}}
+Generated by: SGBlog {{.Version}}
+`
+       TmplGemEntry = `{{$CR := printf "\r"}}20 text/gemini{{$CR}}
+# {{.Title}}
+What: {{.Commit.Hash.String}}
+When: {{.When}}
+{{if .Topics}}Topics:{{range .Topics}} {{.}}{{end}}{{end}}
+` + "```" + `
+{{.Commit.Message}}` + "```" + `
+{{- if .Note}}
+## Note:
+` + "```" + `
+{{.Note}}
+` + "```" + `
+{{end -}}
+{{- if .Cfg.CommentsEmail}}
+=> mailto:{{.Cfg.CommentsEmail}}?subject={{.TitleEscaped}} leave comment
+{{end}}{{range $idx, $comment := .Comments}}
+## comment {{$idx}}:
+` + "```" + `
+{{$comment}}
+` + "```" + `
+{{end}}
+Generated by: SGBlog {{.Version}}
+`
+)
+
+func makeGemErr(err error) {
+       fmt.Print("59 " + err.Error() + "\r\n")
+       log.Fatalln(err)
+}
+
+func serveGemini(cfgPath string) {
+       cfg, err := readCfg(cfgPath)
+       if err != nil {
+               log.Fatalln(err)
+       }
+
+       headHash, err := initRepo(cfg)
+       if err != nil {
+               log.Fatalln(err)
+       }
+
+       scanner := bufio.NewScanner(io.LimitReader(os.Stdin, 2+1<<10))
+       if !scanner.Scan() {
+               makeGemErr(errors.New("no CRLF found"))
+       }
+       urlRaw := scanner.Text()
+       u, err := url.Parse(urlRaw)
+       if err != nil {
+               makeGemErr(err)
+       }
+       if u.Scheme != "gemini" {
+               makeGemErr(errors.New("only gemini:// is supported" + u.String()))
+       }
+
+       if u.Path == "/" {
+               offset := 0
+               if offsetRaw, exists := u.Query()["offset"]; exists {
+                       offset, err = strconv.Atoi(offsetRaw[0])
+                       if err != nil {
+                               makeGemErr(err)
+                       }
+               }
+               repoLog, err := repo.Log(&git.LogOptions{From: *headHash})
+               if err != nil {
+                       makeGemErr(err)
+               }
+               topicsCache, err := getTopicsCache(cfg, repoLog)
+               if err != nil {
+                       makeGemErr(err)
+               }
+               repoLog, err = repo.Log(&git.LogOptions{From: *headHash})
+               if err != nil {
+                       makeGemErr(err)
+               }
+
+               commitN := 0
+               var commits CommitIterNext
+               var topic string
+               if t, exists := u.Query()["topic"]; exists {
+                       topic = t[0]
+                       hashes := topicsCache[topic]
+                       if hashes == nil {
+                               makeGemErr(errors.New("no posts with that topic"))
+                       }
+                       if len(hashes) > offset {
+                               hashes = hashes[offset:]
+                               commitN += offset
+                       }
+                       commits = &HashesIter{hashes}
+               } else {
+                       for i := 0; i < offset; i++ {
+                               if _, err = repoLog.Next(); err != nil {
+                                       break
+                               }
+                               commitN++
+                       }
+                       commits = repoLog
+               }
+
+               logEnded := false
+               entries := make([]TableMenuEntry, 0, PageEntries)
+               for i := 0; i < PageEntries; i++ {
+                       commit, err := commits.Next()
+                       if err != nil {
+                               logEnded = true
+                               break
+                       }
+                       lines := msgSplit(commit.Message)
+                       entries = append(entries, TableMenuEntry{
+                               Commit:   commit,
+                               Title:    lines[0],
+                               LinesNum: len(lines) - 2,
+                               CommentsNum: len(sgblog.ParseComments(sgblog.GetNote(
+                                       repo, commentsTree, commit.Hash,
+                               ))),
+                               Topics: sgblog.ParseTopics(sgblog.GetNote(
+                                       repo, topicsTree, commit.Hash,
+                               )),
+                       })
+               }
+               tmpl := template.Must(template.New("menu").Parse(TmplGemMenu))
+               offsetPrev := offset - PageEntries
+               if offsetPrev < 0 {
+                       offsetPrev = 0
+               }
+               err = tmpl.Execute(os.Stdout, struct {
+                       Cfg        *Cfg
+                       Topic      string
+                       Offset     int
+                       OffsetPrev int
+                       OffsetNext int
+                       LogEnded   bool
+                       Entries    []TableMenuEntry
+                       Topics     []string
+                       Version    string
+               }{
+                       Cfg:        cfg,
+                       Topic:      topic,
+                       Offset:     offset,
+                       OffsetPrev: offsetPrev,
+                       OffsetNext: offset + PageEntries,
+                       LogEnded:   logEnded,
+                       Entries:    entries,
+                       Topics:     topicsCache.Topics(),
+                       Version:    sgblog.Version,
+               })
+               if err != nil {
+                       log.Fatalln(err)
+               }
+       } else if sha1DigestRe.MatchString(u.Path[1:]) {
+               commit, err := repo.CommitObject(plumbing.NewHash(u.Path[1:]))
+               if err != nil {
+                       log.Fatalln(err)
+               }
+               tmpl := template.Must(template.New("entry").Parse(TmplGemEntry))
+               title := msgSplit(commit.Message)[0]
+               err = tmpl.Execute(os.Stdout, struct {
+                       Title        string
+                       Commit       *object.Commit
+                       When         string
+                       Cfg          *Cfg
+                       Note         string
+                       Comments     []string
+                       Topics       []string
+                       Version      string
+                       TitleEscaped string
+               }{
+                       Title:    title,
+                       Commit:   commit,
+                       When:     commit.Author.When.Format(sgblog.WhenFmt),
+                       Cfg:      cfg,
+                       Note:     string(sgblog.GetNote(repo, notesTree, commit.Hash)),
+                       Comments: sgblog.ParseComments(sgblog.GetNote(repo, commentsTree, commit.Hash)),
+                       Topics:   sgblog.ParseTopics(sgblog.GetNote(repo, topicsTree, commit.Hash)),
+                       Version:  sgblog.Version,
+                       TitleEscaped: url.PathEscape(fmt.Sprintf(
+                               "Re: %s (%s)", title, commit.Hash,
+                       )),
+               })
+               if err != nil {
+                       log.Fatalln(err)
+               }
+       } else {
+               makeGemErr(errors.New("unknown URL action"))
+       }
+}
index 78c09878f492225d54f0d24b56350a18836d387c..27ad8845167c2a311a26ee29dc5f093b7a2a8d52 100644 (file)
@@ -1,5 +1,5 @@
 /*
-SGBlog -- Git-backed CGI/inetd blogging/phlogging engine
+SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine
 Copyright (C) 2020-2021 Sergey Matveev <stargrave@stargrave.org>
 
 This program is free software: you can redistribute it and/or modify
index 9ee50ce77a86fe78773def1b6e96eb74ac67ce03..f1e7404b81da51558c08b824971bbca6e6c00a03 100644 (file)
@@ -1,5 +1,5 @@
 /*
-SGBlog -- Git-backed CGI/inetd blogging/phlogging engine
+SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine
 Copyright (C) 2020-2021 Sergey Matveev <stargrave@stargrave.org>
 
 This program is free software: you can redistribute it and/or modify
index ef3ff45d0128cf97c112c994f222e8e76a21553a..8dbb1d8156ef1d03c63cda655562bc79e978ad9d 100644 (file)
@@ -1,5 +1,5 @@
 /*
-SGBlog -- Git-backed CGI/inetd blogging/phlogging engine
+SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine
 Copyright (C) 2020-2021 Sergey Matveev <stargrave@stargrave.org>
 
 This program is free software: you can redistribute it and/or modify
@@ -15,7 +15,7 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-// Git-backed CGI/inetd blogging/phlogging engine
+// Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine
 package main
 
 import (
@@ -24,6 +24,7 @@ import (
        "flag"
        "fmt"
        "io/ioutil"
+       "log"
        "regexp"
        "strings"
 
@@ -146,15 +147,20 @@ func readCfg(cfgPath string) (*Cfg, error) {
 
 func main() {
        gopherCfgPath := flag.String("gopher", "", "Path to gopher-related configuration file")
+       geminiCfgPath := flag.String("gemini", "", "Path to gemini-related configuration file")
        flag.Usage = func() {
                fmt.Fprintf(flag.CommandLine.Output(), `Usage of sgblog:
        sgblog -- run CGI HTTP backend
        sgblog -gopher /path/to/cfg.hjson -- run UCSPI/inetd Gopher backend
+       sgblog -gemini /path/to/cfg.hjson -- run UCSPI+tlss Gemini backend
 `)
        }
        flag.Parse()
+       log.SetFlags(log.Lshortfile)
        if *gopherCfgPath != "" {
                serveGopher(*gopherCfgPath)
+       } else if *geminiCfgPath != "" {
+               serveGemini(*geminiCfgPath)
        } else {
                serveHTTP()
        }
index 2ab6328f930828e10a0a3b0c93911ebb37b40315..9a8979376fce2c09ac3fcafa0d454b4fea33a0ad 100644 (file)
@@ -1,5 +1,5 @@
 /*
-SGBlog -- Git-backed CGI/inetd blogging/phlogging engine
+SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine
 Copyright (C) 2020-2021 Sergey Matveev <stargrave@stargrave.org>
 
 This program is free software: you can redistribute it and/or modify
index 90adb7a75495b888ae6799b0fec7f1c5431c47b2..4e0179f61d84a272165365610e6c8d3d95cea482 100644 (file)
--- a/common.go
+++ b/common.go
@@ -1,4 +1,4 @@
-// SGBlog -- Git-backed CGI/inetd blogging/phlogging engine
+// SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine
 package sgblog
 
 import (
@@ -15,7 +15,7 @@ import (
 )
 
 const (
-       Version = "0.20.2"
+       Version = "0.21.0"
        WhenFmt = "2006-01-02 15:04:05Z07:00"
 )