/* SGBlog -- Git-backed CGI/inetd blogging/phlogging engine Copyright (C) 2020 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/hjson/hjson-go" "go.stargrave.org/sgblog" "gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4/plumbing" "gopkg.in/src-d/go-git.v4/plumbing/object" ) const ( TmplGopherMenu = `{{$Cfg := .Cfg}}{{$CR := .CR}}i{{.Cfg.Title}} ({{.Offset}}-{{.OffsetNext}}) err {{.Cfg.GopherDomain}} 70{{.CR}} {{if .Cfg.AboutURL}}hAbout URL:{{.Cfg.AboutURL}} {{.Cfg.GopherDomain}} 70{{.CR}}{{end}} {{if .Offset}}1Prev offset/{{.OffsetPrev}} {{.Cfg.GopherDomain}} 70{{.CR}}{{end}} {{if not .LogEnded}}1Next offset/{{.OffsetNext}} {{.Cfg.GopherDomain}} 70{{.CR}}{{end -}} {{- $yearPrev := 0}}{{$monthPrev := 0}}{{$dayPrev := 0 -}} {{- range .Entries }}{{$yearCur := .Commit.Author.When.Year}}{{$monthCur := .Commit.Author.When.Month}}{{$dayCur := .Commit.Author.When.Day -}} {{- if or (ne $dayCur $dayPrev) (ne $monthCur $monthPrev) (ne $yearCur $yearPrev)}}{{$dayPrev = $dayCur}}{{$monthPrev = $monthCur}}{{$yearPrev = $yearCur}} i{{$yearCur | printf "%04d"}}-{{$monthCur | printf "%02d"}}-{{$dayCur | printf "%02d"}} err {{$Cfg.GopherDomain}} 70{{$CR}}{{end}} 0[{{.Commit.Author.When.Hour | printf "%02d"}}:{{.Commit.Author.When.Minute | printf "%02d"}}] {{.Title}} ({{.LinesNum}}L){{if .CommentsNum}} ({{.CommentsNum}}C){{end}} /{{.Commit.Hash.String}} {{$Cfg.GopherDomain}} 70{{$CR}}{{end}} iGenerated by: SGBlog {{.Version}} err {{.Cfg.GopherDomain}} 70{{.CR}} .{{.CR}} ` TmplGopherEntry = `What: {{.Commit.Hash.String}} When: {{.When}} ------------------------------------------------------------------------ {{.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 } 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" } if strings.HasPrefix(selector, "offset/") { offset, err := strconv.Atoi(selector[len("offset/"):]) if err != nil { log.Fatalln(err) } repoLog, err := repo.Log(&git.LogOptions{From: *headHash}) if err != nil { log.Fatalln(err) } for i := 0; i < offset; i++ { if _, err = repoLog.Next(); err != nil { break } } logEnded := false entries := make([]TableMenuEntry, 0, PageEntries) for i := 0; i < PageEntries; i++ { commit, err := repoLog.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))), }) } tmpl := template.Must(template.New("menu").Parse(TmplGopherMenu)) err = tmpl.Execute(os.Stdout, struct { Cfg *Cfg Offset int OffsetPrev int OffsetNext int LogEnded bool Entries []TableMenuEntry Version string CR string }{ Cfg: cfg, Offset: offset, OffsetPrev: offset - PageEntries, OffsetNext: offset + PageEntries, LogEnded: logEnded, Entries: entries, Version: sgblog.Version, CR: "\r", }) if err != nil { log.Fatalln(err) } } else 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 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)), Version: sgblog.Version, }) if err != nil { log.Fatalln(err) } } else { log.Fatalln(errors.New("unknown selector")) } }