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