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