From 5b76117cf8060a7953514b493b85b4931b588e2d Mon Sep 17 00:00:00 2001 From: Brian Buller Date: Thu, 26 Apr 2018 06:53:03 -0500 Subject: [PATCH] Initial Commit --- .gitignore | 31 +++---- endpoints_api.go | 16 ++++ endpoints_api_comics.go | 91 +++++++++++++++++++ endpoints_api_users.go | 122 +++++++++++++++++++++++++ endpoints_public.go | 39 ++++++++ helper_dilbert.go | 68 ++++++++++++++ helper_gocomics.go | 98 ++++++++++++++++++++ helper_xkcd.go | 74 +++++++++++++++ helpers.go | 102 +++++++++++++++++++++ main.go | 169 ++++++++++++++++++++++++++++++++++ model.go | 100 +++++++++++++++++++++ model_comics.go | 194 ++++++++++++++++++++++++++++++++++++++++ model_site.go | 86 ++++++++++++++++++ model_user.go | 143 +++++++++++++++++++++++++++++ page_session.go | 54 +++++++++++ scheduler.go | 51 +++++++++++ 16 files changed, 1417 insertions(+), 21 deletions(-) create mode 100644 endpoints_api.go create mode 100644 endpoints_api_comics.go create mode 100644 endpoints_api_users.go create mode 100644 endpoints_public.go create mode 100644 helper_dilbert.go create mode 100644 helper_gocomics.go create mode 100644 helper_xkcd.go create mode 100644 helpers.go create mode 100644 main.go create mode 100644 model.go create mode 100644 model_comics.go create mode 100644 model_site.go create mode 100644 model_user.go create mode 100644 page_session.go create mode 100644 scheduler.go 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() +}