diff options
-rw-r--r-- | config/config.go | 5 | ||||
-rw-r--r-- | main.go | 54 | ||||
-rw-r--r-- | models/db.go | 55 | ||||
-rw-r--r-- | web/rice-box.go | 8 | ||||
-rw-r--r-- | web/web.go | 2 |
5 files changed, 98 insertions, 26 deletions
diff --git a/config/config.go b/config/config.go index 9a6df3a..91ffd87 100644 --- a/config/config.go +++ b/config/config.go @@ -7,9 +7,8 @@ import ( ) type Settings struct { - DBDriver string `json:"db_driver"` - DBServer string `json:"db"` - WebServer string `json:"web"` + DBFile string `json:"db"` + Port int `json:"web"` Username string `json:"username"` DigestPassword string `json:"password"` ProxyImages bool `json:"proxy_images"` @@ -12,42 +12,64 @@ import ( ) func main() { - var serve, update, verbose bool - var configFile, newFeed, export string + var help, update, verbose, proxyImages bool + var dbfile, newFeed, export, password string + var port int - flag.StringVarP(&configFile, "config", "c", "config.json", "`configuration` file") - flag.BoolVarP(&update, "update", "u", false, "fetch feeds and store them in the database") - flag.BoolVarP(&serve, "serve", "s", false, "run http server") - flag.BoolVarP(&verbose, "verbose", "v", false, "verbose output") + // dbfile + // port + // proxyImages + // username + // password + + flag.BoolVarP(&help, "help", "h", false, "print usage information") + + flag.BoolVarP(&update, "update", "u", false, "fetch feeds and store new items") flag.StringVarP(&newFeed, "add", "a", "", "add the feed at URL `http://example.com/rss.xml`") - flag.StringVarP(&export, "export", "x", "", "export feeds as `text`, json or opml") + flag.StringVarP(&export, "export", "x", "", "export feed. format required: text, json or opml") + + // flag.BoolVarP(&serve, "serve", "s", false, "run neko app by starting HTTP server") + + flag.StringVarP(&dbfile, "db", "d", "neko.db", "sqlite database file") + flag.StringVarP(&dbfile, "password", "p", "", "password to access web interface") + + flag.IntVarP(&port, "http", "s", 4994, "HTTP port to serve on") + flag.BoolVarP(&verbose, "verbose", "v", true, "verbose output") + flag.BoolVarP(&proxyImages, "imageproxy", "i", false, "rewrite and proxy all image requests for privacy (experimental)") + flag.Parse() - // no command - if !update && !serve && newFeed == "" && export == "" { + if help { flag.Usage() return } - config.Read(configFile) - models.InitDB() vlog.VERBOSE = verbose + config.Config.DBFile = dbfile + config.Config.Port = port + config.Config.ProxyImages = proxyImages + config.Config.DigestPassword = password + + models.InitDB() if update { vlog.Printf("starting crawl\n") crawler.Crawl() - } - if serve { - vlog.Printf("starting web server at %s\n", - config.Config.WebServer) - web.Serve() + return } if newFeed != "" { vlog.Printf("creating new feed\n") feed.NewFeed(newFeed) + return } if export != "" { vlog.Printf("feed export\n") exporter.ExportFeeds(export) + return } + + vlog.Printf("starting web server at 127.0.0.1:%d\n", + config.Config.Port) + web.Serve() + } diff --git a/models/db.go b/models/db.go index 31d8bdd..d5bc7dc 100644 --- a/models/db.go +++ b/models/db.go @@ -5,8 +5,8 @@ package models import ( "adammathes.com/neko/config" + "adammathes.com/neko/vlog" "database/sql" - _ "github.com/go-sql-driver/mysql" _ "github.com/mattn/go-sqlite3" "log" ) @@ -16,7 +16,9 @@ var DB *sql.DB func InitDB() { var err error // DB, err = sql.Open("mysql", dataSourceName) - DB, err = sql.Open(config.Config.DBDriver, config.Config.DBServer) + vlog.Printf("using sqlite3 db file %s\n", config.Config.DBFile) + + DB, err = sql.Open("sqlite3", config.Config.DBFile) if err != nil { log.Panic(err) } @@ -24,4 +26,53 @@ func InitDB() { if err = DB.Ping(); err != nil { log.Panic(err) } + + schema := ` +CREATE TABLE IF NOT EXISTS feed ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + url varchar(100) NOT NULL UNIQUE, + web_url varchar(255) NOT NULL DEFAULT '', + title varchar(255) NOT NULL DEFAULT '', + last_updated timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + category varchar(255) NOT NULL DEFAULT 'uncategorized' +); +CREATE INDEX feed_url ON feed (url); +CREATE INDEX feed_category ON feed (category); + +CREATE TABLE IF NOT EXISTS item ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + feed_id int(11) NOT NULL, + title text NOT NULL DEFAULT '', + url varchar(255) NOT NULL UNIQUE, + description text NOT NULL DEFAULT '', + publish_date timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + read_state tinyint(1) NOT NULL DEFAULT '0', + starred tinyint(1) NOT NULL DEFAULT '0', + full_content text NOT NULL DEFAULT '', + header_image text NOT NULL DEFAULT '', + CONSTRAINT item_ibfk_1 FOREIGN KEY (feed_id) REFERENCES feed(id) ON DELETE CASCADE +); +CREATE INDEX item_url ON item (url); +CREATE INDEX item_publish_date ON item (publish_date); +CREATE INDEX item_feed_id ON item (feed_id); +CREATE INDEX item_read_state ON item (read_state); + +CREATE VIRTUAL TABLE fts_item using fts4(content="item", title, url, description); +CREATE TRIGGER item_bu BEFORE UPDATE ON item BEGIN + DELETE FROM fts_item WHERE docid=old.rowid; +END; +CREATE TRIGGER item_bd BEFORE DELETE ON item BEGIN + DELETE FROM fts_item WHERE docid=old.rowid; +END; +CREATE TRIGGER item_au AFTER UPDATE ON item BEGIN + INSERT INTO fts_item(docid, title, url, description) VALUES(new.rowid, new.title, new.url, new.description); +END; +CREATE TRIGGER item_ai AFTER INSERT ON item BEGIN + INSERT INTO fts_item(docid, title, url, description) VALUES(new.rowid, new.title, new.url, new.description); +END; + + INSERT INTO fts_item(fts_item) VALUES('rebuild'); +` + + _, _ = DB.Exec(schema) } diff --git a/web/rice-box.go b/web/rice-box.go index d82fd72..bb8b8b5 100644 --- a/web/rice-box.go +++ b/web/rice-box.go @@ -60,8 +60,8 @@ func init() { } filec := &embedded.EmbeddedFile{ Filename: "ui.html", - FileModTime: time.Unix(1529168764, 0), - Content: string("<!DOCTYPE html>\n<html>\n <head>\n <title>neko rss mode</title>\n <link rel=\"stylesheet\" href=\"/static/style.css\" />\n <script src=\"/static/jquery-3.3.1.min.js\"></script>\n <script src=\"/static/jquery.tmpl.min.js\"></script>\n <script src=\"/static/underscore-1.8.3.min.js\"></script>\n <script src=\"/static/backbone-1.3.3.min.js\"></script>\n <script>\n PUBLIC_VERSION = false;\n </script>\n <script src=\"/static/ui.js\"></script>\n <meta name=\"viewport\" content=\"width=device-width,height=device-height, initial-scale=1, maximum-scale=1\" />\n <base target=\"_blank\">\n </head>\n <body>\n <h1 class=\"logo\" onclick=\"$('#filters').toggleClass('hidden');\">🐱</h1>\n\n <div id=\"filters\">\n\n <div id=\"controls\"></div>\n <h4 onclick=\"$('#tags').toggle();\">Tags</h4> \n <ul id=\"tags\" style=\"display: none;\">\n </ul>\n \n <h4 onclick=\"$('#feeds').toggle();\">Feeds</h4> \n <ul id=\"feeds\" style=\"display: none;\">\n </ul>\n </div>\n\n </div>\n\n <div id=\"c\">\n <div id=\"items\">\n </div> \n </div>\n \n <script id=\"item_template\" type=\"text/jqtmp\">\n <h2><a class=\"i\" id=\"i_${item_id}\" href=\"${item.url}\">${item.title }</a> \n <span class={{if item.starred}}\"unstar\"{{else}}\"star\"{{/if}}>★</span>\n </h2>\n <p class=\"dateline\" style=\"clear: both;\">\n <a href=\"${item.feed_url}\">${item.feed_title}</a> | <a href=\"${item.url}\">${item.p_url}</a>\n | ${item.feed_category} |\n <span class=\"full\">{{if item.full}}hide{{else}}scrape{{/if}} full text</span>\n \n </p>\n {{if item.header_image}}\n <div class=\"img\"><img src=\"${item.header_image}\" /></div>\n {{/if}}\n <div class=\"description\">\n {{if item.full}}\n {{html item.full_content}}\n {{else}}\n {{html item.description}}\n {{/if}}\n </div>\n </script>\n\n <script id=\"tag_template\" type=\"text/jqtmp\">\n {{if tag.selected}}<b>{{/if}}\n ${tag.title}\n {{if tag.selected}}</b>{{/if}}\n </script>\n\n <script id=\"feed_template\" type=\"text/jqtmp\">\n {{if feed.selected}}<b>{{/if}}\n <span class=\"txt\">\n {{if feed.title}}\n ${feed.title}\n {{else}}\n ${feed.url}\n {{/if}}\n\n </span>\n <span class=\"edit\">[e]</span>\n <span class=\"delete\">[x]</span>\n {{if feed.selected}}</b>{{/if}}\n </script>\n\n <script id=\"controls_template\" type=\"text/jqtmp\">\n <ul>\n <li>\n <a {{if app.unreadFilter}}style=\"font-weight: bold;\"{{/if}} \n class=\"unread_filter\">unread</a>\n </li>\n <li>\n <a \n {{if app.allFilter}}style=\"font-weight: bold;\"{{/if}} \n class=\"all_filter\">all</a> \n </li>\n <li>\n <a {{if app.starredFilter}}style=\"font-weight: bold;\"{{/if}}\n class=\"starred_filter\">★ starred</a>\n </li>\n <li>\n <button class=\"new_feed\"> + New </button>\n </li>\n <li>\n <input id=\"search\" type=\"search\" /><button class=\"search_go\">search</button>\n </li>\n\t\t<li>\n\t\t</li>\n\n\t </ul>\n </script> \n \n</body>\n</html> \n"), + FileModTime: time.Unix(1529169644, 0), + Content: string("<!DOCTYPE html>\n<html>\n <head>\n <title>neko rss mode</title>\n <link rel=\"stylesheet\" href=\"/static/style.css\" />\n <script src=\"/static/jquery-3.3.1.min.js\"></script>\n <script src=\"/static/jquery.tmpl.min.js\"></script>\n <script src=\"/static/underscore-1.8.3.min.js\"></script>\n <script src=\"/static/backbone-1.3.3.min.js\"></script>\n <script>\n PUBLIC_VERSION = false;\n </script>\n <script src=\"/static/ui.js\"></script>\n <meta name=\"viewport\" content=\"width=device-width,height=device-height, initial-scale=1, maximum-scale=1\" />\n <base target=\"_blank\">\n </head>\n <body>\n <h1 class=\"logo\" onclick=\"$('#filters').toggleClass('hidden');\">🐱</h1>\n\n <div id=\"filters\">\n\n <div id=\"controls\"></div>\n <h4 onclick=\"$('#tags').toggle();\">Tags</h4> \n <ul id=\"tags\" style=\"display: none;\">\n </ul>\n \n <h4 onclick=\"$('#feeds').toggle();\">Feeds</h4> \n <ul id=\"feeds\" style=\"display: none;\">\n </ul>\n </div>\n\n </div>\n\n <div id=\"c\">\n <div id=\"items\">\n </div> \n </div>\n \n <script id=\"item_template\" type=\"text/jqtmp\">\n <h2><a class=\"i\" id=\"i_${item_id}\" href=\"${item.url}\">${item.title }</a> \n <span class={{if item.starred}}\"unstar\"{{else}}\"star\"{{/if}}>★</span>\n </h2>\n <p class=\"dateline\" style=\"clear: both;\">\n <a href=\"${item.feed_url}\">${item.feed_title}</a> | <a href=\"${item.url}\">${item.p_url}</a>\n | ${item.feed_category} |\n <span class=\"full\">{{if item.full}}hide{{else}}scrape{{/if}} full text</span>\n \n </p>\n {{if item.header_image}}\n <div class=\"img\"><img src=\"${item.header_image}\" /></div>\n {{/if}}\n <div class=\"description\">\n {{if item.full}}\n {{html item.full_content}}\n {{else}}\n {{html item.description}}\n {{/if}}\n </div>\n </script>\n\n <script id=\"tag_template\" type=\"text/jqtmp\">\n {{if tag.selected}}<b>{{/if}}\n ${tag.title}\n {{if tag.selected}}</b>{{/if}}\n </script>\n\n <script id=\"feed_template\" type=\"text/jqtmp\">\n {{if feed.selected}}<b>{{/if}}\n <span class=\"txt\">\n {{if feed.title}}\n ${feed.title}\n {{else}}\n ${feed.url}\n {{/if}}\n\n </span>\n <span class=\"edit\">[e]</span>\n <span class=\"delete\">[x]</span>\n {{if feed.selected}}</b>{{/if}}\n </script>\n\n <script id=\"controls_template\" type=\"text/jqtmp\">\n <ul>\n <li>\n <a {{if app.unreadFilter}}style=\"font-weight: bold;\"{{/if}} \n class=\"unread_filter\">unread</a>\n </li>\n <li>\n <a \n {{if app.allFilter}}style=\"font-weight: bold;\"{{/if}} \n class=\"all_filter\">all</a> \n </li>\n <li>\n <a {{if app.starredFilter}}style=\"font-weight: bold;\"{{/if}}\n class=\"starred_filter\">★ starred</a>\n </li>\n <li>\n <button class=\"new_feed\"> + new </button>\n </li>\n <li>\n <input id=\"search\" type=\"search\" /><button class=\"search_go\">search</button>\n </li>\n\t\t<li>\n\t\t</li>\n\n\t </ul>\n </script> \n \n</body>\n</html> \n"), } filed := &embedded.EmbeddedFile{ Filename: "ui.js", @@ -82,7 +82,7 @@ func init() { // define dirs dir1 := &embedded.EmbeddedDir{ Filename: "", - DirModTime: time.Unix(1529169528, 0), + DirModTime: time.Unix(1529169644, 0), ChildFiles: []*embedded.EmbeddedFile{ file2, // ".DS_Store" file3, // "backbone-1.3.3.min.js" @@ -108,7 +108,7 @@ func init() { // register embeddedBox embedded.RegisterEmbeddedBox(`../static`, &embedded.EmbeddedBox{ Name: `../static`, - Time: time.Unix(1529169528, 0), + Time: time.Unix(1529169644, 0), Dirs: map[string]*embedded.EmbeddedDir{ "": dir1, }, @@ -295,5 +295,5 @@ func Serve() { http.HandleFunc("/", AuthWrap(indexHandler)) - log.Fatal(http.ListenAndServe(config.Config.WebServer, nil)) + log.Fatal(http.ListenAndServe(":"+strconv.Itoa(config.Config.Port), nil)) } |