ADDED acl.t
Index: acl.t
==================================================================
--- acl.t
+++ acl.t
@@ -0,0 +1,1 @@
+-- vim: ft=terra
Index: backend/pgsql.t
==================================================================
--- backend/pgsql.t
+++ backend/pgsql.t
@@ -1,6 +1,8 @@
-- 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
@@ -22,25 +24,28 @@
]];
};
actor_fetch_uid = {
params = {uint64}, sql = [[
- select
- id, nym, handle, origin, bio,
- avataruri, rank, quota, key,
- extract(epoch from knownsince)::bigint
+ select a.id, a.nym, a.handle, a.origin, a.bio,
+ a.avataruri, a.rank, a.quota, a.key,
+ extract(epoch from a.knownsince)::bigint,
+ coalesce(a.handle || '@' || s.domain,
+ '@' || a.handle) as xid
- from parsav_actors
- where id = $1::bigint
+ 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 = {lib.mem.ptr(int8)}, sql = [[
+ params = {pstring}, sql = [[
select a.id, a.nym, a.handle, a.origin, a.bio,
a.avataruri, a.rank, a.quota, a.key,
- extract(epoch from knownsince)::bigint,
+ extract(epoch from a.knownsince)::bigint,
coalesce(a.handle || '@' || s.domain,
'@' || a.handle) as xid,
coalesce(s.domain,
(select value from parsav_config
@@ -57,11 +62,11 @@
$1::text = ('@' || a.handle))
]];
};
actor_auth_pw = {
- params = {lib.mem.ptr(int8),rawstring,lib.mem.ptr(int8),lib.store.inet}, sql = [[
+ params = {pstring,rawstring,pstring,lib.store.inet}, sql = [[
select a.aid 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
@@ -85,11 +90,11 @@
actor_enum = {
params = {}, sql = [[
select a.id, a.nym, a.handle, a.origin, a.bio,
a.avataruri, a.rank, a.quota, a.key,
- extract(epoch from knownsince)::bigint,
+ 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
]];
@@ -138,11 +143,11 @@
actor_session_fetch = {
params = {uint64, lib.store.inet}, sql = [[
select a.id, a.nym, a.handle, a.origin, a.bio,
a.avataruri, a.rank, a.quota, a.key,
- extract(epoch from knownsince)::bigint,
+ extract(epoch from a.knownsince)::bigint,
coalesce(a.handle || '@' || s.domain,
'@' || a.handle) as xid,
au.restrict,
array['post' ] <@ au.restrict as can_post,
@@ -158,11 +163,46 @@
where au.aid = $1::bigint and au.blacklist = false and
(au.netmask is null or au.netmask >> $2::inet)
]];
};
+
+ post_create = {
+ params = {uint64, rawstring, rawstring, rawstring}, sql = [[
+ insert into parsav_posts (
+ author, subject, acl, body,
+ posted, discovered,
+ circles, mentions
+ ) values (
+ $1::bigint, case when $2::text = '' then null else $2::text end,
+ $3::text, $4::text,
+ now(), now(), array[]::bigint[], array[]::bigint[]
+ ) returning id
+ ]]; -- TODO array handling
+ };
+
+ instance_timeline_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,
+ p.parent, null::text
+ from parsav_posts as p
+ inner join parsav_actors as a on p.author = a.id
+ 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
+ ]]
+ };
}
+ --($5::bool = false or p.parent is null) and
local struct pqr {
sz: intptr
res: &lib.pq.PGresult
}
@@ -280,16 +320,19 @@
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
- --lib.io.fmt('uid=%llu(%llx)\n',[args[i]],[args[i]])
[args[i]] = lib.math.netswap(ty, [args[i]])
end
end
end
@@ -316,30 +359,60 @@
return pqr {ct, res}
end
end
end
+local terra row_to_post(r: &pqr, row: intptr): lib.mem.ptr(lib.store.post)
+ --lib.io.fmt("body ptr %p len %llu\n", r:string(row,5), r:len(row,5))
+ --lib.io.fmt("acl ptr %p len %llu\n", r:string(row,4), r:len(row,4))
+ var subj: rawstring, sblen: intptr
+ if r:null(row,3)
+ then subj = nil sblen = 0
+ else subj = r:string(row,3) sblen = r:len(row,3)+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 = { `nil, `0 }; --FIXME
+ }) ]
+ p.ptr.id = r:int(uint64,row,1)
+ p.ptr.author = r:int(uint64,row,2)
+ p.ptr.posted = r:int(uint64,row,6)
+ p.ptr.discovered = r:int(uint64,row,7)
+ if r:null(row,8)
+ then p.ptr.parent = 0
+ else p.ptr.parent = r:int(uint64,row,8)
+ end
+ 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)
-
- if r:cols() >= 9 then
- a = [ lib.str.encapsulate(lib.store.actor, {
- nym = {`r:string(row,1), `r:len(row,1)+1};
- bio = {`r:string(row,4), `r:len(row,4)+1};
- avatar = {`r:string(row,5), `r:len(row,5)+1};
- handle = {`r:string(row, 2); `r:len(row,2) + 1};
- xid = {`r:string(row, 10); `r:len(row,10) + 1};
- }) ]
- else
- a = [ lib.str.encapsulate(lib.store.actor, {
- nym = {`r:string(row,1), `r:len(row,1)+1};
- bio = {`r:string(row,4), `r:len(row,4)+1};
- avatar = {`r:string(row,5), `r:len(row,5)+1};
- handle = {`r:string(row, 2); `r:len(row,2) + 1};
- }) ]
- a.ptr.xid = nil
+ var av: rawstring, avlen: intptr
+ var nym: rawstring, nymlen: intptr
+ var bio: rawstring, biolen: 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
+ a = [ lib.str.encapsulate(lib.store.actor, {
+ nym = {`nym, `nymlen};
+ bio = {`bio, `biolen};
+ avatar = {`av,`avlen};
+ handle = {`r:string(row, 2); `r:len(row,2) + 1};
+ xid = {`r:string(row, 10); `r:len(row,10) + 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, 9);
@@ -552,8 +625,37 @@
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)
+ if r.sz == 0 then return 0 end
+ defer r:free()
+ var id = r:int(uint64,0,0)
+ return id
+ end];
+
+ instance_timeline_fetch = [terra(src: &lib.store.source, rg: lib.store.range)
+ var r = pqr { sz = 0 }
+ if rg.mode == 0 then
+ r = queries.instance_timeline_fetch.exec(src,rg.from_time,rg.to_time,0,0)
+ elseif rg.mode == 1 then
+ r = queries.instance_timeline_fetch.exec(src,rg.from_time,0,rg.to_idx,0)
+ elseif rg.mode == 2 then
+ r = queries.instance_timeline_fetch.exec(src,0,rg.to_time,0,rg.from_idx)
+ elseif rg.mode == 3 then
+ r = queries.instance_timeline_fetch.exec(src,0,0,rg.to_idx,rg.from_idx)
+ end
+
+ 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) end -- MUST FREE ALL
+
+ return ret
+ end];
}
return b
Index: mem.t
==================================================================
--- mem.t
+++ mem.t
@@ -16,22 +16,48 @@
return `p {
ptr = [&ty:astype()](m.heapa_raw(sizeof(ty) * sz));
ct = sz;
}
end)
+
+function m.cache(ty,sz)
+ sz = sz or 32
+ local struct c {
+ store: ty[sz]
+ top: intptr
+ cur: intptr
+ }
+ c.name = string.format('cache<%s,%u>', tostring(ty), sz)
+ terra c:insert(v: ty)
+ if [ty.ptr_basetype ~= nil] then
+ if self.cur < self.top then self.store[self.cur]:free() end
+ end
+ self.store[self.cur] = v
+ self.top = lib.math.biggest(self.top, self.cur + 1)
+ self.cur = (self.cur + 1) % sz
+ return v
+ end
+ c.metamethods.__apply = terra(self: &c, idx: intptr) return &self.store[idx] end
+ if ty.ptr_basetype then
+ terra c:free()
+ for i=0,self.top do self.store[i]:free() end
+ end
+ end
+ return c
+end
local function mkptr(ty, dyn)
local t = terralib.types.newstruct(string.format('%s<%s>', dyn and 'ptr' or 'ref', ty))
t.entries = {
{'ptr', &ty};
{'ct', intptr};
}
t.ptr_basetype = ty
local recurse = false
- if ty:isstruct() then
- if ty.methods.free then recurse = true end
- end
+ --if ty:isstruct() then
+ --if ty.methods.free then recurse = true end
+ --end
t.metamethods.__not = macro(function(self)
return `self.ptr
end)
if dyn then
t.methods = {
@@ -45,10 +71,11 @@
return true
end
return false
end;
init = terra(self: &t, newct: intptr): bool
+ if newct == 0 then self.ct = 0 self.ptr = nil return false end
var nv = [&ty](m.heapa_raw(sizeof(ty) * newct))
if nv ~= nil then
self.ptr = nv
self.ct = newct
return true
@@ -107,47 +134,45 @@
{field = 'run', type = intptr};
}
local terra biggest(a: intptr, b: intptr)
if a > b then return a else return b end
end
+ terra v:init(run: intptr): bool
+ if not self.storage:init(run) then return false end
+ self.run = run
+ self.sz = 0
+ return true
+ end;
terra v:assure(n: intptr)
if self.storage.ct < n then
self.storage:resize(biggest(n, self.storage.ct + self.run))
end
end
- v.methods = {
- init = terra(self: &v, run: intptr): bool
- if not self.storage:init(run) then return false end
- self.run = run
- self.sz = 0
- return true
- end;
- new = terra(self: &v): &ty
- self:assure(self.sz + 1)
- self.sz = self.sz + 1
- return self.storage.ptr + (self.sz - 1)
- end;
- push = terra(self: &v, val: ty)
- self:assure(self.sz + 1)
- self.storage.ptr[self.sz] = val
- self.sz = self.sz + 1
- end;
- free = terra(self: &v) self.storage:free() end;
- last = terra(self: &v, idx: intptr): &ty
- if self.sz > idx then
- return self.storage.ptr + (self.sz - (idx+1))
- else lib.bail('vector underrun!') end
- end;
- crush = terra(self: &v)
- self.storage:resize(self.sz)
- return self.storage
- end;
- }
+ terra v:new(): &ty
+ self:assure(self.sz + 1)
+ self.sz = self.sz + 1
+ return self.storage.ptr + (self.sz - 1)
+ end;
+ terra v:push(val: ty)
+ self:assure(self.sz + 1)
+ self.storage.ptr[self.sz] = val
+ self.sz = self.sz + 1
+ end;
+ terra v:free() self.storage:free() end;
+ terra v:last(idx: intptr): &ty
+ if self.sz > idx then
+ return self.storage.ptr + (self.sz - (idx+1))
+ else lib.bail('vector underrun!') end
+ end;
+ terra v:crush()
+ self.storage:resize(self.sz)
+ return self.storage
+ end;
v.metamethods.__apply = terra(self: &v, idx: intptr): &ty -- no index??
if self.sz > idx then
return self.storage.ptr + idx
else lib.bail('vector overrun!') end
end
return v
end)
return m
Index: parsav.t
==================================================================
--- parsav.t
+++ parsav.t
@@ -92,22 +92,31 @@
end);
coalesce = macro(function(...)
local args = {...}
local ty = args[1].tree.type
local val = symbol(ty)
- local empty if ty.type == 'integer'
- then empty = `0
- else empty = `nil
- end
+ local empty
+ if ty.ptr_basetype then empty = `[ty]{ptr=nil,ct=0}
+ elseif ty.type == 'integer' then empty = `0
+ else empty = `nil end
local exp = quote val = [empty] end
for i=#args, 1, -1 do
local v = args[i]
- exp = quote
- if [v] ~= [empty]
- then val = v
- else [exp]
+ if ty.ptr_basetype then
+ exp = quote
+ if [v].ptr ~= nil
+ then val = v
+ else [exp]
+ end
+ end
+ else
+ exp = quote
+ if [v] ~= [empty]
+ then val = v
+ else [exp]
+ end
end
end
end
local q = quote
@@ -287,10 +296,12 @@
lib.pq = lib.loadlib('libpq','libpq-fe.h')
lib.load {
'mem', 'math', 'str', 'file', 'crypt';
'http', 'session', 'tpl', 'store';
+
+ 'smackdown'; -- md-alike parser
}
local be = {}
for _, b in pairs(config.backends) do
be[#be+1] = terralib.loadfile('backend/' .. b .. '.t')()
@@ -326,10 +337,12 @@
'render:nav';
'render:login';
'render:profile';
'render:userpage';
'render:compose';
+ 'render:tweet';
+ 'render:timeline';
'route';
}
do
local p = string.format('parsav: %s\nbuilt on %s\n', config.build.str, config.build.when)
@@ -446,11 +459,11 @@
os.exit(0)
end
local holler = print
local out = config.exe and 'parsav' or ('parsav.' .. config.outform)
-local linkargs = {'-O4'}
+local linkargs = {}
if bflag('quiet','q') then holler = function() end end
if bflag('asan','s') then linkargs[#linkargs+1] = '-fsanitize=address' end
if bflag('lsan','S') then linkargs[#linkargs+1] = '-fsanitize=leak' end
Index: render/compose.t
==================================================================
--- render/compose.t
+++ render/compose.t
@@ -11,19 +11,19 @@
}
end
var cotxt = form:tostr() defer cotxt:free()
var doc = data.view.docskel {
- instance = co.srv.cfg.instance.ptr;
- title = 'compose';
- body = cotxt.ptr;
- class = 'compose';
- navlinks = co.navbar.ptr;
+ instance = co.srv.cfg.instance;
+ title = lib.str.plit 'compose';
+ body = cotxt;
+ class = lib.str.plit 'compose';
+ navlinks = co.navbar;
}
var hdrs = array(
lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' }
)
doc:send(co.con,200,[lib.mem.ptr(lib.http.header)] {ct = 1, ptr = &hdrs[0]})
end
return render_compose
Index: render/login.t
==================================================================
--- render/login.t
+++ render/login.t
@@ -1,24 +1,25 @@
-- vim: ft=terra
+local pstr = lib.mem.ptr(int8)
+local P = lib.str.plit
local terra
-login_form(co: &lib.srv.convo, user: &lib.store.actor, creds: &lib.store.credset, msg: &int8)
+login_form(co: &lib.srv.convo, user: &lib.store.actor, creds: &lib.store.credset, msg: pstr)
var doc = data.view.docskel {
- instance = co.srv.cfg.instance.ptr;
- title = 'instance logon';
- class = 'login';
- navlinks = co.navbar.ptr;
+ instance = co.srv.cfg.instance;
+ title = lib.str.plit 'instance logon';
+ class = lib.str.plit 'login';
+ navlinks = co.navbar;
}
if user == nil then
var form = data.view.login_username {
loginmsg = msg;
}
- if form.loginmsg == nil then
- form.loginmsg = 'identify yourself for access to this instance.'
+ if form.loginmsg.ptr == nil then
+ form.loginmsg = lib.str.plit 'identify yourself for access to this instance.'
end
- var formtxt = form:tostr()
- doc.body = formtxt.ptr
+ doc.body = form:tostr()
elseif creds:sz() == 0 then
co:complain(403,'access denied','your host is not eligible to authenticate as this user')
return
elseif creds:sz() == 1 then
if creds.trust() then
@@ -29,34 +30,34 @@
var ch = data.view.login_challenge {
handle = user.handle;
name = lib.coalesce(user.nym, user.handle);
}
if creds.pw() then
- ch.challenge = 'enter the password associated with your account'
- ch.label = 'password'
- ch.method = 'pw'
+ ch.challenge = P'enter the password associated with your account'
+ ch.label = P'password'
+ ch.method = P'pw'
elseif creds.otp() then
- ch.challenge = 'enter a valid one-time password for your account'
- ch.label = 'OTP code'
- ch.method = 'otp'
+ ch.challenge = P'enter a valid one-time password for your account'
+ ch.label = P'OTP code'
+ ch.method = P'otp'
elseif creds.challenge() then
- ch.challenge = 'sign the challenge token: ...'
- ch.label = 'digest'
- ch.method = 'challenge'
+ ch.challenge = P'sign the challenge token: ...'
+ ch.label = P'digest'
+ ch.method = P'challenge'
else
co:complain(500,'login failure','unknown login method')
return
end
- doc.body = ch:tostr().ptr
+ doc.body = ch:tostr()
else
-- pick a method
end
var hdrs = array(
lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' }
)
doc:send(co.con,200,[lib.mem.ptr(lib.http.header)] {ct = 1, ptr = &hdrs[0]})
- lib.mem.heapf(doc.body)
+ doc.body:free()
end
return login_form
Index: render/nav.t
==================================================================
--- render/nav.t
+++ render/nav.t
@@ -5,12 +5,12 @@
if co.who ~= nil or co.srv.cfg.pol_sec == lib.srv.secmode.public then
t:lpush('timeline')
end
if co.who ~= nil then
t:lpush('compose profile configure log out')
+ t:lpush('">profile configure help log out')
else
- t:lpush('log in')
+ t:lpush('help log in')
end
return t:finalize()
end
return render_nav
Index: render/profile.t
==================================================================
--- render/profile.t
+++ render/profile.t
@@ -1,19 +1,24 @@
-- vim: ft=terra
+local pstr = lib.mem.ptr(int8)
+local terra cs(s: rawstring)
+ return pstr { ptr = s, ct = lib.str.sz(s) }
+end
+
local terra
render_profile(co: &lib.srv.convo, actor: &lib.store.actor)
var aux: lib.str.acc
- var auxp: rawstring
+ var auxp: pstr
if co.aid ~= 0 and co.who.id == actor.id then
- auxp = 'alter'
+ auxp = lib.str.plit 'alter'
elseif co.aid ~= 0 then
aux:compose('followchat')
if co.who.rights.powers:affect_users() then
aux:push('control',17)
end
- auxp = aux.buf
+ auxp = aux:finalize()
else
aux:compose('remote follow')
end
var avistr: lib.str.acc if actor.origin == 0 then
avistr:compose('/avi/',actor.handle)
@@ -20,32 +25,32 @@
end
var timestr: int8[26] lib.osclock.ctime_r(&actor.knownsince, ×tr[0])
var strfbuf: int8[28*4]
var stats = co.srv:actor_stats(actor.id)
- var sn_posts = lib.math.decstr_friendly(stats.posts, &strfbuf[ [strfbuf.type.N - 1] ])
- var sn_follows = lib.math.decstr_friendly(stats.follows, sn_posts - 1)
- var sn_followers = lib.math.decstr_friendly(stats.followers, sn_follows - 1)
- var sn_mutuals = lib.math.decstr_friendly(stats.mutuals, sn_followers - 1)
+ var sn_posts = cs(lib.math.decstr_friendly(stats.posts, &strfbuf[ [strfbuf.type.N - 1] ]))
+ var sn_follows = cs(lib.math.decstr_friendly(stats.follows, sn_posts.ptr - 1))
+ var sn_followers = cs(lib.math.decstr_friendly(stats.followers, sn_follows.ptr - 1))
+ var sn_mutuals = cs(lib.math.decstr_friendly(stats.mutuals, sn_followers.ptr - 1))
var profile = data.view.profile {
- nym = lib.coalesce(actor.nym, actor.handle);
- bio = lib.coalesce(actor.bio, "tall, dark, and mysterious");
- xid = actor.xid;
- avatar = lib.trn(actor.origin == 0, avistr.buf,
- lib.coalesce(actor.avatar, '/s/default-avatar.webp'));
+ nym = cs(lib.coalesce(actor.nym, actor.handle));
+ bio = cs(lib.coalesce(actor.bio, "tall, dark, and mysterious"));
+ xid = cs(actor.xid);
+ avatar = lib.trn(actor.origin == 0, pstr{ptr=avistr.buf,ct=avistr.sz},
+ cs(lib.coalesce(actor.avatar, '/s/default-avatar.webp')));
nposts = sn_posts, nfollows = sn_follows;
nfollowers = sn_followers, nmutuals = sn_mutuals;
- tweetday = timestr;
- timephrase = lib.trn(actor.origin == 0, 'joined', 'known since');
+ tweetday = cs(timestr);
+ timephrase = lib.trn(actor.origin == 0, lib.str.plit'joined', lib.str.plit'known since');
auxbtn = auxp;
}
var ret = profile:tostr()
if actor.origin == 0 then avistr:free() end
- if not (co.aid ~= 0 and co.who.id == actor.id) then aux:free() end
+ if not (co.aid ~= 0 and co.who.id == actor.id) then auxp:free() end
return ret
end
return render_profile
ADDED render/timeline.t
Index: render/timeline.t
==================================================================
--- render/timeline.t
+++ render/timeline.t
@@ -0,0 +1,49 @@
+-- vim: ft=terra
+local modes = lib.enum {'follow','mutual','srvlocal','fediglobal','circle'}
+local terra
+render_timeline(co: &lib.srv.convo, modestr: lib.mem.ref(int8))
+ var mode = modes.srvlocal
+ var circle: uint64 = 0
+ -- if modestr:cmpl('local') then mode = modes.srvlocal
+ -- elseif modestr:cmpl('mutual') then mode = modes.mutual
+ -- elseif modestr:cmpl('global') then mode = modes.fediglobal
+ -- elseif modestr:cmpl('circle') then mode = modes.circle
+ -- end
+
+ var stoptime = lib.osclock.time(nil)
+
+ var posts = [lib.mem.vec(lib.mem.ptr(lib.store.post))] {
+ sz = 0, run = 0
+ }
+ if mode == modes.follow then
+ elseif mode == modes.srvlocal then
+ posts = co.srv:instance_timeline_fetch(lib.store.range {
+ mode = 1; -- T->I
+ from_time = stoptime;
+ to_idx = 64;
+ })
+ elseif mode == modes.fediglobal then
+ elseif mode == modes.circle then
+ end
+
+ var acc: lib.str.acc acc:init(1024)
+ for i = 0, posts.sz do
+ lib.render.tweet(co, posts(i).ptr, &acc)
+ posts(i):free()
+ end
+ posts:free()
+
+ var doc = data.view.docskel {
+ instance = co.srv.cfg.instance;
+ title = lib.str.plit'timeline';
+ body = acc:finalize();
+ class = lib.str.plit'timeline';
+ navlinks = co.navbar;
+ }
+ var hdrs = array(
+ lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' }
+ )
+ doc:send(co.con,200,[lib.mem.ptr(lib.http.header)] {ct = 1, ptr = &hdrs[0]})
+ doc.body:free()
+end
+return render_timeline
ADDED render/tweet.t
Index: render/tweet.t
==================================================================
--- render/tweet.t
+++ render/tweet.t
@@ -0,0 +1,51 @@
+-- vim: ft=terra
+local pstr = lib.mem.ptr(int8)
+local terra cs(s: rawstring)
+ return pstr { ptr = s, ct = lib.str.sz(s) }
+end
+
+local terra
+render_tweet(co: &lib.srv.convo, p: &lib.store.post, acc: &lib.str.acc)
+ var author: &lib.store.actor
+ for j = 0, co.actorcache.top do
+ lib.io.fmt('scanning cache for author %llu (%llu/%llu)\n', p.author, j, co.actorcache.top)
+ if p.author == co.actorcache(j).ptr.id then
+ author = co.actorcache(j).ptr
+ lib.io.fmt('cache hit on idx %llu, skipping db lookup\n', j)
+ goto foundauth
+ end
+ end
+ lib.io.fmt('cache miss, checking db for id %llu\n', p.author)
+ author = co.actorcache:insert(co.srv:actor_fetch_uid(p.author)).ptr
+ lib.io.fmt('got author %s\n', author.handle)
+
+ ::foundauth::
+ var avistr: lib.str.acc if author.origin == 0 then
+ avistr:compose('/avi/',author.handle)
+ end
+ var timestr: int8[26] lib.osclock.ctime_r(&p.posted, ×tr[0])
+ lib.io.fmt('got body %s\n', author.handle)
+
+ var bhtml = lib.smackdown.html([lib.mem.ptr(int8)] {ptr=p.body,ct=0}) defer bhtml:free()
+
+ var idbuf: int8[lib.math.shorthand.maxlen]
+ var idlen = lib.math.shorthand.gen(p.id, idbuf)
+ var permalink: lib.str.acc permalink:compose('/post/',{idbuf,idlen})
+
+ var tpl = data.view.tweet {
+ text = bhtml;
+ subject = cs(lib.coalesce(p.subject,''));
+ nym = cs(lib.coalesce(author.nym, author.handle));
+ xid = cs(author.xid);
+ when = cs(×tr[0]);
+ avatar = cs(lib.trn(author.origin == 0, avistr.buf,
+ lib.coalesce(author.avatar, '/s/default-avatar.webp')));
+ acctlink = cs(author.xid);
+ permalink = permalink:finalize();
+ }
+ defer tpl.permalink:free()
+ if acc ~= nil then tpl:append(acc) return [lib.mem.ptr(int8)]{ptr=nil,ct=0} end
+ var txt = tpl:tostr()
+ return txt
+end
+return render_tweet
Index: render/userpage.t
==================================================================
--- render/userpage.t
+++ render/userpage.t
@@ -1,26 +1,27 @@
-- vim: ft=terra
local terra
render_userpage(co: &lib.srv.convo, actor: &lib.store.actor)
- var ti: lib.str.acc defer ti:free()
+ var ti: lib.str.acc
if co.aid ~= 0 and co.who.id == actor.id then
ti:compose('my profile')
else
ti:compose('profile :: ', actor.handle)
end
var pftxt = lib.render.profile(co,actor) defer pftxt:free()
var doc = data.view.docskel {
- instance = co.srv.cfg.instance.ptr;
- title = ti.buf;
- body = pftxt.ptr;
- class = 'profile';
- navlinks = co.navbar.ptr;
+ instance = co.srv.cfg.instance;
+ title = ti:finalize();
+ body = pftxt;
+ class = lib.str.plit 'profile';
+ navlinks = co.navbar;
}
var hdrs = array(
lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' }
)
doc:send(co.con,200,[lib.mem.ptr(lib.http.header)] {ct = 1, ptr = &hdrs[0]})
+ doc.title:free()
end
return render_userpage
Index: route.t
==================================================================
--- route.t
+++ route.t
@@ -1,8 +1,11 @@
-- vim: ft=terra
local r = lib.srv.route
local method = lib.http.method
+local pstring = lib.mem.ptr(int8)
+local rstring = lib.mem.ref(int8)
+local hpath = lib.mem.ptr(rstring)
local http = {}
terra http.actor_profile_xid(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t)
var handle = [lib.mem.ptr(int8)] { ptr = &uri.ptr[2], ct = 0 }
for i=2,uri.ct do
@@ -59,23 +62,21 @@
end
terra http.login_form(co: &lib.srv.convo, meth: method.t)
if meth == method.get then
-- request a username
- lib.render.login(co, nil, nil, nil)
+ lib.render.login(co, nil, nil, lib.str.plit(nil))
elseif meth == method.post then
var usn, usnl = co:postv('user')
- lib.dbg('got name ',{usn,usnl})
- lib.io.fmt('name len %llu\n',usnl)
var am, aml = co:postv('authmethod')
var chrs, chrsl = co:postv('response')
var cs, authok = co.srv:actor_auth_how(co.peer, usn)
var act = co.srv:actor_fetch_xid([lib.mem.ptr(int8)] {
ptr = usn, ct = usnl
})
if authok == false then
- lib.render.login(co, nil, nil, 'access denied')
+ lib.render.login(co, nil, nil, lib.str.plit'access denied')
return
end
var fakeact = false
var fakeactor: lib.store.actor
if act.ptr == nil then
@@ -92,11 +93,11 @@
act.ptr = &fakeactor
act.ptr.rights = lib.store.rights_default()
end
if am == nil then
-- pick an auth method
- lib.render.login(co, act.ptr, &cs, nil)
+ lib.render.login(co, act.ptr, &cs, lib.str.plit(nil))
else var aid: uint64 = 0
lib.dbg('authentication attempt beginning')
-- attempt login with provided method
if lib.str.ncmp('pw', am, lib.math.biggest(2,aml)) == 0 and chrs ~= nil then
aid = co.srv:actor_auth_pw(co.peer,
@@ -107,14 +108,13 @@
-- ··· --
else
lib.dbg('invalid auth method')
end
- lib.io.fmt('login got aid = %llu\n', aid)
-- error out
if aid == 0 then
- lib.render.login(co, nil, nil, 'authentication failure')
+ lib.render.login(co, nil, nil, lib.str.plit 'authentication failure')
else
var sesskey: int8[lib.session.maxlen + #lib.session.cookiename + #"=; Path=/" + 1]
do var p = &sesskey[0]
p = lib.str.ncpy(p, [lib.session.cookiename .. '='], [#lib.session.cookiename + 1])
p = p + lib.session.cookie_gen(co.srv.cfg.secret, aid, lib.osclock.time(nil), p)
@@ -136,13 +136,36 @@
lib.render.compose(co, nil)
elseif meth == method.post then
if co.who.rights.powers.post() == false then
co:complain(401,'insufficient privileges','you lack the post power and cannot perform this action') return
end
+ var text, textlen = co:postv("post")
+ var acl, acllen = co:postv("acl")
+ var subj, subjlen = co:postv("subject")
+ if text == nil or acl == nil then
+ co:complain(405, 'invalid post', 'every post must have at least body text and an ACL')
+ return
+ end
+ if subj == nil then subj = '' end
+
+ var p = lib.store.post {
+ author = co.who.id, acl = acl;
+ body = text, subject = subj;
+ }
+ var newid = co.srv:post_create(&p)
+ var idbuf: int8[lib.math.shorthand.maxlen]
+ var idlen = lib.math.shorthand.gen(newid, idbuf)
+ var redirto: lib.str.acc redirto:compose('/post/',{idbuf,idlen}) defer redirto:free()
+ co:reroute(redirto.buf)
end
end
+
+terra http.timeline(co: &lib.srv.convo, mode: hpath)
+ lib.render.timeline(co,lib.trn(mode.ptr == nil, rstring{ptr=nil}, mode.ptr[1]))
+ return
+end
do local branches = quote end
local filename, flen = symbol(&int8), symbol(intptr)
local page = symbol(lib.http.page)
local send = label()
@@ -182,22 +205,23 @@
-- entry points
terra r.dispatch_http(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t)
lib.dbg('handling URI of form ', {uri.ptr,uri.ct})
co.navbar = lib.render.nav(co)
+ lib.dbg('got nav ', {co.navbar.ptr,co.navbar.ct}, "||", co.navbar.ptr)
-- some routes are non-hierarchical, and can be resolved with a simple strcmp
-- we run through those first before giving up and parsing the URI
if uri.ptr[0] ~= @'/' then
co:complain(404, 'what the hell', 'how did you do that')
return
elseif uri.ct == 1 then -- root
- lib.io.fmt('root directory, aid is %llu\n', co.aid)
if (co.srv.cfg.pol_sec == lib.srv.secmode.private or
co.srv.cfg.pol_sec == lib.srv.secmode.lockdown) and co.aid == 0 then
http.login_form(co, meth)
else
-- FIXME display home screen
+ http.timeline(co, hpath {ptr=nil})
goto notfound
end
return
elseif uri.ptr[1] == @'@' then
http.actor_profile_xid(co, uri, meth)
@@ -227,12 +251,14 @@
return
else -- hierarchical routes
var path = lib.http.hier(uri) defer path:free()
if path.ptr[0]:cmp(lib.str.lit('user')) then
http.actor_profile_uid(co, path, meth)
+ elseif path.ptr[0]:cmp(lib.str.lit('tl')) then
+ http.timeline(co, path)
else goto notfound end
return
end
::wrongmeth:: co:complain(405, 'method not allowed', 'that method is not meaningful for this endpoint') do return end
::notfound:: co:complain(404, 'not found', 'no such resource available') do return end
end
Index: schema.sql
==================================================================
--- schema.sql
+++ schema.sql
@@ -33,10 +33,11 @@
drop table if exists parsav_servers cascade;
create table parsav_servers (
id bigint primary key default (1+random()*(2^63-1))::bigint,
domain text not null,
key bytea,
+ knownsince timestamp,
parsav boolean -- whether to use parsav protocol extensions
);
drop table if exists parsav_actors cascade;
create table parsav_actors (
@@ -43,16 +44,17 @@
id bigint primary key default (1+random()*(2^63-1))::bigint,
nym text,
handle text not null, -- nym [@handle@origin]
origin bigint references parsav_servers(id)
on delete cascade, -- null origin = local actor
+ knownsince timestamp,
bio text,
avataruri text, -- null if local
rank smallint not null default 0,
quota integer not null default 1000,
key bytea, -- private if localactor; public if remote
- title text
+ title text,
unique (handle,origin)
);
drop table if exists parsav_rights cascade;
@@ -83,24 +85,21 @@
subject text,
acl text not null default 'all', -- just store the script raw 🤷
body text,
posted timestamp not null,
discovered timestamp not null,
- scope smallint not null,
- convo bigint,
- parent bigint,
- circles bigint[],
- mentions bigint[]
+ parent bigint not null default 0,
+ circles bigint[], -- TODO at edit or creation, iterate through each circle
+ mentions bigint[], -- a user has, check if it can see her post, and if so add
+
+ convoheaduri text
+ -- only used for tracking foreign conversations and tying them to post heads;
+ -- local conversations are tracked directly and mapped to URIs based on the
+ -- head's ID. null if native tweet or not the first tweet in convo
);
drop table if exists parsav_conversations cascade;
-create table parsav_conversations (
- id bigint primary key default (1+random()*(2^63-1))::bigint,
- uri text not null,
- discovered timestamp not null,
- head bigint references parsav_posts(id)
-);
drop table if exists parsav_rels cascade;
create table parsav_rels (
relator bigint references parsav_actors(id)
on delete cascade, -- e.g. follower
@@ -144,11 +143,11 @@
drop table if exists parsav_circles cascade;
create table parsav_circles (
id bigint primary key default (1+random()*(2^63-1))::bigint,
owner bigint not null references parsav_actors(id),
name text not null,
- members bigint[] not null default array[],
+ members bigint[] not null default array[]::bigint[],
unique (owner,name)
);
drop table if exists parsav_rooms cascade;
@@ -164,23 +163,25 @@
create table parsav_room_members (
room bigint references parsav_rooms(id),
member bigint references parsav_actors(id),
rank smallint not null default 0,
admin boolean not null default false, -- non-admins with rank can only moderate + invite
- title text -- admin-granted title like reddit flair
+ title text, -- admin-granted title like reddit flair
+ vouchedby bigint references parsav_actors(id)
);
drop table if exists parsav_invites cascade;
create table parsav_invites (
id bigint primary key default (1+random()*(2^63-1))::bigint,
-- when a user is created from an invite, the invite is deleted and the invite
-- ID becomes the user ID. privileges granted on the invite ID during the invite
-- process are thus inherited by the user
+ issuer bigint references parsav_actors(id),
handle text, -- admin can lock invite to specific handle
rank smallint not null default 0,
quota integer not null default 1000
-};
+);
drop table if exists parsav_interventions cascade;
create table parsav_interventions (
id bigint primary key default (1+random()*(2^63-1))::bigint,
issuer bigint references parsav_actors(id) not null,
ADDED smackdown.t
Index: smackdown.t
==================================================================
--- smackdown.t
+++ smackdown.t
@@ -0,0 +1,138 @@
+-- vim: ft=terra
+-- smackdown is parsav's terrible, terrible custom markdown-alike parser
+
+local m = {}
+local pstr = lib.mem.ptr(int8)
+
+local segt = {
+ none = 0, para = 1, head = 2, listing = 3
+}
+
+local autolink_protos = {
+ 'https', 'http', 'ftp', 'gopher', 'gemini', 'ircs', 'irc';
+ 'mailto', 'about', 'sshfs', 'afp', 'smb', 'data', 'file';
+ 'dav', 'git', 'svn', 'cvs', 'dns', 'finger', 'pop', 'imap';
+ 'pops', 'imaps', 'torrent', 'magnet', 'news', 'snews', 'nfs';
+ 'nntp', 'sms', 'tel', 'telnet', 'vnc', 'webcal', 'ws', 'wss';
+ 'xmpp';
+}
+
+local struct state {
+ segt: uint
+ bqlvl: uint
+ curpos: rawstring
+ blockstart: rawstring
+}
+
+terra state:segend(ofs: uint)
+-- takes a string offset and returns true if it indexes th
+-- end of the current block
+ var s = self.curpos + ofs
+ if s[0] ~= @'\n' then return false end
+ if self.segt == segt.head then return true end -- headers can only be 1 line
+-- if s[1] == '#'
+
+end
+
+local terra isws(c: int8)
+ return c == @' ' or c == @'\n' or c == @'\t' or c == @'\r'
+end
+
+local terra scanline(l: rawstring, max: intptr, n: rawstring, nc: intptr)
+ if l == nil then return nil end
+ for i=0,max do
+ for j=0,nc do
+ if l[i+j] == @'\n' then return nil end
+ if l[i+j] ~= n[j] then goto nexti end
+ end
+ do return l+i end
+ ::nexti::end
+end
+
+local terra scanline_wordend(l: rawstring, max: intptr, n: rawstring, nc: intptr)
+ var sl = scanline(l,max,n,nc)
+ if sl == nil then return nil else sl = sl + nc end
+ if sl >= l+max or isws(@sl) then return sl-nc end
+ return nil
+end
+
+terra m.html(md: pstr)
+ if md.ct == 0 then md.ct = lib.str.sz(md.ptr) end
+ var styled: lib.str.acc styled:init(md.ct)
+
+ do var i = 0 while i < md.ct do
+ var wordstart = (i == 0 or isws(md.ptr[i-1]))
+ var wordend = (i == md.ct - 1 or isws(md.ptr[i+1]))
+
+ var here = md.ptr + i
+ var rem = md.ct - i
+ if @here == @'[' then
+ var sep = scanline(here,rem, '](', 2)
+ var term = scanline(sep+2,rem - ((sep+2)-here), ')', 1)
+ if sep ~= nil and term ~= nil then
+ styled:lpush('')
+ :push(here+1,(sep-here) - 1)
+ :lpush('')
+ i = (term - md.ptr) + 1
+ goto skip
+ else goto fallback end
+ end
+
+ if wordstart and rem >= 7 and lib.str.ncmp('***',here,3)==0 then
+ var term = scanline_wordend(here+4,rem-4,'***',3)
+ if term ~= nil then
+ styled:lpush('')
+ :push(here+3, (term-here) - 3)
+ :lpush('')
+ i = (term - md.ptr) + 3
+ goto skip
+ end
+ end
+
+ if wordstart and rem >= 5 and lib.str.ncmp('**',here,2)==0 then
+ var term = scanline_wordend(here+3,rem-3,'**',2)
+ if term ~= nil then
+ styled:lpush('')
+ :push(here+2, (term-here) - 2)
+ :lpush('')
+ i = (term - md.ptr) + 2
+ goto skip
+ end
+ end
+
+ if wordstart and rem >= 3 and @here == @'*' then
+ var term = scanline_wordend(here+2,rem-2,'*',1)
+ if term ~= nil then
+ styled:lpush('')
+ :push(here+1, (term-here) - 1)
+ :lpush('')
+ i = (term - md.ptr) + 1
+ goto skip
+ end
+ end
+
+ ::fallback::styled:push(here,1) -- :/
+ i = i + 1
+ ::skip::end end
+
+ -- we make two passes: the first detects and transforms inline elements,
+ -- the second carries out block-level organization
+
+ var html: lib.str.acc html:init(styled.sz)
+ var s = state {
+ segt = segt.none;
+ bqlvl = 0;
+ curpos = md.ptr;
+ blockstart = nil;
+ }
+ while s.curpos < md.ptr + md.ct do
+ s.curpos = s.curpos + 1
+ end
+
+ html:free() -- JUST FOR NOW
+ return styled:finalize()
+end
+
+return m
Index: srv.t
==================================================================
--- srv.t
+++ srv.t
@@ -18,10 +18,23 @@
terra cfgcache:free() -- :/
self.secret:free()
self.instance:free()
end
+
+terra srv:instance_timeline_fetch(r: lib.store.range): lib.mem.vec(lib.mem.ptr(lib.store.post))
+ var all: lib.mem.vec(lib.mem.ptr(lib.store.post)) all:init(64)
+ for i=0,self.sources.ct do var src = self.sources.ptr + i
+ if src.handle ~= nil and src.backend.instance_timeline_fetch ~= nil then
+ var lst = src:instance_timeline_fetch(r)
+ all:assure(all.sz + lst.ct)
+ for j=0, lst.ct do all:push(lst.ptr[j]) end
+ lst:free()
+ end
+ end
+ return all
+end
srv.metamethods.__methodmissing = macro(function(meth, self, ...)
local primary, ptr, stat, simple, oid = 0,1,2,3,4
local tk, rt = primary
local expr = {...}
@@ -36,14 +49,24 @@
elseif rt.ptr_basetype then tk = ptr end
break
end
end
+ local r = symbol(rt)
if tk == primary then
- return `self.sources.ptr[0]:[meth]([expr])
+ return quote
+ var [r]
+ for i=0,self.sources.ct do var src = self.sources.ptr + i
+ if src.handle ~= nil and src.backend.[meth] ~= nil then
+ r = src:[meth]([expr])
+ goto success
+ end
+ end
+ lib.bail(['no active backends provide critical capability ' .. meth .. '!'])
+ ::success::;
+ in r end
else local ok, empty
- local r = symbol(rt)
if tk == ptr then
ok = `r.ptr ~= nil
empty = `[rt]{ptr=nil,ct=0}
elseif tk == stat then
ok = `r.ok == true
@@ -56,11 +79,11 @@
empty = `0
end
return quote
var [r] = empty
for i=0,self.sources.ct do var src = self.sources.ptr + i
- if src.handle ~= nil then
+ if src.handle ~= nil and src.backend.[meth] ~= nil then
r = src:[meth]([expr])
if [ok] then break
else r = empty end
end
end
@@ -76,10 +99,11 @@
who: &lib.store.actor -- who we're logged in as, if aid ~= 0
peer: lib.store.inet
reqtype: lib.http.mime.t -- negotiated content type
-- cache
navbar: lib.mem.ptr(int8)
+ actorcache: lib.mem.cache(lib.mem.ptr(lib.store.actor),32) -- naive cache to avoid unnecessary queries
-- private
varbuf: lib.mem.ptr(int8)
vbofs: &int8
}
@@ -120,27 +144,30 @@
terra convo:reroute(dest: rawstring) self:reroute_cookie(dest,nil) end
terra convo:complain(code: uint16, title: rawstring, msg: rawstring)
var hdrs = array(lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' })
- var ti: lib.str.acc ti:compose('error :: ', title) defer ti:free()
- var bo: lib.str.acc bo:compose('