]> Sergey Matveev's repositories - sgblog.git/blob - cmd/sgblog/main.go
6cd1efd17b7a35a7bb12f4faa16ffdee120bbb14
[sgblog.git] / cmd / sgblog / main.go
1 /*
2 SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine
3 Copyright (C) 2020-2023 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/UCSPI blogging/phlogging/gemlogging engine
19 package main
20
21 import (
22         "crypto/sha1"
23         "embed"
24         "encoding/json"
25         "flag"
26         "fmt"
27         "io/fs"
28         "log"
29         "os"
30         "regexp"
31         "strings"
32
33         "github.com/go-git/go-git/v5"
34         "github.com/go-git/go-git/v5/plumbing"
35         "github.com/go-git/go-git/v5/plumbing/object"
36         "github.com/hjson/hjson-go"
37         "github.com/vorlif/spreak"
38         "golang.org/x/text/language"
39 )
40
41 const (
42         PageEntries = 50
43 )
44
45 var (
46         sha1DigestRe = regexp.MustCompilePOSIX(fmt.Sprintf("([0-9a-f]{%d,%d})", sha1.Size*2, sha1.Size*2))
47         repo         *git.Repository
48         notesTree    *object.Tree
49         commentsRef  *plumbing.Reference
50         commentsTree *object.Tree
51         topicsRef    *plumbing.Reference
52         topicsTree   *object.Tree
53
54         localizer *spreak.Localizer
55
56         //go:embed locale/*
57         locales embed.FS
58 )
59
60 type Cfg struct {
61         GitPath string
62         Branch  string
63         Title   string
64         Lang    string
65
66         URLPrefix string
67
68         AtomBaseURL string
69         AtomId      string
70         AtomAuthor  string
71
72         CSS       string
73         Webmaster string
74         AboutURL  string
75         GitURLs   []string
76
77         CommentsNotesRef string
78         CommentsEmail    string
79
80         TopicsNotesRef  string
81         TopicsCachePath string
82
83         GopherDomain string
84 }
85
86 func msgSplit(msg string) []string {
87         lines := strings.Split(msg, "\n")
88         lines = lines[:len(lines)-1]
89         if len(lines) < 3 {
90                 lines = []string{lines[0], "", ""}
91         }
92         return lines
93 }
94
95 func initRepo(cfg *Cfg) (*plumbing.Hash, error) {
96         var err error
97         repo, err = git.PlainOpen(cfg.GitPath)
98         if err != nil {
99                 return nil, err
100         }
101         head, err := repo.Reference(plumbing.ReferenceName(cfg.Branch), false)
102         if err != nil {
103                 return nil, err
104         }
105         headHash := head.Hash()
106         if notes, err := repo.Notes(); err == nil {
107                 var notesRef *plumbing.Reference
108                 notes.ForEach(func(ref *plumbing.Reference) error {
109                         switch string(ref.Name()) {
110                         case "refs/notes/commits":
111                                 notesRef = ref
112                         case cfg.CommentsNotesRef:
113                                 commentsRef = ref
114                         case cfg.TopicsNotesRef:
115                                 topicsRef = ref
116                         }
117                         return nil
118                 })
119                 if notesRef != nil {
120                         if commentsCommit, err := repo.CommitObject(notesRef.Hash()); err == nil {
121                                 notesTree, _ = commentsCommit.Tree()
122                         }
123                 }
124                 if commentsRef != nil {
125                         if commentsCommit, err := repo.CommitObject(commentsRef.Hash()); err == nil {
126                                 commentsTree, _ = commentsCommit.Tree()
127                         }
128                 }
129                 if topicsRef != nil {
130                         if topicsCommit, err := repo.CommitObject(topicsRef.Hash()); err == nil {
131                                 topicsTree, _ = topicsCommit.Tree()
132                         }
133                 }
134         }
135         return &headHash, nil
136 }
137
138 func readCfg(cfgPath string) (*Cfg, error) {
139         cfgRaw, err := os.ReadFile(cfgPath)
140         if err != nil {
141                 return nil, err
142         }
143         var cfgGeneral map[string]interface{}
144         if err = hjson.Unmarshal(cfgRaw, &cfgGeneral); err != nil {
145                 return nil, err
146         }
147         cfgRaw, err = json.Marshal(cfgGeneral)
148         if err != nil {
149                 return nil, err
150         }
151         var cfg *Cfg
152         if err = json.Unmarshal(cfgRaw, &cfg); err != nil {
153                 return nil, err
154         }
155         return cfg, nil
156 }
157
158 func initLocalizer(lang string) {
159         fsys, _ := fs.Sub(locales, "locale")
160         bundle, err := spreak.NewBundle(
161                 spreak.WithSourceLanguage(language.English),
162                 spreak.WithDomainFs(spreak.NoDomain, fsys),
163                 spreak.WithLanguage(language.Russian),
164         )
165         if err != nil {
166                 log.Fatalln(err)
167         }
168         if lang == "" {
169                 lang = language.English.String()
170         }
171         localizer = spreak.NewLocalizer(bundle, language.MustParse(lang))
172 }
173
174 func main() {
175         gopherCfgPath := flag.String("gopher", "", "Path to gopher-related configuration file")
176         geminiCfgPath := flag.String("gemini", "", "Path to gemini-related configuration file")
177         flag.Usage = func() {
178                 fmt.Fprintf(flag.CommandLine.Output(), `Usage of sgblog:
179         sgblog -- run CGI HTTP backend
180         sgblog -gopher /path/to/cfg.hjson -- run UCSPI/inetd Gopher backend
181         sgblog -gemini /path/to/cfg.hjson -- run UCSPI+tlss Gemini backend
182 `)
183         }
184         flag.Parse()
185         log.SetFlags(log.Lshortfile)
186         if *gopherCfgPath != "" {
187                 serveGopher(*gopherCfgPath)
188         } else if *geminiCfgPath != "" {
189                 serveGemini(*geminiCfgPath)
190         } else {
191                 serveHTTP()
192         }
193 }