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