diff --git a/assets/style.css b/assets/style.css index bb86eb2..d8cafd2 100644 --- a/assets/style.css +++ b/assets/style.css @@ -7,8 +7,15 @@ body{ padding:0 10px } h1,h2,h3{line-height:1.2} -p{margin-bottom:0px} +p,.tag-list{margin-bottom:0px} .spoiler,.spoiler2{background:#444} .spoiler:hover,.spoiler2:hover{color:#FFF} .greentext{color:#282} .pinktext{color:#928} +.tag-list{list-style-type:none} +.tag{ + line-height:1.5em; + height:1.5em; + padding: 0 1em 0 1em; + margin: 0 1px 0 1px; +} diff --git a/conf/smr.conf b/conf/smr.conf index 4394059..b6948ab 100644 --- a/conf/smr.conf +++ b/conf/smr.conf @@ -43,6 +43,7 @@ domain * { route /_claim claim route /_download download route /_preview preview + route /_search search # Leading ^ is needed for dynamic routes, kore says the route is dynamic if it does not start with '/' route ^/[^_].* read_story @@ -58,18 +59,24 @@ domain * { validate text v_any validate pasteas v_subdomain validate markup v_markup + validate tags v_any } params post /_paste { validate title v_any validate text v_any validate pasteas v_subdomain validate markup v_markup + validate tags v_any } params post /_preview { validate title v_any validate text v_any validate pasteas v_subdomain validate markup v_markup + validate tags v_any + } + params get /_search { + validate tag v_any } params get ^/[^_].* { validate comments v_bool diff --git a/src/lua/init.lua b/src/lua/init.lua index 5c9b87a..e6a0eda 100644 --- a/src/lua/init.lua +++ b/src/lua/init.lua @@ -27,6 +27,7 @@ local pagenames = { "login", "author_paste", "author_edit", + "search", } local pages = {} for k,v in pairs(pagenames) do @@ -49,11 +50,14 @@ setmetatable(queries,{ ---sql queries local stmnt_index, stmnt_author_index, stmnt_read, stmnt_paste, stmnt_raw +local stmnt_update_views +local stmnt_ins_tag, stmnt_drop_tags, stmnt_get_tags local stmnt_author_create, stmnt_author_acct, stmnt_author_bio local stmnt_cache, stmnt_insert_cache, stmnt_dirty_cache local stmnt_get_session, stmnt_insert_session local stmnt_edit, stmnt_update, stmnt_update_raw, stmnt_author_of local stmnt_comments, stmnt_comment_insert +local stmnt_search --see https://perishablepress.com/stop-using-unsafe-characters-in-urls/ --no underscore because we use that for our operative pages local url_characters = @@ -92,6 +96,7 @@ local function sqlbind(stmnt,call,position,data) end + print("Hello from init.lua") function configure() db = sqlassert(sql.open("data/posts.db")) @@ -147,7 +152,11 @@ function configure() --Select the data we need to read a story (and maybe display an edit --button stmnt_read = assert(db:prepare(queries.select_post)) + --Update the view counter when someone reads a story + stmnt_update_views = assert(db:prepare(queries.update_views)) + --Retreive comments on a story stmnt_comments = assert(db:prepare(queries.select_comments)) + --Add a new comment to a story stmnt_comment_insert = assert(db:prepare(queries.insert_comment)) --TODO: actually let authors edit their bio stmnt_author_bio = assert(db:prepare([[ @@ -173,6 +182,10 @@ function configure() --Keep a copy of the plain text of a post so we can edit it later --It might also be useful for migrations, if that ever needs to happen stmnt_raw = assert(db:prepare(queries.insert_raw)) + --Tags for a story + stmnt_ins_tag = assert(db:prepare(queries.insert_tag)) + stmnt_get_tags = assert(db:prepare(queries.select_tags)) + stmnt_drop_tags = assert(db:prepare(queries.delete_tags)) --Get the data we need to display the edit screen stmnt_edit = assert(db:prepare(queries.select_edit)) --Get the data we need when someone wants to download a paste @@ -183,15 +196,18 @@ function configure() --Someone could keep their story on the front page by just editing it a lot. --If it gets abused I can disable it I guess. stmnt_update = assert(db:prepare(queries.update_post)) + --Check sessions for login support stmnt_insert_session = assert(db:prepare(queries.insert_session)) stmnt_get_session = assert(db:prepare(queries.select_valid_sessions)) + --Search by tag name + stmnt_search = assert(db:prepare(queries.select_post_tags)) --only refresh pages at most once every 10 seconds stmnt_cache = cache:prepare([[ SELECT data FROM cache WHERE path = :path AND - ((dirty = 0) OR (strftime('%s','now') - updated) < 2) + ((dirty = 0) OR (strftime('%s','now') - updated) < 20) ; ]]) stmnt_insert_cache = cache:prepare([[ @@ -258,6 +274,27 @@ local function do_sql(stmnt) return err end +local function get_tags(id) + local ret = {} + stmnt_get_tags:bind_names{ + id = id + } + local err + repeat + err = stmnt_get_tags:step() + if err == sql.BUSY then + coroutine.yield() + elseif err == sql.ROW then + table.insert(ret,stmnt_get_tags:get_value(0)) + elseif err == sql.DONE then + stmnt_get_tags:reset() + return ret + else + error(string.format("Failed to get tags for story %d : %d", id, err)) + end + until false +end + local function dirty_cache(url) print("Dirtying cache:",url) stmnt_dirty_cache:bind_names{ @@ -351,6 +388,22 @@ local function render(pagename,callback) return text end +--[[Parses a semicolon seperated string into it's parts, trims whitespace, lowercases, and capitalizes the first letter. Tags will not be empty. Returns an array of tags]] +local function parse_tags(str) + local tags = {} + for tag in string.gmatch(str,"([^;]+)") do + assert(tag, "Found a nil or false tag in:" .. str) + local tag_trimmed = string.match(tag,"%s*(.*)%s*") + local tag_lower = string.lower(tag_trimmed) + local tag_capitalized = string.gsub(tag_lower,"^%w",string.upper) + assert(tag_capitalized, "After processing tag:" .. tag .. " it was falsey.") + if string.len(tag_capitalized) > 0 then + table.insert(tags, tag_capitalized) + end + end + return tags +end + function home(req) print("Hello from lua!") print("Method:", http_method_text(req)) @@ -368,12 +421,14 @@ function home(req) --err may be sql.ROW or sql.DONE if we don't have any stories yet while err == sql.ROW do local data = stmnt_index:get_values() + local tags = get_tags(data[1]) table.insert(latest,{ url = encode_id(data[1]), title = data[2], isanon = data[3] == 1, posted = os.date("%B %d %Y",tonumber(data[4])), author = data[5], + tags = tags, }) err = stmnt_index:step() end @@ -410,10 +465,12 @@ function home(req) while err == sql.ROW do local data = stmnt_author:get_values() local id, title, time = unpack(data) + local tags = get_tags(id) table.insert(stories,{ url = encode_id(id), title = title, - posted = os.date("%B %d %Y",tonumber(time)) + posted = os.date("%B %d %Y",tonumber(time)), + tags = tags, }) err = stmnt_author:step() end @@ -556,6 +613,8 @@ function paste(req) local title = assert(http_argument_get_string(req,"title")) local text = assert(http_argument_get_string(req,"text")) local markup = assert(http_argument_get_string(req,"markup")) + local tag_str = assert(http_argument_get_string(req,"tags")) + local tags = parse_tags(tag_str) local pasteas local raw = zlib.compress(text) text = string.gsub(text,"%%(%x%x)",decodeentities) @@ -591,20 +650,28 @@ function paste(req) sqlbind(stmnt_paste,"bind_blob",5,"") --assert(stmnt_paste:bind_blob(5,"") == sql.OK) err = do_sql(stmnt_paste) + stmnt_paste:reset() if err == sql.DONE then local rowid = stmnt_paste:last_insert_rowid() assert(stmnt_raw:bind(1,rowid) == sql.OK) assert(stmnt_raw:bind_blob(2,raw) == sql.OK) + assert(stmnt_raw:bind(3,markup) == sql.OK) err = do_sql(stmnt_raw) + stmnt_raw:reset() if err ~= sql.DONE then print("Failed to save raw text, but paste still went though") end + for _,tag in pairs(tags) do + print("tag 1:",stmnt_ins_tag:bind(1,rowid)) + print("Looking at tag",tag) + print("tag 2:",stmnt_ins_tag:bind(2,tag)) + err = do_sql(stmnt_ins_tag) + stmnt_ins_tag:reset() + end local url = encode_id(rowid) local loc = string.format("https://%s/%s",domain,url) http_response_header(req,"Location",loc) http_response(req,303,"") - stmnt_paste:reset() - stmnt_raw:reset() dirty_cache(string.format("%s/%s",domain,url)) dirty_cache(string.format("%s",domain)) return @@ -640,12 +707,21 @@ function paste(req) end assert(stmnt_paste:bind_blob(5,"") == sql.OK) err = do_sql(stmnt_paste) + stmnt_paste:reset() if err == sql.DONE then local rowid = stmnt_paste:last_insert_rowid() assert(stmnt_raw:bind(1,rowid) == sql.OK) assert(stmnt_raw:bind_blob(2,raw) == sql.OK) assert(stmnt_raw:bind(3,markup) == sql.OK) err = do_sql(stmnt_raw) + stmnt_raw:reset() + for _,tag in pairs(tags) do + print("tag 1:",stmnt_ins_tag:bind(1,rowid)) + print("Looking at tag",tag) + print("tag 2:",stmnt_ins_tag:bind(2,tag)) + err = do_sql(stmnt_ins_tag) + stmnt_ins_tag:reset() + end if err ~= sql.DONE then print("Failed to save raw text, but paste still went through") end @@ -658,8 +734,6 @@ function paste(req) end http_response_header(req,"Location",loc) http_response(req,303,"") - stmnt_paste:reset() - stmnt_raw:reset() dirty_cache(string.format("%s.%s",author,domain)) dirty_cache(string.format("%s/%s",domain,url)) dirty_cache(string.format("%s",domain)) @@ -684,9 +758,15 @@ local function read_story(host,path,idp,show_comments,iam) else cachestr = string.format("%s%s",host,path) end + local id = decode_id(idp) + stmnt_update_views:bind_names{ + id = id + } + print("update:",do_sql(stmnt_update_views)) + stmnt_update_views:reset() + dirty_cache(cachestr) print("cachestr was:",cachestr) local readstoryf = function() - local id = decode_id(idp) stmnt_read:bind_names{ id = id } @@ -697,8 +777,9 @@ local function read_story(host,path,idp,show_comments,iam) path = path } end + local tags = get_tags(id) assert(err == sql.ROW,"Could not get row:" .. tostring(id) .. " Error:" .. tostring(err)) - local title, text, authorid, isanon, authorname = unpack(stmnt_read:get_values()) + local title, text, authorid, isanon, authorname, views = unpack(stmnt_read:get_values()) stmnt_comments:bind_names{ id = id } @@ -726,6 +807,8 @@ local function read_story(host,path,idp,show_comments,iam) comments = comments, show_comments = show_comments, iam = iam, + tags = tags, + views = views, } end --Don't cache if we're logged in, someone might see dirty cache information on the page. @@ -758,6 +841,7 @@ function read(req) id = id } local err = do_sql(stmnt_read) + local tags = get_tags(id) if err == sql.DONE then --We got no story stmnt_read:reset() @@ -768,7 +852,7 @@ function read(req) --If we can edit this story, we don't want to cache --the page, since it'll have an edit button on it. assert(err == sql.ROW) - local title, storytext, tauthor, isanon, authorname = unpack(stmnt_read:get_values()) + local title, storytext, tauthor, isanon, authorname, views = unpack(stmnt_read:get_values()) storytext = zlib.decompress(storytext) stmnt_read:reset() if tauthor == authorid then @@ -782,7 +866,9 @@ function read(req) isanon = isanon == 1, author = authorname, iam = authorname, - owner = true + owner = true, + tags = tags, + views = views, } else @@ -923,6 +1009,8 @@ function edit(req) local data = stmnt_edit:get_values() local txt_compressed, markup, isanon, title = unpack(data) local text = zlib.decompress(txt_compressed) + local tags = get_tags(story_id) + local tags_txt = table.concat(tags,";") stmnt_edit:reset() ret = pages.edit{ title = title, @@ -933,6 +1021,7 @@ function edit(req) domain = domain, story = story_id, err = "", + tags = tags_txt } elseif method == "POST" then http_request_populate_post(req) @@ -941,6 +1030,7 @@ function edit(req) local text = assert(http_argument_get_string(req,"text")) local pasteas = assert(http_argument_get_string(req,"pasteas")) local markup = assert(http_argument_get_string(req,"markup")) + local tags_str = assert(http_argument_get_string(req,"tags")) stmnt_author_of:bind_names{ id = storyid } @@ -956,6 +1046,7 @@ function edit(req) local parsed = parsers[markup](text) local compr_raw = zlib.compress(text) local compr = zlib.compress(parsed) + local tags = parse_tags(tags_str) assert(stmnt_update_raw:bind_blob(1,compr_raw) == sql.OK) assert(stmnt_update_raw:bind(2,markup) == sql.OK) assert(stmnt_update_raw:bind(3,storyid) == sql.OK) @@ -967,6 +1058,17 @@ function edit(req) assert(stmnt_update:bind(4,storyid) == sql.OK) assert(do_sql(stmnt_update) == sql.DONE, "Failed to update text") stmnt_update:reset() + assert(stmnt_drop_tags:bind_names{postid = storyid} == sql.OK) + do_sql(stmnt_drop_tags) + stmnt_drop_tags:reset() + + for _,tag in pairs(tags) do + print("Looking at tag",tag) + assert(stmnt_ins_tag:bind(1,storyid) == sql.OK) + assert(stmnt_ins_tag:bind(2,tag) == sql.OK) + err = do_sql(stmnt_ins_tag) + stmnt_ins_tag:reset() + end local id_enc = encode_id(storyid) local loc = string.format("https://%s/%s",domain,id_enc) dirty_cache(string.format("%s/%s",domain,id_enc)) -- This place to read this post @@ -1030,6 +1132,8 @@ function preview(req) local title = assert(http_argument_get_string(req,"title")) local text = assert(http_argument_get_string(req,"text")) local markup = assert(http_argument_get_string(req,"markup")) + local tag_str = assert(http_argument_get_string(req,"tags")) + local tags = parse_tags(tag_str) print("title:",title,"text:",text,"markup:",markup) local parsed = parsers[markup](text) local ret = pages.read{ @@ -1038,8 +1142,50 @@ function preview(req) author = "preview", idp = "preview", text = parsed, + tags = tags, } http_response(req,200,ret) end +function search(req) + local host = http_request_get_host(req) + local path = http_request_get_path(req) + http_request_populate_qs(req) + local tag = http_argument_get_string(req,"tag") + if tag then + stmnt_search:bind_names{ + tag = tag + } + local results = {} + local err + repeat + err = stmnt_search:step() + if err == sql.BUSY then + coroutine.yield() + elseif err == sql.ROW then + local id, title, anon, time, author = unpack(stmnt_search:get_values()) + local idp = encode_id(id) + local tags = get_tags(id) + table.insert(results,{ + id = idp, + title = title, + anon = anon, + time = os.date("%B %d %Y",tonumber(time)), + author = author, + tags = tags + }) + elseif err == sql.DONE then + else + error("Failed to search, sql error:" .. tostring(err)) + end + until err == sql.DONE + local ret = pages.search{ + domain = domain, + results = results, + tag = tag, + } + http_response(req,200,ret) + end +end + print("Done with init.lua") diff --git a/src/lua/parser_imageboard.lua b/src/lua/parser_imageboard.lua index 77809dd..fc2cb0f 100644 --- a/src/lua/parser_imageboard.lua +++ b/src/lua/parser_imageboard.lua @@ -70,8 +70,12 @@ local grammar = P{ marked = V"spoiler" + V"bold" + V"italic" + V"underline" + V"heading" + V"strike" + V"spoiler2" + V"code", plainline = (V"marked" + word)^0, line = Cs(V"greentext" + V"pinktext" + V"plainline" + P"") * P"\n" / function(a) - print("matched line:",a) - return string.format("

%s",a) + print("matched line:","\"" .. a .. "\"") + if a == "\r" then + return "
" + else + return string.format("

%s

",a) + end end, ending = C(P(1)^0) / function(a) print("failed with ending:", a) return sanitize(a) end, chunk = V"line"^0 * V"plainline" * V"ending" diff --git a/src/pages/author_index.etlua b/src/pages/author_index.etlua index 546baa6..8654483 100644 --- a/src/pages/author_index.etlua +++ b/src/pages/author_index.etlua @@ -29,6 +29,12 @@ By <%= author %> + + <%= v.posted %> diff --git a/src/pages/author_paste.etlua b/src/pages/author_paste.etlua index 5d52659..04e8401 100644 --- a/src/pages/author_paste.etlua +++ b/src/pages/author_paste.etlua @@ -26,11 +26,14 @@ +
+ +

- +