Started work on refactor

Started work on moveing each endpoint into it's own file.
This commit is contained in:
Robin Malley 2020-12-20 08:16:23 +00:00
parent 86a14e9d62
commit 2cd10b6968
35 changed files with 1319 additions and 92 deletions

93
src/lua/cache.lua Normal file
View File

@ -0,0 +1,93 @@
local sql = require("lsqlite3")
local queries = require("queries")
local util = require("util")
local ret = {}
local stmnt_cache, stmnt_insert_cache
local oldconfigure = configure
function configure(...)
local cache = sqlassert(sql.open_memory())
--A cache table to store rendered pages that do not need to be
--rerendered. In theory this could OOM the program eventually and start
--swapping to disk. TODO: fixme
assert(cache:exec([[
CREATE TABLE IF NOT EXISTS cache (
path TEXT PRIMARY KEY,
data BLOB,
updated INTEGER,
dirty INTEGER
);
]]))
stmnt_cache = cache:prepare([[
SELECT data
FROM cache
WHERE
path = :path AND
((dirty = 0) OR (strftime('%s','now') - updated) < 20)
;
]])
stmnt_insert_cache = cache:prepare([[
INSERT OR REPLACE INTO cache (
path, data, updated, dirty
) VALUES (
:path, :data, strftime('%s','now'), 0
);
]])
stmnt_dirty_cache = cache:prepare([[
UPDATE OR IGNORE cache
SET dirty = 1
WHERE path = :path;
]])
return oldconfigure(...)
end
--Render a page, with cacheing. If you need to dirty a cache, call dirty_cache()
function ret.render(pagename,callback)
print("Running render...")
stmnt_cache:bind_names{path=pagename}
local err = util.do_sql(stmnt_cache)
if err == sql.DONE then
stmnt_cache:reset()
--page is not cached
elseif err == sql.ROW then
print("Cache hit:" .. pagename)
data = stmnt_cache:get_values()
stmnt_cache:reset()
return data[1]
else --sql.ERROR or sql.MISUSE
error("Failed to check cache for page " .. pagename)
end
--We didn't have the paged cached, render it
print("Cache miss, running function")
local text = callback()
print("Saving data...")
--And save the data back into the cache
stmnt_insert_cache:bind_names{
path=pagename,
data=text,
}
err = util.do_sql(stmnt_insert_cache)
if err == sql.ERROR or err == sql.MISUSE then
error("Failed to update cache for page " .. pagename)
end
stmnt_insert_cache:reset()
return text
end
function ret.dirty(url)
print("Dirtying cache:",url)
stmnt_dirty_cache:bind_names{
path = url
}
err = util.do_sql(stmnt_dirty_cache)
stmnt_dirty_cache:reset()
end
function ret.close()
end
return ret

16
src/lua/claim_get.lua Normal file
View File

@ -0,0 +1,16 @@
local cache = require("cache")
local config = require("config")
local pages = require("pages")
local function claim_get(req)
--Get the page to claim a name
local cachestr = string.format("%s/_claim",config.domain)
local text = cache.render(cachestr,function()
print("cache miss, rendering claim page")
return pages.claim{err=""}
end)
http_response(req,200,text)
end
return claim_get

66
src/lua/claim_post.lua Normal file
View File

@ -0,0 +1,66 @@
local sql = require("lsqlite3")
local pages = require("pages")
local db = require("db")
local queries = require("queries")
local stmnt_author_create
local oldconfigure = configure
function configure(...)
stmnt_author_create = assert(db.conn:prepare(queries.insert_author))
return oldconfigure(...)
end
local function claim_post(req)
--Actually claim a name
http_request_populate_post(req)
local name = assert(http_argument_get_string(req,"user"))
local text
--What in the world, Kore should be rejecting names that
--are not lower case & no symbols, but some still get through somehow.
if not name:match("^[a-z0-9]*$") then
print("Bad username:",name)
text = pages.claim{
err = "Usernames must match ^[a-z0-9]{1,30}$"
}
http_response(req,200,text)
return
end
local rngf = assert(io.open("/dev/urandom","rb"))
local passlength = string.byte(rngf:read(1)) + 64
local salt = rngf:read(64)
local password = rngf:read(passlength)
rngf:close()
local hash = sha3(salt .. password)
stmnt_author_create:bind_names{
name = name,
}
stmnt_author_create:bind_blob(2,salt)
stmnt_author_create:bind_blob(3,hash)
local err = do_sql(stmnt_author_create)
if err == sql.DONE then
--We sucessfully made athe new author
local id = stmnt_author_create:last_insert_rowid()
stmnt_author_create:reset()
--Give them a file back
http_response_header(req,"Content-Type","application/octet-stream")
http_response_header(req,"Content-Disposition","attachment; filename=\"" .. name .. "." .. domain .. ".passfile\"")
local session = start_session(id)
text = password
elseif err == sql.CONSTRAINT then
--If the creation failed, they probably just tried
--to use a name that was already taken
text = pages.claim {
err = "Failed to claim. That name may already be taken."
}
elseif err == sql.ERROR or err == sql.MISUSE then
--This is bad though
text = pages.claim {
err = "Failed to claim"
}
end
stmnt_author_create:reset()
end
return claim_post

5
src/lua/config.lua Normal file
View File

@ -0,0 +1,5 @@
return {
domain = "test.monster:8888",
production = false,
}

48
src/lua/db.lua Normal file
View File

