Initial Commit

This commit is contained in:
Brian Buller 2018-04-26 06:53:03 -05:00
parent f34d2d90d0
commit 5b76117cf8
16 changed files with 1417 additions and 21 deletions

31
.gitignore vendored
View File

@ -1,26 +1,15 @@
# ---> Go # ---> Go
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe *.exe
*.test *.test
*.prof *.prof
# Binary
ribbit
# Database
ribbit.db
# Example Feeds
rssfeed.xml
# vim-restconsole
console.rest

16
endpoints_api.go Normal file
View File

@ -0,0 +1,16 @@
package main
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
)
func handleApiCall(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/json")
vars := mux.Vars(req)
_ = vars
fmt.Fprint(w, req.Header)
}

91
endpoints_api_comics.go Normal file
View File

@ -0,0 +1,91 @@
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:
http.Error(w, "You did a bad", 400)
return
}
default:
http.Error(w, "You did a bad", 400)
return
}
}
func handleApiListComics(w http.ResponseWriter) {
var res []byte
var err error
if res, err = json.Marshal(m.Comics); err != nil {
http.Error(w, "I did a bad", 500)
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 {
http.Error(w, "I did a bad", 500)
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 {
http.Error(w, "You did a bad", 400)
return
}
if c, err = m.GetComic(pts[0], pts[1]); err != nil {
http.Error(w, err.Error(), 400)
return
}
var res []byte
if res, err = json.Marshal(c); err != nil {
http.Error(w, err.Error(), 400)
return
}
fmt.Fprint(w, string(res))
return
}

122
endpoints_api_users.go Normal file
View File

@ -0,0 +1,122 @@
package main
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"github.com/gorilla/mux"
)
func handleApiUsersCall(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/json")
vars := mux.Vars(req)
uid, uidok := vars["uid"]
fn, fnok := vars["function"]
switch req.Method {
case "GET":
if !uidok {
handleApiListUsers(w)
return
}
switch fn {
default:
handleApiShowUser(uid, w)
}
case "POST", "PUT":
if !uidok {
http.Error(w, "You did a bad", 400)
return
}
if !fnok {
// Creating a new user
var u *User
var err error
if u, err = m.GetUserByName(uid); err == nil {
http.Error(w, "You did a bad", 400)
return
}
u = NewUser(uid)
m.SaveUser(u)
m.LoadUsers()
handleApiShowUser(u.Uuid, w)
return
}
switch fn {
case "slugs":
slug, slugok := vars["slug"]
if !slugok {
http.Error(w, "You did a bad", 400)
return
}
handleApiSubUser(uid, slug, w)
default:
http.Error(w, "You did a bad", 400)
return
}
default:
http.Error(w, "You did a bad", 400)
return
}
}
func handleApiListUsers(w http.ResponseWriter) {
var res []byte
var err error
if res, err = json.Marshal(m.Users); err != nil {
http.Error(w, "I did a bad", 500)
return
}
fmt.Fprint(w, string(res))
}
func handleApiShowUser(uid string, w http.ResponseWriter) {
var err error
var u *User
if u, err = m.GetUser(uid); err != nil {
http.Error(w, err.Error(), 400)
return
}
var res []byte
if res, err = json.Marshal(u); err != nil {
http.Error(w, err.Error(), 400)
return
}
fmt.Fprint(w, string(res))
return
}
func handleApiSubUser(uid string, slug string, w http.ResponseWriter) {
fmt.Println("Sub User", uid, slug)
var u *User
var err error
if u, err = m.GetUser(uid); err != nil {
http.Error(w, err.Error(), 400)
return
}
pts := strings.Split(slug, ";")
if len(pts) != 2 {
http.Error(w, err.Error(), 400)
return
}
_, err = m.GetComic(pts[0], pts[1])
if err != nil {
http.Error(w, err.Error(), 400)
return
}
u.SubSlugs = append(u.SubSlugs, slug)
err = m.SaveUser(u)
if err != nil {
http.Error(w, err.Error(), 400)
return
}
m.LoadUsers()
handleApiShowUser(u.Uuid, w)
}

39
endpoints_public.go Normal file
View File

@ -0,0 +1,39 @@
package main
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
)
func handleRequest(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
var fOk, uidOk bool
var f, uid string
f, fOk = vars["function"]
uid, uidOk = vars["uid"]
if !fOk || !uidOk {
// Not sure what you want me to do here, Hoss.
http.Error(w, "You did a bad", 400)
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)
}

