Started Plugin Rewrite

This commit is contained in:
Brian Buller 2019-01-10 10:39:42 -06:00
parent 5096f3522e
commit e0f25b8529
28 changed files with 1697 additions and 681 deletions

428
assets.go Normal file
View File

@ -0,0 +1,428 @@
// 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/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/user_dashboard.html": {
local: "templates/user_dashboard.html",
size: 0,
modtime: 1547138339,
compressed: `
H4sIAAAAAAAC/wEAAP//AAAAAAAAAAA=
`,
},
"/": {
isDir: true,
local: "",
},
"/assets": {
isDir: true,
local: "assets",
},
"/assets/css": {
isDir: true,
local: "assets/css",
},
"/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) { }

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

@ -8,31 +8,30 @@ import (
) )
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)
}
}
func handleRssFeed(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
var uid string
var uidOk bool
if uid, uidOk = vars["uid"]; !uidOk {
userError(w)
return 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") w.Header().Set("Content-Type", "application/xml")
v, err := buildRssFeed(uid) v, err := buildRssFeed(uid)
if err != nil { if err != nil {
http.Error(w, err.Error(), 400) userError(w)
return return
} }
fmt.Fprint(w, v) fmt.Fprint(w, v)

69
endpoints_user.go Normal file
View File

@ -0,0 +1,69 @@
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", w)
return
}
page.SubTitle = "dashboard"
page.show("user_dashboard.html", w)
}
func handleUserLoginForm(page *pageData) {
page.SubTitle = "ribbit login"
page.show("login.html", page.session.w)
}
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 {
fmt.Println("Doing Login", uid, password)
if strings.TrimSpace(uid) != "" && strings.TrimSpace(password) != "" {
return m.checkCredentials(uid, password)
}
return errors.New("Invalid Credentials")
}

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 {

233
main.go
View File

@ -1,7 +1,11 @@
package main package main
//go:generate esc -o assets.go assets templates
import ( import (
"bufio"
"fmt" "fmt"
"html/template"
"log" "log"
"net/http" "net/http"
"os" "os"
@ -9,17 +13,42 @@ import (
"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"
// 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{}
}
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,72 +61,24 @@ 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", handleApiCall)
pub.HandleFunc("/api/users", handleApiUsersCall) pub.HandleFunc("/api/users", handleApiUsersCall)
pub.HandleFunc("/api/users/{uid}", handleApiUsersCall) pub.HandleFunc("/api/users/{uid}", handleApiUsersCall)
@ -106,15 +87,15 @@ func main() {
pub.HandleFunc("/api/comics", handleApiComicsCall) pub.HandleFunc("/api/comics", handleApiComicsCall)
pub.HandleFunc("/api/comics/{cid}", handleApiComicsCall) pub.HandleFunc("/api/comics/{cid}", handleApiComicsCall)
pub.HandleFunc("/api/comics/{cid}/{function}", handleApiComicsCall) pub.HandleFunc("/api/comics/{cid}/{function}", handleApiComicsCall)
pub.HandleFunc("/{function}", handleRequest) */
pub.HandleFunc("/{function}/{uid}", handleRequest) pub.HandleFunc("/rss/{uid}", handleRssFeed)
pub.HandleFunc("/{function}/{uid}/{subfunc}", handleRequest) pub.HandleFunc("/user/{function}", handleUserRequest)
pub.HandleFunc("/{function}/{uid}/{subfunc}/{slug}", handleRequest)
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() { go func() {
for { for {
if m.Site.LastSave.IsZero() || (time.Now().Day() != m.Site.LastSave.Day() && time.Now().Hour() == 2) { if m.Site.LastSave.IsZero() || (time.Now().Day() != m.Site.LastSave.Day() && time.Now().Hour() == 2) {
@ -132,6 +113,7 @@ func main() {
time.Sleep(time.Minute) 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)
@ -148,10 +130,97 @@ func main() {
log.Fatal(http.ListenAndServe("127.0.0.1:"+strconv.Itoa(m.Site.Port), chain)) log.Fatal(http.ListenAndServe("127.0.0.1:"+strconv.Itoa(m.Site.Port), chain))
} }
func buildRssFeed(uid string) (string, error) {
return uid, nil
}
func loggingHandler(h http.Handler) http.Handler { 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 (p *pageData) show(tmplName string, w http.ResponseWriter) error {
for _, tmpl := range []string{
"htmlheader.html",
"header.html",
tmplName,
"footer.html",
"htmlfooter.html",
} {
if err := outputTemplate(tmpl, p, w); err != nil {
fmt.Printf("%s\n", err)
return err
}
}
return nil
}
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 +235,41 @@ 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())
}
}

View File

@ -14,7 +14,7 @@ type model struct {
dbFileName string dbFileName string
Users []User Users []User
Comics []Comic Feeds []Feed
Site *SiteData Site *SiteData
} }
@ -31,11 +31,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
} }
@ -55,16 +58,15 @@ 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) saveChanges() { func (m *model) saveChanges() {
m.Site.LastSave = time.Now() m.Site.SaveToDB()
m.SaveSite() //m.SaveAllFeeds(m.Feeds)
//m.SaveAllComics(m.Comics)
m.SaveAllUsers(m.Users) m.SaveAllUsers(m.Users)
} }
@ -76,14 +78,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 +94,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 +102,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)
}
}
}

