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