/* SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine Copyright (C) 2020-2022 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" "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 gemini-menu.tmpl TmplGemMenuRaw string //go:embed gemini-entry.tmpl TmplGemEntryRaw string TmplGemMenu = template.Must(template.New("menu").Parse(TmplGemMenuRaw)) TmplGemEntry = template.Must(template.New("entry").Parse(TmplGemEntryRaw)) ) 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) } initLocalizer(cfg.Lang) 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, )), }) } offsetPrev := offset - PageEntries if offsetPrev < 0 { offsetPrev = 0 } err = TmplGemMenu.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 if sha1DigestRe.MatchString(u.Path[1:]) { commit, err := repo.CommitObject(plumbing.NewHash(u.Path[1:])) if err != nil { log.Fatalln(err) } title := msgSplit(commit.Message)[0] err = TmplGemEntry.Execute(os.Stdout, struct { T *spreak.Localizer Title string Commit *object.Commit When string Cfg *Cfg Note string Comments []string Topics []string Version string TitleEscaped string }{ T: localizer, 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")) } }