2018-04-26 11:53:03 +00:00
|
|
|
package main
|
|
|
|
|
2019-01-10 16:39:42 +00:00
|
|
|
//go:generate esc -o assets.go assets templates
|
|
|
|
|
2018-04-26 11:53:03 +00:00
|
|
|
import (
|
2019-01-10 16:39:42 +00:00
|
|
|
"bufio"
|
2018-04-26 11:53:03 +00:00
|
|
|
"fmt"
|
2019-01-10 16:39:42 +00:00
|
|
|
"html/template"
|
2019-01-13 14:22:29 +00:00
|
|
|
"io/ioutil"
|
2018-04-26 11:53:03 +00:00
|
|
|
"log"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"os/signal"
|
2019-01-13 14:22:29 +00:00
|
|
|
"plugin"
|
2018-04-26 11:53:03 +00:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"syscall"
|
|
|
|
|
|
|
|
"github.com/gorilla/handlers"
|
|
|
|
"github.com/gorilla/mux"
|
|
|
|
"github.com/gorilla/sessions"
|
|
|
|
"github.com/justinas/alice"
|
2019-01-10 16:39:42 +00:00
|
|
|
"golang.org/x/crypto/ssh/terminal"
|
2018-04-26 11:53:03 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const AppName = "ribbit"
|
|
|
|
const DbName = AppName + ".db"
|
|
|
|
|
2019-01-10 16:39:42 +00:00
|
|
|
type menuItem struct {
|
|
|
|
Label string
|
|
|
|
Location string
|
|
|
|
Icon string
|
|
|
|
}
|
|
|
|
|
2018-04-26 11:53:03 +00:00
|
|
|
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())
|
|
|
|
}
|
|
|
|
|
2019-01-10 16:39:42 +00:00
|
|
|
initialize()
|
|
|
|
sessionStore = sessions.NewCookieStore([]byte(m.Site.sessionSecret))
|
2018-04-26 11:53:03 +00:00
|
|
|
|
2019-01-10 16:39:42 +00:00
|
|
|
for _, arg := range os.Args {
|
|
|
|
switch arg {
|
|
|
|
case "-dev":
|
|
|
|
m.Site.DevMode = true
|
|
|
|
fmt.Println("Running in Dev Mode")
|
2018-04-26 11:53:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
r = mux.NewRouter()
|
|
|
|
r.StrictSlash(true)
|
2019-01-10 16:39:42 +00:00
|
|
|
r.PathPrefix("/assets/").Handler(http.FileServer(FS(m.Site.DevMode)))
|
2018-04-26 11:53:03 +00:00
|
|
|
|
|
|
|
pub := r.PathPrefix("/").Subrouter()
|
2019-01-10 16:39:42 +00:00
|
|
|
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)
|
2020-09-28 18:03:57 +00:00
|
|
|
pub.HandleFunc("/rss/{uid}/{slug}", handleRssFeed)
|
2019-01-10 16:39:42 +00:00
|
|
|
pub.HandleFunc("/user/{function}", handleUserRequest)
|
2018-04-26 11:53:03 +00:00
|
|
|
|
|
|
|
http.Handle("/", r)
|
|
|
|
chain := alice.New(loggingHandler).Then(r)
|
|
|
|
|
2018-12-27 14:37:44 +00:00
|
|
|
// Refresh the DB at 2 AM
|
2019-01-10 16:39:42 +00:00
|
|
|
/*
|
|
|
|
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.")
|
2018-04-26 11:53:03 +00:00
|
|
|
}
|
2019-01-10 16:39:42 +00:00
|
|
|
time.Sleep(time.Minute)
|
2018-04-26 11:53:03 +00:00
|
|
|
}
|
2019-01-10 16:39:42 +00:00
|
|
|
}()
|
|
|
|
*/
|
2018-04-26 11:53:03 +00:00
|
|
|
|
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
2019-01-10 16:39:42 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2018-04-26 11:53:03 +00:00
|
|
|
func done() {
|
|
|
|
os.Exit(0)
|
|
|
|
}
|
|
|
|
|
|
|
|
func errorExit(msg string) {
|
|
|
|
fmt.Println(msg)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
func assertError(err error) {
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
}
|
2019-01-10 16:39:42 +00:00
|
|
|
|
|
|
|
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())
|
|
|
|
}
|
2019-01-13 14:22:29 +00:00
|
|
|
|
|
|
|
// 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))
|
|
|
|
}
|
2019-01-10 16:39:42 +00:00
|
|
|
}
|