mirror of
https://github.com/writeas/writefreely
synced 2025-02-03 13:57:41 +01:00
Add Gopher support
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 T559
This commit is contained in:
parent
fca864c94a
commit
6aa8de3a4b
5
app.go
5
app.go
@ -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:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
34
database.go
34
database.go
@ -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{}
|
||||||
|
|
||||||
|
1
go.mod
1
go.mod
@ -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
|
||||||
|
2
go.sum
2
go.sum
@ -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=
|
||||||
|
146
gopher.go
Normal file
146
gopher.go
Normal file
@ -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()
|
||||||
|
}
|
20
handle.go
20
handle.go
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user