// SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine // Copyright (C) 2020-2024 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" _ "embed" "errors" "fmt" "io" "log" "net/url" "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/vorlif/spreak" "go.stargrave.org/sgblog" ) var ( //go:embed gopher-menu.tmpl TmplGopherMenuRaw string //go:embed gopher-entry.tmpl TmplGopherEntryRaw string TmplGopherMenu = template.Must(template.New("gopher-menu").Parse(TmplGopherMenuRaw)) TmplGopherEntry = template.Must(template.New("gopher-entry").Parse(TmplGopherEntryRaw)) ) type TableMenuEntry struct { Commit *object.Commit Title string LinesNum int ImagesNum int CommentsNum int Topics []string } func serveGopher(cfgPath string) { cfg, err := readCfg(cfgPath) if err != nil { log.Fatalln(err) } if cfg.GopherDomain == "" { log.Fatalln("GopherDomain is not configured") } initLocalizer(cfg.Lang) 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) } err = TmplGopherEntry.Execute(os.Stdout, struct { T *spreak.Localizer Commit *object.Commit When string Cfg *Cfg Note string Images []Img Comments []string Topics []string Version string TitleEscaped string }{ T: localizer, Commit: commit, When: commit.Author.When.Format(sgblog.WhenFmt), Cfg: cfg, Note: string(sgblog.GetNote(repo, notesTree, commit.Hash)), Images: listImgs(cfg, 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)", msgSplit(commit.Message)[0], commit.Hash, )), }) if err != nil { log.Fatalln(err) } } else if len(selectorParts) > 1 && 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, ImagesNum: len(listImgs(cfg, commit.Hash)), CommentsNum: len(sgblog.ParseComments(sgblog.GetNote( repo, commentsTree, commit.Hash, ))), Topics: sgblog.ParseTopics(sgblog.GetNote( repo, topicsTree, commit.Hash, )), }) } offsetPrev := offset - PageEntries if offsetPrev < 0 { offsetPrev = 0 } err = TmplGopherMenu.Execute(os.Stdout, struct { T *spreak.Localizer Cfg *Cfg Topic string Offset int OffsetPrev int OffsetNext int LogEnded bool Entries []TableMenuEntry Topics []string Version string }{ T: localizer, 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")) } }