1 // SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine
2 // Copyright (C) 2020-2024 Sergey Matveev <stargrave@stargrave.org>
4 // This program is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU Affero General Public License as
6 // published by the Free Software Foundation, version 3 of the License.
8 // This program is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 // GNU General Public License for more details.
13 // You should have received a copy of the GNU Affero General Public License
14 // along with this program. If not, see <http://www.gnu.org/licenses/>.
32 "github.com/go-git/go-git/v5"
33 "github.com/go-git/go-git/v5/plumbing"
34 "github.com/go-git/go-git/v5/plumbing/object"
35 "github.com/vorlif/spreak"
36 "go.stargrave.org/sgblog"
40 //go:embed gemini-menu.tmpl
42 //go:embed gemini-entry.tmpl
43 TmplGemEntryRaw string
45 TmplGemMenu = template.Must(template.New("menu").Parse(TmplGemMenuRaw))
46 TmplGemEntry = template.Must(template.New("entry").Parse(TmplGemEntryRaw))
49 func makeGemErr(err error) {
50 fmt.Print("59 " + err.Error() + "\r\n")
54 func serveGemini(cfgPath string) {
55 cfg, err := readCfg(cfgPath)
59 initLocalizer(cfg.Lang)
61 headHash, err := initRepo(cfg)
66 scanner := bufio.NewScanner(io.LimitReader(os.Stdin, 2+1<<10))
68 makeGemErr(errors.New("no CRLF found"))
70 urlRaw := scanner.Text()
71 u, err := url.Parse(urlRaw)
75 if u.Scheme != "gemini" {
76 makeGemErr(errors.New("only gemini:// is supported" + u.String()))
81 if offsetRaw, exists := u.Query()["offset"]; exists {
82 offset, err = strconv.Atoi(offsetRaw[0])
87 repoLog, err := repo.Log(&git.LogOptions{From: *headHash})
91 topicsCache, err := getTopicsCache(cfg, repoLog)
95 repoLog, err = repo.Log(&git.LogOptions{From: *headHash})
101 var commits CommitIterNext
103 if t, exists := u.Query()["topic"]; exists {
105 hashes := topicsCache[topic]
107 makeGemErr(errors.New("no posts with that topic"))
109 if len(hashes) > offset {
110 hashes = hashes[offset:]
113 commits = &HashesIter{hashes}
115 for i := 0; i < offset; i++ {
116 if _, err = repoLog.Next(); err != nil {
125 entries := make([]TableMenuEntry, 0, PageEntries)
126 for i := 0; i < PageEntries; i++ {
127 commit, err := commits.Next()
132 lines := msgSplit(commit.Message)
133 entries = append(entries, TableMenuEntry{
136 LinesNum: len(lines) - 2,
137 ImagesNum: len(listImgs(cfg, commit.Hash)),
138 CommentsNum: len(sgblog.ParseComments(sgblog.GetNote(
139 repo, commentsTree, commit.Hash,
141 Topics: sgblog.ParseTopics(sgblog.GetNote(
142 repo, topicsTree, commit.Hash,
146 offsetPrev := offset - PageEntries
150 err = TmplGemMenu.Execute(os.Stdout, struct {
158 Entries []TableMenuEntry
166 OffsetPrev: offsetPrev,
167 OffsetNext: offset + PageEntries,
170 Topics: topicsCache.Topics(),
171 Version: sgblog.Version,
176 } else if sha1DigestRe.MatchString(u.Path[1:]) {
177 commit, err := repo.CommitObject(plumbing.NewHash(u.Path[1:]))
181 title := msgSplit(commit.Message)[0]
182 err = TmplGemEntry.Execute(os.Stdout, struct {
185 Commit *object.Commit
198 When: commit.Author.When.Format(sgblog.WhenFmt),
200 Note: string(sgblog.GetNote(repo, notesTree, commit.Hash)),
201 Images: listImgs(cfg, commit.Hash),
202 Comments: sgblog.ParseComments(sgblog.GetNote(repo, commentsTree, commit.Hash)),
203 Topics: sgblog.ParseTopics(sgblog.GetNote(repo, topicsTree, commit.Hash)),
204 Version: sgblog.Version,
205 TitleEscaped: url.PathEscape(fmt.Sprintf(
206 "Re: %s (%s)", title, commit.Hash,
212 } else if strings.HasPrefix(u.Path, "/img/") {
213 pth := strings.TrimPrefix(u.Path, "/img/")
214 if strings.Contains(pth, "..") {
215 log.Fatalln("unacceptable double dots")
217 typ := ImgTypes[path.Ext(pth)]
219 typ = "application/octet-stream"
221 fd, err := os.Open(path.Join(cfg.ImgPath, pth))
225 bw := bufio.NewWriter(os.Stdout)
226 bw.Write([]byte("20 " + typ + "\r\n"))
227 io.Copy(bw, bufio.NewReader(fd))
231 makeGemErr(errors.New("unknown URL action"))