repos / pico

pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh
git clone https://github.com/picosh/pico.git

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 cmd/lists/ssh/main.go
+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)
M cmd/pastes/ssh/main.go
+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)
M cmd/prose/ssh/main.go
+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)
M lists/api.go
+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)
M lists/config.go
+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-}
M lists/db_handler.go
+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 
M lists/gemini/gemini.go
+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,
M lists/gemini/router.go
+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 
D lists/router.go
+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:
M pastes/api.go
+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)
M pastes/config.go
+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-}
M pastes/cron.go
+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 {
M pastes/db_handler.go
+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,
D pastes/router.go
+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-}
D pastes/util.go
+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-}
M prose/api.go
+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)
M prose/config.go
+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-}
M prose/db_handler.go
+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)
D prose/util.go
+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-}
A shared/config.go
+142, -0
  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+}
R prose/router.go => shared/router.go
+1, -1
1@@ -1,4 +1,4 @@
2-package prose
3+package shared
4 
5 import (
6 	"context"
R lists/util.go => shared/util.go
+2, -4
 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