@ -0,0 +1,48 @@
local sql = require("lsqlite3")
local queries = require("queries")
local db = {}
local oldconfigure = configure
function configure(...)
db.conn = sqlassert(sql.open("data/posts.db"))
--Create sql tables
assert(db.conn:exec(queries.create_table_authors))
--Create a fake "anonymous" user, so we don't run into trouble
--so that no one runs into trouble being able to paste under this account.
assert(db.conn:exec(queries.insert_anon_author))
--If/when an author delets their account, all posts
--and comments by that author are also deleted (on
--delete cascade) this is intentional. This also
--means that all comments by other users on a post
--an author makes will also be deleted.
--
--Post text uses zlib compression
assert(db.conn:exec(queries.create_table_posts))
--Store the raw text so people can download it later, maybe
--we can use it for "download as image" or "download as pdf"
--in the future too. Stil stored zlib compressed
assert(db.conn:exec(queries.create_table_raw_text))
--Maybe we want to store images one day?
assert(db.conn:exec(queries.create_table_images))
--Comments on a post
assert(db.conn:exec(queries.create_table_comments))
--Tags for a post
assert(db.conn:exec(queries.create_table_tags))
--Index for tags
assert(db.conn:exec(queries.create_index_tags))
--Store a cookie for logged in users. Logged in users can edit
--their own posts, and edit their biographies.
assert(db:exec(queries.create_table_session))
print("Created db tables")
return configure(...)
end
function db.close()
db.conn:close()
end
return db

64
src/lua/edit_get.lua Normal file
View File

@ -0,0 +1,64 @@
local zlib = require("zlib")
local sql = require("lsqlite3")
local db = require("db")
local queries = require("queries")
local util = require("util")
local pages = require("pages")
local tags = require("tags")
local stmnt_edit
local oldconfigure = configure
function configure(...)
stmnt_edit = assert(db.conn:prepare(queries.select_edit))
return configure(...)
end
local function edit_get(req)
local host = http_request_get_host(req)
local path = http_request_get_path(req)
local author, authorid = get_session(req)
http_request_populate_qs(req)
local story = assert(http_argument_get_string(req,"story"))
local story_id = decode_id(story)
local ret
print("we want to edit story:",story)
--Check that the logged in user is the owner of the story
--sql-side. If we're not the owner, we'll get 0 rows back.
stmnt_edit:bind_names{
postid = story_id,
authorid = author_id
}
local err = util.do_sql(stmnt_edit)
if err == sql.DONE then
--No rows, we're probably not the owner (it might
--also be because there's no such story)
ret = pages.cantedit{
path = story,
}
stmnt_edit:reset()
http_response(req,200,ret)
return
end
assert(err == sql.ROW)
local data = stmnt_edit:get_values()
local txt_compressed, markup, isanon, title = unpack(data)
local text = zlib.decompress(txt_compressed)
local tags = tags.get(story_id)
local tags_txt = table.concat(tags,";")
stmnt_edit:reset()
ret = pages.edit{
title = title,
text = text,
markup = markup,
user = author,
isanon = isanon == 1,
domain = domain,
story = story_id,
err = "",
tags = tags_txt
}
http_response(req,200,ret)
end

85
src/lua/edit_post.lua Normal file
View File

@ -0,0 +1,85 @@
local sql = require("lsqlite3")
local zlib = require("zlib")
local queries = require("queries")
local pages = require("pages")
local parsers = require("parsers")
local util = require("util")
local tagslib = require("tags")
local cache = require("cache")
local config = require("config")
local stmnt_author_of, stmnt_update_raw, stmnt_update
local oldconfigure = configure
function configure(...)
stmnt_author_of = assert(db.conn:prepare(queries.select_author_of_post))
stmnt_update_raw = assert(db.conn:prepare(queries.update_raw))
stmnt_update = assert(db.conn:prepare(queries.update_post))
return oldconfigure(...)
end
local function edit_post(req)
local host = http_request_get_host(req)
local path = http_request_get_path(req)
local author, author_id = get_session(req)
http_request_populate_post(req)
local storyid = tonumber(assert(http_argument_get_string(req,"story")))
local title = assert(http_argument_get_string(req,"title"))
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 = http_argument_get_string(req,"tags")
stmnt_author_of:bind_names{
id = storyid
}
local err = do_sql(stmnt_author_of)
if err ~= sql.ROW then
stmnt_author_of:reset()
error("No author found for story:" .. storyid)
end
local data = stmnt_author_of:get_values()
stmnt_author_of:reset()
local realauthor = data[1]
assert(realauthor == author_id) --Make sure the author of the story is the currently logged in user
local parsed = parsers[markup](text)
local compr_raw = zlib.compress(text)
local compr = zlib.compress(parsed)
local tags = {}
if tags_str then
tags = util.parse_tags(tags_str)
end
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)
assert(util.do_sql(stmnt_update_raw) == sql.DONE, "Failed to update raw")
stmnt_update_raw:reset()
assert(stmnt_update:bind(1,title) == sql.OK)
assert(stmnt_update:bind_blob(2,compr) == sql.OK)
assert(stmnt_update:bind(3,pasteas == "anonymous" and 1 or 0) == sql.OK)
assert(stmnt_update:bind(4,storyid) == sql.OK)
assert(do_sql(stmnt_update) == sql.DONE, "Failed to update text")
stmnt_update:reset()
tagslib.set(storyid,tags)
--[[
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 = util.encode_id(storyid)
local loc = string.format("https://%s/%s",config.domain,id_enc)
cache.dirty(string.format("%s/%s",config.domain,id_enc)) -- This place to read this post
cache.dirty(string.format("%s",config.domain)) -- The site index (ex, if the author changed the paste from their's to "Anonymous", the cache should reflect that).
cache.dirty(string.format("%s.%s",author,config.domain)) -- The author's index, same reasoning as above.
http_response_header(req,"Location",loc)
http_response(req,303,"")
return
end

110
src/lua/index_get.lua Normal file
View File

@ -0,0 +1,110 @@
local sql = require("lsqlite3")
local cache = require("cache")
local queries = require("queries")
local db = require("db")
local util = require("util")
local config = require("config")
local pages = require("pages")
local stmnt_index, stmnt_author, stmnt_author_bio
local oldconfigure = configure
function configure(...)
stmnt_index = assert(db.conn:prepare(queries.select_site_index))
--TODO: actually let authors edit their bio
stmnt_author_bio = assert(db.conn:prepare([[
SELECT authors.biography FROM authors WHERE authors.name = :author;
]]))
stmnt_author = assert(db.conn:prepare(queries.select_author_index))
return configure(...)
end
local function get_site_home(req)
print("Cache miss, rendering index")
stmnt_index:bind_names{}
local err = util.do_sql(stmnt_index)
local latest = {}
--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 storytags = tags.get(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 = storytags,
})
err = stmnt_index:step()
end
stmnt_index:reset()
return pages.index{
domain = config.domain,
stories = latest
}
end
local function get_author_home(req)
local host = http_request_get_host(req)
local subdomain = host:match("([^\\.]+)")
stmnt_author_bio:bind_names{author=subdomain}
local err = do_sql(stmnt_author_bio)
if err == sql.DONE then
print("No such author")
stmnt_author_bio:reset()
return pages.noauthor{
author = subdomain
}
end
print("err:",err)
assert(err == sql.ROW,"failed to get author:" .. subdomain .. " error:" .. tostring(err))
local data = stmnt_author_bio:get_values()
local bio = data[1]
stmnt_author_bio:reset()
print("Getting author's stories")
stmnt_author:bind_names{author=subdomain}
err = do_sql(stmnt_author)
print("err:",err)
local stories = {}
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)),
tags = tags,
})
err = stmnt_author:step()
end
stmnt_author:reset()
return pages.author_index{
domain=config.domain,
author=subdomain,
stories=stories,
bio=bio
}
end
local function index_get(req)
local method = http_method_text(req)
local host = http_request_get_host(req)
local path = http_request_get_path(req)
--Default home page
local subdomain = host:match("([^\\.]+)")
local text
if host == config.domain then
local cachepath = string.format("%s",config.domain),
text = cache.render(cachepath, function()
return get_site_home(req)
end)
else --author home page
local cachepath = string.format("%s.%s",subdomain,config.domain)
text = cache.render(cachepath, function()
return get_author_home(req)
end)
end
end

