From 64e52ba00d23d8056b3674cc7655fb0657ced8ca Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Fri, 9 Nov 2018 22:19:24 -0500 Subject: [PATCH 01/11] Add make release --- .gitignore | 1 + Makefile | 30 ++++++++++++++++++++++++++++++ cmd/writefreely/.gitignore | 1 + 3 files changed, 32 insertions(+) diff --git a/.gitignore b/.gitignore index 4e501c0..228e67d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ *.swp *.swo +build config.ini diff --git a/Makefile b/Makefile index 09a216f..2372257 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,15 @@ all : build build: deps cd cmd/writefreely; $(GOBUILD) -v +build-linux: deps + cd cmd/writefreely; GOOS=linux GOARCH=amd64 $(GOBUILD) -v + +build-windows: deps + cd cmd/writefreely; GOOS=windows GOARCH=amd64 $(GOBUILD) -v + +build-darwin: deps + cd cmd/writefreely; GOOS=darwin GOARCH=amd64 $(GOBUILD) -v + test: $(GOTEST) -v ./... @@ -24,10 +33,31 @@ install : build cmd/writefreely/$(BINARY_NAME) --gen-keys cd less/; $(MAKE) install $(MFLAGS) +release : clean ui + mkdir build + cp -r templates build + cp -r pages build + cp -r static build + mkdir build/keys + cp keys.sh build + $(MAKE) build-linux + cp cmd/writefreely/$(BINARY_NAME) build + cd build; tar -cvzf ../$(BINARY_NAME)_linux_amd64.tar.gz * + rm build/$(BINARY_NAME) + $(MAKE) build-darwin + cp cmd/writefreely/$(BINARY_NAME) build + cd build; tar -cvzf ../$(BINARY_NAME)_darwin_amd64.tar.gz * + rm build/$(BINARY_NAME) + rm build/keys.sh + $(MAKE) build-windows + cp cmd/writefreely/$(BINARY_NAME).exe build + cd build; zip -r ../$(BINARY_NAME)_windows_amd64.zip ./* + ui : force_look cd less/; $(MAKE) $(MFLAGS) clean : + -rm -rf build cd less/; $(MAKE) clean $(MFLAGS) force_look : diff --git a/cmd/writefreely/.gitignore b/cmd/writefreely/.gitignore index 0c3aa8d..6c3985c 100644 --- a/cmd/writefreely/.gitignore +++ b/cmd/writefreely/.gitignore @@ -1 +1,2 @@ writefreely +writefreely.exe From 87ffafeb54ef8ec659d1b0e31a85de1e2231e468 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sun, 11 Nov 2018 17:56:11 -0500 Subject: [PATCH 02/11] Remove keys.sh from make release --- Makefile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 2372257..82c619a 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,7 @@ release : clean ui cp -r pages build cp -r static build mkdir build/keys - cp keys.sh build + cp schema.sql build $(MAKE) build-linux cp cmd/writefreely/$(BINARY_NAME) build cd build; tar -cvzf ../$(BINARY_NAME)_linux_amd64.tar.gz * @@ -48,7 +48,6 @@ release : clean ui cp cmd/writefreely/$(BINARY_NAME) build cd build; tar -cvzf ../$(BINARY_NAME)_darwin_amd64.tar.gz * rm build/$(BINARY_NAME) - rm build/keys.sh $(MAKE) build-windows cp cmd/writefreely/$(BINARY_NAME).exe build cd build; zip -r ../$(BINARY_NAME)_windows_amd64.zip ./* From bf5ed0048486c5cf85d4c45dc84176068b348d5b Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Tue, 20 Nov 2018 18:35:52 -0500 Subject: [PATCH 03/11] Include version in archives made by `make release` --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index e894819..126bbb1 100644 --- a/Makefile +++ b/Makefile @@ -45,15 +45,15 @@ release : clean ui cp schema.sql build $(MAKE) build-linux cp cmd/writefreely/$(BINARY_NAME) build - cd build; tar -cvzf ../$(BINARY_NAME)_linux_amd64.tar.gz * + cd build; tar -cvzf ../$(BINARY_NAME)_$(GITREV)_linux_amd64.tar.gz * rm build/$(BINARY_NAME) $(MAKE) build-darwin cp cmd/writefreely/$(BINARY_NAME) build - cd build; tar -cvzf ../$(BINARY_NAME)_darwin_amd64.tar.gz * + cd build; tar -cvzf ../$(BINARY_NAME)_$(GITREV)_darwin_amd64.tar.gz * rm build/$(BINARY_NAME) $(MAKE) build-windows cp cmd/writefreely/$(BINARY_NAME).exe build - cd build; zip -r ../$(BINARY_NAME)_windows_amd64.zip ./* + cd build; zip -r ../$(BINARY_NAME)_$(GITREV)_windows_amd64.zip ./* ui : force_look cd less/; $(MAKE) $(MFLAGS) From 8a5811e3e96dbeb23cbde497a8504ee87167a8e4 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 21 Nov 2018 11:12:44 -0500 Subject: [PATCH 04/11] Fix About page link in Admin dash --- templates/user/admin.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/user/admin.tmpl b/templates/user/admin.tmpl index 228dfc3..bdfbd71 100644 --- a/templates/user/admin.tmpl +++ b/templates/user/admin.tmpl @@ -56,7 +56,7 @@ function savePage(el) {

Site

About page

-

Describe what your instance is about. Accepts Markdown.

+

Describe what your instance is about. Accepts Markdown.

From 4c7969d80867cf9eea480ba2d48893f074e84fb4 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 21 Nov 2018 11:46:23 -0500 Subject: [PATCH 05/11] Add AUTHORS.md --- AUTHORS.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 AUTHORS.md diff --git a/AUTHORS.md b/AUTHORS.md new file mode 100644 index 0000000..bf363a6 --- /dev/null +++ b/AUTHORS.md @@ -0,0 +1,8 @@ +# WriteFreely Contributors + +WriteFreely is built by [Matt Baer](https://github.com/thebaer), with contributions from: + +* [Jean-Francois Arseneau](https://github.com/TheJF) +* [Ben Overmyer](https://github.com/BenOvermyer) +* [Marcel van der Boom](https://github.com/mrvdb) + From 82e45ef5ae1f2c24910b5cdc1f4685307adb8571 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 21 Nov 2018 11:54:01 -0500 Subject: [PATCH 06/11] Mention Contributing Guide in README --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b5cf0db..6467555 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ WriteFreely is a beautifully pared-down blogging platform that's simple on the surface, yet powerful underneath. -It's designed to be flexible and share your writing widely, so it's built around plain text and can publish to the _fediverse_ via ActivityPub. It's easy to install and lightweight. +It's designed to be flexible and share your writing widely, so it's built around plain text and can publish to the _fediverse_ via ActivityPub. It's easy to install and light enough to run on a Raspberry Pi. **[Start a blog on our instance](https://write.as/new/blog/federated)** @@ -123,6 +123,12 @@ will shut down your environment without destroying your data. Write Freely doesn't yet provide an official Docker pathway to production. We're working on it, though! +## Contributing + +We gladly welcome contributions to WriteFreely, whether in the form of [code](https://github.com/writeas/writefreely/blob/master/CONTRIBUTING.md#contributing-to-writefreely), [bug reports](https://github.com/writeas/writefreely/issues/new?template=bug_report.md), [feature requests](https://discuss.write.as/c/feedback/feature-requests), [translations](https://poeditor.com/join/project/TIZ6HFRFdE), or documentation improvements. + +Before contributing anything, please read our [Contributing Guide](https://github.com/writeas/writefreely/blob/master/CONTRIBUTING.md#contributing-to-writefreely). It describes the correct channels for submitting contributions and any potential requirements. + ## License Licensed under the AGPL. From f3df2b4159d4c969faf66b2bb0b07b239323834c Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 21 Nov 2018 13:10:10 -0500 Subject: [PATCH 07/11] Use and validate database type before connecting Just the start of changes needed for T529. --- app.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app.go b/app.go index ada7392..cdc7860 100644 --- a/app.go +++ b/app.go @@ -403,8 +403,13 @@ func Serve() { } func connectToDatabase(app *app) { - log.Info("Connecting to database...") - db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true&loc=%s", app.cfg.Database.User, app.cfg.Database.Password, app.cfg.Database.Host, app.cfg.Database.Port, app.cfg.Database.Database, url.QueryEscape(time.Local.String()))) + if app.cfg.Database.Type != "mysql" { + log.Error("Invalid database type '%s'. Only 'mysql' is supported right now.", app.cfg.Database.Type) + os.Exit(1) + } + + log.Info("Connecting to %s database...", app.cfg.Database.Type) + db, err := sql.Open(app.cfg.Database.Type, fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true&loc=%s", app.cfg.Database.User, app.cfg.Database.Password, app.cfg.Database.Host, app.cfg.Database.Port, app.cfg.Database.Database, url.QueryEscape(time.Local.String()))) if err != nil { log.Error("%s", err) os.Exit(1) From b9d7d4ce24de6e3d1b30307208a3d982d07bddab Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 21 Nov 2018 13:36:00 -0500 Subject: [PATCH 08/11] Change default database name to writefreely (not writeas) --- app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.go b/app.go index cdc7860..65939a9 100644 --- a/app.go +++ b/app.go @@ -357,7 +357,7 @@ func Serve() { app.cfg.Database.Host = "localhost" } if app.cfg.Database.Database == "" { - app.cfg.Database.Database = "writeas" + app.cfg.Database.Database = "writefreely" } connectToDatabase(app) From be2c7ef86bb4771325724eb9897e491bae9bc789 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 21 Nov 2018 14:05:44 -0500 Subject: [PATCH 09/11] Show instance stats on About page This also moves the stats database logic out of nodeinfo.go and into database.go. --- app.go | 7 +++++++ database.go | 18 ++++++++++++++++++ instance.go | 6 ++++++ nodeinfo.go | 12 +++++++----- pages/about.tmpl | 5 +++++ 5 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 instance.go diff --git a/app.go b/app.go index 65939a9..172a00e 100644 --- a/app.go +++ b/app.go @@ -99,6 +99,8 @@ func handleTemplatedPage(app *app, w http.ResponseWriter, r *http.Request, t *te page.StaticPage Content template.HTML Updated string + + AboutStats *InstanceStats }{ StaticPage: pageForReq(app, r), } @@ -109,6 +111,11 @@ func handleTemplatedPage(app *app, w http.ResponseWriter, r *http.Request, t *te if r.URL.Path == "/about" { c, err = getAboutPage(app) + + // Fetch stats + p.AboutStats = &InstanceStats{} + p.AboutStats.NumPosts, _ = app.db.GetTotalPosts() + p.AboutStats.NumBlogs, _ = app.db.GetTotalCollections() } else { c, updated, err = getPrivacyPage(app) } diff --git a/database.go b/database.go index 0c79e10..468b04d 100644 --- a/database.go +++ b/database.go @@ -50,6 +50,8 @@ type writestore interface { GetCollections(u *User) (*[]Collection, error) GetPublishableCollections(u *User) (*[]Collection, error) GetMeStats(u *User) userMeStats + GetTotalCollections() (int64, error) + GetTotalPosts() (int64, error) GetTopPosts(u *User, alias string) (*[]PublicPost, error) GetAnonymousPosts(u *User) (*[]PublicPost, error) GetUserPosts(u *User) (*[]PublicPost, error) @@ -1541,6 +1543,22 @@ func (db *datastore) GetMeStats(u *User) userMeStats { return s } +func (db *datastore) GetTotalCollections() (collCount int64, err error) { + err = db.QueryRow(`SELECT COUNT(*) FROM collections`).Scan(&collCount) + if err != nil { + log.Error("Unable to fetch collections count: %v", err) + } + return +} + +func (db *datastore) GetTotalPosts() (postCount int64, err error) { + err = db.QueryRow(`SELECT COUNT(*) FROM posts`).Scan(&postCount) + if err != nil { + log.Error("Unable to fetch posts count: %v", err) + } + return +} + func (db *datastore) GetTopPosts(u *User, alias string) (*[]PublicPost, error) { params := []interface{}{u.ID} where := "" diff --git a/instance.go b/instance.go new file mode 100644 index 0000000..482e65d --- /dev/null +++ b/instance.go @@ -0,0 +1,6 @@ +package writefreely + +type InstanceStats struct { + NumPosts int64 + NumBlogs int64 +} diff --git a/nodeinfo.go b/nodeinfo.go index fd4b445..eab15cc 100644 --- a/nodeinfo.go +++ b/nodeinfo.go @@ -57,12 +57,14 @@ func (r nodeInfoResolver) IsOpenRegistration() (bool, error) { } func (r nodeInfoResolver) Usage() (nodeinfo.Usage, error) { - var collCount, postCount, activeHalfYear, activeMonth int - err := r.db.QueryRow(`SELECT COUNT(*) FROM collections`).Scan(&collCount) + var collCount, postCount int64 + var activeHalfYear, activeMonth int + var err error + collCount, err = r.db.GetTotalCollections() if err != nil { collCount = 0 } - err = r.db.QueryRow(`SELECT COUNT(*) FROM posts`).Scan(&postCount) + postCount, err = r.db.GetTotalPosts() if err != nil { log.Error("Unable to fetch post counts: %v", err) } @@ -88,10 +90,10 @@ WHERE collection_id IS NOT NULL return nodeinfo.Usage{ Users: nodeinfo.UsageUsers{ - Total: collCount, + Total: int(collCount), ActiveHalfYear: activeHalfYear, ActiveMonth: activeMonth, }, - LocalPosts: postCount, + LocalPosts: int(postCount), }, nil } diff --git a/pages/about.tmpl b/pages/about.tmpl index a47faf6..71a6223 100644 --- a/pages/about.tmpl +++ b/pages/about.tmpl @@ -6,6 +6,11 @@ {{.Content}} + {{if .Federation}} +
+

{{.SiteName}} is home to {{largeNumFmt .AboutStats.NumPosts}} {{pluralize "article" "articles" .AboutStats.NumPosts}} across {{largeNumFmt .AboutStats.NumBlogs}} {{pluralize "blog" "blogs" .AboutStats.NumBlogs}}.

+ {{end}} +

About WriteFreely

WriteFreely is a self-hosted, decentralized blogging platform for publishing beautiful, simple blogs.

It lets you publish a single blog, or host a community of writers who can create multiple blogs under one account. You can also enable federation, which allows people in the fediverse to follow your blog, bookmark your posts, and share them with others.

From 77e79acd065f2eaf67bee814f74657a2d47fed9b Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 21 Nov 2018 15:04:47 -0500 Subject: [PATCH 10/11] Include About/Privacy page content in page description --- app.go | 7 +++++-- pages/about.tmpl | 1 + pages/privacy.tmpl | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app.go b/app.go index 172a00e..e061de4 100644 --- a/app.go +++ b/app.go @@ -20,6 +20,7 @@ import ( "github.com/gorilla/schema" "github.com/gorilla/sessions" "github.com/manifoldco/promptui" + "github.com/writeas/go-strip-markdown" "github.com/writeas/web-core/converter" "github.com/writeas/web-core/log" "github.com/writeas/writefreely/config" @@ -97,8 +98,9 @@ func handleViewHome(app *app, w http.ResponseWriter, r *http.Request) error { func handleTemplatedPage(app *app, w http.ResponseWriter, r *http.Request, t *template.Template) error { p := struct { page.StaticPage - Content template.HTML - Updated string + Content template.HTML + PlainContent string + Updated string AboutStats *InstanceStats }{ @@ -124,6 +126,7 @@ func handleTemplatedPage(app *app, w http.ResponseWriter, r *http.Request, t *te return err } p.Content = template.HTML(applyMarkdown([]byte(c))) + p.PlainContent = shortPostDescription(stripmd.Strip(c)) if updated != nil { p.Updated = updated.Format("January 2, 2006") } diff --git a/pages/about.tmpl b/pages/about.tmpl index 71a6223..64c2500 100644 --- a/pages/about.tmpl +++ b/pages/about.tmpl @@ -1,4 +1,5 @@ {{define "head"}}About {{.SiteName}} + {{end}} {{define "content"}}
diff --git a/pages/privacy.tmpl b/pages/privacy.tmpl index a32d028..9d0d177 100644 --- a/pages/privacy.tmpl +++ b/pages/privacy.tmpl @@ -1,4 +1,5 @@ {{define "head"}}{{.SiteName}} Privacy Policy + {{end}} {{define "content"}}

Privacy Policy

From 09f5953431c4f541861fd06beb7437ef48a92cce Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 21 Nov 2018 18:26:19 -0500 Subject: [PATCH 11/11] Work as a standalone server, including TLS This supports running the server on port 443, serving secure pages, with automatic redirects from the insecure site. It also modifies the configuration process to better guide users through configuring for running behind a reverse proxy or as a standalone server. This closes T537 --- app.go | 23 +++++++++++--- config/config.go | 7 +++++ config/setup.go | 79 +++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 97 insertions(+), 12 deletions(-) diff --git a/app.go b/app.go index e061de4..d72ee49 100644 --- a/app.go +++ b/app.go @@ -401,11 +401,26 @@ func Serve() { os.Exit(0) }() - // Start web application server http.Handle("/", r) - log.Info("Serving on http://localhost:%d\n", app.cfg.Server.Port) - log.Info("---") - err = http.ListenAndServe(fmt.Sprintf(":%d", app.cfg.Server.Port), nil) + + // Start web application server + if app.cfg.IsSecureStandalone() { + log.Info("Serving redirects on http://localhost:80") + go func() { + err = http.ListenAndServe(":80", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, app.cfg.App.Host, http.StatusMovedPermanently) + })) + log.Error("Unable to start redirect server: %v", err) + }() + + log.Info("Serving on https://localhost:443") + log.Info("---") + err = http.ListenAndServeTLS(":443", app.cfg.Server.TLSCertPath, app.cfg.Server.TLSKeyPath, nil) + } else { + log.Info("Serving on http://localhost:%d\n", app.cfg.Server.Port) + log.Info("---") + err = http.ListenAndServe(fmt.Sprintf(":%d", app.cfg.Server.Port), nil) + } if err != nil { log.Error("Unable to start: %v", err) os.Exit(1) diff --git a/config/config.go b/config/config.go index c3c0628..56d8848 100644 --- a/config/config.go +++ b/config/config.go @@ -13,6 +13,9 @@ type ( HiddenHost string `ini:"hidden_host"` Port int `ini:"port"` + TLSCertPath string `ini:"tls_cert_path"` + TLSKeyPath string `ini:"tls_key_path"` + Dev bool `ini:"-"` } @@ -76,6 +79,10 @@ func New() *Config { } } +func (cfg *Config) IsSecureStandalone() bool { + return cfg.Server.Port == 443 && cfg.Server.TLSCertPath != "" && cfg.Server.TLSKeyPath != "" +} + func Load() (*Config, error) { cfg, err := ini.Load(FileName) if err != nil { diff --git a/config/setup.go b/config/setup.go index 54fa961..6a2f780 100644 --- a/config/setup.go +++ b/config/setup.go @@ -47,17 +47,80 @@ func Configure() (*SetupData, error) { Selected: fmt.Sprintf(`{{.Label}} {{ . | faint }}`), } - prompt := promptui.Prompt{ - Templates: tmpls, - Label: "Local port", - Validate: validatePort, - Default: fmt.Sprintf("%d", data.Config.Server.Port), + // Environment selection + selPrompt := promptui.Select{ + Templates: selTmpls, + Label: "Environment", + Items: []string{"Development", "Production, standalone", "Production, behind reverse proxy"}, } - port, err := prompt.Run() + _, envType, err := selPrompt.Run() if err != nil { return data, err } - data.Config.Server.Port, _ = strconv.Atoi(port) // Ignore error, as we've already validated number + isDevEnv := envType == "Development" + isStandalone := envType == "Production, standalone" + + data.Config.Server.Dev = isDevEnv + + var prompt promptui.Prompt + if isDevEnv || !isStandalone { + // Running in dev environment or behind reverse proxy; ask for port + prompt = promptui.Prompt{ + Templates: tmpls, + Label: "Local port", + Validate: validatePort, + Default: fmt.Sprintf("%d", data.Config.Server.Port), + } + port, err := prompt.Run() + if err != nil { + return data, err + } + data.Config.Server.Port, _ = strconv.Atoi(port) // Ignore error, as we've already validated number + } + + if isStandalone { + selPrompt = promptui.Select{ + Templates: selTmpls, + Label: "Web server mode", + Items: []string{"Insecure (port 80)", "Secure (port 443)"}, + } + sel, _, err := selPrompt.Run() + if err != nil { + return data, err + } + if sel == 0 { + data.Config.Server.Port = 80 + data.Config.Server.TLSCertPath = "" + data.Config.Server.TLSKeyPath = "" + } else if sel == 1 { + data.Config.Server.Port = 443 + + prompt = promptui.Prompt{ + Templates: tmpls, + Label: "Certificate path", + Validate: validateNonEmpty, + Default: data.Config.Server.TLSCertPath, + } + data.Config.Server.TLSCertPath, err = prompt.Run() + if err != nil { + return data, err + } + + prompt = promptui.Prompt{ + Templates: tmpls, + Label: "Key path", + Validate: validateNonEmpty, + Default: data.Config.Server.TLSKeyPath, + } + data.Config.Server.TLSKeyPath, err = prompt.Run() + if err != nil { + return data, err + } + } + } else { + data.Config.Server.TLSCertPath = "" + data.Config.Server.TLSKeyPath = "" + } fmt.Println() title(" Database setup ") @@ -124,7 +187,7 @@ func Configure() (*SetupData, error) { title(" App setup ") fmt.Println() - selPrompt := promptui.Select{ + selPrompt = promptui.Select{ Templates: selTmpls, Label: "Site type", Items: []string{"Single user blog", "Multi-user instance"},