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:
commit
d63db27917
|
@ -2,4 +2,5 @@
|
|||
*.swp
|
||||
*.swo
|
||||
|
||||
build
|
||||
config.ini
|
||||
|
|
|
@ -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)
|
||||
|
29
Makefile
29
Makefile
|
@ -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 :
|
||||
|
|
|
@ -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
48
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,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)
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
writefreely
|
||||
writefreely.exe
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"},
|
||||
|
|
18
database.go
18
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 := ""
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
package writefreely
|
||||
|
||||
type InstanceStats struct {
|
||||
NumPosts int64
|
||||
NumBlogs int64
|
||||
}
|
12
nodeinfo.go
12
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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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" />
|
||||
|
|
Loading…
Reference in New Issue