]> Sergey Matveev's repositories - sgblog.git/blob - cmd/sgblog/gopher.go
Raise copyright years
[sgblog.git] / cmd / sgblog / gopher.go
1 /*
2 SGBlog -- Git-backed CGI/inetd blogging/phlogging 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         "encoding/json"
23         "errors"
24         "fmt"
25         "io"
26         "io/ioutil"
27         "log"
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/hjson/hjson-go"
37         "go.stargrave.org/sgblog"
38 )
39
40 const (
41         TmplGopherMenu = `{{$CR := printf "\r"}}{{$CRLF := printf "\r\n" -}}
42 {{- define "domainPort" }}      {{.GopherDomain}}       70{{end}}{{$Cfg := .Cfg -}}
43 i{{.Cfg.Title}} {{if .Topic}}(topic: {{.Topic}}) {{end}}({{.Offset}}-{{.OffsetNext}})   err{{template "domainPort" .Cfg}}{{$CRLF -}}
44 {{- if .Cfg.AboutURL}}hAbout    URL:{{.Cfg.AboutURL}}{{template "domainPort" .Cfg}}{{$CRLF}}{{end -}}
45 {{- if .Offset}}1Prev   {{if .Topic}}{{.Topic}}/{{end}}offset/{{.OffsetPrev}}{{template "domainPort" .Cfg}}{{$CRLF}}{{end -}}
46 {{- if not .LogEnded}}1Next     {{if .Topic}}{{.Topic}}/{{end}}offset/{{.OffsetNext}}{{template "domainPort" .Cfg}}{{$CRLF}}{{end -}}
47 {{- $datePrev := "0001-01-01" -}}
48 {{- range .Entries -}}
49 {{- $dateCur := .Commit.Author.When.Format "2006-01-02" -}}
50 {{- if ne $dateCur $datePrev}}{{$datePrev = $dateCur}}
51 i{{$dateCur}}   err{{template "domainPort" $Cfg}}{{$CR}}{{end}}
52 0[{{.Commit.Author.When.Format "15:04"}}] {{.Title}} ({{.LinesNum}}L){{with .CommentsNum}} ({{.}}C){{end}}{{if .Topics}}{{range .Topics}} {{.}}{{end}}{{end}}   /{{.Commit.Hash.String}}{{template "domainPort" $Cfg}}{{$CR}}{{end}}
53 {{range .Topics}}
54 1Topic: {{.}}   {{.}}/offset/0{{template "domainPort" $Cfg}}{{$CR}}{{end}}
55 iGenerated by: SGBlog {{.Version}}      err{{template "domainPort" .Cfg}}{{$CR}}
56 .{{$CRLF}}`
57         TmplGopherEntry = `What: {{.Commit.Hash.String}}
58 When: {{.When}}
59 ------------------------------------------------------------------------
60 {{if .Topics}}Topics:{{range .Topics}} {{.}}{{end}}{{end}}
61 ------------------------------------------------------------------------
62 {{.Commit.Message -}}
63 {{- if .Note}}
64 ------------------------------------------------------------------------
65 Note:
66 {{.Note}}{{end -}}
67 {{- if .Cfg.CommentsEmail}}
68 ------------------------------------------------------------------------
69 leave comment: mailto:{{.Cfg.CommentsEmail}}?subject={{.Commit.Hash.String}}
70 {{end}}{{range $idx, $comment := .Comments}}
71 ------------------------------------------------------------------------
72 comment {{$idx}}:
73 {{$comment}}
74 {{end}}
75 ------------------------------------------------------------------------
76 Generated by: SGBlog {{.Version}}
77 `
78 )
79
80 type TableMenuEntry struct {
81         Commit      *object.Commit
82         Title       string
83         LinesNum    int
84         CommentsNum int
85         Topics      []string
86 }
87
88 func serveGopher() {
89         cfgPath := os.Args[2]
90         cfgRaw, err := ioutil.ReadFile(cfgPath)
91         if err != nil {
92                 log.Fatalln(err)
93         }
94         var cfgGeneral map[string]interface{}
95         if err = hjson.Unmarshal(cfgRaw, &cfgGeneral); err != nil {
96                 log.Fatalln(err)
97         }
98         cfgRaw, err = json.Marshal(cfgGeneral)
99         if err != nil {
100                 log.Fatalln(err)
101         }
102         var cfg *Cfg
103         if err = json.Unmarshal(cfgRaw, &cfg); err != nil {
104                 log.Fatalln(err)
105         }
106         if cfg.GopherDomain == "" {
107                 log.Fatalln("GopherDomain is not configured")
108         }
109
110         headHash, err := initRepo(cfg)
111         if err != nil {
112                 log.Fatalln(err)
113         }
114
115         scanner := bufio.NewScanner(io.LimitReader(os.Stdin, 1<<8))
116         if !scanner.Scan() {
117                 log.Fatalln(errors.New("no CRLF found"))
118         }
119         selector := scanner.Text()
120         if selector == "" {
121                 selector = "offset/0"
122         }
123         selectorParts := strings.Split(selector, "/")
124         if strings.HasPrefix(selector, "URL:") {
125                 selector = selector[len("URL:"):]
126                 fmt.Printf(`<html>
127 <head>
128         <meta http-equiv="Refresh" content="1; url=%s" />
129         <title>Redirect to non-gopher URL</title>
130 </head>
131 <body>
132 Redirecting to <a href="%s">%s</a>...
133 </body>
134 </html>
135 `, selector, selector, selector)
136         } else if sha1DigestRe.MatchString(selector) {
137                 commit, err := repo.CommitObject(plumbing.NewHash(selector[1:]))
138                 if err != nil {
139                         log.Fatalln(err)
140                 }
141                 tmpl := template.Must(template.New("entry").Parse(TmplGopherEntry))
142                 err = tmpl.Execute(os.Stdout, struct {
143                         Commit   *object.Commit
144                         When     string
145                         Cfg      *Cfg
146                         Note     string
147                         Comments []string
148                         Topics   []string
149                         Version  string
150                 }{
151                         Commit:   commit,
152                         When:     commit.Author.When.Format(sgblog.WhenFmt),
153                         Cfg:      cfg,
154                         Note:     string(getNote(notesTree, commit.Hash)),
155                         Comments: parseComments(getNote(commentsTree, commit.Hash)),
156                         Topics:   parseTopics(getNote(topicsTree, commit.Hash)),
157                         Version:  sgblog.Version,
158                 })
159                 if err != nil {
160                         log.Fatalln(err)
161                 }
162         } else if selectorParts[len(selectorParts)-2] == "offset" {
163                 offset, err := strconv.Atoi(selectorParts[len(selectorParts)-1])
164                 if err != nil {
165                         log.Fatalln(err)
166                 }
167                 repoLog, err := repo.Log(&git.LogOptions{From: *headHash})
168                 if err != nil {
169                         log.Fatalln(err)
170                 }
171                 topicsCache, err := getTopicsCache(cfg, repoLog)
172                 if err != nil {
173                         log.Fatalln(err)
174                 }
175                 repoLog, err = repo.Log(&git.LogOptions{From: *headHash})
176                 if err != nil {
177                         log.Fatalln(err)
178                 }
179
180                 var topic string
181                 if len(selectorParts) == 3 {
182                         topic = selectorParts[0]
183                 }
184                 var commits CommitIterNext
185                 if topic == "" {
186                         for i := 0; i < offset; i++ {
187                                 if _, err = repoLog.Next(); err != nil {
188                                         break
189                                 }
190                         }
191                         commits = repoLog
192                 } else {
193                         hashes := topicsCache[topic]
194                         if hashes == nil {
195                                 log.Fatalln(errors.New("no posts with that topic"))
196                         }
197                         if len(hashes) > offset {
198                                 hashes = hashes[offset:]
199                         }
200                         commits = &HashesIter{hashes}
201                 }
202
203                 logEnded := false
204                 entries := make([]TableMenuEntry, 0, PageEntries)
205                 for i := 0; i < PageEntries; i++ {
206                         commit, err := commits.Next()
207                         if err != nil {
208                                 logEnded = true
209                                 break
210                         }
211                         lines := msgSplit(commit.Message)
212                         entries = append(entries, TableMenuEntry{
213                                 Commit:      commit,
214                                 Title:       lines[0],
215                                 LinesNum:    len(lines) - 2,
216                                 CommentsNum: len(parseComments(getNote(commentsTree, commit.Hash))),
217                                 Topics:      parseTopics(getNote(topicsTree, commit.Hash)),
218                         })
219                 }
220                 tmpl := template.Must(template.New("menu").Parse(TmplGopherMenu))
221                 offsetPrev := offset - PageEntries
222                 if offsetPrev < 0 {
223                         offsetPrev = 0
224                 }
225                 err = tmpl.Execute(os.Stdout, struct {
226                         Cfg        *Cfg
227                         Topic      string
228                         Offset     int
229                         OffsetPrev int
230                         OffsetNext int
231                         LogEnded   bool
232                         Entries    []TableMenuEntry
233                         Topics     []string
234                         Version    string
235                 }{
236                         Cfg:        cfg,
237                         Topic:      topic,
238                         Offset:     offset,
239                         OffsetPrev: offsetPrev,
240                         OffsetNext: offset + PageEntries,
241                         LogEnded:   logEnded,
242                         Entries:    entries,
243                         Topics:     topicsCache.Topics(),
244                         Version:    sgblog.Version,
245                 })
246                 if err != nil {
247                         log.Fatalln(err)
248                 }
249         } else {
250                 log.Fatalln(errors.New("unknown selector"))
251         }
252 }