]> Sergey Matveev's repositories - sgblog.git/blob - cmd/sgblog/gopher.go
59e2607644ec8d8c9da44175283fb2e54c592ffb
[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 = `{{$CR := printf "\r"}}{{$CRLF := printf "\r\n" -}}
42 {{- define "domainPort" }}      {{.GopherDomain}}       70{{end}}{{$Cfg := .Cfg -}}
43 i{{.Cfg.Title}} ({{.Offset}}-{{.OffsetNext}})   err{{template "domainPort" .Cfg}}{{$CRLF -}}
44 {{- if .Cfg.AboutURL}}hAbout    URL:{{.Cfg.AboutURL}}{{template "domainPort" .Cfg}}{{$CRLF}}{{end -}}
45 {{- if .Offset}}1Prev   offset/{{.OffsetPrev}}{{template "domainPort" .Cfg}}{{$CRLF}}{{end -}}
46 {{- if not .LogEnded}}1Next     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}}      /{{.Commit.Hash.String}}{{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 {{.Commit.Message -}}
59 {{- if .Note}}
60 ------------------------------------------------------------------------
61 Note:
62 {{.Note}}{{end -}}
63 {{- if .Cfg.CommentsEmail}}
64 ------------------------------------------------------------------------
65 leave comment: mailto:{{.Cfg.CommentsEmail}}?subject={{.Commit.Hash.String}}
66 {{end}}{{range $idx, $comment := .Comments}}
67 ------------------------------------------------------------------------
68 comment {{$idx}}:
69 {{$comment}}
70 {{end}}
71 ------------------------------------------------------------------------
72 Generated by: SGBlog {{.Version}}
73 `
74 )
75
76 type TableMenuEntry struct {
77         Commit      *object.Commit
78         Title       string
79         LinesNum    int
80         CommentsNum int
81 }
82
83 func serveGopher() {
84         cfgPath := os.Args[2]
85         cfgRaw, err := ioutil.ReadFile(cfgPath)
86         if err != nil {
87                 log.Fatalln(err)
88         }
89         var cfgGeneral map[string]interface{}
90         if err = hjson.Unmarshal(cfgRaw, &cfgGeneral); err != nil {
91                 log.Fatalln(err)
92         }
93         cfgRaw, err = json.Marshal(cfgGeneral)
94         if err != nil {
95                 log.Fatalln(err)
96         }
97         var cfg *Cfg
98         if err = json.Unmarshal(cfgRaw, &cfg); err != nil {
99                 log.Fatalln(err)
100         }
101         if cfg.GopherDomain == "" {
102                 log.Fatalln("GopherDomain is not configured")
103         }
104
105         headHash, err := initRepo(cfg)
106         if err != nil {
107                 log.Fatalln(err)
108         }
109
110         scanner := bufio.NewScanner(io.LimitReader(os.Stdin, 1<<8))
111         if !scanner.Scan() {
112                 log.Fatalln(errors.New("no CRLF found"))
113         }
114         selector := scanner.Text()
115         if selector == "" {
116                 selector = "offset/0"
117         }
118         if strings.HasPrefix(selector, "offset/") {
119                 offset, err := strconv.Atoi(selector[len("offset/"):])
120                 if err != nil {
121                         log.Fatalln(err)
122                 }
123                 repoLog, err := repo.Log(&git.LogOptions{From: *headHash})
124                 if err != nil {
125                         log.Fatalln(err)
126                 }
127                 for i := 0; i < offset; i++ {
128                         if _, err = repoLog.Next(); err != nil {
129                                 break
130                         }
131                 }
132                 logEnded := false
133                 entries := make([]TableMenuEntry, 0, PageEntries)
134                 for i := 0; i < PageEntries; i++ {
135                         commit, err := repoLog.Next()
136                         if err != nil {
137                                 logEnded = true
138                                 break
139                         }
140                         lines := msgSplit(commit.Message)
141                         entries = append(entries, TableMenuEntry{
142                                 Commit:      commit,
143                                 Title:       lines[0],
144                                 LinesNum:    len(lines) - 2,
145                                 CommentsNum: len(parseComments(getNote(commentsTree, commit.Hash))),
146                         })
147                 }
148                 tmpl := template.Must(template.New("menu").Parse(TmplGopherMenu))
149                 err = tmpl.Execute(os.Stdout, struct {
150                         Cfg        *Cfg
151                         Offset     int
152                         OffsetPrev int
153                         OffsetNext int
154                         LogEnded   bool
155                         Entries    []TableMenuEntry
156                         Version    string
157                 }{
158                         Cfg:        cfg,
159                         Offset:     offset,
160                         OffsetPrev: offset - PageEntries,
161                         OffsetNext: offset + PageEntries,
162                         LogEnded:   logEnded,
163                         Entries:    entries,
164                         Version:    sgblog.Version,
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 }