]> Sergey Matveev's repositories - sgblog.git/blob - cmd/sgblog/gopher.go
More compact Base64 ETag
[sgblog.git] / cmd / sgblog / gopher.go
1 // SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine
2 // Copyright (C) 2020-2024 Sergey Matveev <stargrave@stargrave.org>
3 //
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.
7 //
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.
12 //
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/>.
15
16 package main
17
18 import (
19         "bufio"
20         _ "embed"
21         "errors"
22         "fmt"
23         "io"
24         "log"
25         "net/url"
26         "os"
27         "strconv"
28         "strings"
29         "text/template"
30
31         "github.com/go-git/go-git/v5"
32         "github.com/go-git/go-git/v5/plumbing"
33         "github.com/go-git/go-git/v5/plumbing/object"
34         "github.com/vorlif/spreak"
35         "go.stargrave.org/sgblog"
36 )
37
38 var (
39         //go:embed gopher-menu.tmpl
40         TmplGopherMenuRaw string
41         //go:embed gopher-entry.tmpl
42         TmplGopherEntryRaw string
43
44         TmplGopherMenu  = template.Must(template.New("gopher-menu").Parse(TmplGopherMenuRaw))
45         TmplGopherEntry = template.Must(template.New("gopher-entry").Parse(TmplGopherEntryRaw))
46 )
47
48 type TableMenuEntry struct {
49         Commit      *object.Commit
50         Title       string
51         LinesNum    int
52         ImagesNum   int
53         CommentsNum int
54         Topics      []string
55 }
56
57 func serveGopher(cfgPath string) {
58         cfg, err := readCfg(cfgPath)
59         if err != nil {
60                 log.Fatalln(err)
61         }
62         if cfg.GopherDomain == "" {
63                 log.Fatalln("GopherDomain is not configured")
64         }
65         initLocalizer(cfg.Lang)
66
67         headHash, err := initRepo(cfg)
68         if err != nil {
69                 log.Fatalln(err)
70         }
71
72         scanner := bufio.NewScanner(io.LimitReader(os.Stdin, 1<<8))
73         if !scanner.Scan() {
74                 log.Fatalln(errors.New("no CRLF found"))
75         }
76         selector := scanner.Text()
77         if selector == "" {
78                 selector = "offset/0"
79         }
80         selectorParts := strings.Split(selector, "/")
81         if strings.HasPrefix(selector, "URL:") {
82                 selector = selector[len("URL:"):]
83                 fmt.Printf(`<!DOCTYPE html>
84 <html>
85 <head>
86         <meta http-equiv="Refresh" content="1; url=%s" />
87         <title>Redirect to non-gopher URL</title>
88 </head>
89 <body>
90 Redirecting to <a href="%s">%s</a>...
91 </body>
92 </html>
93 `, selector, selector, selector)
94         } else if sha1DigestRe.MatchString(selector) {
95                 commit, err := repo.CommitObject(plumbing.NewHash(selector[1:]))
96                 if err != nil {
97                         log.Fatalln(err)
98                 }
99                 err = TmplGopherEntry.Execute(os.Stdout, struct {
100                         T            *spreak.Localizer
101                         Commit       *object.Commit
102                         When         string
103                         Cfg          *Cfg
104                         Note         string
105                         Images       []Img
106                         Comments     []string
107                         Topics       []string
108                         Version      string
109                         TitleEscaped string
110                 }{
111                         T:        localizer,
112                         Commit:   commit,
113                         When:     commit.Author.When.Format(sgblog.WhenFmt),
114                         Cfg:      cfg,
115                         Note:     string(sgblog.GetNote(repo, notesTree, commit.Hash)),
116                         Images:   listImgs(cfg, commit.Hash),
117                         Comments: sgblog.ParseComments(sgblog.GetNote(repo, commentsTree, commit.Hash)),
118                         Topics:   sgblog.ParseTopics(sgblog.GetNote(repo, topicsTree, commit.Hash)),
119                         Version:  sgblog.Version,
120                         TitleEscaped: url.PathEscape(fmt.Sprintf(
121                                 "Re: %s (%s)", msgSplit(commit.Message)[0], commit.Hash,
122                         )),
123                 })
124                 if err != nil {
125                         log.Fatalln(err)
126                 }
127         } else if len(selectorParts) > 1 &&
128                 selectorParts[len(selectorParts)-2] == "offset" {
129                 offset, err := strconv.Atoi(selectorParts[len(selectorParts)-1])
130                 if err != nil {
131                         log.Fatalln(err)
132                 }
133                 repoLog, err := repo.Log(&git.LogOptions{From: *headHash})
134                 if err != nil {
135                         log.Fatalln(err)
136                 }
137                 topicsCache, err := getTopicsCache(cfg, repoLog)
138                 if err != nil {
139                         log.Fatalln(err)
140                 }
141                 repoLog, err = repo.Log(&git.LogOptions{From: *headHash})
142                 if err != nil {
143                         log.Fatalln(err)
144                 }
145
146                 var topic string
147                 if len(selectorParts) == 3 {
148                         topic = selectorParts[0]
149                 }
150                 var commits CommitIterNext
151                 if topic == "" {
152                         for i := 0; i < offset; i++ {
153                                 if _, err = repoLog.Next(); err != nil {
154                                         break
155                                 }
156                         }
157                         commits = repoLog
158                 } else {
159                         hashes := topicsCache[topic]
160                         if hashes == nil {
161                                 log.Fatalln(errors.New("no posts with that topic"))
162                         }
163                         if len(hashes) > offset {
164                                 hashes = hashes[offset:]
165                         }
166                         commits = &HashesIter{hashes}
167                 }
168
169                 logEnded := false
170                 entries := make([]TableMenuEntry, 0, PageEntries)
171                 for i := 0; i < PageEntries; i++ {
172                         commit, err := commits.Next()
173                         if err != nil {
174                                 logEnded = true
175                                 break
176                         }
177                         lines := msgSplit(commit.Message)
178                         entries = append(entries, TableMenuEntry{
179                                 Commit:    commit,
180                                 Title:     lines[0],
181                                 LinesNum:  len(lines) - 2,
182                                 ImagesNum: len(listImgs(cfg, commit.Hash)),
183                                 CommentsNum: len(sgblog.ParseComments(sgblog.GetNote(
184                                         repo, commentsTree, commit.Hash,
185                                 ))),
186                                 Topics: sgblog.ParseTopics(sgblog.GetNote(
187                                         repo, topicsTree, commit.Hash,
188                                 )),
189                         })
190                 }
191                 offsetPrev := offset - PageEntries
192                 if offsetPrev < 0 {
193                         offsetPrev = 0
194                 }
195                 err = TmplGopherMenu.Execute(os.Stdout, struct {
196                         T          *spreak.Localizer
197                         Cfg        *Cfg
198                         Topic      string
199                         Offset     int
200                         OffsetPrev int
201                         OffsetNext int
202                         LogEnded   bool
203                         Entries    []TableMenuEntry
204                         Topics     []string
205                         Version    string
206                 }{
207                         T:          localizer,
208                         Cfg:        cfg,
209                         Topic:      topic,
210                         Offset:     offset,
211                         OffsetPrev: offsetPrev,
212                         OffsetNext: offset + PageEntries,
213                         LogEnded:   logEnded,
214                         Entries:    entries,
215                         Topics:     topicsCache.Topics(),
216                         Version:    sgblog.Version,
217                 })
218                 if err != nil {
219                         log.Fatalln(err)
220                 }
221         } else {
222                 log.Fatalln(errors.New("unknown selector"))
223         }
224 }