209
model_feeds.go Normal file
View File

@ -0,0 +1,209 @@
package main
import (
"time"
)
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{"feed", 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.bolt.OpenDB(); err != nil {
return err
}
defer m.bolt.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.bolt.OpenDB(); err != nil {
return nil, err
}
defer m.bolt.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(feeds []Feed) {
var err error
if err = m.bolt.OpenDB(); err != nil {
return
}
defer m.bolt.CloseDB()
for i := range feeds {
m.SaveFeed(&feeds[i])
}
}
// Get all feeds from the db
func (m *model) GetAllFeeds() []Feed {
var ret []Feed
var err error
if err = m.bolt.OpenDB(); err != nil {
return ret
}
defer m.bolt.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.bolt.OpenDB(); err != nil {
return err
}
defer m.bolt.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.bolt.OpenDB(); err != nil {
return err
}
defer s.m.bolt.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.bolt.OpenDB(); err != nil {
return err
}
defer s.m.bolt.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,6 +36,7 @@ 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
@ -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,7 +71,39 @@ func (m *model) SaveUser(u *User) error {
return nil return nil
} }
func (m *model) isValidUser(uid string) bool {
var err error
if err = m.bolt.OpenDB(); err != nil {
return false
}
defer m.bolt.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) { func (m *model) GetUser(uid string) (*User, error) {
if uid == "" {
return nil, errors.New("No user id given")
}
var err error var err error
if err = m.bolt.OpenDB(); err != nil { if err = m.bolt.OpenDB(); err != nil {
return nil, err return nil, err
@ -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
@ -143,3 +188,50 @@ func (m *model) GetUserIdList() []string {
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.bolt.OpenDB(); err != nil {
return err
}
defer m.bolt.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.bolt.OpenDB(); err != nil {
return err
}
defer m.bolt.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
}

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

@ -1,5 +1,8 @@
package main package main
import (
"C"
)
import ( import (
"errors" "errors"
"fmt" "fmt"
@ -9,33 +12,33 @@ import (
"github.com/PuerkitoBio/goquery" "github.com/PuerkitoBio/goquery"
) )
func downloadDilbertList() []Comic { func getFeedList() []Feed {
var ret []Comic var ret []Feed
ret = append(ret, *NewComic("dilbert", "Dilbert", "Scott Adams", "dilbert")) ret = append(ret, *NewFeed("dilbert", "Dilbert", "Scott Adams", "dilbert"))
return ret return ret
} }
func getDilbertRssItem(slug string) (string, error) { func getRssItem(slug string) (string, error) {
desc, err := getDilbertFeedDesc(time.Now()) desc, err := getFeedDesc(time.Now())
if err != nil { if err != nil {
return "", err return "", err
} }
comic, err := m.GetComic(SRC_DILBERT, slug) feed, err := m.GetFeed(SRC_DILBERT, slug)
if err != nil { if err != nil {
return "", err return "", err
} }
desc = "<![CDATA[" + desc + "]]>" desc = "<![CDATA[" + desc + "]]>"
ret := " <item>\n" ret := " <item>\n"
ret += " <title>" + comic.Name + "</title>\n" ret += " <title>" + feed.Name + "</title>\n"
ret += " <pubDate>" + comic.LastUpdate.Format(time.RFC1123Z) + "</pubDate>\n" ret += " <pubDate>" + feed.LastUpdate.Format(time.RFC1123Z) + "</pubDate>\n"
ret += " <guid>dilbert;" + slug + ";" + comic.LastUpdate.Format(time.RFC1123Z) + "</guid>\n" ret += " <guid>dilbert;" + slug + ";" + feed.LastUpdate.Format(time.RFC1123Z) + "</guid>\n"
ret += " <link>" + getDilbertComicUrl(time.Now()) + "</link>\n" ret += " <link>" + getFeedUrl(time.Now()) + "</link>\n"
ret += " <description>" + desc + "</description>\n" ret += " <description>" + desc + "</description>\n"
ret += " </item>\n" ret += " </item>\n"
return ret, nil return ret, nil
} }
func getDilbertComicUrl(date time.Time) string { func getFeedUrl(date time.Time) string {
return fmt.Sprintf( return fmt.Sprintf(
"http://dilbert.com/strip/%4d-%02d-%02d", "http://dilbert.com/strip/%4d-%02d-%02d",
date.Year(), date.Year(),
@ -44,8 +47,8 @@ func getDilbertComicUrl(date time.Time) string {
) )
} }
func getDilbertFeedDesc(date time.Time) (string, error) { func getFeedDesc(date time.Time) (string, error) {
res, err := http.Get(getDilbertComicUrl(date)) res, err := http.Get(getFeedUrl(date))
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"C"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
@ -10,8 +11,8 @@ import (
"github.com/PuerkitoBio/goquery" "github.com/PuerkitoBio/goquery"
) )
func downloadGoComicsList() []Comic { func downloadList() {
var ret []Comic var ret []Feed
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,14 +48,14 @@ 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")) ret = append(ret, *NewFeed(slug, name, author, "gocomics"))
} }
}) })
return ret return ret
} }
func getGoComicsRssItem(slug string) (string, error) { func getRssItem(slug string) (string, error) {
desc, err := getGoComicsFeedDesc(slug, time.Now()) desc, err := getFeedDesc(slug, time.Now())
if err != nil { if err != nil {
return "", err return "", err
} }
@ -73,7 +74,7 @@ func getGoComicsRssItem(slug string) (string, error) {
return ret, nil return ret, nil
} }
func getGoComicsComicUrl(slug string, date time.Time) string { func getFeedUrl(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,
@ -83,8 +84,8 @@ func getGoComicsComicUrl(slug string, date time.Time) string {
) )
} }
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)) res, err := http.Get(getFeedUrl(slug, date))
if err != nil { if err != nil {
return "", err return "", err
} }

84
plugin_src/plugin_xkcd.go Normal file
View File

@ -0,0 +1,84 @@
package main
import (
"C"
"errors"
"fmt"
"net/http"
"time"
"github.com/PuerkitoBio/goquery"
)
func getFeedList() []Feed {
var ret []Feed
ret = append(ret, *NewFeed("xkcd", "XKCD", "Randall Munroe", "xkcd"))
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(date 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(date time.Time) (string, error) {
res, err := http.Get(getUrl(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
}

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>

View File