diff --git a/assets/index_scroll.js b/assets/index_scroll.js new file mode 100644 index 0000000..6e2ec29 --- /dev/null +++ b/assets/index_scroll.js @@ -0,0 +1,107 @@ +/* +Allows for lazy loading of stories on the main page as the user scrolls down +*/ + + +function add_stories(stories){ + var tbody_el = document.querySelector("table#story_list tbody") + for(var i = 0; i < stories.length; i++){ + var story = stories[i]; + /* This chunk should match /src/pages/parts/story_brief */ + console.log("Adding story:",story) + var row = document.createElement("tr"); + row.appendChild( + document.createElement("td") + ); // unlisted cell + var link_cell = document.createElement("td"); + var link = document.createElement("a"); + link.textContent = story.title; + link.href = story.url; + link_cell.appendChild(link); + row.appendChild(link_cell); + + var author_cell = document.createElement("td"); + author_cell.appendChild( + document.createTextNode("By ") + ); + if(story.isanon){ + author_cell.appendChild( + document.createTextNode("Anonymous") + ); + }else{ + var author_page = document.createElement("a"); + author_page.textContent = story.author; + author_page.href = story.author; // TODO: fix + author_cell.appendChild(author_page); + } + row.appendChild(author_cell); + var hits_cell = document.createElement("td") + hits_cell.appendChild( + document.createTextNode(story.hits + " hits") + ); + row.appendChild(hits_cell); + var comments_cell = document.createElement("td"); + comments_cell.appendChild( + document.createTextNode(story.ncomments + " comments") + ); + row.appendChild(comments_cell); + var tag_cell = document.createElement("td"); + var tag_list = document.createElement("ul"); + tag_list.className = "row tag-list"; + tag_cell.appendChild(tag_list); + for(var j = 0; j < Math.min(story.tags.length,5); j++){ + var tag = story.tags[j]; + var tag_item = document.createElement("li"); + var tag_button = document.createElement("a"); + tag_button.className = "tag button button-outline"; + tag_button.textContent = tag; + tag_button.href = "/_search?q=%2B" + tag; + tag_item.appendChild(tag_button); + tag_list.appendChild(tag_item); + } + row.appendChild(tag_cell); + var date_cell = document.createElement("td"); + date_cell.appendChild( + document.createTextNode(story.posted) + ); + row.appendChild(date_cell); + tbody_el.appendChild(row); + } +} + +/* +A tiny state machine: +0 - idle +1 - loading more stories (do not send another request) +2 - stories loaded, waiting for next scroll event to transition to idle +*/ +var state = 0 +var loaded = 50 // by default we load 50 stories on the site index +document.addEventListener("scroll",function(e){ + var tobot = window.scrollMaxY - window.scrollY + if (tobot < 100){ + if (state == 0){ + //Ask the server for stories + // TODO: Finish this + var xhr = new XMLHttpRequest(); + xhr.open("GET", "/_api?call=stories&data=" + loaded); + xhr.onreadystatechange = function(e){ + if(xhr.readyState === 4){ + console.log("response:",xhr.response) + resp = JSON.parse(xhr.response); + console.log("resp:",resp) + add_stories(resp.stories); + loaded += resp.stories.length; + } + state = 2 + } + xhr.send() + state = 1 + }else if (state == 1){ + // Do nothing + }else if (state == 2){ + state = 0 + } + console.log("we should load more stories") + } +}) diff --git a/conf/smr.conf.in b/conf/smr.conf.in index 1f5ede4..c799c71 100644 --- a/conf/smr.conf.in +++ b/conf/smr.conf.in @@ -89,6 +89,11 @@ domain * { handler asset_serve_intervine_deletion_js methods get } + + route /_js/index_scroll.js { + handler asset_serve_index_scroll_js + methods get + } route /favicon.ico { handler asset_serve_favicon_ico diff --git a/src/lua/addon.lua b/src/lua/addon.lua new file mode 100644 index 0000000..c488e11 --- /dev/null +++ b/src/lua/addon.lua @@ -0,0 +1,23 @@ +--[[ +Addon loader - Addons are either: +* A folder with at least two files: + - meta.lua - contains load order information + - init.lua - entrypoint that gets run to load the addon +* A zip file with the same +* A sqlite3 database with a table "files" that has at least the columns + * name :: TEXT - The filename + * data :: BINARY - The file data + And there are at least 2 rows with filenames `meta.lua` and `init.lua` + as described above. Addons should be placed in $SMR_ADDONS, defined in config.lua + +The `meta.lua` file is run at worker init time (i.e. it will be run once for +each worker), and should return a table with at least the following information +{ + name :: string - A name for the addon (all addons must have unique names) + desc :: string - A description for the addon. + entry :: table[number -> string] - Describes the load order for this + addon. Each addon's meta.lua is run, and sorted to get a load + order for entrypoints (the strings are the names files) +]] + + diff --git a/src/lua/endpoints/api_get.lua b/src/lua/endpoints/api_get.lua index 98a8174..c6ee513 100644 --- a/src/lua/endpoints/api_get.lua +++ b/src/lua/endpoints/api_get.lua @@ -3,16 +3,25 @@ local sql = require("lsqlite3") local db = require("db") local queries = require("queries") local util = require("util") +local tags = require("tags") +require("global") -local stmnt_tags_get +local stmnt_tags_get, stmnt_stories_get local oldconfigure = configure function configure(...) stmnt_tags_get = db.sqlassert(db.conn:prepare(queries.select_suggest_tags)) + stmnt_stories_get = db.sqlassert(db.conn:prepare(queries.select_site_index)) return oldconfigure(...) end local function suggest_tags(req,data) + --[[ + Prevent a malicious user from injecting '%' into the string + we're searching for, potentially causing a DoS with a + sufficiently backtrack-ey search/tag combination. + ]] + assert(data:match("^[a-zA-Z0-9,%s-]+$"),string.format("Bad characters in tag: %q",data)) stmnt_tags_get:bind_names{ match = data .. "%" } @@ -25,19 +34,93 @@ local function suggest_tags(req,data) http_response(req,200,table.concat(tags,";")) end +--[[ +A poor mans json builder, since I don't need one big enough to pull in a +dependency for it (yet) +]] +local function poor_json(builder, ltbl) + local function write_bool(builder,bool) + table.insert(builder,bool and "true" or "false") + end + local function write_number(builder,num) + local number + if num % 1 == 0 then + num = string.format("%d",num) + else + num = string.format("%f",num) + end + table.insert(builder,num) + end + local function write_string(builder,s) + table.insert(builder, string.format("%q",s)) + end + local function write_array(builder,tbl) + table.insert(builder,"[") + for _,item in ipairs(tbl) do + write_string(builder,item) + table.insert(builder,",") + end + if #tbl > 0 then + table.remove(builder,#builder) -- Remove the last comma + end + table.insert(builder,"]") + end + local lua_to_json = { + boolean = write_bool, + number = write_number, + string = write_string, + table = write_array + } + table.insert(builder,"{") + for k,v in pairs(ltbl) do + assert(type(k) == "string", "Field was not a string, was: " .. type(k)) + table.insert(builder,string.format("%q",k)) + table.insert(builder,":") + assert(lua_to_json[type(v)], "Unknown type for json:" .. type(v) .. " at " .. k) + lua_to_json[type(v)](builder,v) + table.insert(builder,",") + end + table.remove(builder,#builder) -- Remove the last comma before closing object + table.insert(builder,"}") + table.insert(builder,",") -- Can't do this on the same line as above + -- we need to remove the last comma, but not } +end + +local function get_stories(req,data) + local nstories = tonumber(data) + stmnt_stories_get:bind_names{offset=nstories} + local builder = setmetatable({'{"stories":['},table) + for id, title, anon, time, author, hits, ncomments in db.sql_rows(stmnt_stories_get) do + local story = { + url = util.encode_id(id), + title = title, + isanon = tonumber(anon) == 1, + posted = os.date("%B %d %Y",tonumber(time)), + author = author, + tags = tags.get(id), + hits = hits, + ncomments = ncomments + } + poor_json(builder,story) + end + table.remove(builder,#builder) -- Remove last comma before closing list + table.insert(builder,"]}") + stmnt_stories_get:reset() + http_response_header(req,"Content-Type","text/plain") + http_response(req,200,table.concat(builder)) +end + +local api_points = {} +local function register_api(call,func) + api_points[call] = func +end +register_api("suggest",suggest_tags) +register_api("stories",get_stories) local function api_get(req) http_request_populate_qs(req) local call = assert(http_argument_get_string(req,"call")) local data = assert(http_argument_get_string(req,"data")) - local body - if call == "suggest" then - --[[ - Prevent a malicious user from injecting '%' into the string - we're searching for, potentially causing a DoS with a - sufficiently backtrack-ey search/tag combination. - ]] - assert(data:match("^[a-zA-Z0-9,%s-]+$"),string.format("Bad characters in tag: %q",data)) - return suggest_tags(req,data) - end + assertf(api_points[call], "Unknown api endpoint: %s", call) + api_points[call](req,data) end return api_get diff --git a/src/lua/global.lua b/src/lua/global.lua index 2d99571..7370a9f 100644 --- a/src/lua/global.lua +++ b/src/lua/global.lua @@ -7,3 +7,49 @@ function assertf(bool, fmt, ...) error(string.format(fmt,...),2) end end + + +function errorf(str, ...) + --try calling string.format, if it errors, try calling it with exactly + --1 less argument (the error level) + local args = {...} + local succ, ret = pcall(string.format,str,...) + if not succ and type(args[#args]) ~= "number" then + errorf("Failed displaying error that looks like %q",str) + elseif type(args[#args]) == "number" then + local errlevel = table.remove(args,#args) + local succ2, ret = pcall(string.format,str,unpack(args)) + if not succ2 then + errorf("Failed displaying error that looks like %q",str) + end + error(ret, errlevel+1) + end + error(ret,2) +end + +local oldtostring = tostring +function tostring(any) + --Pretty print tables by default + local printed_tables = {} + local function tostring_helper(a,tabs) + if type(a) ~= "table" then + return oldtostring(a) + end + if printed_tables[a] then + return oldtostring(a) + end + printed_tables[a] = true + local sbuilder = {"{\n"} + for k,v in pairs(a) do + table.insert(sbuilder,string.rep("\t",tabs)) + table.insert(sbuilder,tostring_helper(k,tabs+1)) + table.insert(sbuilder,":") + table.insert(sbuilder,tostring_helper(v,tabs+1)) + table.insert(sbuilder,"\n") + end + table.insert(sbuilder,string.rep("\t",tabs-1)) + table.insert(sbuilder,"}") + return table.concat(sbuilder) + end + return tostring_helper(any,1) +end diff --git a/src/lua/hooks.lua b/src/lua/hooks.lua index cef8daf..0d3b461 100644 --- a/src/lua/hooks.lua +++ b/src/lua/hooks.lua @@ -1,21 +1,24 @@ --[[ -Global functions that smr exposes that can be overriden by addons +Global functions that smr exposes that can be detoured by addons ]] local api = {} -- Called before any request processing. Returning true "traps" the request, and --- does not continue calling smr logic, or logic of any other addons. +-- does not continue calling smr logic. Well-behaved addons should check for +-- "true" from the detoured function, and return true immediately if the check +-- succeeds. api.pre_request = function(req) end --- Called after smr request processing. Returning true "traps" the request, and --- does not continue calling any other addon logic. This will not stop smr from +-- Called after smr request processing. Returning true "traps" the request. +-- Well-behaved addons should check for true from the detoured function, and +-- immediately return true if the check succeeds. This will not stop smr from -- responding to the request, since by this time http_request_response() has -- already been called. api.post_request = function(req) end -- Called during startup of the worker process --- Failures in this function will prevent kore from starting. +-- calling error() in this function will prevent kore from starting. -- Return value is ignored. api.worker_init = function() end @@ -42,4 +45,7 @@ api.get = { page_reader = function(env) return {} end, } +-- Called when the /_api endpoint is accessed +api.call = function() end + return api diff --git a/src/lua/pages.lua b/src/lua/pages.lua index 9b3f321..d340fda 100644 --- a/src/lua/pages.lua +++ b/src/lua/pages.lua @@ -3,6 +3,7 @@ Compiles all the pages under src/pages/ with etlua. See the etlua documentation for more info (https://github.com/leafo/etlua) ]] local et = require("etlua") +require("global") local pagenames = { "index", "author_index", diff --git a/src/pages/index.etlua.in b/src/pages/index.etlua.in index 7da9ece..5acc0a6 100644 --- a/src/pages/index.etlua.in +++ b/src/pages/index.etlua.in @@ -28,7 +28,7 @@ <% if #stories == 0 then %> No stories available. <% else %> - +
<% for k,story in pairs(stories) do %> <{system cat src/pages/parts/story_breif.etlua}> <% end %>