Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
8de77dcb83 | |||
f6f2dc57e8 | |||
d78814b9c9 | |||
445fffdf2b | |||
82b73f26b4 | |||
77bf2b9dd3 | |||
e67df627ce | |||
e2cfb2d70c | |||
7376947e8a | |||
c556f862e5 | |||
2fd5fccc5b | |||
86d5367d8e | |||
dee7c8eb98 | |||
87f53c9895 | |||
0943ff34b1 | |||
1821ef60d4 | |||
9b938ccff3 | |||
517a2ee86b |
32
blog.go
32
blog.go
|
@ -9,19 +9,6 @@ import (
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
"github.com/kirsle/blog/internal/controllers/admin"
|
|
||||||
"github.com/kirsle/blog/internal/controllers/authctl"
|
|
||||||
commentctl "github.com/kirsle/blog/internal/controllers/comments"
|
|
||||||
"github.com/kirsle/blog/internal/controllers/contact"
|
|
||||||
postctl "github.com/kirsle/blog/internal/controllers/posts"
|
|
||||||
"github.com/kirsle/blog/internal/controllers/setup"
|
|
||||||
"github.com/kirsle/blog/internal/log"
|
|
||||||
"github.com/kirsle/blog/internal/markdown"
|
|
||||||
"github.com/kirsle/blog/internal/middleware"
|
|
||||||
"github.com/kirsle/blog/internal/middleware/auth"
|
|
||||||
"github.com/kirsle/blog/internal/render"
|
|
||||||
"github.com/kirsle/blog/internal/responses"
|
|
||||||
"github.com/kirsle/blog/internal/sessions"
|
|
||||||
"github.com/kirsle/blog/jsondb"
|
"github.com/kirsle/blog/jsondb"
|
||||||
"github.com/kirsle/blog/jsondb/caches"
|
"github.com/kirsle/blog/jsondb/caches"
|
||||||
"github.com/kirsle/blog/jsondb/caches/null"
|
"github.com/kirsle/blog/jsondb/caches/null"
|
||||||
|
@ -30,6 +17,22 @@ import (
|
||||||
"github.com/kirsle/blog/models/posts"
|
"github.com/kirsle/blog/models/posts"
|
||||||
"github.com/kirsle/blog/models/settings"
|
"github.com/kirsle/blog/models/settings"
|
||||||
"github.com/kirsle/blog/models/users"
|
"github.com/kirsle/blog/models/users"
|
||||||
|
"github.com/kirsle/blog/src/controllers/admin"
|
||||||
|
"github.com/kirsle/blog/src/controllers/authctl"
|
||||||
|
commentctl "github.com/kirsle/blog/src/controllers/comments"
|
||||||
|
"github.com/kirsle/blog/src/controllers/contact"
|
||||||
|
postctl "github.com/kirsle/blog/src/controllers/posts"
|
||||||
|
questionsctl "github.com/kirsle/blog/src/controllers/questions"
|
||||||
|
"github.com/kirsle/blog/src/controllers/setup"
|
||||||
|
"github.com/kirsle/blog/src/log"
|
||||||
|
"github.com/kirsle/blog/src/markdown"
|
||||||
|
"github.com/kirsle/blog/src/middleware"
|
||||||
|
"github.com/kirsle/blog/src/middleware/auth"
|
||||||
|
"github.com/kirsle/blog/src/models"
|
||||||
|
"github.com/kirsle/blog/src/ratelimit"
|
||||||
|
"github.com/kirsle/blog/src/render"
|
||||||
|
"github.com/kirsle/blog/src/responses"
|
||||||
|
"github.com/kirsle/blog/src/sessions"
|
||||||
"github.com/shurcooL/github_flavored_markdown/gfmstyle"
|
"github.com/shurcooL/github_flavored_markdown/gfmstyle"
|
||||||
"github.com/urfave/negroni"
|
"github.com/urfave/negroni"
|
||||||
)
|
)
|
||||||
|
@ -104,6 +107,7 @@ func (b *Blog) Configure() {
|
||||||
posts.DB = b.jsonDB
|
posts.DB = b.jsonDB
|
||||||
users.DB = b.jsonDB
|
users.DB = b.jsonDB
|
||||||
comments.DB = b.jsonDB
|
comments.DB = b.jsonDB
|
||||||
|
models.UseDB(b.db)
|
||||||
|
|
||||||
// Redis cache?
|
// Redis cache?
|
||||||
if config.Redis.Enabled {
|
if config.Redis.Enabled {
|
||||||
|
@ -120,6 +124,7 @@ func (b *Blog) Configure() {
|
||||||
b.Cache = cache
|
b.Cache = cache
|
||||||
b.jsonDB.Cache = cache
|
b.jsonDB.Cache = cache
|
||||||
markdown.Cache = cache
|
markdown.Cache = cache
|
||||||
|
ratelimit.Cache = cache
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,6 +141,7 @@ func (b *Blog) SetupHTTP() {
|
||||||
contact.Register(r)
|
contact.Register(r)
|
||||||
postctl.Register(r, b.MustLogin)
|
postctl.Register(r, b.MustLogin)
|
||||||
commentctl.Register(r)
|
commentctl.Register(r)
|
||||||
|
questionsctl.Register(r, b.MustLogin)
|
||||||
|
|
||||||
// GitHub Flavored Markdown CSS.
|
// GitHub Flavored Markdown CSS.
|
||||||
r.Handle("/css/gfm.css", http.StripPrefix("/css", http.FileServer(gfmstyle.Assets)))
|
r.Handle("/css/gfm.css", http.StripPrefix("/css", http.FileServer(gfmstyle.Assets)))
|
||||||
|
|
|
@ -27,6 +27,7 @@ var (
|
||||||
var (
|
var (
|
||||||
fDebug bool
|
fDebug bool
|
||||||
fAddress string
|
fAddress string
|
||||||
|
fVersion bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -34,11 +35,17 @@ func init() {
|
||||||
flag.BoolVar(&fDebug, "d", false, "Debug mode (alias)")
|
flag.BoolVar(&fDebug, "d", false, "Debug mode (alias)")
|
||||||
flag.StringVar(&fAddress, "address", ":8000", "Bind address")
|
flag.StringVar(&fAddress, "address", ":8000", "Bind address")
|
||||||
flag.StringVar(&fAddress, "a", ":8000", "Bind address (alias)")
|
flag.StringVar(&fAddress, "a", ":8000", "Bind address (alias)")
|
||||||
|
flag.BoolVar(&fVersion, "v", false, "Print version info and quit")
|
||||||
rand.Seed(time.Now().UnixNano())
|
rand.Seed(time.Now().UnixNano())
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
if fVersion {
|
||||||
|
fmt.Printf("This is blog v%s build %s", Version, Build)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
userRoot := flag.Arg(0)
|
userRoot := flag.Arg(0)
|
||||||
if userRoot == "" {
|
if userRoot == "" {
|
||||||
fmt.Printf("Need user root\n")
|
fmt.Printf("Need user root\n")
|
||||||
|
|
|
@ -3,9 +3,9 @@ package blog
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/kirsle/blog/internal/log"
|
"github.com/kirsle/blog/src/log"
|
||||||
"github.com/kirsle/blog/internal/render"
|
"github.com/kirsle/blog/src/render"
|
||||||
"github.com/kirsle/blog/internal/responses"
|
"github.com/kirsle/blog/src/responses"
|
||||||
)
|
)
|
||||||
|
|
||||||
// registerErrors loads the error handlers into the responses subpackage.
|
// registerErrors loads the error handlers into the responses subpackage.
|
||||||
|
|
45
go.mod
Normal file
45
go.mod
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
module github.com/kirsle/blog
|
||||||
|
|
||||||
|
go 1.22
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/edwvee/exiffix v0.0.0-20180602190213-b57537c92a6b
|
||||||
|
github.com/garyburd/redigo v1.6.0
|
||||||
|
github.com/google/uuid v1.1.1
|
||||||
|
github.com/gorilla/feeds v1.1.1
|
||||||
|
github.com/gorilla/mux v1.7.2
|
||||||
|
github.com/gorilla/sessions v1.1.3
|
||||||
|
github.com/jinzhu/gorm v1.9.9
|
||||||
|
github.com/kirsle/golog v0.0.0-20180411020913-51290b4f9292
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.2
|
||||||
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||||
|
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470
|
||||||
|
github.com/urfave/negroni v1.0.0
|
||||||
|
golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443
|
||||||
|
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/disintegration/imaging v1.6.0 // indirect
|
||||||
|
github.com/gorilla/context v1.1.1 // indirect
|
||||||
|
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.10.0 // indirect
|
||||||
|
github.com/russross/blackfriday v1.5.2 // indirect
|
||||||
|
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect
|
||||||
|
github.com/sergi/go-diff v1.0.0 // indirect
|
||||||
|
github.com/shurcooL/go v0.0.0-20230706063926-5fe729b41b3a // indirect
|
||||||
|
github.com/shurcooL/go-goon v1.0.0 // indirect
|
||||||
|
github.com/shurcooL/highlight_diff v0.0.0-20181222201841-111da2e7d480 // indirect
|
||||||
|
github.com/shurcooL/highlight_go v0.0.0-20181215221002-9d8641ddf2e1 // indirect
|
||||||
|
github.com/shurcooL/octicon v0.0.0-20181222203144-9ff1a4cf27f4 // indirect
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||||
|
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d // indirect
|
||||||
|
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e // indirect
|
||||||
|
github.com/tomnomnom/xtermcolor v0.0.0-20160428124646-b78803f00a7e // indirect
|
||||||
|
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81 // indirect
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d // indirect
|
||||||
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||||
|
)
|
206
go.sum
Normal file
206
go.sum
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
cloud.google.com/go v0.37.4 h1:glPeL3BQJsbF6aIIYfZizMwc5LTYz250bDMjttbBGAU=
|
||||||
|
cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||||
|
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||||
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
|
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||||
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3 h1:tkum0XDgfR0jcVVXuTsYv/erY2NnEDqwRojbxR1rBYA=
|
||||||
|
github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM=
|
||||||
|
github.com/disintegration/imaging v1.6.0 h1:nVPXRUUQ36Z7MNf0O77UzgnOb1mkMMor7lmJMJXc/mA=
|
||||||
|
github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ=
|
||||||
|
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||||
|
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||||
|
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||||
|
github.com/edwvee/exiffix v0.0.0-20180602190213-b57537c92a6b h1:6CBzNasH8+bKeFwr5Bt5JtALHLFN4iQp7sf4ShlP/ik=
|
||||||
|
github.com/edwvee/exiffix v0.0.0-20180602190213-b57537c92a6b/go.mod h1:KoE3Ti1qbQXCb3s/XGj0yApHnbnNnn1bXTtB5Auq/Vc=
|
||||||
|
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
|
||||||
|
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc=
|
||||||
|
github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
|
||||||
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
|
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
|
||||||
|
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
|
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||||
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||||
|
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||||
|
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||||
|
github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
|
||||||
|
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
|
||||||
|
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
|
github.com/gorilla/mux v1.7.2 h1:zoNxOV7WjqXptQOVngLmcSQgXmgk4NMz1HibBchjl/I=
|
||||||
|
github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
|
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||||
|
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
|
github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU=
|
||||||
|
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/jinzhu/gorm v1.9.9 h1:Gc8bP20O+vroFUzZEXA1r7vNGQZGQ+RKgOnriuNF3ds=
|
||||||
|
github.com/jinzhu/gorm v1.9.9/go.mod h1:Kh6hTsSGffh4ui079FHrR5Gg+5D0hgihqDcsDN2BBJY=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
|
||||||
|
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||||
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
|
github.com/kirsle/golog v0.0.0-20180411020913-51290b4f9292 h1:Ihk5qKHfi9W/1B9A9GuxKgriCiieK03xp0XN6YYXOtw=
|
||||||
|
github.com/kirsle/golog v0.0.0-20180411020913-51290b4f9292/go.mod h1:0KaOvOX8s5YINMREeyTILsuU0wkmnKQQTy99e/2oDGc=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
|
||||||
|
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
|
||||||
|
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
|
||||||
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||||
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||||
|
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||||
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
|
github.com/pkg/errors v0.8.0/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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
|
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
|
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
|
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
||||||
|
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||||
|
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
|
||||||
|
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
||||||
|
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||||
|
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||||
|
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470 h1:qb9IthCFBmROJ6YBS31BEMeSYjOscSiG+EO+JVNTz64=
|
||||||
|
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
|
||||||
|
github.com/shurcooL/go v0.0.0-20230706063926-5fe729b41b3a h1:ZHfoO7ZJhws9NU1kzZhStUnnVQiPtDe1PzpUnc6HirU=
|
||||||
|
github.com/shurcooL/go v0.0.0-20230706063926-5fe729b41b3a/go.mod h1:DNrlr0AR9NsHD/aoc2pPeu4uSBZ/71yCHkR42yrzW3M=
|
||||||
|
github.com/shurcooL/go-goon v1.0.0 h1:BCQPvxGkHHJ4WpBO4m/9FXbITVIsvAm/T66cCcCGI7E=
|
||||||
|
github.com/shurcooL/go-goon v1.0.0/go.mod h1:2wTHMsGo7qnpmqA8ADYZtP4I1DD94JpXGQ3Dxq2YQ5w=
|
||||||
|
github.com/shurcooL/highlight_diff v0.0.0-20181222201841-111da2e7d480 h1:KaKXZldeYH73dpQL+Nr38j1r5BgpAYQjYvENOUpIZDQ=
|
||||||
|
github.com/shurcooL/highlight_diff v0.0.0-20181222201841-111da2e7d480/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
|
||||||
|
github.com/shurcooL/highlight_go v0.0.0-20181215221002-9d8641ddf2e1 h1:a6a6gGfBoO2ty+yyHNd7M6gkp37EwE3GIoycUnLo1Oo=
|
||||||
|
github.com/shurcooL/highlight_go v0.0.0-20181215221002-9d8641ddf2e1/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
|
||||||
|
github.com/shurcooL/octicon v0.0.0-20181222203144-9ff1a4cf27f4 h1:H0v7bJx9CDGHx402wE08Fk5AS2mWdTYK9JI5vyrx8jQ=
|
||||||
|
github.com/shurcooL/octicon v0.0.0-20181222203144-9ff1a4cf27f4/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
|
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d h1:yKm7XZV6j9Ev6lojP2XaIshpT4ymkqhMeSghO5Ps00E=
|
||||||
|
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
|
||||||
|
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e h1:qpG93cPwA5f7s/ZPBJnGOYQNK/vKsaDaseuKT5Asee8=
|
||||||
|
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/tomnomnom/xtermcolor v0.0.0-20160428124646-b78803f00a7e h1:Ee+VZw13r9NTOMnwTPs6O5KZ0MJU54hsxu9FpZ4pQ10=
|
||||||
|
github.com/tomnomnom/xtermcolor v0.0.0-20160428124646-b78803f00a7e/go.mod h1:fSIW/szJHsRts/4U8wlMPhs+YqJC+7NYR+Qqb1uJVpA=
|
||||||
|
github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc=
|
||||||
|
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
|
||||||
|
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||||
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443 h1:IcSOAf4PyMp3U3XbIEj1/xJ2BjNN2jWv7JoyOsMxXUU=
|
||||||
|
golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81 h1:00VmoueYNlNz/aHIilyyQz/MHSqGoWJzpFv/HW8xpzI=
|
||||||
|
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||||
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
|
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
|
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||||
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||||
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
|
||||||
|
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
@ -1,64 +0,0 @@
|
||||||
package postctl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/feeds"
|
|
||||||
"github.com/kirsle/blog/internal/responses"
|
|
||||||
"github.com/kirsle/blog/models/posts"
|
|
||||||
"github.com/kirsle/blog/models/settings"
|
|
||||||
"github.com/kirsle/blog/models/users"
|
|
||||||
)
|
|
||||||
|
|
||||||
func feedHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
config, _ := settings.Load()
|
|
||||||
admin, err := users.Load(1)
|
|
||||||
if err != nil {
|
|
||||||
responses.Error(w, r, "Blog isn't ready yet.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
feed := &feeds.Feed{
|
|
||||||
Title: config.Site.Title,
|
|
||||||
Link: &feeds.Link{Href: config.Site.URL},
|
|
||||||
Description: config.Site.Description,
|
|
||||||
Author: &feeds.Author{
|
|
||||||
Name: admin.Name,
|
|
||||||
Email: admin.Email,
|
|
||||||
},
|
|
||||||
Created: time.Now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
feed.Items = []*feeds.Item{}
|
|
||||||
for i, p := range RecentPosts(r, "", "") {
|
|
||||||
post, _ := posts.Load(p.ID)
|
|
||||||
var suffix string
|
|
||||||
if strings.Contains(post.Body, "<snip>") {
|
|
||||||
post.Body = strings.Split(post.Body, "<snip>")[0]
|
|
||||||
suffix = "..."
|
|
||||||
}
|
|
||||||
|
|
||||||
feed.Items = append(feed.Items, &feeds.Item{
|
|
||||||
Title: p.Title,
|
|
||||||
Link: &feeds.Link{Href: config.Site.URL + p.Fragment},
|
|
||||||
Description: post.Body + suffix,
|
|
||||||
Created: p.Created,
|
|
||||||
})
|
|
||||||
if i == 9 { // 10 -1
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// What format to encode it in?
|
|
||||||
if strings.Contains(r.URL.Path, ".atom") {
|
|
||||||
atom, _ := feed.ToAtom()
|
|
||||||
w.Header().Set("Content-Type", "application/atom+xml")
|
|
||||||
w.Write([]byte(atom))
|
|
||||||
} else {
|
|
||||||
rss, _ := feed.ToRss()
|
|
||||||
w.Header().Set("Content-Type", "application/rss+xml")
|
|
||||||
w.Write([]byte(rss))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,7 @@
|
||||||
package redis
|
package redis
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -44,6 +45,16 @@ func (r *Redis) Get(key string) ([]byte, error) {
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetJSON gets a JSON value from a Redis key.
|
||||||
|
func (r *Redis) GetJSON(key string, v any) error {
|
||||||
|
val, err := r.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Unmarshal(val, v)
|
||||||
|
}
|
||||||
|
|
||||||
// Set a key in Redis.
|
// Set a key in Redis.
|
||||||
func (r *Redis) Set(key string, v []byte, expires int) error {
|
func (r *Redis) Set(key string, v []byte, expires int) error {
|
||||||
conn := r.pool.Get()
|
conn := r.pool.Get()
|
||||||
|
@ -55,6 +66,16 @@ func (r *Redis) Set(key string, v []byte, expires int) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetJSON sets a JSON encoded value into a Redis key.
|
||||||
|
func (r *Redis) SetJSON(key string, v any, expires int) error {
|
||||||
|
bin, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Set(key, bin, expires)
|
||||||
|
}
|
||||||
|
|
||||||
// Delete keys from Redis.
|
// Delete keys from Redis.
|
||||||
func (r *Redis) Delete(key ...string) {
|
func (r *Redis) Delete(key ...string) {
|
||||||
conn := r.pool.Get()
|
conn := r.pool.Get()
|
||||||
|
|
|
@ -15,8 +15,9 @@ func UpdateIndex(p *Post) error {
|
||||||
|
|
||||||
// Index caches high level metadata about the blog's contents for fast access.
|
// Index caches high level metadata about the blog's contents for fast access.
|
||||||
type Index struct {
|
type Index struct {
|
||||||
Posts map[int]Post `json:"posts"`
|
Posts map[int]Post `json:"posts"`
|
||||||
Fragments map[string]int `json:"fragments"`
|
Fragments map[string]int `json:"fragments"`
|
||||||
|
Thumbnails map[int]string `json:"thumbnails"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetIndex loads the index, or rebuilds it first if it doesn't exist.
|
// GetIndex loads the index, or rebuilds it first if it doesn't exist.
|
||||||
|
@ -33,8 +34,9 @@ func GetIndex() (*Index, error) {
|
||||||
// RebuildIndex builds the index from scratch.
|
// RebuildIndex builds the index from scratch.
|
||||||
func RebuildIndex() (*Index, error) {
|
func RebuildIndex() (*Index, error) {
|
||||||
idx := &Index{
|
idx := &Index{
|
||||||
Posts: map[int]Post{},
|
Posts: map[int]Post{},
|
||||||
Fragments: map[string]int{},
|
Fragments: map[string]int{},
|
||||||
|
Thumbnails: map[int]string{},
|
||||||
}
|
}
|
||||||
entries, _ := DB.List("blog/posts")
|
entries, _ := DB.List("blog/posts")
|
||||||
for _, doc := range entries {
|
for _, doc := range entries {
|
||||||
|
@ -53,16 +55,24 @@ func RebuildIndex() (*Index, error) {
|
||||||
// Update a blog's entry in the index.
|
// Update a blog's entry in the index.
|
||||||
func (idx *Index) Update(p *Post) error {
|
func (idx *Index) Update(p *Post) error {
|
||||||
idx.Posts[p.ID] = Post{
|
idx.Posts[p.ID] = Post{
|
||||||
ID: p.ID,
|
ID: p.ID,
|
||||||
Title: p.Title,
|
Title: p.Title,
|
||||||
Fragment: p.Fragment,
|
Fragment: p.Fragment,
|
||||||
AuthorID: p.AuthorID,
|
AuthorID: p.AuthorID,
|
||||||
Privacy: p.Privacy,
|
Privacy: p.Privacy,
|
||||||
Tags: p.Tags,
|
Sticky: p.Sticky,
|
||||||
Created: p.Created,
|
EnableComments: p.EnableComments,
|
||||||
Updated: p.Updated,
|
Tags: p.Tags,
|
||||||
|
Created: p.Created,
|
||||||
|
Updated: p.Updated,
|
||||||
}
|
}
|
||||||
idx.Fragments[p.Fragment] = p.ID
|
idx.Fragments[p.Fragment] = p.ID
|
||||||
|
|
||||||
|
// Find a thumbnail image if possible.
|
||||||
|
if thumb, ok := p.ExtractThumbnail(); ok {
|
||||||
|
idx.Thumbnails[p.ID] = thumb
|
||||||
|
}
|
||||||
|
|
||||||
err := DB.Commit("blog/index", idx)
|
err := DB.Commit("blog/index", idx)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,12 @@ var DB *jsondb.DB
|
||||||
|
|
||||||
var log *golog.Logger
|
var log *golog.Logger
|
||||||
|
|
||||||
|
// Regexp used to parse a thumbnail image from a blog post. Looks for the first
|
||||||
|
// URI component ending with an image extension.
|
||||||
|
var (
|
||||||
|
ThumbnailImageRegexp = regexp.MustCompile(`['"(]([a-zA-Z0-9-_:/?.=&]+\.(?:jpe?g|png|gif))['")]`)
|
||||||
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
log = golog.GetLogger("blog")
|
log = golog.GetLogger("blog")
|
||||||
}
|
}
|
||||||
|
@ -27,7 +33,7 @@ type Post struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Fragment string `json:"fragment"`
|
Fragment string `json:"fragment"`
|
||||||
ContentType string `json:"contentType"`
|
ContentType string `json:"contentType,omitempty"`
|
||||||
AuthorID int `json:"author"`
|
AuthorID int `json:"author"`
|
||||||
Body string `json:"body,omitempty"`
|
Body string `json:"body,omitempty"`
|
||||||
Privacy string `json:"privacy"`
|
Privacy string `json:"privacy"`
|
||||||
|
@ -191,6 +197,16 @@ func (p *Post) Delete() error {
|
||||||
return idx.Delete(p)
|
return idx.Delete(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExtractThumbnail searches and returns a thumbnail image to represent the
|
||||||
|
// post. This will be the first image embedded in the post, or nothing.
|
||||||
|
func (p *Post) ExtractThumbnail() (string, bool) {
|
||||||
|
result := ThumbnailImageRegexp.FindStringSubmatch(p.Body)
|
||||||
|
if len(result) < 2 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return result[1], true
|
||||||
|
}
|
||||||
|
|
||||||
// getNextID gets the next blog post ID.
|
// getNextID gets the next blog post ID.
|
||||||
func (p *Post) nextID() int {
|
func (p *Post) nextID() int {
|
||||||
// Highest ID seen so far.
|
// Highest ID seen so far.
|
||||||
|
|
63
models/posts/thumbnail_test.go
Normal file
63
models/posts/thumbnail_test.go
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
package posts_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/kirsle/blog/models/posts"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestThumbnailRegexp(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
Text string
|
||||||
|
Expect string
|
||||||
|
ExpectFail bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var tests = []testCase{
|
||||||
|
{
|
||||||
|
Text: "Hello world",
|
||||||
|
ExpectFail: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "Some text.\n\n![An image](/static/photos/Image-1.jpg)\n" +
|
||||||
|
"![Another image](/static/photos/Image-2.jpg)",
|
||||||
|
Expect: "/static/photos/Image-1.jpg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: `<a href="/static/photos/12Abc456.jpg" target="_blank">` +
|
||||||
|
`<img src="/static/photos/34Xyz123.jpg"></a>`,
|
||||||
|
Expect: "/static/photos/12Abc456.jpg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: `A markdown image: ![With text](/test1.gif) and an HTML ` +
|
||||||
|
`image: <img src="/test2.png">`,
|
||||||
|
Expect: "/test1.gif",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: `<a href="https://google.com/"><img src="https://example.com/logo.gif?query=string.jpg"></a>`,
|
||||||
|
Expect: "https://example.com/logo.gif?query=string.jpg",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
p := &posts.Post{
|
||||||
|
Body: test.Text,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, ok := p.ExtractThumbnail()
|
||||||
|
if !ok && !test.ExpectFail {
|
||||||
|
t.Errorf("Text: %s\nExpected to fail, but did not!\nGot: %s",
|
||||||
|
test.Text,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if result != test.Expect {
|
||||||
|
t.Errorf("Text: %s\nExpect: %s\nGot: %s",
|
||||||
|
test.Text,
|
||||||
|
test.Expect,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
pages.go
10
pages.go
|
@ -6,11 +6,11 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/kirsle/blog/internal/controllers/posts"
|
"github.com/kirsle/blog/src/controllers/posts"
|
||||||
"github.com/kirsle/blog/internal/log"
|
"github.com/kirsle/blog/src/log"
|
||||||
"github.com/kirsle/blog/internal/markdown"
|
"github.com/kirsle/blog/src/markdown"
|
||||||
"github.com/kirsle/blog/internal/render"
|
"github.com/kirsle/blog/src/render"
|
||||||
"github.com/kirsle/blog/internal/responses"
|
"github.com/kirsle/blog/src/responses"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PageHandler is the catch-all route handler, for serving static web pages.
|
// PageHandler is the catch-all route handler, for serving static web pages.
|
||||||
|
|
39
root/.email/generic.gohtml
Normal file
39
root/.email/generic.gohtml
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="x-apple-disable-message-reformatting"><!-- Disable auto-scale in iOS 10 Mail -->
|
||||||
|
<title>{{ .Subject }}</title>
|
||||||
|
</head>
|
||||||
|
<body width="100%" bgcolor="#FFFFFF" color="#000000" style="margin: 0; mso-line-height-rule: exactly;">
|
||||||
|
|
||||||
|
<center>
|
||||||
|
<table width="90%" cellspacing="0" cellpadding="8" style="border: 1px solid #000000">
|
||||||
|
<tr>
|
||||||
|
<td align="left" valign="top" bgcolor="#C0C0C0">
|
||||||
|
<font face="Helvetica,Arial,Verdana-sans-serif" size="6" color="#000000">
|
||||||
|
<b>{{ .Subject }}</b>
|
||||||
|
</font>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="left" valign="top" bgcolor="#FEFEFE">
|
||||||
|
<font face="Helvetica,Arial,Verdana-sans-serif" size="3" color="#000000">
|
||||||
|
{{ .Data.Message }}
|
||||||
|
</font>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="left" valign="top" bgcolor="#C0C0C0">
|
||||||
|
<font face="Helvetica,Arial,Verdana-sans-serif" size="3" color="#000000">
|
||||||
|
This e-mail was automatically generated; do not reply to it.
|
||||||
|
</font>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</center>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,7 +1,9 @@
|
||||||
{{ define "title" }}{{ .Data.Title }}{{ end }}
|
{{ define "title" }}{{ .Data.Title }}{{ end }}
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
|
|
||||||
|
<div class="markdown">
|
||||||
{{ .Data.HTML }}
|
{{ .Data.HTML }}
|
||||||
|
</div>
|
||||||
|
|
||||||
{{ if and .CurrentUser.Admin .Editable }}
|
{{ if and .CurrentUser.Admin .Editable }}
|
||||||
<p class="mt-4">
|
<p class="mt-4">
|
||||||
|
|
|
@ -69,6 +69,10 @@
|
||||||
name="body"
|
name="body"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
required>{{ .Data.Body }}</textarea>
|
required>{{ .Data.Body }}</textarea>
|
||||||
|
|
||||||
|
<button id="ace-toggle-button" type="button" class="mt-2 btn btn-sm btn-secondary">
|
||||||
|
Toggle Rich Code Editor
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
@ -77,10 +81,15 @@
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<script src="/js/ace-toggle.js"></script>
|
||||||
<script src="/js/ace-editor/src-min-noconflict/ace.js" type="text/javascript" charset="utf-8"></script>
|
<script src="/js/ace-editor/src-min-noconflict/ace.js" type="text/javascript" charset="utf-8"></script>
|
||||||
<script>
|
<script>
|
||||||
var ACE;
|
var ACE;
|
||||||
(function() {
|
(function() {
|
||||||
|
if (DISABLE_ACE_EDITOR) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var editor = ace.edit("ace-editor");
|
var editor = ace.edit("ace-editor");
|
||||||
ACE = editor;
|
ACE = editor;
|
||||||
document.querySelector("#editor-box").style.display = "block";
|
document.querySelector("#editor-box").style.display = "block";
|
||||||
|
|
|
@ -3,22 +3,40 @@
|
||||||
|
|
||||||
<h1>Archive</h1>
|
<h1>Archive</h1>
|
||||||
|
|
||||||
|
{{ $thumbs := .Data.Thumbnails }}
|
||||||
|
|
||||||
{{ range .Data.Archive }}
|
{{ range .Data.Archive }}
|
||||||
<h3>{{ .Date.Format "January, 2006" }}</h3>
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>{{ .Date.Format "January, 2006" }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
<ul class="list-unstyled">
|
<div class="row">
|
||||||
{{ range .Posts }}
|
{{ range .Posts }}
|
||||||
<li class="list-item">
|
{{ $thumb := index $thumbs .ID }}
|
||||||
<a href="/{{ .Fragment }}">{{ .Title }}</a>
|
<div class="col-12 col-sm-6 col-md-4 col-lg-3 mb-4">
|
||||||
<small class="blog-meta">
|
<div class="card bg-secondary"
|
||||||
{{ .Created.Format "Jan 02 2006" }}
|
style="height: auto; min-height: 150px;
|
||||||
{{ if ne .Privacy "public" }}
|
{{ if $thumb }}background-image: url({{ $thumb }}); background-size: cover{{ end }}
|
||||||
<span class="blog-{{ .Privacy }}">[{{ .Privacy }}]</span>
|
"
|
||||||
|
title="Tags: {{ range .Tags }}#{{ . }} {{ end }}">
|
||||||
|
<span class="p-1" style="background-color: RGBA(255, 255, 255, 0.8)">
|
||||||
|
<a href="/{{ .Fragment }}">{{ .Title }}</a><br>
|
||||||
|
<small class="blog-meta">
|
||||||
|
{{ .Created.Format "Jan 02 2006" }}
|
||||||
|
{{ if ne .Privacy "public" }}
|
||||||
|
<span class="blog-{{ .Privacy }}">[{{ .Privacy }}]</span>
|
||||||
|
{{ end }}
|
||||||
|
</small>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</small>
|
</div>
|
||||||
</li>
|
|
||||||
{{ end }}
|
</div>
|
||||||
</ul>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{{ define "title" }}Update Blog{{ end }}
|
{{ define "title" }}Update Blog{{ end }}
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<form action="/blog/edit" method="POST">
|
<form name="blog-edit" action="/blog/edit" method="POST">
|
||||||
<input type="hidden" name="_csrf" value="{{ .CSRF }}">
|
<input type="hidden" name="_csrf" value="{{ .CSRF }}">
|
||||||
{{ if .Data.preview }}
|
{{ if .Data.preview }}
|
||||||
<div class="card mb-5">
|
<div class="card mb-5">
|
||||||
|
@ -97,6 +97,15 @@
|
||||||
name="body"
|
name="body"
|
||||||
id="body"
|
id="body"
|
||||||
placeholder="Post body goes here">{{ .Body }}</textarea>
|
placeholder="Post body goes here">{{ .Body }}</textarea>
|
||||||
|
|
||||||
|
<div class="mt-2">
|
||||||
|
<button id="ace-toggle-button" type="button" class="btn btn-sm btn-secondary">
|
||||||
|
Toggle Rich Code Editor
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<span class="ml-2">Attach a file:</span>
|
||||||
|
<input type="file" id="attach-file-button" onChange="uploadFile()">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -174,10 +183,15 @@
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<script src="/js/ace-toggle.js"></script>
|
||||||
<script src="/js/ace-editor/src-min-noconflict/ace.js" type="text/javascript" charset="utf-8"></script>
|
<script src="/js/ace-editor/src-min-noconflict/ace.js" type="text/javascript" charset="utf-8"></script>
|
||||||
<script>
|
<script>
|
||||||
var ACE;
|
var ACE;
|
||||||
(function() {
|
(function() {
|
||||||
|
if (DISABLE_ACE_EDITOR) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var editor = ace.edit("ace-editor");
|
var editor = ace.edit("ace-editor");
|
||||||
ACE = editor;
|
ACE = editor;
|
||||||
document.querySelector("#editor-box").style.display = "block";
|
document.querySelector("#editor-box").style.display = "block";
|
||||||
|
@ -198,6 +212,43 @@ var ACE;
|
||||||
setSyntax("markdown");
|
setSyntax("markdown");
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
function uploadFile() {
|
||||||
|
let $input = document.querySelector("#attach-file-button");
|
||||||
|
let syntax = document.querySelector("input[name='content-type']:checked").value;
|
||||||
|
let file = $input.files[0];
|
||||||
|
|
||||||
|
var data = new FormData();
|
||||||
|
data.append("file", file);
|
||||||
|
data.append("_csrf", "{{ .CSRF }}");
|
||||||
|
|
||||||
|
fetch("/admin/upload", {
|
||||||
|
method: "POST",
|
||||||
|
body: data,
|
||||||
|
credentials: "same-origin",
|
||||||
|
cache: "no-cache"
|
||||||
|
}).then(resp => resp.json()).then(resp => {
|
||||||
|
if (!resp.success) {
|
||||||
|
window.alert(resp.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let filename = resp.filename;
|
||||||
|
let uri = resp.uri;
|
||||||
|
let insert = `![${filename}](${uri})\n`;
|
||||||
|
if (syntax === "html") {
|
||||||
|
insert = `<img alt="${filename}" src="${uri}" class="portrait">\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DISABLE_ACE_EDITOR) {
|
||||||
|
document.querySelector("#body").value += insert;
|
||||||
|
} else {
|
||||||
|
ACE.insert(insert);
|
||||||
|
}
|
||||||
|
|
||||||
|
$input.value = "";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function setSyntax(lang) {
|
function setSyntax(lang) {
|
||||||
if (typeof(ACE) !== undefined) {
|
if (typeof(ACE) !== undefined) {
|
||||||
ACE.getSession().setMode("ace/mode/"+lang);
|
ACE.getSession().setMode("ace/mode/"+lang);
|
||||||
|
|
|
@ -16,6 +16,11 @@
|
||||||
{{ else if eq $p.Privacy "unlisted" }}
|
{{ else if eq $p.Privacy "unlisted" }}
|
||||||
<span class="blog-unlisted">[unlisted]</span>
|
<span class="blog-unlisted">[unlisted]</span>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
|
{{ if $p.Sticky }}
|
||||||
|
<span class="blog-sticky">[pinned]</span>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
<span title="{{ $p.Created.Format "Jan 2 2006 @ 15:04:05 MST" }}">
|
<span title="{{ $p.Created.Format "Jan 2 2006 @ 15:04:05 MST" }}">
|
||||||
{{ $p.Created.Format "January 2, 2006" }}
|
{{ $p.Created.Format "January 2, 2006" }}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -22,6 +22,9 @@ a.blog-title {
|
||||||
.blog-meta .blog-draft {
|
.blog-meta .blog-draft {
|
||||||
color: #909;
|
color: #909;
|
||||||
}
|
}
|
||||||
|
.blog-meta .blog-sticky {
|
||||||
|
color: #F0F;
|
||||||
|
}
|
||||||
|
|
||||||
/* Comment metadata line */
|
/* Comment metadata line */
|
||||||
.comment-meta {
|
.comment-meta {
|
||||||
|
|
34
root/js/ace-toggle.js
Normal file
34
root/js/ace-toggle.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
Reusable script to disable the ACE Code Editor at the touch of a button, i.e.
|
||||||
|
for mobile where the editor doesn't work very well.
|
||||||
|
|
||||||
|
Include this script at the bottom of the .gohtml page and have a button with
|
||||||
|
the ID "ace-toggle-button".
|
||||||
|
|
||||||
|
It sets the global window variable DISABLE_ACE_EDITOR=true if disabled.
|
||||||
|
*/
|
||||||
|
(function() {
|
||||||
|
let key = "ace-toggle.disabled";
|
||||||
|
let disabled = localStorage[key] ? true : false;
|
||||||
|
window.DISABLE_ACE_EDITOR = disabled;
|
||||||
|
|
||||||
|
let $button = document.querySelector("#ace-toggle-button");
|
||||||
|
if (disabled) {
|
||||||
|
$button.innerText = "Enable Rich Code Editor";
|
||||||
|
} else {
|
||||||
|
$button.innerText = "Disable Rich Code Editor";
|
||||||
|
}
|
||||||
|
|
||||||
|
$button.addEventListener("click", function() {
|
||||||
|
if (!window.confirm("Toggling the code editor will reload the page. Are you sure?")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disabled) {
|
||||||
|
delete localStorage[key];
|
||||||
|
} else {
|
||||||
|
localStorage[key] = true;
|
||||||
|
}
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
})();
|
107
root/questions.gohtml
Normal file
107
root/questions.gohtml
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
{{ define "title" }}Ask Me Anything{{ end }}
|
||||||
|
{{ define "content" }}
|
||||||
|
<h1>Ask Me Anything</h1>
|
||||||
|
|
||||||
|
{{ if .Data.Error }}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
Error: {{ .Data.Error }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<form name="askme" method="POST" action="/ask">
|
||||||
|
<input type="hidden" name="_csrf" value="{{ .CSRF }}">
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-12 col-md-2">
|
||||||
|
<label class="col-form-label" for="name">Name:</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-10">
|
||||||
|
<input type="text" class="form-control" id="name" name="name" value="{{ .Data.Q.Name }}" placeholder="Anonymous">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-12 col-md-2">
|
||||||
|
<label class="col-form-label" for="email">Email:</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-10">
|
||||||
|
<div><input type="email" class="form-control" id="email" name="email" value="{{ .Data.Q.Email }}" placeholder="name@example.com"></div>
|
||||||
|
<small>
|
||||||
|
Optional. You will receive a one-time e-mail when I answer your question and no spam.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-12" for="question">Question: <small>(required)</small></label>
|
||||||
|
<textarea cols="80" rows="6"
|
||||||
|
class="col-12 form-control"
|
||||||
|
name="question"
|
||||||
|
id="question"
|
||||||
|
placeholder="Ask me anything">{{ .Data.Q.Question }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col">
|
||||||
|
<button type="submit"
|
||||||
|
name="submit"
|
||||||
|
value="ask"
|
||||||
|
class="btn btn-primary">Ask away!</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ if .LoggedIn }}
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header">
|
||||||
|
Pending Questions
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{{ if not .Data.Pending }}
|
||||||
|
<em>There are no pending questions.</em>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ range .Data.Pending }}
|
||||||
|
<p>
|
||||||
|
<strong>{{ .Name }}</strong> {{ if .Email }}(with email){{ end }} asks:<br>
|
||||||
|
<small class="text-muted">
|
||||||
|
<em>{{ .Created.Format "January 2, 2006 @ 15:04 MST" }}</em> by
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{{ .Question }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div id="form-{{ .ID }}" class="dhtml-forms">
|
||||||
|
<form method="POST" action="/ask/answer">
|
||||||
|
<input type="hidden" name="_csrf" value="{{ $.CSRF }}">
|
||||||
|
<input type="hidden" name="id" value="{{ .ID }}">
|
||||||
|
<textarea cols="80" rows="4"
|
||||||
|
class="form-control"
|
||||||
|
name="answer"
|
||||||
|
placeholder="Answer (Markdown formatting allowed)"></textarea>
|
||||||
|
|
||||||
|
<div class="btn-group mt-3">
|
||||||
|
<button type="submit" name="submit" value="answer" class="btn btn-primary">
|
||||||
|
Answer
|
||||||
|
</button>
|
||||||
|
<button type="submit" name="submit" value="delete" class="btn btn-danger">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div id="button-{{ .ID }}" class="dhtml-buttons" style="display: none">
|
||||||
|
<button type="button" class="btn" id="show-{{ .ID }}" class="dhtml-show-button">Answer or delete</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ end }}
|
|
@ -4,8 +4,8 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/kirsle/blog/internal/middleware/auth"
|
"github.com/kirsle/blog/src/middleware/auth"
|
||||||
"github.com/kirsle/blog/internal/render"
|
"github.com/kirsle/blog/src/render"
|
||||||
"github.com/urfave/negroni"
|
"github.com/urfave/negroni"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ func Register(r *mux.Router, authErrorFunc http.HandlerFunc) {
|
||||||
adminRouter.HandleFunc("/", indexHandler)
|
adminRouter.HandleFunc("/", indexHandler)
|
||||||
adminRouter.HandleFunc("/settings", settingsHandler)
|
adminRouter.HandleFunc("/settings", settingsHandler)
|
||||||
adminRouter.HandleFunc("/editor", editorHandler)
|
adminRouter.HandleFunc("/editor", editorHandler)
|
||||||
|
adminRouter.HandleFunc("/upload", uploadHandler)
|
||||||
|
|
||||||
r.PathPrefix("/admin").Handler(negroni.New(
|
r.PathPrefix("/admin").Handler(negroni.New(
|
||||||
negroni.HandlerFunc(auth.LoginRequired(authErrorFunc)),
|
negroni.HandlerFunc(auth.LoginRequired(authErrorFunc)),
|
|
@ -8,8 +8,8 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/kirsle/blog/internal/render"
|
"github.com/kirsle/blog/src/render"
|
||||||
"github.com/kirsle/blog/internal/responses"
|
"github.com/kirsle/blog/src/responses"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FileTree holds information about files in the document roots.
|
// FileTree holds information about files in the document roots.
|
|
@ -4,9 +4,9 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/kirsle/blog/internal/forms"
|
"github.com/kirsle/blog/src/forms"
|
||||||
"github.com/kirsle/blog/internal/render"
|
"github.com/kirsle/blog/src/render"
|
||||||
"github.com/kirsle/blog/internal/responses"
|
"github.com/kirsle/blog/src/responses"
|
||||||
"github.com/kirsle/blog/models/settings"
|
"github.com/kirsle/blog/models/settings"
|
||||||
)
|
)
|
||||||
|
|
150
src/controllers/admin/upload.go
Normal file
150
src/controllers/admin/upload.go
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"image/gif"
|
||||||
|
"image/jpeg"
|
||||||
|
"image/png"
|
||||||
|
|
||||||
|
"github.com/edwvee/exiffix"
|
||||||
|
"github.com/kirsle/blog/src/log"
|
||||||
|
"github.com/kirsle/blog/src/render"
|
||||||
|
"github.com/kirsle/blog/src/responses"
|
||||||
|
"github.com/nfnt/resize"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: configurable max image width.
|
||||||
|
var (
|
||||||
|
MaxImageWidth = 1280
|
||||||
|
JpegQuality = 90
|
||||||
|
)
|
||||||
|
|
||||||
|
// processImage manhandles an image's binary data, scaling it down to <= 1280
|
||||||
|
// pixels and stripping off any metadata.
|
||||||
|
func processImage(input []byte, ext string) ([]byte, error) {
|
||||||
|
if ext == ".gif" {
|
||||||
|
return input, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
reader := bytes.NewReader(input)
|
||||||
|
|
||||||
|
// Decode the image using exiffix, which will auto-rotate jpeg images etc.
|
||||||
|
// based on their EXIF values.
|
||||||
|
origImage, _, err := exiffix.Decode(reader)
|
||||||
|
if err != nil {
|
||||||
|
return input, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the config to get the image width.
|
||||||
|
reader.Seek(0, io.SeekStart)
|
||||||
|
config, _, _ := image.DecodeConfig(reader)
|
||||||
|
width := config.Width
|
||||||
|
|
||||||
|
// If the width is too great, scale it down.
|
||||||
|
if width > MaxImageWidth {
|
||||||
|
width = MaxImageWidth
|
||||||
|
}
|
||||||
|
newImage := resize.Resize(uint(width), 0, origImage, resize.Lanczos3)
|
||||||
|
|
||||||
|
var output bytes.Buffer
|
||||||
|
switch ext {
|
||||||
|
case ".jpeg":
|
||||||
|
fallthrough
|
||||||
|
case ".jpg":
|
||||||
|
jpeg.Encode(&output, newImage, &jpeg.Options{
|
||||||
|
Quality: JpegQuality,
|
||||||
|
})
|
||||||
|
case ".png":
|
||||||
|
png.Encode(&output, newImage)
|
||||||
|
case ".gif":
|
||||||
|
gif.Encode(&output, newImage, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return output.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func uploadHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
type response struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
Filename string `json:"filename,omitempty"`
|
||||||
|
URI string `json:"uri,omitempty"`
|
||||||
|
Checksum string `json:"checksum,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the file from the form data.
|
||||||
|
file, header, err := r.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
responses.JSON(w, http.StatusBadRequest, response{
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Validate the extension is an image type.
|
||||||
|
ext := strings.ToLower(filepath.Ext(header.Filename))
|
||||||
|
if ext != ".jpg" && ext != ".jpeg" && ext != ".png" && ext != ".gif" {
|
||||||
|
responses.JSON(w, http.StatusBadRequest, response{
|
||||||
|
Error: "Invalid file type, only common image types are supported: jpg, png, gif",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the file.
|
||||||
|
io.Copy(&buf, file)
|
||||||
|
binary := buf.Bytes()
|
||||||
|
|
||||||
|
// Process and image and resize it down, strip metadata, etc.
|
||||||
|
binary, err = processImage(binary, ext)
|
||||||
|
if err != nil {
|
||||||
|
responses.JSON(w, http.StatusBadRequest, response{
|
||||||
|
Error: "Resize error: " + err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a checksum of it.
|
||||||
|
sha := sha256.New()
|
||||||
|
sha.Write(binary)
|
||||||
|
checksum := hex.EncodeToString(sha.Sum(nil))
|
||||||
|
|
||||||
|
log.Info("Uploaded file names: %s Checksum is: %s", header.Filename, checksum)
|
||||||
|
|
||||||
|
// Write to the /static/photos directory of the user root. Ensure the path
|
||||||
|
// exists or create it if not.
|
||||||
|
outputPath := filepath.Join(*render.UserRoot, "static", "photos")
|
||||||
|
if _, err := os.Stat(outputPath); os.IsNotExist(err) {
|
||||||
|
os.MkdirAll(outputPath, 0755)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the output file.
|
||||||
|
filename := filepath.Join(outputPath, checksum+ext)
|
||||||
|
outfh, err := os.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
responses.JSON(w, http.StatusBadRequest, response{
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer outfh.Close()
|
||||||
|
outfh.Write(binary)
|
||||||
|
|
||||||
|
responses.JSON(w, http.StatusOK, response{
|
||||||
|
Success: true,
|
||||||
|
Filename: header.Filename,
|
||||||
|
URI: fmt.Sprintf("/static/photos/%s%s", checksum, ext),
|
||||||
|
Checksum: checksum,
|
||||||
|
})
|
||||||
|
}
|
|
@ -3,10 +3,10 @@ package authctl
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/kirsle/blog/internal/log"
|
"github.com/kirsle/blog/src/log"
|
||||||
"github.com/kirsle/blog/internal/render"
|
"github.com/kirsle/blog/src/render"
|
||||||
"github.com/kirsle/blog/internal/responses"
|
"github.com/kirsle/blog/src/responses"
|
||||||
"github.com/kirsle/blog/internal/sessions"
|
"github.com/kirsle/blog/src/sessions"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AgeGate handles age verification for NSFW blogs.
|
// AgeGate handles age verification for NSFW blogs.
|
|
@ -5,13 +5,14 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/kirsle/blog/internal/forms"
|
|
||||||
"github.com/kirsle/blog/internal/log"
|
|
||||||
"github.com/kirsle/blog/internal/middleware/auth"
|
|
||||||
"github.com/kirsle/blog/models/users"
|
"github.com/kirsle/blog/models/users"
|
||||||
"github.com/kirsle/blog/internal/render"
|
"github.com/kirsle/blog/src/forms"
|
||||||
"github.com/kirsle/blog/internal/responses"
|
"github.com/kirsle/blog/src/log"
|
||||||
"github.com/kirsle/blog/internal/sessions"
|
"github.com/kirsle/blog/src/middleware/auth"
|
||||||
|
"github.com/kirsle/blog/src/ratelimit"
|
||||||
|
"github.com/kirsle/blog/src/render"
|
||||||
|
"github.com/kirsle/blog/src/responses"
|
||||||
|
"github.com/kirsle/blog/src/sessions"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Register the initial setup routes.
|
// Register the initial setup routes.
|
||||||
|
@ -44,6 +45,20 @@ func loginHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
vars["Error"] = err
|
vars["Error"] = err
|
||||||
} else {
|
} else {
|
||||||
|
// Rate limit by guessed username.
|
||||||
|
limiter := &ratelimit.Limiter{
|
||||||
|
Namespace: "login",
|
||||||
|
ID: form.Username,
|
||||||
|
Limit: 10,
|
||||||
|
Window: 3600,
|
||||||
|
CooldownAt: 3,
|
||||||
|
Cooldown: 10,
|
||||||
|
}
|
||||||
|
if err := limiter.Ping(); err != nil {
|
||||||
|
responses.FlashAndReload(w, r, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Test the login.
|
// Test the login.
|
||||||
user, err := users.CheckAuth(form.Username, form.Password)
|
user, err := users.CheckAuth(form.Username, form.Password)
|
||||||
if err != nil {
|
if err != nil {
|
|
@ -8,15 +8,15 @@ import (
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/kirsle/blog/internal/log"
|
"github.com/kirsle/blog/src/log"
|
||||||
"github.com/kirsle/blog/internal/mail"
|
"github.com/kirsle/blog/src/mail"
|
||||||
"github.com/kirsle/blog/internal/markdown"
|
"github.com/kirsle/blog/src/markdown"
|
||||||
"github.com/kirsle/blog/internal/middleware/auth"
|
"github.com/kirsle/blog/src/middleware/auth"
|
||||||
"github.com/kirsle/blog/models/comments"
|
"github.com/kirsle/blog/models/comments"
|
||||||
"github.com/kirsle/blog/models/users"
|
"github.com/kirsle/blog/models/users"
|
||||||
"github.com/kirsle/blog/internal/render"
|
"github.com/kirsle/blog/src/render"
|
||||||
"github.com/kirsle/blog/internal/responses"
|
"github.com/kirsle/blog/src/responses"
|
||||||
"github.com/kirsle/blog/internal/sessions"
|
"github.com/kirsle/blog/src/sessions"
|
||||||
)
|
)
|
||||||
|
|
||||||
var badRequest func(http.ResponseWriter, *http.Request, string)
|
var badRequest func(http.ResponseWriter, *http.Request, string)
|
|
@ -5,8 +5,8 @@ import (
|
||||||
"net/mail"
|
"net/mail"
|
||||||
|
|
||||||
"github.com/kirsle/blog/models/comments"
|
"github.com/kirsle/blog/models/comments"
|
||||||
"github.com/kirsle/blog/internal/render"
|
"github.com/kirsle/blog/src/render"
|
||||||
"github.com/kirsle/blog/internal/responses"
|
"github.com/kirsle/blog/src/responses"
|
||||||
)
|
)
|
||||||
|
|
||||||
func subscriptionHandler(w http.ResponseWriter, r *http.Request) {
|
func subscriptionHandler(w http.ResponseWriter, r *http.Request) {
|
|
@ -9,12 +9,12 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/kirsle/blog/internal/forms"
|
"github.com/kirsle/blog/src/forms"
|
||||||
"github.com/kirsle/blog/internal/log"
|
"github.com/kirsle/blog/src/log"
|
||||||
"github.com/kirsle/blog/internal/mail"
|
"github.com/kirsle/blog/src/mail"
|
||||||
"github.com/kirsle/blog/internal/markdown"
|
"github.com/kirsle/blog/src/markdown"
|
||||||
"github.com/kirsle/blog/internal/render"
|
"github.com/kirsle/blog/src/render"
|
||||||
"github.com/kirsle/blog/internal/responses"
|
"github.com/kirsle/blog/src/responses"
|
||||||
"github.com/kirsle/blog/models/settings"
|
"github.com/kirsle/blog/models/settings"
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,9 +7,9 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/kirsle/blog/internal/log"
|
"github.com/kirsle/blog/src/log"
|
||||||
"github.com/kirsle/blog/internal/responses"
|
"github.com/kirsle/blog/src/responses"
|
||||||
"github.com/kirsle/blog/internal/sessions"
|
"github.com/kirsle/blog/src/sessions"
|
||||||
"github.com/kirsle/blog/models/contacts"
|
"github.com/kirsle/blog/models/contacts"
|
||||||
"github.com/kirsle/blog/models/events"
|
"github.com/kirsle/blog/models/events"
|
||||||
)
|
)
|
|
@ -5,9 +5,9 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/kirsle/blog/internal/markdown"
|
"github.com/kirsle/blog/src/markdown"
|
||||||
"github.com/kirsle/blog/internal/render"
|
"github.com/kirsle/blog/src/render"
|
||||||
"github.com/kirsle/blog/internal/responses"
|
"github.com/kirsle/blog/src/responses"
|
||||||
"github.com/kirsle/blog/models/events"
|
"github.com/kirsle/blog/models/events"
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,10 +6,10 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/kirsle/blog/internal/log"
|
"github.com/kirsle/blog/src/log"
|
||||||
"github.com/kirsle/blog/internal/middleware/auth"
|
"github.com/kirsle/blog/src/middleware/auth"
|
||||||
"github.com/kirsle/blog/internal/render"
|
"github.com/kirsle/blog/src/render"
|
||||||
"github.com/kirsle/blog/internal/responses"
|
"github.com/kirsle/blog/src/responses"
|
||||||
"github.com/kirsle/blog/models/comments"
|
"github.com/kirsle/blog/models/comments"
|
||||||
"github.com/kirsle/blog/models/events"
|
"github.com/kirsle/blog/models/events"
|
||||||
"github.com/urfave/negroni"
|
"github.com/urfave/negroni"
|
|
@ -6,9 +6,9 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/kirsle/blog/internal/log"
|
"github.com/kirsle/blog/src/log"
|
||||||
"github.com/kirsle/blog/internal/render"
|
"github.com/kirsle/blog/src/render"
|
||||||
"github.com/kirsle/blog/internal/responses"
|
"github.com/kirsle/blog/src/responses"
|
||||||
"github.com/kirsle/blog/models/contacts"
|
"github.com/kirsle/blog/models/contacts"
|
||||||
"github.com/kirsle/blog/models/events"
|
"github.com/kirsle/blog/models/events"
|
||||||
)
|
)
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/kirsle/blog/internal/mail"
|
"github.com/kirsle/blog/src/mail"
|
||||||
"github.com/kirsle/blog/models/events"
|
"github.com/kirsle/blog/models/events"
|
||||||
"github.com/kirsle/blog/models/settings"
|
"github.com/kirsle/blog/models/settings"
|
||||||
)
|
)
|
|
@ -5,11 +5,11 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/kirsle/blog/internal/middleware/auth"
|
|
||||||
"github.com/kirsle/blog/models/posts"
|
"github.com/kirsle/blog/models/posts"
|
||||||
"github.com/kirsle/blog/internal/render"
|
"github.com/kirsle/blog/src/middleware/auth"
|
||||||
"github.com/kirsle/blog/internal/responses"
|
"github.com/kirsle/blog/src/render"
|
||||||
"github.com/kirsle/blog/internal/types"
|
"github.com/kirsle/blog/src/responses"
|
||||||
|
"github.com/kirsle/blog/src/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// archiveHandler summarizes all blog entries in an archive view.
|
// archiveHandler summarizes all blog entries in an archive view.
|
||||||
|
@ -54,7 +54,8 @@ func archiveHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
v := map[string]interface{}{
|
v := map[string]interface{}{
|
||||||
"Archive": result,
|
"Archive": result,
|
||||||
|
"Thumbnails": idx.Thumbnails,
|
||||||
}
|
}
|
||||||
render.Template(w, r, "blog/archive", v)
|
render.Template(w, r, "blog/archive", v)
|
||||||
}
|
}
|
|
@ -7,11 +7,11 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/kirsle/blog/internal/markdown"
|
"github.com/kirsle/blog/src/markdown"
|
||||||
"github.com/kirsle/blog/internal/middleware/auth"
|
"github.com/kirsle/blog/src/middleware/auth"
|
||||||
"github.com/kirsle/blog/internal/render"
|
"github.com/kirsle/blog/src/render"
|
||||||
"github.com/kirsle/blog/internal/responses"
|
"github.com/kirsle/blog/src/responses"
|
||||||
"github.com/kirsle/blog/internal/types"
|
"github.com/kirsle/blog/src/types"
|
||||||
"github.com/kirsle/blog/models/posts"
|
"github.com/kirsle/blog/models/posts"
|
||||||
)
|
)
|
||||||
|
|
98
src/controllers/posts/feeds.go
Normal file
98
src/controllers/posts/feeds.go
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
package postctl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/feeds"
|
||||||
|
"github.com/kirsle/blog/models/posts"
|
||||||
|
"github.com/kirsle/blog/models/settings"
|
||||||
|
"github.com/kirsle/blog/models/users"
|
||||||
|
"github.com/kirsle/blog/src/markdown"
|
||||||
|
"github.com/kirsle/blog/src/responses"
|
||||||
|
"github.com/kirsle/blog/src/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Feed configuration. TODO make configurable.
|
||||||
|
var (
|
||||||
|
FeedPostsPerPage = 20
|
||||||
|
|
||||||
|
reRelativeLink = regexp.MustCompile(` (src|href|poster)=(['"])/([^'"]+)['"]`)
|
||||||
|
)
|
||||||
|
|
||||||
|
func feedHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
config, _ := settings.Load()
|
||||||
|
admin, err := users.Load(1)
|
||||||
|
if err != nil {
|
||||||
|
responses.Error(w, r, "Blog isn't ready yet.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
feed := &feeds.Feed{
|
||||||
|
Title: config.Site.Title,
|
||||||
|
Link: &feeds.Link{Href: config.Site.URL},
|
||||||
|
Description: config.Site.Description,
|
||||||
|
Author: &feeds.Author{
|
||||||
|
Name: admin.Name,
|
||||||
|
Email: admin.Email,
|
||||||
|
},
|
||||||
|
Created: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
feed.Items = []*feeds.Item{}
|
||||||
|
for i, p := range RecentPosts(r, "", "") {
|
||||||
|
post, _ := posts.Load(p.ID)
|
||||||
|
|
||||||
|
// Render the post to HTML.
|
||||||
|
var rendered string
|
||||||
|
if post.ContentType == string(types.MARKDOWN) {
|
||||||
|
rendered = markdown.RenderTrustedMarkdown(post.Body)
|
||||||
|
} else {
|
||||||
|
rendered = post.Body
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make relative links absolute.
|
||||||
|
matches := reRelativeLink.FindAllStringSubmatch(rendered, -1)
|
||||||
|
for _, match := range matches {
|
||||||
|
var (
|
||||||
|
attr = match[1]
|
||||||
|
quote = match[2]
|
||||||
|
uri = match[3]
|
||||||
|
absURI = config.Site.URL + "/" + uri
|
||||||
|
new = fmt.Sprintf(" %s%s%s%s",
|
||||||
|
attr, quote, absURI, quote,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
rendered = strings.Replace(rendered, match[0], new, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
feed.Items = append(feed.Items, &feeds.Item{
|
||||||
|
Id: fmt.Sprintf("%d", p.ID),
|
||||||
|
Title: p.Title,
|
||||||
|
Link: &feeds.Link{Href: config.Site.URL + "/" + p.Fragment},
|
||||||
|
Description: rendered,
|
||||||
|
Created: p.Created,
|
||||||
|
})
|
||||||
|
if i == FeedPostsPerPage-1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// What format to encode it in?
|
||||||
|
if strings.Contains(r.URL.Path, ".atom") {
|
||||||
|
atom, _ := feed.ToAtom()
|
||||||
|
w.Header().Set("Content-Type", "application/atom+xml; encoding=utf-8")
|
||||||
|
w.Write([]byte(atom))
|
||||||
|
} else if strings.Contains(r.URL.Path, ".json") {
|
||||||
|
jsonData, _ := feed.ToJSON()
|
||||||
|
w.Header().Set("Content-Type", "application/json; encoding=utf-8")
|
||||||
|
w.Write([]byte(jsonData))
|
||||||
|
} else {
|
||||||
|
rss, _ := feed.ToRss()
|
||||||
|
w.Header().Set("Content-Type", "application/rss+xml; encoding=utf-8")
|
||||||
|
w.Write([]byte(rss))
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,9 +8,9 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/kirsle/blog/internal/log"
|
"github.com/kirsle/blog/src/log"
|
||||||
"github.com/kirsle/blog/internal/render"
|
"github.com/kirsle/blog/src/render"
|
||||||
"github.com/kirsle/blog/internal/types"
|
"github.com/kirsle/blog/src/types"
|
||||||
"github.com/kirsle/blog/models/comments"
|
"github.com/kirsle/blog/models/comments"
|
||||||
"github.com/kirsle/blog/models/posts"
|
"github.com/kirsle/blog/models/posts"
|
||||||
"github.com/kirsle/blog/models/users"
|
"github.com/kirsle/blog/models/users"
|
|
@ -10,14 +10,14 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/kirsle/blog/internal/log"
|
|
||||||
"github.com/kirsle/blog/internal/markdown"
|
|
||||||
"github.com/kirsle/blog/internal/middleware/auth"
|
|
||||||
"github.com/kirsle/blog/models/posts"
|
"github.com/kirsle/blog/models/posts"
|
||||||
"github.com/kirsle/blog/models/users"
|
"github.com/kirsle/blog/models/users"
|
||||||
"github.com/kirsle/blog/internal/render"
|
"github.com/kirsle/blog/src/log"
|
||||||
"github.com/kirsle/blog/internal/responses"
|
"github.com/kirsle/blog/src/markdown"
|
||||||
"github.com/kirsle/blog/internal/types"
|
"github.com/kirsle/blog/src/middleware/auth"
|
||||||
|
"github.com/kirsle/blog/src/render"
|
||||||
|
"github.com/kirsle/blog/src/responses"
|
||||||
|
"github.com/kirsle/blog/src/types"
|
||||||
"github.com/urfave/negroni"
|
"github.com/urfave/negroni"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -48,6 +48,7 @@ func Register(r *mux.Router, loginError http.HandlerFunc) {
|
||||||
r.HandleFunc("/blog", indexHandler)
|
r.HandleFunc("/blog", indexHandler)
|
||||||
r.HandleFunc("/blog.rss", feedHandler)
|
r.HandleFunc("/blog.rss", feedHandler)
|
||||||
r.HandleFunc("/blog.atom", feedHandler)
|
r.HandleFunc("/blog.atom", feedHandler)
|
||||||
|
r.HandleFunc("/blog.json", feedHandler)
|
||||||
r.HandleFunc("/archive", archiveHandler)
|
r.HandleFunc("/archive", archiveHandler)
|
||||||
r.HandleFunc("/tagged", taggedHandler)
|
r.HandleFunc("/tagged", taggedHandler)
|
||||||
r.HandleFunc("/tagged/{tag}", taggedHandler)
|
r.HandleFunc("/tagged/{tag}", taggedHandler)
|
||||||
|
@ -91,6 +92,7 @@ func RecentPosts(r *http.Request, tag, privacy string) []posts.Post {
|
||||||
|
|
||||||
// The set of blog posts to show.
|
// The set of blog posts to show.
|
||||||
var pool []posts.Post
|
var pool []posts.Post
|
||||||
|
var sticky []posts.Post // sticky pinned posts on top
|
||||||
for _, post := range idx.Posts {
|
for _, post := range idx.Posts {
|
||||||
// Limiting by a specific privacy setting? (drafts or private only)
|
// Limiting by a specific privacy setting? (drafts or private only)
|
||||||
if privacy != "" {
|
if privacy != "" {
|
||||||
|
@ -130,10 +132,20 @@ func RecentPosts(r *http.Request, tag, privacy string) []posts.Post {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pool = append(pool, post)
|
// Group them by sticky vs. not
|
||||||
|
if post.Sticky {
|
||||||
|
sticky = append(sticky, post)
|
||||||
|
} else {
|
||||||
|
pool = append(pool, post)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Sort(sort.Reverse(posts.ByUpdated(pool)))
|
sort.Sort(sort.Reverse(posts.ByUpdated(pool)))
|
||||||
|
if len(sticky) > 0 {
|
||||||
|
sort.Sort(sort.Reverse(posts.ByUpdated(sticky)))
|
||||||
|
pool = append(sticky, pool...)
|
||||||
|
}
|
||||||
|
|
||||||
return pool
|
return pool
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/kirsle/blog/models/posts"
|
"github.com/kirsle/blog/models/posts"
|
||||||
"github.com/kirsle/blog/internal/render"
|
"github.com/kirsle/blog/src/render"
|
||||||
)
|
)
|
||||||
|
|
||||||
// tagged lets you browse blog posts by category.
|
// tagged lets you browse blog posts by category.
|
226
src/controllers/questions/questions.go
Normal file
226
src/controllers/questions/questions.go
Normal file
|
@ -0,0 +1,226 @@
|
||||||
|
package questions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/kirsle/blog/models/posts"
|
||||||
|
"github.com/kirsle/blog/models/settings"
|
||||||
|
"github.com/kirsle/blog/src/log"
|
||||||
|
"github.com/kirsle/blog/src/mail"
|
||||||
|
"github.com/kirsle/blog/src/markdown"
|
||||||
|
"github.com/kirsle/blog/src/middleware/auth"
|
||||||
|
"github.com/kirsle/blog/src/models"
|
||||||
|
"github.com/kirsle/blog/src/render"
|
||||||
|
"github.com/kirsle/blog/src/responses"
|
||||||
|
"github.com/kirsle/blog/src/sessions"
|
||||||
|
"github.com/urfave/negroni"
|
||||||
|
)
|
||||||
|
|
||||||
|
var badRequest func(http.ResponseWriter, *http.Request, string)
|
||||||
|
|
||||||
|
// Register the comment routes to the app.
|
||||||
|
func Register(r *mux.Router, loginError http.HandlerFunc) {
|
||||||
|
badRequest = responses.BadRequest
|
||||||
|
|
||||||
|
r.HandleFunc("/ask", questionsHandler)
|
||||||
|
r.Handle("/ask/answer",
|
||||||
|
negroni.New(
|
||||||
|
negroni.HandlerFunc(auth.LoginRequired(loginError)),
|
||||||
|
negroni.WrapFunc(answerHandler),
|
||||||
|
),
|
||||||
|
).Methods(http.MethodPost)
|
||||||
|
}
|
||||||
|
|
||||||
|
func questionsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Share their name and email with the commenting system.
|
||||||
|
session := sessions.Get(r)
|
||||||
|
name, _ := session.Values["c.name"].(string)
|
||||||
|
email, _ := session.Values["c.email"].(string)
|
||||||
|
|
||||||
|
Q := models.NewQuestion()
|
||||||
|
Q.Name = name
|
||||||
|
Q.Email = email
|
||||||
|
|
||||||
|
cfg, err := settings.Load()
|
||||||
|
if err != nil {
|
||||||
|
responses.Error(w, r, "Error loading site configuration!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
v := map[string]interface{}{}
|
||||||
|
|
||||||
|
// Previewing, deleting, or posting?
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
Q.ParseForm(r)
|
||||||
|
log.Info("Q: %+v", Q)
|
||||||
|
|
||||||
|
if err := Q.Validate(); err != nil {
|
||||||
|
log.Debug("Validation error on question form: %s", err.Error())
|
||||||
|
v["Error"] = err
|
||||||
|
} else {
|
||||||
|
// Cache their name and email in their session.
|
||||||
|
session.Values["c.name"] = Q.Name
|
||||||
|
session.Values["c.email"] = Q.Email
|
||||||
|
session.Save(r, w)
|
||||||
|
|
||||||
|
// Append their comment.
|
||||||
|
err := Q.Save()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error saving new question: %s", err.Error())
|
||||||
|
responses.FlashAndRedirect(w, r, "/ask", "Error saving question: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email the site admin.
|
||||||
|
subject := fmt.Sprintf("Ask Me Anything (%s) from %s", cfg.Site.Title, Q.Name)
|
||||||
|
log.Info("Emailing site admin about this question")
|
||||||
|
go mail.SendEmail(mail.Email{
|
||||||
|
To: cfg.Site.AdminEmail,
|
||||||
|
Admin: true,
|
||||||
|
ReplyTo: Q.Email,
|
||||||
|
Subject: subject,
|
||||||
|
Template: ".email/generic.gohtml",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"Subject": subject,
|
||||||
|
"Message": template.HTML(
|
||||||
|
markdown.RenderMarkdown(
|
||||||
|
Q.Question +
|
||||||
|
"\n\nAnswer this at " + strings.Trim(cfg.Site.URL, "/") + "/ask",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Log it to disk, too.
|
||||||
|
fh, err := os.OpenFile(filepath.Join(*render.UserRoot, ".questions.log"), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
responses.Flash(w, r, "Error logging the message to disk: %s", err)
|
||||||
|
} else {
|
||||||
|
fh.WriteString(fmt.Sprintf(
|
||||||
|
"Date: %s\nName: %s\nEmail: %s\n\n%s\n\n--------------------\n\n",
|
||||||
|
time.Now().Format(time.UnixDate),
|
||||||
|
Q.Name,
|
||||||
|
Q.Email,
|
||||||
|
Q.Question,
|
||||||
|
))
|
||||||
|
fh.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Recorded question from %s: %s", Q.Name, Q.Question)
|
||||||
|
responses.FlashAndRedirect(w, r, "/ask", "Your question has been recorded!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v["Q"] = Q
|
||||||
|
|
||||||
|
// Load the pending questions.
|
||||||
|
pending, err := models.PendingQuestions(0, 20)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err.Error())
|
||||||
|
}
|
||||||
|
v["Pending"] = pending
|
||||||
|
|
||||||
|
render.Template(w, r, "questions.gohtml", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func answerHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
submit := r.FormValue("submit")
|
||||||
|
|
||||||
|
cfg, err := settings.Load()
|
||||||
|
if err != nil {
|
||||||
|
responses.Error(w, r, "Error loading site configuration!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type answerForm struct {
|
||||||
|
ID int
|
||||||
|
Answer string
|
||||||
|
Submit string
|
||||||
|
}
|
||||||
|
id, _ := strconv.Atoi(r.FormValue("id"))
|
||||||
|
form := answerForm{
|
||||||
|
ID: id,
|
||||||
|
Answer: r.FormValue("answer"),
|
||||||
|
Submit: r.FormValue("submit"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up the question.
|
||||||
|
Q, err := models.GetQuestion(form.ID)
|
||||||
|
if err != nil {
|
||||||
|
responses.FlashAndRedirect(w, r, "/ask",
|
||||||
|
fmt.Sprintf("Did not find question ID %d", form.ID),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch submit {
|
||||||
|
case "answer":
|
||||||
|
// Prepare a Markdown-themed blog post and go to the Preview page for it.
|
||||||
|
blog := posts.New()
|
||||||
|
blog.Title = "Ask"
|
||||||
|
blog.Tags = []string{"ask"}
|
||||||
|
blog.Fragment = fmt.Sprintf("ask-%s",
|
||||||
|
time.Now().Format("20060102150405"),
|
||||||
|
)
|
||||||
|
blog.Body = fmt.Sprintf(
|
||||||
|
"> **%s** asks:\n>\n> %s\n\n"+
|
||||||
|
"%s\n",
|
||||||
|
Q.Name,
|
||||||
|
strings.Replace(Q.Question, "\n", "> \n", 0),
|
||||||
|
form.Answer,
|
||||||
|
)
|
||||||
|
|
||||||
|
Q.Status = models.Answered
|
||||||
|
Q.Save()
|
||||||
|
|
||||||
|
// TODO: email the person who asked about the new URL.
|
||||||
|
if Q.Email != "" {
|
||||||
|
log.Info("Notifying user %s by email that the question is answered", Q.Email)
|
||||||
|
go mail.SendEmail(mail.Email{
|
||||||
|
To: Q.Email,
|
||||||
|
Subject: "Your question has been answered",
|
||||||
|
Template: ".email/generic.gohtml",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"Subject": "Your question has been answered",
|
||||||
|
"Message": template.HTML(
|
||||||
|
markdown.RenderMarkdown(
|
||||||
|
fmt.Sprintf(
|
||||||
|
"Hello, %s\n\n"+
|
||||||
|
"Your recent question on %s has been answered. To "+
|
||||||
|
"view the answer, please visit the following link:\n\n"+
|
||||||
|
"%s/%s",
|
||||||
|
Q.Name,
|
||||||
|
cfg.Site.Title,
|
||||||
|
cfg.Site.URL,
|
||||||
|
blog.Fragment,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render.Template(w, r, "blog/edit", map[string]interface{}{
|
||||||
|
"preview": template.HTML(markdown.RenderTrustedMarkdown(blog.Body)),
|
||||||
|
"post": blog,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
case "delete":
|
||||||
|
Q.Status = models.Deleted
|
||||||
|
Q.Save()
|
||||||
|
responses.FlashAndRedirect(w, r, "/ask", "Question deleted.")
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
responses.FlashAndRedirect(w, r, "/ask", "Unknown submit action.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
16
src/controllers/questions/questions_doc.go
Normal file
16
src/controllers/questions/questions_doc.go
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
Package questions implements the "Ask Me Anything" feature.
|
||||||
|
|
||||||
|
Routes
|
||||||
|
|
||||||
|
/ask Ask Me Anything
|
||||||
|
|
||||||
|
Related Models
|
||||||
|
|
||||||
|
questions
|
||||||
|
|
||||||
|
Description
|
||||||
|
|
||||||
|
Pending description.
|
||||||
|
*/
|
||||||
|
package questions
|
|
@ -4,14 +4,14 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/kirsle/blog/internal/forms"
|
"github.com/kirsle/blog/src/forms"
|
||||||
"github.com/kirsle/blog/internal/log"
|
"github.com/kirsle/blog/src/log"
|
||||||
"github.com/kirsle/blog/internal/middleware/auth"
|
"github.com/kirsle/blog/src/middleware/auth"
|
||||||
"github.com/kirsle/blog/models/settings"
|
"github.com/kirsle/blog/models/settings"
|
||||||
"github.com/kirsle/blog/models/users"
|
"github.com/kirsle/blog/models/users"
|
||||||
"github.com/kirsle/blog/internal/render"
|
"github.com/kirsle/blog/src/render"
|
||||||
"github.com/kirsle/blog/internal/responses"
|
"github.com/kirsle/blog/src/responses"
|
||||||
"github.com/kirsle/blog/internal/sessions"
|
"github.com/kirsle/blog/src/sessions"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Register the initial setup routes.
|
// Register the initial setup routes.
|
|
@ -8,9 +8,9 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/kirsle/blog/internal/log"
|
"github.com/kirsle/blog/src/log"
|
||||||
"github.com/kirsle/blog/internal/markdown"
|
"github.com/kirsle/blog/src/markdown"
|
||||||
"github.com/kirsle/blog/internal/render"
|
"github.com/kirsle/blog/src/render"
|
||||||
"github.com/kirsle/blog/models/comments"
|
"github.com/kirsle/blog/models/comments"
|
||||||
"github.com/kirsle/blog/models/settings"
|
"github.com/kirsle/blog/models/settings"
|
||||||
"github.com/microcosm-cc/bluemonday"
|
"github.com/microcosm-cc/bluemonday"
|
|
@ -11,7 +11,7 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/kirsle/blog/internal/log"
|
"github.com/kirsle/blog/src/log"
|
||||||
"github.com/kirsle/blog/jsondb/caches"
|
"github.com/kirsle/blog/jsondb/caches"
|
||||||
"github.com/microcosm-cc/bluemonday"
|
"github.com/microcosm-cc/bluemonday"
|
||||||
"github.com/shurcooL/github_flavored_markdown"
|
"github.com/shurcooL/github_flavored_markdown"
|
|
@ -4,13 +4,16 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/kirsle/blog/internal/responses"
|
|
||||||
"github.com/kirsle/blog/internal/sessions"
|
|
||||||
"github.com/kirsle/blog/models/settings"
|
"github.com/kirsle/blog/models/settings"
|
||||||
|
"github.com/kirsle/blog/src/responses"
|
||||||
|
"github.com/kirsle/blog/src/sessions"
|
||||||
"github.com/urfave/negroni"
|
"github.com/urfave/negroni"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ageGateSuffixes = []string{
|
var ageGateSuffixes = []string{
|
||||||
|
"/blog.rss", // Allow public access to RSS and Atom feeds.
|
||||||
|
"/blog.atom",
|
||||||
|
"/blog.json",
|
||||||
".js",
|
".js",
|
||||||
".css",
|
".css",
|
||||||
".txt",
|
".txt",
|
||||||
|
@ -19,6 +22,13 @@ var ageGateSuffixes = []string{
|
||||||
".jpg",
|
".jpg",
|
||||||
".jpeg",
|
".jpeg",
|
||||||
".gif",
|
".gif",
|
||||||
|
".mp4",
|
||||||
|
".webm",
|
||||||
|
".ttf",
|
||||||
|
".eot",
|
||||||
|
".svg",
|
||||||
|
".woff",
|
||||||
|
".woff2",
|
||||||
}
|
}
|
||||||
|
|
||||||
// AgeGate is a middleware generator that does age verification for NSFW sites.
|
// AgeGate is a middleware generator that does age verification for NSFW sites.
|
||||||
|
@ -37,19 +47,28 @@ func AgeGate(verifyHandler func(http.ResponseWriter, *http.Request)) negroni.Han
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow static files and things through.
|
// Allow static files and things through.
|
||||||
for _, prefix := range ageGateSuffixes {
|
for _, suffix := range ageGateSuffixes {
|
||||||
if strings.HasSuffix(path, prefix) {
|
if strings.HasSuffix(path, suffix) {
|
||||||
next(w, r)
|
next(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST requests are allowed.
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
next(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// See if they've been cleared.
|
// See if they've been cleared.
|
||||||
session := sessions.Get(r)
|
session := sessions.Get(r)
|
||||||
if val, _ := session.Values["age-ok"].(bool); !val {
|
if val, _ := session.Values["age-ok"].(bool); !val {
|
||||||
// They haven't been verified.
|
// They haven't been verified.
|
||||||
responses.Redirect(w, "/age-verify?next="+r.URL.Path)
|
// Allow single-page loads with ?over18=1 in query parameter.
|
||||||
return
|
if r.FormValue("over18") == "" {
|
||||||
|
responses.Redirect(w, "/age-verify?next="+r.URL.Path)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
next(w, r)
|
next(w, r)
|
|
@ -6,8 +6,8 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/kirsle/blog/models/users"
|
"github.com/kirsle/blog/models/users"
|
||||||
"github.com/kirsle/blog/internal/sessions"
|
"github.com/kirsle/blog/src/sessions"
|
||||||
"github.com/kirsle/blog/internal/types"
|
"github.com/kirsle/blog/src/types"
|
||||||
"github.com/urfave/negroni"
|
"github.com/urfave/negroni"
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,8 +5,8 @@ import (
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
gorilla "github.com/gorilla/sessions"
|
gorilla "github.com/gorilla/sessions"
|
||||||
"github.com/kirsle/blog/internal/log"
|
"github.com/kirsle/blog/src/log"
|
||||||
"github.com/kirsle/blog/internal/sessions"
|
"github.com/kirsle/blog/src/sessions"
|
||||||
"github.com/urfave/negroni"
|
"github.com/urfave/negroni"
|
||||||
)
|
)
|
||||||
|
|
21
src/models/models.go
Normal file
21
src/models/models.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jinzhu/gorm"
|
||||||
|
"github.com/kirsle/golog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DB is a reference to the parent app's gorm DB.
|
||||||
|
var DB *gorm.DB
|
||||||
|
|
||||||
|
// UseDB registers the DB from the root app.
|
||||||
|
func UseDB(db *gorm.DB) {
|
||||||
|
DB = db
|
||||||
|
DB.AutoMigrate(&Question{})
|
||||||
|
}
|
||||||
|
|
||||||
|
var log *golog.Logger
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log = golog.GetLogger("blog")
|
||||||
|
}
|
125
src/models/questions.go
Normal file
125
src/models/questions.go
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/mail"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Question is a question asked of the blog owner.
|
||||||
|
type Question struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Question string `json:"question"`
|
||||||
|
Status Status `json:"status"`
|
||||||
|
Created time.Time `json:"created"`
|
||||||
|
Updated time.Time `json:"updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewQuestion creates a blank Question with sensible defaults.
|
||||||
|
func NewQuestion() *Question {
|
||||||
|
return &Question{
|
||||||
|
Status: Pending,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetQuestion by its ID.
|
||||||
|
func GetQuestion(id int) (*Question, error) {
|
||||||
|
result := &Question{}
|
||||||
|
err := DB.First(&result, id).Error
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllQuestions returns all the Questions.
|
||||||
|
func AllQuestions() ([]*Question, error) {
|
||||||
|
result := []*Question{}
|
||||||
|
err := DB.Order("created desc").Find(&result).Error
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// PendingQuestions returns pending questions in order of recency.
|
||||||
|
func PendingQuestions(offset, limit int) ([]*Question, error) {
|
||||||
|
result := []*Question{}
|
||||||
|
err := DB.Where("status = ?", Pending).
|
||||||
|
Offset(offset).Limit(limit).
|
||||||
|
Order("created desc").
|
||||||
|
Find(&result).Error
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseForm populates the Question from form values.
|
||||||
|
func (ev *Question) ParseForm(r *http.Request) {
|
||||||
|
id, _ := strconv.Atoi(r.FormValue("id"))
|
||||||
|
|
||||||
|
ev.ID = id
|
||||||
|
ev.Name = r.FormValue("name")
|
||||||
|
ev.Email = r.FormValue("email")
|
||||||
|
ev.Question = r.FormValue("question")
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseDateTime parses separate date + time fields into a single time.Time.
|
||||||
|
func parseDateTime(r *http.Request, dateField, timeField string) (time.Time, error) {
|
||||||
|
dateValue := r.FormValue(dateField)
|
||||||
|
timeValue := r.FormValue(timeField)
|
||||||
|
|
||||||
|
if dateValue != "" && timeValue != "" {
|
||||||
|
datetime, err := time.Parse("2006-01-02 15:04", dateValue+" "+timeValue)
|
||||||
|
return datetime, err
|
||||||
|
} else if dateValue != "" {
|
||||||
|
datetime, err := time.Parse("2006-01-02", dateValue)
|
||||||
|
return datetime, err
|
||||||
|
} else {
|
||||||
|
return time.Time{}, errors.New("no date/times given")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate makes sure the required fields are all present.
|
||||||
|
func (ev *Question) Validate() error {
|
||||||
|
if ev.Question == "" {
|
||||||
|
return errors.New("question is required")
|
||||||
|
}
|
||||||
|
if ev.Email != "" {
|
||||||
|
if _, err := mail.ParseAddress(ev.Email); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load an Question by its ID.
|
||||||
|
func Load(id int) (*Question, error) {
|
||||||
|
ev := &Question{}
|
||||||
|
err := DB.First(ev, id).Error
|
||||||
|
return ev, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the Question.
|
||||||
|
func (ev *Question) Save() error {
|
||||||
|
if ev.Name == "" {
|
||||||
|
ev.Name = "Anonymous"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dates & times.
|
||||||
|
if ev.Created.IsZero() {
|
||||||
|
ev.Created = time.Now().UTC()
|
||||||
|
}
|
||||||
|
if ev.Updated.IsZero() {
|
||||||
|
ev.Updated = ev.Created
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the Question.
|
||||||
|
return DB.Save(&ev).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete an Question.
|
||||||
|
func (ev *Question) Delete() error {
|
||||||
|
if ev.ID == 0 {
|
||||||
|
return errors.New("Question has no ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the DB files.
|
||||||
|
return DB.Delete(ev).Error
|
||||||
|
}
|
11
src/models/questions_types.go
Normal file
11
src/models/questions_types.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
// Status of a Question.
|
||||||
|
type Status string
|
||||||
|
|
||||||
|
// Status options.
|
||||||
|
const (
|
||||||
|
Pending = "pending"
|
||||||
|
Answered = "answered"
|
||||||
|
Deleted = "deleted"
|
||||||
|
)
|
108
src/ratelimit/ratelimit.go
Normal file
108
src/ratelimit/ratelimit.go
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
package ratelimit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kirsle/blog/jsondb/caches/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Limiter implements a Redis-backed rate limit for logins or otherwise.
|
||||||
|
type Limiter struct {
|
||||||
|
Namespace string // kind of rate limiter ("login")
|
||||||
|
ID interface{} // unique ID of the resource being pinged (str or ints)
|
||||||
|
Limit int // how many pings within the window period
|
||||||
|
Window int // the window period/expiration of Redis key
|
||||||
|
CooldownAt int // how many pings before the cooldown is enforced
|
||||||
|
Cooldown int // time to wait between fails
|
||||||
|
}
|
||||||
|
|
||||||
|
// The active Redis cache given by the webapp.
|
||||||
|
var Cache *redis.Redis
|
||||||
|
|
||||||
|
// Redis object behind the rate limiter.
|
||||||
|
type Data struct {
|
||||||
|
Pings int
|
||||||
|
NotBefore time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ping the rate limiter.
|
||||||
|
func (l *Limiter) Ping() error {
|
||||||
|
if Cache == nil {
|
||||||
|
return errors.New("redis not ready")
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
key = l.Key()
|
||||||
|
now = time.Now()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get stored data from Redis if any.
|
||||||
|
var data Data
|
||||||
|
Cache.GetJSON(key, &data)
|
||||||
|
|
||||||
|
// Are we cooling down?
|
||||||
|
if now.Before(data.NotBefore) {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"You are doing that too often.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment the ping count.
|
||||||
|
data.Pings++
|
||||||
|
|
||||||
|
// Have we hit the wall?
|
||||||
|
if data.Pings >= l.Limit {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"You have hit the rate limit; come back later.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Are we throttled?
|
||||||
|
if l.CooldownAt > 0 && data.Pings > l.CooldownAt {
|
||||||
|
data.NotBefore = now.Add(time.Duration(l.Cooldown) * time.Second)
|
||||||
|
if err := Cache.SetJSON(key, data, l.Window); err != nil {
|
||||||
|
return fmt.Errorf("Couldn't set Redis key for rate limiter: %s", err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf(
|
||||||
|
"Please wait %ds before trying again. You have %d more attempt(s) remaining before you will be locked "+
|
||||||
|
"out for %ds.",
|
||||||
|
l.Cooldown,
|
||||||
|
l.Limit-data.Pings,
|
||||||
|
l.Window,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save their ping count to Redis.
|
||||||
|
if err := Cache.SetJSON(key, data, l.Window); err != nil {
|
||||||
|
return fmt.Errorf("Couldn't set Redis key for rate limiter: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the rate limiter, cleaning up the Redis key (e.g., after successful login).
|
||||||
|
func (l *Limiter) Clear() {
|
||||||
|
Cache.Delete(l.Key())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key formats the Redis key.
|
||||||
|
func (l *Limiter) Key() string {
|
||||||
|
var str string
|
||||||
|
switch t := l.ID.(type) {
|
||||||
|
case int:
|
||||||
|
str = fmt.Sprintf("%d", t)
|
||||||
|
case uint64:
|
||||||
|
str = fmt.Sprintf("%d", t)
|
||||||
|
case int64:
|
||||||
|
str = fmt.Sprintf("%d", t)
|
||||||
|
case uint32:
|
||||||
|
str = fmt.Sprintf("%d", t)
|
||||||
|
case int32:
|
||||||
|
str = fmt.Sprintf("%d", t)
|
||||||
|
default:
|
||||||
|
str = fmt.Sprintf("%s", t)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("rlimit/%s/%s", l.Namespace, str)
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/kirsle/blog/internal/markdown"
|
"github.com/kirsle/blog/src/markdown"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Funcs is a global funcmap that the blog can hook its internal
|
// Funcs is a global funcmap that the blog can hook its internal
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/kirsle/blog/internal/log"
|
"github.com/kirsle/blog/src/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Blog configuration bindings.
|
// Blog configuration bindings.
|
|
@ -8,11 +8,11 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
gorilla "github.com/gorilla/sessions"
|
gorilla "github.com/gorilla/sessions"
|
||||||
"github.com/kirsle/blog/internal/log"
|
"github.com/kirsle/blog/src/log"
|
||||||
"github.com/kirsle/blog/internal/middleware"
|
"github.com/kirsle/blog/src/middleware"
|
||||||
"github.com/kirsle/blog/internal/middleware/auth"
|
"github.com/kirsle/blog/src/middleware/auth"
|
||||||
"github.com/kirsle/blog/internal/sessions"
|
"github.com/kirsle/blog/src/sessions"
|
||||||
"github.com/kirsle/blog/internal/types"
|
"github.com/kirsle/blog/src/types"
|
||||||
"github.com/kirsle/blog/models/settings"
|
"github.com/kirsle/blog/models/settings"
|
||||||
"github.com/kirsle/blog/models/users"
|
"github.com/kirsle/blog/models/users"
|
||||||
)
|
)
|
|
@ -1,10 +1,11 @@
|
||||||
package responses
|
package responses
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/kirsle/blog/internal/sessions"
|
"github.com/kirsle/blog/src/sessions"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error handlers to be filled in by the blog app.
|
// Error handlers to be filled in by the blog app.
|
||||||
|
@ -39,3 +40,15 @@ func Redirect(w http.ResponseWriter, location string) {
|
||||||
w.Header().Set("Location", location)
|
w.Header().Set("Location", location)
|
||||||
w.WriteHeader(http.StatusFound)
|
w.WriteHeader(http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JSON serializes a JSON response to the browser.
|
||||||
|
func JSON(w http.ResponseWriter, statusCode int, v interface{}) {
|
||||||
|
w.Header().Set("Content-Type", "application/json; encoding=utf-8")
|
||||||
|
w.WriteHeader(statusCode)
|
||||||
|
|
||||||
|
serial, err := json.MarshalIndent(v, "", "\t")
|
||||||
|
if err != nil {
|
||||||
|
serial, _ = json.Marshal(err.Error())
|
||||||
|
}
|
||||||
|
w.Write(serial)
|
||||||
|
}
|
|
@ -6,8 +6,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
"github.com/kirsle/blog/internal/log"
|
"github.com/kirsle/blog/src/log"
|
||||||
"github.com/kirsle/blog/internal/types"
|
"github.com/kirsle/blog/src/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Store holds your cookie store information.
|
// Store holds your cookie store information.
|
Loading…
Reference in New Issue
Block a user