started working on unlisted posts

This commit is contained in:
Robin Malley 2021-01-10 21:34:13 +00:00
parent 7d71b119c1
commit 85a730ebcb
17 changed files with 136 additions and 22 deletions

View File

@ -29,7 +29,8 @@ p,.tag-list{margin-bottom:0px}
flex:10 10 auto; flex:10 10 auto;
translate: -100%; translate: -100%;
} }
.column-0{margin-right:5px;} .column-0{margin-right:5px}
.label-inline{margin:0.5rem}
@media (prefers-color-scheme: dark){ @media (prefers-color-scheme: dark){
body, input, select, textarea, pre, code{ body, input, select, textarea, pre, code{

View File

@ -25,6 +25,8 @@ validator v_storyid regex [a-zA-Z0-9$+!*'(),-]+
validator v_subdomain regex [a-z0-9]{1,30} validator v_subdomain regex [a-z0-9]{1,30}
validator v_markup regex (plain|imageboard) validator v_markup regex (plain|imageboard)
validator v_bool regex (0|1) validator v_bool regex (0|1)
validator v_checkbox regex (|on)
validator v_hex_128 regex [0-9a-f]{128}
domain * { domain * {
attach tls attach tls
@ -65,6 +67,7 @@ domain * {
validate pasteas v_subdomain validate pasteas v_subdomain
validate markup v_markup validate markup v_markup
validate tags v_any validate tags v_any
validate unlisted v_checkbox
} }
params post /_paste { params post /_paste {
validate title v_any validate title v_any
@ -72,6 +75,7 @@ domain * {
validate pasteas v_subdomain validate pasteas v_subdomain
validate markup v_markup validate markup v_markup
validate tags v_any validate tags v_any
validate unlisted v_checkbox
} }
params post /_preview { params post /_preview {
validate title v_any validate title v_any
@ -79,12 +83,14 @@ domain * {
validate pasteas v_subdomain validate pasteas v_subdomain
validate markup v_markup validate markup v_markup
validate tags v_any validate tags v_any
validate unlisted v_checkbox
} }
params get /_search { params get /_search {
validate q v_any validate q v_any
} }
params get ^/[^_].* { params get ^/[^_].* {
validate comments v_bool validate comments v_bool
validate pwd v_hex_128
} }
params post ^/[^_].* { params post ^/[^_].* {
validate text v_any validate text v_any

View File

@ -32,6 +32,7 @@ local function edit_post(req)
local text = assert(http_argument_get_string(req,"text")) local text = assert(http_argument_get_string(req,"text"))
local pasteas = assert(http_argument_get_string(req,"pasteas")) local pasteas = assert(http_argument_get_string(req,"pasteas"))
local markup = assert(http_argument_get_string(req,"markup")) local markup = assert(http_argument_get_string(req,"markup"))
local unlisted = http_argument_get_string(req,"unlisted") == "on"
local tags_str = http_argument_get_string(req,"tags") local tags_str = http_argument_get_string(req,"tags")
stmnt_author_of:bind_names{ stmnt_author_of:bind_names{
id = storyid id = storyid
@ -61,11 +62,14 @@ local function edit_post(req)
assert(stmnt_update:bind_blob(2,compr) == 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(3,pasteas == "anonymous" and 1 or 0) == sql.OK)
assert(stmnt_update:bind(4,storyid) == sql.OK) assert(stmnt_update:bind(4,storyid) == sql.OK)
assert(stmnt_update:bind(5,unlisted) == sql.OK)
assert(util.do_sql(stmnt_update) == sql.DONE, "Failed to update text") assert(util.do_sql(stmnt_update) == sql.DONE, "Failed to update text")
stmnt_update:reset() stmnt_update:reset()
tagslib.set(storyid,tags) tagslib.set(storyid,tags)
local id_enc = util.encode_id(storyid) local id_enc = util.encode_id(storyid)
local loc = string.format("https://%s/%s",config.domain,id_enc) local loc = string.format("https://%s/%s",config.domain,id_enc)
--Turning something from not unlisted to unlisted should dirty all these
--places anyway, so the post can now be hidden.
cache.dirty(string.format("%s/%s",config.domain,id_enc)) -- This place to read this post 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",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. cache.dirty(string.format("%s.%s",author,config.domain)) -- The author's index, same reasoning as above.

View File

@ -14,11 +14,18 @@ local stmnt_raw,stmnt_paste
local oldconfigure = configure local oldconfigure = configure
function configure(...) function configure(...)
stmnt_paste = assert(db.conn:prepare(queries.insert_post)) stmnt_paste = assert(db.conn:prepare(queries.insert_post),db.conn:errmsg())
stmnt_raw = assert(db.conn:prepare(queries.insert_raw)) stmnt_raw = assert(db.conn:prepare(queries.insert_raw),db.conn:errmsg())
return oldconfigure(...) return oldconfigure(...)
end end
local function get_random_bytes(n)
local f = assert(io.open("/dev/urandom","r"))
local ret = assert(f:read(n))
assert(f:close())
return ret
end
local function anon_paste(req,ps) local function anon_paste(req,ps)
--Public paste --Public paste
--[[ --[[
@ -34,16 +41,24 @@ local function anon_paste(req,ps)
--Don't store this information for now, until I come up --Don't store this information for now, until I come up
--with a more elegent solution. --with a more elegent solution.
log(LOG_DEBUG,string.format("new story: %q, length: %d",ps.title,string.len(ps.text)))
print("Unlisted:",ps.unlisted)
local textsha3 = sha3(ps.text .. get_random_bytes(32))
util.sqlbind(stmnt_paste,"bind_blob",1,ps.text) util.sqlbind(stmnt_paste,"bind_blob",1,ps.text)
util.sqlbind(stmnt_paste,"bind",2,ps.title) util.sqlbind(stmnt_paste,"bind",2,ps.title)
util.sqlbind(stmnt_paste,"bind",3,-1) util.sqlbind(stmnt_paste,"bind",3,-1)
util.sqlbind(stmnt_paste,"bind",4,true) util.sqlbind(stmnt_paste,"bind",4,true)
util.sqlbind(stmnt_paste,"bind_blob",5,"") util.sqlbind(stmnt_paste,"bind_blob",5,"")
util.sqlbind(stmnt_paste,"bind",6,ps.unlisted)
util.sqlbind(stmnt_paste,"bind_blob",7,textsha3)
err = util.do_sql(stmnt_paste) err = util.do_sql(stmnt_paste)
stmnt_paste:reset() stmnt_paste:reset()
if err == sql.DONE then if err == sql.DONE then
local rowid = stmnt_paste:last_insert_rowid() local rowid = stmnt_paste:last_insert_rowid()
local url = util.encode_id(rowid) local url = util.encode_id(rowid)
if ps.unlisted then
url = url .. "?pwd=" .. util.encode_unlisted(textsha3)
end
assert(stmnt_raw:bind(1,rowid) == sql.OK) assert(stmnt_raw:bind(1,rowid) == sql.OK)
assert(stmnt_raw:bind_blob(2,ps.raw) == sql.OK) assert(stmnt_raw:bind_blob(2,ps.raw) == sql.OK)
assert(stmnt_raw:bind(3,ps.markup) == sql.OK) assert(stmnt_raw:bind(3,ps.markup) == sql.OK)
@ -61,10 +76,12 @@ local function anon_paste(req,ps)
end end
tags.set(rowid,ps.tags) tags.set(rowid,ps.tags)
local loc = string.format("https://%s/%s",config.domain,url) local loc = string.format("https://%s/%s",config.domain,url)
if not ps.unlisted then
cache.dirty(string.format("%s/%s",config.domain,url))
cache.dirty(string.format("%s",config.domain))
end
http_response_header(req,"Location",loc) http_response_header(req,"Location",loc)
http_response(req,303,"") http_response(req,303,"")
cache.dirty(string.format("%s/%s",config.domain,url))
cache.dirty(string.format("%s",config.domain))
return return
elseif err == sql.ERROR or err == sql.MISUSE then elseif err == sql.ERROR or err == sql.MISUSE then
error("Failed to paste:" .. tostring(err)) error("Failed to paste:" .. tostring(err))
@ -93,6 +110,8 @@ local function author_paste(req,ps)
assert(stmnt_paste:bind(3,authorid) == sql.OK) assert(stmnt_paste:bind(3,authorid) == sql.OK)
assert(stmnt_paste:bind(4,asanon == "anonymous") == sql.OK) assert(stmnt_paste:bind(4,asanon == "anonymous") == sql.OK)
assert(stmnt_paste:bind_blob(5,"") == sql.OK) assert(stmnt_paste:bind_blob(5,"") == sql.OK)
util.sqlbind(stmnt_paste,"bind",6,ps.unlisted)
util.sqlbind(stmnt_paste,"bind_blob",7,sha3(ps.text))
err = util.do_sql(stmnt_paste) err = util.do_sql(stmnt_paste)
stmnt_paste:reset() stmnt_paste:reset()
if err == sql.DONE then if err == sql.DONE then
@ -120,11 +139,13 @@ local function author_paste(req,ps)
else else
loc = string.format("https://%s.%s/%s",author,config.domain,url) loc = string.format("https://%s.%s/%s",author,config.domain,url)
end end
if not ps.unlisted then
cache.dirty(string.format("%s.%s",author,config.domain))
cache.dirty(string.format("%s/%s",config.domain,url))
cache.dirty(string.format("%s",config.domain))
end
http_response_header(req,"Location",loc) http_response_header(req,"Location",loc)
http_response(req,303,"") http_response(req,303,"")
cache.dirty(string.format("%s.%s",author,config.domain))
cache.dirty(string.format("%s/%s",config.domain,url))
cache.dirty(string.format("%s",config.domain))
return return
elseif err == sql.ERROR or err == sql.MISUSE then elseif err == sql.ERROR or err == sql.MISUSE then
error("Failed to paste: " .. tostring(err) .. " : " .. db.conn:errmsg()) error("Failed to paste: " .. tostring(err) .. " : " .. db.conn:errmsg())
@ -171,6 +192,8 @@ local function paste_post(req)
--Always sanatize the title with the plain parser. no markup --Always sanatize the title with the plain parser. no markup
--in the title. --in the title.
ps.title = parsers.plain(title) ps.title = parsers.plain(title)
local unlisted = http_argument_get_string(req,"unlisted")
ps.unlisted = unlisted == "on" --might be nil
if host == config.domain then if host == config.domain then
anon_paste(req,ps) anon_paste(req,ps)
else else

View File

@ -39,8 +39,10 @@ or nil if it wasn't
local function populate_ps_story(req,ps) local function populate_ps_story(req,ps)
--Make sure our story exists --Make sure our story exists
stmnt_read:bind_names{ stmnt_read:bind_names{
id = ps.storyid id = ps.storyid,
} }
print("populating, hash was:",ps.hash)
stmnt_read:bind(2,ps.hash or "")
local err = util.do_sql(stmnt_read) local err = util.do_sql(stmnt_read)
if err == sql.DONE then if err == sql.DONE then
--We got no story --We got no story
@ -99,6 +101,7 @@ local function read_get(req)
ps.storyid = util.decode_id(ps.idp) ps.storyid = util.decode_id(ps.idp)
add_view(ps.storyid) add_view(ps.storyid)
--If we're logged in, set author and authorid --If we're logged in, set author and authorid
local author, authorid = session.get(req) local author, authorid = session.get(req)
if author and authorid then if author and authorid then
@ -113,15 +116,34 @@ local function read_get(req)
if ps.show_comments then if ps.show_comments then
ps.comments = get_comments(req,ps) ps.comments = get_comments(req,ps)
end end
--If this post is unlisted, get the hash
local hashstr = http_argument_get_string(req,"pwd")
print("hashstr was:",hashstr)
if hashstr then
ps.hash = util.decode_unlisted(hashstr)
end
local text local text
--normal story display --normal story display
if (not ps.loggedauthor) then if (not ps.loggedauthor) then
local cachestr = string.format("%s%s%s", local params = {}
if ps.show_comments then
table.insert(params,"comments=1")
end
if ps.hash then
table.insert(params,"pwd=" .. hashstr)
end
local cachestrparts = {
ps.host, ps.host,
ps.path, ps.path,
ps.show_comments and "?comments=1" or "" }
) if #params > 0 then
table.insert(cachestrparts,"?")
table.insert(cachestrparts,table.concat(params,"&"))
end
local cachestr = table.concat(cachestrparts)
text = cache.render(cachestr,function() text = cache.render(cachestr,function()
log(LOG_DEBUG,"Cache miss, rendering story " .. cachestr) log(LOG_DEBUG,"Cache miss, rendering story " .. cachestr)
if not populate_ps_story(req,ps) then if not populate_ps_story(req,ps) then

View File

@ -1,6 +1,5 @@
local sql = require("lsqlite3") local sql = require("lsqlite3")
local util = {} local util = {}
--[[ --[[
@ -124,6 +123,26 @@ function util.decode_id(s)
end end
end end
--arbitary data to hex encoded string
function util.encode_unlisted(str)
local safe = {}
for i = 1,#str do
local byte = str:byte(i)
table.insert(safe,string.format("%02x",byte))
end
return table.concat(safe)
end
--hex encoded string to arbitrary data
function util.decode_unlisted(str)
print("str was:",str)
local output = {}
for byte in str:gmatch("%x%x") do
table.insert(output, string.char(tonumber(byte,16)))
end
return table.concat(output)
end
--[[ --[[
Parses a semicolon seperated string into it's parts: Parses a semicolon seperated string into it's parts:
1. seperates by semicolon 1. seperates by semicolon

14
src/pages/.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
author_edit.etlua
author_index.etlua
author_paste.etlua
cantedit.etlua
claim.etlua
edit.etlua
index.etlua
login.etlua
noauthor.etlua
nostory.etlua
paste.etlua
read.etlua
search.etlua
search_sql.etlua

View File

@ -6,7 +6,7 @@
<form action="https://<%= user %>.<%= domain %>/_paste" method="post" class="container"> <form action="https://<%= user %>.<%= domain %>/_paste" method="post" class="container">
<fieldset> <fieldset>
<div class="row"> <div class="row">
<input type="text" name="title" placeholder="Title" class="column column-80"></input> <input type="text" name="title" placeholder="Title" class="column column-70"></input>
<select id="pasteas" name="pasteas" class="column column-10"> <select id="pasteas" name="pasteas" class="column column-10">
<option value="<%= user %>"><%= user %></option> <option value="<%= user %>"><%= user %></option>
<option value="anonymous">Anonymous</option> <option value="anonymous">Anonymous</option>
@ -15,6 +15,10 @@
<option value="plain">Plain</option> <option value="plain">Plain</option>
<option value="imageboard">Imageboard</option> <option value="imageboard">Imageboard</option>
</select> </select>
<div class="column column-10">
<label for="unlisted" class="label-inline">Unlisted</label>
<input type="checkbox" name="unlisted" id="unlisted"></input>
</div>
</div> </div>
<div class="row"> <div class="row">
<input type="text" name="tags" placeholder="Tags (semicolon;seperated)" class="column"></input> <input type="text" name="tags" placeholder="Tags (semicolon;seperated)" class="column"></input>

View File

@ -5,11 +5,15 @@
<% if err then %><em class="error"><%= err %></em><% end %> <% if err then %><em class="error"><%= err %></em><% end %>
<form action="https://<%= domain %>/_paste" method="post" class="container"><fieldset> <form action="https://<%= domain %>/_paste" method="post" class="container"><fieldset>
<div class="row"> <div class="row">
<input type="text" name="title" placeholder="Title" class="column column-80"></input> <input type="text" name="title" placeholder="Title" class="column column-70"></input>
<select id="markup" name="markup" class="column column-20"> <select id="markup" name="markup" class="column column-20">
<option value="plain">Plain</option> <option value="plain">Plain</option>
<option value="imageboard">Imageboard</option> <option value="imageboard">Imageboard</option>
</select> </select>
<div class="column column-10">
<label for="unlisted" class="label-inline">Unlisted</label>
<input type="checkbox" name="unlisted" id="unlisted"></input>
</div>
</div> </div>
<div class="row"> <div class="row">
<input type="text" name="tags" placeholder="Tags (semicolon;seperated)" class="column"></input> <input type="text" name="tags" placeholder="Tags (semicolon;seperated)" class="column"></input>

View File

@ -0,0 +1 @@
CREATE INDEX unlisted_index ON posts(hash);

View File

@ -6,6 +6,8 @@ means that all comments by other users on a post
an author makes will also be deleted. an author makes will also be deleted.
Post text uses zlib compression Post text uses zlib compression
Unlisted hashes are SHAv3 521
*/ */
CREATE TABLE IF NOT EXISTS posts ( CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
@ -15,5 +17,7 @@ CREATE TABLE IF NOT EXISTS posts (
isanon INTEGER, isanon INTEGER,
hashedip BLOB, hashedip BLOB,
post_time INTEGER, post_time INTEGER,
views INTEGER DEFAULT 0 views INTEGER DEFAULT 0,
unlisted INTEGER,
hash BLOB
); );

View File

@ -4,6 +4,8 @@ INSERT INTO posts (
authorid, authorid,
isanon, isanon,
hashedip, hashedip,
unlisted,
hash,
post_time post_time
) VALUES ( ) VALUES (
?, ?,
@ -11,5 +13,7 @@ INSERT INTO posts (
?, ?,
?, ?,
?, ?,
?,
?,
strftime('%s','now') strftime('%s','now')
); );

View File

@ -11,7 +11,7 @@ FROM
WHERE WHERE
posts.isanon = 0 AND posts.isanon = 0 AND
posts.authorid = authors.id AND posts.authorid = authors.id AND
authors.name = :author authors.name = :author AND
posts.unlisted = 0
ORDER BY ORDER BY
posts.post_time DESC posts.post_time DESC
LIMIT 10;

View File

@ -4,4 +4,5 @@ FROM
raw_text, posts raw_text, posts
WHERE WHERE
raw_text.id = posts.id AND raw_text.id = posts.id AND
raw_text.id = :postid raw_text.id = :postid AND
posts.unlisted = 0

View File

@ -13,4 +13,9 @@ FROM
posts,authors posts,authors
WHERE WHERE
posts.authorid = authors.id AND posts.authorid = authors.id AND
posts.id = :id; posts.id = :id AND
(
posts.unlisted = 1 AND
posts.hash = :hash
) OR
posts.unlisted = 0;

View File

@ -10,7 +10,8 @@ FROM
posts, posts,
authors authors
WHERE WHERE
posts.authorid = authors.id posts.authorid = authors.id AND
posts.unlisted = 0
ORDER BY ORDER BY
posts.post_time DESC posts.post_time DESC
LIMIT 10; LIMIT 50;

View File

@ -4,6 +4,7 @@ SET
post_title = ?, post_title = ?,
post_text = ?, post_text = ?,
isanon = ?, isanon = ?,
unlisted = ?,
post_time = strftime('%s','now') post_time = strftime('%s','now')
WHERE WHERE
posts.id = ?; posts.id = ?;