+/*
+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"))
+ }
+}