Compare commits
3 Commits
master
...
plugin_rew
Author | SHA1 | Date | |
---|---|---|---|
3fbcfdcb5d | |||
8cc411b89d | |||
e0f25b8529 |
8
.gitignore
vendored
8
.gitignore
vendored
@ -11,8 +11,6 @@ ribbit.db
|
|||||||
# Example Feeds
|
# Example Feeds
|
||||||
rssfeed.xml
|
rssfeed.xml
|
||||||
|
|
||||||
# Netcaptain
|
# vim-restconsole
|
||||||
.ncpt
|
console.rest
|
||||||
|
vrc_cookie_jar
|
||||||
# vim local config
|
|
||||||
.vimrc.local
|
|
||||||
|
566
assets.go
Normal file
566
assets.go
Normal file
@ -0,0 +1,566 @@
|
|||||||
|
// Code generated by "esc -o assets.go assets templates"; DO NOT EDIT.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"encoding/base64"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type _escLocalFS struct{}
|
||||||
|
|
||||||
|
var _escLocal _escLocalFS
|
||||||
|
|
||||||
|
type _escStaticFS struct{}
|
||||||
|
|
||||||
|
var _escStatic _escStaticFS
|
||||||
|
|
||||||
|
type _escDirectory struct {
|
||||||
|
fs http.FileSystem
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
type _escFile struct {
|
||||||
|
compressed string
|
||||||
|
size int64
|
||||||
|
modtime int64
|
||||||
|
local string
|
||||||
|
isDir bool
|
||||||
|
|
||||||
|
once sync.Once
|
||||||
|
data []byte
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_escLocalFS) Open(name string) (http.File, error) {
|
||||||
|
f, present := _escData[path.Clean(name)]
|
||||||
|
if !present {
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
return os.Open(f.local)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_escStaticFS) prepare(name string) (*_escFile, error) {
|
||||||
|
f, present := _escData[path.Clean(name)]
|
||||||
|
if !present {
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
f.once.Do(func() {
|
||||||
|
f.name = path.Base(name)
|
||||||
|
if f.size == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var gr *gzip.Reader
|
||||||
|
b64 := base64.NewDecoder(base64.StdEncoding, bytes.NewBufferString(f.compressed))
|
||||||
|
gr, err = gzip.NewReader(b64)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.data, err = ioutil.ReadAll(gr)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs _escStaticFS) Open(name string) (http.File, error) {
|
||||||
|
f, err := fs.prepare(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return f.File()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dir _escDirectory) Open(name string) (http.File, error) {
|
||||||
|
return dir.fs.Open(dir.name + name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *_escFile) File() (http.File, error) {
|
||||||
|
type httpFile struct {
|
||||||
|
*bytes.Reader
|
||||||
|
*_escFile
|
||||||
|
}
|
||||||
|
return &httpFile{
|
||||||
|
Reader: bytes.NewReader(f.data),
|
||||||
|
_escFile: f,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *_escFile) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *_escFile) Readdir(count int) ([]os.FileInfo, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *_escFile) Stat() (os.FileInfo, error) {
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *_escFile) Name() string {
|
||||||
|
return f.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *_escFile) Size() int64 {
|
||||||
|
return f.size
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *_escFile) Mode() os.FileMode {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *_escFile) ModTime() time.Time {
|
||||||
|
return time.Unix(f.modtime, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *_escFile) IsDir() bool {
|
||||||
|
return f.isDir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *_escFile) Sys() interface{} {
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// FS returns a http.Filesystem for the embedded assets. If useLocal is true,
|
||||||
|
// the filesystem's contents are instead used.
|
||||||
|
func FS(useLocal bool) http.FileSystem {
|
||||||
|
if useLocal {
|
||||||
|
return _escLocal
|
||||||
|
}
|
||||||
|
return _escStatic
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dir returns a http.Filesystem for the embedded assets on a given prefix dir.
|
||||||
|
// If useLocal is true, the filesystem's contents are instead used.
|
||||||
|
func Dir(useLocal bool, name string) http.FileSystem {
|
||||||
|
if useLocal {
|
||||||
|
return _escDirectory{fs: _escLocal, name: name}
|
||||||
|
}
|
||||||
|
return _escDirectory{fs: _escStatic, name: name}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FSByte returns the named file from the embedded assets. If useLocal is
|
||||||
|
// true, the filesystem's contents are instead used.
|
||||||
|
func FSByte(useLocal bool, name string) ([]byte, error) {
|
||||||
|
if useLocal {
|
||||||
|
f, err := _escLocal.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
b, err := ioutil.ReadAll(f)
|
||||||
|
_ = f.Close()
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
f, err := _escStatic.prepare(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return f.data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FSMustByte is the same as FSByte, but panics if name is not present.
|
||||||
|
func FSMustByte(useLocal bool, name string) []byte {
|
||||||
|
b, err := FSByte(useLocal, name)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// FSString is the string version of FSByte.
|
||||||
|
func FSString(useLocal bool, name string) (string, error) {
|
||||||
|
b, err := FSByte(useLocal, name)
|
||||||
|
return string(b), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// FSMustString is the string version of FSMustByte.
|
||||||
|
func FSMustString(useLocal bool, name string) string {
|
||||||
|
return string(FSMustByte(useLocal, name))
|
||||||
|
}
|
||||||
|
|
||||||
|
var _escData = map[string]*_escFile{
|
||||||
|
|
||||||
|
"/assets/css/main.css": {
|
||||||
|
local: "assets/css/main.css",
|
||||||
|
size: 4980,
|
||||||
|
modtime: 1547133837,
|
||||||
|
compressed: `
|
||||||
|
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
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
"/assets/img/favicon.png": {
|
||||||
|
local: "assets/img/favicon.png",
|
||||||
|
size: 4752,
|
||||||
|
modtime: 1547232209,
|
||||||
|
compressed: `
|
||||||
|
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
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
"/assets/vendor/css/grids-responsive-min.css": {
|
||||||
|
local: "assets/vendor/css/grids-responsive-min.css",
|
||||||
|
size: 8032,
|
||||||
|
modtime: 1547133812,
|
||||||
|
compressed: `
|
||||||
|
H4sIAAAAAAAC/8SVT4/bRg/G7/4UziHA+wa1LM9oRrL3UjTNoUBQFMipR68trIXqjyHLSbaL/e6FIc1G
|
||||||
|
fEjOIqc9PhQ1Esnh81t/eLf469qXy69p4hOz+NidH/vq4TQsTbqxy7/3p657t/hcHcr2Uh6X1/ZY9svh
|
||||||
|
VC5/+/L7cgoni9MwnC+79fqhGk7X++TQNevH25vr87Uv1/d1d79u9peh7Nef//j46c8vn5LmuPiwXvza
|
||||||
|
lMdqv7wc+rJsl/v2uPxfU7Wrb9VxOO2sS1zZ/P8puR2yuq4uzWrzy1wwaahGmVFtqYSnjkpPZUFkimez
|
||||||
|
f2FfNxiwGMgw4DDgMZBjoMDAFgKGaUslaYPBQg3WZdiBWJfBuizTIB2VpPV4WEazHcwBW+gw3VNJvoXt
|
||||||
|
zuHwnD0nr+MsbqN4OlaXc71/3FVtXbXl6r7uDv/c/dt1zW5zV5fDUPary3l/qNqHXdv1zb6++9b1R4x9
|
||||||
|
LfuhOuzr1b6uHtrd0J3vhvL7sOrL27reEvfXoXuGbXga1yxLNt7n758jq2R+ZBeJtdZCdoHznJI3JnGQ
|
||||||
|
6nF6IdUnnv+Gm56alDxwP94zaVLwH8pwbiGb/k4+e7KV2mBxgFO2tbwNcDO3s+wc2mBeCstoYcxJbhc4
|
||||||
|
pG6EDs3n6KROGOY6U7qDL8/G5qQrkQm3P+QXUjdCiR4+xHewCJnsunhuTyFXui+zeebSvSi41YR0+PBs
|
||||||
|
erlwM7KX6gpaneED9CFTuDPMP/OXZhR4aRhUzGyaW+FymNlIt9LliMCUubR7qXiTpu+fn6Pkzoo5t5vj
|
||||||
|
7OybYNJQjTKj2lIJTx2VnsqCyBTPZv/Cvm4wYDGQYcBhwGMgx0CBgS0EDNOWStIGg4UarMuwA7Eug3VZ
|
||||||
|
pkE6Kknr8bCMZjuYA7bQYbqnknwL253D4Tl7Tl7HWbwdt6dtULgtrFKE23wbVG7zPYpxe1xCgdvTGDVu
|
||||||
|
811WuD1NTOM2t4gYt/nNVLk9rpXAbclJYtwOvqJxW/BAhdvBfzRuS3YU4/a4tgK3JRvTuC05XITbwf80
|
||||||
|
bkt2KHM7+KTG7dFmBG5LDhnhtuSfGrclqES5HaxX4/YrMGUu/VPc9tmc2/XD7OybYNJQjTKj2lIJTx2V
|
||||||
|
nsqCyBTPZv/Cvm4wYDGQYcBhwGMgx0CBgS0EDNOWStIGg4UarMuwA7Eug3VZpkE6Kknr8bCMZjuYA7bQ
|
||||||
|
YbqnknwL253D4Tl7Tl7HWbwdt6dtULgtrFKE23wbVG7zPYpxe1xCgdvTGDVu811WuD1NTOM2t4gYt/nN
|
||||||
|
VLk9rpXAbclJYtwOvqJxW/BAhdvBfzRuS3YU4/a4tgK3JRvTuC05XITbwf80bkt2KHM7+KTG7dFmBG5L
|
||||||
|
DhnhtuSfGrclqES5HaxX4/YrMGUu/VPcLtI5t7/Xs7NvgklDNcqMakslPHVUeioLIlM8m/0L+7rBgMVA
|
||||||
|
hgGHAY+BHAMFBrYQMExbKkkbDBZqsC7DDsS6DNZlmQbpqCStx8Mymu1gDthCh+meSvItbHcOh+fsOXkd
|
||||||
|
Z/F23J62QeG2sEoRbvNtULnN9yjG7XEJBW5PY9S4zXdZ4fY0MY3b3CJi3OY3U+X2uFYCtyUniXE7+IrG
|
||||||
|
bcEDFW4H/9G4LdlRjNvj2grclmxM47bkcBFuB//TuC3Zoczt4JMat0ebEbgtOWSE25J/atyWoBLldrBe
|
||||||
|
jduvwJS5NHL7vwAAAP//TZMIY2AfAAA=
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
"/assets/vendor/css/pure-min.css": {
|
||||||
|
local: "assets/vendor/css/pure-min.css",
|
||||||
|
size: 16449,
|
||||||
|
modtime: 1547133812,
|
||||||
|
compressed: `
|
||||||
|
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
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
"/templates/footer.html": {
|
||||||
|
local: "templates/footer.html",
|
||||||
|
size: 228,
|
||||||
|
modtime: 1547128347,
|
||||||
|
compressed: `
|
||||||
|
H4sIAAAAAAAC/1zPwQqDMAzG8btPEXIX0XPsu9SlYCFbh0kLvv3Yplh76uH/ox+hgWNxHXEsEHnGZ2Iv
|
||||||
|
fSphE7+j6wDoBwAAaB0rY9EkoKNhHc88VVnzconpEPeVJfH+zddA07NZeikesXEP8aozbkGD9e8sUn11
|
||||||
|
XvV/PgEAAP///4iBjuQAAAA=
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
"/templates/header.html": {
|
||||||
|
local: "templates/header.html",
|
||||||
|
size: 200,
|
||||||
|
modtime: 1547136888,
|
||||||
|
compressed: `
|
||||||
|
H4sIAAAAAAAC/0yNsQrDIBRFd7/ikj0JZLYuhW6d2h+w+lIFayGaLPL+PRgkZHzvcM6V1m8wQad06+ag
|
||||||
|
k4OhmGlBKcOj3vfKmDslcP6elJL+ErOQo/WbEteK+cdMMbdOr0NoLvyM4bV+3j4HArMArp4jbWnpfxTX
|
||||||
|
QwCkm1Qpp8IsRzdV1FaPKEVbW3sAAAD//7XiV5HIAAAA
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
"/templates/htmlfooter.html": {
|
||||||
|
local: "templates/htmlfooter.html",
|
||||||
|
size: 115,
|
||||||
|
modtime: 1547128255,
|
||||||
|
compressed: `
|
||||||
|
H4sIAAAAAAAC/1JQUFCw0U/JLLPjAjGrqxWKEvPSUxVUMnUUVMoUrGwV9IKTizILSooVamvBSmyKwXyF
|
||||||
|
4qJkW6XqapCq2lolOxt9iDjcnNS8FIgWG/2k/JRKOy4b/YyS3Bw7LkAAAAD//zPK1EBzAAAA
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
"/templates/htmlheader.html": {
|
||||||
|
local: "templates/htmlheader.html",
|
||||||
|
size: 801,
|
||||||
|
modtime: 1547128244,
|
||||||
|
compressed: `
|
||||||
|
H4sIAAAAAAAC/5yST2/UMBDF7/spplaPdQxCCKjivZRKcAGk9oKqHrz2ZD2LYwd7krKK8t1R9g+70J44
|
||||||
|
JeOZ95uXF9cXH7/e3H//dgue27Bc1PsHQO3RuPkFoG6RDVhvckHWoudGvhfnrWha1GIgfOpSZgE2RcbI
|
||||||
|
WjyRY68dDmRR7ooroEhMJshiTUD9unr1AsphsZk6phTPaH8NeuZO4s+eBi1ujPUo57mcwpngS5K71lEY
|
||||||
|
KP6AjEEL03UBJafeekl23uIzNlooUwpyUdSuVWOGuVV1cS2Atx1qQa1Zo5oPnhGLT5ltz/CfuD2PiQMu
|
||||||
|
x7G6I8bqfq6mCSTMJ/3qUNdqP7ZY1BdSPlADgeHzLXx4PLjahwclWy3mnK6Vsi5uSmVD6l0TTMbKplaZ
|
||||||
|
jfmlAq2Kmv/62+JpUG+qd6eq2hSxrNWet1zUFw8YHTWPUh4MjyNkE9cIl3QFlwNca6jueBuweEQuME3P
|
||||||
|
cvrTPYY0jrNymg6ZjiNgdEflSws+oXGY73amTivOP/qEPJn/l12r4w2vV8ltD9E5GoCcFsFsU89iufgd
|
||||||
|
AAD//1puW/YhAwAA
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
"/templates/login.html": {
|
||||||
|
local: "templates/login.html",
|
||||||
|
size: 812,
|
||||||
|
modtime: 1547136935,
|
||||||
|
compressed: `
|
||||||
|
H4sIAAAAAAAC/6SSTW7jMAyF9zkFwb3hC9gGZjHLwQSTuYBs0bFQSRQoKU1uX8RW/LNogaIr88mPTx8F
|
||||||
|
NtrcYLAqxhYnZUcYyCeSSlmL3QmgGVncyxCyUDUfrFWlrLl60ghqSIZ9i3WOJLXlq/EIjtLEusXz38v/
|
||||||
|
Oe+ZaMjqSGmRAHuEOXdgn4RtdRXOAV82gMaqniyMLC2SU8Zi9/v5gV9aC8XY1LNh12B8yAmMfvnBK0er
|
||||||
|
SI9ALSa6J4Rg1UATW03S4iEVQeXEIw85rsS1Nrfu9BP+oGJ8Z9HYnUv1Ff3qLgNseplh04c5zusl3wKP
|
||||||
|
nzALOXI9CR6bJhreer7vmg7sW9fCvumFfeuHf+UX/KEN4PgsZYIi+pwS+xIUc+9MOsIVw66ughin5IHd
|
||||||
|
ZfY39XJelrPeb2dTP1e8O5VLPwIAAP//0xnP9ywDAAA=
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
"/templates/rss_feed.xml": {
|
||||||
|
local: "templates/rss_feed.xml",
|
||||||
|
size: 700,
|
||||||
|
modtime: 1547486966,
|
||||||
|
compressed: `
|
||||||
|
H4sIAAAAAAAC/2xSTY+bMBC951dMEbdt7bTqaev1alvUU9VDlZyiHAyeEivGIH/QShb/fQUG8qGcsGfe
|
||||||
|
e543D/b6v9HQo3WqNS/ZZ7LNXvmGWecuxS9km/ENAKtOwhjU4xmAeeU18j+qLJWHn4iS0VRKba3MmZ+8
|
||||||
|
754ptROIlEFrtFUr8V9rz45UbUNRKk9jJDtsOi08FsILsndoyT4oOQyMTkJJU6KrrOq8ag0fX4S/rYXH
|
||||||
|
ZIfWiAZHgWvWPJswdRA1cjSfgmN0vS9t578HpWUhPPJ7/bUzDXcDnRfjNf+6ZXT8TpUYwQpTI+TqI+Q9
|
||||||
|
PL/AreToxcEwJP6Hw4/ibfd2yOAJ8p4Ul/HhCbLjkWcJpzw26YE1jRjznvyefV+lAcC6UM528p78Es7v
|
||||||
|
OzmbWFoLtA5KJpxrg61wGL6lmw71cr5VmBgLfQrsOre75Cb+6OpxOPTiK0ZAI9NiGF1/P0atc3zzHgAA
|
||||||
|
//+UZXAKvAIAAA==
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
"/templates/user_dashboard.html": {
|
||||||
|
local: "templates/user_dashboard.html",
|
||||||
|
size: 97,
|
||||||
|
modtime: 1547486320,
|
||||||
|
compressed: `
|
||||||
|
H4sIAAAAAAAC/7JJVMgoSk2zVdIvLU4t0k9LTU0pVrJzTcksUQguTSpOLsosKMnMzyu20U+04+JCqC4q
|
||||||
|
LtavrtYLSc0tyEksSXVJLEnUCy3NTKmtVbILCg5WcEtNTQHrAQQAAP//EcPbEmEAAAA=
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
"/templates/user_feeds.html": {
|
||||||
|
local: "templates/user_feeds.html",
|
||||||
|
size: 4065,
|
||||||
|
modtime: 1547235825,
|
||||||
|
compressed: `
|
||||||
|
H4sIAAAAAAAC/5xX3W7bNhS+11MQRC5kxJHbXdaWgTbdgALFBszpbgLPoMVjiyhNCiRlJwj87gNJyaQU
|
||||||
|
WfN2kcDS+c4Pv/PDowVlR8RojgnnGy1rVYDeFFIYwgQovFzMKDsuk8VOqgMS5AA53gHQja63ulCsMkwK
|
||||||
|
jREp7I8cz2oNalZXlBjoQaTQ9fbATI69eBWJ/yK8hnSClwlCCyaq2riYrLGNdacxMq8V5LhklILATSSx
|
||||||
|
/GhN5BijmTOyrY2RotHyjvFyRY6wmHnRMlnM7KGWycKHsUyORKFgEuXoOXl7Q4qIPaA7NkV3R/QpR9kT
|
||||||
|
HCpODHwlhmQ/NKhsVW9XvN5rdD4nCOG3t7vj+YynVhsEtW/X88SZtyz/B+u/AdCVT4q3HTSOTmVnde6O
|
||||||
|
DthAEHpz/xHCliX8yQe0y34nB3BhNVJSm1KqIP/snmOE5vU+yO0RO1IXWCR3zzGCgi6C/CvoIpZyos3G
|
||||||
|
10IAfSfa/HDvzmfskOepP3fDZJfTXS1c5SEmmEkn7uiW56aSH9tCRjmisqgPIEy2B/MrB/vzy+s3ml6p
|
||||||
|
/Mm8a8ombG3f7aRK7XuGcvRhjhhahKxmHMTelHPE7u8nTR7YLm1MZExQePljl17wz2ydeeEELdCHySV1
|
||||||
|
rUZV63IQ7qlJ/N9ASK2BgYD63GSkqkDQx5JxmhYKiIFQd09ky6E9wDNbTxwv5+QccT+m00/JYEa8gSYp
|
||||||
|
Kabs2Kf/iRkOIyrlR68RoTMmBKgneDEob97fY4RwgA0zEJm4BGHsiUbcO7mPwP3MCk60/s60yQilKa5q
|
||||||
|
BQ8eNI0eHrZSUVBA8RQXIAyoB8I5nrZjLvgvgdAx/1Yew80Yv0bF0Mfy56jlGCtuQJpSdKjHdu7gYIPc
|
||||||
|
ZIN0bfjZFFnhN1nhXSt2uCA/XbAHmG7uxyxO/mfzD3QvytuKDB3v62a4FV1D/SlPHVs+oDAExmraGe+1
|
||||||
|
1KqUpxESSdxQFptp88rhGReSS4XXls8trwFfQdVKt7BKMlvbfWQnNce+VIqCs8KWZjtn0ojUfpc1c1un
|
||||||
|
l9bpz9L3Lv/GDYEIuIZ/hR9x4Pt9mxu533NIO5177g2k91PGepkMDaSGxQNReyYettIYefBk/gIHPE8S
|
||||||
|
hBSYWom+5vzKZL6UkC2fbwYOYTCr0TqIh0VR/nwEPtp6tIP+Il9GwG7R83iPzTSYz8Yotq0NpNgub3iK
|
||||||
|
cFFC8XMrX0aglBjy4NaVKWpP+NxuKOt7PMf30WuLW8fGRorNJzZeVlNTMm23iEH/k0vq2S4N2+Tl7r81
|
||||||
|
OrTMw0LQhOmIAHsRGFVD66fJSlNeqce6KJQ8RW8t6JIcuxremMsW2mmHEK9bMtc9d63OxZ9dNm/010Kv
|
||||||
|
+GvW1r7HVuvisa7ojQ4b5BV/8Zrad9poerJ9Pyp56vbgQAHZFPvUDteIl8cFEIF0xVkBV9Wm6KOvwHiq
|
||||||
|
RWC3UTpkWz9XP8e659iDWdVbO0JC+M2Zr67WSp42+N7iMwUVJwWkeI6neOPa5ByMv4/BL5HezcCtG51o
|
||||||
|
4NoNYy1EHTTs5TlvbxIlT+GqsKkNZ9BfXp/I3q4vl1n1/GE90IXtJRzTdZVV5+wqY9En7SRz37Qojw7r
|
||||||
|
UuK/debJVdLmyWLWftT+EwAA///sH8RP4Q8AAA==
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
"/": {
|
||||||
|
isDir: true,
|
||||||
|
local: "",
|
||||||
|
},
|
||||||
|
|
||||||
|
"/assets": {
|
||||||
|
isDir: true,
|
||||||
|
local: "assets",
|
||||||
|
},
|
||||||
|
|
||||||
|
"/assets/css": {
|
||||||
|
isDir: true,
|
||||||
|
local: "assets/css",
|
||||||
|
},
|
||||||
|
|
||||||
|
"/assets/img": {
|
||||||
|
isDir: true,
|
||||||
|
local: "assets/img",
|
||||||
|
},
|
||||||
|
|
||||||
|
"/assets/vendor": {
|
||||||
|
isDir: true,
|
||||||
|
local: "assets/vendor",
|
||||||
|
},
|
||||||
|
|
||||||
|
"/assets/vendor/css": {
|
||||||
|
isDir: true,
|
||||||
|
local: "assets/vendor/css",
|
||||||
|
},
|
||||||
|
|
||||||
|
"/templates": {
|
||||||
|
isDir: true,
|
||||||
|
local: "templates",
|
||||||
|
},
|
||||||
|
}
|
355
assets/css/main.css
Normal file
355
assets/css/main.css
Normal file
@ -0,0 +1,355 @@
|
|||||||
|
html {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
color: #777;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.content {
|
||||||
|
padding: 15px;
|
||||||
|
min-height: 100%;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.half {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa-2 {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
color: #0078e7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-bg {
|
||||||
|
background-color: #0078e7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: #DD0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-bg {
|
||||||
|
background-color: #DD0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.optionalfield {
|
||||||
|
margin: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
input.file {
|
||||||
|
padding: .5em .6em;
|
||||||
|
display: inline-block;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
box-shadow: inset 0 1px 3px #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.ranking-input {
|
||||||
|
width: 50px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid #CCC;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
i.move-icon {
|
||||||
|
cursor: ns-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu {
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
position: relative;
|
||||||
|
background: #191818;
|
||||||
|
webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu .pure-menu-heading {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu .pure-menu-nonlink {
|
||||||
|
color: #777;
|
||||||
|
padding: .5em 1em;
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu .menu-button {
|
||||||
|
display: inline;
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
right: 0px;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-bottom {
|
||||||
|
width: 150px;
|
||||||
|
border-top: 1px solid white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu .menu-container {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
right: 0px;
|
||||||
|
top: 40px;
|
||||||
|
background-color: #191818;
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu .menu-container>ul {
|
||||||
|
background-color: #191818;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.horizontal-scroll {
|
||||||
|
overflow-x: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.thumbnail {
|
||||||
|
height: 100px;
|
||||||
|
max-height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail-container {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #EEE;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.space {
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.space-sides {
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.space-vertical {
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.large {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.big-space {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-all {
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: center;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.center tbody>td {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.padding td {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tfoot {
|
||||||
|
border-top: 1px solid #CCC;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pure-button-error {
|
||||||
|
background-color: #DD0000;
|
||||||
|
color: #FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pure-button-toggle-first {
|
||||||
|
border-top-right-radius: 0px;
|
||||||
|
border-bottom-right-radius: 0px;
|
||||||
|
}
|
||||||
|
.pure-button-toggle-middle {
|
||||||
|
border-radius: 0px;
|
||||||
|
margin-left: -5px;
|
||||||
|
border-left: 1px solid #CCC;
|
||||||
|
}
|
||||||
|
.pure-button-toggle-last {
|
||||||
|
border-top-left-radius: 0px;
|
||||||
|
border-bottom-left-radius: 0px;
|
||||||
|
margin-left: -5px;
|
||||||
|
border-left: 1px solid #CCC;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pure-button:disabled {
|
||||||
|
background-color: #CCC;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
#modal-body {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#modal-overlay {
|
||||||
|
visibility: hidden;
|
||||||
|
position: absolute;
|
||||||
|
left: 0px;
|
||||||
|
top: 0px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
z-index: 1000;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#modal-overlay>div {
|
||||||
|
margin: 10% 10%;
|
||||||
|
width: 80%;
|
||||||
|
margin: 100px auto;
|
||||||
|
background-color: #FFF;
|
||||||
|
border: 1px solid #000;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#embiggenedScreenShot {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#embiggenedScreenShot>img{
|
||||||
|
max-width:100%;
|
||||||
|
max-height:100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.fullscreen {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
text-align: center;
|
||||||
|
margin: auto auto;
|
||||||
|
z-index:1001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pure-form input[disabled]:not([type]).disabled-but-visible {
|
||||||
|
background-color: #FFF;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
input.larger {
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 40em) {
|
||||||
|
#menu {
|
||||||
|
width: 150px;
|
||||||
|
height: 100%;
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu .menu-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu .menu-container{
|
||||||
|
display: initial;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-bottom {
|
||||||
|
width: 150px;
|
||||||
|
position: fixed;
|
||||||
|
left: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-left: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.flash {
|
||||||
|
font-size: 24px;
|
||||||
|
position: fixed;
|
||||||
|
top: 0px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.flash.error {
|
||||||
|
background-color: #DD0000;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.flash.success {
|
||||||
|
background-color: #229af9;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small Screens */
|
||||||
|
@media screen and (max-width:35.5em) {
|
||||||
|
div.pure-control-group label.control-label {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.only-large {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.only-small {
|
||||||
|
display: default;
|
||||||
|
}
|
||||||
|
.team-management-buttons {
|
||||||
|
text-align:left;
|
||||||
|
}
|
||||||
|
.team-management-buttons>.pure-button {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Starting at Small, horizontal */
|
||||||
|
@media screen and (min-width: 35.5em) {
|
||||||
|
div.pure-control-group label.control-label {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.only-small {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small Horizontal to Medium */
|
||||||
|
@media screen and (min-width: 35.5em) and (max-width: 48em) { }
|
||||||
|
|
||||||
|
/* Medium Screens */
|
||||||
|
@media screen and (min-width: 48em) and (max-width:64em) { }
|
||||||
|
|
||||||
|
/* Larger Screens */
|
||||||
|
@media (min-width: 64em) { }
|
||||||
|
|
BIN
assets/img/favicon.png
Normal file
BIN
assets/img/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
7
assets/vendor/css/grids-responsive-min.css
vendored
Normal file
7
assets/vendor/css/grids-responsive-min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
11
assets/vendor/css/pure-min.css
vendored
Normal file
11
assets/vendor/css/pure-min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
8
buildplugins.sh
Executable file
8
buildplugins.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
cd plugin_src
|
||||||
|
for i in `ls`; do
|
||||||
|
go build -buildmode=plugin $i
|
||||||
|
done
|
||||||
|
cd ..
|
||||||
|
mv plugin_src/*.so plugins/
|
@ -1,91 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
|
||||||
|
|
||||||
func handleApiComicsCall(w http.ResponseWriter, req *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
vars := mux.Vars(req)
|
|
||||||
cid, cidok := vars["cid"]
|
|
||||||
fn, fnok := vars["function"]
|
|
||||||
switch req.Method {
|
|
||||||
case "GET":
|
|
||||||
if !cidok {
|
|
||||||
handleApiListComics(w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !fnok {
|
|
||||||
handleApiShowComic(cid, w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch fn {
|
|
||||||
case "search":
|
|
||||||
handleApiSearchComic(cid, w)
|
|
||||||
return
|
|
||||||
|
|
||||||
default:
|
|
||||||
userError(w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
userError(w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleApiListComics(w http.ResponseWriter) {
|
|
||||||
var res []byte
|
|
||||||
var err error
|
|
||||||
if res, err = json.Marshal(m.Comics); err != nil {
|
|
||||||
serverError(w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Fprint(w, string(res))
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleApiSearchComic(nm string, w http.ResponseWriter) {
|
|
||||||
nm = strings.ToLower(nm)
|
|
||||||
var cs []Comic
|
|
||||||
for _, c := range m.Comics {
|
|
||||||
if strings.Contains(strings.ToLower(c.Name), nm) {
|
|
||||||
cs = append(cs, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var res []byte
|
|
||||||
var err error
|
|
||||||
if res, err = json.Marshal(cs); err != nil {
|
|
||||||
serverError(w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Fprint(w, string(res))
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleApiShowComic(cid string, w http.ResponseWriter) {
|
|
||||||
var err error
|
|
||||||
var c *Comic
|
|
||||||
pts := strings.Split(cid, ";")
|
|
||||||
if len(pts) != 2 {
|
|
||||||
userError(w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if c, err = m.GetComic(pts[0], pts[1]); err != nil {
|
|
||||||
userError(w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var res []byte
|
|
||||||
if res, err = json.Marshal(c); err != nil {
|
|
||||||
userError(w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Fprint(w, string(res))
|
|
||||||
return
|
|
||||||
}
|
|
@ -125,7 +125,7 @@ func handleApiSubUser(uid string, slug string, w http.ResponseWriter) {
|
|||||||
userError(w)
|
userError(w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, err = m.GetComic(pts[0], pts[1])
|
//_, err = m.GetComic(pts[0], pts[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
userError(w)
|
userError(w)
|
||||||
return
|
return
|
||||||
|
@ -1,39 +1,18 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleRequest(w http.ResponseWriter, req *http.Request) {
|
func handleRequest(w http.ResponseWriter, req *http.Request) {
|
||||||
vars := mux.Vars(req)
|
page := initPageData(w, req)
|
||||||
|
|
||||||
var fOk, uidOk bool
|
//vars := mux.Vars(req)
|
||||||
var f, uid string
|
//var fOk, uidOk bool
|
||||||
f, fOk = vars["function"]
|
//var f, uid string
|
||||||
uid, uidOk = vars["uid"]
|
//f, fOk = vars["function"]
|
||||||
if !fOk || !uidOk {
|
//uid, uidOk = vars[""]
|
||||||
// Not sure what you want me to do here, Hoss.
|
if !page.LoggedIn {
|
||||||
http.Error(w, "You did a bad", 400)
|
handleUserLoginForm(page)
|
||||||
return
|
|
||||||
}
|
|
||||||
switch f {
|
|
||||||
case "rss":
|
|
||||||
handleRssFeed(uid, w)
|
|
||||||
default:
|
|
||||||
http.Error(w, "You did a bad", 400)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleRssFeed(uid string, w http.ResponseWriter) {
|
|
||||||
w.Header().Set("Content-Type", "application/xml")
|
|
||||||
|
|
||||||
v, err := buildRssFeed(uid)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), 400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Fprint(w, v)
|
|
||||||
}
|
|
||||||
|
73
endpoints_rss.go
Normal file
73
endpoints_rss.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleRssFeed(w http.ResponseWriter, req *http.Request) {
|
||||||
|
page := initPageData(w, req)
|
||||||
|
|
||||||
|
vars := mux.Vars(req)
|
||||||
|
var uid string
|
||||||
|
var uidOk bool
|
||||||
|
if uid, uidOk = vars["uid"]; !uidOk {
|
||||||
|
userError(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/xml")
|
||||||
|
var slug string
|
||||||
|
var slugOk bool
|
||||||
|
slug, slugOk = vars["slug"]
|
||||||
|
if !slugOk {
|
||||||
|
// Print the user's entire feed
|
||||||
|
buildUserRssFeed(page, uid)
|
||||||
|
} else {
|
||||||
|
// Print the feed for a specific slug
|
||||||
|
buildSlugRssFeed(page, uid, slug)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type rssPageData struct {
|
||||||
|
User *User
|
||||||
|
Feeds []Feed
|
||||||
|
BuildDate time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildUserRssFeed(page *pageData, uid string) {
|
||||||
|
var err error
|
||||||
|
rpd := new(rssPageData)
|
||||||
|
rpd.BuildDate = time.Now()
|
||||||
|
rpd.User, err = m.GetUser(uid)
|
||||||
|
if err != nil {
|
||||||
|
userError(page.session.w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, v := range rpd.User.SubSlugs {
|
||||||
|
pts := strings.Split(v, ";")
|
||||||
|
fd, err := m.GetFeed(pts[0], pts[1])
|
||||||
|
if err != nil {
|
||||||
|
userError(page.session.w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rpd.Feeds = append(rpd.Feeds, *fd)
|
||||||
|
}
|
||||||
|
page.TemplateData = rpd
|
||||||
|
page.showRss()
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSlugRssFeed(page *pageData, uid, slug string) {
|
||||||
|
var err error
|
||||||
|
rpd := new(rssPageData)
|
||||||
|
rpd.BuildDate = time.Now()
|
||||||
|
rpd.User, err = m.GetUser(uid)
|
||||||
|
if err != nil {
|
||||||
|
userError(page.session.w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
page.TemplateData = rpd
|
||||||
|
page.showRss()
|
||||||
|
}
|
141
endpoints_user.go
Normal file
141
endpoints_user.go
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleUserRequest(w http.ResponseWriter, req *http.Request) {
|
||||||
|
page := initPageData(w, req)
|
||||||
|
|
||||||
|
vars := mux.Vars(req)
|
||||||
|
f, fOk := vars["function"]
|
||||||
|
if fOk && f == "login" {
|
||||||
|
handleUserLogin(page)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !page.LoggedIn {
|
||||||
|
page.SubTitle = "ribbit login"
|
||||||
|
page.show("login.html")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch f {
|
||||||
|
case "feeds":
|
||||||
|
handleUserFeeds(page)
|
||||||
|
case "updatesubscriptions":
|
||||||
|
handleUpdateUserSubs(page)
|
||||||
|
default:
|
||||||
|
handleUserDashboard(page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleUserLoginForm(page *pageData) {
|
||||||
|
page.SubTitle = "ribbit login"
|
||||||
|
page.show("login.html")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleUserLogin(page *pageData) {
|
||||||
|
var err error
|
||||||
|
var u *User
|
||||||
|
if page.LoggedIn {
|
||||||
|
redirect("/", page.session.w, page.session.req)
|
||||||
|
}
|
||||||
|
email := page.session.req.FormValue("email")
|
||||||
|
|
||||||
|
if u, err = m.GetUserByEmail(email); err != nil {
|
||||||
|
page.session.setFlashMessage("Invalid Login", "error")
|
||||||
|
redirect("/", page.session.w, page.session.req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
password := page.session.req.FormValue("password")
|
||||||
|
fmt.Println("Hitting func")
|
||||||
|
if err := doLogin(u.Uuid, password); err != nil {
|
||||||
|
fmt.Println("Login Failure: ", err.Error())
|
||||||
|
page.session.setFlashMessage("Invalid Login", "error")
|
||||||
|
} else {
|
||||||
|
page.session.setStringValue("id", u.Uuid)
|
||||||
|
}
|
||||||
|
redirect("/", page.session.w, page.session.req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// doLogin attempts to log in with the given email/password
|
||||||
|
// If it can't, it returns an error
|
||||||
|
func doLogin(uid, password string) error {
|
||||||
|
if strings.TrimSpace(uid) != "" && strings.TrimSpace(password) != "" {
|
||||||
|
return m.checkCredentials(uid, password)
|
||||||
|
}
|
||||||
|
return errors.New("Invalid Credentials")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleUserDashboard(page *pageData) {
|
||||||
|
page.SubTitle = "dashboard"
|
||||||
|
var err error
|
||||||
|
id, err := page.session.getStringValue("id")
|
||||||
|
if err != nil {
|
||||||
|
userError(page.session.w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
page.TemplateData, err = m.GetUser(id)
|
||||||
|
if err != nil {
|
||||||
|
userError(page.session.w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
page.show("user_dashboard.html")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleUserFeeds(page *pageData) {
|
||||||
|
page.SubTitle = "feeds"
|
||||||
|
type feedsPageData struct {
|
||||||
|
User *User
|
||||||
|
FeedSources []FeedSource
|
||||||
|
}
|
||||||
|
fpd := new(feedsPageData)
|
||||||
|
fpd.FeedSources = m.FeedSources
|
||||||
|
id, err := page.session.getStringValue("id")
|
||||||
|
if err != nil {
|
||||||
|
userError(page.session.w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fpd.User, err = m.GetUser(id)
|
||||||
|
if err != nil {
|
||||||
|
userError(page.session.w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
page.TemplateData = fpd
|
||||||
|
page.show("user_feeds.html")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleUpdateUserSubs(page *pageData) {
|
||||||
|
subs := page.session.req.FormValue("user_feeds")
|
||||||
|
fmt.Println(subs)
|
||||||
|
subPts := strings.Split(subs, ",")
|
||||||
|
var userSubs []string
|
||||||
|
for i := range subPts {
|
||||||
|
subPts[i] = strings.TrimSpace(subPts[i])
|
||||||
|
if subPts[i] != "" {
|
||||||
|
userSubs = append(userSubs, subPts[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
id, err := page.session.getStringValue("id")
|
||||||
|
if err != nil {
|
||||||
|
userError(page.session.w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var u *User
|
||||||
|
u, err = m.GetUser(id)
|
||||||
|
u.SubSlugs = userSubs
|
||||||
|
m.SaveUser(u)
|
||||||
|
for k := range m.Users {
|
||||||
|
if m.Users[k].Uuid == id {
|
||||||
|
m.Users[k].SubSlugs = userSubs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
redirect("/user/dashboard", page.session.w, page.session.req)
|
||||||
|
}
|
@ -1,68 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/PuerkitoBio/goquery"
|
|
||||||
)
|
|
||||||
|
|
||||||
func downloadDilbertList() []Comic {
|
|
||||||
var ret []Comic
|
|
||||||
ret = append(ret, *NewComic("dilbert", "Dilbert", "Scott Adams", "dilbert"))
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDilbertRssItem(slug string) (string, error) {
|
|
||||||
desc, err := getDilbertFeedDesc(time.Now())
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
comic, err := m.GetComic(SRC_DILBERT, slug)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
desc = "<![CDATA[" + desc + "]]>"
|
|
||||||
ret := " <item>\n"
|
|
||||||
ret += " <title>" + comic.Name + "</title>\n"
|
|
||||||
ret += " <pubDate>" + comic.LastUpdate.Format(time.RFC1123Z) + "</pubDate>\n"
|
|
||||||
ret += " <guid>dilbert;" + slug + ";" + comic.LastUpdate.Format(time.RFC1123Z) + "</guid>\n"
|
|
||||||
ret += " <link>" + getDilbertComicUrl(time.Now()) + "</link>\n"
|
|
||||||
ret += " <description>" + desc + "</description>\n"
|
|
||||||
ret += " </item>\n"
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDilbertComicUrl(date time.Time) string {
|
|
||||||
return fmt.Sprintf(
|
|
||||||
"http://dilbert.com/strip/%4d-%02d-%02d",
|
|
||||||
date.Year(),
|
|
||||||
date.Month(),
|
|
||||||
date.Day(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDilbertFeedDesc(date time.Time) (string, error) {
|
|
||||||
res, err := http.Get(getDilbertComicUrl(date))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
if res.StatusCode != 200 {
|
|
||||||
return "", errors.New(fmt.Sprintf("Status code error: %d %s", res.StatusCode, res.Status))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the HTML document
|
|
||||||
doc, err := goquery.NewDocumentFromReader(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
// Find the Picture
|
|
||||||
src, exists := doc.Find("img.img-comic").Attr("src")
|
|
||||||
if !exists {
|
|
||||||
return "", errors.New("Couldn't find image source")
|
|
||||||
}
|
|
||||||
return "<img src=\"https:" + src + "\" />", nil
|
|
||||||
}
|
|
@ -1,74 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/PuerkitoBio/goquery"
|
|
||||||
)
|
|
||||||
|
|
||||||
func downloadXKCDList() []Comic {
|
|
||||||
var ret []Comic
|
|
||||||
ret = append(ret, *NewComic("xkcd", "XKCD", "Randall Munroe", "xkcd"))
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func getXKCDRssItem(slug string) (string, error) {
|
|
||||||
desc, err := getXKCDFeedDesc(time.Now())
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
comic, err := m.GetComic(SRC_XKCD, slug)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
desc = "<![CDATA[" + desc + "]]>"
|
|
||||||
ret := " <item>\n"
|
|
||||||
ret += " <title>" + comic.Name + "</title>\n"
|
|
||||||
ret += " <pubDate>" + comic.LastUpdate.Format(time.RFC1123Z) + "</pubDate>\n"
|
|
||||||
ret += " <guid>xkcd;" + slug + ";" + comic.LastUpdate.Format(time.RFC1123Z) + "</guid>\n"
|
|
||||||
ret += " <link>" + getXKCDComicUrl(time.Now()) + "</link>\n"
|
|
||||||
ret += " <description>" + desc + "</description>\n"
|
|
||||||
ret += " </item>\n"
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getXKCDComicUrl(date time.Time) string {
|
|
||||||
// TODO: Actually make this work correctly
|
|
||||||
// Get the previous comic number
|
|
||||||
// and find the next one
|
|
||||||
return fmt.Sprintf(
|
|
||||||
"http://xkcd.com/",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getXKCDFeedDesc(date time.Time) (string, error) {
|
|
||||||
res, err := http.Get(getXKCDComicUrl(date))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
if res.StatusCode != 200 {
|
|
||||||
return "", errors.New(fmt.Sprintf("Status code error: %d %s", res.StatusCode, res.Status))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the HTML document
|
|
||||||
doc, err := goquery.NewDocumentFromReader(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
// Find the Picture
|
|
||||||
sel := doc.Find("div#comic>img")
|
|
||||||
src, exists := sel.Attr("src")
|
|
||||||
if !exists {
|
|
||||||
return "", errors.New("Couldn't find image source")
|
|
||||||
}
|
|
||||||
src = "https:" + src
|
|
||||||
title, exists := sel.Attr("title")
|
|
||||||
if !exists {
|
|
||||||
title = ""
|
|
||||||
}
|
|
||||||
return "<img src=\"" + src + "\" /><p>" + title + "</p>", nil
|
|
||||||
}
|
|
89
helpers.go
89
helpers.go
@ -1,98 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
SRC_GOCOMICS = "gocomics"
|
|
||||||
SRC_DILBERT = "dilbert"
|
|
||||||
SRC_XKCD = "xkcd"
|
|
||||||
)
|
|
||||||
|
|
||||||
func downloadComicsList() []Comic {
|
|
||||||
var ret []Comic
|
|
||||||
ret = append(ret, downloadGoComicsList()...)
|
|
||||||
ret = append(ret, downloadDilbertList()...)
|
|
||||||
ret = append(ret, downloadXKCDList()...)
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func getRssItem(source, slug string) (string, error) {
|
|
||||||
switch source {
|
|
||||||
case SRC_GOCOMICS:
|
|
||||||
return getGoComicsRssItem(slug)
|
|
||||||
case SRC_DILBERT:
|
|
||||||
return getDilbertRssItem(slug)
|
|
||||||
case SRC_XKCD:
|
|
||||||
return getXKCDRssItem(slug)
|
|
||||||
}
|
|
||||||
return "", errors.New("Invalid source")
|
|
||||||
}
|
|
||||||
|
|
||||||
func getComicUrl(source, slug string, dt time.Time) (string, error) {
|
|
||||||
switch source {
|
|
||||||
case SRC_GOCOMICS:
|
|
||||||
return getGoComicsComicUrl(slug, dt), nil
|
|
||||||
case SRC_DILBERT:
|
|
||||||
return getDilbertComicUrl(dt), nil
|
|
||||||
case SRC_XKCD:
|
|
||||||
return getXKCDComicUrl(dt), nil
|
|
||||||
}
|
|
||||||
return "", errors.New("Invalid source")
|
|
||||||
}
|
|
||||||
|
|
||||||
func getComicDesc(source, slug string, dt time.Time) (string, error) {
|
|
||||||
switch source {
|
|
||||||
case SRC_GOCOMICS:
|
|
||||||
return getGoComicsFeedDesc(slug, dt)
|
|
||||||
case SRC_DILBERT:
|
|
||||||
return getDilbertFeedDesc(dt)
|
|
||||||
case SRC_XKCD:
|
|
||||||
return getXKCDFeedDesc(dt)
|
|
||||||
}
|
|
||||||
return "", errors.New("Unknown Comic Source")
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildRssFeed(uid string) (string, error) {
|
|
||||||
var usr *User
|
|
||||||
var err error
|
|
||||||
if usr, err = m.GetUser(uid); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
output := []string{
|
|
||||||
"<?xml version=\"1.0\"?>",
|
|
||||||
"<rss version=\"2.0\">",
|
|
||||||
" <channel>",
|
|
||||||
" <title>BCW Comic Feed</title>",
|
|
||||||
" <link>http://ribbit.bullercodeworks.com/edit/" + uid + "</link>",
|
|
||||||
" <description>Comic feed for " + usr.Username + "</description>",
|
|
||||||
" <language>en-us</language>",
|
|
||||||
" <lastBuildDate>" + time.Now().Format(time.RFC1123) + "</lastBuildDate>",
|
|
||||||
" <ttl>40</ttl>",
|
|
||||||
}
|
|
||||||
|
|
||||||
//date := time.Now()
|
|
||||||
for _, slug := range usr.SubSlugs {
|
|
||||||
pts := strings.Split(slug, ";")
|
|
||||||
if len(pts) != 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if comic, err := m.GetComic(pts[0], pts[1]); err == nil {
|
|
||||||
output = append(output, comic.GetRssItem())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
output = append(output, []string{
|
|
||||||
" </channel>",
|
|
||||||
"</rss>",
|
|
||||||
}...)
|
|
||||||
return strings.Join(output, "\n"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func addStringIfUnique(st string, sl []string) []string {
|
func addStringIfUnique(st string, sl []string) []string {
|
||||||
for i := range sl {
|
for i := range sl {
|
||||||
if sl[i] == st {
|
if sl[i] == st {
|
||||||
|
263
main.go
263
main.go
@ -1,25 +1,37 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
|
//go:generate esc -o assets.go assets templates
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"plugin"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
"github.com/justinas/alice"
|
"github.com/justinas/alice"
|
||||||
|
"golang.org/x/crypto/ssh/terminal"
|
||||||
)
|
)
|
||||||
|
|
||||||
const AppName = "ribbit"
|
const AppName = "ribbit"
|
||||||
const DbName = AppName + ".db"
|
const DbName = AppName + ".db"
|
||||||
|
|
||||||
|
type menuItem struct {
|
||||||
|
Label string
|
||||||
|
Location string
|
||||||
|
Icon string
|
||||||
|
}
|
||||||
|
|
||||||
var sessionStore *sessions.CookieStore
|
var sessionStore *sessions.CookieStore
|
||||||
var r *mux.Router
|
var r *mux.Router
|
||||||
var m *model
|
var m *model
|
||||||
@ -32,106 +44,60 @@ func main() {
|
|||||||
errorExit("Unable to initialize Model: " + err.Error())
|
errorExit("Unable to initialize Model: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(os.Args) > 2 {
|
initialize()
|
||||||
key, val := os.Args[1], os.Args[2]
|
sessionStore = sessions.NewCookieStore([]byte(m.Site.sessionSecret))
|
||||||
switch key {
|
|
||||||
case "--add-user-sub":
|
|
||||||
if len(os.Args) < 3 {
|
|
||||||
errorExit("Usage: --add-user-sub <username> <comic-slug>\nFor a list of slugs, use --list-comics")
|
|
||||||
}
|
|
||||||
slug := os.Args[3]
|
|
||||||
var u *User
|
|
||||||
if u, err = m.GetUserByName(val); err != nil {
|
|
||||||
errorExit("Couldn't find a user with the username " + val)
|
|
||||||
}
|
|
||||||
pts := strings.Split(slug, ";")
|
|
||||||
if len(pts) != 2 {
|
|
||||||
errorExit("Invalid slug given: " + slug)
|
|
||||||
}
|
|
||||||
_, err := m.GetComic(pts[0], pts[1])
|
|
||||||
if err != nil {
|
|
||||||
errorExit("Couldn't find comic with slug: " + slug)
|
|
||||||
}
|
|
||||||
fmt.Println(u.SubSlugs)
|
|
||||||
fmt.Println(slug)
|
|
||||||
u.SubSlugs = append(u.SubSlugs, slug)
|
|
||||||
fmt.Println(u.SubSlugs)
|
|
||||||
m.SaveUser(u)
|
|
||||||
done()
|
|
||||||
default:
|
|
||||||
errorExit("Unknown argument")
|
|
||||||
}
|
|
||||||
} else if len(os.Args) > 1 {
|
|
||||||
switch os.Args[1] {
|
|
||||||
case "--test":
|
|
||||||
fmt.Println(buildRssFeed("30f14e57-6500-443c-8c77-f352788eacb0"))
|
|
||||||
done()
|
|
||||||
case "--list-comics":
|
|
||||||
comics := m.GetAllComics()
|
|
||||||
for _, c := range comics {
|
|
||||||
fmt.Printf("[ %s;%s ] %s\n", c.Source, c.Slug, c.Name)
|
|
||||||
}
|
|
||||||
done()
|
|
||||||
case "--update-feeds":
|
|
||||||
fmt.Println("Updating User Feeds...")
|
|
||||||
m.UpdateAllUserFeeds()
|
|
||||||
fmt.Println("Done.")
|
|
||||||
done()
|
|
||||||
case "--update-comics":
|
|
||||||
fmt.Println("Updating the Comics List...")
|
|
||||||
comics := downloadComicsList()
|
|
||||||
for _, c := range comics {
|
|
||||||
fmt.Printf("Updating [ %s - %s, %s ]\n", c.Slug, c.Name, c.Artist)
|
|
||||||
m.SaveComic(&c)
|
|
||||||
}
|
|
||||||
m.saveChanges()
|
|
||||||
fmt.Println("Done.")
|
|
||||||
|
|
||||||
default:
|
for _, arg := range os.Args {
|
||||||
errorExit("Unknown argument")
|
switch arg {
|
||||||
|
case "-dev":
|
||||||
|
m.Site.DevMode = true
|
||||||
|
fmt.Println("Running in Dev Mode")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
r = mux.NewRouter()
|
r = mux.NewRouter()
|
||||||
r.StrictSlash(true)
|
r.StrictSlash(true)
|
||||||
//r.PathPrefix("/assets/").Handler(http.FileServer())
|
r.PathPrefix("/assets/").Handler(http.FileServer(FS(m.Site.DevMode)))
|
||||||
|
|
||||||
pub := r.PathPrefix("/").Subrouter()
|
pub := r.PathPrefix("/").Subrouter()
|
||||||
pub.HandleFunc("/", handleRequest)
|
pub.HandleFunc("/", handleUserRequest)
|
||||||
pub.HandleFunc("/api", handleApiCall)
|
/*
|
||||||
pub.HandleFunc("/api/users", handleApiUsersCall)
|
pub.HandleFunc("/api", handleApiCall)
|
||||||
pub.HandleFunc("/api/users/{uid}", handleApiUsersCall)
|
pub.HandleFunc("/api/users", handleApiUsersCall)
|
||||||
pub.HandleFunc("/api/users/{uid}/{function}", handleApiUsersCall)
|
pub.HandleFunc("/api/users/{uid}", handleApiUsersCall)
|
||||||
pub.HandleFunc("/api/users/{uid}/{function}/{slug}", handleApiUsersCall)
|
pub.HandleFunc("/api/users/{uid}/{function}", handleApiUsersCall)
|
||||||
pub.HandleFunc("/api/comics", handleApiComicsCall)
|
pub.HandleFunc("/api/users/{uid}/{function}/{slug}", handleApiUsersCall)
|
||||||
pub.HandleFunc("/api/comics/{cid}", handleApiComicsCall)
|
pub.HandleFunc("/api/comics", handleApiComicsCall)
|
||||||
pub.HandleFunc("/api/comics/{cid}/{function}", handleApiComicsCall)
|
pub.HandleFunc("/api/comics/{cid}", handleApiComicsCall)
|
||||||
pub.HandleFunc("/{function}", handleRequest)
|
pub.HandleFunc("/api/comics/{cid}/{function}", handleApiComicsCall)
|
||||||
pub.HandleFunc("/{function}/{uid}", handleRequest)
|
*/
|
||||||
pub.HandleFunc("/{function}/{uid}/{subfunc}", handleRequest)
|
pub.HandleFunc("/rss/{uid}", handleRssFeed)
|
||||||
pub.HandleFunc("/{function}/{uid}/{subfunc}/{slug}", handleRequest)
|
pub.HandleFunc("/rss/{uid}/{slug}", handleRssFeed)
|
||||||
|
pub.HandleFunc("/user/{function}", handleUserRequest)
|
||||||
|
|
||||||
http.Handle("/", r)
|
http.Handle("/", r)
|
||||||
chain := alice.New(loggingHandler).Then(r)
|
chain := alice.New(loggingHandler).Then(r)
|
||||||
|
|
||||||
// Refresh the DB at 2 AM
|
// Refresh the DB at 2 AM
|
||||||
go func() {
|
/*
|
||||||
for {
|
go func() {
|
||||||
if m.Site.LastSave.IsZero() || (time.Now().Day() != m.Site.LastSave.Day() && time.Now().Hour() == 2) {
|
for {
|
||||||
fmt.Println("Updating GoComics List...")
|
if m.Site.LastSave.IsZero() || (time.Now().Day() != m.Site.LastSave.Day() && time.Now().Hour() == 2) {
|
||||||
comics := downloadComicsList()
|
fmt.Println("Updating GoComics List...")
|
||||||
for _, c := range comics {
|
comics := downloadComicsList()
|
||||||
fmt.Printf("Updating [ %s - %s, %s ]\n", c.Slug, c.Name, c.Artist)
|
for _, c := range comics {
|
||||||
m.SaveComic(&c)
|
fmt.Printf("Updating [ %s - %s, %s ]\n", c.Slug, c.Name, c.Artist)
|
||||||
|
m.SaveComic(&c)
|
||||||
|
}
|
||||||
|
fmt.Println("Updating User Feeds...")
|
||||||
|
m.UpdateAllUserFeeds()
|
||||||
|
m.saveChanges()
|
||||||
|
fmt.Println("Done.")
|
||||||
}
|
}
|
||||||
fmt.Println("Updating User Feeds...")
|
time.Sleep(time.Minute)
|
||||||
m.UpdateAllUserFeeds()
|
|
||||||
m.saveChanges()
|
|
||||||
fmt.Println("Done.")
|
|
||||||
}
|
}
|
||||||
time.Sleep(time.Minute)
|
}()
|
||||||
}
|
*/
|
||||||
}()
|
|
||||||
|
|
||||||
// Set up a channel to intercept Ctrl+C for graceful shutdowns
|
// Set up a channel to intercept Ctrl+C for graceful shutdowns
|
||||||
c := make(chan os.Signal, 1)
|
c := make(chan os.Signal, 1)
|
||||||
@ -152,6 +118,73 @@ func loggingHandler(h http.Handler) http.Handler {
|
|||||||
return handlers.LoggingHandler(os.Stdout, h)
|
return handlers.LoggingHandler(os.Stdout, h)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func initPageData(w http.ResponseWriter, req *http.Request) *pageData {
|
||||||
|
if m.Site.DevMode {
|
||||||
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
|
}
|
||||||
|
p := new(pageData)
|
||||||
|
|
||||||
|
// Get session
|
||||||
|
var err error
|
||||||
|
var s *sessions.Session
|
||||||
|
if s, err = sessionStore.Get(req, m.Site.sessionSecret); err != nil {
|
||||||
|
fmt.Println("Session error... Recreating.")
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
}
|
||||||
|
p.session = new(pageSession)
|
||||||
|
p.session.session = s
|
||||||
|
p.session.req = req
|
||||||
|
p.session.w = w
|
||||||
|
|
||||||
|
// First check if we're logged in
|
||||||
|
userId, _ := p.session.getStringValue("id")
|
||||||
|
// With a valid account
|
||||||
|
user, err := m.GetUser(userId)
|
||||||
|
if err == nil {
|
||||||
|
p.LoggedIn = true
|
||||||
|
p.IsAdmin = user.IsAdmin
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Site = m.Site
|
||||||
|
p.SubTitle = "ribbit"
|
||||||
|
p.Stylesheets = make([]string, 0, 0)
|
||||||
|
p.Stylesheets = append(p.Stylesheets, "/assets/vendor/css/pure-min.css")
|
||||||
|
p.Stylesheets = append(p.Stylesheets, "/assets/vendor/css/grids-responsive-min.css")
|
||||||
|
p.Stylesheets = append(p.Stylesheets, "/assets/css/main.css")
|
||||||
|
|
||||||
|
p.HeaderScripts = make([]string, 0, 0)
|
||||||
|
|
||||||
|
p.Scripts = make([]string, 0, 0)
|
||||||
|
|
||||||
|
p.FlashMessage, p.FlashClass = p.session.getFlashMessage()
|
||||||
|
if p.FlashClass == "" {
|
||||||
|
p.FlashClass = "hidden"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the menu
|
||||||
|
if p.LoggedIn {
|
||||||
|
p.BottomMenu = append(p.BottomMenu, menuItem{"Logout", "/user/logout", "fa-sign-out"})
|
||||||
|
} else {
|
||||||
|
if p.IsAdmin {
|
||||||
|
p.BottomMenu = append(p.BottomMenu, menuItem{"Admin", "/admin", "fa-sign-in"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func outputTemplate(tmplName string, tmplData interface{}, w http.ResponseWriter) error {
|
||||||
|
n := "/templates/" + tmplName
|
||||||
|
l := template.Must(template.New("layout").Parse(FSMustString(m.Site.DevMode, n)))
|
||||||
|
t := template.Must(l.Parse(FSMustString(m.Site.DevMode, n)))
|
||||||
|
return t.Execute(w, tmplData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// redirect can be used only for GET redirects
|
||||||
|
func redirect(url string, w http.ResponseWriter, req *http.Request) {
|
||||||
|
http.Redirect(w, req, url, 303)
|
||||||
|
}
|
||||||
|
|
||||||
func done() {
|
func done() {
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
@ -166,3 +199,61 @@ func assertError(err error) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func initialize() {
|
||||||
|
// Check if we have an admin user already
|
||||||
|
if !m.hasAdminUser() {
|
||||||
|
// Nope, create one
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
fmt.Println("Create new Admin user")
|
||||||
|
fmt.Print("Email: ")
|
||||||
|
email, _ := reader.ReadString('\n')
|
||||||
|
email = strings.TrimSpace(email)
|
||||||
|
u := NewUser(email)
|
||||||
|
var pw1, pw2 []byte
|
||||||
|
for string(pw1) != string(pw2) || string(pw1) == "" {
|
||||||
|
fmt.Print("Password: ")
|
||||||
|
pw1, _ = terminal.ReadPassword(0)
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Print("Repeat Password: ")
|
||||||
|
pw2, _ = terminal.ReadPassword(0)
|
||||||
|
fmt.Println("")
|
||||||
|
if string(pw1) != string(pw2) {
|
||||||
|
fmt.Println("Entered Passwords don't match!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
u.IsAdmin = true
|
||||||
|
m.SaveUser(u)
|
||||||
|
assertError(m.updateUserPassword(u.Uuid, string(pw1)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Site.sessionSecret == "" {
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
fmt.Println("A good session secret is like a good password")
|
||||||
|
fmt.Print("Create New Session Secret: ")
|
||||||
|
sessSc, _ := reader.ReadString('\n')
|
||||||
|
sessSc = strings.TrimSpace(sessSc)
|
||||||
|
m.Site.sessionSecret = sessSc
|
||||||
|
assertError(m.Site.SaveToDB())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load Feed Sources from Plugins
|
||||||
|
files, err := ioutil.ReadDir("./plugins/")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, f := range files {
|
||||||
|
fmt.Print("Loading plugin ", f.Name(), "...")
|
||||||
|
p, err := plugin.Open("./plugins/" + f.Name())
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
var feedSource *FeedSource
|
||||||
|
if feedSource, err = LoadFeedPlugin(p); err != nil {
|
||||||
|
fmt.Println(" ", err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m.FeedSources = append(m.FeedSources, *feedSource)
|
||||||
|
fmt.Printf("Done. (%d feeds)\n", len(feedSource.Feeds))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
57
model.go
57
model.go
@ -10,11 +10,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type model struct {
|
type model struct {
|
||||||
|
dbOpen int
|
||||||
bolt *boltease.DB
|
bolt *boltease.DB
|
||||||
dbFileName string
|
dbFileName string
|
||||||
|
dbChanged bool
|
||||||
|
|
||||||
Users []User
|
Users []User
|
||||||
Comics []Comic
|
FeedSources []FeedSource
|
||||||
|
Feeds []Feed
|
||||||
|
|
||||||
Site *SiteData
|
Site *SiteData
|
||||||
}
|
}
|
||||||
@ -31,11 +34,14 @@ func NewModel() (*model, error) {
|
|||||||
if err = m.initDB(); err != nil {
|
if err = m.initDB(); err != nil {
|
||||||
return nil, errors.New("Unable to initialzie DB: " + err.Error())
|
return nil, errors.New("Unable to initialzie DB: " + err.Error())
|
||||||
}
|
}
|
||||||
m.LoadSiteData()
|
m.Site = NewSiteData(m)
|
||||||
|
if err = m.Site.LoadFromDB(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if err = m.LoadUsers(); err != nil {
|
if err = m.LoadUsers(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err = m.LoadComics(); err != nil {
|
if err = m.LoadFeeds(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,10 +50,10 @@ func NewModel() (*model, error) {
|
|||||||
|
|
||||||
func (m *model) initDB() error {
|
func (m *model) initDB() error {
|
||||||
var err error
|
var err error
|
||||||
if err = m.bolt.OpenDB(); err != nil {
|
if err = m.openDB(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer m.bolt.CloseDB()
|
defer m.closeDB()
|
||||||
|
|
||||||
if err = m.bolt.MkBucketPath([]string{"site"}); err != nil {
|
if err = m.bolt.MkBucketPath([]string{"site"}); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -55,16 +61,37 @@ func (m *model) initDB() error {
|
|||||||
if err = m.bolt.MkBucketPath([]string{"users"}); err != nil {
|
if err = m.bolt.MkBucketPath([]string{"users"}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = m.bolt.MkBucketPath([]string{"comics"}); err != nil {
|
if err = m.bolt.MkBucketPath([]string{"feeds"}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *model) openDB() error {
|
||||||
|
m.dbOpen = m.dbOpen + 1
|
||||||
|
if m.dbOpen > 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
if err = m.openDB(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) closeDB() {
|
||||||
|
m.dbOpen = m.dbOpen - 1
|
||||||
|
if m.dbOpen == 0 {
|
||||||
|
m.closeDB()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (m *model) saveChanges() {
|
func (m *model) saveChanges() {
|
||||||
m.Site.LastSave = time.Now()
|
fmt.Println("Saving Site to DB")
|
||||||
m.SaveSite()
|
m.Site.SaveToDB()
|
||||||
//m.SaveAllComics(m.Comics)
|
//fmt.Println("Saving Feeds to DB")
|
||||||
|
//m.SaveAllFeeds(m.FeedSources)
|
||||||
|
fmt.Println("Saving Users to DB")
|
||||||
m.SaveAllUsers(m.Users)
|
m.SaveAllUsers(m.Users)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,14 +103,14 @@ func (m *model) UpdateAllUserFeeds() {
|
|||||||
allSubs = addStringIfUnique(sub, allSubs)
|
allSubs = addStringIfUnique(sub, allSubs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// So we have allSubs which contains all subscribed comics for all users
|
// So we have allSubs which contains all subscribed feeds for all users
|
||||||
for _, sub := range allSubs {
|
for _, sub := range allSubs {
|
||||||
fmt.Println("Updating Comic: " + sub)
|
fmt.Println("Updating Feed: " + sub)
|
||||||
pts := strings.Split(sub, ";")
|
pts := strings.Split(sub, ";")
|
||||||
if len(pts) != 2 {
|
if len(pts) != 2 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
c, err := m.GetComic(pts[0], pts[1])
|
c, err := m.GetFeed(pts[0], pts[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(sub, ":", err)
|
fmt.Println(sub, ":", err)
|
||||||
continue
|
continue
|
||||||
@ -92,7 +119,7 @@ func (m *model) UpdateAllUserFeeds() {
|
|||||||
fmt.Println(sub, ":", err.Error())
|
fmt.Println(sub, ":", err.Error())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err = m.SaveComic(c); err != nil {
|
if err = m.SaveFeed(c); err != nil {
|
||||||
fmt.Println(sub, ":", err.Error())
|
fmt.Println(sub, ":", err.Error())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -100,7 +127,7 @@ func (m *model) UpdateAllUserFeeds() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Source interface {
|
type Source interface {
|
||||||
downloadList() []Comic
|
downloadList() []Feed
|
||||||
getRssItem(slug string) (string, error)
|
getRssItem(slug string) (string, error)
|
||||||
getUrl(slug string, dt time.Time) (string, error)
|
getUrl(slug string, dt time.Time) (string, error)
|
||||||
getDescription(slug string, dt time.Time) (string, error)
|
getDescription(slug string, dt time.Time) (string, error)
|
||||||
|
202
model_comics.go
202
model_comics.go
@ -1,202 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Comic struct {
|
|
||||||
Name string
|
|
||||||
Artist string
|
|
||||||
Slug string
|
|
||||||
Source string
|
|
||||||
Desc string
|
|
||||||
LastUpdate time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewComic(s, n, a, source string) *Comic {
|
|
||||||
return &Comic{
|
|
||||||
Name: n,
|
|
||||||
Artist: a,
|
|
||||||
Slug: s,
|
|
||||||
Source: source,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Comic) GetBucket() []string {
|
|
||||||
return []string{"comics", c.Source, c.Slug}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Comic) Update() error {
|
|
||||||
dt := time.Now()
|
|
||||||
desc, err := getComicDesc(c.Source, c.Slug, dt)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if desc == c.Desc {
|
|
||||||
return errors.New("Comic didn't change")
|
|
||||||
}
|
|
||||||
c.Desc = desc
|
|
||||||
c.LastUpdate = dt
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Comic) GetUrl(dt time.Time) string {
|
|
||||||
var v string
|
|
||||||
var e error
|
|
||||||
if v, e = getComicUrl(c.Source, c.Slug, dt); e != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Comic) GetDesc(dt time.Time) string {
|
|
||||||
var v string
|
|
||||||
var e error
|
|
||||||
if v, e = getComicDesc(c.Source, c.Slug, dt); e != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Comic) GetRssItem() string {
|
|
||||||
var v string
|
|
||||||
var e error
|
|
||||||
if v, e = getRssItem(c.Source, c.Slug); e != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// DB Function to save a comic
|
|
||||||
func (m *model) SaveComic(c *Comic) error {
|
|
||||||
var err error
|
|
||||||
if err = m.bolt.OpenDB(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer m.bolt.CloseDB()
|
|
||||||
bkt := c.GetBucket()
|
|
||||||
if err = m.bolt.MkBucketPath(bkt); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = m.bolt.SetValue(bkt, "name", c.Name); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = m.bolt.SetValue(bkt, "artist", c.Artist); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = m.bolt.SetValue(bkt, "desc", c.Desc); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = m.bolt.SetTimestamp(bkt, "lastupdate", c.LastUpdate); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Add it to the cached list
|
|
||||||
for i, v := range m.Comics {
|
|
||||||
if v.Source == c.Source && v.Slug == c.Slug {
|
|
||||||
m.Comics[i] = *c
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m.Comics = append(m.Comics, *c)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DB Function to get a comic
|
|
||||||
func (m *model) GetComic(source, slug string) (*Comic, error) {
|
|
||||||
var err error
|
|
||||||
if err = m.bolt.OpenDB(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer m.bolt.CloseDB()
|
|
||||||
ret := new(Comic)
|
|
||||||
ret.Source = source
|
|
||||||
ret.Slug = slug
|
|
||||||
bkt := ret.GetBucket()
|
|
||||||
if ret.Name, err = m.bolt.GetValue(bkt, "name"); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if ret.Artist, err = m.bolt.GetValue(bkt, "artist"); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if ret.Desc, err = m.bolt.GetValue(bkt, "desc"); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if ret.LastUpdate, err = m.bolt.GetTimestamp(bkt, "lastupdate"); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load all comics into the model
|
|
||||||
func (m *model) LoadComics() error {
|
|
||||||
m.Comics = m.GetAllComics()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save all comics to the DB
|
|
||||||
func (m *model) SaveAllComics(comics []Comic) {
|
|
||||||
var err error
|
|
||||||
if err = m.bolt.OpenDB(); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer m.bolt.CloseDB()
|
|
||||||
for i := range comics {
|
|
||||||
m.SaveComic(&comics[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all comics from the db
|
|
||||||
func (m *model) GetAllComics() []Comic {
|
|
||||||
var ret []Comic
|
|
||||||
var err error
|
|
||||||
if err = m.bolt.OpenDB(); err != nil {
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
defer m.bolt.CloseDB()
|
|
||||||
var srcs []string
|
|
||||||
bkt := []string{"comics"}
|
|
||||||
if srcs, err = m.bolt.GetBucketList(bkt); err != nil {
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
for _, src := range srcs {
|
|
||||||
srcBkt := append(bkt, src)
|
|
||||||
var slugs []string
|
|
||||||
if slugs, err = m.bolt.GetBucketList(srcBkt); err != nil {
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
for _, slg := range slugs {
|
|
||||||
c, err := m.GetComic(src, slg)
|
|
||||||
if err == nil {
|
|
||||||
ret = append(ret, *c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete a comic from the DB
|
|
||||||
func (m *model) DeleteComic(slug string) error {
|
|
||||||
var err error
|
|
||||||
if err = m.bolt.OpenDB(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer m.bolt.CloseDB()
|
|
||||||
|
|
||||||
return m.bolt.DeleteBucket([]string{"comics"}, slug)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *model) RemoveMissingComics(comics []Comic) {
|
|
||||||
for _, c := range m.Comics {
|
|
||||||
var fnd bool
|
|
||||||
for _, nc := range comics {
|
|
||||||
if nc.Slug == c.Slug {
|
|
||||||
fnd = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !fnd {
|
|
||||||
m.DeleteComic(c.Slug)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
281
model_feeds.go
Normal file
281
model_feeds.go
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"plugin"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FeedSource struct {
|
||||||
|
Feeds []*Feed
|
||||||
|
|
||||||
|
// Plugin Functions
|
||||||
|
getSourceName func() string
|
||||||
|
refreshFeedList func() []map[string]string
|
||||||
|
getFeedUrl func(string, time.Time) (string, error)
|
||||||
|
getFeedDesc func(string, time.Time) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadFeedPlugin(p *plugin.Plugin) (*FeedSource, error) {
|
||||||
|
ret := new(FeedSource)
|
||||||
|
feedSourceSymbol, err := p.Lookup("GetSourceName")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ret.getSourceName = feedSourceSymbol.(func() string)
|
||||||
|
|
||||||
|
feedListSymbol, err := p.Lookup("GetFeedList")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ret.refreshFeedList = feedListSymbol.(func() []map[string]string)
|
||||||
|
feeds := ret.refreshFeedList()
|
||||||
|
// `feeds` is a []map, each map should contain keys: `slug`, `name`, `author`, `source`
|
||||||
|
for _, f := range feeds {
|
||||||
|
var slug, name, author, source string
|
||||||
|
var ok bool
|
||||||
|
if slug, ok = f["slug"]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if name, ok = f["name"]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if author, ok = f["author"]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if source, ok = f["source"]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ret.Feeds = append(ret.Feeds, NewFeed(slug, name, author, source))
|
||||||
|
}
|
||||||
|
|
||||||
|
feedUrlSymbol, err := p.Lookup("GetFeedUrl")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ret.getFeedUrl = feedUrlSymbol.(func(string, time.Time) (string, error))
|
||||||
|
|
||||||
|
feedDescSymbol, err := p.Lookup("GetFeedDesc")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ret.getFeedDesc = feedDescSymbol.(func(string, time.Time) (string, error))
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FeedSource) getFeed(slug string) *Feed {
|
||||||
|
for i := range f.Feeds {
|
||||||
|
if f.Feeds[i].Slug == slug {
|
||||||
|
return f.Feeds[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Feed struct {
|
||||||
|
Name string
|
||||||
|
Author string
|
||||||
|
Slug string
|
||||||
|
Source string
|
||||||
|
Desc string
|
||||||
|
LastUpdate time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFeed(s, n, a, source string) *Feed {
|
||||||
|
return &Feed{
|
||||||
|
Name: n,
|
||||||
|
Author: a,
|
||||||
|
Slug: s,
|
||||||
|
Source: source,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Feed) GetBucket() []string {
|
||||||
|
return []string{"feeds", f.Source, f.Slug}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Feed) Update() error {
|
||||||
|
/*
|
||||||
|
dt := time.Now()
|
||||||
|
desc, err := getFeedDesc(f.Source, f.Slug, dt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if desc == f.Desc {
|
||||||
|
return errors.New("Feed didn't change")
|
||||||
|
}
|
||||||
|
f.Desc = desc
|
||||||
|
f.LastUpdate = dt
|
||||||
|
*/
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Feed) GetUrl(dt time.Time) string {
|
||||||
|
var v string
|
||||||
|
/*
|
||||||
|
var e error
|
||||||
|
if v, e = getFeedUrl(f.Source, f.Slug, dt); e != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Feed) GetDesc(dt time.Time) string {
|
||||||
|
var v string
|
||||||
|
/*
|
||||||
|
var e error
|
||||||
|
if v, e = getFeedDesc(f.Source, f.Slug, dt); e != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Feed) GetRssItem() string {
|
||||||
|
var v string
|
||||||
|
/*
|
||||||
|
var e error
|
||||||
|
if v, e = getRssItem(f.Source, f.Slug); e != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// DB Function to save a feed
|
||||||
|
func (m *model) SaveFeed(f *Feed) error {
|
||||||
|
var err error
|
||||||
|
if err = m.openDB(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer m.closeDB()
|
||||||
|
bkt := f.GetBucket()
|
||||||
|
if err = m.bolt.MkBucketPath(bkt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = m.bolt.SetValue(bkt, "name", f.Name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = m.bolt.SetValue(bkt, "author", f.Author); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = m.bolt.SetValue(bkt, "desc", f.Desc); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = m.bolt.SetTimestamp(bkt, "lastupdate", f.LastUpdate); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Add it to the cached list
|
||||||
|
for i, v := range m.Feeds {
|
||||||
|
if v.Source == f.Source && v.Slug == f.Slug {
|
||||||
|
m.Feeds[i] = *f
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.Feeds = append(m.Feeds, *f)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DB Function to get a feed
|
||||||
|
func (m *model) GetFeed(source, slug string) (*Feed, error) {
|
||||||
|
var err error
|
||||||
|
if err = m.openDB(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer m.closeDB()
|
||||||
|
ret := new(Feed)
|
||||||
|
ret.Source = source
|
||||||
|
ret.Slug = slug
|
||||||
|
bkt := ret.GetBucket()
|
||||||
|
if ret.Name, err = m.bolt.GetValue(bkt, "name"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if ret.Author, err = m.bolt.GetValue(bkt, "author"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if ret.Desc, err = m.bolt.GetValue(bkt, "desc"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if ret.LastUpdate, err = m.bolt.GetTimestamp(bkt, "lastupdate"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load all feeds into the model
|
||||||
|
func (m *model) LoadFeeds() error {
|
||||||
|
m.Feeds = m.GetAllFeeds()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save all feeds to the DB
|
||||||
|
func (m *model) SaveAllFeeds(feedSources []FeedSource) {
|
||||||
|
var err error
|
||||||
|
if err = m.openDB(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer m.closeDB()
|
||||||
|
for _, v := range feedSources {
|
||||||
|
for j := range v.Feeds {
|
||||||
|
fmt.Printf("Saving Feed to DB (%s;%s)\n", v.Feeds[j].Source, v.Feeds[j].Slug)
|
||||||
|
m.SaveFeed(v.Feeds[j])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all feeds from the db
|
||||||
|
func (m *model) GetAllFeeds() []Feed {
|
||||||
|
var ret []Feed
|
||||||
|
var err error
|
||||||
|
if err = m.openDB(); err != nil {
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
defer m.closeDB()
|
||||||
|
var srcs []string
|
||||||
|
bkt := []string{"feeds"}
|
||||||
|
if srcs, err = m.bolt.GetBucketList(bkt); err != nil {
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
for _, src := range srcs {
|
||||||
|
srcBkt := append(bkt, src)
|
||||||
|
var slugs []string
|
||||||
|
if slugs, err = m.bolt.GetBucketList(srcBkt); err != nil {
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
for _, slg := range slugs {
|
||||||
|
c, err := m.GetFeed(src, slg)
|
||||||
|
if err == nil {
|
||||||
|
ret = append(ret, *c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a feed from the DB
|
||||||
|
func (m *model) DeleteFeed(slug string) error {
|
||||||
|
var err error
|
||||||
|
if err = m.openDB(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer m.closeDB()
|
||||||
|
|
||||||
|
return m.bolt.DeleteBucket([]string{"feeds"}, slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) RemoveMissingFeeds(feeds []Feed) {
|
||||||
|
for _, c := range m.Feeds {
|
||||||
|
var fnd bool
|
||||||
|
for _, nc := range feeds {
|
||||||
|
if nc.Slug == c.Slug {
|
||||||
|
fnd = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !fnd {
|
||||||
|
m.DeleteFeed(c.Slug)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,86 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type SiteData struct {
|
|
||||||
Title string
|
|
||||||
Port int
|
|
||||||
SessionName string
|
|
||||||
ServerDir string
|
|
||||||
SessionSecret string
|
|
||||||
|
|
||||||
LastSave time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSiteData() *SiteData {
|
|
||||||
ret := new(SiteData)
|
|
||||||
ret.Title = "BCW Comic Feed"
|
|
||||||
ret.Port = 8080
|
|
||||||
ret.SessionName = "bcw-comic-feed"
|
|
||||||
ret.ServerDir = "./"
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *model) LoadSiteData() {
|
|
||||||
m.Site = m.GetSite()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *model) SaveSite() error {
|
|
||||||
var err error
|
|
||||||
if err = m.bolt.OpenDB(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer m.bolt.CloseDB()
|
|
||||||
bkt := []string{"site"}
|
|
||||||
if err = m.bolt.SetValue(bkt, "title", m.Site.Title); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = m.bolt.SetInt(bkt, "port", m.Site.Port); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = m.bolt.SetValue(bkt, "session-name", m.Site.SessionName); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = m.bolt.SetValue(bkt, "session-secret", m.Site.SessionSecret); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = m.bolt.SetValue(bkt, "server-dir", m.Site.ServerDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = m.bolt.SetTimestamp(bkt, "last-save", m.Site.LastSave); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *model) GetSite() *SiteData {
|
|
||||||
s := NewSiteData()
|
|
||||||
var err error
|
|
||||||
if err = m.bolt.OpenDB(); err != nil {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
defer m.bolt.CloseDB()
|
|
||||||
bkt := []string{"site"}
|
|
||||||
var wrkStr string
|
|
||||||
var wrkInt int
|
|
||||||
var wrkTm time.Time
|
|
||||||
if wrkStr, err = m.bolt.GetValue(bkt, "title"); err == nil {
|
|
||||||
s.Title = wrkStr
|
|
||||||
}
|
|
||||||
if wrkInt, err = m.bolt.GetInt(bkt, "port"); err == nil {
|
|
||||||
s.Port = wrkInt
|
|
||||||
}
|
|
||||||
if wrkStr, err = m.bolt.GetValue(bkt, "session-name"); err == nil {
|
|
||||||
s.SessionName = wrkStr
|
|
||||||
}
|
|
||||||
if wrkStr, err = m.bolt.GetValue(bkt, "session-secret"); err == nil {
|
|
||||||
s.SessionSecret = wrkStr
|
|
||||||
}
|
|
||||||
if wrkStr, err = m.bolt.GetValue(bkt, "server-dir"); err == nil {
|
|
||||||
s.ServerDir = wrkStr
|
|
||||||
}
|
|
||||||
if wrkTm, err = m.bolt.GetTimestamp(bkt, "last-save"); err == nil {
|
|
||||||
s.LastSave = wrkTm
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
106
model_sitedata.go
Normal file
106
model_sitedata.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SiteData
|
||||||
|
* Contains configuration for the website
|
||||||
|
*/
|
||||||
|
type SiteData struct {
|
||||||
|
Title string
|
||||||
|
Ip string
|
||||||
|
Port int
|
||||||
|
SessionName string
|
||||||
|
ServerDir string
|
||||||
|
|
||||||
|
DevMode bool
|
||||||
|
|
||||||
|
m *model
|
||||||
|
mPath []string // The path in the db to this site data
|
||||||
|
changed bool
|
||||||
|
lastSave time.Time
|
||||||
|
|
||||||
|
sessionSecret string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSiteData returns a SiteData object with the default values
|
||||||
|
func NewSiteData(m *model) *SiteData {
|
||||||
|
ret := new(SiteData)
|
||||||
|
ret.Title = "ribbit"
|
||||||
|
ret.Ip = "127.0.0.1"
|
||||||
|
ret.Port = 8080
|
||||||
|
ret.SessionName = "good-ol-ribbit"
|
||||||
|
ret.ServerDir = "./"
|
||||||
|
ret.mPath = []string{"site"}
|
||||||
|
ret.m = m
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the site data out of the database
|
||||||
|
// If fields don't exist in the DB, don't clobber what is already in s
|
||||||
|
func (s *SiteData) LoadFromDB() error {
|
||||||
|
if err := s.m.openDB(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer s.m.closeDB()
|
||||||
|
|
||||||
|
if title, _ := s.m.bolt.GetValue(s.mPath, "title"); strings.TrimSpace(title) != "" {
|
||||||
|
s.Title = title
|
||||||
|
}
|
||||||
|
if ip, err := s.m.bolt.GetValue(s.mPath, "ip"); err == nil {
|
||||||
|
s.Ip = ip
|
||||||
|
}
|
||||||
|
if port, err := s.m.bolt.GetInt(s.mPath, "port"); err == nil {
|
||||||
|
s.Port = port
|
||||||
|
}
|
||||||
|
if sessionName, _ := s.m.bolt.GetValue(s.mPath, "session-name"); strings.TrimSpace(sessionName) != "" {
|
||||||
|
s.SessionName = sessionName
|
||||||
|
}
|
||||||
|
if serverDir, _ := s.m.bolt.GetValue(s.mPath, "server-dir"); strings.TrimSpace(serverDir) != "" {
|
||||||
|
s.ServerDir = serverDir
|
||||||
|
}
|
||||||
|
s.changed = false
|
||||||
|
if secret, _ := s.m.bolt.GetValue(s.mPath, "session-secret"); strings.TrimSpace(secret) != "" {
|
||||||
|
s.sessionSecret = secret
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return if the site data in memory has changed
|
||||||
|
func (s *SiteData) NeedsSave() bool {
|
||||||
|
return s.changed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the site data into the DB
|
||||||
|
func (s *SiteData) SaveToDB() error {
|
||||||
|
s.lastSave = time.Now()
|
||||||
|
var err error
|
||||||
|
if err = s.m.openDB(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer s.m.closeDB()
|
||||||
|
|
||||||
|
if err = s.m.bolt.SetValue(s.mPath, "title", s.Title); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = s.m.bolt.SetValue(s.mPath, "ip", s.Ip); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = s.m.bolt.SetInt(s.mPath, "port", s.Port); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = s.m.bolt.SetValue(s.mPath, "session-name", s.SessionName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = s.m.bolt.SetValue(s.mPath, "server-dir", s.ServerDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.changed = false
|
||||||
|
if err = s.m.bolt.SetValue(s.mPath, "session-secret", s.sessionSecret); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
118
model_user.go
118
model_user.go
@ -6,12 +6,14 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/pborman/uuid"
|
"github.com/pborman/uuid"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
Username string `json:username`
|
Username string `json:username`
|
||||||
Uuid string `json:uuid`
|
Uuid string `json:uuid`
|
||||||
SubSlugs []string `json:subs`
|
SubSlugs []string `json:subs`
|
||||||
|
IsAdmin bool `json:admin`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUser(un string) *User {
|
func NewUser(un string) *User {
|
||||||
@ -21,6 +23,7 @@ func NewUser(un string) *User {
|
|||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
func (u *User) UpdateFeed() error {
|
func (u *User) UpdateFeed() error {
|
||||||
for _, slug := range u.SubSlugs {
|
for _, slug := range u.SubSlugs {
|
||||||
pts := strings.Split(slug, ";")
|
pts := strings.Split(slug, ";")
|
||||||
@ -33,13 +36,14 @@ func (u *User) UpdateFeed() error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
func (m *model) SaveUser(u *User) error {
|
func (m *model) SaveUser(u *User) error {
|
||||||
var err error
|
var err error
|
||||||
if err = m.bolt.OpenDB(); err != nil {
|
if err = m.openDB(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer m.bolt.CloseDB()
|
defer m.closeDB()
|
||||||
bkt := []string{"users", u.Uuid}
|
bkt := []string{"users", u.Uuid}
|
||||||
if err = m.bolt.MkBucketPath(bkt); err != nil {
|
if err = m.bolt.MkBucketPath(bkt); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -47,6 +51,13 @@ func (m *model) SaveUser(u *User) error {
|
|||||||
if err = m.bolt.SetValue(bkt, "username", u.Username); err != nil {
|
if err = m.bolt.SetValue(bkt, "username", u.Username); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
var adminVal = "false"
|
||||||
|
if u.IsAdmin {
|
||||||
|
adminVal = "true"
|
||||||
|
}
|
||||||
|
if err = m.bolt.SetValue(bkt, "admin", adminVal); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
var newSubs []string
|
var newSubs []string
|
||||||
for _, v := range u.SubSlugs {
|
for _, v := range u.SubSlugs {
|
||||||
if strings.TrimSpace(v) != "" {
|
if strings.TrimSpace(v) != "" {
|
||||||
@ -60,12 +71,44 @@ func (m *model) SaveUser(u *User) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *model) GetUser(uid string) (*User, error) {
|
func (m *model) isValidUser(uid string) bool {
|
||||||
var err error
|
var err error
|
||||||
if err = m.bolt.OpenDB(); err != nil {
|
if err = m.openDB(); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer m.closeDB()
|
||||||
|
ret := new(User)
|
||||||
|
bkt := []string{"users", uid}
|
||||||
|
ret.Uuid = uid
|
||||||
|
if ret.Username, err = m.bolt.GetValue(bkt, "username"); err != nil {
|
||||||
|
fmt.Println("Error getting username value:", err.Error())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) GetUserByEmail(email string) (*User, error) {
|
||||||
|
if email == "" {
|
||||||
|
return nil, errors.New("No email given")
|
||||||
|
}
|
||||||
|
u := m.GetAllUsers()
|
||||||
|
for i := range u {
|
||||||
|
if u[i].Username == email {
|
||||||
|
return &u[i], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("No user found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) GetUser(uid string) (*User, error) {
|
||||||
|
if uid == "" {
|
||||||
|
return nil, errors.New("No user id given")
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
if err = m.openDB(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer m.bolt.CloseDB()
|
defer m.closeDB()
|
||||||
ret := new(User)
|
ret := new(User)
|
||||||
bkt := []string{"users", uid}
|
bkt := []string{"users", uid}
|
||||||
ret.Uuid = uid
|
ret.Uuid = uid
|
||||||
@ -73,6 +116,8 @@ func (m *model) GetUser(uid string) (*User, error) {
|
|||||||
fmt.Println("Error getting username value:", err.Error())
|
fmt.Println("Error getting username value:", err.Error())
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
admin, _ := m.bolt.GetValue(bkt, "admin")
|
||||||
|
ret.IsAdmin = (admin == "true")
|
||||||
var subs string
|
var subs string
|
||||||
if subs, err = m.bolt.GetValue(bkt, "subs"); err != nil {
|
if subs, err = m.bolt.GetValue(bkt, "subs"); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -83,10 +128,10 @@ func (m *model) GetUser(uid string) (*User, error) {
|
|||||||
|
|
||||||
func (m *model) SaveAllUsers(users []User) {
|
func (m *model) SaveAllUsers(users []User) {
|
||||||
var err error
|
var err error
|
||||||
if err = m.bolt.OpenDB(); err != nil {
|
if err = m.openDB(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer m.bolt.CloseDB()
|
defer m.closeDB()
|
||||||
for i := range users {
|
for i := range users {
|
||||||
m.SaveUser(&users[i])
|
m.SaveUser(&users[i])
|
||||||
}
|
}
|
||||||
@ -100,10 +145,10 @@ func (m *model) LoadUsers() error {
|
|||||||
func (m *model) GetAllUsers() []User {
|
func (m *model) GetAllUsers() []User {
|
||||||
var err error
|
var err error
|
||||||
var ret []User
|
var ret []User
|
||||||
if err = m.bolt.OpenDB(); err != nil {
|
if err = m.openDB(); err != nil {
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
defer m.bolt.CloseDB()
|
defer m.closeDB()
|
||||||
|
|
||||||
uids := m.GetUserIdList()
|
uids := m.GetUserIdList()
|
||||||
for _, uid := range uids {
|
for _, uid := range uids {
|
||||||
@ -116,10 +161,10 @@ func (m *model) GetAllUsers() []User {
|
|||||||
|
|
||||||
func (m *model) GetUserByName(nm string) (*User, error) {
|
func (m *model) GetUserByName(nm string) (*User, error) {
|
||||||
var err error
|
var err error
|
||||||
if err = m.bolt.OpenDB(); err != nil {
|
if err = m.openDB(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer m.bolt.CloseDB()
|
defer m.closeDB()
|
||||||
usrids := m.GetUserIdList()
|
usrids := m.GetUserIdList()
|
||||||
for i := range usrids {
|
for i := range usrids {
|
||||||
bkt := []string{"users", usrids[i]}
|
bkt := []string{"users", usrids[i]}
|
||||||
@ -135,11 +180,58 @@ func (m *model) GetUserByName(nm string) (*User, error) {
|
|||||||
func (m *model) GetUserIdList() []string {
|
func (m *model) GetUserIdList() []string {
|
||||||
var ret []string
|
var ret []string
|
||||||
var err error
|
var err error
|
||||||
if err = m.bolt.OpenDB(); err != nil {
|
if err = m.openDB(); err != nil {
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
defer m.bolt.CloseDB()
|
defer m.closeDB()
|
||||||
bkt := []string{"users"}
|
bkt := []string{"users"}
|
||||||
ret, _ = m.bolt.GetBucketList(bkt)
|
ret, _ = m.bolt.GetBucketList(bkt)
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateUserPassword
|
||||||
|
// Takes a user id and a password
|
||||||
|
// Fails if the user doesn't exist
|
||||||
|
func (m *model) updateUserPassword(uid, password string) error {
|
||||||
|
cryptPw, cryptError := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
|
if cryptError != nil {
|
||||||
|
return cryptError
|
||||||
|
}
|
||||||
|
if _, err := m.GetUser(uid); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.openDB(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer m.closeDB()
|
||||||
|
|
||||||
|
usrPath := []string{"users", uid}
|
||||||
|
return m.bolt.SetValue(usrPath, "password", string(cryptPw))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is the uid and pw given valid?
|
||||||
|
func (m *model) checkCredentials(uid, pw string) error {
|
||||||
|
var err error
|
||||||
|
if err = m.openDB(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer m.closeDB()
|
||||||
|
|
||||||
|
var uPw string
|
||||||
|
usrPath := []string{"users", uid}
|
||||||
|
if uPw, err = m.bolt.GetValue(usrPath, "password"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return bcrypt.CompareHashAndPassword([]byte(uPw), []byte(pw))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) hasAdminUser() bool {
|
||||||
|
users := m.GetAllUsers()
|
||||||
|
for i := range users {
|
||||||
|
if users[i].IsAdmin {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
48
page_data.go
Normal file
48
page_data.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// pageData is stuff that changes per request
|
||||||
|
type pageData struct {
|
||||||
|
Site *SiteData
|
||||||
|
Title string
|
||||||
|
SubTitle string
|
||||||
|
Stylesheets []string
|
||||||
|
HeaderScripts []string
|
||||||
|
Scripts []string
|
||||||
|
FlashMessage string
|
||||||
|
FlashClass string
|
||||||
|
LoggedIn bool
|
||||||
|
IsAdmin bool
|
||||||
|
Menu []menuItem
|
||||||
|
BottomMenu []menuItem
|
||||||
|
session *pageSession
|
||||||
|
|
||||||
|
TemplateData interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pageData) show(tmplName string) error {
|
||||||
|
for _, tmpl := range []string{
|
||||||
|
"htmlheader.html",
|
||||||
|
"header.html",
|
||||||
|
tmplName,
|
||||||
|
"footer.html",
|
||||||
|
"htmlfooter.html",
|
||||||
|
} {
|
||||||
|
if err := outputTemplate(tmpl, p, p.session.w); err != nil {
|
||||||
|
fmt.Printf("%s\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pageData) showRss() error {
|
||||||
|
if err := outputTemplate("rss_feed.xml", p, p.session.w); err != nil {
|
||||||
|
fmt.Printf("%s\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
@ -16,6 +17,9 @@ type pageSession struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *pageSession) getStringValue(key string) (string, error) {
|
func (p *pageSession) getStringValue(key string) (string, error) {
|
||||||
|
if p.session == nil {
|
||||||
|
return "", errors.New("Session is nil")
|
||||||
|
}
|
||||||
val := p.session.Values[key]
|
val := p.session.Values[key]
|
||||||
var retVal string
|
var retVal string
|
||||||
var ok bool
|
var ok bool
|
||||||
|
62
plugin_src/plugin_dilbert.go
Normal file
62
plugin_src/plugin_dilbert.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"C"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetSourceName() string {
|
||||||
|
return "dilbert"
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFeedList() []map[string]string {
|
||||||
|
var ret []map[string]string
|
||||||
|
feedMap := make(map[string]string)
|
||||||
|
feedMap["slug"] = "dilbert"
|
||||||
|
feedMap["name"] = "Dilbert"
|
||||||
|
feedMap["author"] = "Scott Adams"
|
||||||
|
feedMap["source"] = GetSourceName()
|
||||||
|
ret = append(ret, feedMap)
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFeedUrl(slug string, date time.Time) (string, error) {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"http://dilbert.com/strip/%4d-%02d-%02d",
|
||||||
|
date.Year(),
|
||||||
|
date.Month(),
|
||||||
|
date.Day(),
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFeedDesc(slug string, date time.Time) (string, error) {
|
||||||
|
url, err := GetFeedUrl(slug, date)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
res, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != 200 {
|
||||||
|
return "", errors.New(fmt.Sprintf("Status code error: %d %s", res.StatusCode, res.Status))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the HTML document
|
||||||
|
doc, err := goquery.NewDocumentFromReader(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// Find the Picture
|
||||||
|
src, exists := doc.Find("img.img-comic").Attr("src")
|
||||||
|
if !exists {
|
||||||
|
return "", errors.New("Couldn't find image source")
|
||||||
|
}
|
||||||
|
return "<img src=\"https:" + src + "\" />", nil
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"C"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -10,8 +11,12 @@ import (
|
|||||||
"github.com/PuerkitoBio/goquery"
|
"github.com/PuerkitoBio/goquery"
|
||||||
)
|
)
|
||||||
|
|
||||||
func downloadGoComicsList() []Comic {
|
func GetSourceName() string {
|
||||||
var ret []Comic
|
return "gocomics"
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFeedList() []map[string]string {
|
||||||
|
var ret []map[string]string
|
||||||
lstUrl := "http://www.gocomics.com/comics/a-to-z"
|
lstUrl := "http://www.gocomics.com/comics/a-to-z"
|
||||||
res, err := http.Get(lstUrl)
|
res, err := http.Get(lstUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -47,44 +52,33 @@ func downloadGoComicsList() []Comic {
|
|||||||
}
|
}
|
||||||
author = strings.TrimPrefix(author, "By ")
|
author = strings.TrimPrefix(author, "By ")
|
||||||
author = strings.Replace(author, "\u0026", "&", -1)
|
author = strings.Replace(author, "\u0026", "&", -1)
|
||||||
ret = append(ret, *NewComic(slug, name, author, "gocomics"))
|
feedMap := make(map[string]string)
|
||||||
|
feedMap["slug"] = slug
|
||||||
|
feedMap["name"] = name
|
||||||
|
feedMap["author"] = author
|
||||||
|
feedMap["source"] = GetSourceName()
|
||||||
|
ret = append(ret, feedMap)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func getGoComicsRssItem(slug string) (string, error) {
|
func GetFeedUrl(slug string, date time.Time) (string, error) {
|
||||||
desc, err := getGoComicsFeedDesc(slug, time.Now())
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
comic, err := m.GetComic(SRC_GOCOMICS, slug)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
desc = "<![CDATA[" + desc + "]]>"
|
|
||||||
ret := " <item>\n"
|
|
||||||
ret += " <title>" + comic.Name + "</title>\n"
|
|
||||||
ret += " <pubDate>" + comic.LastUpdate.Format(time.RFC1123Z) + "</pubDate>\n"
|
|
||||||
ret += " <guid>gocomics;" + slug + ";" + comic.LastUpdate.Format(time.RFC1123Z) + "</guid>\n"
|
|
||||||
ret += " <link>" + getGoComicsComicUrl(slug, time.Now()) + "</link>\n"
|
|
||||||
ret += " <description>" + desc + "</description>\n"
|
|
||||||
ret += " </item>\n"
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getGoComicsComicUrl(slug string, date time.Time) string {
|
|
||||||
return fmt.Sprintf(
|
return fmt.Sprintf(
|
||||||
"http://www.gocomics.com/%s/%04d/%02d/%02d",
|
"http://www.gocomics.com/%s/%04d/%02d/%02d",
|
||||||
slug,
|
slug,
|
||||||
date.Year(),
|
date.Year(),
|
||||||
date.Month(),
|
date.Month(),
|
||||||
date.Day(),
|
date.Day(),
|
||||||
)
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getGoComicsFeedDesc(slug string, date time.Time) (string, error) {
|
func GetFeedDesc(slug string, date time.Time) (string, error) {
|
||||||
res, err := http.Get(getGoComicsComicUrl(slug, date))
|
url, err := GetFeedUrl(slug, date)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
res, err := http.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
100
plugin_src/plugin_xkcd.go
Normal file
100
plugin_src/plugin_xkcd.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"C"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetSourceName() string {
|
||||||
|
return "xkcd"
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFeedList() []map[string]string {
|
||||||
|
var ret []map[string]string
|
||||||
|
feedMap := make(map[string]string)
|
||||||
|
feedMap["slug"] = "xkcd"
|
||||||
|
feedMap["name"] = "XKCD"
|
||||||
|
feedMap["author"] = "Randall Munroe"
|
||||||
|
feedMap["source"] = GetSourceName()
|
||||||
|
ret = append(ret, feedMap)
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
func getRssItem(slug string) (string, error) {
|
||||||
|
desc, err := getFeedDesc(time.Now())
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
feed, err := m.GetFeed(SRC_XKCD, slug)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
desc = "<![CDATA[" + desc + "]]>"
|
||||||
|
ret := " <item>\n"
|
||||||
|
ret += " <title>" + feed.Name + "</title>\n"
|
||||||
|
ret += " <pubDate>" + feed.LastUpdate.Format(time.RFC1123Z) + "</pubDate>\n"
|
||||||
|
ret += " <guid>xkcd;" + slug + ";" + feed.LastUpdate.Format(time.RFC1123Z) + "</guid>\n"
|
||||||
|
ret += " <link>" + getUrl(time.Now()) + "</link>\n"
|
||||||
|
ret += " <description>" + desc + "</description>\n"
|
||||||
|
ret += " </item>\n"
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
func GetFeedUrl(slug string, dt time.Time) (string, error) {
|
||||||
|
var isComicDay = func(dt time.Time) bool {
|
||||||
|
return dt.Weekday() == time.Monday || dt.Weekday() == time.Wednesday || dt.Weekday() == time.Friday
|
||||||
|
}
|
||||||
|
if !isComicDay(dt) {
|
||||||
|
return "", errors.New("No URL for the given day")
|
||||||
|
}
|
||||||
|
var num int
|
||||||
|
wrkDate := time.Date(2005, time.August, 19, 0, 0, 0, 0, time.UTC)
|
||||||
|
for wrkDate.Before(dt) {
|
||||||
|
if isComicDay(wrkDate) {
|
||||||
|
num++
|
||||||
|
}
|
||||||
|
wrkDate = wrkDate.Add(time.Hour * 24)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("https://xkcd.com/%d", num), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFeedDesc(slug string, dt time.Time) (string, error) {
|
||||||
|
var url string
|
||||||
|
var err error
|
||||||
|
if url, err = GetFeedUrl(slug, dt); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
res, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != 200 {
|
||||||
|
return "", errors.New(fmt.Sprintf("Status code error: %d %s", res.StatusCode, res.Status))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the HTML document
|
||||||
|
doc, err := goquery.NewDocumentFromReader(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// Find the Picture
|
||||||
|
sel := doc.Find("div#comic>img")
|
||||||
|
src, exists := sel.Attr("src")
|
||||||
|
if !exists {
|
||||||
|
return "", errors.New("Couldn't find image source")
|
||||||
|
}
|
||||||
|
src = "https:" + src
|
||||||
|
title, exists := sel.Attr("title")
|
||||||
|
if !exists {
|
||||||
|
title = ""
|
||||||
|
}
|
||||||
|
return "<img src=\"" + src + "\" /><p>" + title + "</p>", nil
|
||||||
|
}
|
BIN
plugins/plugin_dilbert.so
Normal file
BIN
plugins/plugin_dilbert.so
Normal file
Binary file not shown.
BIN
plugins/plugin_gocomics.so
Normal file
BIN
plugins/plugin_gocomics.so
Normal file
Binary file not shown.
BIN
plugins/plugin_xkcd.so
Normal file
BIN
plugins/plugin_xkcd.so
Normal file
Binary file not shown.
11
templates/footer.html
Normal file
11
templates/footer.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
</div>
|
||||||
|
<div id="modal-overlay">
|
||||||
|
<div>
|
||||||
|
<h1 id="modal-title"></h1>
|
||||||
|
<h2 id="modal-subtitle"></h2>
|
||||||
|
<div id="modal-body"></div>
|
||||||
|
<div id="modal-buttons">
|
||||||
|
</div>
|
||||||
|
<div class="reset-pull"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
9
templates/header.html
Normal file
9
templates/header.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<div class="flash center {{.FlashClass}}">
|
||||||
|
{{.FlashMessage}}
|
||||||
|
</div>
|
||||||
|
<div class="content center-all">
|
||||||
|
{{ if .SubTitle }}
|
||||||
|
<div class="header-menu">
|
||||||
|
<h2>{{.SubTitle}}</h2>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
6
templates/htmlfooter.html
Normal file
6
templates/htmlfooter.html
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
</div>
|
||||||
|
{{ range $i, $v := .Scripts }}
|
||||||
|
<script src="{{ $v }}"></script>
|
||||||
|
{{ end }}
|
||||||
|
</body>
|
||||||
|
</html>
|
25
templates/htmlheader.html
Normal file
25
templates/htmlheader.html
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta http-equiv="Cache-control" content="No-Cache">
|
||||||
|
<link rel="apple-touch-icon" href="/assets/img/favicon.png" type="image/png">
|
||||||
|
<link rel="shortcut icon" href="/assets/img/favicon.png" type="image/png">
|
||||||
|
|
||||||
|
<title>{{.Site.Title}} - {{.SubTitle}}</title>
|
||||||
|
|
||||||
|
<!--[if lt IE 9]>
|
||||||
|
<script src="http://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7/html5shiv.js"></script>
|
||||||
|
<![endif]-->
|
||||||
|
|
||||||
|
{{ range $i, $v := .Stylesheets }}
|
||||||
|
<link rel="stylesheet" href="{{ $v }}">
|
||||||
|
{{ end }}
|
||||||
|
{{ range $i, $v := .HeaderScripts }}
|
||||||
|
<script src="{{ $v }}"></script>
|
||||||
|
{{ end }}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="layout">
|
22
templates/login.html
Normal file
22
templates/login.html
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<div class="half center-all">
|
||||||
|
<form class="pure-form pure-form-aligned" action="/user/login" method="POST">
|
||||||
|
<fieldset>
|
||||||
|
<div class="pure-control-group">
|
||||||
|
<label for="email">Email Address</label>
|
||||||
|
<input id="email" name="email" type="text" placeholder="Email Address" autofocus>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input id="password" name="password" type="password" placeholder="Password">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-controls">
|
||||||
|
<label for="remember" class="pure-checkbox">
|
||||||
|
<input id="remember" name="remember" type="checkbox"> Remember Me
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="pure-button pure-button-primary">Submit</button>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
20
templates/rss_feed.xml
Normal file
20
templates/rss_feed.xml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<rss version="2.0">
|
||||||
|
<channel>
|
||||||
|
<title>Ribbit Feed</title>
|
||||||
|
<link>http://ribbit.bullercodeworks.com/edit/{{.TemplateData.User.Uuid}}</link>
|
||||||
|
<description>Feed for {{.TemplateData.User.Username}}</description>
|
||||||
|
<language>en-us</language>
|
||||||
|
<lastBuildDate>{{.TemplateData.BuildDate}}</lastBuildDate>
|
||||||
|
<ttl>40</ttl>
|
||||||
|
{{ range $i, $v := .TemplateData.Feeds }}
|
||||||
|
<item>
|
||||||
|
<title>{{$v.Name}}</title>
|
||||||
|
<pubDate>{{$v.LastUpdate}}</pubDate>
|
||||||
|
<guid>{{$v.Source}};{{$v.Slug}};{{$v.LastUpdate}}</guid>
|
||||||
|
<link></link>
|
||||||
|
<description><![CDATA[{{$v.Desc}}]]></description>
|
||||||
|
</item>
|
||||||
|
{{ end }}
|
||||||
|
</channel>
|
||||||
|
</rss>
|
3
templates/user_dashboard.html
Normal file
3
templates/user_dashboard.html
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<a href="/user/feeds">Edit Subscriptions</a>
|
||||||
|
|
||||||
|
<a href="/rss/{{.TemplateData.Uuid}}">RSS Feed</a>
|
135
templates/user_feeds.html
Normal file
135
templates/user_feeds.html
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
<div id="all_sources_container"></div>
|
||||||
|
<form name="feed_subscriptions" action="/user/updatesubscriptions" onsubmit="updateSubscriptionValue()">
|
||||||
|
<input id="user_feeds" type="hidden" name="user_feeds" value="" />
|
||||||
|
<button type="submit">Save</button>
|
||||||
|
</form>
|
||||||
|
<script>
|
||||||
|
var user_feeds = [
|
||||||
|
{{ range $i, $v := .TemplateData.User.SubSlugs }}
|
||||||
|
"{{$v}}",
|
||||||
|
{{ end }}
|
||||||
|
];
|
||||||
|
|
||||||
|
var all_feeds = [
|
||||||
|
{{ range $i, $v := .TemplateData.FeedSources }}
|
||||||
|
{{ range $vi, $vf := $v.Feeds }}
|
||||||
|
{
|
||||||
|
"name": "{{$vf.Name}}",
|
||||||
|
"author": "{{$vf.Author}}",
|
||||||
|
"slug": "{{$vf.Slug}}",
|
||||||
|
"source": "{{$vf.Source}}",
|
||||||
|
"desc": "{{$vf.Desc}}",
|
||||||
|
"last_update": "{{$vf.LastUpdate}}"
|
||||||
|
},
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
];
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
var sourcesContainer = document.getElementById("all_sources_container");
|
||||||
|
var sources = [];
|
||||||
|
for(var i = 0; i < all_feeds.length; i++) {
|
||||||
|
if(sources.indexOf(all_feeds[i].source) < 0) {
|
||||||
|
sources.push(all_feeds[i].source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for(var i = 0; i < sources.length; i++) {
|
||||||
|
sourcesContainer.appendChild(createFeedSourceTable(sources[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFeedSourceTable(source) {
|
||||||
|
var sourceContainer = document.createElement("div");
|
||||||
|
var sourceTitle = document.createElement("h1");
|
||||||
|
sourceTitle.innerText = source+" ";
|
||||||
|
sourceContainer.appendChild(sourceTitle);
|
||||||
|
var table = document.createElement("table");
|
||||||
|
table.classList.add("pure-table","pure-table-bordered","center-all","hidden");
|
||||||
|
var thead = document.createElement("thead");
|
||||||
|
var thtr = document.createElement("tr");
|
||||||
|
var thChk = document.createElement("th");
|
||||||
|
var thn = document.createElement("th");
|
||||||
|
thn.innerText = "Name";
|
||||||
|
var tha = document.createElement("th");
|
||||||
|
tha.innerText = "Author";
|
||||||
|
var thl = document.createElement("th");
|
||||||
|
thl.innerText = "Last Update";
|
||||||
|
thtr.appendChild(document.createElement("th"));
|
||||||
|
for(var i = 0; i < all_feeds.length; i++) {
|
||||||
|
if(all_feeds[i].source == source) {
|
||||||
|
table.appendChild(createFeedTableRow(all_feeds[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sourceContainer.appendChild(table);
|
||||||
|
var sourceShow = document.createElement("a");
|
||||||
|
sourceShow.style["color"] = "blue";
|
||||||
|
sourceShow.style["cursor"] = "pointer";
|
||||||
|
sourceShow.innerText = "v";
|
||||||
|
sourceShow.onclick = function() {
|
||||||
|
if(table.classList.contains("hidden")) {
|
||||||
|
sourceShow.innerText = "^";
|
||||||
|
} else {
|
||||||
|
sourceShow.innerText = "v";
|
||||||
|
}
|
||||||
|
table.classList.toggle("hidden");
|
||||||
|
}
|
||||||
|
sourceTitle.appendChild(sourceShow);
|
||||||
|
sourceContainer.style["margin-bottom"] = "2em";
|
||||||
|
|
||||||
|
return sourceContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFeedTableRow(feedItem) {
|
||||||
|
var row = document.createElement("tr");
|
||||||
|
var chkCell = document.createElement("td");
|
||||||
|
var chkBox = document.createElement("input");
|
||||||
|
chkBox.setAttribute("type", "checkbox");
|
||||||
|
chkBox.setAttribute("data-slug", feedItem["source"]+";"+feedItem["slug"]);
|
||||||
|
chkBox.onclick = function() {
|
||||||
|
toggleSubscription(this.getAttribute("data-slug"));
|
||||||
|
}
|
||||||
|
if(user_feeds.indexOf(feedItem["source"]+";"+feedItem["slug"]) >= 0) {
|
||||||
|
chkBox.checked = true;
|
||||||
|
}
|
||||||
|
chkCell.append(chkBox);
|
||||||
|
row.append(chkCell);
|
||||||
|
var nameCell = document.createElement("td");
|
||||||
|
nameCell.innerText = feedItem["name"];
|
||||||
|
row.append(nameCell);
|
||||||
|
var authCell = document.createElement("td");
|
||||||
|
authCell.innerText = feedItem["author"];
|
||||||
|
row.append(authCell);
|
||||||
|
var updCell = document.createElement("td");
|
||||||
|
updCell.innerText = feedItem["last_update"];
|
||||||
|
row.append(updCell);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSubscription(slug) {
|
||||||
|
if(user_feeds.indexOf(slug) >= 0) {
|
||||||
|
user_feeds.splice(user_feeds.indexOf(slug), 1);
|
||||||
|
} else {
|
||||||
|
user_feeds.push(slug);
|
||||||
|
}
|
||||||
|
updateSubscriptionValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSubRow(slug) {
|
||||||
|
return document.getElementById("row_"+slug.replace(";","_"));
|
||||||
|
}
|
||||||
|
function updateSubscriptionTable() {
|
||||||
|
for(var i = 0; i < user_feeds.length; i++) {
|
||||||
|
var row = getSubRow(user_feeds[i]);
|
||||||
|
if(row) {
|
||||||
|
row.getElementsByTagName("input")[0].checked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSubscriptionValue() {
|
||||||
|
document.getElementById("user_feeds").value = user_feeds;
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
|
updateSubscriptionTable();
|
||||||
|
</script>
|
Loading…
Reference in New Issue
Block a user