diff --git a/.gitignore b/.gitignore
index d3beee5..28e1ea4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,26 +1,15 @@
# ---> 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
*.test
*.prof
+# Binary
+ribbit
+# Database
+ribbit.db
+
+# Example Feeds
+rssfeed.xml
+
+# vim-restconsole
+console.rest
diff --git a/endpoints_api.go b/endpoints_api.go
new file mode 100644
index 0000000..0162a56
--- /dev/null
+++ b/endpoints_api.go
@@ -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)
+}
diff --git a/endpoints_api_comics.go b/endpoints_api_comics.go
new file mode 100644
index 0000000..b8cdf22
--- /dev/null
+++ b/endpoints_api_comics.go
@@ -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
+}
diff --git a/endpoints_api_users.go b/endpoints_api_users.go
new file mode 100644
index 0000000..e6b7e27
--- /dev/null
+++ b/endpoints_api_users.go
@@ -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)
+}
diff --git a/endpoints_public.go b/endpoints_public.go
new file mode 100644
index 0000000..07c32f7
--- /dev/null
+++ b/endpoints_public.go
@@ -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)
+}
diff --git a/helper_dilbert.go b/helper_dilbert.go
new file mode 100644
index 0000000..59744ec
--- /dev/null
+++ b/helper_dilbert.go
@@ -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 = ""
+ ret := " - \n"
+ ret += " " + comic.Name + "\n"
+ ret += " " + comic.LastUpdate.Format(time.RFC1123Z) + "\n"
+ ret += " dilbert;" + slug + ";" + comic.LastUpdate.Format(time.RFC1123Z) + "\n"
+ ret += " " + getDilbertComicUrl(time.Now()) + "\n"
+ ret += " " + desc + "\n"
+ ret += "
\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 "", nil
+}
diff --git a/helper_gocomics.go b/helper_gocomics.go
new file mode 100644
index 0000000..33e1ef7
--- /dev/null
+++ b/helper_gocomics.go
@@ -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 = ""
+ ret := " - \n"
+ ret += " " + comic.Name + "\n"
+ ret += " " + comic.LastUpdate.Format(time.RFC1123Z) + "\n"
+ ret += " gocomics;" + slug + ";" + comic.LastUpdate.Format(time.RFC1123Z) + "\n"
+ ret += " " + getGoComicsComicUrl(slug, time.Now()) + "\n"
+ ret += " " + desc + "\n"
+ ret += "
\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 "", nil
+}
diff --git a/helper_xkcd.go b/helper_xkcd.go
new file mode 100644
index 0000000..48f3e6e
--- /dev/null
+++ b/helper_xkcd.go
@@ -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 = ""
+ ret := " - \n"
+ ret += " " + comic.Name + "\n"
+ ret += " " + comic.LastUpdate.Format(time.RFC1123Z) + "\n"
+ ret += " xkcd;" + slug + ";" + comic.LastUpdate.Format(time.RFC1123Z) + "\n"
+ ret += " " + getXKCDComicUrl(time.Now()) + "\n"
+ ret += " " + desc + "\n"
+ ret += "
\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 "
" + title + "
", nil
+}
diff --git a/helpers.go b/helpers.go
new file mode 100644
index 0000000..9150a80
--- /dev/null
+++ b/helpers.go
@@ -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{
+ "",
+ "",
+ " ",
+ " BCW Comic Feed",
+ " http://comics.bullercodeworks.com/edit/" + uid + "",
+ " Comic feed for " + usr.Username + "",
+ " en-us",
+ " " + time.Now().Format(time.RFC1123) + "",
+ " 40",
+ }
+
+ //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{
+ " ",
+ "",
+ }...)
+ 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)
+}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..d86b6ac
--- /dev/null
+++ b/main.go
@@ -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 \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)
+ }
+}
diff --git a/model.go b/model.go
new file mode 100644
index 0000000..f1d5437
--- /dev/null
+++ b/model.go
@@ -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
+ }
+ }
+}
diff --git a/model_comics.go b/model_comics.go
new file mode 100644
index 0000000..2dcfb63
--- /dev/null
+++ b/model_comics.go
@@ -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)
+ }
+ }
+}
diff --git a/model_site.go b/model_site.go
new file mode 100644
index 0000000..5c3d38d
--- /dev/null
+++ b/model_site.go
@@ -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
+}
diff --git a/model_user.go b/model_user.go
new file mode 100644
index 0000000..e92385c
--- /dev/null
+++ b/model_user.go
@@ -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
+}
diff --git a/page_session.go b/page_session.go
new file mode 100644
index 0000000..a5fb7cd
--- /dev/null
+++ b/page_session.go
@@ -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)
+}
diff --git a/scheduler.go b/scheduler.go
new file mode 100644
index 0000000..ee23f59
--- /dev/null
+++ b/scheduler.go
@@ -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()
+}