Compare commits

...

3 Commits

Author SHA1 Message Date
Brian Buller 3fbcfdcb5d Commiting changes long overdue 2020-09-28 13:03:57 -05:00
Brian Buller 8cc411b89d Progress 2019-01-13 08:22:29 -06:00
Brian Buller e0f25b8529 Started Plugin Rewrite 2019-01-10 10:39:42 -06:00
37 changed files with 2347 additions and 781 deletions

566
assets.go Normal file
View 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: `
H4sIAAAAAAAC/7RXW2vcOBR+n18hCIWkVK4nzTSJA2GhF/Zh96mPpQ+yJdsismRkeTJpyX9fdLMlW54u
CwsJjHUuOpfvXNSqjoFfOwA6ymFLaNOqAuzz/M3D7nW3KwV+MdRKMCELcHF7e/uwxYwMpyInBTGphESK
Cl4ALjgxdEyPWSW4IlwZzh5hTHlTgP2hP6W1TheXDFVPk5YWsdqoeKZYtQU4OAuyliBMpPUHyYZyyEit
CpBbco3gtSHWgis40J+kANeks8Re0g7J2N08v70jtxEdlo1hKVH11EgxcgxT3ERKISNdnz/neZ4H1DOa
Al7tr+h1KBGrKWE48G62fkd5P6qspozEsc0OpAPZR80GAKZDz9BLAShnlBNYMqGjCkApJCayAPv+BAbB
KAYXVVVZygkOLcLiWUsNRIHccH3oT+ACYzxLQ4kwHYcC3OhsvnqTJOJPlDfQfMVJs1lfSB/C08iiT58+
PXiAIUYbXoCKcEWkvS3rxJFAWgluwz7KQceSD1ASnWrDddERPoZWeJx53N04q3oxUItfSRhS9EgeolQV
4GJ/v7/b3+njZ1I+UQXFkciaiWc4VFIwZuKvxFi1wdVZP0oC9U+owUq5xcBGZlJiXGiWp1RZxlnfL5I+
ZXujRAF4bqkicOhRRfThs0R9aIO5vhyVciFeGB2HDZWDYKMyp0r0BXCBlTbO7stVqWHYx2fSNwJ76lw1
Jj6c7TPOTqGU6KJULxBnL53w5TQv3NUNC1HumsrksY9Y2t/YR3OPx1Wi2D2Otm5+HNlWnwhETV8Ukv7U
YsxB0MhNsDwVwB7bgumaTLVjV3JELWPQe30mTquGrM+i2tERn/RsxWvC3lJdwqkvX74EYLZpus5dU8nc
cdzlDp5qsBt1yJgEB4rJsB4QhxT0FqJHIhWtEAuljXGxsAVeIM2QbMhy7OzvPLmkDVybvZ8c1gbOk9U1
Pn2oyZkxdUU2p4ZuO+SKIWicjgUiB5cUGwDe+TXFW4xGJYxChUpG/MVKrxCPCp+zwAr4xDreVW4NF1C1
ENbfdBXbKWGAojum7VZwnsZnhu3cTr9+/brWoUTTMAJrKoelARYw0wiL24zFQ4rlNXlBRzF2U3wxGuP+
aIELo3npDtfhSF3E0NoRLX/ejxTHf7EoMqnAdNDpxVs5crPff97f37t+KTBicFpTFyW499hxfLoTMmRZ
j3SgJWVUvRSgpRgTvt3P3RIZtHP3e2OJ8N/pUvoJKcfkZNjydA+UTYku83fA/WWHq4Qbj5geFz3jjf4P
DLvzPdsz5P3JFWoyzAb4yeXLmxoj0g/n1Sq/UemYHi9IV9KmIZzgb5UkhH9rXUH7ja0X1Idqy8Qzqh5p
1/yK5lQ4uVyGpsmlp2Y9MjYY+eUUfJPI8e83nBgtSQiEPXPKh8fFPs/3QYXUQnbAbM/ffZX8KLhQl9/V
S09+XGX+VNcSNLD27WMzwf7zcDjom+ymbuaUDDemm9xPoT86gikCl/qJNhFJd2W456V6vWut4xmGsKYn
Yp4Qr7tJz2rNTKxdK/Zp7VgIUE4VRewhss0ZYpSslsWUCwmDF2nWlWF7jjuwyoP36KJLTtoNo4EhQ0Pr
WMP36c1ZK0LgpT2cdAdv0t/MwWgSOsgsdA1jVZFh2NZ2fX2P6vttba+73fu34Fun1w5bvQN4+94DzdUj
4hhczpX84aAfNldutTza+tCpl4JBfX8PGCoJy/yZ+XI2rtcnbQYAmeDsBc5LWhJunm3o/JoUsGFSo5HN
ChVBHewQRw3pCFcOy8PajsiMDanHcE7GSLKrj33lTQFVSCq9RiFlg/sOzK+CrQDPRf0/RjgZujnCIR7+
nC1WAvxNMB27f2/7AjTg5s54BOwNTttvIBe0ubuEzo83ocq/bONcqwz1BCL/BAAA///ACu/RdBMAAA==
`,
},
"/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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

File diff suppressed because one or more lines are too long

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
View 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/

View File

@ -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
}

View File

@ -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

View File

@ -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
View 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
View 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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
View File

@ -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))
}
}

View File

@ -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)

View File

@ -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
View 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)
}
}
}

View File

@ -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
View 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
}

View File

@ -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
View 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
}

View File

@ -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

View 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
}

View File

@ -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
View 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

Binary file not shown.

BIN
plugins/plugin_gocomics.so Normal file

Binary file not shown.

BIN
plugins/plugin_xkcd.so Normal file

Binary file not shown.

11
templates/footer.html Normal file
View 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
View 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 }}

View File

@ -0,0 +1,6 @@
</div>
{{ range $i, $v := .Scripts }}
<script src="{{ $v }}"></script>
{{ end }}
</body>
</html>

25
templates/htmlheader.html Normal file
View 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
View 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
View 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>

View 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
View 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>