/* SGBlog -- Git-backed CGI/inetd blogging/phlogging 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" "encoding/json" "errors" "fmt" "io" "io/ioutil" "log" "os" "strconv" "strings" "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" "github.com/hjson/hjson-go" "go.stargrave.org/sgblog" ) const ( TmplGopherMenu = `{{$CR := printf "\r"}}{{$CRLF := printf "\r\n" -}} {{- define "domainPort" }} {{.GopherDomain}} 70{{end}}{{$Cfg := .Cfg -}} i{{.Cfg.Title}} {{if .Topic}}(topic: {{.Topic}}) {{end}}({{.Offset}}-{{.OffsetNext}}) err{{template "domainPort" .Cfg}}{{$CRLF -}} {{- if .Cfg.AboutURL}}hAbout URL:{{.Cfg.AboutURL}}{{template "domainPort" .Cfg}}{{$CRLF}}{{end -}} {{- if .Offset}}1Prev {{if .Topic}}{{.Topic}}/{{end}}offset/{{.OffsetPrev}}{{template "domainPort" .Cfg}}{{$CRLF}}{{end -}} {{- if not .LogEnded}}1Next {{if .Topic}}{{.Topic}}/{{end}}offset/{{.OffsetNext}}{{template "domainPort" .Cfg}}{{$CRLF}}{{end -}} {{- $datePrev := "0001-01-01" -}} {{- range .Entries -}} {{- $dateCur := .Commit.Author.When.Format "2006-01-02" -}} {{- if ne $dateCur $datePrev}}{{$datePrev = $dateCur}} i{{$dateCur}} err{{template "domainPort" $Cfg}}{{$CR}}{{end}} 0[{{.Commit.Author.When.Format "15:04"}}] {{.Title}} ({{.LinesNum}}L){{with .CommentsNum}} ({{.}}C){{end}}{{if .Topics}}{{range .Topics}} {{.}}{{end}}{{end}} /{{.Commit.Hash.String}}{{template "domainPort" $Cfg}}{{$CR}}{{end}} {{range .Topics}} 1Topic: {{.}} {{.}}/offset/0{{template "domainPort" $Cfg}}{{$CR}}{{end}} iGenerated by: SGBlog {{.Version}} err{{template "domainPort" .Cfg}}{{$CR}} .{{$CRLF}}` TmplGopherEntry = `What: {{.Commit.Hash.String}} When: {{.When}} ------------------------------------------------------------------------ {{if .Topics}}Topics:{{range .Topics}} {{.}}{{end}}{{end}} ------------------------------------------------------------------------ {{.Commit.Message -}} {{- if .Note}} ------------------------------------------------------------------------ Note: {{.Note}}{{end -}} {{- if .Cfg.CommentsEmail}} ------------------------------------------------------------------------ leave comment: mailto:{{.Cfg.CommentsEmail}}?subject={{.Commit.Hash.String}} {{end}}{{range $idx, $comment := .Comments}} ------------------------------------------------------------------------ comment {{$idx}}: {{$comment}} {{end}} ------------------------------------------------------------------------ Generated by: SGBlog {{.Version}} ` ) type TableMenuEntry struct { Commit *object.Commit Title string LinesNum int CommentsNum int Topics []string } func serveGopher() { cfgPath := os.Args[2] cfgRaw, err := ioutil.ReadFile(cfgPath) if err != nil { log.Fatalln(err) } var cfgGeneral map[string]interface{} if err = hjson.Unmarshal(cfgRaw, &cfgGeneral); err != nil { log.Fatalln(err) } cfgRaw, err = json.Marshal(cfgGeneral) if err != nil { log.Fatalln(err) } var cfg *Cfg if err = json.Unmarshal(cfgRaw, &cfg); err != nil { log.Fatalln(err) } if cfg.GopherDomain == "" { log.Fatalln("GopherDomain is not configured") } headHash, err := initRepo(cfg) if err != nil { log.Fatalln(err) } scanner := bufio.NewScanner(io.LimitReader(os.Stdin, 1<<8)) if !scanner.Scan() { log.Fatalln(errors.New("no CRLF found")) } selector := scanner.Text() if selector == "" { selector = "offset/0" } selectorParts := strings.Split(selector, "/") if strings.HasPrefix(selector, "URL:") { selector = selector[len("URL:"):] fmt.Printf(` Redirect to non-gopher URL Redirecting to %s... `, selector, selector, selector) } else if sha1DigestRe.MatchString(selector) { commit, err := repo.CommitObject(plumbing.NewHash(selector[1:])) if err != nil { log.Fatalln(err) } tmpl := template.Must(template.New("entry").Parse(TmplGopherEntry)) err = tmpl.Execute(os.Stdout, struct { Commit *object.Commit When string Cfg *Cfg Note string Comments []string Topics []string Version string }{ Commit: commit, When: commit.Author.When.Format(sgblog.WhenFmt), Cfg: cfg, Note: string(getNote(notesTree, commit.Hash)), Comments: parseComments(getNote(commentsTree, commit.Hash)), Topics: parseTopics(getNote(topicsTree, commit.Hash)), Version: sgblog.Version, }) if err != nil { log.Fatalln(err) } } else if selectorParts[len(selectorParts)-2] == "offset" { offset, err := strconv.Atoi(selectorParts[len(selectorParts)-1]) if err != nil { log.Fatalln(err) } repoLog, err := repo.Log(&git.LogOptions{From: *headHash}) if err != nil { log.Fatalln(err) } topicsCache, err := getTopicsCache(cfg, repoLog) if err != nil { log.Fatalln(err) } repoLog, err = repo.Log(&git.LogOptions{From: *headHash}) if err != nil { log.Fatalln(err) } var topic string if len(selectorParts) == 3 { topic = selectorParts[0] } var commits CommitIterNext if topic == "" { for i := 0; i < offset; i++ { if _, err = repoLog.Next(); err != nil { break } } commits = repoLog } else { hashes := topicsCache[topic] if hashes == nil { log.Fatalln(errors.New("no posts with that topic")) } if len(hashes) > offset { hashes = hashes[offset:] } commits = &HashesIter{hashes} } 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(parseComments(getNote(commentsTree, commit.Hash))), Topics: parseTopics(getNote(topicsTree, commit.Hash)), }) } tmpl := template.Must(template.New("menu").Parse(TmplGopherMenu)) 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 { log.Fatalln(errors.New("unknown selector")) } }