]> Sergey Matveev's repositories - sgblog.git/blob - cmd/sgblog/gopher.go
Use templates for code simplicity
[sgblog.git] / cmd / sgblog / gopher.go
1 /*
2 SGBlog -- Git-backed CGI/inetd blogging/phlogging engine
3 Copyright (C) 2020 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/hjson/hjson-go"
34         "go.stargrave.org/sgblog"
35         "gopkg.in/src-d/go-git.v4"
36         "gopkg.in/src-d/go-git.v4/plumbing"
37         "gopkg.in/src-d/go-git.v4/plumbing/object"
38 )
39
40 const (
41         TmplGopherMenu = `{{$Cfg := .Cfg}}{{$CR := .CR}}i{{.Cfg.Title}} ({{.Offset}}-{{.OffsetNext}})   err     {{.Cfg.GopherDomain}}   70{{.CR}}
42 {{if .Cfg.AboutURL}}hAbout      URL:{{.Cfg.AboutURL}}   {{.Cfg.GopherDomain}}   70{{.CR}}{{end}}
43 {{if .Offset}}1Prev     offset/{{.OffsetPrev}}  {{.Cfg.GopherDomain}}   70{{.CR}}{{end}}
44 {{if not .LogEnded}}1Next       offset/{{.OffsetNext}}  {{.Cfg.GopherDomain}}   70{{.CR}}{{end -}}
45 {{- $yearPrev := 0}}{{$monthPrev := 0}}{{$dayPrev := 0 -}}
46 {{- range .Entries }}{{$yearCur := .Commit.Author.When.Year}}{{$monthCur := .Commit.Author.When.Month}}{{$dayCur := .Commit.Author.When.Day -}}
47 {{- if or (ne $dayCur $dayPrev) (ne $monthCur $monthPrev) (ne $yearCur $yearPrev)}}{{$dayPrev = $dayCur}}{{$monthPrev = $monthCur}}{{$yearPrev = $yearCur}}
48 i{{$yearCur | printf "%04d"}}-{{$monthCur | printf "%02d"}}-{{$dayCur | printf "%02d"}} err     {{$Cfg.GopherDomain}}   70{{$CR}}{{end}}
49 0[{{.Commit.Author.When.Hour | printf "%02d"}}:{{.Commit.Author.When.Minute | printf "%02d"}}] {{.Title}} ({{.LinesNum}}L){{if .CommentsNum}} ({{.CommentsNum}}C){{end}}        /{{.Commit.Hash.String}}        {{$Cfg.GopherDomain}}   70{{$CR}}{{end}}
50 iGenerated by: SGBlog {{.Version}}      err     {{.Cfg.GopherDomain}}   70{{.CR}}
51 .{{.CR}}
52 `
53         TmplGopherEntry = `What: {{.Commit.Hash.String}}
54 When: {{.When}}
55 ------------------------------------------------------------------------
56 {{.Commit.Message -}}
57 {{- if .Note}}
58 ------------------------------------------------------------------------
59 Note:
60 {{.Note}}{{end -}}
61 {{- if .Cfg.CommentsEmail}}
62 ------------------------------------------------------------------------
63 leave comment: mailto:{{.Cfg.CommentsEmail}}?subject={{.Commit.Hash.String}}
64 {{end}}{{range $idx, $comment := .Comments}}
65 ------------------------------------------------------------------------
66 comment {{$idx}}:
67 {{$comment}}
68 {{end}}
69 ------------------------------------------------------------------------
70 Generated by: SGBlog {{.Version}}
71 `
72 )
73
74 type TableMenuEntry struct {
75         Commit      *object.Commit
76         Title       string
77         LinesNum    int
78         CommentsNum int
79 }
80
81 func serveGopher() {
82         cfgPath := os.Args[2]
83         cfgRaw, err := ioutil.ReadFile(cfgPath)
84         if err != nil {
85                 log.Fatalln(err)
86         }
87         var cfgGeneral map[string]interface{}
88         if err = hjson.Unmarshal(cfgRaw, &cfgGeneral); err != nil {
89                 log.Fatalln(err)
90         }
91         cfgRaw, err = json.Marshal(cfgGeneral)
92         if err != nil {
93                 log.Fatalln(err)
94         }
95         var cfg *Cfg
96         if err = json.Unmarshal(cfgRaw, &cfg); err != nil {
97                 log.Fatalln(err)
98         }
99         if cfg.GopherDomain == "" {
100                 log.Fatalln("GopherDomain is not configured")
101         }
102
103         headHash, err := initRepo(cfg)
104         if err != nil {
105                 log.Fatalln(err)
106         }
107
108         scanner := bufio.NewScanner(io.LimitReader(os.Stdin, 1<<8))
109         if !scanner.Scan() {
110                 log.Fatalln(errors.New("no CRLF found"))
111         }
112         selector := scanner.Text()
113         if selector == "" {
114                 selector = "offset/0"
115         }
116         if strings.HasPrefix(selector, "offset/") {
117                 offset, err := strconv.Atoi(selector[len("offset/"):])
118                 if err != nil {
119                         log.Fatalln(err)
120                 }
121                 repoLog, err := repo.Log(&git.LogOptions{From: *headHash})
122                 if err != nil {
123                         log.Fatalln(err)
124                 }
125                 for i := 0; i < offset; i++ {
126                         if _, err = repoLog.Next(); err != nil {
127                                 break
128                         }
129                 }
130                 logEnded := false
131                 entries := make([]TableMenuEntry, 0, PageEntries)
132                 for i := 0; i < PageEntries; i++ {
133                         commit, err := repoLog.Next()
134                         if err != nil {
135                                 logEnded = true
136                                 break
137                         }
138                         lines := msgSplit(commit.Message)
139                         entries = append(entries, TableMenuEntry{
140                                 Commit:      commit,
141                                 Title:       lines[0],
142                                 LinesNum:    len(lines) - 2,
143                                 CommentsNum: len(parseComments(getNote(commentsTree, commit.Hash))),
144                         })
145                 }
146                 tmpl := template.Must(template.New("menu").Parse(TmplGopherMenu))
147                 err = tmpl.Execute(os.Stdout, struct {
148                         Cfg        *Cfg
149                         Offset     int
150                         OffsetPrev int
151                         OffsetNext int
152                         LogEnded   bool
153                         Entries    []TableMenuEntry
154                         Version    string
155                         CR         string
156                 }{
157                         Cfg:        cfg,
158                         Offset:     offset,
159                         OffsetPrev: offset - PageEntries,
160                         OffsetNext: offset + PageEntries,
161                         LogEnded:   logEnded,
162                         Entries:    entries,
163                         Version:    sgblog.Version,
164                         CR:         "\r",
165                 })
166                 if err != nil {
167                         log.Fatalln(err)
168                 }
169         } else if strings.HasPrefix(selector, "URL:") {
170                 selector = selector[len("URL:"):]
171                 fmt.Printf(`<html>
172 <head>
173         <meta http-equiv="Refresh" content="1; url=%s" />
174         <title>Redirect to non-gopher URL</title>
175 </head>
176 <body>
177 Redirecting to <a href="%s">%s</a>...
178 </body>
179 </html>
180 `, selector, selector, selector)
181         } else if sha1DigestRe.MatchString(selector) {
182                 commit, err := repo.CommitObject(plumbing.NewHash(selector[1:]))
183                 if err != nil {
184                         log.Fatalln(err)
185                 }
186                 tmpl := template.Must(template.New("entry").Parse(TmplGopherEntry))
187                 err = tmpl.Execute(os.Stdout, struct {
188                         Commit   *object.Commit
189                         When     string
190                         Cfg      *Cfg
191                         Note     string
192                         Comments []string
193                         Version  string
194                 }{
195                         Commit:   commit,
196                         When:     commit.Author.When.Format(sgblog.WhenFmt),
197                         Cfg:      cfg,
198                         Note:     string(getNote(notesTree, commit.Hash)),
199                         Comments: parseComments(getNote(commentsTree, commit.Hash)),
200                         Version:  sgblog.Version,
201                 })
202                 if err != nil {
203                         log.Fatalln(err)
204                 }
205         } else {
206                 log.Fatalln(errors.New("unknown selector"))
207         }
208 }