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