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
*.swo
build
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
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 ./...
@ -27,10 +36,30 @@ 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 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
cd less/; $(MAKE) $(MFLAGS)
clean :
-rm -rf build
cd less/; $(MAKE) clean $(MFLAGS)
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.
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.

48
app.go
View File

@ -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,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 {
p := struct {
page.StaticPage
Content template.HTML
Updated string
Content template.HTML
PlainContent string
Updated string
AboutStats *InstanceStats
}{
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" {
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)
}
@ -117,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")
}
@ -357,7 +367,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)
@ -391,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)
@ -403,8 +428,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)

View File

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

View File

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

View File

@ -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"},

View File

@ -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 := ""

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) {
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
}

View File

@ -1,4 +1,5 @@
{{define "head"}}<title>About {{.SiteName}}</title>
<meta name="description" content="{{.PlainContent}}">
{{end}}
{{define "content"}}
<div class="content-container snug">
@ -6,6 +7,11 @@
{{.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>
<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>

View File

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

View File

@ -56,7 +56,7 @@ function savePage(el) {
<h2>Site</h2>
<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)">
<textarea id="about-editor" class="section codable norm edit-page" name="content">{{.AboutPage}}</textarea>
<input type="submit" value="Save" />