View File

@ -1,19 +1,34 @@
print("Really fast print from init.lua")
--Luarocks libraries
local et = require("etlua")
local sql = require("lsqlite3")
local zlib = require("zlib")
if PRODUCTION then
--smr code
local cache = require("cache")
local pages = require("pages")
local util = require("util")
local config = require("config")
local db = require("db")
if config.production then
local function print() end --squash prints
end
--[[
local parser_names = {"plain","imageboard"}
local parsers = {}
for _,v in pairs(parser_names) do
parsers[v] = require("parser_" .. v)
end
local db,cache --databases
local domain = "test.monster:8888" --The domain to write links as
]]
--pages
read_get = require("read_get")
read_post = require("read_post")
preview_post = require("preview_post")
--local db,cache --databases
--local domain = "test.monster:8888" --The domain to write links as
--[[
local pagenames = {
"index",
"author_index",
@ -36,7 +51,9 @@ for k,v in pairs(pagenames) do
pages[v] = assert(et.compile(f:read("*a")))
f:close()
end
]]
--[=[
local queries = {}
--These are all loaded during startup, won't affect ongoing performance.
setmetatable(queries,{
@ -47,13 +64,13 @@ setmetatable(queries,{
return ret
end
})
]=]
---sql queries
local stmnt_index, stmnt_author_index, stmnt_read, stmnt_paste, stmnt_raw
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_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
@ -79,6 +96,7 @@ local function decodeentities(capture)
end
end
--[[
local function sqlassert(...)
local r,errcode,err = ...
if not r then
@ -86,6 +104,7 @@ local function sqlassert(...)
end
return r
end
]]
local function sqlbind(stmnt,call,position,data)
assert(call == "bind" or call == "bind_blob","Bad bind call, call was:" .. call)
@ -99,22 +118,22 @@ end
print("Hello from init.lua")
function configure()
db = sqlassert(sql.open("data/posts.db"))
--db = sqlassert(sql.open_memory())
cache = sqlassert(sql.open_memory())
print("Compiled pages...")
--Test that compression works
--db = sqlassert(sql.open("data/posts.db"))
----db = sqlassert(sql.open_memory())
--cache = sqlassert(sql.open_memory())
--Test that compression works. For some reason, the zlib library
--fails if this is done as a one-liner
local msg = "test message"
local one = zlib.compress(msg)
local two = zlib.decompress(one)
--For some reason, the zlib library fails if this is done as a oneliner
assert(two == msg, "zlib not working as expected")
--Create sql tables
assert(db:exec(queries.create_table_authors))
--assert(db:exec(queries.create_table_authors))
--Create a fake "anonymous" user, so we don't run into trouble
--so that no one runs into touble being able to paste under this account.
assert(db:exec(queries.insert_anon_author))
--assert(db:exec(queries.insert_anon_author))
--If/when an author delets their account, all posts
--and comments by that author are also deleted (on
--delete cascade) this is intentional. This also
@ -122,23 +141,24 @@ function configure()
--an author makes will also be deleted.
--
--Post text uses zlib compression
assert(db:exec(queries.create_table_posts))
--assert(db:exec(queries.create_table_posts))
--Store the raw text so people can download it later, maybe
--we can use it for "download as image" or "download as pdf"
--in the future too. Stil stored zlib compressed
assert(db:exec(queries.create_table_raw_text))
assert(db:exec(queries.create_table_images))
assert(db:exec(queries.create_table_comments))
assert(db:exec(queries.create_table_tags))
assert(db:exec(queries.create_index_tags))
--assert(db:exec(queries.create_table_raw_text))
--assert(db:exec(queries.create_table_images))
--assert(db:exec(queries.create_table_comments))
--assert(db:exec(queries.create_table_tags))
--assert(db:exec(queries.create_index_tags))
--Store a cookie for logged in users. Logged in users can edit
--their own posts.
assert(db:exec(queries.create_table_session))
print("Created db tables")
--assert(db:exec(queries.create_table_session))
--print("Created db tables")
--A cache table to store rendered pages that do not need to be
--rerendered. In theory this could OOM the program eventually and start
--swapping to disk. TODO: fixme
--[=[
assert(cache:exec([[
CREATE TABLE IF NOT EXISTS cache (
path TEXT PRIMARY KEY,
@ -147,62 +167,71 @@ function configure()
dirty INTEGER
);
]]))
]=]
--Select the data we need to display the on the front page
stmnt_index = assert(db:prepare(queries.select_site_index))
--stmnt_index = assert(db:prepare(queries.select_site_index))
--Select the data we need to read a story (and maybe display an edit
--button
stmnt_read = assert(db:prepare(queries.select_post))
--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))
--stmnt_update_views = assert(db:prepare(queries.update_views))
--Retreive comments on a story
stmnt_comments = assert(db:prepare(queries.select_comments))
--stmnt_comments = assert(db:prepare(queries.select_comments))
--Add a new comment to a story
stmnt_comment_insert = assert(db:prepare(queries.insert_comment))
--stmnt_comment_insert = assert(db:prepare(queries.insert_comment))
--TODO: actually let authors edit their bio
stmnt_author_bio = assert(db:prepare([[
--[=[stmnt_author_bio = assert(db.conn:prepare([[
SELECT authors.biography FROM authors WHERE authors.name = :author;
]]))
]=]
--Get the author of a story, used to check when editing that the
--author really owns the story they're trying to edit
stmnt_author_of = assert(db:prepare(queries.select_author_of_post))
--stmnt_author_of = assert(db:prepare(queries.select_author_of_post))
--Get the data we need to display a particular author's latest
--stories
stmnt_author = assert(db:prepare(queries.select_author_index))
--stmnt_author = assert(db:prepare(queries.select_author_index))
--Get the data we need to check if someone can log in
--[=[
stmnt_author_acct = assert(db:prepare([[
SELECT id, salt, passhash FROM authors WHERE name = :name;
]]))
]=]
--Create a new author on the site
stmnt_author_create = assert(db:prepare(queries.insert_author))
--stmnt_author_create = assert(db:prepare(queries.insert_author))
--[=[
stmnt_author_login = assert(db:prepare([[
SELECT name, passhash FROM authors WHERE name = :name;
]]))
]=]
--Create a new post
stmnt_paste = assert(db:prepare(queries.insert_post))
--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
stmnt_download = assert(db:prepare(queries.select_download))
--When we update a post, store the plaintext again
stmnt_update_raw = assert(db:prepare(queries.update_raw))
--stmnt_update_raw = assert(db:prepare(queries.update_raw))
--Should we really reset the update time every time someone makes a post?
--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))
--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
@ -223,6 +252,7 @@ function configure()
SET dirty = 1
WHERE path = :path;
]])
]=]
--[=[
]=]
print("finished running configure()")
@ -296,6 +326,7 @@ local function get_tags(id)
until false
end
--[=[
local function dirty_cache(url)
print("Dirtying cache:",url)
stmnt_dirty_cache:bind_names{
@ -304,6 +335,7 @@ local function dirty_cache(url)
err = do_sql(stmnt_dirty_cache)
stmnt_dirty_cache:reset()
end
]=]
--[[
@ -356,6 +388,7 @@ local function get_session(req)
return author,authorid
end
--[=[
--Render a page, with cacheing. If you need to dirty a cache, call dirty_cache()
local function render(pagename,callback)
print("Running render...")
@ -388,7 +421,9 @@ local function render(pagename,callback)
stmnt_insert_cache:reset()
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 = {}
@ -404,6 +439,7 @@ local function parse_tags(str)
end
return tags
end
]=]
function home(req)
print("Hello from lua!")
@ -414,7 +450,7 @@ function home(req)
local text
if host == domain then
--Default home page
text = render(string.format("%s",domain),function()
text = cache.render(string.format("%s",domain),function()
print("Cache miss, rendering index")
stmnt_index:bind_names{}
local err = do_sql(stmnt_index)
@ -442,7 +478,7 @@ function home(req)
else
--Home page for an author
local subdomain = host:match("([^\\.]+)")
text = render(string.format("%s.%s",subdomain,domain),function()
text = cache.render(string.format("%s.%s",subdomain,domain),function()
print("Cache miss, rendering author:" .. subdomain)
stmnt_author_bio:bind_names{author=subdomain}
local err = do_sql(stmnt_author_bio)
@ -505,7 +541,7 @@ function claim(req)
local text
if method == "GET" then
--Get the page to claim a name
text = render(string.format("%s/_claim",domain),function()
text = cache.render(string.format("%s/_claim",domain),function()
print("cache miss, rendering claim page")
return pages.claim{err=""}
end)
@ -578,7 +614,7 @@ function paste(req)
return
else
--For an anonymous user
ret = render(string.format("%s/_paste",host),function()
ret = cache.render(string.format("%s/_paste",host),function()
print("Cache missing, rendering post page")
return pages.paste{
domain = domain,
@ -686,8 +722,8 @@ function paste(req)
local loc = string.format("https://%s/%s",domain,url)
http_response_header(req,"Location",loc)
http_response(req,303,"")
dirty_cache(string.format("%s/%s",domain,url))
dirty_cache(string.format("%s",domain))
cache.dirty(string.format("%s/%s",domain,url))
cache.dirty(string.format("%s",domain))
return
elseif err == sql.ERROR or err == sql.MISUSE then
ret = "Failed to paste: " .. tostring(err)
@ -748,9 +784,9 @@ function paste(req)
end
http_response_header(req,"Location",loc)
http_response(req,303,"")
dirty_cache(string.format("%s.%s",author,domain))
dirty_cache(string.format("%s/%s",domain,url))
dirty_cache(string.format("%s",domain))
cache.dirty(string.format("%s.%s",author,domain))
cache.dirty(string.format("%s/%s",domain,url))
cache.dirty(string.format("%s",domain))
return
elseif err == sql.ERROR or err == sql.MISUSE then
ret = "Failed to paste: " .. tostring(err)
@ -778,7 +814,7 @@ local function read_story(host,path,idp,show_comments,iam)
}
print("update:",do_sql(stmnt_update_views))
stmnt_update_views:reset()
dirty_cache(cachestr)
cache.dirty(cachestr)
print("cachestr was:",cachestr)
local readstoryf = function()
stmnt_read:bind_names{
@ -830,7 +866,7 @@ local function read_story(host,path,idp,show_comments,iam)
--which is not the user's, from whoever loaded the cache last) to fix this bug, don't cache
--pages when the user is logged in. All non-logged-in users can see the same page no problem.
if not iam then
return render(cachestr,readstoryf)
return cache.render(cachestr,readstoryf)
else
return readstoryf()
end
@ -841,6 +877,8 @@ function read(req)
local path = http_request_get_path(req)
local method = http_method_text(req)
if method == "GET" then
read_get(req)
--[=[
local idp = string.sub(path,2)--remove leading "/"
assert(string.len(path) > 0,"Tried to read 0-length story id")
local author, authorid = get_session(req)
@ -902,7 +940,10 @@ function read(req)
assert(text)
http_response(req,200,text)
return
]=]
elseif method == "POST" then
read_post(req)
--[=[
--We're posting a comment
http_request_populate_post(req)
http_populate_cookies(req)
@ -928,11 +969,12 @@ function read(req)
http_response(req,500,"Internal error, failed to post comment. Go back and try again.")
else
--When we post a comment, we need to dirty the cache for the "comments displayed" page.
dirty_cache(string.format("%s%s?comments=1",host,path))
cache.dirty(string.format("%s%s?comments=1",host,path))
local redir = string.format("https://%s%s?comments=1", domain, path)
http_response_header(req,"Location",redir)
http_response(req,303,"")
end
]=]
end
end
@ -949,7 +991,7 @@ function login(req)
local text
if method == "GET" then
--Just give them the login page
text = render(string.format("%s/_login",domain),function()
text = cache.render(string.format("%s/_login",domain),function()
return pages.login{
err = "",
}
@ -1093,9 +1135,9 @@ function edit(req)
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
dirty_cache(string.format("%s",domain)) -- The site index (ex, if the author changed the paste from their's to "Anonymous", the cache should reflect that).
dirty_cache(string.format("%s.%s",author,domain)) -- The author's index, same reasoning as above.
cache.dirty(string.format("%s/%s",domain,id_enc)) -- This place to read this post
cache.dirty(string.format("%s",domain)) -- The site index (ex, if the author changed the paste from their's to "Anonymous", the cache should reflect that).
cache.dirty(string.format("%s.%s",author,domain)) -- The author's index, same reasoning as above.
http_response_header(req,"Location",loc)
http_response(req,303,"")
return
@ -1112,10 +1154,10 @@ end
function teardown()
print("Exiting...")
if db then
db:close()
db.close()
end
if cache then
cache:close()
cache.close()
end
print("Finished lua teardown")
end
@ -1127,6 +1169,7 @@ function download(req)
http_request_populate_qs(req)
local story = assert(http_argument_get_string(req,"story"))
local story_id = decode_id(story)
print("Downloading", story_id)
stmnt_download:bind_names{
postid = story_id
}
@ -1137,6 +1180,7 @@ function download(req)
stmnt_download:reset()
return
end
assert(err == sql.ROW, "after doing download sql, result was not a row, was:" .. tostring(err))
local txt_compressed, title = unpack(stmnt_download:get_values())
local text = zlib.decompress(txt_compressed)
stmnt_download:reset()
@ -1147,6 +1191,8 @@ function download(req)
end
function preview(req)
preview_post(req)
--[[
print("We want to preview a paste!")
local host = http_request_get_host(req)
local path = http_request_get_path(req)
@ -1170,6 +1216,7 @@ function preview(req)
tags = tags,
}
http_response(req,200,ret)
]]
end
function search(req)

11
src/lua/login_get.lua Normal file
View File

@ -0,0 +1,11 @@
local config = require("config")
local function login_get(req)
--Just give them the login page
return cache.render(string.format("%s/_login",domain),function()
return pages.login{
err = "",
}
end)
end

58
src/lua/login_post.lua Normal file
View File

@ -0,0 +1,58 @@
local sql = require("lsqlite3")
local db = require("db")
local util = require("util")
local stmnt_author_acct
local oldconfigure = configure
function configure(...)
--Get the data we need to check if someone can log in
stmnt_author_acct = assert(db.conn:prepare([[
SELECT id, salt, passhash FROM authors WHERE name = :name;
]]))
return configure(...)
end
local function login_post(req)
--Try to log in
http_populate_multipart_form(req)
local name = assert(http_argument_get_string(req,"user"))
local pass = assert(http_file_get(req,"pass"))
stmnt_author_acct:bind_names{
name = name
}
local text
local err = util.do_sql(stmnt_author_acct)
if err == sql.ROW then
local id, salt, passhash = unpack(stmnt_author_acct:get_values())
stmnt_author_acct:reset()
local todigest = salt .. pass
local hash = sha3(todigest)
if hash == passhash then
local session = start_session(id)
http_response_cookie(req,"session",session,"/",0,0)
local loc = string.format("https://%s.%s",name,domain)
http_response_header(req,"Location",loc)
http_response(req,303,"")
return
else
text = pages.login{
err = "Incorrect username or password"
}
end
elseif err == sql.DONE then --Allows user enumeration, do we want this?
--Probably not a problem since all passwords are forced to be "good"
stmnt_author_acct:reset()
text = pages.login{
err = "Failed to find user:" .. name
}
else
stmnt_author_acct:reset()
error("Other sql error during login")
end
http_response(req,200,text)
end
return login_post

25
src/lua/pages.lua Normal file
View File

@ -0,0 +1,25 @@
local et = require("etlua")
local pagenames = {
"index",
"author_index",
"claim",
"paste",
"edit",
"read",
"nostory",
"cantedit",
"noauthor",
"login",
"author_paste",
"author_edit",
"search",
}
local pages = {}
for k,v in pairs(pagenames) do
print("Compiling page:",v)
local f = assert(io.open("pages/" .. v .. ".etlua","r"))
pages[v] = assert(et.compile(f:read("*a")))
f:close()
end
return pages

7
src/lua/parsers.lua Normal file
View File

@ -0,0 +1,7 @@
local parser_names = {"plain","imageboard"}
local parsers = {}
for _,v in pairs(parser_names) do
parsers[v] = require("parser_" .. v)
end
return parsers

81
src/lua/paste_get.lua Normal file
View File

@ -0,0 +1,81 @@
local config = require("config")
local function paste_get(req)
--Get the paste page
local host = http_request_get_host(req)
local text
local author,_ = get_session(req)
if host == config.domain and author then
http_response_header(req,"Location",string.format("https://%s.%s/_paste",author,config.domain))
http_response(req,303,"")
return
elseif host == config.domain and author == nil then
text = cache.render(string.format("%s/_paste",host),function()
print("Cache missing, rendering post page")
return pages.paste{
domain = domain,
err = "",
}
end)
http_response(req,200,text)
elseif host ~= config.domain and author then
elseif host ~= config.domain and author == nil then
else
error(string.format(
"Unable to find a good case for paste:%s,%s,%s",
host,
config.domain,
author
))
end
if host == config.domain then
local author,_ = get_session(req)
if author then
http_response_header(req,"Location",string.format("https://%s.%s/_paste",author,domain))
http_response(req,303,"")
return
else
--For an anonymous user
ret = cache.render(string.format("%s/_paste",host),function()
print("Cache missing, rendering post page")
return pages.paste{
domain = domain,
err = "",
}
end)
end
else
--Or for someone that's logged in
print("Looks like a logged in user wants to paste!")
local subdomain = host:match("([^%.]+)")
local author,_ = get_session(req)
print("subdomain:",subdomain,"author:",author)
--If they try to paste as an author, but are on the
--wrong subdomain, or or not logged in, redirect them
--to the right place. Their own subdomain for authors
--or the anonymous paste page for not logged in users.
if author == nil then
http_response_header(req,"Location","https://"..domain.."/_paste")
http_response(req,303,"")
return
end
if author ~= subdomain then
http_response_header(req,"Location",string.format("https://%s.%s/_paste",author,domain))
http_response(req,303,"")
return
end
assert(author == subdomain,"someone wants to paste as someone else")
--We're where we want to be, serve up this users's
--paste page. No cache, because how often is a user
--going to paste?
ret = pages.author_paste{
domain = domain,
user = author,
text = "",
err = "",
}
end
end

32
src/lua/preview_post.lua Normal file
View File

@ -0,0 +1,32 @@
local parsers = require("parsers")
local tags = require("tags")
local util = require("util")
local pages = require("pages")
local function preview_post(req)
print("We want to preview a paste!")
local host = http_request_get_host(req)
local path = http_request_get_path(req)
http_request_populate_post(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 = http_argument_get_string(req,"tags")
local tags = {}
if tag_str then
tags = util.parse_tags(tag_str)
end
print("title:",title,"text:",text,"markup:",markup)
local parsed = parsers[markup](text)
local ret = pages.read{
domain = domain,
title = title,
author = "preview",
idp = "preview",
text = parsed,
tags = tags,
}
http_response(req,200,ret)
end
return preview_post

13
src/lua/queries.lua Normal file
View File

@ -0,0 +1,13 @@
local queries = {}
setmetatable(queries,{
__index = function(self,key)
local f = assert(io.open("sql/" .. key .. ".sql","r"))
local ret = f:read("*a")
f:close()
return ret
end
})
return queries

140
src/lua/read_get.lua Normal file
View File

@ -0,0 +1,140 @@
local sql = require("sqlite3")
local session = require("session")
local tags = require("tags")
local db = require("db")
local queries = require("queries")
local stmnt_read, stmnt_update_views, stmnt_comments
local oldconfigure = configure
function configure(...)
stmnt_read = assert(db.conn:prepare(queries.select_post))
stmnt_update_views = assert(db.conn:prepare(queries.update_views))
stmnt_comments = assert(db.conn:prepare(queries.select_comments))
return configure(...)
end
--[[
Increases a story's hit counter by 1
]]
local function add_view(storyid)
stmnt_update_views:bind_names{
id = storyid
}
local err = do_sql(stmnt_update_views)
assert(err == sql.DONE, "Failed to update view counter:"..tostring(err))
stmnt_update_views:reset()
end
--[[
Populates ps with story settings, returns true if story was found,
or nil if it wasn't
]]
local function populate_ps_story(req,ps)
--Make sure our story exists
stmnt_read:bind_names{
id = ps.storyid
}
local err = do_sql(stmnt_read)
if err == sql.DONE then
--We got no story
stmnt_read:reset()
return nil
end
--If we've made it here, we have a story. Populate our settings
--with title, text, ect.
assert(err == sql.ROW)
add_view(storyid)
local title, storytext, tauthor, isanon, authorname, views = unpack(
stmnt_read:get_values()
)
ps.title = title
ps.storytext = zlib.decompress(storytext)
ps.tauthor = tauthor
ps.isanon = isanon == 1
ps.authorname = authorname
ps.views = views
stmnt_read:reset()
--Tags
ps.tags = tags.get(id)
return true
end
--[[
Get the comments for a story
]]
local function get_comments(req,ps)
end
--[[
The author is viewing their own story, give them an edit button
]]
local function read_get_author(req,storyid,author,authorid,comments)
end
--[[
An author is viewing a story, allow them to post comments as themselves
]]
local function read_get_loggedin(req,ps)
if ps.tauthor == ps.authorid then
--The story exists and we're logged in as the
--owner, display the edit button
return read_get_author(req,ps)
end
return pages.read(ps)
end
local function read_get(req)
--Pages settings
local ps = {
host = http_request_get_host(req),
path = http_request_get_path(req),
method = http_method_text(req),
}
--Get our story id
assert(string.len(ps.path) > 0,"Tried to read 0-length story id")
local idp = string.sub(ps.path,2)--remove leading "/"
ps.storyid = util.decode_id(idp)
--If we're logged in, set author and authorid
local author, authorid = session.get(req)
if author and authorid then
ps.author = author
ps.authorid = authorid
end
--If we need to show comments
http_request_populate_qs(req)
ps.show_comments = http_argument_get_string(req,"comments")
if ps.show_comments then
ps.comments = get_comments(req,ps)
end
--normal story display
if (not ps.author) then
local cachestr = string.format("%s%s%s",
ps.host,
ps.path,
ps.show_comments and "?comments=1" or ""
)
local text = cache.render(cachestr,function()
populate_ps_story(req,ps)
return pages.read(ps)
end)
else --we are logged in, don't cache
populate_ps_story(req,ps)
ps.owner = (ps.author == ps.tauthor)
text = pages.read(ps)
end
assert(text)
http_response(req,200,text)
return
end
return read_get

53
src/lua/read_post.lua Normal file
View File

@ -0,0 +1,53 @@
local sql = require("sqlite3")
local cache = require("cache")
local session = require("session")
local util = require("util")
local db = require("db")
local queries = require("queries")
local config = require("config")
local stmnt_comment_insert
local oldconfigure = configure
function configure(...)
stmnt_comment_insert = assert(db.conn:prepare(queries.insert_comment))
return oldconfigure(...)
end
local function read_post(req)
local host = http_request_get_host(req)
local path = http_request_get_path(req)
--We're posting a comment
http_request_populate_post(req)
http_populate_cookies(req)
local author, authorid = session.get(req)
local comment_text = assert(http_argument_get_string(req,"text"))
local pasteas = assert(http_argument_get_string(req,"postas"))
local idp = string.sub(path,2)--remove leading "/"
local id = util.decode_id(idp)
local isanon = 1
--Even if an author is logged in, they may post their comment anonymously
if author and pasteas ~= "Anonymous" then
isanon = 0
end
stmnt_comment_insert:bind_names{
postid=id,
authorid = author and authorid or -1,
isanon = isanon,
comment_text = comment_text,
}
local err = util.do_sql(stmnt_comment_insert)
stmnt_comment_insert:reset()
if err ~= sql.DONE then
http_response(req,500,"Internal error, failed to post comment. Go back and try again.")
else
--When we post a comment, we need to dirty the cache for the "comments displayed" page.
cache.dirty(string.format("%s%s?comments=1",host,path))
local redir = string.format("https://%s%s?comments=1", domain, path)
http_response_header(req,"Location",redir)
http_response(req,303,"")
end
end
return read_post

2
src/lua/render.lua Normal file
View File

@ -0,0 +1,2 @@

67
src/lua/session.lua Normal file
View File

@ -0,0 +1,67 @@
local sql = require("lsqlite3")
local db = require("db")
local util = require("util")
local oldconfigure = configure
local stmnt_get_session, stmnt_insert_session
function configure(...)
stmnt_get_session = assert(db:prepare(queries.select_valid_sessions))
return oldconfigure(...)
end
local session = {}
--[[
Retreive the name and authorid of the logged in person,
or nil+error message if not logged in
]]
function session.get(req)
http_populate_cookies(req)
local sessionid = http_request_cookie(req,"session")
if sessionid == nil then
return nil, "No session cookie passed by client"
end
stmnt_get_session:bind_names{
key = sessionid
}
local err = util.do_sql(stmnt_get_session)
if err ~= sql.ROW then
return nil, "No such session by logged in users"
end
print("get session err:",err)
local data = stmnt_get_session:get_values()
stmnt_get_session:reset()
local author = data[1]
local authorid = data[2]
return author,authorid
end
--[[
Start a session for someone who logged in
]]
local function start_session(who)
local rngf = assert(io.open("/dev/urandom","rb"))
local session_t = {}
for i = 1,64 do
local r = string.byte(rngf:read(1))
local s = string.char((r % 26) + 65)
table.insert(session_t,s)
end
local session = table.concat(session_t)
rngf:close()
print("sessionid:",session)
print("authorid:",who)
stmnt_insert_session:bind_names{
sessionid = session,
authorid = who
}
local err = util.do_sql(stmnt_insert_session)
stmnt_insert_session:reset()
print("Err:",err)
assert(err == sql.DONE)
return session
end
return session

54
src/lua/tags.lua Normal file
View File

@ -0,0 +1,54 @@
local db = require("db")
local queries = require("queries")
local util = require("util")
local tags = {}
local stmnt_get_tags, stmnt_ins_tag, stmnt_drop_tags
local oldconfigure = configure
function configure(...)
--Tags for a story
stmnt_ins_tag = assert(db.conn:prepare(queries.insert_tag))
stmnt_get_tags = assert(db.conn:prepare(queries.select_tags))
stmnt_drop_tags = assert(db.conn:prepare(queries.delete_tags))
return configure(...)
end
function tags.get(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
function tags.set(storyid,tags)
assert(stmnt_drop_tags:bind_names{postid = storyid} == sql.OK)
util.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 = util.do_sql(stmnt_ins_tag)
stmnt_ins_tag:reset()
end
end
return tags

103
src/lua/util.lua Normal file
View File

@ -0,0 +1,103 @@
local util = {}
--[[
Runs an sql query and receives the 3 arguments back, prints a nice error
message on fail, and returns true on success.
]]
function util.sqlassert(...)
local r,errcode,err = ...
if not r then
error(string.format("%d: %s",errcode, err))
end
return r
end
--[[
Continuously tries to perform an sql statement until it goes through
]]
function util.do_sql(stmnt)
if not stmnt then error("No statement",2) end
local err
repeat
err = stmnt:step()
print("After stepping, err is", err)
if err == sql.BUSY then
coroutine.yield()
end
until(err ~= sql.BUSY)
return err
end
--see https://perishablepress.com/stop-using-unsafe-characters-in-urls/
--no underscore because we use that for our operative pages
local url_characters =
[[abcdefghijklmnopqrstuvwxyz]]..
[[ABCDEFGHIJKLMNOPQRSTUVWXYZ]]..
[[0123456789]]..
[[$-+!*'(),]]
local url_characters_rev = {}
for i = 1,string.len(url_characters) do
url_characters_rev[string.sub(url_characters,i,i)] = i
end
--[[
Encode a number to a shorter HTML-safe url path
]]
function util.encode_id(number)
local result = {}
local charlen = string.len(url_characters)
repeat
local pos = (number % charlen) + 1
number = math.floor(number / charlen)
table.insert(result,string.sub(url_characters,pos,pos))
until number == 0
return table.concat(result)
end
--[[
Given a short HTML-safe url path, convert it to a storyid
]]
function util.decode_id(s)
local res, id = pcall(function()
local n = 0
local charlen = string.len(url_characters)
for i = 1,string.len(s) do
local char = string.sub(s,i,i)
local pos = url_characters_rev[char] - 1
n = n + (pos*math.pow(charlen,i-1))
end
return n
end)
if res then
return id
else
error("Failed to decode id:" .. s)
end
end
--[[
Parses a semicolon seperated string into it's parts:
1. seperates by semicolon
2. trims whitespace
3. lowercases
4. capitalizes the first letter.
Returns an array of zero or more strings.
There is no blank tag, parsing "one;two;;three" will yield
{"one","two","three"}
]]
function util.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,"^.",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
return util

View File

@ -1,19 +1,16 @@
<% assert(author,"No author specified") %>
<% assert(bio,"No bio included") %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<% assert(author,"No author specified") %> <% assert(bio,"No bio included") %> <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8">
<title>&#x1f351;</title>
<link href="/_css/milligram.css" rel="stylesheet">
<link href="/_css/style.css" rel="stylesheet">
</head>
<body class="container">
<main class="wrapper">
<h1 class="title">
<a href="https://<%= author %>.<%= domain %>"><%= author %></a>.<a href="https://<%= domain %>"><%= domain %></a>
</h1>
<div class="container">
<a href="/_paste" class="button">New paste</a>
</div>
<div class="content">
<%= bio %>
</div>
@ -46,6 +43,7 @@
<footer class="footer">
</footer>
</main>
</body>
<body>

View File

@ -12,6 +12,7 @@
#include "libkore.h"
#include "libcrypto.h"
#include <dirent.h>
#include <kore/seccomp.h>
int home(struct http_request *);
int post_story(struct http_request *);
@ -38,6 +39,15 @@ lua_State *L;
static / _claim claim
*/
/*Allow seccomp things for luajit and sqlite*/
KORE_SECCOMP_FILTER("app",
KORE_SYSCALL_ALLOW(pread64),
KORE_SYSCALL_ALLOW(pwrite64),
KORE_SYSCALL_ALLOW(fdatasync),
KORE_SYSCALL_ALLOW(unlinkat),
KORE_SYSCALL_ALLOW(mremap)
);
int
errhandeler(lua_State *L){
printf("Error: %s\n",lua_tostring(L,1));
@ -66,8 +76,10 @@ do_lua(struct http_request *req, const char *name){
printf("About to pcall\n");
int err = lua_pcall(L,1,0,-3);
if(err != LUA_OK){
size_t retlen;
const char *ret = lua_tolstring(L,-1,&retlen);
printf("Failed to run %s: %s\n",name,lua_tostring(L,-1));
http_response(req, 500, NULL, 0);
http_response(req, 500, ret, retlen);
lua_pop(L,lua_gettop(L));
return (KORE_RESULT_OK);
}

View File

@ -1,3 +1,12 @@
/*
If/when an author delets their account, all posts
and comments by that author are also deleted (on
delete cascade) this is intentional. This also
means that all comments by other users on a post
an author makes will also be deleted.
Post text uses zlib compression
*/
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
post_text BLOB,

View File

@ -1,3 +1,8 @@
/*
Store the raw text so people can download it later, maybe
we can use it for "download as image" or "download as pdf"
in the future too. Stil stored zlib compressed
*/
CREATE TABLE IF NOT EXISTS raw_text (
id INTEGER PRIMARY KEY REFERENCES posts(id) ON DELETE CASCADE,
post_text BLOB,

View File

@ -1,3 +1,8 @@
/*
Store a cookie for logged in users. Logged in users can edit
their own posts.
*/
CREATE TABLE IF NOT EXISTS sessions (
key TEXT PRIMARY KEY,
author REFERENCES authors(id) ON DELETE CASCADE,

View File

@ -1,3 +1,7 @@
/*
Create a fake "anonymous" user, so
that no one runs into touble being able to paste under this account.
*/
INSERT OR IGNORE INTO authors (
id,
name,

View File

@ -1,3 +1,4 @@
/* Add a new comment to a story */
INSERT INTO comments(
postid,
author,

View File

@ -1,3 +1,5 @@
/* Get the data we need to display a particular author's latest stories */
SELECT
posts.id,
posts.post_title,

View File

@ -1,3 +1,7 @@
/*
Get the author of a story, used to check when editing that the
author really owns the story they're trying to edit
*/
SELECT
authors.id,
authors.name

View File

@ -1,3 +1,4 @@
/* Retreive comments on a story */
SELECT
authors.name,
comments.isanon,

View File

@ -1,3 +1,7 @@
/*
Select the data we need to read a story (and maybe display an edit button)
*/
SELECT
post_title,
post_text,

View File

@ -1,3 +1,4 @@
/* Select the data we need to display the on the front page */
SELECT
posts.id,
posts.post_title,

View File

@ -1 +1,2 @@
/* Update the view counter when someone reads a story */
UPDATE posts SET views = views + 1 WHERE id = :id;