mirror of
https://github.com/writefreely/writefreely
synced 2024-11-24 09:33:11 +00: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
7 changed files with 210 additions and 0 deletions
5
app.go
5
app.go
|
@ -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:"-"`
|
||||
}
|
||||
|
||||
|
|
34
database.go
34
database.go
|
@ -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{}
|
||||
|
||||
|
|
1
go.mod
1
go.mod
|
@ -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
|
||||
|
|
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/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=
|
||||
|
|
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"
|
||||
|
||||
"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)
|
||||
|
|
Loading…
Reference in a new issue