-- vim: ft=terra
local pstring = lib.mem.ptr(int8)
local binblob = lib.mem.ptr(uint8)
local queries = {
conf_get = {
params = {rawstring}, sql = [[
select value from parsav_config
where key = $1::text limit 1
]];
};
conf_set = {
params = {rawstring,rawstring}, cmd=true, sql = [[
insert into parsav_config (key, value)
values ($1::text, $2::text)
on conflict (key) do update set value = $2::text
]];
};
conf_reset = {
params = {rawstring}, cmd=true, sql = [[
delete from parsav_config where
key = $1::text
]];
};
actor_fetch_uid = {
params = {uint64}, sql = [[
select a.id, a.nym, a.handle, a.origin, a.bio,
a.avataruri, a.rank, a.quota, a.key, a.epithet,
extract(epoch from a.knownsince)::bigint,
coalesce(a.handle || '@' || s.domain,
'@' || a.handle) as xid
from parsav_actors as a
left join parsav_servers as s
on a.origin = s.id
where a.id = $1::bigint
]];
};
actor_fetch_xid = {
params = {pstring}, sql = [[
select a.id, a.nym, a.handle, a.origin, a.bio,
a.avataruri, a.rank, a.quota, a.key, a.epithet,
extract(epoch from a.knownsince)::bigint,
coalesce(a.handle || '@' || s.domain,
'@' || a.handle) as xid,
coalesce(s.domain,
(select value from parsav_config
where key='domain' limit 1)) as domain
from parsav_actors as a
left join parsav_servers as s
on a.origin = s.id
where $1::text = (a.handle || '@' || domain) or
$1::text = ('@' || a.handle || '@' || domain) or
(a.origin is null and
$1::text = a.handle or
$1::text = ('@' || a.handle))
]];
};
actor_save = {
params = {
uint64, --id
rawstring, --nym
rawstring, --handle
rawstring, --bio
rawstring, --epithet
rawstring, --avataruri
uint64, --avatarid
uint16, --rank
uint32 --quota
}, cmd = true, sql = [[
update parsav_actors set
nym = $2::text,
handle = $3::text,
bio = $4::text,
epithet = $5::text,
avataruri = $6::text,
avatarid = $7::bigint,
rank = $8::smallint,
quota = $9::integer
--invites are controlled by their own specialized routines
where id = $1::bigint
]];
};
actor_create = {
params = {
rawstring, rawstring, uint64, lib.store.timepoint,
rawstring, rawstring, lib.mem.ptr(uint8),
rawstring, uint16, uint32
};
sql = [[
insert into parsav_actors (
nym,handle,
origin,knownsince,
bio,avataruri,key,
epithet,rank,quota
) values ($1::text, $2::text,
case when $3::bigint = 0 then null
else $3::bigint end,
to_timestamp($4::bigint),
$5::bigint, $6::bigint, $7::bytea,
$8::text, $9::smallint, $10::integer
) returning id
]];
};
actor_auth_pw = {
params = {pstring,rawstring,pstring,lib.store.inet}, sql = [[
select a.aid, a.uid, a.name from parsav_auth as a
left join parsav_actors as u on u.id = a.uid
where (a.uid is null or u.handle = $1::text or (
a.uid = 0 and a.name = $1::text
)) and
(a.kind = 'trust' or (a.kind = $2::text and a.cred = $3::bytea)) and
(a.netmask is null or a.netmask >> $4::inet)
order by blacklist desc limit 1
]];
};
actor_enum_local = {
params = {}, sql = [[
select id, nym, handle, origin, bio,
null::text, rank, quota, key, epithet,
extract(epoch from knownsince)::bigint,
handle ||'@'||
(select value from parsav_config
where key='domain' limit 1) as xid
from parsav_actors where origin is null
]];
};
actor_enum = {
params = {}, sql = [[
select a.id, a.nym, a.handle, a.origin, a.bio,
a.avataruri, a.rank, a.quota, a.key, a.epithet,
extract(epoch from a.knownsince)::bigint,
coalesce(a.handle || '@' || s.domain,
'@' || a.handle) as xid
from parsav_actors a
left join parsav_servers s on s.id = a.origin
]];
};
actor_stats = {
params = {uint64}, sql = ([[
with tweets as (
select from parsav_posts where author = $1::bigint
),
follows as (
select relatee as user from parsav_rels
where relator = $1::bigint and kind = <follow>
),
followers as (
select relator as user from parsav_rels
where relatee = $1::bigint and kind = <follow>
),
mutuals as (
select * from follows intersect select * from followers
)
values (
(select count(tweets.*)::bigint from tweets),
(select count(follows.*)::bigint from follows),
(select count(followers.*)::bigint from followers),
(select count(mutuals.*)::bigint from mutuals)
)
]]):gsub('<(%w+)>',function(r) return tostring(lib.store.relation.idvmap[r]) end)
};
actor_auth_how = {
params = {rawstring, lib.store.inet}, sql = [[
with mts as (select a.kind from parsav_auth as a
left join parsav_actors as u on u.id = a.uid
where (a.uid is null or u.handle = $1::text or (
a.uid = 0 and a.name = $1::text
)) and
(a.netmask is null or a.netmask >> $2::inet) and
blacklist = false)
select
(select count(*) from mts where kind like 'pw-%') > 0,
(select count(*) from mts where kind like 'otp-%') > 0,
(select count(*) from mts where kind like 'challenge-%') > 0,
(select count(*) from mts where kind = 'trust') > 0
]]; -- cheat
};
actor_session_fetch = {
params = {uint64, lib.store.inet, int64}, sql = [[
select a.id, a.nym, a.handle, a.origin, a.bio,
a.avataruri, a.rank, a.quota, a.key, a.epithet,
extract(epoch from a.knownsince)::bigint,
coalesce(a.handle || '@' || s.domain,
'@' || a.handle) as xid,
au.restrict,
array['post' ] <@ au.restrict as can_post,
array['edit' ] <@ au.restrict as can_edit,
array['acct' ] <@ au.restrict as can_acct,
array['upload'] <@ au.restrict as can_upload,
array['censor'] <@ au.restrict as can_censor,
array['admin' ] <@ au.restrict as can_admin
from parsav_auth au
left join parsav_actors a on au.uid = a.id
left join parsav_servers s on a.origin = s.id
where au.aid = $1::bigint and au.blacklist = false and
(au.netmask is null or au.netmask >> $2::inet) and
($3::bigint = 0 or --slightly abusing the epoch time fmt here, but
((a.authtime is null or a.authtime <= to_timestamp($3::bigint)) and
(au.valperiod is null or au.valperiod <= to_timestamp($3::bigint))))
]];
};
actor_powers_fetch = {
params = {uint64}, sql = [[
select key, allow from parsav_rights where actor = $1::bigint
]]
};
actor_power_insert = {
params = {uint64,lib.mem.ptr(int8),uint16}, cmd = true, sql = [[
insert into parsav_rights (actor, key, allow) values (
$1::bigint, $2::text, ($3::smallint)::integer::bool
)
]]
};
actor_power_delete = {
params = {uint64,lib.mem.ptr(int8)}, cmd = true, sql = [[
delete from parsav_rights where
actor = $1::bigint and
key = $2::text
]]
};
auth_sigtime_user_fetch = {
params = {uint64}, sql = [[
select extract(epoch from authtime)::bigint
from parsav_actors where id = $1::bigint
]];
};
auth_sigtime_user_alter = {
params = {uint64,int64}, cmd = true, sql = [[
update parsav_actors set
authtime = to_timestamp($2::bigint)
where id = $1::bigint
]];
};
auth_create_pw = {
params = {uint64, binblob, pstring}, cmd = true, sql = [[
insert into parsav_auth (uid, name, kind, cred, comment) values (
$1::bigint,
(select handle from parsav_actors where id = $1::bigint),
'pw-sha256', $2::bytea,
$3::text
)
]]
};
auth_purge_type = {
params = {rawstring, uint64, rawstring}, cmd = true, sql = [[
delete from parsav_auth where
((uid = 0 and name = $1::text) or uid = $2::bigint) and
kind like $3::text
]]
};
auth_enum_uid = {
params = {uint64}, sql = [[
select aid, kind, comment, netmask, blacklist from parsav_auth where uid = $1::bigint
]];
};
auth_enum_handle = {
params = {rawstring}, sql = [[
select aid, kind, comment, netmask, blacklist from parsav_auth where name = $1::text
]];
};
post_save = {
params = {
uint64, uint32, int64;
rawstring, rawstring, rawstring;
}, cmd = true, sql = [[
update parsav_posts set
subject = $4::text,
acl = $5::text,
body = $6::text,
chgcount = $2::integer,
edited = to_timestamp($3::bigint)
where id = $1::bigint
]]
};
post_create = {
params = {
uint64, rawstring, rawstring, rawstring,
uint64, uint64, rawstring
}, sql = [[
insert into parsav_posts (
author, subject, acl, body,
parent, posted, discovered,
circles, mentions, convoheaduri
) values (
$1::bigint, case when $2::text = '' then null else $2::text end,
$3::text, $4::text,
$5::bigint, to_timestamp($6::bigint), now(),
array[]::bigint[], array[]::bigint[], $7::text
) returning id
]]; -- TODO array handling
};
post_destroy_prepare = {
params = {uint64}, cmd = true, sql = [[
update parsav_posts set
parent = (select parent from parsav_posts where id = $1::bigint limit 1)
where parent = $1::bigint
]]
};
post_destroy = {
params = {uint64}, cmd = true, sql = [[
delete from parsav_posts where id = $1::bigint
]]
};
post_fetch = {
params = {uint64}, sql = [[
select a.origin is null,
p.id, p.author, p.subject, p.acl, p.body,
extract(epoch from p.posted )::bigint,
extract(epoch from p.discovered)::bigint,
extract(epoch from p.edited )::bigint,
p.parent, p.convoheaduri, p.chgcount,
coalesce(c.value, -1)::smallint
from parsav_posts as p
inner join parsav_actors as a on p.author = a.id
left join parsav_actor_conf_ints as c on c.uid = a.id and c.key = 'ui-accent'
where p.id = $1::bigint
]];
};
post_enum_parent = {
params = {uint64}, sql = [[
select a.origin is null,
p.id, p.author, p.subject, p.acl, p.body,
extract(epoch from p.posted )::bigint,
extract(epoch from p.discovered)::bigint,
extract(epoch from p.edited )::bigint,
p.parent, p.convoheaduri, p.chgcount,
coalesce(c.value, -1)::smallint
from parsav_posts as p
inner join parsav_actors as a on a.id = p.author
left join parsav_actor_conf_ints as c on c.uid = a.id and c.key = 'ui-accent'
where p.parent = $1::bigint
order by p.posted, p.discovered asc
]]
};
thread_latest_arrival_calc = {
params = {uint64}, sql = [[
with recursive posts(id) as (
select id from parsav_posts where parent = $1::bigint
union
select p.id from parsav_posts as p
inner join posts on posts.id = p.parent
),
maxes as (
select unnest(array[max(p.posted), max(p.discovered), max(p.edited)]) as m
from posts
inner join parsav_posts as p
on p.id = posts.id
)
select extract(epoch from max(m))::bigint from maxes
]];
};
post_enum_author_uid = {
params = {uint64,uint64,uint64,uint64, uint64}, sql = [[
select a.origin is null,
p.id, p.author, p.subject, p.acl, p.body,
extract(epoch from p.posted )::bigint,
extract(epoch from p.discovered)::bigint,
extract(epoch from p.edited )::bigint,
p.parent, p.convoheaduri, p.chgcount,
coalesce((select value from parsav_actor_conf_ints as c where
c.uid = $1::bigint and c.key = 'ui-accent'),-1)::smallint
from parsav_posts as p
inner join parsav_actors as a on p.author = a.id
where p.author = $5::bigint and
($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and
($2::bigint = 0 or to_timestamp($2::bigint) < p.posted)
order by (p.posted, p.discovered) desc
limit case when $3::bigint = 0 then null
else $3::bigint end
offset $4::bigint
]]
};
-- maybe there's some way to unify these two, idk, im tired
timeline_instance_fetch = {
params = {uint64, uint64, uint64, uint64}, sql = [[
select true,
p.id, p.author, p.subject, p.acl, p.body,
extract(epoch from p.posted )::bigint,
extract(epoch from p.discovered)::bigint,
extract(epoch from p.edited )::bigint,
p.parent, null::text, p.chgcount,
coalesce(c.value, -1)::smallint
from parsav_posts as p
inner join parsav_actors as a on p.author = a.id
left join parsav_actor_conf_ints as c on c.uid = a.id and c.key = 'ui-accent'
where
($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and
($2::bigint = 0 or to_timestamp($2::bigint) < p.posted) and
(a.origin is null)
order by (p.posted, p.discovered) desc
limit case when $3::bigint = 0 then null
else $3::bigint end
offset $4::bigint
]]
};
artifact_instantiate = {
params = {binblob, binblob, pstring}, sql = [[
insert into parsav_artifacts (content,hash,mime) values (
$1::bytea, $2::bytea, $3::text
) on conflict do nothing returning id
]];
};
artifact_expropriate = {
params = {uint64, uint64, pstring}, cmd = true, sql = [[
insert into parsav_artifact_claims (uid,rid,description,folder) values (
$1::bigint, $2::bigint, $3::text, 'new'
) on conflict do nothing
]];
};
artifact_quicksearch = {
params = {binblob}, sql = [[
select id, (content is null) from parsav_artifacts where hash = $1::bytea
limit 1
]];
};
artifact_disclaim = {
params = {uint64, uint64}, cmd = true, sql = [[
delete from parsav_artifact_claims where
uid = $1::bigint and
rid = $2::bigint
]];
};
artifact_excise_forget = {
-- delete the blasted thing and pretend it never existed
params = {uint64}, cmd=true, sql = [[
delete from parsav_artifacts where id = $1::bigint
]];
};
artifact_excise_suppress_nullify = {
-- banish the thing into the outer darkness, preventing
-- it from ever being admitted into our databases, and
-- tabulate a -- list of the degenerates who befouled
-- their accounts with such wanton and execrable filth,
-- the better to ensure their long-overdue punishment
params = {uint64}, cmd=true, sql = [[
update parsav_artifacts
set content = null
where id = $1::bigint;
]];
};
artifact_excise_suppress_breaklinks = {
-- "ERROR: cannot insert multiple commands into a prepared
-- statement" are you fucking shitting me with this shit
params = {uint64}, sql = [[
delete from parsav_artifact_claims where
rid = $1::bigint
returning uid, description, birth, folder;
]];
};
post_attach_ctl_ins = {
params = {uint64, uint64}, cmd=true, sql = [[
update parsav_posts set
artifacts = artifacts || $2::bigint
where id = $1::bigint and not
artifacts @> array[$2::bigint] -- prevent duplication
]];
};
post_attach_ctl_del = {
params = {uint64, uint64}, cmd=true, sql = [[
update parsav_posts set
artifacts = array_remove(artifacts, $2::bigint)
where id = $1::bigint and
artifacts @> array[$2::bigint]
]];
};
actor_conf_str_get = {
params = {uint64, rawstring}, sql = [[
select value from parsav_actor_conf_strs where
uid = $1::bigint and
key = $2::text
limit 1
]];
};
actor_conf_str_set = {
params = {uint64, rawstring, rawstring}, cmd = true, sql = [[
insert into parsav_actor_conf_strs (uid,key,value)
values ($1::bigint, $2::text, $3::text)
on conflict (uid,key) do update set value = $3::text
]];
};
actor_conf_str_enum = {
params = {uint64}, sql = [[
select value from parsav_actor_conf_strs where uid = $1::bigint
]];
};
actor_conf_str_reset = {
params = {uint64, rawstring}, cmd = true, sql = [[
delete from parsav_actor_conf_strs where
uid = $1::bigint and ($2::text is null or key = $2::text)
]]
};
actor_conf_int_get = {
params = {uint64, rawstring}, sql = [[
select value from parsav_actor_conf_ints where
uid = $1::bigint and
key = $2::text
limit 1
]];
};
actor_conf_int_set = {
params = {uint64, rawstring, uint64}, cmd = true, sql = [[
insert into parsav_actor_conf_ints (uid,key,value)
values ($1::bigint, $2::text, $3::bigint)
on conflict (uid,key) do update set value = $3::bigint
]];
};
actor_conf_int_enum = {
params = {uint64}, sql = [[
select value from parsav_actor_conf_ints where uid = $1::bigint
]];
};
actor_conf_int_reset = {
params = {uint64, rawstring}, cmd = true, sql = [[
delete from parsav_actor_conf_ints where
uid = $1::bigint and ($2::text is null or key = $2::text)
]]
};
}
local struct pqr {
sz: intptr
res: &lib.pq.PGresult
}
terra pqr:free() if self.sz > 0 then lib.pq.PQclear(self.res) end end
terra pqr:null(row: intptr, col: intptr)
return (lib.pq.PQgetisnull(self.res, row, col) == 1)
end
terra pqr:len(row: intptr, col: intptr)
return lib.pq.PQgetlength(self.res, row, col)
end
terra pqr:cols() return lib.pq.PQnfields(self.res) end
terra pqr:string(row: intptr, col: intptr) -- not to be exported!!
if self:null(row,col) then return nil end
var v = lib.pq.PQgetvalue(self.res, row, col)
return v
end
terra pqr:_string(row: intptr, col: intptr) -- not to be exported!!
if self:null(row,col) then return pstring.null() end
return pstring {
ptr = lib.pq.PQgetvalue (self.res, row, col);
ct = lib.pq.PQgetlength(self.res, row, col);
}
end
terra pqr:bin(row: intptr, col: intptr) -- not to be exported!! DO NOT FREE
if self:null(row,col) then return [lib.mem.ptr(uint8)].null() end
return [lib.mem.ptr(uint8)] {
ptr = [&uint8](lib.pq.PQgetvalue(self.res, row, col));
ct = lib.pq.PQgetlength(self.res, row, col);
}
end
terra pqr:String(row: intptr, col: intptr) -- suitable to be exported
if self:null(row,col) then return pstring.null() end
var s = pstring {
ptr = lib.str.dup(self:string(row,col));
ct = lib.pq.PQgetlength(self.res, row, col);
}
return s
end
terra pqr:bool(row: intptr, col: intptr)
var v = lib.pq.PQgetvalue(self.res, row, col)
if @v == 0x01 then return true else return false end
end
terra pqr:cidr(row: intptr, col: intptr)
var v = lib.pq.PQgetvalue(self.res, row, col)
var i: lib.store.inet
if v[0] == 0x02 then i.pv = 4
elseif v[0] == 0x03 then i.pv = 6
else lib.bail('invalid CIDR type in stream') end
i.fixbits = v[1]
if v[2] ~= 0x1 then lib.bail('expected CIDR but got inet from stream') end
if i.pv == 4 and v[3] ~= 0x04 or i.pv == 6 and v[3] ~= 0x10 then
lib.bail('CIDR failed length sanity check') end
var sz: intptr if i.pv == 4 then sz = 4 else sz = 16 end
for j=0,sz do i.v6[j] = v[4 + j] end -- 😬
return i
end
pqr.methods.int = macro(function(self, ty, row, col)
return quote
var i: ty:astype()
var v = lib.pq.PQgetvalue(self.res, row, col)
--i = @[&uint64](v)
lib.math.netswap_ip(ty, v, &i)
in i end
end)
local pqt = {
[lib.store.inet] = function(cidr)
local tycode = cidr and 0x01 or 0x00
return terra(i: lib.store.inet, buf: &uint8)
var sz: intptr
if i.pv == 4 then sz = 4 else sz = 16 end
if buf == nil then buf = [&uint8](lib.mem.heapa_raw(sz + 4)) end
if i.pv == 4 then buf[0] = 0x02
elseif i.pv == 6 then buf[0] = 0x03 end
if cidr then -- our local 'inet' is not quite orthogonal to the
-- postgres inet type; tweak it to match (ignore port)
buf[1] = i.fixbits
elseif i.pv == 6 then buf[1] = 128
else buf[1] = 32 end
buf[2] = tycode
buf[3] = sz
for j=0,sz do buf[4 + j] = i.v6[j] end -- 😬
return buf
end
end;
}
local con = symbol(&lib.pq.PGconn)
local prep = {}
local function sqlsquash(s) return s
:gsub('%%include (.-)%%',function(f)
return sqlsquash(lib.util.ingest('backend/schema/' .. f))
end) -- include dependencies
:gsub('%-%-.-\n','') -- remove disruptive line comments
:gsub('%-%-.-$','') -- remove unnecessary terminal comments
:gsub('%s+',' ') -- remove whitespace
:gsub('^%s*(.-)%s*$','%1') -- chomp
end
for k,q in pairs(queries) do
local qt = sqlsquash(q.sql)
local stmt = 'parsavpg_' .. k
terra q.prep([con])
var res = lib.pq.PQprepare([con], stmt, qt, [#q.params], nil)
defer lib.pq.PQclear(res)
if res == nil or lib.pq.PQresultStatus(res) ~= lib.pq.PGRES_COMMAND_OK then
if res == nil then
lib.bail('grievous error occurred preparing ',k,' statement')
end
lib.bail('could not prepare PGSQL statement ',k,': ',lib.pq.PQresultErrorMessage(res))
end
lib.dbg('prepared PGSQL statement ',k)
end
prep[#prep + 1] = quote q.prep([con]) end
local args, casts, counters, fixers, ft, yield = {}, {}, {}, {}, {}, {}
local dumpers = {}
for i, ty in ipairs(q.params) do
args[i] = symbol(ty)
ft[i] = `1
if ty == rawstring then
counters[i] = `lib.trn([args[i]] == nil, 0, lib.str.sz([args[i]]))
casts[i] = `[&int8]([args[i]])
dumpers[#dumpers+1] = `lib.io.fmt([tostring(i)..'. got rawstr %s\n'], [args[i]])
elseif ty == lib.store.inet then -- assume not CIDR
counters[i] = `lib.trn([args[i]].pv == 4,4,16)+4
casts[i] = quote
var ipbuf: int8[20]
;[pqt[lib.store.inet](false)]([args[i]], [&uint8](&ipbuf))
in &ipbuf[0] end
dumpers[#dumpers+1] = `lib.io.fmt([tostring(i)..'. got inet\n'])
elseif ty.ptr_basetype == int8 or ty.ptr_basetype == uint8 then
counters[i] = `[args[i]].ct
casts[i] = `[&int8]([args[i]].ptr)
dumpers[#dumpers+1] = `lib.io.fmt([tostring(i)..'. got ptr %llu %.*s\n'], [args[i]].ct, [args[i]].ct, [args[i]].ptr)
elseif ty.ptr_basetype == bool then
counters[i] = `1
casts[i] = `[&int8]([args[i]].ptr)
-- dumpers[#dumpers+1] = `lib.io.fmt([tostring(i)..'. got bool = %hhu\n'], @[args[i]].ptr)
elseif ty:isintegral() then
counters[i] = ty.bytes
casts[i] = `[&int8](&[args[i]])
dumpers[#dumpers+1] = `lib.io.fmt([tostring(i)..'. got int %llu\n'], [args[i]])
fixers[#fixers + 1] = quote
[args[i]] = lib.math.netswap(ty, [args[i]])
end
end
end
local okconst = lib.pq.PGRES_TUPLES_OK
if q.cmd then okconst = lib.pq.PGRES_COMMAND_OK end
terra q.exec(src: &lib.store.source, [args])
var params = arrayof([&int8], [casts])
var params_sz = arrayof(int, [counters])
var params_ft = arrayof(int, [ft])
[fixers]
--[dumpers]
var res = lib.pq.PQexecPrepared([&lib.pq.PGconn](src.handle), stmt,
[#args], params, params_sz, params_ft, 1)
if res == nil then
lib.bail(['grievous error occurred executing '..k..' against database'])
elseif lib.pq.PQresultStatus(res) ~= okconst then
lib.bail(['PGSQL database procedure '..k..' failed\n'],
lib.pq.PQresultErrorMessage(res))
end
var ct = lib.pq.PQntuples(res)
if ct == 0 then
lib.pq.PQclear(res)
return pqr {0, nil}
else
return pqr {ct, res}
end
end
end
local terra row_to_post(r: &pqr, row: intptr): lib.mem.ptr(lib.store.post)
var subj: rawstring, sblen: intptr
var cvhu: rawstring, cvhlen: intptr
if r:null(row,3)
then subj = nil sblen = 0
else subj = r:string(row,3) sblen = r:len(row,3)+1
end
if r:null(row,10)
then cvhu = nil cvhlen = 0
else cvhu = r:string(row,10) cvhlen = r:len(row,10)+1
end
var p = [ lib.str.encapsulate(lib.store.post, {
subject = { `subj, `sblen };
acl = {`r:string(row,4), `r:len(row,4)+1};
body = {`r:string(row,5), `r:len(row,5)+1};
convoheaduri = { `cvhu, `cvhlen }; --FIXME
}) ]
p.ptr.id = r:int(uint64,row,1)
p.ptr.author = r:int(uint64,row,2)
if r:null(row,6)
then p.ptr.posted = 0
else p.ptr.posted = r:int(uint64,row,6)
end
if r:null(row,7)
then p.ptr.discovered = 0
else p.ptr.discovered = r:int(uint64,row,7)
end
if r:null(row,8)
then p.ptr.edited = 0
else p.ptr.edited = r:int(uint64,row,8)
end
p.ptr.parent = r:int(uint64,row,9)
if r:null(row,11)
then p.ptr.chgcount = 0
else p.ptr.chgcount = r:int(uint32,row,11)
end
p.ptr.accent = r:int(int16,row,12)
p.ptr.localpost = r:bool(row,0)
return p
end
local terra row_to_actor(r: &pqr, row: intptr): lib.mem.ptr(lib.store.actor)
var a: lib.mem.ptr(lib.store.actor)
var av: rawstring, avlen: intptr
var nym: rawstring, nymlen: intptr
var bio: rawstring, biolen: intptr
var epi: rawstring, epilen: intptr
if r:null(row,5) then avlen = 0 av = nil else
av = r:string(row,5)
avlen = r:len(row,5)+1
end
if r:null(row,1) then nymlen = 0 nym = nil else
nym = r:string(row,1)
nymlen = r:len(row,1)+1
end
if r:null(row,4) then biolen = 0 bio = nil else
bio = r:string(row,4)
biolen = r:len(row,4)+1
end
if r:null(row,9) then epilen = 0 epi = nil else
epi = r:string(row,9)
epilen = r:len(row,9)+1
end
a = [ lib.str.encapsulate(lib.store.actor, {
nym = {`nym, `nymlen};
bio = {`bio, `biolen};
epithet = {`epi, `epilen};
avatar = {`av,`avlen};
handle = {`r:string(row, 2); `r:len(row,2) + 1};
xid = {`r:string(row, 11); `r:len(row,11) + 1};
}) ]
a.ptr.id = r:int(uint64, row, 0);
a.ptr.rights = lib.store.rights_default();
a.ptr.rights.rank = r:int(uint16, row, 6);
a.ptr.rights.quota = r:int(uint32, row, 7);
a.ptr.knownsince = r:int(int64,row, 10);
if r:null(row,8) then
a.ptr.key.ct = 0 a.ptr.key.ptr = nil
else
a.ptr.key = r:bin(row,8)
end
if r:null(row,3) then a.ptr.origin = 0
else a.ptr.origin = r:int(uint64,row,3) end
return a
end
local privmap = lib.store.privmap
local checksha = function(src, hash, origin, username, pw)
local validate = function(kind, cred, credlen)
return quote
var r = queries.actor_auth_pw.exec(
[&lib.store.source](src),
username,
kind,
[lib.mem.ptr(int8)] {ptr=[&int8](cred), ct=credlen},
origin)
if r.sz > 0 then -- found a record! stop here
var aid = r:int(uint64, 0,0)
var uid = r:int(uint64, 0,1)
var name = r:String(0,2)
r:free()
return aid, uid, name
end
end
end
local out = symbol(uint8[64])
local vdrs = {}
local alg = lib.md['MBEDTLS_MD_SHA' .. tostring(hash)]
vdrs[#vdrs+1] = quote
if lib.md.mbedtls_md(lib.md.mbedtls_md_info_from_type(alg),
[&uint8](pw.ptr), pw.ct, out) ~= 0 then
lib.bail('hashing failure!')
end
[ validate(string.format('pw-sha%u', hash), `&out[0], hash / 8) ]
end
return quote
lib.dbg(['searching for hashed password credentials in format SHA' .. tostring(hash)])
var [out]
[vdrs]
lib.dbg(['could not find password hash'])
end
end
local schema = sqlsquash(lib.util.ingest('backend/schema/pgsql.sql'))
local obliterator = sqlsquash(lib.util.ingest('backend/schema/pgsql-drop.sql'))
local privupdate = terra(
src: &lib.store.source,
ac: &lib.store.actor
): {}
var pdef = lib.store.rights_default().powers
var map = array([privmap])
for i=0, [map.type.N] do
var d = pdef and map[i].priv
var u = ac.rights.powers and map[i].priv
queries.actor_power_delete.exec(src, ac.id, map[i].name)
if d:sz() > 0 and u:sz() == 0 then
lib.dbg('blocking power ', {map[i].name.ptr, map[i].name.ct})
queries.actor_power_insert.exec(src, ac.id, map[i].name, 0)
elseif d:sz() == 0 and u:sz() > 0 then
lib.dbg('granting power ', {map[i].name.ptr, map[i].name.ct})
queries.actor_power_insert.exec(src, ac.id, map[i].name, 1)
end
end
end
local getpow = terra(
src: &lib.store.source,
uid: uint64
): lib.store.powerset
var powers = lib.store.rights_default().powers
var map = array([privmap])
var r = queries.actor_powers_fetch.exec(src, uid)
for i=0, r.sz do
for j=0, [map.type.N] do
var pn = r:_string(i,0)
if map[j].name:cmp(pn) then
if r:bool(i,1)
then powers = powers + map[j].priv
else powers = powers - map[j].priv
end
end
end
end
return powers
end
local txdo = terra(src: &lib.store.source)
var res = lib.pq.PQexec([&lib.pq.PGconn](src.handle), 'begin')
if lib.pq.PQresultStatus(res) == lib.pq.PGRES_COMMAND_OK then
lib.dbg('beginning pgsql transaction')
return true
else
lib.warn('backend pgsql - failed to begin transaction: \n', lib.pq.PQresultErrorMessage(res))
return false
end
end
local txdone = terra(src: &lib.store.source)
var res = lib.pq.PQexec([&lib.pq.PGconn](src.handle), 'end')
if lib.pq.PQresultStatus(res) == lib.pq.PGRES_COMMAND_OK then
lib.dbg('completing pgsql transaction')
return true
else
lib.warn('backend pgsql - failed to complete transaction: \n', lib.pq.PQresultErrorMessage(res))
return false
end
end
local b = `lib.store.backend {
id = "pgsql";
open = [terra(src: &lib.store.source): &opaque
lib.report('connecting to postgres database: ', src.string.ptr)
var [con] = lib.pq.PQconnectdb(src.string.ptr)
if lib.pq.PQstatus(con) ~= lib.pq.CONNECTION_OK then
lib.warn('postgres backend connection failed')
lib.pq.PQfinish(con)
return nil
end
var res = lib.pq.PQexec(con, [[
select pg_catalog.set_config('search_path', 'public', false)
]])
if res == nil then
lib.warn('critical failure to secure postgres connection')
lib.pq.PQfinish(con)
return nil
end
defer lib.pq.PQclear(res)
if lib.pq.PQresultStatus(res) ~= lib.pq.PGRES_TUPLES_OK then
lib.warn('failed to secure postgres connection')
lib.pq.PQfinish(con)
return nil
end
return con
end];
close = [terra(src: &lib.store.source) lib.pq.PQfinish([&lib.pq.PGconn](src.handle)) end];
tx_enter = txdo, tx_complete = txdone;
conprep = [terra(src: &lib.store.source, mode: lib.store.prepmode.t)
var [con] = [&lib.pq.PGconn](src.handle)
if mode == lib.store.prepmode.full then [prep]
elseif mode == lib.store.prepmode.conf or
mode == lib.store.prepmode.admin then
queries.conf_get.prep(con)
queries.conf_set.prep(con)
queries.conf_reset.prep(con)
if mode == lib.store.prepmode.admin then
end
else lib.bail('unsupported connection preparation mode') end
end];
dbsetup = [terra(src: &lib.store.source)
var res = lib.pq.PQexec([&lib.pq.PGconn](src.handle), schema)
if lib.pq.PQresultStatus(res) == lib.pq.PGRES_COMMAND_OK then
lib.report('successfully instantiated schema in database')
return true
else
lib.warn('backend pgsql - failed to initialize database: \n', lib.pq.PQresultErrorMessage(res))
return false
end
end];
obliterate_everything = [terra(src: &lib.store.source)
var res = lib.pq.PQexec([&lib.pq.PGconn](src.handle), obliterator)
if lib.pq.PQresultStatus(res) == lib.pq.PGRES_COMMAND_OK then
lib.report('successfully wiped out everything parsav-related in database')
return true
else
lib.warn('backend pgsql - failed to obliterate database: \n', lib.pq.PQresultErrorMessage(res))
return false
end
end];
conf_get = [terra(src: &lib.store.source, key: rawstring)
var r = queries.conf_get.exec(src, key)
if r.sz == 0 then return [lib.mem.ptr(int8)] { ptr = nil, ct = 0 } else
defer r:free()
return r:String(0,0)
end
end];
conf_set = [terra(src: &lib.store.source, key: rawstring, val: rawstring)
queries.conf_set.exec(src, key, val):free() end];
conf_reset = [terra(src: &lib.store.source, key: rawstring)
queries.conf_reset.exec(src, key):free() end];
actor_fetch_uid = [terra(src: &lib.store.source, uid: uint64)
var r = queries.actor_fetch_uid.exec(src, uid)
if r.sz == 0 then
return [lib.mem.ptr(lib.store.actor)] { ct = 0, ptr = nil }
else defer r:free()
var a = row_to_actor(&r, 0)
a.ptr.rights.powers = getpow(src, uid)
a.ptr.source = src
return a
end
end];
actor_fetch_xid = [terra(src: &lib.store.source, xid: lib.mem.ptr(int8))
var r = queries.actor_fetch_xid.exec(src, xid)
if r.sz == 0 then
return [lib.mem.ptr(lib.store.actor)] { ct = 0, ptr = nil }
else defer r:free()
var a = row_to_actor(&r, 0)
a.ptr.rights.powers = getpow(src, a.ptr.id)
a.ptr.source = src
return a
end
end];
actor_enum = [terra(src: &lib.store.source)
var r = queries.actor_enum.exec(src)
if r.sz == 0 then
return [lib.mem.ptr(&lib.store.actor)] { ct = 0, ptr = nil }
else defer r:free()
var mem = lib.mem.heapa([&lib.store.actor], r.sz)
for i=0,r.sz do
mem.ptr[i] = row_to_actor(&r, i).ptr
mem.ptr[i].source = src
end
return [lib.mem.ptr(&lib.store.actor)] { ct = r.sz, ptr = mem.ptr }
end
end];
actor_enum_local = [terra(src: &lib.store.source)
var r = queries.actor_enum_local.exec(src)
if r.sz == 0 then
return [lib.mem.ptr(&lib.store.actor)] { ct = 0, ptr = nil }
else defer r:free()
var mem = lib.mem.heapa([&lib.store.actor], r.sz)
for i=0,r.sz do
mem.ptr[i] = row_to_actor(&r, i).ptr
mem.ptr[i].source = src
end
return [lib.mem.ptr(&lib.store.actor)] { ct = r.sz, ptr = mem.ptr }
end
end];
actor_auth_how = [terra(
src: &lib.store.source,
ip: lib.store.inet,
username: rawstring
): {lib.store.credset, bool}
var cs: lib.store.credset cs:clear();
var r = queries.actor_auth_how.exec(src, username, ip)
if r.sz == 0 then return cs, false end -- just in case
defer r:free()
(cs.pw << r:bool(0,0))
(cs.otp << r:bool(0,1))
(cs.challenge << r:bool(0,2))
(cs.trust << r:bool(0,3))
return cs, true
end];
actor_auth_pw = [terra(
src: &lib.store.source,
ip: lib.store.inet,
username: lib.mem.ptr(int8),
cred: lib.mem.ptr(int8)
): {uint64, uint64, pstring}
[ checksha(`src, 256, ip, username, cred) ] -- most common
[ checksha(`src, 512, ip, username, cred) ] -- most secure
[ checksha(`src, 384, ip, username, cred) ] -- weird
[ checksha(`src, 224, ip, username, cred) ] -- weirdest
-- TODO: check pbkdf2-hmac
-- TODO: check OTP
return 0, 0, pstring.null()
end];
actor_stats = [terra(src: &lib.store.source, uid: uint64)
var r = queries.actor_stats.exec(src, uid)
if r.sz == 0 then lib.bail('error fetching actor stats!') end
var s: lib.store.actor_stats
s.posts = r:int(uint64, 0, 0)
s.follows = r:int(uint64, 0, 1)
s.followers = r:int(uint64, 0, 2)
s.mutuals = r:int(uint64, 0, 3)
return s
end];
actor_session_fetch = [terra(
src: &lib.store.source,
aid: uint64,
ip : lib.store.inet,
issuetime: lib.store.timepoint
): { lib.stat(lib.store.auth), lib.mem.ptr(lib.store.actor) }
var r = queries.actor_session_fetch.exec(src, aid, ip, issuetime)
if r.sz == 0 then goto fail end
do defer r:free()
if r:null(0,0) then goto fail end
var a = row_to_actor(&r, 0)
a.ptr.source = src
var au = [lib.stat(lib.store.auth)] { ok = true }
au.val.aid = aid
au.val.uid = a.ptr.id
if not r:null(0,13) then -- restricted?
au.val.privs:clear()
(au.val.privs.post << r:bool(0,14))
(au.val.privs.edit << r:bool(0,15))
(au.val.privs.acct << r:bool(0,16))
(au.val.privs.upload << r:bool(0,17))
(au.val.privs.censor << r:bool(0,18))
(au.val.privs.admin << r:bool(0,19))
else au.val.privs:fill() end
return au, a
end
::fail:: return [lib.stat (lib.store.auth) ] { ok = false },
[lib.mem.ptr(lib.store.actor)] { ptr = nil, ct = 0 }
end];
post_create = [terra(
src: &lib.store.source,
post: &lib.store.post
): uint64
var r = queries.post_create.exec(src,
post.author,post.subject,post.acl,post.body,
post.parent,post.posted,post.convoheaduri
)
if r.sz == 0 then return 0 end
defer r:free()
var id = r:int(uint64,0,0)
post.source = src
return id
end];
post_destroy = [terra(
src: &lib.store.source,
post: uint64
): {}
txdo(src)
queries.post_destroy_prepare.exec(src, post)
queries.post_destroy.exec(src, post)
txdone(src)
end];
post_fetch = [terra(
src: &lib.store.source,
post: uint64
): lib.mem.ptr(lib.store.post)
var r = queries.post_fetch.exec(src, post)
if r.sz == 0 then return [lib.mem.ptr(lib.store.post)].null() end
var p = row_to_post(&r, 0)
p.ptr.source = src
return p
end];
timeline_instance_fetch = [terra(src: &lib.store.source, rg: lib.store.range)
var r = pqr { sz = 0 }
var A,B,C,D = rg:matrix() -- :/
r = queries.timeline_instance_fetch.exec(src,A,B,C,D)
var ret: lib.mem.ptr(lib.mem.ptr(lib.store.post)) ret:init(r.sz)
for i=0,r.sz do
ret.ptr[i] = row_to_post(&r, i) -- MUST FREE ALL
ret.ptr[i].ptr.source = src
end
return ret
end];
post_enum_author_uid = [terra(
src: &lib.store.source,
uid: uint64,
rg: lib.store.range
): lib.mem.ptr(lib.mem.ptr(lib.store.post))
var r = pqr { sz = 0 }
var A,B,C,D = rg:matrix() -- :/
r = queries.post_enum_author_uid.exec(src,A,B,C,D,uid)
var ret: lib.mem.ptr(lib.mem.ptr(lib.store.post)) ret:init(r.sz)
for i=0,r.sz do
ret.ptr[i] = row_to_post(&r, i) -- MUST FREE ALL
ret.ptr[i].ptr.source = src
end
return ret
end];
actor_powers_fetch = getpow;
actor_save = [terra(
src: &lib.store.source,
ac: &lib.store.actor
): {}
queries.actor_save.exec(src,
ac.id, ac.nym, ac.handle,
ac.bio, ac.epithet, ac.avatar,
ac.avatarid, ac.rights.rank, ac.rights.quota)
end];
actor_save_privs = privupdate;
actor_create = [terra(
src: &lib.store.source,
ac: &lib.store.actor
): uint64
var r = queries.actor_create.exec(src,ac.nym, ac.handle, ac.origin, ac.knownsince, ac.bio, ac.avatar, ac.key, ac.epithet, ac.rights.rank, ac.rights.quota)
if r.sz == 0 then lib.bail('failed to create actor!') end
ac.id = r:int(uint64,0,0)
-- check against default rights, insert records for wherever powers differ
lib.dbg('created new actor, establishing powers')
privupdate(src,ac)
lib.dbg('powers established')
return ac.id
end];
auth_enum_uid = [terra(
src: &lib.store.source,
uid: uint64
): lib.mem.ptr(lib.mem.ptr(lib.store.auth))
var r = queries.auth_enum_uid.exec(src,uid)
if r.sz == 0 then return [lib.mem.ptr(lib.mem.ptr(lib.store.auth))].null() end
var ret = lib.mem.heapa([lib.mem.ptr(lib.store.auth)], r.sz)
for i=0, r.sz do
var kind = r:_string(i, 1)
var comment = r:_string(i, 2)
var a = [ lib.str.encapsulate(lib.store.auth, {
kind = {`kind.ptr, `kind.ct};
comment = {`comment.ptr, `comment.ct};
}) ]
a.ptr.aid = r:int(uint64, i, 0)
a.ptr.netmask = r:cidr(i, 3)
a.ptr.blacklist = r:bool(i, 4)
ret.ptr[i] = a
end
return ret
end];
auth_attach_pw = [terra(
src: &lib.store.source,
uid: uint64,
reset: bool,
pw: pstring,
comment: pstring
): {}
var hash: uint8[lib.crypt.algsz.sha256]
if lib.md.mbedtls_md(lib.md.mbedtls_md_info_from_type(lib.crypt.alg.sha256.id),
[&uint8](pw.ptr), pw.ct, &hash[0]) ~= 0 then
lib.bail('cannot hash password')
end
if reset then queries.auth_purge_type.exec(src, nil, uid, 'pw-%') end
queries.auth_create_pw.exec(src, uid, binblob {ptr = &hash[0], ct = [hash.type.N]}, comment)
end];
auth_purge_pw = [terra(src: &lib.store.source, uid: uint64, handle: rawstring): {}
queries.auth_purge_type.exec(src, handle, uid, 'pw-%')
end];
auth_purge_otp = [terra(src: &lib.store.source, uid: uint64, handle: rawstring): {}
queries.auth_purge_type.exec(src, handle, uid, 'otp-%')
end];
auth_purge_trust = [terra(src: &lib.store.source, uid: uint64, handle: rawstring): {}
queries.auth_purge_type.exec(src, handle, uid, 'trust')
end];
artifact_quicksearch = [terra(
src: &lib.store.source,
hash: binblob
): {uint64, bool}
var srec = queries.artifact_quicksearch.exec(src, hash)
if srec.sz > 0 then
defer srec:free()
var id = srec:int(uint64,0,0)
var ban = srec:bool(0,1)
return id, ban
else return 0, false end
end];
artifact_instantiate = [terra(
src: &lib.store.source,
artifact: binblob,
mime: pstring
): uint64
var arthash: uint8[lib.crypt.algsz.sha256]
if lib.md.mbedtls_md(lib.md.mbedtls_md_info_from_type(lib.crypt.alg.sha256.id),
artifact.ptr, artifact.ct, &arthash[0]) ~= 0 then
lib.bail('could not hash artifact to be instantiated')
end
var hashb = binblob{ptr=&arthash[0],ct=[arthash.type.N]}
var srec = queries.artifact_quicksearch.exec(src, hashb)
if srec.sz > 0 then
defer srec:free()
var ban = srec:bool(0,1)
if ban then
lib.report('user attempted to instantiate forsaken artifact')
return 0
end
var oldid = srec:int(uint64,0,0)
return oldid
else -- not in db, insert
var nrec = queries.artifact_instantiate.exec(src, artifact, hashb, mime)
if nrec.sz == 0 then
lib.warn('failed to instantiate artifact -- are you running out of storage?')
return 0
else defer nrec:free()
var newid = nrec:int(uint64,0,0)
return newid
end
end
end];
post_attach_ctl = [terra(
src: &lib.store.source,
post: uint64,
artifact: uint64,
detach: bool
): {}
if detach
then queries.post_attach_ctl_del.exec(src,post,artifact)
else queries.post_attach_ctl_ins.exec(src,post,artifact)
end
end];
post_save = [terra(
src: &lib.store.source,
post: &lib.store.post
): {}
queries.post_save.exec(src,
post.id, post.chgcount, post.edited,
post.subject, post.acl, post.body)
end];
post_enum_parent = [terra(
src: &lib.store.source,
post: uint64
): lib.mem.ptr(lib.mem.ptr(lib.store.post))
var r = queries.post_enum_parent.exec(src,post)
if r.sz == 0 then
return [lib.mem.ptr(lib.mem.ptr(lib.store.post))].null()
end
defer r:free()
var lst = lib.mem.heapa([lib.mem.ptr(lib.store.post)], r.sz)
for i=0, r.sz do lst.ptr[i] = row_to_post(&r, i) end
return lst
end];
thread_latest_arrival_calc = [terra(
src: &lib.store.source,
post: uint64
): lib.store.timepoint
var r = queries.thread_latest_arrival_calc.exec(src,post)
if r.sz == 0 or r:null(0,0) then return 0 end
var tp: lib.store.timepoint = r:int(int64,0,0)
r:free()
return tp
end];
auth_sigtime_user_fetch = [terra(
src: &lib.store.source,
uid: uint64
): lib.store.timepoint
var r = queries.auth_sigtime_user_fetch.exec(src, uid)
if r.sz > 0 then defer r:free()
var t = r:int(int64,0,0)
return t
else return 0 end
end];
auth_sigtime_user_alter = [terra(
src: &lib.store.source,
uid: uint64,
time: lib.store.timepoint
): {} queries.auth_sigtime_user_alter.exec(src, uid, time) end];
actor_conf_str_enum = nil;
actor_conf_str_get = [terra(src: &lib.store.source, uid: uint64, key: rawstring): pstring
var r = queries.actor_conf_str_get.exec(src, uid, key)
if r.sz > 0 then
var ret = r:String(0,0)
r:free()
return ret
else return pstring.null() end
end];
actor_conf_str_set = [terra(src: &lib.store.source, uid: uint64, key: rawstring, value: rawstring): {}
queries.actor_conf_str_set.exec(src,uid,key,value) end];
actor_conf_str_reset = [terra(src: &lib.store.source, uid: uint64, key: rawstring): {}
queries.actor_conf_str_reset.exec(src,uid,key) end];
actor_conf_int_enum = nil;
actor_conf_int_get = [terra(src: &lib.store.source, uid: uint64, key: rawstring)
var r = queries.actor_conf_int_get.exec(src, uid, key)
if r.sz > 0 then
var ret = r:int(uint64,0,0)
r:free()
return ret, true
end
return 0, false
end];
actor_conf_int_set = [terra(src: &lib.store.source, uid: uint64, key: rawstring, value: uint64): {}
queries.actor_conf_int_set.exec(src,uid,key,value) end];
actor_conf_int_reset = [terra(src: &lib.store.source, uid: uint64, key: rawstring): {}
queries.actor_conf_int_reset.exec(src,uid,key) end];
actor_auth_register_uid = nil; -- TODO better support non-view based auth
}
return b