package main //go:generate esc -o assets.go assets templates import ( "bufio" "fmt" "html/template" "io/ioutil" "log" "net/http" "os" "os/signal" "plugin" "strconv" "strings" "syscall" "github.com/gorilla/handlers" "github.com/gorilla/mux" "github.com/gorilla/sessions" "github.com/justinas/alice" "golang.org/x/crypto/ssh/terminal" ) const AppName = "ribbit" const DbName = AppName + ".db" type menuItem struct { Label string Location string Icon string } 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()) } initialize() sessionStore = sessions.NewCookieStore([]byte(m.Site.sessionSecret)) for _, arg := range os.Args { switch arg { case "-dev": m.Site.DevMode = true fmt.Println("Running in Dev Mode") } } r = mux.NewRouter() r.StrictSlash(true) r.PathPrefix("/assets/").Handler(http.FileServer(FS(m.Site.DevMode))) pub := r.PathPrefix("/").Subrouter() pub.HandleFunc("/", handleUserRequest) /* 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("/rss/{uid}", handleRssFeed) pub.HandleFunc("/rss/{uid}/{slug}", handleRssFeed) pub.HandleFunc("/user/{function}", handleUserRequest) http.Handle("/", r) chain := alice.New(loggingHandler).Then(r) // Refresh the DB at 2 AM /* 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 initPageData(w http.ResponseWriter, req *http.Request) *pageData { if m.Site.DevMode { w.Header().Set("Cache-Control", "no-cache") } p := new(pageData) // Get session var err error var s *sessions.Session if s, err = sessionStore.Get(req, m.Site.sessionSecret); err != nil { fmt.Println("Session error... Recreating.") http.Error(w, err.Error(), 500) } p.session = new(pageSession) p.session.session = s p.session.req = req p.session.w = w // First check if we're logged in userId, _ := p.session.getStringValue("id") // With a valid account user, err := m.GetUser(userId) if err == nil { p.LoggedIn = true p.IsAdmin = user.IsAdmin } p.Site = m.Site p.SubTitle = "ribbit" p.Stylesheets = make([]string, 0, 0) p.Stylesheets = append(p.Stylesheets, "/assets/vendor/css/pure-min.css") p.Stylesheets = append(p.Stylesheets, "/assets/vendor/css/grids-responsive-min.css") p.Stylesheets = append(p.Stylesheets, "/assets/css/main.css") p.HeaderScripts = make([]string, 0, 0) p.Scripts = make([]string, 0, 0) p.FlashMessage, p.FlashClass = p.session.getFlashMessage() if p.FlashClass == "" { p.FlashClass = "hidden" } // Build the menu if p.LoggedIn { p.BottomMenu = append(p.BottomMenu, menuItem{"Logout", "/user/logout", "fa-sign-out"}) } else { if p.IsAdmin { p.BottomMenu = append(p.BottomMenu, menuItem{"Admin", "/admin", "fa-sign-in"}) } } return p } func outputTemplate(tmplName string, tmplData interface{}, w http.ResponseWriter) error { n := "/templates/" + tmplName l := template.Must(template.New("layout").Parse(FSMustString(m.Site.DevMode, n))) t := template.Must(l.Parse(FSMustString(m.Site.DevMode, n))) return t.Execute(w, tmplData) } // redirect can be used only for GET redirects func redirect(url string, w http.ResponseWriter, req *http.Request) { http.Redirect(w, req, url, 303) } func done() { os.Exit(0) } func errorExit(msg string) { fmt.Println(msg) os.Exit(1) } func assertError(err error) { if err != nil { panic(err) } } func initialize() { // Check if we have an admin user already if !m.hasAdminUser() { // Nope, create one reader := bufio.NewReader(os.Stdin) fmt.Println("Create new Admin user") fmt.Print("Email: ") email, _ := reader.ReadString('\n') email = strings.TrimSpace(email) u := NewUser(email) var pw1, pw2 []byte for string(pw1) != string(pw2) || string(pw1) == "" { fmt.Print("Password: ") pw1, _ = terminal.ReadPassword(0) fmt.Println("") fmt.Print("Repeat Password: ") pw2, _ = terminal.ReadPassword(0) fmt.Println("") if string(pw1) != string(pw2) { fmt.Println("Entered Passwords don't match!") } } u.IsAdmin = true m.SaveUser(u) assertError(m.updateUserPassword(u.Uuid, string(pw1))) } if m.Site.sessionSecret == "" { reader := bufio.NewReader(os.Stdin) fmt.Println("A good session secret is like a good password") fmt.Print("Create New Session Secret: ") sessSc, _ := reader.ReadString('\n') sessSc = strings.TrimSpace(sessSc) m.Site.sessionSecret = sessSc assertError(m.Site.SaveToDB()) } // Load Feed Sources from Plugins files, err := ioutil.ReadDir("./plugins/") if err != nil { log.Fatal(err) } for _, f := range files { fmt.Print("Loading plugin ", f.Name(), "...") p, err := plugin.Open("./plugins/" + f.Name()) if err != nil { panic(err) } var feedSource *FeedSource if feedSource, err = LoadFeedPlugin(p); err != nil { fmt.Println(" ", err.Error()) continue } m.FeedSources = append(m.FeedSources, *feedSource) fmt.Printf("Done. (%d feeds)\n", len(feedSource.Feeds)) } }