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