Merge remote-tracking branch 'upstream/master' into codehighlight

* upstream/master:
  Work as a standalone server, including TLS
  Include About/Privacy page content in page description
  Show instance stats on About page
  Change default database name to writefreely
  Use and validate database type before connecting
  Mention Contributing Guide in README
  Add AUTHORS.md
  Fix About page link in Admin dash
  Include version in archives made by `make release`
  Remove keys.sh from make release
  Add make release
This commit is contained in:
Marcel van der Boom 2018-11-22 15:09:58 +01:00
commit d63db27917
14 changed files with 202 additions and 24 deletions

1
.gitignore vendored
View File

@ -2,4 +2,5 @@
*.swp *.swp
*.swo *.swo
build
config.ini config.ini

8
AUTHORS.md Normal file
View File

@ -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)

View File

@ -13,6 +13,15 @@ all : build
build: deps build: deps
cd cmd/writefreely; $(GOBUILD) -v 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: test:
$(GOTEST) -v ./... $(GOTEST) -v ./...
@ -27,10 +36,30 @@ install : build
cmd/writefreely/$(BINARY_NAME) --gen-keys cmd/writefreely/$(BINARY_NAME) --gen-keys
cd less/; $(MAKE) install $(MFLAGS) 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 schema.sql build
$(MAKE) build-linux
cp cmd/writefreely/$(BINARY_NAME) build
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)_$(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)_$(GITREV)_windows_amd64.zip ./*
ui : force_look ui : force_look
cd less/; $(MAKE) $(MFLAGS) cd less/; $(MAKE) $(MFLAGS)
clean : clean :
-rm -rf build
cd less/; $(MAKE) clean $(MFLAGS) cd less/; $(MAKE) clean $(MFLAGS)
force_look : force_look :

View File

@ -21,7 +21,7 @@
WriteFreely is a beautifully pared-down blogging platform that's simple on the surface, yet powerful underneath. 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)** **[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 Write Freely doesn't yet provide an official Docker pathway to production. We're
working on it, though! 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 ## License
Licensed under the AGPL. Licensed under the AGPL.

48
app.go
View File

@ -20,6 +20,7 @@ import (
"github.com/gorilla/schema" "github.com/gorilla/schema"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
"github.com/manifoldco/promptui" "github.com/manifoldco/promptui"
"github.com/writeas/go-strip-markdown"
"github.com/writeas/web-core/converter" "github.com/writeas/web-core/converter"
"github.com/writeas/web-core/log" "github.com/writeas/web-core/log"
"github.com/writeas/writefreely/config" "github.com/writeas/writefreely/config"
@ -97,8 +98,11 @@ 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 { func handleTemplatedPage(app *app, w http.ResponseWriter, r *http.Request, t *template.Template) error {
p := struct { p := struct {
page.StaticPage page.StaticPage
Content template.HTML Content template.HTML
Updated string PlainContent string
Updated string
AboutStats *InstanceStats
}{ }{
StaticPage: pageForReq(app, r), StaticPage: pageForReq(app, r),
} }
@ -109,6 +113,11 @@ func handleTemplatedPage(app *app, w http.ResponseWriter, r *http.Request, t *te
if r.URL.Path == "/about" { if r.URL.Path == "/about" {
c, err = getAboutPage(app) c, err = getAboutPage(app)
// Fetch stats
p.AboutStats = &InstanceStats{}
p.AboutStats.NumPosts, _ = app.db.GetTotalPosts()
p.AboutStats.NumBlogs, _ = app.db.GetTotalCollections()
} else { } else {
c, updated, err = getPrivacyPage(app) c, updated, err = getPrivacyPage(app)
} }
@ -117,6 +126,7 @@ func handleTemplatedPage(app *app, w http.ResponseWriter, r *http.Request, t *te
return err return err
} }
p.Content = template.HTML(applyMarkdown([]byte(c))) p.Content = template.HTML(applyMarkdown([]byte(c)))
p.PlainContent = shortPostDescription(stripmd.Strip(c))
if updated != nil { if updated != nil {
p.Updated = updated.Format("January 2, 2006") p.Updated = updated.Format("January 2, 2006")
} }
@ -357,7 +367,7 @@ func Serve() {
app.cfg.Database.Host = "localhost" app.cfg.Database.Host = "localhost"
} }
if app.cfg.Database.Database == "" { if app.cfg.Database.Database == "" {
app.cfg.Database.Database = "writeas" app.cfg.Database.Database = "writefreely"
} }
connectToDatabase(app) connectToDatabase(app)
@ -391,11 +401,26 @@ func Serve() {
os.Exit(0) os.Exit(0)
}() }()
// Start web application server
http.Handle("/", r) http.Handle("/", r)
log.Info("Serving on http://localhost:%d\n", app.cfg.Server.Port)
log.Info("---") // Start web application server
err = http.ListenAndServe(fmt.Sprintf(":%d", app.cfg.Server.Port), nil) 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 { if err != nil {
log.Error("Unable to start: %v", err) log.Error("Unable to start: %v", err)
os.Exit(1) os.Exit(1)
@ -403,8 +428,13 @@ func Serve() {
} }
func connectToDatabase(app *app) { func connectToDatabase(app *app) {
log.Info("Connecting to database...") if app.cfg.Database.Type != "mysql" {
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()))) 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 { if err != nil {
log.Error("%s", err) log.Error("%s", err)
os.Exit(1) os.Exit(1)

View File

@ -1 +1,2 @@
writefreely writefreely
writefreely.exe

View File

@ -13,6 +13,9 @@ type (
HiddenHost string `ini:"hidden_host"` HiddenHost string `ini:"hidden_host"`
Port int `ini:"port"` Port int `ini:"port"`
TLSCertPath string `ini:"tls_cert_path"`
TLSKeyPath string `ini:"tls_key_path"`
Dev bool `ini:"-"` 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) { func Load() (*Config, error) {
cfg, err := ini.Load(FileName) cfg, err := ini.Load(FileName)
if err != nil { if err != nil {

View File

@ -47,17 +47,80 @@ func Configure() (*SetupData, error) {
Selected: fmt.Sprintf(`{{.Label}} {{ . | faint }}`), Selected: fmt.Sprintf(`{{.Label}} {{ . | faint }}`),
} }
prompt := promptui.Prompt{ // Environment selection
Templates: tmpls, selPrompt := promptui.Select{
Label: "Local port", Templates: selTmpls,
Validate: validatePort, Label: "Environment",
Default: fmt.Sprintf("%d", data.Config.Server.Port), Items: []string{"Development", "Production, standalone", "Production, behind reverse proxy"},
} }
port, err := prompt.Run() _, envType, err := selPrompt.Run()
if err != nil { if err != nil {
return data, err 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() fmt.Println()
title(" Database setup ") title(" Database setup ")
@ -124,7 +187,7 @@ func Configure() (*SetupData, error) {
title(" App setup ") title(" App setup ")
fmt.Println() fmt.Println()
selPrompt := promptui.Select{ selPrompt = promptui.Select{
Templates: selTmpls, Templates: selTmpls,
Label: "Site type", Label: "Site type",
Items: []string{"Single user blog", "Multi-user instance"}, Items: []string{"Single user blog", "Multi-user instance"},

View File

@ -50,6 +50,8 @@ type writestore interface {
GetCollections(u *User) (*[]Collection, error) GetCollections(u *User) (*[]Collection, error)
GetPublishableCollections(u *User) (*[]Collection, error) GetPublishableCollections(u *User) (*[]Collection, error)
GetMeStats(u *User) userMeStats GetMeStats(u *User) userMeStats
GetTotalCollections() (int64, error)
GetTotalPosts() (int64, error)
GetTopPosts(u *User, alias string) (*[]PublicPost, error) GetTopPosts(u *User, alias string) (*[]PublicPost, error)
GetAnonymousPosts(u *User) (*[]PublicPost, error) GetAnonymousPosts(u *User) (*[]PublicPost, error)
GetUserPosts(u *User) (*[]PublicPost, error) GetUserPosts(u *User) (*[]PublicPost, error)
@ -1541,6 +1543,22 @@ func (db *datastore) GetMeStats(u *User) userMeStats {
return s 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) { func (db *datastore) GetTopPosts(u *User, alias string) (*[]PublicPost, error) {
params := []interface{}{u.ID} params := []interface{}{u.ID}
where := "" where := ""

6
instance.go Normal file
View File

@ -0,0 +1,6 @@
package writefreely
type InstanceStats struct {
NumPosts int64
NumBlogs int64
}

View File

@ -57,12 +57,14 @@ func (r nodeInfoResolver) IsOpenRegistration() (bool, error) {
} }
func (r nodeInfoResolver) Usage() (nodeinfo.Usage, error) { func (r nodeInfoResolver) Usage() (nodeinfo.Usage, error) {
var collCount, postCount, activeHalfYear, activeMonth int var collCount, postCount int64
err := r.db.QueryRow(`SELECT COUNT(*) FROM collections`).Scan(&collCount) var activeHalfYear, activeMonth int
var err error
collCount, err = r.db.GetTotalCollections()
if err != nil { if err != nil {
collCount = 0 collCount = 0
} }
err = r.db.QueryRow(`SELECT COUNT(*) FROM posts`).Scan(&postCount) postCount, err = r.db.GetTotalPosts()
if err != nil { if err != nil {
log.Error("Unable to fetch post counts: %v", err) log.Error("Unable to fetch post counts: %v", err)
} }
@ -88,10 +90,10 @@ WHERE collection_id IS NOT NULL
return nodeinfo.Usage{ return nodeinfo.Usage{
Users: nodeinfo.UsageUsers{ Users: nodeinfo.UsageUsers{
Total: collCount, Total: int(collCount),
ActiveHalfYear: activeHalfYear, ActiveHalfYear: activeHalfYear,
ActiveMonth: activeMonth, ActiveMonth: activeMonth,
}, },
LocalPosts: postCount, LocalPosts: int(postCount),
}, nil }, nil
} }

View File

@ -1,4 +1,5 @@
{{define "head"}}<title>About {{.SiteName}}</title> {{define "head"}}<title>About {{.SiteName}}</title>
<meta name="description" content="{{.PlainContent}}">
{{end}} {{end}}
{{define "content"}} {{define "content"}}
<div class="content-container snug"> <div class="content-container snug">
@ -6,6 +7,11 @@
{{.Content}} {{.Content}}
{{if .Federation}}
<hr style="margin:1.5em 0;" />
<p><em>{{.SiteName}}</em> is home to <strong>{{largeNumFmt .AboutStats.NumPosts}}</strong> {{pluralize "article" "articles" .AboutStats.NumPosts}} across <strong>{{largeNumFmt .AboutStats.NumBlogs}}</strong> {{pluralize "blog" "blogs" .AboutStats.NumBlogs}}.</p>
{{end}}
<h2 style="margin-top:2em">About WriteFreely</h2> <h2 style="margin-top:2em">About WriteFreely</h2>
<p><a href="https://writefreely.org">WriteFreely</a> is a self-hosted, decentralized blogging platform for publishing beautiful, simple blogs.</p> <p><a href="https://writefreely.org">WriteFreely</a> is a self-hosted, decentralized blogging platform for publishing beautiful, simple blogs.</p>
<p>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.</p> <p>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.</p>

View File

@ -1,4 +1,5 @@
{{define "head"}}<title>{{.SiteName}} Privacy Policy</title> {{define "head"}}<title>{{.SiteName}} Privacy Policy</title>
<meta name="description" content="{{.PlainContent}}">
{{end}} {{end}}
{{define "content"}}<div class="content-container snug"> {{define "content"}}<div class="content-container snug">
<h1>Privacy Policy</h1> <h1>Privacy Policy</h1>

View File

@ -56,7 +56,7 @@ function savePage(el) {
<h2>Site</h2> <h2>Site</h2>
<h3 id="page-about">About page</h3> <h3 id="page-about">About page</h3>
<p>Describe what your instance is <a href="/privacy">about</a>. <em>Accepts Markdown</em>.</p> <p>Describe what your instance is <a href="/about">about</a>. <em>Accepts Markdown</em>.</p>
<form method="post" action="/admin/update/about" onsubmit="savePage(this)"> <form method="post" action="/admin/update/about" onsubmit="savePage(this)">
<textarea id="about-editor" class="section codable norm edit-page" name="content">{{.AboutPage}}</textarea> <textarea id="about-editor" class="section codable norm edit-page" name="content">{{.AboutPage}}</textarea>
<input type="submit" value="Save" /> <input type="submit" value="Save" />