Compare commits

..

No commits in common. "master" and "events" have entirely different histories.

70 changed files with 209 additions and 1603 deletions

32
blog.go
View File

@ -9,6 +9,19 @@ import (
"github.com/gorilla/mux"
"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/caches"
"github.com/kirsle/blog/jsondb/caches/null"
@ -17,22 +30,6 @@ import (
"github.com/kirsle/blog/models/posts"
"github.com/kirsle/blog/models/settings"
"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/urfave/negroni"
)
@ -107,7 +104,6 @@ func (b *Blog) Configure() {
posts.DB = b.jsonDB
users.DB = b.jsonDB
comments.DB = b.jsonDB
models.UseDB(b.db)
// Redis cache?
if config.Redis.Enabled {
@ -124,7 +120,6 @@ func (b *Blog) Configure() {
b.Cache = cache
b.jsonDB.Cache = cache
markdown.Cache = cache
ratelimit.Cache = cache
}
}
@ -141,7 +136,6 @@ func (b *Blog) SetupHTTP() {
contact.Register(r)
postctl.Register(r, b.MustLogin)
commentctl.Register(r)
questionsctl.Register(r, b.MustLogin)
// GitHub Flavored Markdown CSS.
r.Handle("/css/gfm.css", http.StripPrefix("/css", http.FileServer(gfmstyle.Assets)))

View File

@ -27,7 +27,6 @@ var (
var (
fDebug bool
fAddress string
fVersion bool
)
func init() {
@ -35,17 +34,11 @@ func init() {
flag.BoolVar(&fDebug, "d", false, "Debug mode (alias)")
flag.StringVar(&fAddress, "address", ":8000", "Bind address")
flag.StringVar(&fAddress, "a", ":8000", "Bind address (alias)")
flag.BoolVar(&fVersion, "v", false, "Print version info and quit")
rand.Seed(time.Now().UnixNano())
}
func main() {
flag.Parse()
if fVersion {
fmt.Printf("This is blog v%s build %s", Version, Build)
return
}
userRoot := flag.Arg(0)
if userRoot == "" {
fmt.Printf("Need user root\n")

View File

@ -3,9 +3,9 @@ package blog
import (
"net/http"
"github.com/kirsle/blog/src/log"
"github.com/kirsle/blog/src/render"
"github.com/kirsle/blog/src/responses"
"github.com/kirsle/blog/internal/log"
"github.com/kirsle/blog/internal/render"
"github.com/kirsle/blog/internal/responses"
)
// registerErrors loads the error handlers into the responses subpackage.

45
go.mod
View File

@ -1,45 +0,0 @@
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
View File

@ -1,206 +0,0 @@
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=

View File

@ -4,8 +4,8 @@ import (
"net/http"
"github.com/gorilla/mux"
"github.com/kirsle/blog/src/middleware/auth"
"github.com/kirsle/blog/src/render"
"github.com/kirsle/blog/internal/middleware/auth"
"github.com/kirsle/blog/internal/render"
"github.com/urfave/negroni"
)
@ -15,7 +15,6 @@ func Register(r *mux.Router, authErrorFunc http.HandlerFunc) {
adminRouter.HandleFunc("/", indexHandler)
adminRouter.HandleFunc("/settings", settingsHandler)
adminRouter.HandleFunc("/editor", editorHandler)
adminRouter.HandleFunc("/upload", uploadHandler)
r.PathPrefix("/admin").Handler(negroni.New(
negroni.HandlerFunc(auth.LoginRequired(authErrorFunc)),

View File

@ -8,8 +8,8 @@ import (
"path/filepath"
"strings"
"github.com/kirsle/blog/src/render"
"github.com/kirsle/blog/src/responses"
"github.com/kirsle/blog/internal/render"
"github.com/kirsle/blog/internal/responses"
)
// FileTree holds information about files in the document roots.

View File

@ -4,9 +4,9 @@ import (
"net/http"
"strconv"
"github.com/kirsle/blog/src/forms"
"github.com/kirsle/blog/src/render"
"github.com/kirsle/blog/src/responses"
"github.com/kirsle/blog/internal/forms"
"github.com/kirsle/blog/internal/render"
"github.com/kirsle/blog/internal/responses"
"github.com/kirsle/blog/models/settings"
)

View File

@ -3,10 +3,10 @@ package authctl
import (
"net/http"
"github.com/kirsle/blog/src/log"
"github.com/kirsle/blog/src/render"
"github.com/kirsle/blog/src/responses"
"github.com/kirsle/blog/src/sessions"
"github.com/kirsle/blog/internal/log"
"github.com/kirsle/blog/internal/render"
"github.com/kirsle/blog/internal/responses"
"github.com/kirsle/blog/internal/sessions"
)
// AgeGate handles age verification for NSFW blogs.

View File

@ -5,14 +5,13 @@ import (
"net/http"
"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/src/forms"
"github.com/kirsle/blog/src/log"
"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"
"github.com/kirsle/blog/internal/render"
"github.com/kirsle/blog/internal/responses"
"github.com/kirsle/blog/internal/sessions"
)
// Register the initial setup routes.
@ -45,20 +44,6 @@ func loginHandler(w http.ResponseWriter, r *http.Request) {
if err != nil {
vars["Error"] = err
} 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.
user, err := users.CheckAuth(form.Username, form.Password)
if err != nil {

View File

@ -8,15 +8,15 @@ import (
"github.com/google/uuid"
"github.com/gorilla/mux"
"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/internal/log"
"github.com/kirsle/blog/internal/mail"
"github.com/kirsle/blog/internal/markdown"
"github.com/kirsle/blog/internal/middleware/auth"
"github.com/kirsle/blog/models/comments"
"github.com/kirsle/blog/models/users"
"github.com/kirsle/blog/src/render"
"github.com/kirsle/blog/src/responses"
"github.com/kirsle/blog/src/sessions"
"github.com/kirsle/blog/internal/render"
"github.com/kirsle/blog/internal/responses"
"github.com/kirsle/blog/internal/sessions"
)
var badRequest func(http.ResponseWriter, *http.Request, string)

View File

@ -5,8 +5,8 @@ import (
"net/mail"
"github.com/kirsle/blog/models/comments"
"github.com/kirsle/blog/src/render"
"github.com/kirsle/blog/src/responses"
"github.com/kirsle/blog/internal/render"
"github.com/kirsle/blog/internal/responses"
)
func subscriptionHandler(w http.ResponseWriter, r *http.Request) {

View File

@ -9,12 +9,12 @@ import (
"time"
"github.com/gorilla/mux"
"github.com/kirsle/blog/src/forms"
"github.com/kirsle/blog/src/log"
"github.com/kirsle/blog/src/mail"
"github.com/kirsle/blog/src/markdown"
"github.com/kirsle/blog/src/render"
"github.com/kirsle/blog/src/responses"
"github.com/kirsle/blog/internal/forms"
"github.com/kirsle/blog/internal/log"
"github.com/kirsle/blog/internal/mail"
"github.com/kirsle/blog/internal/markdown"
"github.com/kirsle/blog/internal/render"
"github.com/kirsle/blog/internal/responses"
"github.com/kirsle/blog/models/settings"
)

View File

@ -7,9 +7,9 @@ import (
"strings"
"github.com/gorilla/mux"
"github.com/kirsle/blog/src/log"
"github.com/kirsle/blog/src/responses"
"github.com/kirsle/blog/src/sessions"
"github.com/kirsle/blog/internal/log"
"github.com/kirsle/blog/internal/responses"
"github.com/kirsle/blog/internal/sessions"
"github.com/kirsle/blog/models/contacts"
"github.com/kirsle/blog/models/events"
)

View File

@ -5,9 +5,9 @@ import (
"net/http"
"strconv"
"github.com/kirsle/blog/src/markdown"
"github.com/kirsle/blog/src/render"
"github.com/kirsle/blog/src/responses"
"github.com/kirsle/blog/internal/markdown"
"github.com/kirsle/blog/internal/render"
"github.com/kirsle/blog/internal/responses"
"github.com/kirsle/blog/models/events"
)

View File

@ -6,10 +6,10 @@ import (
"sort"
"github.com/gorilla/mux"
"github.com/kirsle/blog/src/log"
"github.com/kirsle/blog/src/middleware/auth"
"github.com/kirsle/blog/src/render"
"github.com/kirsle/blog/src/responses"
"github.com/kirsle/blog/internal/log"
"github.com/kirsle/blog/internal/middleware/auth"
"github.com/kirsle/blog/internal/render"
"github.com/kirsle/blog/internal/responses"
"github.com/kirsle/blog/models/comments"
"github.com/kirsle/blog/models/events"
"github.com/urfave/negroni"

View File

@ -6,9 +6,9 @@ import (
"strings"
"github.com/gorilla/mux"
"github.com/kirsle/blog/src/log"
"github.com/kirsle/blog/src/render"
"github.com/kirsle/blog/src/responses"
"github.com/kirsle/blog/internal/log"
"github.com/kirsle/blog/internal/render"
"github.com/kirsle/blog/internal/responses"
"github.com/kirsle/blog/models/contacts"
"github.com/kirsle/blog/models/events"
)

View File

@ -4,7 +4,7 @@ import (
"fmt"
"strings"
"github.com/kirsle/blog/src/mail"
"github.com/kirsle/blog/internal/mail"
"github.com/kirsle/blog/models/events"
"github.com/kirsle/blog/models/settings"
)

View File

@ -5,11 +5,11 @@ import (
"sort"
"time"
"github.com/kirsle/blog/internal/middleware/auth"
"github.com/kirsle/blog/models/posts"
"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/kirsle/blog/internal/render"
"github.com/kirsle/blog/internal/responses"
"github.com/kirsle/blog/internal/types"
)
// archiveHandler summarizes all blog entries in an archive view.
@ -54,8 +54,7 @@ func archiveHandler(w http.ResponseWriter, r *http.Request) {
}
v := map[string]interface{}{
"Archive": result,
"Thumbnails": idx.Thumbnails,
"Archive": result,
}
render.Template(w, r, "blog/archive", v)
}

View File

@ -7,11 +7,11 @@ import (
"strconv"
"time"
"github.com/kirsle/blog/src/markdown"
"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/kirsle/blog/internal/markdown"
"github.com/kirsle/blog/internal/middleware/auth"
"github.com/kirsle/blog/internal/render"
"github.com/kirsle/blog/internal/responses"
"github.com/kirsle/blog/internal/types"
"github.com/kirsle/blog/models/posts"
)

View File

@ -0,0 +1,64 @@
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))
}
}

View File

@ -8,9 +8,9 @@ import (
"net/http"
"strconv"
"github.com/kirsle/blog/src/log"
"github.com/kirsle/blog/src/render"
"github.com/kirsle/blog/src/types"
"github.com/kirsle/blog/internal/log"
"github.com/kirsle/blog/internal/render"
"github.com/kirsle/blog/internal/types"
"github.com/kirsle/blog/models/comments"
"github.com/kirsle/blog/models/posts"
"github.com/kirsle/blog/models/users"

View File

@ -10,14 +10,14 @@ import (
"time"
"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/users"
"github.com/kirsle/blog/src/log"
"github.com/kirsle/blog/src/markdown"
"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/kirsle/blog/internal/render"
"github.com/kirsle/blog/internal/responses"
"github.com/kirsle/blog/internal/types"
"github.com/urfave/negroni"
)
@ -48,7 +48,6 @@ func Register(r *mux.Router, loginError http.HandlerFunc) {
r.HandleFunc("/blog", indexHandler)
r.HandleFunc("/blog.rss", feedHandler)
r.HandleFunc("/blog.atom", feedHandler)
r.HandleFunc("/blog.json", feedHandler)
r.HandleFunc("/archive", archiveHandler)
r.HandleFunc("/tagged", taggedHandler)
r.HandleFunc("/tagged/{tag}", taggedHandler)
@ -92,7 +91,6 @@ func RecentPosts(r *http.Request, tag, privacy string) []posts.Post {
// The set of blog posts to show.
var pool []posts.Post
var sticky []posts.Post // sticky pinned posts on top
for _, post := range idx.Posts {
// Limiting by a specific privacy setting? (drafts or private only)
if privacy != "" {
@ -132,20 +130,10 @@ func RecentPosts(r *http.Request, tag, privacy string) []posts.Post {
}
}
// Group them by sticky vs. not
if post.Sticky {
sticky = append(sticky, post)
} else {
pool = append(pool, post)
}
pool = append(pool, post)
}
sort.Sort(sort.Reverse(posts.ByUpdated(pool)))
if len(sticky) > 0 {
sort.Sort(sort.Reverse(posts.ByUpdated(sticky)))
pool = append(sticky, pool...)
}
return pool
}

View File

@ -7,7 +7,7 @@ import (
"github.com/gorilla/mux"
"github.com/kirsle/blog/models/posts"
"github.com/kirsle/blog/src/render"
"github.com/kirsle/blog/internal/render"
)
// tagged lets you browse blog posts by category.

View File

@ -4,14 +4,14 @@ import (
"net/http"
"github.com/gorilla/mux"
"github.com/kirsle/blog/src/forms"
"github.com/kirsle/blog/src/log"
"github.com/kirsle/blog/src/middleware/auth"
"github.com/kirsle/blog/internal/forms"
"github.com/kirsle/blog/internal/log"
"github.com/kirsle/blog/internal/middleware/auth"
"github.com/kirsle/blog/models/settings"
"github.com/kirsle/blog/models/users"
"github.com/kirsle/blog/src/render"
"github.com/kirsle/blog/src/responses"
"github.com/kirsle/blog/src/sessions"
"github.com/kirsle/blog/internal/render"
"github.com/kirsle/blog/internal/responses"
"github.com/kirsle/blog/internal/sessions"
)
// Register the initial setup routes.

View File

@ -8,9 +8,9 @@ import (
"net/url"
"strings"
"github.com/kirsle/blog/src/log"
"github.com/kirsle/blog/src/markdown"
"github.com/kirsle/blog/src/render"
"github.com/kirsle/blog/internal/log"
"github.com/kirsle/blog/internal/markdown"
"github.com/kirsle/blog/internal/render"
"github.com/kirsle/blog/models/comments"
"github.com/kirsle/blog/models/settings"
"github.com/microcosm-cc/bluemonday"

View File

@ -11,7 +11,7 @@ import (
"regexp"
"strings"
"github.com/kirsle/blog/src/log"
"github.com/kirsle/blog/internal/log"
"github.com/kirsle/blog/jsondb/caches"
"github.com/microcosm-cc/bluemonday"
"github.com/shurcooL/github_flavored_markdown"

View File

@ -4,16 +4,13 @@ import (
"net/http"
"strings"
"github.com/kirsle/blog/internal/responses"
"github.com/kirsle/blog/internal/sessions"
"github.com/kirsle/blog/models/settings"
"github.com/kirsle/blog/src/responses"
"github.com/kirsle/blog/src/sessions"
"github.com/urfave/negroni"
)
var ageGateSuffixes = []string{
"/blog.rss", // Allow public access to RSS and Atom feeds.
"/blog.atom",
"/blog.json",
".js",
".css",
".txt",
@ -22,13 +19,6 @@ var ageGateSuffixes = []string{
".jpg",
".jpeg",
".gif",
".mp4",
".webm",
".ttf",
".eot",
".svg",
".woff",
".woff2",
}
// AgeGate is a middleware generator that does age verification for NSFW sites.
@ -47,28 +37,19 @@ func AgeGate(verifyHandler func(http.ResponseWriter, *http.Request)) negroni.Han
}
// Allow static files and things through.
for _, suffix := range ageGateSuffixes {
if strings.HasSuffix(path, suffix) {
for _, prefix := range ageGateSuffixes {
if strings.HasSuffix(path, prefix) {
next(w, r)
return
}
}
// POST requests are allowed.
if r.Method == http.MethodPost {
next(w, r)
return
}
// See if they've been cleared.
session := sessions.Get(r)
if val, _ := session.Values["age-ok"].(bool); !val {
// They haven't been verified.
// Allow single-page loads with ?over18=1 in query parameter.
if r.FormValue("over18") == "" {
responses.Redirect(w, "/age-verify?next="+r.URL.Path)
return
}
responses.Redirect(w, "/age-verify?next="+r.URL.Path)
return
}
next(w, r)

View File

@ -6,8 +6,8 @@ import (
"net/http"
"github.com/kirsle/blog/models/users"
"github.com/kirsle/blog/src/sessions"
"github.com/kirsle/blog/src/types"
"github.com/kirsle/blog/internal/sessions"
"github.com/kirsle/blog/internal/types"
"github.com/urfave/negroni"
)

View File

@ -5,8 +5,8 @@ import (
"github.com/google/uuid"
gorilla "github.com/gorilla/sessions"
"github.com/kirsle/blog/src/log"
"github.com/kirsle/blog/src/sessions"
"github.com/kirsle/blog/internal/log"
"github.com/kirsle/blog/internal/sessions"
"github.com/urfave/negroni"
)

View File

@ -5,7 +5,7 @@ import (
"strings"
"time"
"github.com/kirsle/blog/src/markdown"
"github.com/kirsle/blog/internal/markdown"
)
// Funcs is a global funcmap that the blog can hook its internal

View File

@ -7,7 +7,7 @@ import (
"path/filepath"
"strings"
"github.com/kirsle/blog/src/log"
"github.com/kirsle/blog/internal/log"
)
// Blog configuration bindings.

View File

@ -8,11 +8,11 @@ import (
"time"
gorilla "github.com/gorilla/sessions"
"github.com/kirsle/blog/src/log"
"github.com/kirsle/blog/src/middleware"
"github.com/kirsle/blog/src/middleware/auth"
"github.com/kirsle/blog/src/sessions"
"github.com/kirsle/blog/src/types"
"github.com/kirsle/blog/internal/log"
"github.com/kirsle/blog/internal/middleware"
"github.com/kirsle/blog/internal/middleware/auth"
"github.com/kirsle/blog/internal/sessions"
"github.com/kirsle/blog/internal/types"
"github.com/kirsle/blog/models/settings"
"github.com/kirsle/blog/models/users"
)

View File

@ -1,11 +1,10 @@
package responses
import (
"encoding/json"
"fmt"
"net/http"
"github.com/kirsle/blog/src/sessions"
"github.com/kirsle/blog/internal/sessions"
)
// Error handlers to be filled in by the blog app.
@ -40,15 +39,3 @@ func Redirect(w http.ResponseWriter, location string) {
w.Header().Set("Location", location)
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)
}

View File

@ -6,8 +6,8 @@ import (
"time"
"github.com/gorilla/sessions"
"github.com/kirsle/blog/src/log"
"github.com/kirsle/blog/src/types"
"github.com/kirsle/blog/internal/log"
"github.com/kirsle/blog/internal/types"
)
// Store holds your cookie store information.

View File

@ -1,7 +1,6 @@
package redis
import (
"encoding/json"
"fmt"
"time"
@ -45,16 +44,6 @@ func (r *Redis) Get(key string) ([]byte, error) {
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.
func (r *Redis) Set(key string, v []byte, expires int) error {
conn := r.pool.Get()
@ -66,16 +55,6 @@ func (r *Redis) Set(key string, v []byte, expires int) error {
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.
func (r *Redis) Delete(key ...string) {
conn := r.pool.Get()

View File

@ -15,9 +15,8 @@ func UpdateIndex(p *Post) error {
// Index caches high level metadata about the blog's contents for fast access.
type Index struct {
Posts map[int]Post `json:"posts"`
Fragments map[string]int `json:"fragments"`
Thumbnails map[int]string `json:"thumbnails"`
Posts map[int]Post `json:"posts"`
Fragments map[string]int `json:"fragments"`
}
// GetIndex loads the index, or rebuilds it first if it doesn't exist.
@ -34,9 +33,8 @@ func GetIndex() (*Index, error) {
// RebuildIndex builds the index from scratch.
func RebuildIndex() (*Index, error) {
idx := &Index{
Posts: map[int]Post{},
Fragments: map[string]int{},
Thumbnails: map[int]string{},
Posts: map[int]Post{},
Fragments: map[string]int{},
}
entries, _ := DB.List("blog/posts")
for _, doc := range entries {
@ -55,24 +53,16 @@ func RebuildIndex() (*Index, error) {
// Update a blog's entry in the index.
func (idx *Index) Update(p *Post) error {
idx.Posts[p.ID] = Post{
ID: p.ID,
Title: p.Title,
Fragment: p.Fragment,
AuthorID: p.AuthorID,
Privacy: p.Privacy,
Sticky: p.Sticky,
EnableComments: p.EnableComments,
Tags: p.Tags,
Created: p.Created,
Updated: p.Updated,
ID: p.ID,
Title: p.Title,
Fragment: p.Fragment,
AuthorID: p.AuthorID,
Privacy: p.Privacy,
Tags: p.Tags,
Created: p.Created,
Updated: p.Updated,
}
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)
return err
}

View File

@ -18,12 +18,6 @@ var DB *jsondb.DB
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() {
log = golog.GetLogger("blog")
}
@ -33,7 +27,7 @@ type Post struct {
ID int `json:"id"`
Title string `json:"title"`
Fragment string `json:"fragment"`
ContentType string `json:"contentType,omitempty"`
ContentType string `json:"contentType"`
AuthorID int `json:"author"`
Body string `json:"body,omitempty"`
Privacy string `json:"privacy"`
@ -197,16 +191,6 @@ func (p *Post) Delete() error {
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.
func (p *Post) nextID() int {
// Highest ID seen so far.

View File

@ -1,63 +0,0 @@
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,
)
}
}
}

View File

@ -6,11 +6,11 @@ import (
"net/http"
"strings"
"github.com/kirsle/blog/src/controllers/posts"
"github.com/kirsle/blog/src/log"
"github.com/kirsle/blog/src/markdown"
"github.com/kirsle/blog/src/render"
"github.com/kirsle/blog/src/responses"
"github.com/kirsle/blog/internal/controllers/posts"
"github.com/kirsle/blog/internal/log"
"github.com/kirsle/blog/internal/markdown"
"github.com/kirsle/blog/internal/render"
"github.com/kirsle/blog/internal/responses"
)
// PageHandler is the catch-all route handler, for serving static web pages.

View File

@ -1,39 +0,0 @@
<!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>

View File

@ -1,9 +1,7 @@
{{ define "title" }}{{ .Data.Title }}{{ end }}
{{ define "content" }}
<div class="markdown">
{{ .Data.HTML }}
</div>
{{ if and .CurrentUser.Admin .Editable }}
<p class="mt-4">

View File

@ -69,10 +69,6 @@
name="body"
class="form-control"
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>
<p>
@ -81,15 +77,10 @@
</p>
</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>
var ACE;
(function() {
if (DISABLE_ACE_EDITOR) {
return;
}
var editor = ace.edit("ace-editor");
ACE = editor;
document.querySelector("#editor-box").style.display = "block";

View File

@ -3,40 +3,22 @@
<h1>Archive</h1>
{{ $thumbs := .Data.Thumbnails }}
{{ range .Data.Archive }}
<div class="card mb-4">
<div class="card-header">
<h3>{{ .Date.Format "January, 2006" }}</h3>
</div>
<div class="card-body">
<h3>{{ .Date.Format "January, 2006" }}</h3>
<div class="row">
{{ range .Posts }}
{{ $thumb := index $thumbs .ID }}
<div class="col-12 col-sm-6 col-md-4 col-lg-3 mb-4">
<div class="card bg-secondary"
style="height: auto; min-height: 150px;
{{ if $thumb }}background-image: url({{ $thumb }}); background-size: cover{{ end }}
"
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>
<ul class="list-unstyled">
{{ range .Posts }}
<li class="list-item">
<a href="/{{ .Fragment }}">{{ .Title }}</a>
<small class="blog-meta">
{{ .Created.Format "Jan 02 2006" }}
{{ if ne .Privacy "public" }}
<span class="blog-{{ .Privacy }}">[{{ .Privacy }}]</span>
{{ end }}
</div>
</div>
</div>
</small>
</li>
{{ end }}
</ul>
{{ end }}
{{ end }}

View File

@ -1,6 +1,6 @@
{{ define "title" }}Update Blog{{ end }}
{{ define "content" }}
<form name="blog-edit" action="/blog/edit" method="POST">
<form action="/blog/edit" method="POST">
<input type="hidden" name="_csrf" value="{{ .CSRF }}">
{{ if .Data.preview }}
<div class="card mb-5">
@ -97,15 +97,6 @@
name="body"
id="body"
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 class="form-group">
@ -183,15 +174,10 @@
</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>
var ACE;
(function() {
if (DISABLE_ACE_EDITOR) {
return;
}
var editor = ace.edit("ace-editor");
ACE = editor;
document.querySelector("#editor-box").style.display = "block";
@ -212,43 +198,6 @@ var ACE;
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) {
if (typeof(ACE) !== undefined) {
ACE.getSession().setMode("ace/mode/"+lang);

View File

@ -16,11 +16,6 @@
{{ else if eq $p.Privacy "unlisted" }}
<span class="blog-unlisted">[unlisted]</span>
{{ end }}
{{ if $p.Sticky }}
<span class="blog-sticky">[pinned]</span>
{{ end }}
<span title="{{ $p.Created.Format "Jan 2 2006 @ 15:04:05 MST" }}">
{{ $p.Created.Format "January 2, 2006" }}
</span>

View File

@ -22,9 +22,6 @@ a.blog-title {
.blog-meta .blog-draft {
color: #909;
}
.blog-meta .blog-sticky {
color: #F0F;
}
/* Comment metadata line */
.comment-meta {

View File

@ -1,34 +0,0 @@
/*
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();
});
})();

View File

@ -1,107 +0,0 @@
{{ 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 }}

View File

@ -1,150 +0,0 @@
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,
})
}

View File

@ -1,98 +0,0 @@
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))
}
}

View File

@ -1,226 +0,0 @@
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
}
}

View File

@ -1,16 +0,0 @@
/*
Package questions implements the "Ask Me Anything" feature.
Routes
/ask Ask Me Anything
Related Models
questions
Description
Pending description.
*/
package questions

View File

@ -1,21 +0,0 @@
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")
}

View File

@ -1,125 +0,0 @@
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
}

View File

@ -1,11 +0,0 @@
package models
// Status of a Question.
type Status string
// Status options.
const (
Pending = "pending"
Answered = "answered"
Deleted = "deleted"
)

View File

@ -1,108 +0,0 @@
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)
}