- commit
- 8d3fd1e
- parent
- 42c3628
- author
- Eric Bower
- date
- 2022-07-29 19:42:22 +0000 UTC
refactor: move code from services to root This is the first of a couple refactors to abstract as much service code into a separate package in order to reduce the amount of code duplication. This code was essentially copy pasted into all of the services because we didn't have a good way to abstract it until now.
23 files changed,
+486,
-1017
M
makefile
+3,
-2
1@@ -9,6 +9,7 @@ import (
2 "time"
3
4 "git.sr.ht/~erock/pico/lists"
5+ "git.sr.ht/~erock/pico/shared"
6 "git.sr.ht/~erock/pico/wish/cms"
7 "git.sr.ht/~erock/pico/wish/cms/db/postgres"
8 "git.sr.ht/~erock/pico/wish/proxy"
9@@ -56,8 +57,8 @@ func withProxy(handler *lists.DbHandler) ssh.Option {
10 }
11
12 func main() {
13- host := lists.GetEnv("PROSE_HOST", "0.0.0.0")
14- port := lists.GetEnv("PROSE_SSH_PORT", "2222")
15+ host := shared.GetEnv("PROSE_HOST", "0.0.0.0")
16+ port := shared.GetEnv("PROSE_SSH_PORT", "2222")
17 cfg := lists.NewConfigSite()
18 logger := cfg.Logger
19 dbh := postgres.NewDB(&cfg.ConfigCms)
+3,
-2
1@@ -9,6 +9,7 @@ import (
2 "time"
3
4 "git.sr.ht/~erock/pico/pastes"
5+ "git.sr.ht/~erock/pico/shared"
6 "git.sr.ht/~erock/pico/wish/cms"
7 "git.sr.ht/~erock/pico/wish/cms/db/postgres"
8 "git.sr.ht/~erock/pico/wish/proxy"
9@@ -56,8 +57,8 @@ func withProxy(handler *pastes.DbHandler) ssh.Option {
10 }
11
12 func main() {
13- host := pastes.GetEnv("PROSE_HOST", "0.0.0.0")
14- port := pastes.GetEnv("PROSE_SSH_PORT", "2222")
15+ host := shared.GetEnv("PROSE_HOST", "0.0.0.0")
16+ port := shared.GetEnv("PROSE_SSH_PORT", "2222")
17 cfg := pastes.NewConfigSite()
18 logger := cfg.Logger
19 dbh := postgres.NewDB(&cfg.ConfigCms)
+3,
-2
1@@ -9,6 +9,7 @@ import (
2 "time"
3
4 "git.sr.ht/~erock/pico/prose"
5+ "git.sr.ht/~erock/pico/shared"
6 "git.sr.ht/~erock/pico/wish/cms"
7 "git.sr.ht/~erock/pico/wish/cms/db/postgres"
8 "git.sr.ht/~erock/pico/wish/proxy"
9@@ -56,8 +57,8 @@ func withProxy(handler *prose.DbHandler) ssh.Option {
10 }
11
12 func main() {
13- host := prose.GetEnv("PROSE_HOST", "0.0.0.0")
14- port := prose.GetEnv("PROSE_SSH_PORT", "2222")
15+ host := shared.GetEnv("PROSE_HOST", "0.0.0.0")
16+ port := shared.GetEnv("PROSE_SSH_PORT", "2222")
17 cfg := prose.NewConfigSite()
18 logger := cfg.Logger
19 dbh := postgres.NewDB(&cfg.ConfigCms)
+84,
-76
1@@ -8,9 +8,11 @@ import (
2 "net/http"
3 "net/url"
4 "strconv"
5+ "strings"
6 "time"
7
8 "git.sr.ht/~erock/pico/lists/pkg"
9+ "git.sr.ht/~erock/pico/shared"
10 "git.sr.ht/~erock/pico/wish/cms/db"
11 "git.sr.ht/~erock/pico/wish/cms/db/postgres"
12 "github.com/gorilla/feeds"
13@@ -18,7 +20,7 @@ import (
14 )
15
16 type PageData struct {
17- Site SitePageData
18+ Site shared.SitePageData
19 }
20
21 type PostItemData struct {
22@@ -35,7 +37,7 @@ type PostItemData struct {
23 }
24
25 type BlogPageData struct {
26- Site SitePageData
27+ Site shared.SitePageData
28 PageTitle string
29 URL template.URL
30 RSSURL template.URL
31@@ -46,14 +48,14 @@ type BlogPageData struct {
32 }
33
34 type ReadPageData struct {
35- Site SitePageData
36+ Site shared.SitePageData
37 NextPage string
38 PrevPage string
39 Posts []PostItemData
40 }
41
42 type PostPageData struct {
43- Site SitePageData
44+ Site shared.SitePageData
45 PageTitle string
46 URL template.URL
47 BlogURL template.URL
48@@ -68,7 +70,7 @@ type PostPageData struct {
49 }
50
51 type TransparencyPageData struct {
52- Site SitePageData
53+ Site shared.SitePageData
54 Analytics *db.Analytics
55 }
56
57@@ -76,7 +78,7 @@ func isRequestTrackable(r *http.Request) bool {
58 return true
59 }
60
61-func renderTemplate(cfg *ConfigSite, templates []string) (*template.Template, error) {
62+func renderTemplate(cfg *shared.ConfigSite, templates []string) (*template.Template, error) {
63 files := make([]string, len(templates))
64 copy(files, templates)
65 files = append(
66@@ -95,8 +97,8 @@ func renderTemplate(cfg *ConfigSite, templates []string) (*template.Template, er
67
68 func createPageHandler(fname string) http.HandlerFunc {
69 return func(w http.ResponseWriter, r *http.Request) {
70- logger := GetLogger(r)
71- cfg := GetCfg(r)
72+ logger := shared.GetLogger(r)
73+ cfg := shared.GetCfg(r)
74 ts, err := renderTemplate(cfg, []string{cfg.StaticPath(fname)})
75
76 if err != nil {
77@@ -130,20 +132,20 @@ type ReadmeTxt struct {
78 }
79
80 func GetUsernameFromRequest(r *http.Request) string {
81- subdomain := GetSubdomain(r)
82- cfg := GetCfg(r)
83+ subdomain := shared.GetSubdomain(r)
84+ cfg := shared.GetCfg(r)
85
86 if !cfg.IsSubdomains() || subdomain == "" {
87- return GetField(r, 0)
88+ return shared.GetField(r, 0)
89 }
90 return subdomain
91 }
92
93 func blogHandler(w http.ResponseWriter, r *http.Request) {
94 username := GetUsernameFromRequest(r)
95- dbpool := GetDB(r)
96- logger := GetLogger(r)
97- cfg := GetCfg(r)
98+ dbpool := shared.GetDB(r)
99+ logger := shared.GetLogger(r)
100+ cfg := shared.GetCfg(r)
101
102 user, err := dbpool.FindUserForName(username)
103 if err != nil {
104@@ -158,6 +160,12 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
105 return
106 }
107
108+ hostDomain := strings.Split(r.Host, ":")[0]
109+ appDomain := strings.Split(cfg.ConfigCms.Domain, ":")[0]
110+
111+ onSubdomain := cfg.IsSubdomains() && strings.Contains(hostDomain, appDomain)
112+ withUserName := (!onSubdomain && hostDomain == appDomain) || !cfg.IsCustomdomains()
113+
114 ts, err := renderTemplate(cfg, []string{
115 cfg.StaticPath("html/blog.page.tmpl"),
116 cfg.StaticPath("html/list.partial.tmpl"),
117@@ -200,12 +208,12 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
118 }
119 } else {
120 p := PostItemData{
121- URL: template.URL(cfg.PostURL(post.Username, post.Filename)),
122- BlogURL: template.URL(cfg.BlogURL(post.Username)),
123- Title: FilenameToTitle(post.Filename, post.Title),
124+ URL: template.URL(cfg.FullPostURL(post.Username, post.Filename, onSubdomain, withUserName)),
125+ BlogURL: template.URL(cfg.FullBlogURL(post.Username, onSubdomain, withUserName)),
126+ Title: shared.FilenameToTitle(post.Filename, post.Title),
127 PublishAt: post.PublishAt.Format("02 Jan, 2006"),
128 PublishAtISO: post.PublishAt.Format(time.RFC3339),
129- UpdatedTimeAgo: TimeAgo(post.UpdatedAt),
130+ UpdatedTimeAgo: shared.TimeAgo(post.UpdatedAt),
131 UpdatedAtISO: post.UpdatedAt.Format(time.RFC3339),
132 }
133 postCollection = append(postCollection, p)
134@@ -215,8 +223,8 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
135 data := BlogPageData{
136 Site: *cfg.GetSiteData(),
137 PageTitle: headerTxt.Title,
138- URL: template.URL(cfg.BlogURL(username)),
139- RSSURL: template.URL(cfg.RssBlogURL(username)),
140+ URL: template.URL(cfg.FullBlogURL(username, onSubdomain, withUserName)),
141+ RSSURL: template.URL(cfg.RssBlogURL(username, onSubdomain, withUserName)),
142 Readme: readmeTxt,
143 Header: headerTxt,
144 Username: username,
145@@ -244,18 +252,18 @@ func GetBlogName(username string) string {
146
147 func postHandler(w http.ResponseWriter, r *http.Request) {
148 username := GetUsernameFromRequest(r)
149- subdomain := GetSubdomain(r)
150- cfg := GetCfg(r)
151+ subdomain := shared.GetSubdomain(r)
152+ cfg := shared.GetCfg(r)
153
154 var filename string
155 if !cfg.IsSubdomains() || subdomain == "" {
156- filename, _ = url.PathUnescape(GetField(r, 1))
157+ filename, _ = url.PathUnescape(shared.GetField(r, 1))
158 } else {
159- filename, _ = url.PathUnescape(GetField(r, 0))
160+ filename, _ = url.PathUnescape(shared.GetField(r, 0))
161 }
162
163- dbpool := GetDB(r)
164- logger := GetLogger(r)
165+ dbpool := shared.GetDB(r)
166+ logger := shared.GetLogger(r)
167
168 user, err := dbpool.FindUserForName(username)
169 if err != nil {
170@@ -302,7 +310,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
171 BlogURL: template.URL(cfg.BlogURL(username)),
172 Description: post.Description,
173 ListType: parsedText.MetaData.ListType,
174- Title: FilenameToTitle(post.Filename, post.Title),
175+ Title: shared.FilenameToTitle(post.Filename, post.Title),
176 PublishAt: post.PublishAt.Format("02 Jan, 2006"),
177 PublishAtISO: post.PublishAt.Format(time.RFC3339),
178 Username: username,
179@@ -348,9 +356,9 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
180 }
181
182 func transparencyHandler(w http.ResponseWriter, r *http.Request) {
183- dbpool := GetDB(r)
184- logger := GetLogger(r)
185- cfg := GetCfg(r)
186+ dbpool := shared.GetDB(r)
187+ logger := shared.GetLogger(r)
188+ cfg := shared.GetCfg(r)
189
190 analytics, err := dbpool.FindSiteAnalytics(cfg.Space)
191 if err != nil {
192@@ -382,9 +390,9 @@ func transparencyHandler(w http.ResponseWriter, r *http.Request) {
193 }
194
195 func readHandler(w http.ResponseWriter, r *http.Request) {
196- dbpool := GetDB(r)
197- logger := GetLogger(r)
198- cfg := GetCfg(r)
199+ dbpool := shared.GetDB(r)
200+ logger := shared.GetLogger(r)
201+ cfg := shared.GetCfg(r)
202
203 page, _ := strconv.Atoi(r.URL.Query().Get("page"))
204 pager, err := dbpool.FindAllUpdatedPosts(&db.Pager{Num: 30, Page: page}, cfg.Space)
205@@ -421,12 +429,12 @@ func readHandler(w http.ResponseWriter, r *http.Request) {
206 item := PostItemData{
207 URL: template.URL(cfg.PostURL(post.Username, post.Filename)),
208 BlogURL: template.URL(cfg.BlogURL(post.Username)),
209- Title: FilenameToTitle(post.Filename, post.Title),
210+ Title: shared.FilenameToTitle(post.Filename, post.Title),
211 Description: post.Description,
212 Username: post.Username,
213 PublishAt: post.PublishAt.Format("02 Jan, 2006"),
214 PublishAtISO: post.PublishAt.Format(time.RFC3339),
215- UpdatedTimeAgo: TimeAgo(post.UpdatedAt),
216+ UpdatedTimeAgo: shared.TimeAgo(post.UpdatedAt),
217 UpdatedAtISO: post.UpdatedAt.Format(time.RFC3339),
218 }
219 data.Posts = append(data.Posts, item)
220@@ -441,9 +449,9 @@ func readHandler(w http.ResponseWriter, r *http.Request) {
221
222 func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
223 username := GetUsernameFromRequest(r)
224- dbpool := GetDB(r)
225- logger := GetLogger(r)
226- cfg := GetCfg(r)
227+ dbpool := shared.GetDB(r)
228+ logger := shared.GetLogger(r)
229+ cfg := shared.GetCfg(r)
230
231 user, err := dbpool.FindUserForName(username)
232 if err != nil {
233@@ -513,7 +521,7 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
234
235 item := &feeds.Item{
236 Id: cfg.PostURL(post.Username, post.Filename),
237- Title: FilenameToTitle(post.Filename, post.Title),
238+ Title: shared.FilenameToTitle(post.Filename, post.Title),
239 Link: &feeds.Link{Href: cfg.PostURL(post.Username, post.Filename)},
240 Content: tpl.String(),
241 Created: *post.PublishAt,
242@@ -541,9 +549,9 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
243 }
244
245 func rssHandler(w http.ResponseWriter, r *http.Request) {
246- dbpool := GetDB(r)
247- logger := GetLogger(r)
248- cfg := GetCfg(r)
249+ dbpool := shared.GetDB(r)
250+ logger := shared.GetLogger(r)
251+ cfg := shared.GetCfg(r)
252
253 pager, err := dbpool.FindAllPosts(&db.Pager{Num: 25, Page: 0}, cfg.Space)
254 if err != nil {
255@@ -613,8 +621,8 @@ func rssHandler(w http.ResponseWriter, r *http.Request) {
256
257 func serveFile(file string, contentType string) http.HandlerFunc {
258 return func(w http.ResponseWriter, r *http.Request) {
259- logger := GetLogger(r)
260- cfg := GetCfg(r)
261+ logger := shared.GetLogger(r)
262+ cfg := shared.GetCfg(r)
263
264 contents, err := ioutil.ReadFile(cfg.StaticPath(fmt.Sprintf("public/%s", file)))
265 if err != nil {
266@@ -631,27 +639,27 @@ func serveFile(file string, contentType string) http.HandlerFunc {
267 }
268 }
269
270-func createStaticRoutes() []Route {
271- return []Route{
272- NewRoute("GET", "/main.css", serveFile("main.css", "text/css")),
273- NewRoute("GET", "/card.png", serveFile("card.png", "image/png")),
274- NewRoute("GET", "/favicon-16x16.png", serveFile("favicon-16x16.png", "image/png")),
275- NewRoute("GET", "/favicon-32x32.png", serveFile("favicon-32x32.png", "image/png")),
276- NewRoute("GET", "/apple-touch-icon.png", serveFile("apple-touch-icon.png", "image/png")),
277- NewRoute("GET", "/favicon.ico", serveFile("favicon.ico", "image/x-icon")),
278- NewRoute("GET", "/robots.txt", serveFile("robots.txt", "text/plain")),
279+func createStaticRoutes() []shared.Route {
280+ return []shared.Route{
281+ shared.NewRoute("GET", "/main.css", serveFile("main.css", "text/css")),
282+ shared.NewRoute("GET", "/card.png", serveFile("card.png", "image/png")),
283+ shared.NewRoute("GET", "/favicon-16x16.png", serveFile("favicon-16x16.png", "image/png")),
284+ shared.NewRoute("GET", "/favicon-32x32.png", serveFile("favicon-32x32.png", "image/png")),
285+ shared.NewRoute("GET", "/apple-touch-icon.png", serveFile("apple-touch-icon.png", "image/png")),
286+ shared.NewRoute("GET", "/favicon.ico", serveFile("favicon.ico", "image/x-icon")),
287+ shared.NewRoute("GET", "/robots.txt", serveFile("robots.txt", "text/plain")),
288 }
289 }
290
291-func createMainRoutes(staticRoutes []Route) []Route {
292- routes := []Route{
293- NewRoute("GET", "/", createPageHandler("html/marketing.page.tmpl")),
294- NewRoute("GET", "/spec", createPageHandler("html/spec.page.tmpl")),
295- NewRoute("GET", "/ops", createPageHandler("html/ops.page.tmpl")),
296- NewRoute("GET", "/privacy", createPageHandler("html/privacy.page.tmpl")),
297- NewRoute("GET", "/help", createPageHandler("html/help.page.tmpl")),
298- NewRoute("GET", "/transparency", transparencyHandler),
299- NewRoute("GET", "/read", readHandler),
300+func createMainRoutes(staticRoutes []shared.Route) []shared.Route {
301+ routes := []shared.Route{
302+ shared.NewRoute("GET", "/", createPageHandler("html/marketing.page.tmpl")),
303+ shared.NewRoute("GET", "/spec", createPageHandler("html/spec.page.tmpl")),
304+ shared.NewRoute("GET", "/ops", createPageHandler("html/ops.page.tmpl")),
305+ shared.NewRoute("GET", "/privacy", createPageHandler("html/privacy.page.tmpl")),
306+ shared.NewRoute("GET", "/help", createPageHandler("html/help.page.tmpl")),
307+ shared.NewRoute("GET", "/transparency", transparencyHandler),
308+ shared.NewRoute("GET", "/read", readHandler),
309 }
310
311 routes = append(
312@@ -661,23 +669,23 @@ func createMainRoutes(staticRoutes []Route) []Route {
313
314 routes = append(
315 routes,
316- NewRoute("GET", "/rss", rssHandler),
317- NewRoute("GET", "/rss.xml", rssHandler),
318- NewRoute("GET", "/atom.xml", rssHandler),
319- NewRoute("GET", "/feed.xml", rssHandler),
320-
321- NewRoute("GET", "/([^/]+)", blogHandler),
322- NewRoute("GET", "/([^/]+)/rss", rssBlogHandler),
323- NewRoute("GET", "/([^/]+)/([^/]+)", postHandler),
324+ shared.NewRoute("GET", "/rss", rssHandler),
325+ shared.NewRoute("GET", "/rss.xml", rssHandler),
326+ shared.NewRoute("GET", "/atom.xml", rssHandler),
327+ shared.NewRoute("GET", "/feed.xml", rssHandler),
328+
329+ shared.NewRoute("GET", "/([^/]+)", blogHandler),
330+ shared.NewRoute("GET", "/([^/]+)/rss", rssBlogHandler),
331+ shared.NewRoute("GET", "/([^/]+)/([^/]+)", postHandler),
332 )
333
334 return routes
335 }
336
337-func createSubdomainRoutes(staticRoutes []Route) []Route {
338- routes := []Route{
339- NewRoute("GET", "/", blogHandler),
340- NewRoute("GET", "/rss", rssBlogHandler),
341+func createSubdomainRoutes(staticRoutes []shared.Route) []shared.Route {
342+ routes := []shared.Route{
343+ shared.NewRoute("GET", "/", blogHandler),
344+ shared.NewRoute("GET", "/rss", rssBlogHandler),
345 }
346
347 routes = append(
348@@ -687,7 +695,7 @@ func createSubdomainRoutes(staticRoutes []Route) []Route {
349
350 routes = append(
351 routes,
352- NewRoute("GET", "/([^/]+)", postHandler),
353+ shared.NewRoute("GET", "/([^/]+)", postHandler),
354 )
355
356 return routes
357@@ -703,7 +711,7 @@ func StartApiServer() {
358 mainRoutes := createMainRoutes(staticRoutes)
359 subdomainRoutes := createSubdomainRoutes(staticRoutes)
360
361- handler := CreateServe(mainRoutes, subdomainRoutes, cfg, db, logger)
362+ handler := shared.CreateServe(mainRoutes, subdomainRoutes, cfg, db, logger)
363 router := http.HandlerFunc(handler)
364
365 portStr := fmt.Sprintf(":%s", cfg.Port)
+10,
-92
1@@ -2,34 +2,18 @@ package lists
2
3 import (
4 "fmt"
5- "html/template"
6- "log"
7- "net/url"
8- "path"
9
10+ "git.sr.ht/~erock/pico/shared"
11 "git.sr.ht/~erock/pico/wish/cms/config"
12- "go.uber.org/zap"
13 )
14
15-type SitePageData struct {
16- Domain template.URL
17- HomeURL template.URL
18- Email string
19-}
20-
21-type ConfigSite struct {
22- config.ConfigCms
23- config.ConfigURL
24- SubdomainsEnabled bool
25-}
26-
27-func NewConfigSite() *ConfigSite {
28- domain := GetEnv("LISTS_DOMAIN", "lists.sh")
29- email := GetEnv("LISTS_EMAIL", "support@lists.sh")
30- subdomains := GetEnv("LISTS_SUBDOMAINS", "0")
31- port := GetEnv("LISTS_WEB_PORT", "3000")
32- protocol := GetEnv("LISTS_PROTOCOL", "https")
33- dbURL := GetEnv("DATABASE_URL", "")
34+func NewConfigSite() *shared.ConfigSite {
35+ domain := shared.GetEnv("LISTS_DOMAIN", "lists.sh")
36+ email := shared.GetEnv("LISTS_EMAIL", "support@lists.sh")
37+ subdomains := shared.GetEnv("LISTS_SUBDOMAINS", "0")
38+ port := shared.GetEnv("LISTS_WEB_PORT", "3000")
39+ protocol := shared.GetEnv("LISTS_PROTOCOL", "https")
40+ dbURL := shared.GetEnv("DATABASE_URL", "")
41 subdomainsEnabled := false
42 if subdomains == "1" {
43 subdomainsEnabled = true
44@@ -41,7 +25,7 @@ func NewConfigSite() *ConfigSite {
45 intro += "Finally, send your list files to us:\n\n"
46 intro += fmt.Sprintf("scp ~/blog/*.txt %s:/\n\n", domain)
47
48- return &ConfigSite{
49+ return &shared.ConfigSite{
50 SubdomainsEnabled: subdomainsEnabled,
51 ConfigCms: config.ConfigCms{
52 Domain: domain,
53@@ -52,73 +36,7 @@ func NewConfigSite() *ConfigSite {
54 Description: "A microblog for your lists.",
55 IntroText: intro,
56 Space: "lists",
57- Logger: CreateLogger(),
58+ Logger: shared.CreateLogger(),
59 },
60 }
61 }
62-
63-func (c *ConfigSite) GetSiteData() *SitePageData {
64- return &SitePageData{
65- Domain: template.URL(c.Domain),
66- HomeURL: template.URL(c.HomeURL()),
67- Email: c.Email,
68- }
69-}
70-
71-func (c *ConfigSite) BlogURL(username string) string {
72- if c.IsSubdomains() {
73- return fmt.Sprintf("%s://%s.%s", c.Protocol, username, c.Domain)
74- }
75-
76- return fmt.Sprintf("/%s", username)
77-}
78-
79-func (c *ConfigSite) PostURL(username, filename string) string {
80- fname := url.PathEscape(filename)
81- if c.IsSubdomains() {
82- return fmt.Sprintf("%s://%s.%s/%s", c.Protocol, username, c.Domain, fname)
83- }
84-
85- return fmt.Sprintf("/%s/%s", username, fname)
86-}
87-
88-func (c *ConfigSite) IsSubdomains() bool {
89- return c.SubdomainsEnabled
90-}
91-
92-func (c *ConfigSite) RssBlogURL(username string) string {
93- if c.IsSubdomains() {
94- return fmt.Sprintf("%s://%s.%s/rss", c.Protocol, username, c.Domain)
95- }
96-
97- return fmt.Sprintf("/%s/rss", username)
98-}
99-
100-func (c *ConfigSite) HomeURL() string {
101- if c.IsSubdomains() {
102- return fmt.Sprintf("%s://%s", c.Protocol, c.Domain)
103- }
104-
105- return "/"
106-}
107-
108-func (c *ConfigSite) ReadURL() string {
109- if c.IsSubdomains() {
110- return fmt.Sprintf("%s://%s/read", c.Protocol, c.Domain)
111- }
112-
113- return "/read"
114-}
115-
116-func (c *ConfigSite) StaticPath(fname string) string {
117- return path.Join(c.Space, fname)
118-}
119-
120-func CreateLogger() *zap.SugaredLogger {
121- logger, err := zap.NewProduction()
122- if err != nil {
123- log.Fatal(err)
124- }
125-
126- return logger.Sugar()
127-}
+6,
-4
1@@ -6,6 +6,7 @@ import (
2 "time"
3
4 "git.sr.ht/~erock/pico/lists/pkg"
5+ "git.sr.ht/~erock/pico/shared"
6 "git.sr.ht/~erock/pico/wish/cms/db"
7 "git.sr.ht/~erock/pico/wish/cms/util"
8 sendutils "git.sr.ht/~erock/pico/wish/send/utils"
9@@ -14,6 +15,7 @@ import (
10 )
11
12 var HiddenPosts = []string{"_readme", "_header"}
13+var allowedExtensions = []string{".txt"}
14
15 type Opener struct {
16 entry *sendutils.FileEntry
17@@ -26,10 +28,10 @@ func (o *Opener) Open(name string) (io.Reader, error) {
18 type DbHandler struct {
19 User *db.User
20 DBPool db.DB
21- Cfg *ConfigSite
22+ Cfg *shared.ConfigSite
23 }
24
25-func NewDbHandler(dbpool db.DB, cfg *ConfigSite) *DbHandler {
26+func NewDbHandler(dbpool db.DB, cfg *shared.ConfigSite) *DbHandler {
27 return &DbHandler{
28 DBPool: dbpool,
29 Cfg: cfg,
30@@ -59,7 +61,7 @@ func (h *DbHandler) Validate(s ssh.Session) error {
31 func (h *DbHandler) Write(s ssh.Session, entry *sendutils.FileEntry) (string, error) {
32 logger := h.Cfg.Logger
33 userID := h.User.ID
34- filename := SanitizeFileExt(entry.Name)
35+ filename := shared.SanitizeFileExt(entry.Name)
36 title := filename
37
38 post, err := h.DBPool.FindPostWithFilename(filename, userID, h.Cfg.Space)
39@@ -77,7 +79,7 @@ func (h *DbHandler) Write(s ssh.Session, entry *sendutils.FileEntry) (string, er
40 text = string(b)
41 }
42
43- if !IsTextFile(text, entry.Filepath) {
44+ if !shared.IsTextFile(text, entry.Filepath, allowedExtensions) {
45 return "", fmt.Errorf("WARNING: (%s) invalid file, format must be '.txt' and the contents must be plain text, skipping", entry.Name)
46 }
47
+19,
-12
1@@ -18,12 +18,13 @@ import (
2 feeds "git.sr.ht/~aw/gorilla-feeds"
3 "git.sr.ht/~erock/pico/lists"
4 "git.sr.ht/~erock/pico/lists/pkg"
5+ "git.sr.ht/~erock/pico/shared"
6 "git.sr.ht/~erock/pico/wish/cms/db"
7 "git.sr.ht/~erock/pico/wish/cms/db/postgres"
8 "golang.org/x/exp/slices"
9 )
10
11-func renderTemplate(cfg *lists.ConfigSite, templates []string) (*template.Template, error) {
12+func renderTemplate(cfg *shared.ConfigSite, templates []string) (*template.Template, error) {
13 files := make([]string, len(templates))
14 copy(files, templates)
15 files = append(
16@@ -82,6 +83,12 @@ func blogHandler(ctx context.Context, w gemini.ResponseWriter, r *gemini.Request
17 return
18 }
19
20+ hostDomain := strings.Split(r.Host, ":")[0]
21+ appDomain := strings.Split(cfg.ConfigCms.Domain, ":")[0]
22+
23+ onSubdomain := cfg.IsSubdomains() && strings.Contains(hostDomain, appDomain)
24+ withUserName := (!onSubdomain && hostDomain == appDomain) || !cfg.IsCustomdomains()
25+
26 ts, err := renderTemplate(cfg, []string{
27 cfg.StaticPath("gmi/blog.page.tmpl"),
28 cfg.StaticPath("gmi/list.partial.tmpl"),
29@@ -124,12 +131,12 @@ func blogHandler(ctx context.Context, w gemini.ResponseWriter, r *gemini.Request
30 }
31 } else {
32 p := lists.PostItemData{
33- URL: html.URL(cfg.PostURL(post.Username, post.Filename)),
34- BlogURL: html.URL(cfg.BlogURL(post.Username)),
35- Title: lists.FilenameToTitle(post.Filename, post.Title),
36+ URL: html.URL(cfg.FullPostURL(post.Username, post.Filename, onSubdomain, withUserName)),
37+ BlogURL: html.URL(cfg.FullBlogURL(post.Username, onSubdomain, withUserName)),
38+ Title: shared.FilenameToTitle(post.Filename, post.Title),
39 PublishAt: post.PublishAt.Format("02 Jan, 2006"),
40 PublishAtISO: post.PublishAt.Format(time.RFC3339),
41- UpdatedTimeAgo: lists.TimeAgo(post.UpdatedAt),
42+ UpdatedTimeAgo: shared.TimeAgo(post.UpdatedAt),
43 UpdatedAtISO: post.UpdatedAt.Format(time.RFC3339),
44 }
45 postCollection = append(postCollection, p)
46@@ -139,8 +146,8 @@ func blogHandler(ctx context.Context, w gemini.ResponseWriter, r *gemini.Request
47 data := lists.BlogPageData{
48 Site: *cfg.GetSiteData(),
49 PageTitle: headerTxt.Title,
50- URL: html.URL(cfg.BlogURL(username)),
51- RSSURL: html.URL(cfg.RssBlogURL(username)),
52+ URL: html.URL(cfg.FullBlogURL(username, onSubdomain, withUserName)),
53+ RSSURL: html.URL(cfg.RssBlogURL(username, onSubdomain, withUserName)),
54 Readme: readmeTxt,
55 Header: headerTxt,
56 Username: username,
57@@ -194,7 +201,7 @@ func readHandler(ctx context.Context, w gemini.ResponseWriter, r *gemini.Request
58
59 longest := 0
60 for _, post := range pager.Data {
61- size := len(lists.TimeAgo(post.UpdatedAt))
62+ size := len(shared.TimeAgo(post.UpdatedAt))
63 if size > longest {
64 longest = size
65 }
66@@ -204,12 +211,12 @@ func readHandler(ctx context.Context, w gemini.ResponseWriter, r *gemini.Request
67 item := lists.PostItemData{
68 URL: html.URL(cfg.PostURL(post.Username, post.Filename)),
69 BlogURL: html.URL(cfg.BlogURL(post.Username)),
70- Title: lists.FilenameToTitle(post.Filename, post.Title),
71+ Title: shared.FilenameToTitle(post.Filename, post.Title),
72 Description: post.Description,
73 Username: post.Username,
74 PublishAt: post.PublishAt.Format("02 Jan, 2006"),
75 PublishAtISO: post.PublishAt.Format(time.RFC3339),
76- UpdatedTimeAgo: lists.TimeAgo(post.UpdatedAt),
77+ UpdatedTimeAgo: shared.TimeAgo(post.UpdatedAt),
78 UpdatedAtISO: post.UpdatedAt.Format(time.RFC3339),
79 }
80
81@@ -278,7 +285,7 @@ func postHandler(ctx context.Context, w gemini.ResponseWriter, r *gemini.Request
82 BlogURL: html.URL(cfg.BlogURL(username)),
83 Description: post.Description,
84 ListType: parsedText.MetaData.ListType,
85- Title: lists.FilenameToTitle(post.Filename, post.Title),
86+ Title: shared.FilenameToTitle(post.Filename, post.Title),
87 PublishAt: post.PublishAt.Format("02 Jan, 2006"),
88 PublishAtISO: post.PublishAt.Format(time.RFC3339),
89 Username: username,
90@@ -411,7 +418,7 @@ func rssBlogHandler(ctx context.Context, w gemini.ResponseWriter, r *gemini.Requ
91
92 item := &feeds.Item{
93 Id: cfg.PostURL(post.Username, post.Filename),
94- Title: lists.FilenameToTitle(post.Filename, post.Title),
95+ Title: shared.FilenameToTitle(post.Filename, post.Title),
96 Link: &feeds.Link{Href: cfg.PostURL(post.Username, post.Filename)},
97 Content: tpl.String(),
98 Created: *post.PublishAt,
+4,
-4
1@@ -5,7 +5,7 @@ import (
2 "regexp"
3
4 "git.sr.ht/~adnano/go-gemini"
5- "git.sr.ht/~erock/pico/lists"
6+ "git.sr.ht/~erock/pico/shared"
7 "git.sr.ht/~erock/pico/wish/cms/db"
8 "go.uber.org/zap"
9 )
10@@ -19,8 +19,8 @@ func GetLogger(ctx context.Context) *zap.SugaredLogger {
11 return ctx.Value(ctxLoggerKey{}).(*zap.SugaredLogger)
12 }
13
14-func GetCfg(ctx context.Context) *lists.ConfigSite {
15- return ctx.Value(ctxCfgKey{}).(*lists.ConfigSite)
16+func GetCfg(ctx context.Context) *shared.ConfigSite {
17+ return ctx.Value(ctxCfgKey{}).(*shared.ConfigSite)
18 }
19
20 func GetDB(ctx context.Context) db.DB {
21@@ -46,7 +46,7 @@ func NewRoute(pattern string, handler gemini.HandlerFunc) Route {
22
23 type ServeFn func(context.Context, gemini.ResponseWriter, *gemini.Request)
24
25-func CreateServe(routes []Route, cfg *lists.ConfigSite, dbpool db.DB, logger *zap.SugaredLogger) ServeFn {
26+func CreateServe(routes []Route, cfg *shared.ConfigSite, dbpool db.DB, logger *zap.SugaredLogger) ServeFn {
27 return func(ctx context.Context, w gemini.ResponseWriter, r *gemini.Request) {
28 curRoutes := routes
29
+0,
-97
1@@ -1,97 +0,0 @@
2-package lists
3-
4-import (
5- "context"
6- "fmt"
7- "net/http"
8- "regexp"
9- "strings"
10-
11- "git.sr.ht/~erock/pico/wish/cms/db"
12- "go.uber.org/zap"
13-)
14-
15-type Route struct {
16- method string
17- regex *regexp.Regexp
18- handler http.HandlerFunc
19-}
20-
21-func NewRoute(method, pattern string, handler http.HandlerFunc) Route {
22- return Route{
23- method,
24- regexp.MustCompile("^" + pattern + "$"),
25- handler,
26- }
27-}
28-
29-type ServeFn func(http.ResponseWriter, *http.Request)
30-
31-func CreateServe(routes []Route, subdomainRoutes []Route, cfg *ConfigSite, dbpool db.DB, logger *zap.SugaredLogger) ServeFn {
32- return func(w http.ResponseWriter, r *http.Request) {
33- var allow []string
34- curRoutes := routes
35-
36- hostDomain := strings.ToLower(strings.Split(r.Host, ":")[0])
37- appDomain := strings.ToLower(strings.Split(cfg.ConfigCms.Domain, ":")[0])
38-
39- subdomain := ""
40- if hostDomain != appDomain && strings.Contains(hostDomain, appDomain) {
41- subdomain = strings.TrimSuffix(hostDomain, fmt.Sprintf(".%s", appDomain))
42- }
43-
44- if cfg.IsSubdomains() && subdomain != "" {
45- curRoutes = subdomainRoutes
46- }
47-
48- for _, route := range curRoutes {
49- matches := route.regex.FindStringSubmatch(r.URL.Path)
50- if len(matches) > 0 {
51- if r.Method != route.method {
52- allow = append(allow, route.method)
53- continue
54- }
55- loggerCtx := context.WithValue(r.Context(), ctxLoggerKey{}, logger)
56- subdomainCtx := context.WithValue(loggerCtx, ctxSubdomainKey{}, subdomain)
57- dbCtx := context.WithValue(subdomainCtx, ctxDBKey{}, dbpool)
58- cfgCtx := context.WithValue(dbCtx, ctxCfg{}, cfg)
59- ctx := context.WithValue(cfgCtx, ctxKey{}, matches[1:])
60- route.handler(w, r.WithContext(ctx))
61- return
62- }
63- }
64- if len(allow) > 0 {
65- w.Header().Set("Allow", strings.Join(allow, ", "))
66- http.Error(w, "405 method not allowed", http.StatusMethodNotAllowed)
67- return
68- }
69- http.NotFound(w, r)
70- }
71-}
72-
73-type ctxDBKey struct{}
74-type ctxKey struct{}
75-type ctxLoggerKey struct{}
76-type ctxSubdomainKey struct{}
77-type ctxCfg struct{}
78-
79-func GetCfg(r *http.Request) *ConfigSite {
80- return r.Context().Value(ctxCfg{}).(*ConfigSite)
81-}
82-
83-func GetLogger(r *http.Request) *zap.SugaredLogger {
84- return r.Context().Value(ctxLoggerKey{}).(*zap.SugaredLogger)
85-}
86-
87-func GetDB(r *http.Request) db.DB {
88- return r.Context().Value(ctxDBKey{}).(db.DB)
89-}
90-
91-func GetField(r *http.Request, index int) string {
92- fields := r.Context().Value(ctxKey{}).([]string)
93- return fields[index]
94-}
95-
96-func GetSubdomain(r *http.Request) string {
97- return r.Context().Value(ctxSubdomainKey{}).(string)
98-}
M
makefile
+1,
-0
1@@ -41,6 +41,7 @@ build-prose:
2 build-lists:
3 go build -o build/lists-web ./cmd/lists/web
4 go build -o build/lists-ssh ./cmd/lists/ssh
5+ go build -o build/lists-gemini ./cmd/lists/gemini
6 .PHONY: build-lists
7
8 build-pastes:
+74,
-66
1@@ -6,14 +6,16 @@ import (
2 "io/ioutil"
3 "net/http"
4 "net/url"
5+ "strings"
6 "time"
7
8+ "git.sr.ht/~erock/pico/shared"
9 "git.sr.ht/~erock/pico/wish/cms/db"
10 "git.sr.ht/~erock/pico/wish/cms/db/postgres"
11 )
12
13 type PageData struct {
14- Site SitePageData
15+ Site shared.SitePageData
16 }
17
18 type PostItemData struct {
19@@ -30,7 +32,7 @@ type PostItemData struct {
20 }
21
22 type BlogPageData struct {
23- Site SitePageData
24+ Site shared.SitePageData
25 PageTitle string
26 URL template.URL
27 RSSURL template.URL
28@@ -40,7 +42,7 @@ type BlogPageData struct {
29 }
30
31 type PostPageData struct {
32- Site SitePageData
33+ Site shared.SitePageData
34 PageTitle string
35 URL template.URL
36 RawURL template.URL
37@@ -55,11 +57,11 @@ type PostPageData struct {
38 }
39
40 type TransparencyPageData struct {
41- Site SitePageData
42+ Site shared.SitePageData
43 Analytics *db.Analytics
44 }
45
46-func renderTemplate(cfg *ConfigSite, templates []string) (*template.Template, error) {
47+func renderTemplate(cfg *shared.ConfigSite, templates []string) (*template.Template, error) {
48 files := make([]string, len(templates))
49 copy(files, templates)
50 files = append(
51@@ -78,8 +80,8 @@ func renderTemplate(cfg *ConfigSite, templates []string) (*template.Template, er
52
53 func createPageHandler(fname string) http.HandlerFunc {
54 return func(w http.ResponseWriter, r *http.Request) {
55- logger := GetLogger(r)
56- cfg := GetCfg(r)
57+ logger := shared.GetLogger(r)
58+ cfg := shared.GetCfg(r)
59 ts, err := renderTemplate(cfg, []string{cfg.StaticPath(fname)})
60
61 if err != nil {
62@@ -112,20 +114,20 @@ type HeaderTxt struct {
63 }
64
65 func GetUsernameFromRequest(r *http.Request) string {
66- subdomain := GetSubdomain(r)
67- cfg := GetCfg(r)
68+ subdomain := shared.GetSubdomain(r)
69+ cfg := shared.GetCfg(r)
70
71 if !cfg.IsSubdomains() || subdomain == "" {
72- return GetField(r, 0)
73+ return shared.GetField(r, 0)
74 }
75 return subdomain
76 }
77
78 func blogHandler(w http.ResponseWriter, r *http.Request) {
79 username := GetUsernameFromRequest(r)
80- dbpool := GetDB(r)
81- logger := GetLogger(r)
82- cfg := GetCfg(r)
83+ dbpool := shared.GetDB(r)
84+ logger := shared.GetLogger(r)
85+ cfg := shared.GetCfg(r)
86
87 user, err := dbpool.FindUserForName(username)
88 if err != nil {
89@@ -140,6 +142,12 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
90 return
91 }
92
93+ hostDomain := strings.Split(r.Host, ":")[0]
94+ appDomain := strings.Split(cfg.ConfigCms.Domain, ":")[0]
95+
96+ onSubdomain := cfg.IsSubdomains() && strings.Contains(hostDomain, appDomain)
97+ withUserName := (!onSubdomain && hostDomain == appDomain) || !cfg.IsCustomdomains()
98+
99 ts, err := renderTemplate(cfg, []string{
100 cfg.StaticPath("html/blog.page.tmpl"),
101 })
102@@ -158,12 +166,12 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
103 postCollection := make([]PostItemData, 0, len(posts))
104 for _, post := range posts {
105 p := PostItemData{
106- URL: template.URL(cfg.PostURL(post.Username, post.Filename)),
107- BlogURL: template.URL(cfg.BlogURL(post.Username)),
108- Title: FilenameToTitle(post.Filename, post.Title),
109+ URL: template.URL(cfg.FullPostURL(post.Username, post.Filename, onSubdomain, withUserName)),
110+ BlogURL: template.URL(cfg.FullBlogURL(post.Username, onSubdomain, withUserName)),
111+ Title: shared.FilenameToTitle(post.Filename, post.Title),
112 PublishAt: post.PublishAt.Format("02 Jan, 2006"),
113 PublishAtISO: post.PublishAt.Format(time.RFC3339),
114- UpdatedTimeAgo: TimeAgo(post.UpdatedAt),
115+ UpdatedTimeAgo: shared.TimeAgo(post.UpdatedAt),
116 UpdatedAtISO: post.UpdatedAt.Format(time.RFC3339),
117 }
118 postCollection = append(postCollection, p)
119@@ -172,8 +180,8 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
120 data := BlogPageData{
121 Site: *cfg.GetSiteData(),
122 PageTitle: headerTxt.Title,
123- URL: template.URL(cfg.BlogURL(username)),
124- RSSURL: template.URL(cfg.RssBlogURL(username)),
125+ URL: template.URL(cfg.FullBlogURL(username, onSubdomain, withUserName)),
126+ RSSURL: template.URL(cfg.RssBlogURL(username, onSubdomain, withUserName)),
127 Header: headerTxt,
128 Username: username,
129 Posts: postCollection,
130@@ -200,18 +208,18 @@ func GetBlogName(username string) string {
131
132 func postHandler(w http.ResponseWriter, r *http.Request) {
133 username := GetUsernameFromRequest(r)
134- subdomain := GetSubdomain(r)
135- cfg := GetCfg(r)
136+ subdomain := shared.GetSubdomain(r)
137+ cfg := shared.GetCfg(r)
138
139 var filename string
140 if !cfg.IsSubdomains() || subdomain == "" {
141- filename, _ = url.PathUnescape(GetField(r, 1))
142+ filename, _ = url.PathUnescape(shared.GetField(r, 1))
143 } else {
144- filename, _ = url.PathUnescape(GetField(r, 0))
145+ filename, _ = url.PathUnescape(shared.GetField(r, 0))
146 }
147
148- dbpool := GetDB(r)
149- logger := GetLogger(r)
150+ dbpool := shared.GetDB(r)
151+ logger := shared.GetLogger(r)
152
153 user, err := dbpool.FindUserForName(username)
154 if err != nil {
155@@ -237,7 +245,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
156 RawURL: template.URL(cfg.RawPostURL(post.Username, post.Filename)),
157 BlogURL: template.URL(cfg.BlogURL(username)),
158 Description: post.Description,
159- Title: FilenameToTitle(post.Filename, post.Title),
160+ Title: shared.FilenameToTitle(post.Filename, post.Title),
161 PublishAt: post.PublishAt.Format("02 Jan, 2006"),
162 PublishAtISO: post.PublishAt.Format(time.RFC3339),
163 Username: username,
164@@ -277,18 +285,18 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
165
166 func postHandlerRaw(w http.ResponseWriter, r *http.Request) {
167 username := GetUsernameFromRequest(r)
168- subdomain := GetSubdomain(r)
169- cfg := GetCfg(r)
170+ subdomain := shared.GetSubdomain(r)
171+ cfg := shared.GetCfg(r)
172
173 var filename string
174 if !cfg.IsSubdomains() || subdomain == "" {
175- filename, _ = url.PathUnescape(GetField(r, 1))
176+ filename, _ = url.PathUnescape(shared.GetField(r, 1))
177 } else {
178- filename, _ = url.PathUnescape(GetField(r, 0))
179+ filename, _ = url.PathUnescape(shared.GetField(r, 0))
180 }
181
182- dbpool := GetDB(r)
183- logger := GetLogger(r)
184+ dbpool := shared.GetDB(r)
185+ logger := shared.GetLogger(r)
186
187 user, err := dbpool.FindUserForName(username)
188 if err != nil {
189@@ -312,9 +320,9 @@ func postHandlerRaw(w http.ResponseWriter, r *http.Request) {
190 }
191
192 func transparencyHandler(w http.ResponseWriter, r *http.Request) {
193- dbpool := GetDB(r)
194- logger := GetLogger(r)
195- cfg := GetCfg(r)
196+ dbpool := shared.GetDB(r)
197+ logger := shared.GetLogger(r)
198+ cfg := shared.GetCfg(r)
199
200 analytics, err := dbpool.FindSiteAnalytics(cfg.Space)
201 if err != nil {
202@@ -347,8 +355,8 @@ func transparencyHandler(w http.ResponseWriter, r *http.Request) {
203
204 func serveFile(file string, contentType string) http.HandlerFunc {
205 return func(w http.ResponseWriter, r *http.Request) {
206- logger := GetLogger(r)
207- cfg := GetCfg(r)
208+ logger := shared.GetLogger(r)
209+ cfg := shared.GetCfg(r)
210
211 contents, err := ioutil.ReadFile(cfg.StaticPath(fmt.Sprintf("public/%s", file)))
212 if err != nil {
213@@ -365,27 +373,27 @@ func serveFile(file string, contentType string) http.HandlerFunc {
214 }
215 }
216
217-func createStaticRoutes() []Route {
218- return []Route{
219- NewRoute("GET", "/main.css", serveFile("main.css", "text/css")),
220- NewRoute("GET", "/syntax.css", serveFile("syntax.css", "text/css")),
221- NewRoute("GET", "/card.png", serveFile("card.png", "image/png")),
222- NewRoute("GET", "/favicon-16x16.png", serveFile("favicon-16x16.png", "image/png")),
223- NewRoute("GET", "/favicon-32x32.png", serveFile("favicon-32x32.png", "image/png")),
224- NewRoute("GET", "/apple-touch-icon.png", serveFile("apple-touch-icon.png", "image/png")),
225- NewRoute("GET", "/favicon.ico", serveFile("favicon.ico", "image/x-icon")),
226- NewRoute("GET", "/robots.txt", serveFile("robots.txt", "text/plain")),
227+func createStaticRoutes() []shared.Route {
228+ return []shared.Route{
229+ shared.NewRoute("GET", "/main.css", serveFile("main.css", "text/css")),
230+ shared.NewRoute("GET", "/syntax.css", serveFile("syntax.css", "text/css")),
231+ shared.NewRoute("GET", "/card.png", serveFile("card.png", "image/png")),
232+ shared.NewRoute("GET", "/favicon-16x16.png", serveFile("favicon-16x16.png", "image/png")),
233+ shared.NewRoute("GET", "/favicon-32x32.png", serveFile("favicon-32x32.png", "image/png")),
234+ shared.NewRoute("GET", "/apple-touch-icon.png", serveFile("apple-touch-icon.png", "image/png")),
235+ shared.NewRoute("GET", "/favicon.ico", serveFile("favicon.ico", "image/x-icon")),
236+ shared.NewRoute("GET", "/robots.txt", serveFile("robots.txt", "text/plain")),
237 }
238 }
239
240-func createMainRoutes(staticRoutes []Route) []Route {
241- routes := []Route{
242- NewRoute("GET", "/", createPageHandler("html/marketing.page.tmpl")),
243- NewRoute("GET", "/spec", createPageHandler("html/spec.page.tmpl")),
244- NewRoute("GET", "/ops", createPageHandler("html/ops.page.tmpl")),
245- NewRoute("GET", "/privacy", createPageHandler("html/privacy.page.tmpl")),
246- NewRoute("GET", "/help", createPageHandler("html/help.page.tmpl")),
247- NewRoute("GET", "/transparency", transparencyHandler),
248+func createMainRoutes(staticRoutes []shared.Route) []shared.Route {
249+ routes := []shared.Route{
250+ shared.NewRoute("GET", "/", createPageHandler("html/marketing.page.tmpl")),
251+ shared.NewRoute("GET", "/spec", createPageHandler("html/spec.page.tmpl")),
252+ shared.NewRoute("GET", "/ops", createPageHandler("html/ops.page.tmpl")),
253+ shared.NewRoute("GET", "/privacy", createPageHandler("html/privacy.page.tmpl")),
254+ shared.NewRoute("GET", "/help", createPageHandler("html/help.page.tmpl")),
255+ shared.NewRoute("GET", "/transparency", transparencyHandler),
256 }
257
258 routes = append(
259@@ -395,18 +403,18 @@ func createMainRoutes(staticRoutes []Route) []Route {
260
261 routes = append(
262 routes,
263- NewRoute("GET", "/([^/]+)", blogHandler),
264- NewRoute("GET", "/([^/]+)/([^/]+)", postHandler),
265- NewRoute("GET", "/([^/]+)/([^/]+)/raw", postHandlerRaw),
266- NewRoute("GET", "/raw/([^/]+)/([^/]+)", postHandlerRaw),
267+ shared.NewRoute("GET", "/([^/]+)", blogHandler),
268+ shared.NewRoute("GET", "/([^/]+)/([^/]+)", postHandler),
269+ shared.NewRoute("GET", "/([^/]+)/([^/]+)/raw", postHandlerRaw),
270+ shared.NewRoute("GET", "/raw/([^/]+)/([^/]+)", postHandlerRaw),
271 )
272
273 return routes
274 }
275
276-func createSubdomainRoutes(staticRoutes []Route) []Route {
277- routes := []Route{
278- NewRoute("GET", "/", blogHandler),
279+func createSubdomainRoutes(staticRoutes []shared.Route) []shared.Route {
280+ routes := []shared.Route{
281+ shared.NewRoute("GET", "/", blogHandler),
282 }
283
284 routes = append(
285@@ -416,9 +424,9 @@ func createSubdomainRoutes(staticRoutes []Route) []Route {
286
287 routes = append(
288 routes,
289- NewRoute("GET", "/([^/]+)", postHandler),
290- NewRoute("GET", "/([^/]+)/raw", postHandlerRaw),
291- NewRoute("GET", "/raw/([^/]+)", postHandlerRaw),
292+ shared.NewRoute("GET", "/([^/]+)", postHandler),
293+ shared.NewRoute("GET", "/([^/]+)/raw", postHandlerRaw),
294+ shared.NewRoute("GET", "/raw/([^/]+)", postHandlerRaw),
295 )
296
297 return routes
298@@ -436,7 +444,7 @@ func StartApiServer() {
299 mainRoutes := createMainRoutes(staticRoutes)
300 subdomainRoutes := createSubdomainRoutes(staticRoutes)
301
302- handler := CreateServe(mainRoutes, subdomainRoutes, cfg, db, logger)
303+ handler := shared.CreateServe(mainRoutes, subdomainRoutes, cfg, db, logger)
304 router := http.HandlerFunc(handler)
305
306 portStr := fmt.Sprintf(":%s", cfg.Port)
+10,
-101
1@@ -2,34 +2,18 @@ package pastes
2
3 import (
4 "fmt"
5- "html/template"
6- "log"
7- "net/url"
8- "path"
9
10+ "git.sr.ht/~erock/pico/shared"
11 "git.sr.ht/~erock/pico/wish/cms/config"
12- "go.uber.org/zap"
13 )
14
15-type SitePageData struct {
16- Domain template.URL
17- HomeURL template.URL
18- Email string
19-}
20-
21-type ConfigSite struct {
22- config.ConfigCms
23- config.ConfigURL
24- SubdomainsEnabled bool
25-}
26-
27-func NewConfigSite() *ConfigSite {
28- domain := GetEnv("PASTES_DOMAIN", "pastes.sh")
29- email := GetEnv("PASTES_EMAIL", "hello@pastes.sh")
30- subdomains := GetEnv("PASTES_SUBDOMAINS", "0")
31- port := GetEnv("PASTES_WEB_PORT", "3000")
32- dbURL := GetEnv("DATABASE_URL", "")
33- protocol := GetEnv("PASTES_PROTOCOL", "https")
34+func NewConfigSite() *shared.ConfigSite {
35+ domain := shared.GetEnv("PASTES_DOMAIN", "pastes.sh")
36+ email := shared.GetEnv("PASTES_EMAIL", "hello@pastes.sh")
37+ subdomains := shared.GetEnv("PASTES_SUBDOMAINS", "0")
38+ port := shared.GetEnv("PASTES_WEB_PORT", "3000")
39+ dbURL := shared.GetEnv("DATABASE_URL", "")
40+ protocol := shared.GetEnv("PASTES_PROTOCOL", "https")
41 subdomainsEnabled := false
42 if subdomains == "1" {
43 subdomainsEnabled = true
44@@ -41,7 +25,7 @@ func NewConfigSite() *ConfigSite {
45 intro += "Finally, send your files to us:\n\n"
46 intro += fmt.Sprintf("scp ~/pastes/* %s:/", domain)
47
48- return &ConfigSite{
49+ return &shared.ConfigSite{
50 SubdomainsEnabled: subdomainsEnabled,
51 ConfigCms: config.ConfigCms{
52 Domain: domain,
53@@ -52,82 +36,7 @@ func NewConfigSite() *ConfigSite {
54 Description: "a pastebin for hackers.",
55 IntroText: intro,
56 Space: "pastes",
57- Logger: CreateLogger(),
58+ Logger: shared.CreateLogger(),
59 },
60 }
61 }
62-
63-func (c *ConfigSite) GetSiteData() *SitePageData {
64- return &SitePageData{
65- Domain: template.URL(c.Domain),
66- HomeURL: template.URL(c.HomeURL()),
67- Email: c.Email,
68- }
69-}
70-
71-func (c *ConfigSite) BlogURL(username string) string {
72- if c.IsSubdomains() {
73- return fmt.Sprintf("%s://%s.%s", c.Protocol, username, c.Domain)
74- }
75-
76- return fmt.Sprintf("/%s", username)
77-}
78-
79-func (c *ConfigSite) PostURL(username, filename string) string {
80- fname := url.PathEscape(filename)
81- if c.IsSubdomains() {
82- return fmt.Sprintf("%s://%s.%s/%s", c.Protocol, username, c.Domain, fname)
83- }
84-
85- return fmt.Sprintf("/%s/%s", username, fname)
86-}
87-
88-func (c *ConfigSite) RawPostURL(username, filename string) string {
89- fname := url.PathEscape(filename)
90- if c.IsSubdomains() {
91- return fmt.Sprintf("%s://%s.%s/raw/%s", c.Protocol, username, c.Domain, fname)
92- }
93-
94- return fmt.Sprintf("/raw/%s/%s", username, fname)
95-}
96-
97-func (c *ConfigSite) IsSubdomains() bool {
98- return c.SubdomainsEnabled
99-}
100-
101-func (c *ConfigSite) RssBlogURL(username string) string {
102- if c.IsSubdomains() {
103- return fmt.Sprintf("%s://%s.%s/rss", c.Protocol, username, c.Domain)
104- }
105-
106- return fmt.Sprintf("/%s/rss", username)
107-}
108-
109-func (c *ConfigSite) HomeURL() string {
110- if c.IsSubdomains() {
111- return fmt.Sprintf("//%s", c.Domain)
112- }
113-
114- return "/"
115-}
116-
117-func (c *ConfigSite) ReadURL() string {
118- if c.IsSubdomains() {
119- return fmt.Sprintf("%s://%s/read", c.Protocol, c.Domain)
120- }
121-
122- return "/read"
123-}
124-
125-func (c *ConfigSite) StaticPath(fname string) string {
126- return path.Join(c.Space, fname)
127-}
128-
129-func CreateLogger() *zap.SugaredLogger {
130- logger, err := zap.NewProduction()
131- if err != nil {
132- log.Fatal(err)
133- }
134-
135- return logger.Sugar()
136-}
+3,
-2
1@@ -3,10 +3,11 @@ package pastes
2 import (
3 "time"
4
5+ "git.sr.ht/~erock/pico/shared"
6 "git.sr.ht/~erock/pico/wish/cms/db"
7 )
8
9-func deleteExpiredPosts(cfg *ConfigSite, dbpool db.DB) error {
10+func deleteExpiredPosts(cfg *shared.ConfigSite, dbpool db.DB) error {
11 cfg.Logger.Infof("checking for expired posts")
12 now := time.Now()
13 // delete posts that are older than three days
14@@ -30,7 +31,7 @@ func deleteExpiredPosts(cfg *ConfigSite, dbpool db.DB) error {
15 return nil
16 }
17
18-func CronDeleteExpiredPosts(cfg *ConfigSite, dbpool db.DB) {
19+func CronDeleteExpiredPosts(cfg *shared.ConfigSite, dbpool db.DB) {
20 for {
21 err := deleteExpiredPosts(cfg, dbpool)
22 if err != nil {
+13,
-2
1@@ -3,14 +3,25 @@ package pastes
2 import (
3 "fmt"
4 "io"
5+ "math"
6 "time"
7
8+ "git.sr.ht/~erock/pico/shared"
9 "git.sr.ht/~erock/pico/wish/cms/db"
10 "git.sr.ht/~erock/pico/wish/cms/util"
11 "git.sr.ht/~erock/pico/wish/send/utils"
12 "github.com/gliderlabs/ssh"
13 )
14
15+// IsTextFile reports whether the file has a known extension indicating
16+// a text file, or if a significant chunk of the specified file looks like
17+// correct UTF-8; that is, if it is likely that the file contains human-
18+// readable text.
19+func IsTextFile(text string, filename string) bool {
20+ num := math.Min(float64(len(text)), 1024)
21+ return shared.IsText(text[0:int(num)])
22+}
23+
24 type Opener struct {
25 entry *utils.FileEntry
26 }
27@@ -22,10 +33,10 @@ func (o *Opener) Open(name string) (io.Reader, error) {
28 type DbHandler struct {
29 User *db.User
30 DBPool db.DB
31- Cfg *ConfigSite
32+ Cfg *shared.ConfigSite
33 }
34
35-func NewDbHandler(dbpool db.DB, cfg *ConfigSite) *DbHandler {
36+func NewDbHandler(dbpool db.DB, cfg *shared.ConfigSite) *DbHandler {
37 return &DbHandler{
38 DBPool: dbpool,
39 Cfg: cfg,
+0,
-97
1@@ -1,97 +0,0 @@
2-package pastes
3-
4-import (
5- "context"
6- "fmt"
7- "net/http"
8- "regexp"
9- "strings"
10-
11- "git.sr.ht/~erock/pico/wish/cms/db"
12- "go.uber.org/zap"
13-)
14-
15-type Route struct {
16- method string
17- regex *regexp.Regexp
18- handler http.HandlerFunc
19-}
20-
21-func NewRoute(method, pattern string, handler http.HandlerFunc) Route {
22- return Route{
23- method,
24- regexp.MustCompile("^" + pattern + "$"),
25- handler,
26- }
27-}
28-
29-type ServeFn func(http.ResponseWriter, *http.Request)
30-
31-func CreateServe(routes []Route, subdomainRoutes []Route, cfg *ConfigSite, dbpool db.DB, logger *zap.SugaredLogger) ServeFn {
32- return func(w http.ResponseWriter, r *http.Request) {
33- var allow []string
34- curRoutes := routes
35-
36- hostDomain := strings.ToLower(strings.Split(r.Host, ":")[0])
37- appDomain := strings.ToLower(strings.Split(cfg.ConfigCms.Domain, ":")[0])
38-
39- subdomain := ""
40- if hostDomain != appDomain && strings.Contains(hostDomain, appDomain) {
41- subdomain = strings.TrimSuffix(hostDomain, fmt.Sprintf(".%s", appDomain))
42- }
43-
44- if cfg.IsSubdomains() && subdomain != "" {
45- curRoutes = subdomainRoutes
46- }
47-
48- for _, route := range curRoutes {
49- matches := route.regex.FindStringSubmatch(r.URL.Path)
50- if len(matches) > 0 {
51- if r.Method != route.method {
52- allow = append(allow, route.method)
53- continue
54- }
55- loggerCtx := context.WithValue(r.Context(), ctxLoggerKey{}, logger)
56- subdomainCtx := context.WithValue(loggerCtx, ctxSubdomainKey{}, subdomain)
57- dbCtx := context.WithValue(subdomainCtx, ctxDBKey{}, dbpool)
58- cfgCtx := context.WithValue(dbCtx, ctxCfg{}, cfg)
59- ctx := context.WithValue(cfgCtx, ctxKey{}, matches[1:])
60- route.handler(w, r.WithContext(ctx))
61- return
62- }
63- }
64- if len(allow) > 0 {
65- w.Header().Set("Allow", strings.Join(allow, ", "))
66- http.Error(w, "405 method not allowed", http.StatusMethodNotAllowed)
67- return
68- }
69- http.NotFound(w, r)
70- }
71-}
72-
73-type ctxDBKey struct{}
74-type ctxKey struct{}
75-type ctxLoggerKey struct{}
76-type ctxSubdomainKey struct{}
77-type ctxCfg struct{}
78-
79-func GetCfg(r *http.Request) *ConfigSite {
80- return r.Context().Value(ctxCfg{}).(*ConfigSite)
81-}
82-
83-func GetLogger(r *http.Request) *zap.SugaredLogger {
84- return r.Context().Value(ctxLoggerKey{}).(*zap.SugaredLogger)
85-}
86-
87-func GetDB(r *http.Request) db.DB {
88- return r.Context().Value(ctxDBKey{}).(db.DB)
89-}
90-
91-func GetField(r *http.Request, index int) string {
92- fields := r.Context().Value(ctxKey{}).([]string)
93- return fields[index]
94-}
95-
96-func GetSubdomain(r *http.Request) string {
97- return r.Context().Value(ctxSubdomainKey{}).(string)
98-}
+0,
-107
1@@ -1,107 +0,0 @@
2-package pastes
3-
4-import (
5- "encoding/base64"
6- "fmt"
7- "math"
8- "os"
9- "path/filepath"
10- "regexp"
11- "strings"
12- "time"
13- "unicode"
14- "unicode/utf8"
15-
16- "github.com/gliderlabs/ssh"
17-)
18-
19-var fnameRe = regexp.MustCompile(`[-_]+`)
20-
21-func FilenameToTitle(filename string, title string) string {
22- if filename != title {
23- return title
24- }
25-
26- pre := fnameRe.ReplaceAllString(title, " ")
27- r := []rune(pre)
28- r[0] = unicode.ToUpper(r[0])
29- return string(r)
30-}
31-
32-func SanitizeFileExt(fname string) string {
33- return strings.TrimSuffix(fname, filepath.Ext(fname))
34-}
35-
36-func KeyText(s ssh.Session) (string, error) {
37- if s.PublicKey() == nil {
38- return "", fmt.Errorf("Session doesn't have public key")
39- }
40- kb := base64.StdEncoding.EncodeToString(s.PublicKey().Marshal())
41- return fmt.Sprintf("%s %s", s.PublicKey().Type(), kb), nil
42-}
43-
44-func GetEnv(key string, defaultVal string) string {
45- if value, exists := os.LookupEnv(key); exists {
46- return value
47- }
48-
49- return defaultVal
50-}
51-
52-// IsText reports whether a significant prefix of s looks like correct UTF-8;
53-// that is, if it is likely that s is human-readable text.
54-func IsText(s string) bool {
55- const max = 1024 // at least utf8.UTFMax
56- if len(s) > max {
57- s = s[0:max]
58- }
59- for i, c := range s {
60- if i+utf8.UTFMax > len(s) {
61- // last char may be incomplete - ignore
62- break
63- }
64- if c == 0xFFFD || c < ' ' && c != '\n' && c != '\t' && c != '\f' && c != '\r' {
65- // decoding error or control character - not a text file
66- return false
67- }
68- }
69- return true
70-}
71-
72-// IsTextFile reports whether the file has a known extension indicating
73-// a text file, or if a significant chunk of the specified file looks like
74-// correct UTF-8; that is, if it is likely that the file contains human-
75-// readable text.
76-func IsTextFile(text string, filename string) bool {
77- num := math.Min(float64(len(text)), 1024)
78- return IsText(text[0:int(num)])
79-}
80-
81-const solarYearSecs = 31556926
82-
83-func TimeAgo(t *time.Time) string {
84- d := time.Since(*t)
85- var metric string
86- var amount int
87- if d.Seconds() < 60 {
88- amount = int(d.Seconds())
89- metric = "second"
90- } else if d.Minutes() < 60 {
91- amount = int(d.Minutes())
92- metric = "minute"
93- } else if d.Hours() < 24 {
94- amount = int(d.Hours())
95- metric = "hour"
96- } else if d.Seconds() < solarYearSecs {
97- amount = int(d.Hours()) / 24
98- metric = "day"
99- } else {
100- amount = int(d.Seconds()) / solarYearSecs
101- metric = "year"
102- }
103- if amount == 1 {
104- return fmt.Sprintf("%d %s ago", amount, metric)
105- } else {
106- return fmt.Sprintf("%d %ss ago", amount, metric)
107- }
108-}
+91,
-90
1@@ -11,6 +11,7 @@ import (
2 "strings"
3 "time"
4
5+ "git.sr.ht/~erock/pico/shared"
6 "git.sr.ht/~erock/pico/wish/cms/db"
7 "git.sr.ht/~erock/pico/wish/cms/db/postgres"
8 "github.com/gorilla/feeds"
9@@ -18,7 +19,7 @@ import (
10 )
11
12 type PageData struct {
13- Site SitePageData
14+ Site shared.SitePageData
15 }
16
17 type PostItemData struct {
18@@ -36,7 +37,7 @@ type PostItemData struct {
19 }
20
21 type BlogPageData struct {
22- Site SitePageData
23+ Site shared.SitePageData
24 PageTitle string
25 URL template.URL
26 RSSURL template.URL
27@@ -49,14 +50,14 @@ type BlogPageData struct {
28 }
29
30 type ReadPageData struct {
31- Site SitePageData
32+ Site shared.SitePageData
33 NextPage string
34 PrevPage string
35 Posts []PostItemData
36 }
37
38 type PostPageData struct {
39- Site SitePageData
40+ Site shared.SitePageData
41 PageTitle string
42 URL template.URL
43 BlogURL template.URL
44@@ -72,7 +73,7 @@ type PostPageData struct {
45 }
46
47 type TransparencyPageData struct {
48- Site SitePageData
49+ Site shared.SitePageData
50 Analytics *db.Analytics
51 }
52
53@@ -80,7 +81,7 @@ func isRequestTrackable(r *http.Request) bool {
54 return true
55 }
56
57-func renderTemplate(cfg *ConfigSite, templates []string) (*template.Template, error) {
58+func renderTemplate(cfg *shared.ConfigSite, templates []string) (*template.Template, error) {
59 files := make([]string, len(templates))
60 copy(files, templates)
61 files = append(
62@@ -99,8 +100,8 @@ func renderTemplate(cfg *ConfigSite, templates []string) (*template.Template, er
63
64 func createPageHandler(fname string) http.HandlerFunc {
65 return func(w http.ResponseWriter, r *http.Request) {
66- logger := GetLogger(r)
67- cfg := GetCfg(r)
68+ logger := shared.GetLogger(r)
69+ cfg := shared.GetCfg(r)
70 ts, err := renderTemplate(cfg, []string{cfg.StaticPath(fname)})
71
72 if err != nil {
73@@ -138,20 +139,20 @@ type ReadmeTxt struct {
74 }
75
76 func GetUsernameFromRequest(r *http.Request) string {
77- subdomain := GetSubdomain(r)
78- cfg := GetCfg(r)
79+ subdomain := shared.GetSubdomain(r)
80+ cfg := shared.GetCfg(r)
81
82 if !cfg.IsSubdomains() || subdomain == "" {
83- return GetField(r, 0)
84+ return shared.GetField(r, 0)
85 }
86 return subdomain
87 }
88
89 func blogStyleHandler(w http.ResponseWriter, r *http.Request) {
90 username := GetUsernameFromRequest(r)
91- dbpool := GetDB(r)
92- logger := GetLogger(r)
93- cfg := GetCfg(r)
94+ dbpool := shared.GetDB(r)
95+ logger := shared.GetLogger(r)
96+ cfg := shared.GetCfg(r)
97
98 user, err := dbpool.FindUserForName(username)
99 if err != nil {
100@@ -177,9 +178,9 @@ func blogStyleHandler(w http.ResponseWriter, r *http.Request) {
101
102 func blogHandler(w http.ResponseWriter, r *http.Request) {
103 username := GetUsernameFromRequest(r)
104- dbpool := GetDB(r)
105- logger := GetLogger(r)
106- cfg := GetCfg(r)
107+ dbpool := shared.GetDB(r)
108+ logger := shared.GetLogger(r)
109+ cfg := shared.GetCfg(r)
110
111 user, err := dbpool.FindUserForName(username)
112 if err != nil {
113@@ -239,10 +240,10 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
114 p := PostItemData{
115 URL: template.URL(cfg.FullPostURL(post.Username, post.Filename, onSubdomain, withUserName)),
116 BlogURL: template.URL(cfg.FullBlogURL(post.Username, onSubdomain, withUserName)),
117- Title: FilenameToTitle(post.Filename, post.Title),
118+ Title: shared.FilenameToTitle(post.Filename, post.Title),
119 PublishAt: post.PublishAt.Format("02 Jan, 2006"),
120 PublishAtISO: post.PublishAt.Format(time.RFC3339),
121- UpdatedTimeAgo: TimeAgo(post.UpdatedAt),
122+ UpdatedTimeAgo: shared.TimeAgo(post.UpdatedAt),
123 UpdatedAtISO: post.UpdatedAt.Format(time.RFC3339),
124 }
125 postCollection = append(postCollection, p)
126@@ -283,18 +284,18 @@ func GetBlogName(username string) string {
127
128 func postRawHandler(w http.ResponseWriter, r *http.Request) {
129 username := GetUsernameFromRequest(r)
130- subdomain := GetSubdomain(r)
131- cfg := GetCfg(r)
132+ subdomain := shared.GetSubdomain(r)
133+ cfg := shared.GetCfg(r)
134
135 var filename string
136 if !cfg.IsSubdomains() || subdomain == "" {
137- filename, _ = url.PathUnescape(GetField(r, 1))
138+ filename, _ = url.PathUnescape(shared.GetField(r, 1))
139 } else {
140- filename, _ = url.PathUnescape(GetField(r, 0))
141+ filename, _ = url.PathUnescape(shared.GetField(r, 0))
142 }
143
144- dbpool := GetDB(r)
145- logger := GetLogger(r)
146+ dbpool := shared.GetDB(r)
147+ logger := shared.GetLogger(r)
148
149 user, err := dbpool.FindUserForName(username)
150 if err != nil {
151@@ -321,18 +322,18 @@ func postRawHandler(w http.ResponseWriter, r *http.Request) {
152
153 func postHandler(w http.ResponseWriter, r *http.Request) {
154 username := GetUsernameFromRequest(r)
155- subdomain := GetSubdomain(r)
156- cfg := GetCfg(r)
157+ subdomain := shared.GetSubdomain(r)
158+ cfg := shared.GetCfg(r)
159
160 var filename string
161 if !cfg.IsSubdomains() || subdomain == "" {
162- filename, _ = url.PathUnescape(GetField(r, 1))
163+ filename, _ = url.PathUnescape(shared.GetField(r, 1))
164 } else {
165- filename, _ = url.PathUnescape(GetField(r, 0))
166+ filename, _ = url.PathUnescape(shared.GetField(r, 0))
167 }
168
169- dbpool := GetDB(r)
170- logger := GetLogger(r)
171+ dbpool := shared.GetDB(r)
172+ logger := shared.GetLogger(r)
173
174 user, err := dbpool.FindUserForName(username)
175 if err != nil {
176@@ -391,7 +392,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
177 URL: template.URL(cfg.FullPostURL(post.Username, post.Filename, onSubdomain, withUserName)),
178 BlogURL: template.URL(cfg.FullBlogURL(username, onSubdomain, withUserName)),
179 Description: post.Description,
180- Title: FilenameToTitle(post.Filename, post.Title),
181+ Title: shared.FilenameToTitle(post.Filename, post.Title),
182 PublishAt: post.PublishAt.Format("02 Jan, 2006"),
183 PublishAtISO: post.PublishAt.Format(time.RFC3339),
184 Username: username,
185@@ -432,9 +433,9 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
186 }
187
188 func transparencyHandler(w http.ResponseWriter, r *http.Request) {
189- dbpool := GetDB(r)
190- logger := GetLogger(r)
191- cfg := GetCfg(r)
192+ dbpool := shared.GetDB(r)
193+ logger := shared.GetLogger(r)
194+ cfg := shared.GetCfg(r)
195
196 analytics, err := dbpool.FindSiteAnalytics(cfg.Space)
197 if err != nil {
198@@ -466,15 +467,15 @@ func transparencyHandler(w http.ResponseWriter, r *http.Request) {
199 }
200
201 func checkHandler(w http.ResponseWriter, r *http.Request) {
202- dbpool := GetDB(r)
203- cfg := GetCfg(r)
204+ dbpool := shared.GetDB(r)
205+ cfg := shared.GetCfg(r)
206
207 if cfg.IsCustomdomains() {
208 hostDomain := r.URL.Query().Get("domain")
209 appDomain := strings.Split(cfg.ConfigCms.Domain, ":")[0]
210
211 if !strings.Contains(hostDomain, appDomain) {
212- subdomain := GetCustomDomain(hostDomain)
213+ subdomain := shared.GetCustomDomain(hostDomain)
214 if subdomain != "" {
215 u, err := dbpool.FindUserForName(subdomain)
216 if u != nil && err == nil {
217@@ -489,9 +490,9 @@ func checkHandler(w http.ResponseWriter, r *http.Request) {
218 }
219
220 func readHandler(w http.ResponseWriter, r *http.Request) {
221- dbpool := GetDB(r)
222- logger := GetLogger(r)
223- cfg := GetCfg(r)
224+ dbpool := shared.GetDB(r)
225+ logger := shared.GetLogger(r)
226+ cfg := shared.GetCfg(r)
227
228 page, _ := strconv.Atoi(r.URL.Query().Get("page"))
229 pager, err := dbpool.FindAllPosts(&db.Pager{Num: 30, Page: page}, cfg.Space)
230@@ -528,12 +529,12 @@ func readHandler(w http.ResponseWriter, r *http.Request) {
231 item := PostItemData{
232 URL: template.URL(cfg.FullPostURL(post.Username, post.Filename, true, true)),
233 BlogURL: template.URL(cfg.FullBlogURL(post.Username, true, true)),
234- Title: FilenameToTitle(post.Filename, post.Title),
235+ Title: shared.FilenameToTitle(post.Filename, post.Title),
236 Description: post.Description,
237 Username: post.Username,
238 PublishAt: post.PublishAt.Format("02 Jan, 2006"),
239 PublishAtISO: post.PublishAt.Format(time.RFC3339),
240- UpdatedTimeAgo: TimeAgo(post.UpdatedAt),
241+ UpdatedTimeAgo: shared.TimeAgo(post.UpdatedAt),
242 UpdatedAtISO: post.UpdatedAt.Format(time.RFC3339),
243 Score: post.Score,
244 }
245@@ -549,9 +550,9 @@ func readHandler(w http.ResponseWriter, r *http.Request) {
246
247 func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
248 username := GetUsernameFromRequest(r)
249- dbpool := GetDB(r)
250- logger := GetLogger(r)
251- cfg := GetCfg(r)
252+ dbpool := shared.GetDB(r)
253+ logger := shared.GetLogger(r)
254+ cfg := shared.GetCfg(r)
255
256 user, err := dbpool.FindUserForName(username)
257 if err != nil {
258@@ -633,7 +634,7 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
259
260 item := &feeds.Item{
261 Id: realUrl,
262- Title: FilenameToTitle(post.Filename, post.Title),
263+ Title: shared.FilenameToTitle(post.Filename, post.Title),
264 Link: &feeds.Link{Href: realUrl},
265 Content: tpl.String(),
266 Created: *post.PublishAt,
267@@ -661,9 +662,9 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
268 }
269
270 func rssHandler(w http.ResponseWriter, r *http.Request) {
271- dbpool := GetDB(r)
272- logger := GetLogger(r)
273- cfg := GetCfg(r)
274+ dbpool := shared.GetDB(r)
275+ logger := shared.GetLogger(r)
276+ cfg := shared.GetCfg(r)
277
278 pager, err := dbpool.FindAllPosts(&db.Pager{Num: 25, Page: 0}, cfg.Space)
279 if err != nil {
280@@ -744,8 +745,8 @@ func rssHandler(w http.ResponseWriter, r *http.Request) {
281
282 func serveFile(file string, contentType string) http.HandlerFunc {
283 return func(w http.ResponseWriter, r *http.Request) {
284- logger := GetLogger(r)
285- cfg := GetCfg(r)
286+ logger := shared.GetLogger(r)
287+ cfg := shared.GetCfg(r)
288
289 contents, err := ioutil.ReadFile(cfg.StaticPath(fmt.Sprintf("public/%s", file)))
290 if err != nil {
291@@ -762,29 +763,29 @@ func serveFile(file string, contentType string) http.HandlerFunc {
292 }
293 }
294
295-func createStaticRoutes() []Route {
296- return []Route{
297- NewRoute("GET", "/main.css", serveFile("main.css", "text/css")),
298- NewRoute("GET", "/syntax.css", serveFile("syntax.css", "text/css")),
299- NewRoute("GET", "/card.png", serveFile("card.png", "image/png")),
300- NewRoute("GET", "/favicon-16x16.png", serveFile("favicon-16x16.png", "image/png")),
301- NewRoute("GET", "/favicon-32x32.png", serveFile("favicon-32x32.png", "image/png")),
302- NewRoute("GET", "/apple-touch-icon.png", serveFile("apple-touch-icon.png", "image/png")),
303- NewRoute("GET", "/favicon.ico", serveFile("favicon.ico", "image/x-icon")),
304- NewRoute("GET", "/robots.txt", serveFile("robots.txt", "text/plain")),
305+func createStaticRoutes() []shared.Route {
306+ return []shared.Route{
307+ shared.NewRoute("GET", "/main.css", serveFile("main.css", "text/css")),
308+ shared.NewRoute("GET", "/syntax.css", serveFile("syntax.css", "text/css")),
309+ shared.NewRoute("GET", "/card.png", serveFile("card.png", "image/png")),
310+ shared.NewRoute("GET", "/favicon-16x16.png", serveFile("favicon-16x16.png", "image/png")),
311+ shared.NewRoute("GET", "/favicon-32x32.png", serveFile("favicon-32x32.png", "image/png")),
312+ shared.NewRoute("GET", "/apple-touch-icon.png", serveFile("apple-touch-icon.png", "image/png")),
313+ shared.NewRoute("GET", "/favicon.ico", serveFile("favicon.ico", "image/x-icon")),
314+ shared.NewRoute("GET", "/robots.txt", serveFile("robots.txt", "text/plain")),
315 }
316 }
317
318-func createMainRoutes(staticRoutes []Route) []Route {
319- routes := []Route{
320- NewRoute("GET", "/", createPageHandler("html/marketing.page.tmpl")),
321- NewRoute("GET", "/spec", createPageHandler("html/spec.page.tmpl")),
322- NewRoute("GET", "/ops", createPageHandler("html/ops.page.tmpl")),
323- NewRoute("GET", "/privacy", createPageHandler("html/privacy.page.tmpl")),
324- NewRoute("GET", "/help", createPageHandler("html/help.page.tmpl")),
325- NewRoute("GET", "/transparency", transparencyHandler),
326- NewRoute("GET", "/read", readHandler),
327- NewRoute("GET", "/check", checkHandler),
328+func createMainRoutes(staticRoutes []shared.Route) []shared.Route {
329+ routes := []shared.Route{
330+ shared.NewRoute("GET", "/", createPageHandler("html/marketing.page.tmpl")),
331+ shared.NewRoute("GET", "/spec", createPageHandler("html/spec.page.tmpl")),
332+ shared.NewRoute("GET", "/ops", createPageHandler("html/ops.page.tmpl")),
333+ shared.NewRoute("GET", "/privacy", createPageHandler("html/privacy.page.tmpl")),
334+ shared.NewRoute("GET", "/help", createPageHandler("html/help.page.tmpl")),
335+ shared.NewRoute("GET", "/transparency", transparencyHandler),
336+ shared.NewRoute("GET", "/read", readHandler),
337+ shared.NewRoute("GET", "/check", checkHandler),
338 }
339
340 routes = append(
341@@ -794,26 +795,26 @@ func createMainRoutes(staticRoutes []Route) []Route {
342
343 routes = append(
344 routes,
345- NewRoute("GET", "/rss", rssHandler),
346- NewRoute("GET", "/rss.xml", rssHandler),
347- NewRoute("GET", "/atom.xml", rssHandler),
348- NewRoute("GET", "/feed.xml", rssHandler),
349-
350- NewRoute("GET", "/([^/]+)", blogHandler),
351- NewRoute("GET", "/([^/]+)/rss", rssBlogHandler),
352- NewRoute("GET", "/([^/]+)/styles.css", blogStyleHandler),
353- NewRoute("GET", "/([^/]+)/([^/]+)", postHandler),
354- NewRoute("GET", "/raw/([^/]+)/([^/]+)", postRawHandler),
355+ shared.NewRoute("GET", "/rss", rssHandler),
356+ shared.NewRoute("GET", "/rss.xml", rssHandler),
357+ shared.NewRoute("GET", "/atom.xml", rssHandler),
358+ shared.NewRoute("GET", "/feed.xml", rssHandler),
359+
360+ shared.NewRoute("GET", "/([^/]+)", blogHandler),
361+ shared.NewRoute("GET", "/([^/]+)/rss", rssBlogHandler),
362+ shared.NewRoute("GET", "/([^/]+)/styles.css", blogStyleHandler),
363+ shared.NewRoute("GET", "/([^/]+)/([^/]+)", postHandler),
364+ shared.NewRoute("GET", "/raw/([^/]+)/([^/]+)", postRawHandler),
365 )
366
367 return routes
368 }
369
370-func createSubdomainRoutes(staticRoutes []Route) []Route {
371- routes := []Route{
372- NewRoute("GET", "/", blogHandler),
373- NewRoute("GET", "/_styles.css", blogStyleHandler),
374- NewRoute("GET", "/rss", rssBlogHandler),
375+func createSubdomainRoutes(staticRoutes []shared.Route) []shared.Route {
376+ routes := []shared.Route{
377+ shared.NewRoute("GET", "/", blogHandler),
378+ shared.NewRoute("GET", "/_styles.css", blogStyleHandler),
379+ shared.NewRoute("GET", "/rss", rssBlogHandler),
380 }
381
382 routes = append(
383@@ -823,8 +824,8 @@ func createSubdomainRoutes(staticRoutes []Route) []Route {
384
385 routes = append(
386 routes,
387- NewRoute("GET", "/([^/]+)", postHandler),
388- NewRoute("GET", "/raw/([^/]+)", postRawHandler),
389+ shared.NewRoute("GET", "/([^/]+)", postHandler),
390+ shared.NewRoute("GET", "/raw/([^/]+)", postRawHandler),
391 )
392
393 return routes
394@@ -840,7 +841,7 @@ func StartApiServer() {
395 mainRoutes := createMainRoutes(staticRoutes)
396 subdomainRoutes := createSubdomainRoutes(staticRoutes)
397
398- handler := CreateServe(mainRoutes, subdomainRoutes, cfg, db, logger)
399+ handler := shared.CreateServe(mainRoutes, subdomainRoutes, cfg, db, logger)
400 router := http.HandlerFunc(handler)
401
402 portStr := fmt.Sprintf(":%s", cfg.Port)
+11,
-136
1@@ -2,36 +2,19 @@ package prose
2
3 import (
4 "fmt"
5- "html/template"
6- "log"
7- "net/url"
8- "path"
9
10+ "git.sr.ht/~erock/pico/shared"
11 "git.sr.ht/~erock/pico/wish/cms/config"
12- "go.uber.org/zap"
13 )
14
15-type SitePageData struct {
16- Domain template.URL
17- HomeURL template.URL
18- Email string
19-}
20-
21-type ConfigSite struct {
22- config.ConfigCms
23- config.ConfigURL
24- SubdomainsEnabled bool
25- CustomdomainsEnabled bool
26-}
27-
28-func NewConfigSite() *ConfigSite {
29- domain := GetEnv("PROSE_DOMAIN", "prose.sh")
30- email := GetEnv("PROSE_EMAIL", "hello@prose.sh")
31- subdomains := GetEnv("PROSE_SUBDOMAINS", "0")
32- customdomains := GetEnv("PROSE_CUSTOMDOMAINS", "0")
33- port := GetEnv("PROSE_WEB_PORT", "3000")
34- protocol := GetEnv("PROSE_PROTOCOL", "https")
35- dbURL := GetEnv("DATABASE_URL", "")
36+func NewConfigSite() *shared.ConfigSite {
37+ domain := shared.GetEnv("PROSE_DOMAIN", "prose.sh")
38+ email := shared.GetEnv("PROSE_EMAIL", "hello@prose.sh")
39+ subdomains := shared.GetEnv("PROSE_SUBDOMAINS", "0")
40+ customdomains := shared.GetEnv("PROSE_CUSTOMDOMAINS", "0")
41+ port := shared.GetEnv("PROSE_WEB_PORT", "3000")
42+ protocol := shared.GetEnv("PROSE_PROTOCOL", "https")
43+ dbURL := shared.GetEnv("DATABASE_URL", "")
44 subdomainsEnabled := false
45 if subdomains == "1" {
46 subdomainsEnabled = true
47@@ -48,7 +31,7 @@ func NewConfigSite() *ConfigSite {
48 intro += "Finally, send your files to us:\n\n"
49 intro += fmt.Sprintf("scp ~/blog/*.md %s:/", domain)
50
51- return &ConfigSite{
52+ return &shared.ConfigSite{
53 SubdomainsEnabled: subdomainsEnabled,
54 CustomdomainsEnabled: customdomainsEnabled,
55 ConfigCms: config.ConfigCms{
56@@ -60,115 +43,7 @@ func NewConfigSite() *ConfigSite {
57 Description: "a blog platform for hackers.",
58 IntroText: intro,
59 Space: "prose",
60- Logger: CreateLogger(),
61+ Logger: shared.CreateLogger(),
62 },
63 }
64 }
65-
66-func (c *ConfigSite) GetSiteData() *SitePageData {
67- return &SitePageData{
68- Domain: template.URL(c.Domain),
69- HomeURL: template.URL(c.HomeURL()),
70- Email: c.Email,
71- }
72-}
73-
74-func (c *ConfigSite) BlogURL(username string) string {
75- if c.IsSubdomains() {
76- return fmt.Sprintf("%s://%s.%s", c.Protocol, username, c.Domain)
77- }
78-
79- return fmt.Sprintf("/%s", username)
80-}
81-
82-func (c *ConfigSite) FullBlogURL(username string, onSubdomain bool, withUserName bool) string {
83- if c.IsSubdomains() && onSubdomain {
84- return fmt.Sprintf("%s://%s.%s", c.Protocol, username, c.Domain)
85- }
86-
87- if withUserName {
88- return fmt.Sprintf("/%s", username)
89- }
90-
91- return "/"
92-}
93-
94-func (c *ConfigSite) PostURL(username, filename string) string {
95- fname := url.PathEscape(filename)
96- if c.IsSubdomains() {
97- return fmt.Sprintf("%s://%s.%s/%s", c.Protocol, username, c.Domain, fname)
98- }
99-
100- return fmt.Sprintf("/%s/%s", username, fname)
101-
102-}
103-
104-func (c *ConfigSite) FullPostURL(username, filename string, onSubdomain bool, withUserName bool) string {
105- fname := url.PathEscape(filename)
106- if c.IsSubdomains() && onSubdomain {
107- return fmt.Sprintf("%s://%s.%s/%s", c.Protocol, username, c.Domain, fname)
108- }
109-
110- if withUserName {
111- return fmt.Sprintf("/%s/%s", username, fname)
112- }
113-
114- return fmt.Sprintf("/%s", fname)
115-}
116-
117-func (c *ConfigSite) IsSubdomains() bool {
118- return c.SubdomainsEnabled
119-}
120-
121-func (c *ConfigSite) IsCustomdomains() bool {
122- return c.CustomdomainsEnabled
123-}
124-
125-func (c *ConfigSite) RssBlogURL(username string, onSubdomain bool, withUserName bool) string {
126- if c.IsSubdomains() && onSubdomain {
127- return fmt.Sprintf("%s://%s.%s/rss", c.Protocol, username, c.Domain)
128- }
129-
130- if withUserName {
131- return fmt.Sprintf("/%s/rss", username)
132- }
133-
134- return "/rss"
135-}
136-
137-func (c *ConfigSite) HomeURL() string {
138- if c.IsSubdomains() || c.IsCustomdomains() {
139- return fmt.Sprintf("//%s", c.Domain)
140- }
141-
142- return "/"
143-}
144-
145-func (c *ConfigSite) ReadURL() string {
146- if c.IsSubdomains() || c.IsCustomdomains() {
147- return fmt.Sprintf("%s://%s/read", c.Protocol, c.Domain)
148- }
149-
150- return "/read"
151-}
152-
153-func (c *ConfigSite) CssURL(username string) string {
154- if c.IsSubdomains() || c.IsCustomdomains() {
155- return fmt.Sprintf("%s://%s.%s/_styles.css", c.Protocol, username, c.Domain)
156- }
157-
158- return fmt.Sprintf("/%s/styles.css", username)
159-}
160-
161-func (c *ConfigSite) StaticPath(fname string) string {
162- return path.Join(c.Space, fname)
163-}
164-
165-func CreateLogger() *zap.SugaredLogger {
166- logger, err := zap.NewProduction()
167- if err != nil {
168- log.Fatal(err)
169- }
170-
171- return logger.Sugar()
172-}
+6,
-4
1@@ -6,6 +6,7 @@ import (
2 "strings"
3 "time"
4
5+ "git.sr.ht/~erock/pico/shared"
6 "git.sr.ht/~erock/pico/wish/cms/db"
7 "git.sr.ht/~erock/pico/wish/cms/util"
8 "git.sr.ht/~erock/pico/wish/send/utils"
9@@ -14,6 +15,7 @@ import (
10 )
11
12 var hiddenPosts = []string{"_readme.md", "_styles.css"}
13+var allowedExtensions = []string{".md", ".css"}
14
15 type Opener struct {
16 entry *utils.FileEntry
17@@ -26,10 +28,10 @@ func (o *Opener) Open(name string) (io.Reader, error) {
18 type DbHandler struct {
19 User *db.User
20 DBPool db.DB
21- Cfg *ConfigSite
22+ Cfg *shared.ConfigSite
23 }
24
25-func NewDbHandler(dbpool db.DB, cfg *ConfigSite) *DbHandler {
26+func NewDbHandler(dbpool db.DB, cfg *shared.ConfigSite) *DbHandler {
27 return &DbHandler{
28 DBPool: dbpool,
29 Cfg: cfg,
30@@ -59,7 +61,7 @@ func (h *DbHandler) Validate(s ssh.Session) error {
31 func (h *DbHandler) Write(s ssh.Session, entry *utils.FileEntry) (string, error) {
32 logger := h.Cfg.Logger
33 userID := h.User.ID
34- filename := SanitizeFileExt(entry.Name)
35+ filename := shared.SanitizeFileExt(entry.Name)
36 title := filename
37 var err error
38 post, err := h.DBPool.FindPostWithFilename(filename, userID, h.Cfg.Space)
39@@ -77,7 +79,7 @@ func (h *DbHandler) Write(s ssh.Session, entry *utils.FileEntry) (string, error)
40 text = string(b)
41 }
42
43- if !IsTextFile(text, entry.Filepath) {
44+ if !shared.IsTextFile(text, entry.Filepath, allowedExtensions) {
45 extStr := strings.Join(allowedExtensions, ",")
46 logger.Errorf("WARNING: (%s) invalid file, format must be (%s) and the contents must be plain text, skipping", entry.Name, extStr)
47 return "", fmt.Errorf("WARNING: (%s) invalid file, format must be (%s) and the contents must be plain text, skipping", entry.Name, extStr)
+0,
-116
1@@ -1,116 +0,0 @@
2-package prose
3-
4-import (
5- "encoding/base64"
6- "fmt"
7- "math"
8- "os"
9- pathpkg "path"
10- "path/filepath"
11- "regexp"
12- "strings"
13- "time"
14- "unicode"
15- "unicode/utf8"
16-
17- "github.com/gliderlabs/ssh"
18- "golang.org/x/exp/slices"
19-)
20-
21-var fnameRe = regexp.MustCompile(`[-_]+`)
22-
23-func FilenameToTitle(filename string, title string) string {
24- if filename != title {
25- return title
26- }
27-
28- pre := fnameRe.ReplaceAllString(title, " ")
29- r := []rune(pre)
30- r[0] = unicode.ToUpper(r[0])
31- return string(r)
32-}
33-
34-func SanitizeFileExt(fname string) string {
35- return strings.TrimSuffix(fname, filepath.Ext(fname))
36-}
37-
38-func KeyText(s ssh.Session) (string, error) {
39- if s.PublicKey() == nil {
40- return "", fmt.Errorf("Session doesn't have public key")
41- }
42- kb := base64.StdEncoding.EncodeToString(s.PublicKey().Marshal())
43- return fmt.Sprintf("%s %s", s.PublicKey().Type(), kb), nil
44-}
45-
46-func GetEnv(key string, defaultVal string) string {
47- if value, exists := os.LookupEnv(key); exists {
48- return value
49- }
50-
51- return defaultVal
52-}
53-
54-// IsText reports whether a significant prefix of s looks like correct UTF-8;
55-// that is, if it is likely that s is human-readable text.
56-func IsText(s string) bool {
57- const max = 1024 // at least utf8.UTFMax
58- if len(s) > max {
59- s = s[0:max]
60- }
61- for i, c := range s {
62- if i+utf8.UTFMax > len(s) {
63- // last char may be incomplete - ignore
64- break
65- }
66- if c == 0xFFFD || c < ' ' && c != '\n' && c != '\t' && c != '\f' && c != '\r' {
67- // decoding error or control character - not a text file
68- return false
69- }
70- }
71- return true
72-}
73-
74-var allowedExtensions = []string{".md", ".css"}
75-
76-// IsTextFile reports whether the file has a known extension indicating
77-// a text file, or if a significant chunk of the specified file looks like
78-// correct UTF-8; that is, if it is likely that the file contains human-
79-// readable text.
80-func IsTextFile(text string, filename string) bool {
81- ext := pathpkg.Ext(filename)
82- if !slices.Contains(allowedExtensions, ext) {
83- return false
84- }
85-
86- num := math.Min(float64(len(text)), 1024)
87- return IsText(text[0:int(num)])
88-}
89-
90-const solarYearSecs = 31556926
91-
92-func TimeAgo(t *time.Time) string {
93- d := time.Since(*t)
94- var metric string
95- var amount int
96- if d.Seconds() < 60 {
97- amount = int(d.Seconds())
98- metric = "second"
99- } else if d.Minutes() < 60 {
100- amount = int(d.Minutes())
101- metric = "minute"
102- } else if d.Hours() < 24 {
103- amount = int(d.Hours())
104- metric = "hour"
105- } else if d.Seconds() < solarYearSecs {
106- amount = int(d.Hours()) / 24
107- metric = "day"
108- } else {
109- amount = int(d.Seconds()) / solarYearSecs
110- metric = "year"
111- }
112- if amount == 1 {
113- return fmt.Sprintf("%d %s ago", amount, metric)
114- } else {
115- return fmt.Sprintf("%d %ss ago", amount, metric)
116- }
117-}
1@@ -0,0 +1,142 @@
2+package shared
3+
4+import (
5+ "fmt"
6+ "html/template"
7+ "log"
8+ "net/url"
9+ "path"
10+
11+ "git.sr.ht/~erock/pico/wish/cms/config"
12+ "go.uber.org/zap"
13+)
14+
15+type SitePageData struct {
16+ Domain template.URL
17+ HomeURL template.URL
18+ Email string
19+}
20+
21+type ConfigSite struct {
22+ config.ConfigCms
23+ config.ConfigURL
24+ SubdomainsEnabled bool
25+ CustomdomainsEnabled bool
26+}
27+
28+func (c *ConfigSite) GetSiteData() *SitePageData {
29+ return &SitePageData{
30+ Domain: template.URL(c.Domain),
31+ HomeURL: template.URL(c.HomeURL()),
32+ Email: c.Email,
33+ }
34+}
35+
36+func (c *ConfigSite) BlogURL(username string) string {
37+ if c.IsSubdomains() {
38+ return fmt.Sprintf("%s://%s.%s", c.Protocol, username, c.Domain)
39+ }
40+
41+ return fmt.Sprintf("/%s", username)
42+}
43+
44+func (c *ConfigSite) FullBlogURL(username string, onSubdomain bool, withUserName bool) string {
45+ if c.IsSubdomains() && onSubdomain {
46+ return fmt.Sprintf("%s://%s.%s", c.Protocol, username, c.Domain)
47+ }
48+
49+ if withUserName {
50+ return fmt.Sprintf("/%s", username)
51+ }
52+
53+ return "/"
54+}
55+
56+func (c *ConfigSite) PostURL(username, filename string) string {
57+ fname := url.PathEscape(filename)
58+ if c.IsSubdomains() {
59+ return fmt.Sprintf("%s://%s.%s/%s", c.Protocol, username, c.Domain, fname)
60+ }
61+
62+ return fmt.Sprintf("/%s/%s", username, fname)
63+
64+}
65+
66+func (c *ConfigSite) FullPostURL(username, filename string, onSubdomain bool, withUserName bool) string {
67+ fname := url.PathEscape(filename)
68+ if c.IsSubdomains() && onSubdomain {
69+ return fmt.Sprintf("%s://%s.%s/%s", c.Protocol, username, c.Domain, fname)
70+ }
71+
72+ if withUserName {
73+ return fmt.Sprintf("/%s/%s", username, fname)
74+ }
75+
76+ return fmt.Sprintf("/%s", fname)
77+}
78+
79+func (c *ConfigSite) IsSubdomains() bool {
80+ return c.SubdomainsEnabled
81+}
82+
83+func (c *ConfigSite) IsCustomdomains() bool {
84+ return c.CustomdomainsEnabled
85+}
86+
87+func (c *ConfigSite) RssBlogURL(username string, onSubdomain bool, withUserName bool) string {
88+ if c.IsSubdomains() && onSubdomain {
89+ return fmt.Sprintf("%s://%s.%s/rss", c.Protocol, username, c.Domain)
90+ }
91+
92+ if withUserName {
93+ return fmt.Sprintf("/%s/rss", username)
94+ }
95+
96+ return "/rss"
97+}
98+
99+func (c *ConfigSite) HomeURL() string {
100+ if c.IsSubdomains() || c.IsCustomdomains() {
101+ return fmt.Sprintf("//%s", c.Domain)
102+ }
103+
104+ return "/"
105+}
106+
107+func (c *ConfigSite) ReadURL() string {
108+ if c.IsSubdomains() || c.IsCustomdomains() {
109+ return fmt.Sprintf("%s://%s/read", c.Protocol, c.Domain)
110+ }
111+
112+ return "/read"
113+}
114+
115+func (c *ConfigSite) CssURL(username string) string {
116+ if c.IsSubdomains() || c.IsCustomdomains() {
117+ return fmt.Sprintf("%s://%s.%s/_styles.css", c.Protocol, username, c.Domain)
118+ }
119+
120+ return fmt.Sprintf("/%s/styles.css", username)
121+}
122+
123+func (c *ConfigSite) StaticPath(fname string) string {
124+ return path.Join(c.Space, fname)
125+}
126+
127+func (c *ConfigSite) RawPostURL(username, filename string) string {
128+ fname := url.PathEscape(filename)
129+ if c.IsSubdomains() {
130+ return fmt.Sprintf("%s://%s.%s/raw/%s", c.Protocol, username, c.Domain, fname)
131+ }
132+
133+ return fmt.Sprintf("/raw/%s/%s", username, fname)
134+}
135+
136+func CreateLogger() *zap.SugaredLogger {
137+ logger, err := zap.NewProduction()
138+ if err != nil {
139+ log.Fatal(err)
140+ }
141+
142+ return logger.Sugar()
143+}
1@@ -1,4 +1,4 @@
2-package prose
3+package shared
4
5 import (
6 "context"
1@@ -1,4 +1,4 @@
2-package lists
3+package shared
4
5 import (
6 "encoding/base64"
7@@ -70,13 +70,11 @@ func IsText(s string) bool {
8 return true
9 }
10
11-var allowedExtensions = []string{".txt"}
12-
13 // IsTextFile reports whether the file has a known extension indicating
14 // a text file, or if a significant chunk of the specified file looks like
15 // correct UTF-8; that is, if it is likely that the file contains human-
16 // readable text.
17-func IsTextFile(text string, filename string) bool {
18+func IsTextFile(text string, filename string, allowedExtensions []string) bool {
19 ext := pathpkg.Ext(filename)
20 if !slices.Contains(allowedExtensions, ext) {
21 return false