/* SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine Copyright (C) 2020-2021 Sergey Matveev 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 . */ 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")) } }