]> Sergey Matveev's repositories - sgblog.git/blob - cmd/sgblog/gemini.go
527874eae26488228777311f4369c0745da12d73
[sgblog.git] / cmd / sgblog / gemini.go
1 /*
2 SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine
3 Copyright (C) 2020-2023 Sergey Matveev <stargrave@stargrave.org>
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU Affero General Public License as
7 published by the Free Software Foundation, version 3 of the License.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU Affero General Public License
15 along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 package main
19
20 import (
21         "bufio"
22         _ "embed"
23         "errors"
24         "fmt"
25         "io"
26         "log"
27         "net/url"
28         "os"
29         "strconv"
30         "text/template"
31
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"
37 )
38
39 var (
40         //go:embed gemini-menu.tmpl
41         TmplGemMenuRaw string
42         //go:embed gemini-entry.tmpl
43         TmplGemEntryRaw string
44
45         TmplGemMenu  = template.Must(template.New("menu").Parse(TmplGemMenuRaw))
46         TmplGemEntry = template.Must(template.New("entry").Parse(TmplGemEntryRaw))
47 )
48
49 func makeGemErr(err error) {
50         fmt.Print("59 " + err.Error() + "\r\n")
51         log.Fatalln(err)
52 }
53
54 func serveGemini(cfgPath string) {
55         cfg, err := readCfg(cfgPath)
56         if err != nil {
57                 log.Fatalln(err)
58         }
59         initLocalizer(cfg.Lang)
60
61         headHash, err := initRepo(cfg)
62         if err != nil {
63                 log.Fatalln(err)
64         }
65
66         scanner := bufio.NewScanner(io.LimitReader(os.Stdin, 2+1<<10))
67         if !scanner.Scan() {
68                 makeGemErr(errors.New("no CRLF found"))
69         }
70         urlRaw := scanner.Text()
71         u, err := url.Parse(urlRaw)
72         if err != nil {
73                 makeGemErr(err)
74         }
75         if u.Scheme != "gemini" {
76                 makeGemErr(errors.New("only gemini:// is supported" + u.String()))
77         }
78
79         if u.Path == "/" {
80                 offset := 0
81                 if offsetRaw, exists := u.Query()["offset"]; exists {
82                         offset, err = strconv.Atoi(offsetRaw[0])
83                         if err != nil {
84                                 makeGemErr(err)
85                         }
86                 }
87                 repoLog, err := repo.Log(&git.LogOptions{From: *headHash})
88                 if err != nil {
89                         makeGemErr(err)
90                 }
91                 topicsCache, err := getTopicsCache(cfg, repoLog)
92                 if err != nil {
93                         makeGemErr(err)
94                 }
95                 repoLog, err = repo.Log(&git.LogOptions{From: *headHash})
96                 if err != nil {
97                         makeGemErr(err)
98                 }
99
100                 commitN := 0
101                 var commits CommitIterNext
102                 var topic string
103                 if t, exists := u.Query()["topic"]; exists {
104                         topic = t[0]
105                         hashes := topicsCache[topic]
106                         if hashes == nil {
107                                 makeGemErr(errors.New("no posts with that topic"))
108                         }
109                         if len(hashes) > offset {
110                                 hashes = hashes[offset:]
111                                 commitN += offset
112                         }
113                         commits = &HashesIter{hashes}
114                 } else {
115                         for i := 0; i < offset; i++ {
116                                 if _, err = repoLog.Next(); err != nil {
117                                         break
118                                 }
119                                 commitN++
120                         }
121                         commits = repoLog
122                 }
123
124                 logEnded := false
125                 entries := make([]TableMenuEntry, 0, PageEntries)
126                 for i := 0; i < PageEntries; i++ {
127                         commit, err := commits.Next()
128                         if err != nil {
129                                 logEnded = true
130                                 break
131                         }
132                         lines := msgSplit(commit.Message)
133                         entries = append(entries, TableMenuEntry{
134                                 Commit:    commit,
135                                 Title:     lines[0],
136                                 LinesNum:  len(lines) - 2,
137                                 ImagesNum: len(listImgs(cfg, commit.Hash)),
138                                 CommentsNum: len(sgblog.ParseComments(sgblog.GetNote(
139                                         repo, commentsTree, commit.Hash,
140                                 ))),
141                                 Topics: sgblog.ParseTopics(sgblog.GetNote(
142                                         repo, topicsTree, commit.Hash,
143                                 )),
144                         })
145                 }
146                 offsetPrev := offset - PageEntries
147                 if offsetPrev < 0 {
148                         offsetPrev = 0
149                 }
150                 err = TmplGemMenu.Execute(os.Stdout, struct {
151                         T          *spreak.Localizer
152                         Cfg        *Cfg
153                         Topic      string
154                         Offset     int
155                         OffsetPrev int
156                         OffsetNext int
157                         LogEnded   bool
158                         Entries    []TableMenuEntry
159                         Topics     []string
160                         Version    string
161                 }{
162                         T:          localizer,
163                         Cfg:        cfg,
164                         Topic:      topic,
165                         Offset:     offset,
166                         OffsetPrev: offsetPrev,
167                         OffsetNext: offset + PageEntries,
168                         LogEnded:   logEnded,
169                         Entries:    entries,
170                         Topics:     topicsCache.Topics(),
171                         Version:    sgblog.Version,
172                 })
173                 if err != nil {
174                         log.Fatalln(err)
175                 }
176         } else if sha1DigestRe.MatchString(u.Path[1:]) {
177                 commit, err := repo.CommitObject(plumbing.NewHash(u.Path[1:]))
178                 if err != nil {
179                         log.Fatalln(err)
180                 }
181                 title := msgSplit(commit.Message)[0]
182                 err = TmplGemEntry.Execute(os.Stdout, struct {
183                         T            *spreak.Localizer
184                         Title        string
185                         Commit       *object.Commit
186                         When         string
187                         Cfg          *Cfg
188                         Note         string
189                         Images       []Img
190                         Comments     []string
191                         Topics       []string
192                         Version      string
193                         TitleEscaped string
194                 }{
195                         T:        localizer,
196                         Title:    title,
197                         Commit:   commit,
198                         When:     commit.Author.When.Format(sgblog.WhenFmt),
199                         Cfg:      cfg,
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,
207                         )),
208                 })
209                 if err != nil {
210                         log.Fatalln(err)
211                 }
212         } else {
213                 makeGemErr(errors.New("unknown URL action"))
214         }
215 }