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('

error

',msg,'

') defer bo:free() + var ti: lib.str.acc ti:compose('error :: ', title) + var bo: lib.str.acc bo:compose('

',title,'

',msg,'

') var body = data.view.docskel { - instance = self.srv.cfg.instance.ptr; - title = ti.buf; - body = bo.buf; - class = 'error'; - navlinks = lib.coalesce(self.navbar.ptr, ''); + instance = self.srv.cfg.instance; + title = ti:finalize(); + body = bo:finalize(); + class = lib.str.plit 'error'; + navlinks = lib.coalesce(self.navbar, [lib.mem.ptr(int8)]{ptr='',ct=0}); } - if body.body == nil then - body.body = "i'm sorry, dave. i can't let you do that" + if body.body.ptr == nil then + body.body = lib.str.plit"i'm sorry, dave. i can't let you do that" end body:send(self.con, code, [lib.mem.ptr(lib.http.header)] { ptr = &hdrs[0], ct = [hdrs.type.N] }) + + body.title:free() + body.body:free() end -- CALL ONLY ONCE PER VAR terra convo:postv(name: rawstring) if self.varbuf.ptr == nil then @@ -148,12 +175,14 @@ self.vbofs = self.varbuf.ptr end var o = lib.net.mg_http_get_var(&self.msg.body, name, self.vbofs, self.varbuf.ct - (self.vbofs - self.varbuf.ptr)) if o > 0 then var r = self.vbofs - self.vbofs = self.vbofs + o - return r, o + self.vbofs = self.vbofs + o + 1 + @(self.vbofs - 1) = 0 + var norm = lib.str.normalize([lib.mem.ptr(int8)]{ptr = r, ct = o}) + return norm.ptr, norm.ct else return nil, 0 end end terra convo:getv(name: rawstring) if self.varbuf.ptr == nil then @@ -161,12 +190,14 @@ self.vbofs = self.varbuf.ptr end var o = lib.net.mg_http_get_var(&self.msg.query, name, self.vbofs, self.varbuf.ct - (self.vbofs - self.varbuf.ptr)) if o > 0 then var r = self.vbofs - self.vbofs = self.vbofs + o - return r, o + self.vbofs = self.vbofs + o + 1 + @(self.vbofs - 1) = 0 + var norm = lib.str.normalize([lib.mem.ptr(int8)]{ptr = r, ct = o}) + return norm.ptr, norm.ct else return nil, 0 end end local urimatch = macro(function(uri, ptn) return `lib.net.mg_globmatch(ptn, [#ptn], uri.ptr, uri.ct+1) @@ -225,10 +256,12 @@ con = con, srv = server, msg = msg; aid = 0, who = nil, peer = peer; reqtype = lib.http.mime.none; } co.varbuf.ptr = nil co.navbar.ptr = nil + co.actorcache.top = 0 + co.actorcache.cur = 0 -- first, check for an accept header. if it's there, we need to -- iterate over the values and pick the highest-priority one do var acc = lib.http.findheader(msg, 'Accept') -- TODO handle q-value @@ -346,10 +379,11 @@ end if co.aid ~= 0 then lib.mem.heapf(co.who) end if co.varbuf.ptr ~= nil then co.varbuf:free() end if co.navbar.ptr ~= nil then co.navbar:free() end + co.actorcache:free() end end end; } Index: static/style.scss ================================================================== --- static/style.scss +++ static/style.scss @@ -1,25 +1,27 @@ $color: hsl(323,100%,65%); %sans { font-family: "Alegreya Sans", "Open Sans", sans-serif; } %serif { font-family: Alegreya, GaramondNo8, "Garamond Premier Pro", "Adobe Garamond", Garamond, Junicode, serif; } %teletype { font-family: "Inconsolata LGC", Inconsolata, monospace; font-size: 12pt !important; } -@function tone($pct) { @return adjust-color($color, $lightness: $pct) } +@function tone($pct, $alpha: 0) { @return adjust-color($color, $lightness: $pct, $alpha: $alpha) } body { @extend %sans; - background-color: adjust-color($color, $lightness: -55%); - color: adjust-color($color, $lightness: 25%); + background-color: tone(-55%); + color: tone(25%); font-size: 14pt; margin: 0; padding: 0; } a[href] { - color: adjust-color($color, $lightness: 10%); + color: tone(10%); + text-decoration-color: adjust-color($color, $lightness: 10%, $alpha: -0.5); &:hover { color: white; - text-shadow: 0 0 15px adjust-color($color, $lightness: 20%); + text-shadow: 0 0 15px tone(20%); + text-decoration-color: adjust-color($color, $lightness: 10%, $alpha: -0.1); } } a[href^="//"], a[href^="http://"], a[href^="https://"] { // external link @@ -412,10 +414,56 @@ } } code { @extend %teletype; - background: adjust-color($color, $lightness: -50%); - border: 1px solid adjust-color($color, $lightness: -20%); + background: tone(-50%); + border: 1px solid tone(-20%); padding: 2px 6px; text-shadow: 2px 2px black; } + +div.post { + @extend %box; + display: grid; + grid-template-columns: 1in 1fr max-content; + grid-template-rows: 1fr max-content; + margin-bottom: 0.1in; + >.avatar { + grid-column: 1/2; grid-row: 1/2; + width: 1in; + } + >a[href].username { + display: block; + grid-column: 1/3; + grid-row: 2/3; + text-align: left; + text-decoration: none; + padding: 0.1in; + padding-left: 0.15in; + >.nym { font-weight: bold; } + color: tone(0%,-0.4); + > span.nym { color: tone(10%) } + > span.handle { color: tone(-5%) } + background: linear-gradient(to right, tone(-55%), transparent); + } + >.content { + grid-column: 2/4; grid-row: 1/2; + padding: 0.2in; + @extend %serif; + font-size: 110%; + } + > a[href].permalink { + display: block; + grid-column: 3/4; grid-row: 2/3; + font-size: 80%; + text-align: right; + padding: 0.1in; + padding-right: 0.15in; + font-style: italic; + background: linear-gradient(to left, tone(-55%,-0.5), transparent); + } +} + +a[href].rawlink { + @extend %teletype; +} Index: store.t ================================================================== --- store.t +++ store.t @@ -1,8 +1,8 @@ -- vim: ft=terra local m = { - timepoint = uint64; + timepoint = int64; scope = lib.enum { 'public', 'private', 'local'; 'personal', 'direct', 'circle'; }; notiftype = lib.enum { @@ -64,11 +64,11 @@ nym: str handle: str origin: uint64 bio: str avatar: str - knownsince: int64 + knownsince: m.timepoint rights: m.rights key: lib.mem.ptr(uint8) -- ephemera xid: str @@ -81,11 +81,11 @@ followers: intptr mutuals: intptr } struct m.range { - time: bool + mode: uint8 -- 0 == I->I, 1 == T->I, 2 == I->T, 3 == T->T union { from_time: m.timepoint from_idx: uint64 } union { @@ -97,18 +97,19 @@ struct m.post { id: uint64 author: uint64 subject: str body: str + acl: str posted: m.timepoint discovered: m.timepoint - scope: m.scope.t mentions: lib.mem.ptr(uint64) circles: lib.mem.ptr(uint64) --only meaningful if scope is set to circle convo: uint64 parent: uint64 - +-- ephemera + localpost: bool source: &m.source } local cnf = terralib.memoize(function(ty,rty) rty = rty or ty @@ -209,18 +210,18 @@ -- origin: inet actor_conf_str: cnf(rawstring, lib.mem.ptr(int8)) actor_conf_int: cnf(intptr, lib.stat(intptr)) - post_save: {&m.source, &m.post} -> bool - post_create: {&m.source, &m.post} -> bool + post_save: {&m.source, &m.post} -> {} + post_create: {&m.source, &m.post} -> uint64 actor_post_fetch_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(m.post) convo_fetch_xid: {&m.source,rawstring} -> lib.mem.ptr(m.post) convo_fetch_uid: {&m.source,uint64} -> lib.mem.ptr(m.post) - actor_timeline_fetch_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(m.post) - instance_timeline_fetch: {&m.source, m.range} -> lib.mem.ptr(m.post) + actor_timeline_fetch_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post)) + instance_timeline_fetch: {&m.source, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post)) } struct m.source { backend: &m.backend id: lib.mem.ptr(int8) Index: str.t ================================================================== --- str.t +++ str.t @@ -1,8 +1,10 @@ -- vim: ft=terra -- string.t: string classes local util = dofile('common.lua') +local pstr = lib.mem.ptr(int8) +local pref = lib.mem.ref(int8) local m = { sz = terralib.externfunction('strlen', rawstring -> intptr); cmp = terralib.externfunction('strcmp', {rawstring, rawstring} -> int); ncmp = terralib.externfunction('strncmp', {rawstring, rawstring, intptr} -> int); @@ -16,10 +18,11 @@ terralib.types.funcpointer({rawstring,rawstring},{int},true)); span = terralib.externfunction('strspn',{rawstring, rawstring} -> rawstring); } do local strptr = (lib.mem.ptr(int8)) + local strref = (lib.mem.ref(int8)) local byteptr = (lib.mem.ptr(uint8)) strptr.metamethods.__cast = function(from,to,e) if from == &int8 then return `strptr {ptr = e, ct = m.sz(e)} elseif to == &int8 then @@ -26,17 +29,38 @@ return e.ptr end end terra strptr:cmp(other: strptr) + if self.ptr == nil and other.ptr == nil then return true end + if self.ptr == nil or other.ptr == nil then return false end + + var sz = lib.math.biggest(self.ct, other.ct) + for i = 0, sz do + if self.ptr[i] == 0 and other.ptr[i] == 0 then return true end + if self.ptr[i] ~= other.ptr[i] then return false end + end + return true + end + terra strref:cmp(other: strref) + if self.ptr == nil and other.ptr == nil then return true end + if self.ptr == nil or other.ptr == nil then return false end + var sz = lib.math.biggest(self.ct, other.ct) for i = 0, sz do if self.ptr[i] == 0 and other.ptr[i] == 0 then return true end if self.ptr[i] ~= other.ptr[i] then return false end end return true end + + strptr.methods.cmpl = macro(function(self,other) + return `self:cmp(strptr { ptr = [other:asvalue()], ct = [#(other:asvalue())] }) + end) + strref.methods.cmpl = macro(function(self,other) + return `self:cmp(strref { ptr = [other:asvalue()], ct = [#(other:asvalue())] }) + end) terra byteptr:cmp(other: byteptr) var sz = lib.math.biggest(self.ct, other.ct) for i = 0, sz do if self.ptr[i] == 0 and other.ptr[i] == 0 then return true end @@ -43,10 +67,28 @@ if self.ptr[i] ~= other.ptr[i] then return false end end return true end end + +terra m.normalize(s: pstr) + var c: rawstring = s.ptr + var n: rawstring = s.ptr + while n < s.ptr + s.ct do + while @n == 0 or @n == @'\r' do + n = n + 1 + if n > s.ptr + s.ct then + c = c + 1 goto done + end + end + @c = @n + c = c + 1 + n = n + 1 + end ::done:: + @c = 0 + return pstr { ptr = s.ptr, ct = c - s.ptr } +end struct m.acc { buf: rawstring sz: intptr run: intptr @@ -72,18 +114,18 @@ lib.mem.heapf(self.buf) end end; terra m.acc:crush() - lib.dbg('crushing string accumulator') + --lib.dbg('crushing string accumulator') self.buf = [rawstring](lib.mem.heapr_raw(self.buf, self.sz)) self.space = self.sz return self end; terra m.acc:finalize() - lib.dbg('finalizing string accumulator') + --lib.dbg('finalizing string accumulator') self:crush() var pt: lib.mem.ptr(int8) pt.ptr = self.buf pt.ct = self.sz self.buf = nil @@ -116,11 +158,23 @@ self.buf[self.sz] = 0 return self end; m.lit = macro(function(str) - return `[lib.mem.ref(int8)] {ptr = [str:asvalue()], ct = [#(str:asvalue())]} + if str:asvalue() ~= nil then + return `[lib.mem.ref(int8)] {ptr = [str:asvalue()], ct = [#(str:asvalue())]} + else + return `[lib.mem.ref(int8)] {ptr = nil, ct = 0} + end +end) + +m.plit = macro(function(str) + if str:asvalue() ~= nil then + return `[lib.mem.ptr(int8)] {ptr = [str:asvalue()], ct = [#(str:asvalue())]} + else + return `[lib.mem.ptr(int8)] {ptr = nil, ct = 0} + end end) m.acc.methods.lpush = macro(function(self,str) return `self:push([str:asvalue()], [#(str:asvalue())]) end) m.acc.methods.ppush = terra(self: &m.acc, str: lib.mem.ptr(int8)) @@ -214,11 +268,15 @@ memreq_const = memreq_const + sz elseif type(sz:asvalue()) == 'number' then memreq_const = memreq_const + sz:asvalue() else memreq_exp = `[sz] + [memreq_exp] end - cpy = quote [kp] ; [ptr] = [&int8](lib.mem.cpy([ptr], str, sz)) end + cpy = quote [kp] ; + --lib.io.fmt('encapsulating string %p → %p [%s] sz %llu\n', str, [ptr], str, sz) + [ptr] = [&int8](lib.mem.cpy([ptr], str, sz)) + --lib.io.fmt(' :: encapsulated string %p [%s]\n', box.obj.[k],box.obj.[k]) + end if ty.ptr_basetype then cpy = quote [cpy]; [box].obj.[k].ct = sz end end isnull = `[str] == nil else Index: tpl.t ================================================================== --- tpl.t +++ tpl.t @@ -54,18 +54,18 @@ do local kfac = {} for afterseg,key in pairs(fields) do if not kfac[key] then rec.entries[#rec.entries + 1] = { field = key; - type = rawstring; + type = lib.mem.ptr(int8); } end kfac[key] = (kfac[key] or 0) + 1 end for key, fac in pairs(kfac) do tallyup[#tallyup + 1] = quote - [runningtally] = [runningtally] + lib.str.sz([symself].[key])*fac + [runningtally] = [runningtally] + ([symself].[key].ct)*fac end end end local copiers = {} @@ -78,19 +78,21 @@ for idx, seg in ipairs(segs) do copiers[#copiers+1] = quote [cpypos] = lib.mem.cpy([cpypos], [&opaque]([seg]), [#seg]) end senders[#senders+1] = quote lib.net.mg_send([destcon], [seg], [#seg]) end appenders[#appenders+1] = quote [accumulator]:push([seg], [#seg]) end if fields[idx] then + --local fsz = `lib.str.sz(symself.[fields[idx]]) + local fval = `symself.[fields[idx]].ptr + local fsz = `symself.[fields[idx]].ct copiers[#copiers+1] = quote - [cpypos] = lib.mem.cpy([cpypos], - [&opaque](symself.[fields[idx]]), - lib.str.sz(symself.[fields[idx]])) + [cpypos] = lib.mem.cpy([cpypos], [&opaque]([fval]), [fsz]) end senders[#senders+1] = quote - lib.net.mg_send([destcon], - symself.[fields[idx]], - lib.str.sz(symself.[fields[idx]])) + lib.net.mg_send([destcon], [fval], [fsz]) + end + appenders[#appenders+1] = quote + [accumulator]:push([fval], [fsz]) end end end local tid = tplspec.id or '' Index: view/tweet.tpl ================================================================== --- view/tweet.tpl +++ view/tweet.tpl @@ -1,12 +1,11 @@
- -
@when
-
+ + + @nym [@xid] +
- +
@subject
@text
+