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