]> Sergey Matveev's repositories - sgblog.git/blob - cmd/sgblog/gopher.go
Leave comments link on gopher page
[sgblog.git] / cmd / sgblog / gopher.go
1 /*
2 SGBlog -- Git-based 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         "bytes"
23         "encoding/json"
24         "errors"
25         "fmt"
26         "io"
27         "io/ioutil"
28         "log"
29         "os"
30         "strconv"
31         "strings"
32         "time"
33
34         "github.com/hjson/hjson-go"
35         "go.stargrave.org/sgblog"
36         "gopkg.in/src-d/go-git.v4"
37         "gopkg.in/src-d/go-git.v4/plumbing"
38 )
39
40 const CRLF = "\r\n"
41
42 var DashLine = strings.Repeat("-", 72)
43
44 func makeI(cfg *Cfg, value string) string {
45         return strings.Join([]string{"i" + value, "err", cfg.GopherDomain, "70", CRLF}, "\t")
46 }
47
48 func serveGopher() {
49         cfgPath := os.Args[2]
50         cfgRaw, err := ioutil.ReadFile(cfgPath)
51         if err != nil {
52                 log.Fatalln(err)
53         }
54         var cfgGeneral map[string]interface{}
55         if err = hjson.Unmarshal(cfgRaw, &cfgGeneral); err != nil {
56                 log.Fatalln(err)
57         }
58         cfgRaw, err = json.Marshal(cfgGeneral)
59         if err != nil {
60                 log.Fatalln(err)
61         }
62         var cfg *Cfg
63         if err = json.Unmarshal(cfgRaw, &cfg); err != nil {
64                 log.Fatalln(err)
65         }
66         if cfg.GopherDomain == "" {
67                 log.Fatalln("GopherDomain is not configured")
68         }
69
70         headHash, err := initRepo(cfg)
71         if err != nil {
72                 log.Fatalln(err)
73         }
74
75         scanner := bufio.NewScanner(io.LimitReader(os.Stdin, 1<<8))
76         if !scanner.Scan() {
77                 log.Fatalln(errors.New("no CRLF found"))
78         }
79         selector := scanner.Text()
80         if selector == "" {
81                 selector = "offset/0"
82         }
83         if strings.HasPrefix(selector, "offset/") {
84                 offset, err := strconv.Atoi(selector[len("offset/"):])
85                 if err != nil {
86                         log.Fatalln(err)
87                 }
88                 repoLog, err := repo.Log(&git.LogOptions{From: *headHash})
89                 if err != nil {
90                         log.Fatalln(err)
91                 }
92                 commitN := 0
93                 for i := 0; i < offset; i++ {
94                         if _, err = repoLog.Next(); err != nil {
95                                 break
96                         }
97                         commitN++
98                 }
99
100                 logEnded := false
101                 var menu bytes.Buffer
102                 var yearPrev int
103                 var monthPrev time.Month
104                 var dayPrev int
105                 for i := 0; i < PageEntries; i++ {
106                         commit, err := repoLog.Next()
107                         if err != nil {
108                                 logEnded = true
109                                 break
110                         }
111                         yearCur, monthCur, dayCur := commit.Author.When.Date()
112                         if dayCur != dayPrev || monthCur != monthPrev || yearCur != yearPrev {
113                                 menu.WriteString(makeI(cfg, fmt.Sprintf(
114                                         "%04d-%02d-%02d", yearCur, monthCur, dayCur,
115                                 )))
116                                 yearPrev, monthPrev, dayPrev = yearCur, monthCur, dayCur
117                         }
118                         commitN++
119                         lines := msgSplit(commit.Message)
120                         var commentsValue string
121                         if l := len(parseComments(getNote(commentsTree, commit.Hash))); l > 0 {
122                                 commentsValue = fmt.Sprintf(" (%dC)", l)
123                         }
124                         menu.WriteString(fmt.Sprintf(
125                                 "0[%02d:%02d] %s (%dL)%s\t/%s\t%s\t%d%s",
126                                 commit.Author.When.Hour(),
127                                 commit.Author.When.Minute(),
128                                 lines[0],
129                                 len(lines)-2,
130                                 commentsValue,
131                                 commit.Hash.String(),
132                                 cfg.GopherDomain, 70, CRLF,
133                         ))
134                 }
135
136                 fmt.Print(makeI(cfg, fmt.Sprintf("%s (%d-%d)", cfg.Title, offset, offset+PageEntries)))
137                 if cfg.AboutURL != "" {
138                         fmt.Printf(
139                                 "hAbout\tURL:%s\t%s\t%d%s",
140                                 cfg.AboutURL,
141                                 cfg.GopherDomain, 70, CRLF,
142                         )
143                 }
144                 if offset > 0 {
145                         offsetPrev := offset - PageEntries
146                         if offsetPrev < 0 {
147                                 offsetPrev = 0
148                         }
149                         fmt.Printf(
150                                 "1Prev\toffset/%d\t%s\t%d%s",
151                                 offsetPrev,
152                                 cfg.GopherDomain, 70, CRLF,
153                         )
154                 }
155                 if !logEnded {
156                         fmt.Printf(
157                                 "1Next\toffset/%d\t%s\t%d%s",
158                                 offset+PageEntries,
159                                 cfg.GopherDomain, 70, CRLF,
160                         )
161                 }
162                 fmt.Print(menu.String())
163                 fmt.Printf(makeI(cfg, "Generated by: SGBlog "+sgblog.Version))
164                 fmt.Print("." + CRLF)
165         } else if strings.HasPrefix(selector, "URL:") {
166                 selector = selector[len("URL:"):]
167                 fmt.Printf(`<html>
168 <head>
169         <meta http-equiv="Refresh" content="1; url=%s" />
170         <title>Redirect to non-gopher URL</title>
171 </head>
172 <body>
173 Redirecting to <a href="%s">%s</a>...
174 </body>
175 </html>
176 `, selector, selector, selector)
177         } else if sha1DigestRe.MatchString(selector) {
178                 commit, err := repo.CommitObject(plumbing.NewHash(selector[1:]))
179                 if err != nil {
180                         log.Fatalln(err)
181                 }
182                 fmt.Printf(
183                         "What: %s\nWhen: %s\n%s\n%s",
184                         commit.Hash.String(),
185                         commit.Author.When.Format(sgblog.WhenFmt),
186                         DashLine,
187                         commit.Message,
188                 )
189                 notesRaw := getNote(notesTree, commit.Hash)
190                 if len(notesRaw) > 0 {
191                         fmt.Printf("%s\nNote:\n%s\n", DashLine, string(notesRaw))
192                 }
193                 if cfg.CommentsEmail != "" {
194                         fmt.Printf(
195                                 "%s\nleave comment: mailto:%s?subject=%s\n",
196                                 DashLine,
197                                 cfg.CommentsEmail,
198                                 commit.Hash.String(),
199                         )
200                 }
201                 for i, comment := range parseComments(getNote(commentsTree, commit.Hash)) {
202                         fmt.Printf("%s\ncomment %d:\n%s\n", DashLine, i, comment)
203                 }
204                 fmt.Printf("%s\nGenerated by: SGBlog %s\n", DashLine, sgblog.Version)
205         } else {
206                 log.Fatalln(errors.New("unknown selector"))
207         }
208 }