]> Sergey Matveev's repositories - sgblog.git/blob - cmd/sgblog/gemini.go
Raised copyright years
[sgblog.git] / cmd / sgblog / gemini.go
1 /*
2 SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine
3 Copyright (C) 2020-2022 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         "go.stargrave.org/sgblog"
36 )
37
38 var (
39         //go:embed gemini-menu.tmpl
40         TmplGemMenuRaw string
41         //go:embed gemini-entry.tmpl
42         TmplGemEntryRaw string
43
44         TmplGemMenu  = template.Must(template.New("menu").Parse(TmplGemMenuRaw))
45         TmplGemEntry = template.Must(template.New("entry").Parse(TmplGemEntryRaw))
46 )
47
48 func makeGemErr(err error) {
49         fmt.Print("59 " + err.Error() + "\r\n")
50         log.Fatalln(err)
51 }
52
53 func serveGemini(cfgPath string) {
54         cfg, err := readCfg(cfgPath)
55         if err != nil {
56                 log.Fatalln(err)
57         }
58
59         headHash, err := initRepo(cfg)
60         if err != nil {
61                 log.Fatalln(err)
62         }
63
64         scanner := bufio.NewScanner(io.LimitReader(os.Stdin, 2+1<<10))
65         if !scanner.Scan() {
66                 makeGemErr(errors.New("no CRLF found"))
67         }
68         urlRaw := scanner.Text()
69         u, err := url.Parse(urlRaw)
70         if err != nil {
71                 makeGemErr(err)
72         }
73         if u.Scheme != "gemini" {
74                 makeGemErr(errors.New("only gemini:// is supported" + u.String()))
75         }
76
77         if u.Path == "/" {
78                 offset := 0
79                 if offsetRaw, exists := u.Query()["offset"]; exists {
80                         offset, err = strconv.Atoi(offsetRaw[0])
81                         if err != nil {
82                                 makeGemErr(err)
83                         }
84                 }
85                 repoLog, err := repo.Log(&git.LogOptions{From: *headHash})
86                 if err != nil {
87                         makeGemErr(err)
88                 }
89                 topicsCache, err := getTopicsCache(cfg, repoLog)
90                 if err != nil {
91                         makeGemErr(err)
92                 }
93                 repoLog, err = repo.Log(&git.LogOptions{From: *headHash})
94                 if err != nil {
95                         makeGemErr(err)
96                 }
97
98                 commitN := 0
99                 var commits CommitIterNext
100                 var topic string
101                 if t, exists := u.Query()["topic"]; exists {
102                         topic = t[0]
103                         hashes := topicsCache[topic]
104                         if hashes == nil {
105                                 makeGemErr(errors.New("no posts with that topic"))
106                         }
107                         if len(hashes) > offset {
108                                 hashes = hashes[offset:]
109                                 commitN += offset
110                         }
111                         commits = &HashesIter{hashes}
112                 } else {
113                         for i := 0; i < offset; i++ {
114                                 if _, err = repoLog.Next(); err != nil {
115                                         break
116                                 }
117                                 commitN++
118                         }
119                         commits = repoLog
120                 }
121
122                 logEnded := false
123                 entries := make([]TableMenuEntry, 0, PageEntries)
124                 for i := 0; i < PageEntries; i++ {
125                         commit, err := commits.Next()
126                         if err != nil {
127                                 logEnded = true
128                                 break
129                         }
130                         lines := msgSplit(commit.Message)
131                         entries = append(entries, TableMenuEntry{
132                                 Commit:   commit,
133                                 Title:    lines[0],
134                                 LinesNum: len(lines) - 2,
135                                 CommentsNum: len(sgblog.ParseComments(sgblog.GetNote(
136                                         repo, commentsTree, commit.Hash,
137                                 ))),
138                                 Topics: sgblog.ParseTopics(sgblog.GetNote(
139                                         repo, topicsTree, commit.Hash,
140                                 )),
141                         })
142                 }
143                 offsetPrev := offset - PageEntries
144                 if offsetPrev < 0 {
145                         offsetPrev = 0
146                 }
147                 err = TmplGemMenu.Execute(os.Stdout, struct {
148                         Cfg        *Cfg
149                         Topic      string
150                         Offset     int
151                         OffsetPrev int
152                         OffsetNext int
153                         LogEnded   bool
154                         Entries    []TableMenuEntry
155                         Topics     []string
156                         Version    string
157                 }{
158                         Cfg:        cfg,
159                         Topic:      topic,
160                         Offset:     offset,
161                         OffsetPrev: offsetPrev,
162                         OffsetNext: offset + PageEntries,
163                         LogEnded:   logEnded,
164                         Entries:    entries,
165                         Topics:     topicsCache.Topics(),
166                         Version:    sgblog.Version,
167                 })
168                 if err != nil {
169                         log.Fatalln(err)
170                 }
171         } else if sha1DigestRe.MatchString(u.Path[1:]) {
172                 commit, err := repo.CommitObject(plumbing.NewHash(u.Path[1:]))
173                 if err != nil {
174                         log.Fatalln(err)
175                 }
176                 title := msgSplit(commit.Message)[0]
177                 err = TmplGemEntry.Execute(os.Stdout, struct {
178                         Title        string
179                         Commit       *object.Commit
180                         When         string
181                         Cfg          *Cfg
182                         Note         string
183                         Comments     []string
184                         Topics       []string
185                         Version      string
186                         TitleEscaped string
187                 }{
188                         Title:    title,
189                         Commit:   commit,
190                         When:     commit.Author.When.Format(sgblog.WhenFmt),
191                         Cfg:      cfg,
192                         Note:     string(sgblog.GetNote(repo, notesTree, commit.Hash)),
193                         Comments: sgblog.ParseComments(sgblog.GetNote(repo, commentsTree, commit.Hash)),
194                         Topics:   sgblog.ParseTopics(sgblog.GetNote(repo, topicsTree, commit.Hash)),
195                         Version:  sgblog.Version,
196                         TitleEscaped: url.PathEscape(fmt.Sprintf(
197                                 "Re: %s (%s)", title, commit.Hash,
198                         )),
199                 })
200                 if err != nil {
201                         log.Fatalln(err)
202                 }
203         } else {
204                 makeGemErr(errors.New("unknown URL action"))
205         }
206 }