]> Sergey Matveev's repositories - sgblog.git/blob - cmd/sgblog-comment-add/main.go
recfiles instead of netstrings
[sgblog.git] / cmd / sgblog-comment-add / main.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 // Git-backed CGI/inetd blogging/phlogging engine
19 package main
20
21 import (
22         "bytes"
23         "crypto/sha1"
24         "encoding/hex"
25         "flag"
26         "fmt"
27         "io/ioutil"
28         "log"
29         "mime"
30         "net/mail"
31         "os"
32         "os/exec"
33         "regexp"
34         "strconv"
35         "strings"
36         "syscall"
37         "time"
38
39         "go.stargrave.org/sgblog"
40 )
41
42 var hashFinder = regexp.MustCompile("([0-9a-f]{40})")
43
44 // Remove various whitespaces and excess lines, because git-notes-add
45 // will remove and we have to know exact bytes count
46 func cleanupBody(body string) []string {
47         lines := strings.Split(string(body), "\n")
48         for i, line := range lines {
49                 line = strings.ReplaceAll(line, "       ", "    ")
50                 line = strings.TrimRight(line, " \r")
51                 lines[i] = line
52         }
53         for lines[0] == "" {
54                 lines = lines[1:]
55         }
56         for lines[len(lines)-1] == "" {
57                 lines = lines[:len(lines)-1]
58         }
59         withoutDups := make([]string, 0, len(lines))
60         emptyMet := false
61         for _, line := range lines {
62                 if line == "" {
63                         if emptyMet {
64                                 continue
65                         }
66                         emptyMet = true
67                 } else {
68                         emptyMet = false
69                 }
70                 withoutDups = append(withoutDups, line)
71         }
72         return withoutDups
73 }
74
75 func main() {
76         gitCmd := flag.String("git-cmd", "/usr/local/bin/git", "Path to git executable")
77         gitDir := flag.String("git-dir", "", "Path to .git repository")
78         notesRef := flag.String("ref", "comments", "notes reference name")
79         umask := flag.String("umask", "027", "umask value")
80         dryRun := flag.Bool("dryrun", false, "Show comment, do not add")
81         committerEmail := flag.String(
82                 "committer-email",
83                 "comment@blog.example.com",
84                 "Git committer's email",
85         )
86         flag.Parse()
87         uid := syscall.Geteuid()
88         if err := syscall.Setuid(uid); err != nil {
89                 log.Fatal(err)
90         }
91         umaskInt, err := strconv.ParseUint(*umask, 8, 16)
92         if err != nil {
93                 panic(err)
94         }
95         syscall.Umask(int(umaskInt))
96
97         msg, err := mail.ReadMessage(os.Stdin)
98         if err != nil {
99                 log.Fatal(err)
100         }
101         subj, r, err := parseEmail(msg)
102         if err != nil {
103                 log.Fatal(err)
104         }
105         body, err := ioutil.ReadAll(r)
106         if err != nil {
107                 log.Fatal(err)
108         }
109         from := msg.Header.Get("From")
110         if from == "" {
111                 log.Fatal("From is missing")
112         }
113         if len(body) == 0 {
114                 log.Fatal("no body")
115         }
116         from, err = new(mime.WordDecoder).DecodeHeader(from)
117         if err != nil {
118                 log.Fatal(err)
119         }
120
121         subj = hashFinder.FindString(subj)
122         if subj == "" {
123                 log.Fatal("no commit hash found in subject")
124         }
125         if h, err := hex.DecodeString(subj); err != nil || len(h) != sha1.Size {
126                 os.Exit(0)
127         }
128         fromCols := strings.Fields(from)
129         if len(fromCols) == 1 {
130                 if idx := strings.Index(from, "@"); idx != -1 {
131                         from = strings.Trim(from[:idx], "<>")
132                 }
133         } else {
134                 from = strings.Join(fromCols[:len(fromCols)-1], " ")
135         }
136
137         cmd := exec.Command(
138                 *gitCmd, "--git-dir", *gitDir,
139                 "notes", "--ref", *notesRef, "show", subj,
140         )
141         note, _ := cmd.Output()
142         note = bytes.TrimRight(note, "\r\n")
143
144         buf := bytes.NewBuffer(note)
145         buf.WriteString(fmt.Sprintf(
146                 "\n\nFrom: %s\nDate: %s\nBody:\n",
147                 from,
148                 time.Now().UTC().Format(sgblog.WhenFmt),
149         ))
150         for _, s := range cleanupBody(string(body)) {
151                 buf.WriteString("+ ")
152                 buf.WriteString(s)
153                 buf.WriteString("\n")
154         }
155
156         if *dryRun {
157                 fmt.Print(buf.String())
158                 os.Exit(0)
159         }
160
161         cmd = exec.Command(
162                 *gitCmd, "--git-dir", *gitDir,
163                 "notes", "--ref", *notesRef, "add",
164                 "-F", "-", "-f", subj,
165         )
166         cmd.Env = append(
167                 cmd.Env,
168                 "GIT_AUTHOR_NAME=SGBlog "+sgblog.Version,
169                 "GIT_AUTHOR_EMAIL="+*committerEmail,
170                 "GIT_COMMITTER_NAME=SGBlog "+sgblog.Version,
171                 "GIT_COMMITTER_EMAIL="+*committerEmail,
172         )
173         cmd.Stdin = buf
174         if err = cmd.Run(); err != nil {
175                 log.Fatal(err)
176         }
177 }