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) | |||
}() | |||
// Start gopher server | |||
if app.cfg.Server.GopherPort > 0 { | |||
go initGopher(app) | |||
} | |||
// Start web application server | |||
var bindAddress = app.cfg.Server.Bind | |||
if bindAddress == "" { | |||
@@ -45,6 +45,8 @@ type ( | |||
HashSeed string `ini:"hash_seed"` | |||
GopherPort int `ini:"gopher_port"` | |||
Dev bool `ini:"-"` | |||
} | |||
@@ -1633,6 +1633,40 @@ func (db *datastore) GetPublishableCollections(u *User, hostName string) (*[]Col | |||
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 { | |||
s := userMeStats{} | |||
@@ -34,6 +34,7 @@ require ( | |||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d | |||
github.com/pelletier/go-toml v1.2.0 // 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/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | |||
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/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q= | |||
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" | |||
"github.com/gorilla/sessions" | |||
"github.com/prologic/go-gopher" | |||
"github.com/writeas/impart" | |||
"github.com/writeas/web-core/log" | |||
"github.com/writeas/writefreely/config" | |||
@@ -64,6 +65,7 @@ func UserLevelReader(cfg *config.Config) UserLevel { | |||
type ( | |||
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 | |||
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) | |||
@@ -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 { | |||
w.Header().Set("Location", location) | |||
w.WriteHeader(code) | |||