68
helper_dilbert.go Normal file
View File

@ -0,0 +1,68 @@
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=\"" + src + "\" />", nil
}

98
helper_gocomics.go Normal file
View File

@ -0,0 +1,98 @@
package main
import (
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/PuerkitoBio/goquery"
)
func downloadGoComicsList() []Comic {
var ret []Comic
lstUrl := "http://www.gocomics.com/comics/a-to-z"
res, err := http.Get(lstUrl)
if err != nil {
return ret
}
defer res.Body.Close()
if res.StatusCode != 200 {
return ret
}
// Load the HTML document
doc, err := goquery.NewDocumentFromReader(res.Body)
if err != nil {
return ret
}
doc.Find("a.amu-media-item-link").Each(func(i int, s *goquery.Selection) {
// For each item found, get the band and title
slug, exists := s.Attr("href")
if exists {
pts := strings.Split(slug, "/")
if len(pts) > 2 {
slug = pts[1]
}
name := s.Find("h4.media-heading").Text()
author := s.Find("h6.media-subheading").Text()
author = strings.TrimPrefix(author, "By ")
ret = append(ret, *NewComic(slug, name, author, "gocomics"))
}
})
return ret
}
func getGoComicsRssItem(slug string) (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(
"http://www.gocomics.com/%s/%04d/%02d/%02d",
slug,
date.Year(),
date.Month(),
date.Day(),
)
}
func getGoComicsFeedDesc(slug string, date time.Time) (string, error) {
res, err := http.Get(getGoComicsComicUrl(slug, 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("picture.item-comic-image>img").Attr("src")
if !exists {
return "", errors.New("Couldn't find image source")
}
return "<img src=\"" + src + "\" />", nil
}

74
helper_xkcd.go Normal file
View File

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

102
helpers.go Normal file
View File

@ -0,0 +1,102 @@
package main
import (
"errors"
"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://comics.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 {
for i := range sl {
if sl[i] == st {
return sl
}
}
return append(sl, st)
}

169
main.go Normal file
View File

@ -0,0 +1,169 @@
package main
import (
"fmt"
"log"
"net/http"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"time"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"github.com/justinas/alice"
)
const AppName = "ribbit"
const DbName = AppName + ".db"
var sessionStore *sessions.CookieStore
var r *mux.Router
var m *model
var scheduler *JobScheduler
func main() {
var err error
if m, err = NewModel(); err != nil {
errorExit("Unable to initialize Model: " + err.Error())
}
if len(os.Args) > 2 {
key, val := os.Args[1], os.Args[2]
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":
//d, _ := getXKCDFeedDesc(time.Now())
fmt.Println(buildRssFeed("09af5fda-43dc-416e-93ad-cc050e0c098a"))
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:
errorExit("Unknown argument")
}
}
r = mux.NewRouter()
r.StrictSlash(true)
//r.PathPrefix("/assets/").Handler(http.FileServer())
pub := r.PathPrefix("/").Subrouter()
pub.HandleFunc("/", handleRequest)
pub.HandleFunc("/api", handleApiCall)
pub.HandleFunc("/api/users", handleApiUsersCall)
pub.HandleFunc("/api/users/{uid}", handleApiUsersCall)
pub.HandleFunc("/api/users/{uid}/{function}", handleApiUsersCall)
pub.HandleFunc("/api/users/{uid}/{function}/{slug}", handleApiUsersCall)
pub.HandleFunc("/api/comics", handleApiComicsCall)
pub.HandleFunc("/api/comics/{cid}", handleApiComicsCall)
pub.HandleFunc("/api/comics/{cid}/{function}", handleApiComicsCall)
pub.HandleFunc("/{function}", handleRequest)
pub.HandleFunc("/{function}/{uid}", handleRequest)
pub.HandleFunc("/{function}/{uid}/{subfunc}", handleRequest)
pub.HandleFunc("/{function}/{uid}/{subfunc}/{slug}", handleRequest)
http.Handle("/", r)
chain := alice.New(loggingHandler).Then(r)
// Save changes to the DB every 5 minutes
go func() {
for {
if m.Site.LastSave.IsZero() || (time.Now().Day() != m.Site.LastSave.Day() && time.Now().Hour() == 2) {
fmt.Println("Updating GoComics List...")
comics := downloadComicsList()
for _, c := range comics {
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.")
}
time.Sleep(time.Minute)
}
}()
// Set up a channel to intercept Ctrl+C for graceful shutdowns
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
// Save the changes when the app quits
fmt.Println("\nFinishing up...")
m.saveChanges()
os.Exit(0)
}()
fmt.Printf("Listening on port %d\n", m.Site.Port)
log.Fatal(http.ListenAndServe("127.0.0.1:"+strconv.Itoa(m.Site.Port), chain))
}
func loggingHandler(h http.Handler) http.Handler {
return handlers.LoggingHandler(os.Stdout, h)
}
func done() {
os.Exit(0)
}
func errorExit(msg string) {
fmt.Println(msg)
os.Exit(1)
}
func assertError(err error) {
if err != nil {
panic(err)
}
}

100
model.go Normal file
View File

@ -0,0 +1,100 @@
package main
import (
"errors"
"fmt"
"strings"
"time"
"github.com/br0xen/boltease"
)
type model struct {
bolt *boltease.DB
dbFileName string
Users []User
Comics []Comic
Site *SiteData
}
func NewModel() (*model, error) {
var err error
m := new(model)
m.dbFileName = DbName
m.bolt, err = boltease.Create(m.dbFileName, 0600, nil)
if err != nil {
return nil, err
}
if err = m.initDB(); err != nil {
return nil, errors.New("Unable to initialzie DB: " + err.Error())
}
m.LoadSiteData()
if err = m.LoadUsers(); err != nil {
return nil, err
}
if err = m.LoadComics(); err != nil {
return nil, err
}
return m, nil
}
func (m *model) initDB() error {
var err error
if err = m.bolt.OpenDB(); err != nil {
return err
}
defer m.bolt.CloseDB()
if err = m.bolt.MkBucketPath([]string{"site"}); err != nil {
return err
}
if err = m.bolt.MkBucketPath([]string{"users"}); err != nil {
return err
}
if err = m.bolt.MkBucketPath([]string{"comics"}); err != nil {
return err
}
return nil
}
func (m *model) saveChanges() {
m.Site.LastSave = time.Now()
m.SaveSite()
//m.SaveAllComics(m.Comics)
m.SaveAllUsers(m.Users)
}
func (m *model) UpdateAllUserFeeds() {
var allSubs []string
for _, usr := range m.Users {
// Pull all user subs
for _, sub := range usr.SubSlugs {
allSubs = addStringIfUnique(sub, allSubs)
}
}
// So we have allSubs which contains all subscribed comics for all users
for _, sub := range allSubs {
fmt.Println("Updating Comic: " + sub)
pts := strings.Split(sub, ";")
if len(pts) != 2 {
continue
}
c, err := m.GetComic(pts[0], pts[1])
if err != nil {
fmt.Println(sub, ":", err)
continue
}
if err = c.Update(); err != nil {
fmt.Println(sub, ":", err.Error())
continue
}
if err = m.SaveComic(c); err != nil {
fmt.Println(sub, ":", err.Error())
continue
}
}
}

194
model_comics.go Normal file
View File

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

86
model_site.go Normal file
View File

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

143
model_user.go Normal file
View File

@ -0,0 +1,143 @@
package main
import (
"errors"
"strings"
"github.com/pborman/uuid"
)
type User struct {
Username string `json:username`
Uuid string `json:uuid`
SubSlugs []string `json:subs`
}
func NewUser(un string) *User {
u := new(User)
u.Username = un
u.Uuid = uuid.New()
return u
}
func (u *User) UpdateFeed() error {
for _, slug := range u.SubSlugs {
pts := strings.Split(slug, ";")
if len(pts) != 2 {
continue
}
if comic, err := m.GetComic(pts[0], pts[1]); err == nil {
comic.Update()
}
}
return nil
}
func (m *model) SaveUser(u *User) error {
var err error
if err = m.bolt.OpenDB(); err != nil {
return err
}
defer m.bolt.CloseDB()
bkt := []string{"users", u.Uuid}
if err = m.bolt.MkBucketPath(bkt); err != nil {
return err
}
if err = m.bolt.SetValue(bkt, "username", u.Username); err != nil {
return err
}
var newSubs []string
for _, v := range u.SubSlugs {
if strings.TrimSpace(v) != "" {
newSubs = append(newSubs, v)
}
}
slugs := strings.Join(newSubs, ",")
if err = m.bolt.SetValue(bkt, "subs", slugs); err != nil {
return err
}
return nil
}
func (m *model) GetUser(uid string) (*User, error) {
var err error
if err = m.bolt.OpenDB(); err != nil {
return nil, err
}
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 {
return nil, err
}
var subs string
if subs, err = m.bolt.GetValue(bkt, "subs"); err != nil {
return nil, err
}
ret.SubSlugs = strings.Split(subs, ",")
return ret, nil
}
func (m *model) SaveAllUsers(users []User) {
var err error
if err = m.bolt.OpenDB(); err != nil {
return
}
defer m.bolt.CloseDB()
for i := range users {
m.SaveUser(&users[i])
}
}
func (m *model) LoadUsers() error {
m.Users = m.GetAllUsers()
return nil
}
func (m *model) GetAllUsers() []User {
var err error
var ret []User
if err = m.bolt.OpenDB(); err != nil {
return ret
}
defer m.bolt.CloseDB()
uids := m.GetUserIdList()
for _, uid := range uids {
if u, e := m.GetUser(uid); e == nil {
ret = append(ret, *u)
}
}
return ret
}
func (m *model) GetUserByName(nm string) (*User, error) {
var err error
if err = m.bolt.OpenDB(); err != nil {
return nil, err
}
defer m.bolt.CloseDB()
usrids := m.GetUserIdList()
for i := range usrids {
bkt := []string{"users", usrids[i]}
var tstuname string
if tstuname, _ = m.bolt.GetValue(bkt, "username"); tstuname == nm {
// Found it
return m.GetUser(usrids[i])
}
}
return nil, errors.New("No user with username " + nm + " found")
}
func (m *model) GetUserIdList() []string {
var ret []string
var err error
if err = m.bolt.OpenDB(); err != nil {
return ret
}
defer m.bolt.CloseDB()
bkt := []string{"users"}
ret, _ = m.bolt.GetBucketList(bkt)
return ret
}

54
page_session.go Normal file
View File

@ -0,0 +1,54 @@
package main
import (
"fmt"
"net/http"
"github.com/gorilla/sessions"
)
// This is basically a convenience struct for
// easier session management (hopefully ;)
type pageSession struct {
session *sessions.Session
req *http.Request
w http.ResponseWriter
}
func (p *pageSession) getStringValue(key string) (string, error) {
val := p.session.Values[key]
var retVal string
var ok bool
if retVal, ok = val.(string); !ok {
return "", fmt.Errorf("Unable to create string from %s", key)
}
return retVal, nil
}
func (p *pageSession) setStringValue(key, val string) {
p.session.Values[key] = val
p.session.Save(p.req, p.w)
}
func (p *pageSession) setFlashMessage(msg, status string) {
p.setStringValue("flash_message", msg)
p.setStringValue("flash_status", status)
}
func (p *pageSession) getFlashMessage() (string, string) {
var err error
var msg, status string
if msg, err = p.getStringValue("flash_message"); err != nil {
return "", "hidden"
}
if status, err = p.getStringValue("flash_status"); err != nil {
return "", "hidden"
}
p.setFlashMessage("", "hidden")
return msg, status
}
func (p *pageSession) expireSession() {
p.session.Options.MaxAge = -1
p.session.Save(p.req, p.w)
}

51
scheduler.go Normal file
View File

@ -0,0 +1,51 @@
package main
import "time"
const (
RESULT_OK = 0
RESULT_ERR = 1
)
type JobScheduler struct {
Jobs []Job
}
func NewJobScheduler() *JobScheduler {
return new(JobScheduler)
}
func (s *JobScheduler) AddJob(j *Job) {
s.Jobs = append(s.Jobs, *j)
}
func (s *JobScheduler) Run() {
for _, j := range s.Jobs {
if j.ShouldRunNow() {
j.Run()
}
}
}
type Job struct {
freq time.Duration
lastRun time.Time
lastResult int
action func() int
}
func NewJob(a func() int, f time.Duration) *Job {
j := new(Job)
j.action = a
j.freq = f
return j
}
func (j *Job) ShouldRunNow() bool {
return time.Now().After(j.lastRun.Add(j.freq))
}
func (j *Job) Run() {
j.lastResult = j.action()
j.lastRun = time.Now()
}