This adds gopher support to WriteFreely -- both single- and multi-user instances. It is off by default, but can be enabled with the new `gopher_port` config value in the `[server]` section. When enabled, multi-user instances will show all public blogs at gopher://[host]:[gopher_port]/ -- otherwise, blogs are accessible at gopher://[host]:[gopher_port]/[blog]/ This is just a proof of concept for now. We still need to handle some edge cases and different configurations, like private instances. Ref T559pull/273/head
@@ -409,6 +409,11 @@ func Serve(app *App, r *mux.Router) { | |||||
os.Exit(0) | os.Exit(0) | ||||
}() | }() | ||||
// Start gopher server | |||||
if app.cfg.Server.GopherPort > 0 { | |||||
go initGopher(app) | |||||
} | |||||
// Start web application server | // Start web application server | ||||
var bindAddress = app.cfg.Server.Bind | var bindAddress = app.cfg.Server.Bind | ||||
if bindAddress == "" { | if bindAddress == "" { | ||||
@@ -45,6 +45,8 @@ type ( | |||||
HashSeed string `ini:"hash_seed"` | HashSeed string `ini:"hash_seed"` | ||||
GopherPort int `ini:"gopher_port"` | |||||
Dev bool `ini:"-"` | Dev bool `ini:"-"` | ||||
} | } | ||||
@@ -1633,6 +1633,40 @@ func (db *datastore) GetPublishableCollections(u *User, hostName string) (*[]Col | |||||
return c, nil | return c, nil | ||||
} | } | ||||
func (db *datastore) GetPublicCollections(hostName string) (*[]Collection, error) { | |||||
rows, err := db.Query(`SELECT c.id, alias, title, description, privacy, view_count | |||||
FROM collections c | |||||
LEFT JOIN users u ON u.id = c.owner_id | |||||
WHERE c.privacy = 1 AND u.status = 0 | |||||
ORDER BY id ASC`) | |||||
if err != nil { | |||||
log.Error("Failed selecting public collections: %v", err) | |||||
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve public collections."} | |||||
} | |||||
defer rows.Close() | |||||
colls := []Collection{} | |||||
for rows.Next() { | |||||
c := Collection{} | |||||
err = rows.Scan(&c.ID, &c.Alias, &c.Title, &c.Description, &c.Visibility, &c.Views) | |||||
if err != nil { | |||||
log.Error("Failed scanning row: %v", err) | |||||
break | |||||
} | |||||
c.hostName = hostName | |||||
c.URL = c.CanonicalURL() | |||||
c.Public = c.IsPublic() | |||||
colls = append(colls, c) | |||||
} | |||||
err = rows.Err() | |||||
if err != nil { | |||||
log.Error("Error after Next() on rows: %v", err) | |||||
} | |||||
return &colls, nil | |||||
} | |||||
func (db *datastore) GetMeStats(u *User) userMeStats { | func (db *datastore) GetMeStats(u *User) userMeStats { | ||||
s := userMeStats{} | s := userMeStats{} | ||||
@@ -34,6 +34,7 @@ require ( | |||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d | ||||
github.com/pelletier/go-toml v1.2.0 // indirect | github.com/pelletier/go-toml v1.2.0 // indirect | ||||
github.com/pkg/errors v0.8.1 // indirect | github.com/pkg/errors v0.8.1 // indirect | ||||
github.com/prologic/go-gopher v0.0.0-20191226035442-664dbdb49f44 | |||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect | github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect | ||||
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect | github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect | ||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect | github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect | ||||
@@ -110,6 +110,8 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= | |||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||
github.com/prologic/go-gopher v0.0.0-20191226035442-664dbdb49f44 h1:q5sit1FpzEt59aM2Fd2lSBKF+nxcY1o0StRCiJa/pWo= | |||||
github.com/prologic/go-gopher v0.0.0-20191226035442-664dbdb49f44/go.mod h1:a97DSBRiRljeRVd5CRZL5bYCIeeGjSEngGf+QMR2evA= | |||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ= | github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ= | ||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q= | github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q= | ||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= | ||||
@@ -0,0 +1,146 @@ | |||||
/* | |||||
* Copyright © 2020 A Bunch Tell LLC. | |||||
* | |||||
* This file is part of WriteFreely. | |||||
* | |||||
* WriteFreely is free software: you can redistribute it and/or modify | |||||
* it under the terms of the GNU Affero General Public License, included | |||||
* in the LICENSE file in this source code package. | |||||
*/ | |||||
package writefreely | |||||
import ( | |||||
"bytes" | |||||
"fmt" | |||||
"io" | |||||
"strings" | |||||
"github.com/prologic/go-gopher" | |||||
"github.com/writeas/web-core/log" | |||||
) | |||||
func initGopher(apper Apper) { | |||||
handler := NewWFHandler(apper) | |||||
gopher.HandleFunc("/", handler.Gopher(handleGopher)) | |||||
log.Info("Serving on gopher://localhost:%d", apper.App().Config().Server.GopherPort) | |||||
gopher.ListenAndServe(fmt.Sprintf(":%d", apper.App().Config().Server.GopherPort), nil) | |||||
} | |||||
func handleGopher(app *App, w gopher.ResponseWriter, r *gopher.Request) error { | |||||
parts := strings.Split(r.Selector, "/") | |||||
if app.cfg.App.SingleUser { | |||||
if parts[1] != "" { | |||||
return handleGopherCollectionPost(app, w, r) | |||||
} | |||||
return handleGopherCollection(app, w, r) | |||||
} | |||||
// Show all public collections (a gopher Reader view, essentially) | |||||
if len(parts) == 3 { | |||||
return handleGopherCollection(app, w, r) | |||||
} | |||||
w.WriteInfo(fmt.Sprintf("Welcome to %s", app.cfg.App.SiteName)) | |||||
colls, err := app.db.GetPublicCollections(app.cfg.App.Host) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
for _, c := range *colls { | |||||
w.WriteItem(&gopher.Item{ | |||||
Type: gopher.DIRECTORY, | |||||
Description: c.DisplayTitle(), | |||||
Selector: "/" + c.Alias + "/", | |||||
}) | |||||
} | |||||
return w.End() | |||||
} | |||||
func handleGopherCollection(app *App, w gopher.ResponseWriter, r *gopher.Request) error { | |||||
var collAlias, slug string | |||||
var c *Collection | |||||
var err error | |||||
var baseSel = "/" | |||||
parts := strings.Split(r.Selector, "/") | |||||
if app.cfg.App.SingleUser { | |||||
// sanity check | |||||
slug = parts[1] | |||||
if slug != "" { | |||||
return handleGopherCollectionPost(app, w, r) | |||||
} | |||||
c, err = app.db.GetCollectionByID(1) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} else { | |||||
collAlias = parts[1] | |||||
slug = parts[2] | |||||
if slug != "" { | |||||
return handleGopherCollectionPost(app, w, r) | |||||
} | |||||
c, err = app.db.GetCollection(collAlias) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
baseSel = "/" + c.Alias + "/" | |||||
} | |||||
c.hostName = app.cfg.App.Host | |||||
posts, err := app.db.GetPosts(app.cfg, c, 0, false, false, false) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
for _, p := range *posts { | |||||
w.WriteItem(&gopher.Item{ | |||||
Type: gopher.FILE, | |||||
Description: p.CreatedDate() + " - " + p.DisplayTitle(), | |||||
Selector: baseSel + p.Slug.String, | |||||
}) | |||||
} | |||||
return w.End() | |||||
} | |||||
func handleGopherCollectionPost(app *App, w gopher.ResponseWriter, r *gopher.Request) error { | |||||
var collAlias, slug string | |||||
var c *Collection | |||||
var err error | |||||
parts := strings.Split(r.Selector, "/") | |||||
if app.cfg.App.SingleUser { | |||||
slug = parts[1] | |||||
c, err = app.db.GetCollectionByID(1) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} else { | |||||
collAlias = parts[1] | |||||
slug = parts[2] | |||||
c, err = app.db.GetCollection(collAlias) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} | |||||
c.hostName = app.cfg.App.Host | |||||
p, err := app.db.GetPost(slug, c.ID) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
b := bytes.Buffer{} | |||||
if p.Title.String != "" { | |||||
b.WriteString(p.Title.String + "\n") | |||||
} | |||||
b.WriteString(p.DisplayDate + "\n\n") | |||||
b.WriteString(p.Content) | |||||
io.Copy(w, &b) | |||||
return w.End() | |||||
} |
@@ -21,6 +21,7 @@ import ( | |||||
"time" | "time" | ||||
"github.com/gorilla/sessions" | "github.com/gorilla/sessions" | ||||
"github.com/prologic/go-gopher" | |||||
"github.com/writeas/impart" | "github.com/writeas/impart" | ||||
"github.com/writeas/web-core/log" | "github.com/writeas/web-core/log" | ||||
"github.com/writeas/writefreely/config" | "github.com/writeas/writefreely/config" | ||||
@@ -64,6 +65,7 @@ func UserLevelReader(cfg *config.Config) UserLevel { | |||||
type ( | type ( | ||||
handlerFunc func(app *App, w http.ResponseWriter, r *http.Request) error | handlerFunc func(app *App, w http.ResponseWriter, r *http.Request) error | ||||
gopherFunc func(app *App, w gopher.ResponseWriter, r *gopher.Request) error | |||||
userHandlerFunc func(app *App, u *User, w http.ResponseWriter, r *http.Request) error | userHandlerFunc func(app *App, u *User, w http.ResponseWriter, r *http.Request) error | ||||
userApperHandlerFunc func(apper Apper, u *User, w http.ResponseWriter, r *http.Request) error | userApperHandlerFunc func(apper Apper, u *User, w http.ResponseWriter, r *http.Request) error | ||||
dataHandlerFunc func(app *App, w http.ResponseWriter, r *http.Request) ([]byte, string, error) | dataHandlerFunc func(app *App, w http.ResponseWriter, r *http.Request) ([]byte, string, error) | ||||
@@ -891,6 +893,24 @@ func (h *Handler) LogHandlerFunc(f http.HandlerFunc) http.HandlerFunc { | |||||
} | } | ||||
} | } | ||||
func (h *Handler) Gopher(f gopherFunc) gopher.HandlerFunc { | |||||
return func(w gopher.ResponseWriter, r *gopher.Request) { | |||||
defer func() { | |||||
if e := recover(); e != nil { | |||||
log.Error("%s: %s", e, debug.Stack()) | |||||
w.WriteError("An internal error occurred") | |||||
} | |||||
log.Info("gopher: %s", r.Selector) | |||||
}() | |||||
err := f(h.app.App(), w, r) | |||||
if err != nil { | |||||
log.Error("failed: %s", err) | |||||
w.WriteError("the page failed for some reason (see logs)") | |||||
} | |||||
} | |||||
} | |||||
func sendRedirect(w http.ResponseWriter, code int, location string) int { | func sendRedirect(w http.ResponseWriter, code int, location string) int { | ||||
w.Header().Set("Location", location) | w.Header().Set("Location", location) | ||||
w.WriteHeader(code) | w.WriteHeader(code) | ||||