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