parsav  Check-in [f9559a83fc]

Overview
Comment:iteration and important api adjustments
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: f9559a83fc2d82b62dc5aed0a13e9802bcccda3ccb9457b98a28439d31d425cb
User & Date: lexi on 2020-12-25 23:37:28
Other Links: manifest | tags
Context
2020-12-27
02:31
permissions work now check-in: bbfea467bf user: lexi tags: trunk
2020-12-25
23:37
iteration and important api adjustments check-in: f9559a83fc user: lexi tags: trunk
03:59
big ol iteration check-in: 5b3a03ad34 user: lexi tags: trunk
Changes

Added acl.t version [3939510d0a].

            1  +-- vim: ft=terra

Modified backend/pgsql.t from [0ea1a47601] to [1740166796].

     1      1   -- vim: ft=terra
            2  +local pstring = lib.mem.ptr(int8)
            3  +local binblob = lib.mem.ptr(uint8)
     2      4   local queries = {
     3      5   	conf_get = {
     4      6   		params = {rawstring}, sql = [[
     5      7   			select value from parsav_config
     6      8   				where key = $1::text limit 1
     7      9   		]];
     8     10   	};
................................................................................
    20     22   			delete from parsav_config where
    21     23   				key = $1::text 
    22     24   		]];
    23     25   	};
    24     26   
    25     27   	actor_fetch_uid = {
    26     28   		params = {uint64}, sql = [[
    27         -			select
    28         -				id, nym, handle, origin, bio,
    29         -				avataruri, rank, quota, key,
    30         -				extract(epoch from knownsince)::bigint
           29  +			select a.id, a.nym, a.handle, a.origin, a.bio,
           30  +			       a.avataruri, a.rank, a.quota, a.key, 
           31  +			       extract(epoch from a.knownsince)::bigint,
           32  +				   coalesce(a.handle || '@' || s.domain,
           33  +				            '@' || a.handle) as xid
    31     34   
    32         -			from parsav_actors
    33         -				where id = $1::bigint
           35  +			from      parsav_actors  as a
           36  +			left join parsav_servers as s
           37  +				on a.origin = s.id
           38  +			where a.id = $1::bigint
    34     39   		]];
    35     40   	};
    36     41   
    37     42   	actor_fetch_xid = {
    38         -		params = {lib.mem.ptr(int8)}, sql = [[
           43  +		params = {pstring}, sql = [[
    39     44   			select a.id, a.nym, a.handle, a.origin, a.bio,
    40     45   			       a.avataruri, a.rank, a.quota, a.key, 
    41         -			       extract(epoch from knownsince)::bigint,
           46  +			       extract(epoch from a.knownsince)::bigint,
    42     47   				   coalesce(a.handle || '@' || s.domain,
    43     48   				            '@' || a.handle) as xid,
    44     49   
    45     50   				coalesce(s.domain,
    46     51   				        (select value from parsav_config
    47     52   							where key='domain' limit 1)) as domain
    48     53   
................................................................................
    55     60   				  (a.origin is null and
    56     61   					  $1::text = a.handle or
    57     62   					  $1::text = ('@' || a.handle))
    58     63   		]];
    59     64   	};
    60     65   
    61     66   	actor_auth_pw = {
    62         -		params = {lib.mem.ptr(int8),rawstring,lib.mem.ptr(int8),lib.store.inet}, sql = [[
           67  +		params = {pstring,rawstring,pstring,lib.store.inet}, sql = [[
    63     68   			select a.aid from parsav_auth as a
    64     69   				left join parsav_actors as u on u.id = a.uid
    65     70   			where (a.uid is null or u.handle = $1::text or (
    66     71   					a.uid = 0 and a.name = $1::text
    67     72   				)) and
    68     73   				(a.kind = 'trust' or (a.kind = $2::text and a.cred = $3::bytea)) and
    69     74   				(a.netmask is null or a.netmask >> $4::inet)
................................................................................
    83     88   		]];
    84     89   	};
    85     90   
    86     91   	actor_enum = {
    87     92   		params = {}, sql = [[
    88     93   			select a.id, a.nym, a.handle, a.origin, a.bio,
    89     94   			       a.avataruri, a.rank, a.quota, a.key,
    90         -			       extract(epoch from knownsince)::bigint,
           95  +			       extract(epoch from a.knownsince)::bigint,
    91     96   				   coalesce(a.handle || '@' || s.domain,
    92     97   				            '@' || a.handle) as xid
    93     98   			from parsav_actors a
    94     99   			left join parsav_servers s on s.id = a.origin
    95    100   		]];
    96    101   	};
    97    102   
................................................................................
   136    141   		]]; -- cheat
   137    142   	};
   138    143   
   139    144   	actor_session_fetch = {
   140    145   		params = {uint64, lib.store.inet}, sql = [[
   141    146   			select a.id, a.nym, a.handle, a.origin, a.bio,
   142    147   			       a.avataruri, a.rank, a.quota, a.key,
   143         -			       extract(epoch from knownsince)::bigint,
          148  +			       extract(epoch from a.knownsince)::bigint,
   144    149   				   coalesce(a.handle || '@' || s.domain,
   145    150   				            '@' || a.handle) as xid,
   146    151   
   147    152   			       au.restrict,
   148    153   						array['post'  ] <@ au.restrict as can_post,
   149    154   						array['edit'  ] <@ au.restrict as can_edit,
   150    155   						array['acct'  ] <@ au.restrict as can_acct,
................................................................................
   156    161   			left join parsav_actors a     on au.uid = a.id
   157    162   			left join parsav_servers s    on a.origin = s.id
   158    163   
   159    164   			where au.aid = $1::bigint and au.blacklist = false and
   160    165   				(au.netmask is null or au.netmask >> $2::inet)
   161    166   		]];
   162    167   	};
          168  +
          169  +	post_create = {
          170  +		params = {uint64, rawstring, rawstring, rawstring}, sql = [[
          171  +			insert into parsav_posts (
          172  +				author, subject, acl, body,
          173  +				posted, discovered,
          174  +				circles, mentions
          175  +			) values (
          176  +				$1::bigint, case when $2::text = '' then null else $2::text end,
          177  +				$3::text, $4::text, 
          178  +				now(), now(), array[]::bigint[], array[]::bigint[]
          179  +			) returning id
          180  +		]]; -- TODO array handling
          181  +	};
          182  +
          183  +	instance_timeline_fetch = {
          184  +		params = {uint64, uint64, uint64, uint64}, sql = [[
          185  +			select true,
          186  +				p.id, p.author, p.subject, p.acl, p.body,
          187  +				extract(epoch from p.posted    )::bigint,
          188  +				extract(epoch from p.discovered)::bigint,
          189  +				p.parent, null::text
          190  +			from parsav_posts as p
          191  +				inner join parsav_actors as a on p.author = a.id
          192  +			where
          193  +				($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and
          194  +				($2::bigint = 0 or to_timestamp($2::bigint) < p.posted) and
          195  +				(a.origin is null)
          196  +			order by (p.posted, p.discovered) desc
          197  +			limit case when $3::bigint = 0 then null
          198  +			           else $3::bigint end
          199  +			offset $4::bigint
          200  +		]]
          201  +	};
   163    202   }
          203  +				--($5::bool = false or p.parent is null) and
   164    204   
   165    205   local struct pqr {
   166    206   	sz: intptr
   167    207   	res: &lib.pq.PGresult
   168    208   }
   169    209   terra pqr:free() if self.sz > 0 then lib.pq.PQclear(self.res) end end
   170    210   terra pqr:null(row: intptr, col: intptr)
................................................................................
   278    318   				;[pqt[lib.store.inet](false)]([args[i]], [&uint8](&ipbuf))
   279    319   			in &ipbuf[0] end
   280    320   			dumpers[#dumpers+1] = `lib.io.fmt([tostring(i)..'. got inet\n'])
   281    321   		elseif ty.ptr_basetype == int8 or ty.ptr_basetype == uint8 then
   282    322   			counters[i] = `[args[i]].ct
   283    323   			casts[i] = `[&int8]([args[i]].ptr)
   284    324   			dumpers[#dumpers+1] = `lib.io.fmt([tostring(i)..'. got ptr %llu %.*s\n'], [args[i]].ct, [args[i]].ct, [args[i]].ptr)
          325  +		elseif ty.ptr_basetype == bool then
          326  +			counters[i] = `1
          327  +			casts[i] = `[&int8]([args[i]].ptr)
          328  +			-- dumpers[#dumpers+1] = `lib.io.fmt([tostring(i)..'. got bool = %hhu\n'], @[args[i]].ptr)
   285    329   		elseif ty:isintegral() then
   286    330   			counters[i] = ty.bytes
   287    331   			casts[i] = `[&int8](&[args[i]])
   288    332   			dumpers[#dumpers+1] = `lib.io.fmt([tostring(i)..'. got int %llu\n'], [args[i]])
   289    333   			fixers[#fixers + 1] = quote
   290         -				--lib.io.fmt('uid=%llu(%llx)\n',[args[i]],[args[i]])
   291    334   				[args[i]] = lib.math.netswap(ty, [args[i]])
   292    335   			end
   293    336   		end
   294    337   	end
   295    338   
   296    339   	terra q.exec(src: &lib.store.source, [args])
   297    340   		var params = arrayof([&int8], [casts])
................................................................................
   314    357   			return pqr {0, nil}
   315    358   		else
   316    359   			return pqr {ct, res}
   317    360   		end
   318    361   	end
   319    362   end
   320    363   
          364  +local terra row_to_post(r: &pqr, row: intptr): lib.mem.ptr(lib.store.post)
          365  +	--lib.io.fmt("body ptr %p  len %llu\n", r:string(row,5), r:len(row,5))
          366  +	--lib.io.fmt("acl ptr %p  len %llu\n", r:string(row,4), r:len(row,4))
          367  +	var subj: rawstring, sblen: intptr
          368  +	if r:null(row,3)
          369  +		then subj = nil sblen = 0
          370  +		else subj = r:string(row,3) sblen = r:len(row,3)+1
          371  +	end
          372  +	var p = [ lib.str.encapsulate(lib.store.post, {
          373  +		subject = { `subj, `sblen };
          374  +		acl = {`r:string(row,4), `r:len(row,4)+1};
          375  +		body = {`r:string(row,5), `r:len(row,5)+1};
          376  +		--convoheaduri = { `nil, `0 }; --FIXME
          377  +	}) ]
          378  +	p.ptr.id = r:int(uint64,row,1)
          379  +	p.ptr.author = r:int(uint64,row,2)
          380  +	p.ptr.posted = r:int(uint64,row,6)
          381  +	p.ptr.discovered = r:int(uint64,row,7)
          382  +	if r:null(row,8)
          383  +		then p.ptr.parent = 0
          384  +		else p.ptr.parent = r:int(uint64,row,8)
          385  +	end 
          386  +	p.ptr.localpost = r:bool(row,0)
          387  +
          388  +	return p
          389  +end
   321    390   local terra row_to_actor(r: &pqr, row: intptr): lib.mem.ptr(lib.store.actor)
   322    391   	var a: lib.mem.ptr(lib.store.actor)
   323         -
   324         -	if r:cols() >= 9 then 
   325         -		a = [ lib.str.encapsulate(lib.store.actor, {
   326         -			nym = {`r:string(row,1), `r:len(row,1)+1};
   327         -			bio = {`r:string(row,4), `r:len(row,4)+1};
   328         -			avatar = {`r:string(row,5), `r:len(row,5)+1};
   329         -			handle = {`r:string(row, 2); `r:len(row,2) + 1};
   330         -			xid = {`r:string(row, 10); `r:len(row,10) + 1};
   331         -		}) ]
   332         -	else
   333         -		a = [ lib.str.encapsulate(lib.store.actor, {
   334         -			nym = {`r:string(row,1), `r:len(row,1)+1};
   335         -			bio = {`r:string(row,4), `r:len(row,4)+1};
   336         -			avatar = {`r:string(row,5), `r:len(row,5)+1};
   337         -			handle = {`r:string(row, 2); `r:len(row,2) + 1};
   338         -		}) ]
   339         -		a.ptr.xid = nil
          392  +	var av: rawstring, avlen: intptr
          393  +	var nym: rawstring, nymlen: intptr
          394  +	var bio: rawstring, biolen: intptr
          395  +	if r:null(row,5) then avlen = 0 av = nil else
          396  +		av = r:string(row,5)
          397  +		avlen = r:len(row,5)+1
          398  +	end
          399  +	if r:null(row,1) then nymlen = 0 nym = nil else
          400  +		nym = r:string(row,1)
          401  +		nymlen = r:len(row,1)+1
          402  +	end
          403  +	if r:null(row,4) then biolen = 0 bio = nil else
          404  +		bio = r:string(row,4)
          405  +		biolen = r:len(row,4)+1
   340    406   	end
          407  +	a = [ lib.str.encapsulate(lib.store.actor, {
          408  +		nym = {`nym, `nymlen};
          409  +		bio = {`bio, `biolen};
          410  +		avatar = {`av,`avlen};
          411  +		handle = {`r:string(row, 2); `r:len(row,2) + 1};
          412  +		xid = {`r:string(row, 10); `r:len(row,10) + 1};
          413  +	}) ]
   341    414   	a.ptr.id = r:int(uint64, row, 0);
   342    415   	a.ptr.rights = lib.store.rights_default();
   343    416   	a.ptr.rights.rank = r:int(uint16, row, 6);
   344    417   	a.ptr.rights.quota = r:int(uint32, row, 7);
   345    418   	a.ptr.knownsince = r:int(int64,row, 9);
   346    419   	if r:null(row,8) then
   347    420   		a.ptr.key.ct = 0 a.ptr.key.ptr = nil
................................................................................
   550    623   
   551    624   			return au, a
   552    625   		end
   553    626   
   554    627   		::fail:: return [lib.stat   (lib.store.auth) ] { ok = false        },
   555    628   			            [lib.mem.ptr(lib.store.actor)] { ptr = nil, ct = 0 }
   556    629   	end];
          630  +
          631  +	post_create = [terra(
          632  +		src: &lib.store.source,
          633  +		post: &lib.store.post
          634  +	): uint64
          635  +		var r = queries.post_create.exec(src,post.author,post.subject,post.acl,post.body) 
          636  +		if r.sz == 0 then return 0 end
          637  +		defer r:free()
          638  +		var id = r:int(uint64,0,0)
          639  +		return id
          640  +	end];
          641  +
          642  +	instance_timeline_fetch = [terra(src: &lib.store.source, rg: lib.store.range)
          643  +		var r = pqr { sz = 0 }
          644  +		if rg.mode == 0 then
          645  +			r = queries.instance_timeline_fetch.exec(src,rg.from_time,rg.to_time,0,0)
          646  +		elseif rg.mode == 1 then
          647  +			r = queries.instance_timeline_fetch.exec(src,rg.from_time,0,rg.to_idx,0)
          648  +		elseif rg.mode == 2 then
          649  +			r = queries.instance_timeline_fetch.exec(src,0,rg.to_time,0,rg.from_idx)
          650  +		elseif rg.mode == 3 then
          651  +			r = queries.instance_timeline_fetch.exec(src,0,0,rg.to_idx,rg.from_idx)
          652  +		end
          653  +		
          654  +		var ret: lib.mem.ptr(lib.mem.ptr(lib.store.post)) ret:init(r.sz)
          655  +		for i=0,r.sz do ret.ptr[i] = row_to_post(&r, i) end -- MUST FREE ALL
          656  +
          657  +		return ret
          658  +	end];
   557    659   }
   558    660   
   559    661   return b

Modified mem.t from [e41dd1991a] to [15de92d877].

    14     14   m.heapa = macro(function(ty, sz)
    15     15   	local p = m.ptr(ty:astype())
    16     16   	return `p {
    17     17   		ptr = [&ty:astype()](m.heapa_raw(sizeof(ty) * sz));
    18     18   		ct = sz;
    19     19   	}
    20     20   end)
           21  +
           22  +function m.cache(ty,sz)
           23  +	sz = sz or 32
           24  +	local struct c {
           25  +		store: ty[sz]
           26  +		top: intptr
           27  +		cur: intptr
           28  +	}
           29  +	c.name = string.format('cache<%s,%u>', tostring(ty), sz)
           30  +	terra c:insert(v: ty)
           31  +		if [ty.ptr_basetype ~= nil] then
           32  +			if self.cur < self.top then self.store[self.cur]:free() end
           33  +		end
           34  +		self.store[self.cur] = v
           35  +		self.top = lib.math.biggest(self.top, self.cur + 1)
           36  +		self.cur = (self.cur + 1) % sz
           37  +		return v
           38  +	end
           39  +	c.metamethods.__apply = terra(self: &c, idx: intptr) return &self.store[idx] end
           40  +	if ty.ptr_basetype then
           41  +		terra c:free()
           42  +			for i=0,self.top do self.store[i]:free() end
           43  +		end
           44  +	end
           45  +	return c
           46  +end
    21     47   
    22     48   local function mkptr(ty, dyn)
    23     49   	local t = terralib.types.newstruct(string.format('%s<%s>', dyn and 'ptr' or 'ref', ty))
    24     50   	t.entries = {
    25     51   		{'ptr', &ty};
    26     52   		{'ct', intptr};
    27     53   	}
    28     54   	t.ptr_basetype = ty
    29     55   	local recurse = false
    30         -	if ty:isstruct() then
    31         -		if ty.methods.free then recurse = true end
    32         -	end
           56  +	--if ty:isstruct() then
           57  +		--if ty.methods.free then recurse = true end
           58  +	--end
    33     59   	t.metamethods.__not = macro(function(self)
    34     60   		return `self.ptr
    35     61   	end)
    36     62   	if dyn then
    37     63   		t.methods = {
    38     64   			free = terra(self: &t): bool
    39     65   				[recurse and quote
................................................................................
    43     69   					m.heapf(self.ptr)
    44     70   					self.ct = 0
    45     71   					return true
    46     72   				end
    47     73   				return false
    48     74   			end;
    49     75   			init = terra(self: &t, newct: intptr): bool
           76  +				if newct == 0 then self.ct = 0 self.ptr = nil return false end
    50     77   				var nv = [&ty](m.heapa_raw(sizeof(ty) * newct))
    51     78   				if nv ~= nil then
    52     79   					self.ptr = nv
    53     80   					self.ct = newct
    54     81   					return true
    55     82   				else return false end
    56     83   			end;
................................................................................
   105    132   		{field = 'storage', type = m.ptr(ty)};
   106    133   		{field = 'sz', type = intptr};
   107    134   		{field = 'run', type = intptr};
   108    135   	}
   109    136   	local terra biggest(a: intptr, b: intptr)
   110    137   		if a > b then return a else return b end
   111    138   	end
          139  +	terra v:init(run: intptr): bool
          140  +		if not self.storage:init(run) then return false end
          141  +		self.run = run
          142  +		self.sz = 0
          143  +		return true
          144  +	end;
   112    145   	terra v:assure(n: intptr)
   113    146   		if self.storage.ct < n then
   114    147   			self.storage:resize(biggest(n, self.storage.ct + self.run))
   115    148   		end
   116    149   	end
   117         -	v.methods = {
   118         -		init = terra(self: &v, run: intptr): bool
   119         -			if not self.storage:init(run) then return false end
   120         -			self.run = run
   121         -			self.sz = 0
   122         -			return true
   123         -		end;
   124         -		new = terra(self: &v): &ty
   125         -			self:assure(self.sz + 1)
   126         -			self.sz = self.sz + 1
   127         -			return self.storage.ptr + (self.sz - 1)
   128         -		end;
   129         -		push = terra(self: &v, val: ty)
   130         -			self:assure(self.sz + 1)
   131         -			self.storage.ptr[self.sz] = val
   132         -			self.sz = self.sz + 1
   133         -		end;
   134         -		free = terra(self: &v) self.storage:free() end;
   135         -		last = terra(self: &v, idx: intptr): &ty
   136         -			if self.sz > idx then
   137         -				return self.storage.ptr + (self.sz - (idx+1))
   138         -			else lib.bail('vector underrun!') end
   139         -		end;
   140         -		crush = terra(self: &v)
   141         -			self.storage:resize(self.sz)
   142         -			return self.storage
   143         -		end;
   144         -	}
          150  +	terra v:new(): &ty
          151  +		self:assure(self.sz + 1)
          152  +		self.sz = self.sz + 1
          153  +		return self.storage.ptr + (self.sz - 1)
          154  +	end;
          155  +	terra v:push(val: ty)
          156  +		self:assure(self.sz + 1)
          157  +		self.storage.ptr[self.sz] = val
          158  +		self.sz = self.sz + 1
          159  +	end;
          160  +	terra v:free() self.storage:free() end;
          161  +	terra v:last(idx: intptr): &ty
          162  +		if self.sz > idx then
          163  +			return self.storage.ptr + (self.sz - (idx+1))
          164  +		else lib.bail('vector underrun!') end
          165  +	end;
          166  +	terra v:crush()
          167  +		self.storage:resize(self.sz)
          168  +		return self.storage
          169  +	end;
   145    170   	v.metamethods.__apply = terra(self: &v, idx: intptr): &ty -- no index??
   146    171   		if self.sz > idx then
   147    172   			return self.storage.ptr + idx
   148    173   		else lib.bail('vector overrun!') end
   149    174   	end
   150    175   	return v 
   151    176   end)
   152    177   
   153    178   return m

Modified parsav.t from [11ad9b3025] to [579976ae23].

    90     90   			if c == true then r = i else r = e end
    91     91   		in r end
    92     92   	end);
    93     93   	coalesce = macro(function(...)
    94     94   		local args = {...}
    95     95   		local ty = args[1].tree.type
    96     96   		local val = symbol(ty)
    97         -		local empty if ty.type == 'integer'
    98         -			then empty = `0
    99         -			else empty = `nil
   100         -		end
           97  +		local empty
           98  +		if ty.ptr_basetype then empty = `[ty]{ptr=nil,ct=0}
           99  +		elseif ty.type == 'integer' then empty = `0
          100  +		else empty = `nil end
   101    101   		local exp = quote val = [empty] end
   102    102   
   103    103   		for i=#args, 1, -1 do
   104    104   			local v = args[i]
   105         -			exp = quote
   106         -				if [v] ~= [empty]
   107         -					then val = v
   108         -					else [exp]
          105  +			if ty.ptr_basetype then
          106  +				exp = quote
          107  +					if [v].ptr ~= nil
          108  +						then val = v
          109  +						else [exp]
          110  +					end
          111  +				end
          112  +			else
          113  +				exp = quote
          114  +					if [v] ~= [empty]
          115  +						then val = v
          116  +						else [exp]
          117  +					end
   109    118   				end
   110    119   			end
   111    120   		end
   112    121   
   113    122   		local q = quote
   114    123   			var [val]
   115    124   			[exp]
................................................................................
   285    294   lib.b64 = lib.loadlib('mbedtls','mbedtls/base64.h')
   286    295   lib.net = lib.loadlib('mongoose','mongoose.h')
   287    296   lib.pq = lib.loadlib('libpq','libpq-fe.h')
   288    297   
   289    298   lib.load {
   290    299   	'mem',  'math', 'str', 'file', 'crypt';
   291    300   	'http', 'session', 'tpl', 'store';
          301  +
          302  +	'smackdown'; -- md-alike parser
   292    303   }
   293    304   
   294    305   local be = {}
   295    306   for _, b in pairs(config.backends) do
   296    307   	be[#be+1] = terralib.loadfile('backend/' .. b .. '.t')()
   297    308   end
   298    309   lib.store.backends = global(`array([be]))
................................................................................
   324    335   lib.load {
   325    336   	'srv';
   326    337   	'render:nav';
   327    338   	'render:login';
   328    339   	'render:profile';
   329    340   	'render:userpage';
   330    341   	'render:compose';
          342  +	'render:tweet';
          343  +	'render:timeline';
   331    344   	'route';
   332    345   }
   333    346   
   334    347   do
   335    348   	local p = string.format('parsav: %s\nbuilt on %s\n', config.build.str, config.build.when)
   336    349   	terra version() lib.io.send(1, p, [#p]) end
   337    350   end
................................................................................
   444    457   if bflag('dump-config','C') then
   445    458   	print(util.dump(config))
   446    459   	os.exit(0)
   447    460   end
   448    461   
   449    462   local holler = print
   450    463   local out = config.exe and 'parsav' or ('parsav.' .. config.outform)
   451         -local linkargs = {'-O4'}
          464  +local linkargs = {}
   452    465   
   453    466   if bflag('quiet','q') then holler = function() end end
   454    467   if bflag('asan','s') then linkargs[#linkargs+1] = '-fsanitize=address' end
   455    468   if bflag('lsan','S') then linkargs[#linkargs+1] = '-fsanitize=leak' end
   456    469   
   457    470   if config.posix then
   458    471   	linkargs[#linkargs+1] = '-pthread'

Modified render/compose.t from [7a7e8f43ac] to [0685338a7e].

     9      9   			acl = lib.trn(target == nil, 'all', 'mentioned'); -- TODO default acl setting?
    10     10   			handle = co.who.handle;
    11     11   		}
    12     12   	end
    13     13   	var cotxt = form:tostr() defer cotxt:free()
    14     14   
    15     15   	var doc = data.view.docskel {
    16         -		instance = co.srv.cfg.instance.ptr;
    17         -		title = 'compose';
    18         -		body = cotxt.ptr;
    19         -		class = 'compose';
    20         -		navlinks = co.navbar.ptr;
           16  +		instance = co.srv.cfg.instance;
           17  +		title = lib.str.plit 'compose';
           18  +		body = cotxt;
           19  +		class = lib.str.plit 'compose';
           20  +		navlinks = co.navbar;
    21     21   	}
    22     22   
    23     23   	var hdrs = array(
    24     24   		lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' }
    25     25   	)
    26     26   	doc:send(co.con,200,[lib.mem.ptr(lib.http.header)] {ct = 1, ptr = &hdrs[0]})
    27     27   end
    28     28   
    29     29   return render_compose

Modified render/login.t from [0d69ec17a3] to [671b92715b].

     1      1   -- vim: ft=terra
            2  +local pstr = lib.mem.ptr(int8)
            3  +local P = lib.str.plit
     2      4   local terra 
     3         -login_form(co: &lib.srv.convo, user: &lib.store.actor, creds: &lib.store.credset, msg: &int8)
            5  +login_form(co: &lib.srv.convo, user: &lib.store.actor, creds: &lib.store.credset, msg: pstr)
     4      6   	var doc = data.view.docskel {
     5         -		instance = co.srv.cfg.instance.ptr;
     6         -		title = 'instance logon';
     7         -		class = 'login';
     8         -		navlinks = co.navbar.ptr;
            7  +		instance = co.srv.cfg.instance;
            8  +		title = lib.str.plit 'instance logon';
            9  +		class = lib.str.plit 'login';
           10  +		navlinks = co.navbar;
     9     11   	}
    10     12   
    11     13   	if user == nil then
    12     14   		var form = data.view.login_username {
    13     15   			loginmsg = msg;
    14     16   		}
    15         -		if form.loginmsg == nil then
    16         -			form.loginmsg = 'identify yourself for access to this instance.'
           17  +		if form.loginmsg.ptr == nil then
           18  +			form.loginmsg = lib.str.plit 'identify yourself for access to this instance.'
    17     19   		end
    18         -		var formtxt = form:tostr()
    19         -		doc.body = formtxt.ptr
           20  +		doc.body = form:tostr()
    20     21   	elseif creds:sz() == 0 then
    21     22   		co:complain(403,'access denied','your host is not eligible to authenticate as this user')
    22     23   		return
    23     24   	elseif creds:sz() == 1 then
    24     25   		if creds.trust() then
    25     26   			-- TODO log in immediately
    26     27   			return
................................................................................
    27     28   		end
    28     29   
    29     30   		var ch = data.view.login_challenge {
    30     31   			handle = user.handle;
    31     32   			name = lib.coalesce(user.nym, user.handle);
    32     33   		}
    33     34   		if creds.pw() then
    34         -			ch.challenge = 'enter the password associated with your account'
    35         -			ch.label = 'password'
    36         -			ch.method = 'pw'
           35  +			ch.challenge = P'enter the password associated with your account'
           36  +			ch.label = P'password'
           37  +			ch.method = P'pw'
    37     38   		elseif creds.otp() then
    38         -			ch.challenge = 'enter a valid one-time password for your account'
    39         -			ch.label = 'OTP code'
    40         -			ch.method = 'otp'
           39  +			ch.challenge = P'enter a valid one-time password for your account'
           40  +			ch.label = P'OTP code'
           41  +			ch.method = P'otp'
    41     42   		elseif creds.challenge() then
    42         -			ch.challenge = 'sign the challenge token: <code>...</code>'
    43         -			ch.label = 'digest'
    44         -			ch.method = 'challenge'
           43  +			ch.challenge = P'sign the challenge token: <code>...</code>'
           44  +			ch.label = P'digest'
           45  +			ch.method = P'challenge'
    45     46   		else
    46     47   			co:complain(500,'login failure','unknown login method')
    47     48   			return
    48     49   		end
    49     50   
    50         -		doc.body = ch:tostr().ptr
           51  +		doc.body = ch:tostr()
    51     52   	else
    52     53   		-- pick a method
    53     54   	end
    54     55   
    55     56   	var hdrs = array(
    56     57   		lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' }
    57     58   	)
    58     59   	doc:send(co.con,200,[lib.mem.ptr(lib.http.header)] {ct = 1, ptr = &hdrs[0]})
    59         -	lib.mem.heapf(doc.body)
           60  +	doc.body:free()
    60     61   end
    61     62   
    62     63   return login_form

Modified render/nav.t from [2d4aa38bec] to [450b73f673].

     3      3   render_nav(co: &lib.srv.convo)
     4      4   	var t: lib.str.acc t:init(64)
     5      5   	if co.who ~= nil or co.srv.cfg.pol_sec == lib.srv.secmode.public then
     6      6   		t:lpush('<a href="/">timeline</a>')
     7      7   	end
     8      8   	if co.who ~= nil then
     9      9   		t:lpush('<a href="/compose">compose</a> <a href="/'):push(co.who.xid,0)
    10         -		t:lpush('">profile</a> <a href="/conf">configure</a> <a href="/logout">log out</a>')
           10  +		t:lpush('">profile</a> <a href="/conf">configure</a> <a href="/help">help</a> <a href="/logout">log out</a>')
    11     11   	else
    12         -		t:lpush('<a href="/login">log in</a>')
           12  +		t:lpush('<a href="/help">help</a> <a href="/login">log in</a>')
    13     13   	end
    14     14   	return t:finalize()
    15     15   end
    16     16   return render_nav

Modified render/profile.t from [ecfc7ba460] to [30c7a0b6c6].

     1      1   -- vim: ft=terra
            2  +local pstr = lib.mem.ptr(int8)
            3  +local terra cs(s: rawstring)
            4  +	return pstr { ptr = s, ct = lib.str.sz(s) }
            5  +end
            6  +
     2      7   local terra 
     3      8   render_profile(co: &lib.srv.convo, actor: &lib.store.actor)
     4      9   	var aux: lib.str.acc
     5         -	var auxp: rawstring
           10  +	var auxp: pstr
     6     11   	if co.aid ~= 0 and co.who.id == actor.id then
     7         -		auxp = '<a href="/conf/profile">alter</a>'
           12  +		auxp = lib.str.plit '<a href="/conf/profile">alter</a>'
     8     13   	elseif co.aid ~= 0 then
     9     14   		aux:compose('<a href="/', actor.xid, '/follow">follow</a><a href="/',
    10     15   			actor.xid, '/chat">chat</a>')
    11     16   		if co.who.rights.powers:affect_users() then
    12     17   			aux:push('<a href="/',11):push(actor.xid,0):push('/ctl">control</a>',17)
    13     18   		end
    14         -		auxp = aux.buf
           19  +		auxp = aux:finalize()
    15     20   	else
    16     21   		aux:compose('<a href="/', actor.xid, '/follow">remote follow</a>')
    17     22   	end
    18     23   	var avistr: lib.str.acc if actor.origin == 0 then
    19     24   		avistr:compose('/avi/',actor.handle)
    20     25   	end
    21     26   	var timestr: int8[26] lib.osclock.ctime_r(&actor.knownsince, &timestr[0])
    22     27   
    23     28   	var strfbuf: int8[28*4]
    24     29   	var stats = co.srv:actor_stats(actor.id)
    25         -		var sn_posts = lib.math.decstr_friendly(stats.posts, &strfbuf[ [strfbuf.type.N - 1] ])
    26         -		var sn_follows = lib.math.decstr_friendly(stats.follows, sn_posts - 1)
    27         -		var sn_followers = lib.math.decstr_friendly(stats.followers, sn_follows - 1)
    28         -		var sn_mutuals = lib.math.decstr_friendly(stats.mutuals, sn_followers - 1)
           30  +		var sn_posts     = cs(lib.math.decstr_friendly(stats.posts, &strfbuf[ [strfbuf.type.N - 1] ]))
           31  +		var sn_follows   = cs(lib.math.decstr_friendly(stats.follows, sn_posts.ptr - 1))
           32  +		var sn_followers = cs(lib.math.decstr_friendly(stats.followers, sn_follows.ptr - 1))
           33  +		var sn_mutuals   = cs(lib.math.decstr_friendly(stats.mutuals, sn_followers.ptr - 1))
    29     34   	
    30     35   	var profile = data.view.profile {
    31         -		nym = lib.coalesce(actor.nym, actor.handle);
    32         -		bio = lib.coalesce(actor.bio, "<em>tall, dark, and mysterious</em>");
    33         -		xid = actor.xid;
    34         -		avatar = lib.trn(actor.origin == 0, avistr.buf,
    35         -			lib.coalesce(actor.avatar, '/s/default-avatar.webp'));
           36  +		nym = cs(lib.coalesce(actor.nym, actor.handle));
           37  +		bio = cs(lib.coalesce(actor.bio, "<em>tall, dark, and mysterious</em>"));
           38  +		xid = cs(actor.xid);
           39  +		avatar = lib.trn(actor.origin == 0, pstr{ptr=avistr.buf,ct=avistr.sz},
           40  +			cs(lib.coalesce(actor.avatar, '/s/default-avatar.webp')));
    36     41   
    37     42   		nposts = sn_posts, nfollows = sn_follows;
    38     43   		nfollowers = sn_followers, nmutuals = sn_mutuals;
    39         -		tweetday = timestr;
    40         -		timephrase = lib.trn(actor.origin == 0, 'joined', 'known since');
           44  +		tweetday = cs(timestr);
           45  +		timephrase = lib.trn(actor.origin == 0, lib.str.plit'joined', lib.str.plit'known since');
    41     46   
    42     47   		auxbtn = auxp;
    43     48   	}
    44     49   
    45     50   	var ret = profile:tostr()
    46     51   	if actor.origin == 0 then avistr:free() end
    47         -	if not (co.aid ~= 0 and co.who.id == actor.id) then aux:free() end
           52  +	if not (co.aid ~= 0 and co.who.id == actor.id) then auxp:free() end
    48     53   	return ret
    49     54   end
    50     55   
    51     56   return render_profile

Added render/timeline.t version [e2a4558814].

            1  +-- vim: ft=terra
            2  +local modes = lib.enum {'follow','mutual','srvlocal','fediglobal','circle'}
            3  +local terra 
            4  +render_timeline(co: &lib.srv.convo, modestr: lib.mem.ref(int8))
            5  +	var mode = modes.srvlocal
            6  +	var circle: uint64 = 0
            7  +	-- if     modestr:cmpl('local') then mode = modes.srvlocal
            8  +	-- elseif modestr:cmpl('mutual') then mode = modes.mutual
            9  +	-- elseif modestr:cmpl('global') then mode = modes.fediglobal
           10  +	-- elseif modestr:cmpl('circle') then mode = modes.circle
           11  +	-- end
           12  +
           13  +	var stoptime = lib.osclock.time(nil)
           14  +
           15  +	var posts = [lib.mem.vec(lib.mem.ptr(lib.store.post))] { 
           16  +		sz = 0, run = 0
           17  +	}
           18  +	if mode == modes.follow then
           19  +	elseif mode == modes.srvlocal then
           20  +		posts = co.srv:instance_timeline_fetch(lib.store.range {
           21  +			mode = 1; -- T->I
           22  +			from_time = stoptime;
           23  +			to_idx = 64;
           24  +		})
           25  +	elseif mode == modes.fediglobal then
           26  +	elseif mode == modes.circle then
           27  +	end
           28  +
           29  +	var acc: lib.str.acc acc:init(1024)
           30  +	for i = 0, posts.sz do
           31  +		lib.render.tweet(co, posts(i).ptr, &acc)
           32  +		posts(i):free()
           33  +	end
           34  +	posts:free()
           35  +
           36  +	var doc = data.view.docskel {
           37  +		instance = co.srv.cfg.instance;
           38  +		title = lib.str.plit'timeline';
           39  +		body = acc:finalize();
           40  +		class = lib.str.plit'timeline';
           41  +		navlinks = co.navbar;
           42  +	}
           43  +	var hdrs = array(
           44  +		lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' }
           45  +	)
           46  +	doc:send(co.con,200,[lib.mem.ptr(lib.http.header)] {ct = 1, ptr = &hdrs[0]})
           47  +	doc.body:free()
           48  +end
           49  +return render_timeline

Added render/tweet.t version [71eb9101d9].

            1  +-- vim: ft=terra
            2  +local pstr = lib.mem.ptr(int8)
            3  +local terra cs(s: rawstring)
            4  +	return pstr { ptr = s, ct = lib.str.sz(s) }
            5  +end
            6  +
            7  +local terra 
            8  +render_tweet(co: &lib.srv.convo, p: &lib.store.post, acc: &lib.str.acc)
            9  +	var author: &lib.store.actor
           10  +	for j = 0, co.actorcache.top do
           11  +		lib.io.fmt('scanning cache for author %llu (%llu/%llu)\n', p.author, j, co.actorcache.top)
           12  +		if p.author == co.actorcache(j).ptr.id then
           13  +			author = co.actorcache(j).ptr
           14  +			lib.io.fmt('cache hit on idx %llu, skipping db lookup\n', j) 
           15  +			goto foundauth
           16  +		end
           17  +	end
           18  +	lib.io.fmt('cache miss, checking db for id %llu\n', p.author) 
           19  +	author = co.actorcache:insert(co.srv:actor_fetch_uid(p.author)).ptr
           20  +	lib.io.fmt('got author %s\n', author.handle) 
           21  +
           22  +	::foundauth::
           23  +	var avistr: lib.str.acc if author.origin == 0 then
           24  +		avistr:compose('/avi/',author.handle)
           25  +	end
           26  +	var timestr: int8[26] lib.osclock.ctime_r(&p.posted, &timestr[0])
           27  +	lib.io.fmt('got body %s\n', author.handle) 
           28  +
           29  +	var bhtml = lib.smackdown.html([lib.mem.ptr(int8)] {ptr=p.body,ct=0}) defer bhtml:free()
           30  +
           31  +	var idbuf: int8[lib.math.shorthand.maxlen]
           32  +	var idlen = lib.math.shorthand.gen(p.id, idbuf)
           33  +	var permalink: lib.str.acc permalink:compose('/post/',{idbuf,idlen})
           34  +
           35  +	var tpl = data.view.tweet {
           36  +		text = bhtml;
           37  +		subject = cs(lib.coalesce(p.subject,''));
           38  +		nym = cs(lib.coalesce(author.nym, author.handle));
           39  +		xid = cs(author.xid);
           40  +		when = cs(&timestr[0]);
           41  +		avatar = cs(lib.trn(author.origin == 0, avistr.buf,
           42  +			lib.coalesce(author.avatar, '/s/default-avatar.webp')));
           43  +		acctlink = cs(author.xid);
           44  +		permalink = permalink:finalize();
           45  +	}
           46  +	defer tpl.permalink:free()
           47  +	if acc ~= nil then tpl:append(acc) return [lib.mem.ptr(int8)]{ptr=nil,ct=0} end
           48  +	var txt = tpl:tostr()
           49  +	return txt
           50  +end
           51  +return render_tweet

Modified render/userpage.t from [cdf1e65d22] to [7b79f9904c].

     1      1   -- vim: ft=terra
     2      2   local terra 
     3      3   render_userpage(co: &lib.srv.convo, actor: &lib.store.actor)
     4         -	var ti: lib.str.acc defer ti:free()
            4  +	var ti: lib.str.acc 
     5      5   	if co.aid ~= 0 and co.who.id == actor.id then
     6      6   		ti:compose('my profile')
     7      7   	else
     8      8   		ti:compose('profile :: ', actor.handle)
     9      9   	end
    10     10   	var pftxt = lib.render.profile(co,actor) defer pftxt:free()
    11     11   
    12     12   	var doc = data.view.docskel {
    13         -		instance = co.srv.cfg.instance.ptr;
    14         -		title = ti.buf;
    15         -		body = pftxt.ptr;
    16         -		class = 'profile';
    17         -		navlinks = co.navbar.ptr;
           13  +		instance = co.srv.cfg.instance;
           14  +		title = ti:finalize();
           15  +		body = pftxt;
           16  +		class = lib.str.plit 'profile';
           17  +		navlinks = co.navbar;
    18     18   	}
    19     19   
    20     20   	var hdrs = array(
    21     21   		lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' }
    22     22   	)
    23     23   	doc:send(co.con,200,[lib.mem.ptr(lib.http.header)] {ct = 1, ptr = &hdrs[0]})
           24  +	doc.title:free()
    24     25   end
    25     26   
    26     27   return render_userpage

Modified route.t from [d7d680b0a3] to [4d69f275c0].

     1      1   -- vim: ft=terra
     2      2   local r = lib.srv.route
     3      3   local method = lib.http.method
            4  +local pstring = lib.mem.ptr(int8)
            5  +local rstring = lib.mem.ref(int8)
            6  +local hpath = lib.mem.ptr(rstring)
     4      7   local http = {}
     5      8   
     6      9   terra http.actor_profile_xid(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t)
     7     10   	var handle = [lib.mem.ptr(int8)] { ptr = &uri.ptr[2], ct = 0 }
     8     11   	for i=2,uri.ct do
     9     12   		if uri.ptr[i] == @'/' or uri.ptr[i] == 0 then handle.ct = i - 2 break end
    10     13   	end
................................................................................
    57     60   
    58     61   	lib.render.userpage(co, actor.ptr)
    59     62   end
    60     63   
    61     64   terra http.login_form(co: &lib.srv.convo, meth: method.t)
    62     65   	if meth == method.get then
    63     66   		-- request a username
    64         -		lib.render.login(co, nil, nil, nil)
           67  +		lib.render.login(co, nil, nil, lib.str.plit(nil))
    65     68   	elseif meth == method.post then
    66     69   		var usn, usnl = co:postv('user')
    67         -		lib.dbg('got name ',{usn,usnl})
    68         -		lib.io.fmt('name len %llu\n',usnl)
    69     70   		var am, aml = co:postv('authmethod')
    70     71   		var chrs, chrsl = co:postv('response')
    71     72   		var cs, authok = co.srv:actor_auth_how(co.peer, usn)
    72     73   		var act = co.srv:actor_fetch_xid([lib.mem.ptr(int8)] {
    73     74   			ptr = usn, ct = usnl
    74     75   		})
    75     76   		if authok == false then
    76         -			lib.render.login(co, nil, nil, 'access denied')
           77  +			lib.render.login(co, nil, nil, lib.str.plit'access denied')
    77     78   			return
    78     79   		end
    79     80   		var fakeact = false
    80     81   		var fakeactor: lib.store.actor
    81     82   		if act.ptr == nil then
    82     83   			-- the user is known to us but has not yet claimed an
    83     84   			-- account on the server. create a template for the
................................................................................
    90     91   			}
    91     92   			act.ct = 1
    92     93   			act.ptr = &fakeactor
    93     94   			act.ptr.rights = lib.store.rights_default()
    94     95   		end
    95     96   		if am == nil then
    96     97   			-- pick an auth method
    97         -			lib.render.login(co, act.ptr, &cs, nil)
           98  +			lib.render.login(co, act.ptr, &cs, lib.str.plit(nil))
    98     99   		else var aid: uint64 = 0
    99    100   			lib.dbg('authentication attempt beginning')
   100    101   			-- attempt login with provided method
   101    102   			if lib.str.ncmp('pw', am, lib.math.biggest(2,aml)) == 0 and chrs ~= nil then
   102    103   				aid = co.srv:actor_auth_pw(co.peer,
   103    104   					[lib.mem.ptr(int8)]{ptr=usn,ct=usnl},
   104    105   					[lib.mem.ptr(int8)]{ptr=chrs,ct=chrsl})
................................................................................
   105    106   			elseif lib.str.ncmp('otp', am, lib.math.biggest(2,aml)) == 0 and chrs ~= nil then
   106    107   				lib.dbg('using otp auth')
   107    108   				-- ··· --
   108    109   			else
   109    110   				lib.dbg('invalid auth method')
   110    111   			end
   111    112   
   112         -			lib.io.fmt('login got aid = %llu\n', aid)
   113    113   			-- error out
   114    114   			if aid == 0 then
   115         -				lib.render.login(co, nil, nil, 'authentication failure')
          115  +				lib.render.login(co, nil, nil, lib.str.plit 'authentication failure')
   116    116   			else
   117    117   				var sesskey: int8[lib.session.maxlen + #lib.session.cookiename + #"=; Path=/" + 1]
   118    118   				do var p = &sesskey[0]
   119    119   					p = lib.str.ncpy(p, [lib.session.cookiename .. '='], [#lib.session.cookiename + 1])
   120    120   					p = p + lib.session.cookie_gen(co.srv.cfg.secret, aid, lib.osclock.time(nil), p)
   121    121   					lib.dbg('sending cookie',&sesskey[0])
   122    122   					p = lib.str.ncpy(p, '; Path=/', 9)
................................................................................
   134    134   terra http.post_compose(co: &lib.srv.convo, meth: method.t)
   135    135   	if meth == method.get then
   136    136   		lib.render.compose(co, nil)
   137    137   	elseif meth == method.post then
   138    138   		if co.who.rights.powers.post() == false then
   139    139   			co:complain(401,'insufficient privileges','you lack the <strong>post</strong> power and cannot perform this action') return
   140    140   		end
          141  +		var text, textlen = co:postv("post")
          142  +		var acl, acllen = co:postv("acl")
          143  +		var subj, subjlen = co:postv("subject")
          144  +		if text == nil or acl == nil then
          145  +			co:complain(405, 'invalid post', 'every post must have at least body text and an ACL')
          146  +			return
          147  +		end
          148  +		if subj == nil then subj = '' end
          149  +
          150  +		var p = lib.store.post {
          151  +			author = co.who.id, acl = acl;
          152  +			body = text, subject = subj;
          153  +		}
          154  +		var newid = co.srv:post_create(&p)
   141    155   
          156  +		var idbuf: int8[lib.math.shorthand.maxlen]
          157  +		var idlen = lib.math.shorthand.gen(newid, idbuf)
          158  +		var redirto: lib.str.acc redirto:compose('/post/',{idbuf,idlen}) defer redirto:free()
          159  +		co:reroute(redirto.buf)
   142    160   	end
   143    161   end
          162  +
          163  +terra http.timeline(co: &lib.srv.convo, mode: hpath)
          164  +	lib.render.timeline(co,lib.trn(mode.ptr == nil, rstring{ptr=nil}, mode.ptr[1]))
          165  +	return
          166  +end
   144    167   
   145    168   do local branches = quote end
   146    169   	local filename, flen = symbol(&int8), symbol(intptr)
   147    170   	local page = symbol(lib.http.page)
   148    171   	local send = label()
   149    172   	local storage = data.stmap
   150    173   	for i,e in ipairs(config.embeds) do local id,mime = e[1],e[2]
................................................................................
   180    203   	co:reroute('/s/default-avatar.webp')
   181    204   end
   182    205   
   183    206   -- entry points
   184    207   terra r.dispatch_http(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t)
   185    208   	lib.dbg('handling URI of form ', {uri.ptr,uri.ct})
   186    209   	co.navbar = lib.render.nav(co)
          210  +	lib.dbg('got nav ', {co.navbar.ptr,co.navbar.ct}, "||", co.navbar.ptr)
   187    211   	-- some routes are non-hierarchical, and can be resolved with a simple strcmp
   188    212   	-- we run through those first before giving up and parsing the URI
   189    213   	if uri.ptr[0] ~= @'/' then
   190    214   		co:complain(404, 'what the hell', 'how did you do that')
   191    215   		return
   192    216   	elseif uri.ct == 1 then -- root
   193         -		lib.io.fmt('root directory, aid is %llu\n', co.aid)
   194    217   		if (co.srv.cfg.pol_sec == lib.srv.secmode.private or
   195    218   		   co.srv.cfg.pol_sec == lib.srv.secmode.lockdown) and co.aid == 0 then
   196    219   		   http.login_form(co, meth)
   197    220   		else
   198    221   			-- FIXME display home screen
          222  +			http.timeline(co, hpath {ptr=nil})
   199    223   			goto notfound
   200    224   		end
   201    225   		return
   202    226   	elseif uri.ptr[1] == @'@' then
   203    227   		http.actor_profile_xid(co, uri, meth)
   204    228   		return
   205    229   	elseif uri.ptr[1] == @'s' and uri.ptr[2] == @'/' and uri.ct > 3 then
................................................................................
   225    249   			else co:reroute_cookie('/','auth=; Path=/')
   226    250   		end
   227    251   		return
   228    252   	else -- hierarchical routes
   229    253   		var path = lib.http.hier(uri) defer path:free()
   230    254   		if path.ptr[0]:cmp(lib.str.lit('user')) then
   231    255   			http.actor_profile_uid(co, path, meth)
          256  +		elseif path.ptr[0]:cmp(lib.str.lit('tl')) then
          257  +			http.timeline(co, path)
   232    258   		else goto notfound end
   233    259   		return
   234    260   	end
   235    261   
   236    262   	::wrongmeth:: co:complain(405, 'method not allowed', 'that method is not meaningful for this endpoint') do return end
   237    263   	::notfound:: co:complain(404, 'not found', 'no such resource available') do return end
   238    264   end

Modified schema.sql from [a3359b8b76] to [6bf306c986].

    31     31   -- note that valid ids should always > 0, as 0 is reserved for null
    32     32   -- on the client side, vastly simplifying code
    33     33   drop table if exists parsav_servers cascade;
    34     34   create table parsav_servers (
    35     35   	id     bigint primary key default (1+random()*(2^63-1))::bigint,
    36     36   	domain text not null,
    37     37   	key    bytea,
           38  +	knownsince timestamp,
    38     39   	parsav boolean -- whether to use parsav protocol extensions
    39     40   );
    40     41   
    41     42   drop table if exists parsav_actors cascade;
    42     43   create table parsav_actors (
    43     44   	id        bigint primary key default (1+random()*(2^63-1))::bigint,
    44     45   	nym       text,
    45     46   	handle    text not null, -- nym [@handle@origin] 
    46     47   	origin    bigint references parsav_servers(id)
    47     48   		on delete cascade, -- null origin = local actor
           49  +	knownsince timestamp,
    48     50   	bio       text,
    49     51   	avataruri text, -- null if local
    50     52   	rank      smallint not null default 0,
    51     53   	quota     integer not null default 1000,
    52     54   	key       bytea, -- private if localactor; public if remote
    53         -	title     text
           55  +	title     text,
    54     56   	
    55     57   	unique (handle,origin)
    56     58   );
    57     59   
    58     60   drop table if exists parsav_rights cascade;
    59     61   create table parsav_rights (
    60     62   	key text,
................................................................................
    81     83   	author     bigint references parsav_actors(id)
    82     84   		on delete cascade,
    83     85   	subject    text,
    84     86   	acl        text not null default 'all', -- just store the script raw 🤷
    85     87   	body       text,
    86     88   	posted     timestamp not null,
    87     89   	discovered timestamp not null,
    88         -	scope      smallint not null,
    89         -	convo      bigint,
    90         -	parent     bigint,
    91         -	circles    bigint[],
    92         -	mentions   bigint[]
           90  +	parent     bigint not null default 0,
           91  +	circles    bigint[], -- TODO at edit or creation, iterate through each circle
           92  +	mentions   bigint[], -- a user has, check if it can see her post, and if so add
           93  +
           94  +	convoheaduri text
           95  +	-- only used for tracking foreign conversations and tying them to post heads;
           96  +	-- local conversations are tracked directly and mapped to URIs based on the
           97  +	-- head's ID. null if native tweet or not the first tweet in convo
    93     98   );
    94     99   
    95    100   drop table if exists parsav_conversations cascade;
    96         -create table parsav_conversations (
    97         -	id         bigint primary key default (1+random()*(2^63-1))::bigint,
    98         -	uri        text      not null,
    99         -	discovered timestamp not null,
   100         -	head       bigint references parsav_posts(id)
   101         -);
   102    101   
   103    102   drop table if exists parsav_rels cascade;
   104    103   create table parsav_rels (
   105    104   	relator bigint references parsav_actors(id)
   106    105   		on delete cascade, -- e.g. follower
   107    106   	relatee bigint references parsav_actors(id)
   108    107   		on delete cascade, -- e.g. followed
................................................................................
   142    141   );
   143    142   
   144    143   drop table if exists parsav_circles cascade;
   145    144   create table parsav_circles (
   146    145   	id          bigint primary key default (1+random()*(2^63-1))::bigint,
   147    146   	owner       bigint not null references parsav_actors(id),
   148    147   	name        text not null,
   149         -	members     bigint[] not null default array[],
          148  +	members     bigint[] not null default array[]::bigint[],
   150    149   
   151    150   	unique (owner,name)
   152    151   );
   153    152   
   154    153   drop table if exists parsav_rooms cascade;
   155    154   create table parsav_rooms (
   156    155   	id          bigint primary key default (1+random()*(2^63-1))::bigint,
................................................................................
   162    161   
   163    162   drop table if exists parsav_room_members cascade;
   164    163   create table parsav_room_members (
   165    164   	room   bigint references parsav_rooms(id),
   166    165   	member bigint references parsav_actors(id),
   167    166   	rank   smallint not null default 0,
   168    167   	admin  boolean not null default false, -- non-admins with rank can only moderate + invite
   169         -	title  text -- admin-granted title like reddit flair
          168  +	title  text, -- admin-granted title like reddit flair
          169  +	vouchedby bigint references parsav_actors(id)
   170    170   );
   171    171   
   172    172   drop table if exists parsav_invites cascade;
   173    173   create table parsav_invites (
   174    174   	id          bigint primary key default (1+random()*(2^63-1))::bigint,
   175    175   	-- when a user is created from an invite, the invite is deleted and the invite
   176    176   	-- ID becomes the user ID. privileges granted on the invite ID during the invite
   177    177   	-- process are thus inherited by the user
          178  +	issuer bigint references parsav_actors(id),
   178    179   	handle text, -- admin can lock invite to specific handle
   179    180   	rank   smallint not null default 0,
   180    181   	quota  integer not null  default 1000
   181         -};
          182  +);
   182    183   
   183    184   drop table if exists parsav_interventions cascade;
   184    185   create table parsav_interventions (
   185    186   	id     bigint primary key default (1+random()*(2^63-1))::bigint,
   186    187   	issuer bigint references parsav_actors(id) not null,
   187    188   	scope  bigint, -- can be null or room for local actions
   188    189   	nature smallint not null, -- silence, suspend, disemvowel, etc
   189    190   	victim bigint not null, -- could potentially target group as well
   190    191   	expire timestamp -- auto-expires if set
   191    192   );
   192    193   
   193    194   end;

Added smackdown.t version [896438e021].

            1  +-- vim: ft=terra
            2  +-- smackdown is parsav's terrible, terrible custom markdown-alike parser
            3  +
            4  +local m = {}
            5  +local pstr = lib.mem.ptr(int8)
            6  +
            7  +local segt = { 
            8  +	none = 0, para = 1, head = 2, listing = 3
            9  +}
           10  +
           11  +local autolink_protos = {
           12  +	'https', 'http', 'ftp', 'gopher', 'gemini', 'ircs', 'irc';
           13  +	'mailto', 'about', 'sshfs', 'afp', 'smb', 'data', 'file';
           14  +	'dav', 'git', 'svn', 'cvs', 'dns', 'finger', 'pop', 'imap';
           15  +	'pops', 'imaps', 'torrent', 'magnet', 'news', 'snews', 'nfs';
           16  +	'nntp', 'sms', 'tel', 'telnet', 'vnc', 'webcal', 'ws', 'wss';
           17  +	'xmpp';
           18  +}
           19  +
           20  +local struct state {
           21  +	segt: uint
           22  +	bqlvl: uint
           23  +	curpos: rawstring
           24  +	blockstart: rawstring
           25  +}
           26  +
           27  +terra state:segend(ofs: uint)
           28  +-- takes a string offset and returns true if it indexes th
           29  +-- end of the current block
           30  +	var s = self.curpos + ofs
           31  +	if s[0] ~= @'\n' then return false end
           32  +	if self.segt == segt.head then return true end -- headers can only be 1 line
           33  +-- 	if s[1] == '#'
           34  +
           35  +end
           36  +
           37  +local terra isws(c: int8)
           38  +	return c == @' ' or c == @'\n' or c == @'\t' or c == @'\r'
           39  +end
           40  +
           41  +local terra scanline(l: rawstring, max: intptr, n: rawstring, nc: intptr)
           42  +	if l == nil then return nil end
           43  +	for i=0,max do
           44  +		for j=0,nc do
           45  +			if l[i+j] == @'\n' then return nil end
           46  +			if l[i+j] ~= n[j] then goto nexti end
           47  +		end
           48  +		do return l+i end
           49  +	::nexti::end
           50  +end
           51  +
           52  +local terra scanline_wordend(l: rawstring, max: intptr, n: rawstring, nc: intptr)
           53  +	var sl = scanline(l,max,n,nc)
           54  +	if sl == nil then return nil else sl = sl + nc end
           55  +	if sl >= l+max or isws(@sl) then return sl-nc end
           56  +	return nil
           57  +end
           58  +
           59  +terra m.html(md: pstr)
           60  +	if md.ct == 0 then md.ct = lib.str.sz(md.ptr) end
           61  +	var styled: lib.str.acc styled:init(md.ct)
           62  +
           63  +	do var i = 0 while i < md.ct do
           64  +		var wordstart = (i == 0 or isws(md.ptr[i-1]))
           65  +		var wordend = (i == md.ct - 1 or isws(md.ptr[i+1]))
           66  +
           67  +		var here = md.ptr + i
           68  +		var rem = md.ct - i
           69  +		if @here == @'[' then
           70  +			var sep = scanline(here,rem, '](', 2)
           71  +			var term = scanline(sep+2,rem - ((sep+2)-here), ')', 1)
           72  +			if sep ~= nil and term ~= nil then
           73  +				styled:lpush('<a href="')
           74  +					:push(sep+2, term-(sep+2))
           75  +					:lpush('" rel="nofollow">')
           76  +					:push(here+1,(sep-here) - 1)
           77  +					:lpush('</a>')
           78  +				i = (term - md.ptr) + 1
           79  +				goto skip
           80  +			else goto fallback end
           81  +		end
           82  +
           83  +		if wordstart and rem >= 7 and lib.str.ncmp('***',here,3)==0 then
           84  +			var term = scanline_wordend(here+4,rem-4,'***',3)
           85  +			if term ~= nil then
           86  +				styled:lpush('<strong><em>')
           87  +					:push(here+3, (term-here) - 3)
           88  +					:lpush('</strong></em>')
           89  +				i = (term - md.ptr) + 3
           90  +				goto skip
           91  +			end
           92  +		end
           93  +
           94  +		if wordstart and rem >= 5 and lib.str.ncmp('**',here,2)==0 then
           95  +			var term = scanline_wordend(here+3,rem-3,'**',2)
           96  +			if term ~= nil then
           97  +				styled:lpush('<strong>')
           98  +					:push(here+2, (term-here) - 2)
           99  +					:lpush('</strong>')
          100  +				i = (term - md.ptr) + 2
          101  +				goto skip
          102  +			end
          103  +		end
          104  +
          105  +		if wordstart and rem >= 3 and @here == @'*' then
          106  +			var term = scanline_wordend(here+2,rem-2,'*',1)
          107  +			if term ~= nil then
          108  +				styled:lpush('<em>')
          109  +					:push(here+1, (term-here) - 1)
          110  +					:lpush('</em>')
          111  +				i = (term - md.ptr) + 1
          112  +				goto skip
          113  +			end
          114  +		end
          115  +
          116  +		::fallback::styled:push(here,1) -- :/
          117  +		i = i + 1
          118  +	::skip::end end
          119  +
          120  +	-- we make two passes: the first detects and transforms inline elements,
          121  +	-- the second carries out block-level organization
          122  +
          123  +	var html: lib.str.acc html:init(styled.sz)
          124  +	var s = state {
          125  +		segt = segt.none;
          126  +		bqlvl = 0;
          127  +		curpos = md.ptr;
          128  +		blockstart = nil;
          129  +	}
          130  +	while s.curpos < md.ptr + md.ct do
          131  +		s.curpos = s.curpos + 1
          132  +	end 
          133  +
          134  +		html:free() -- JUST FOR NOW
          135  +	return styled:finalize()
          136  +end
          137  +
          138  +return m

Modified srv.t from [afa0417e30] to [d14bb4b6ce].

    16     16   	cfg: cfgcache
    17     17   }
    18     18   
    19     19   terra cfgcache:free() -- :/
    20     20   	self.secret:free()
    21     21   	self.instance:free()
    22     22   end
           23  +
           24  +terra srv:instance_timeline_fetch(r: lib.store.range): lib.mem.vec(lib.mem.ptr(lib.store.post))
           25  +	var all: lib.mem.vec(lib.mem.ptr(lib.store.post)) all:init(64)
           26  +	for i=0,self.sources.ct do var src = self.sources.ptr + i
           27  +		if src.handle ~= nil and src.backend.instance_timeline_fetch ~= nil then
           28  +			var lst = src:instance_timeline_fetch(r)
           29  +			all:assure(all.sz + lst.ct)
           30  +			for j=0, lst.ct do all:push(lst.ptr[j]) end
           31  +			lst:free()
           32  +		end
           33  +	end
           34  +	return all
           35  +end
    23     36   
    24     37   srv.metamethods.__methodmissing = macro(function(meth, self, ...)
    25     38   	local primary, ptr, stat, simple, oid = 0,1,2,3,4
    26     39   	local tk, rt = primary
    27     40   	local expr = {...}
    28     41   	for _,f in pairs(lib.store.backend.entries) do
    29     42   		local fn = f.field or f[1]
................................................................................
    34     47   			elseif rt.type == 'integer' then tk = oid
    35     48   			elseif rt.stat_basetype then tk = stat
    36     49   			elseif rt.ptr_basetype then tk = ptr end
    37     50   			break
    38     51   		end
    39     52   	end
    40     53   	
           54  +	local r = symbol(rt)
    41     55   	if tk == primary then
    42         -		return `self.sources.ptr[0]:[meth]([expr])
           56  +		return quote
           57  +			var [r]
           58  +			for i=0,self.sources.ct do var src = self.sources.ptr + i
           59  +				if src.handle ~= nil and src.backend.[meth] ~= nil then
           60  +					r = src:[meth]([expr])
           61  +					goto success
           62  +				end
           63  +			end
           64  +			lib.bail(['no active backends provide critical capability ' .. meth .. '!'])
           65  +			::success::;
           66  +		in r end
    43     67   	else local ok, empty
    44         -		local r = symbol(rt)
    45     68   		if tk == ptr then
    46     69   			ok = `r.ptr ~= nil
    47     70   			empty = `[rt]{ptr=nil,ct=0}
    48     71   		elseif tk == stat then
    49     72   			ok = `r.ok == true
    50     73   			empty = `[rt]{ok=false,error=1}
    51     74   		elseif tk == simple then
................................................................................
    54     77   		elseif tk == oid then
    55     78   			ok = `r ~= 0
    56     79   			empty = `0
    57     80   		end
    58     81   		return quote
    59     82   			var [r] = empty
    60     83   			for i=0,self.sources.ct do var src = self.sources.ptr + i
    61         -				if src.handle ~= nil then
           84  +				if src.handle ~= nil and src.backend.[meth] ~= nil then
    62     85   					r = src:[meth]([expr])
    63     86   					if [ok] then break
    64     87   						else r = empty end
    65     88   				end
    66     89   			end
    67     90   		in r end
    68     91   	end
................................................................................
    74     97   	msg: &lib.net.mg_http_message
    75     98   	aid: uint64 -- 0 if logged out
    76     99   	who: &lib.store.actor -- who we're logged in as, if aid ~= 0
    77    100   	peer: lib.store.inet
    78    101   	reqtype: lib.http.mime.t -- negotiated content type
    79    102   -- cache
    80    103   	navbar: lib.mem.ptr(int8)
          104  +	actorcache: lib.mem.cache(lib.mem.ptr(lib.store.actor),32) -- naive cache to avoid unnecessary queries
    81    105   -- private
    82    106   	varbuf: lib.mem.ptr(int8)
    83    107   	vbofs: &int8
    84    108   }
    85    109   
    86    110   -- this is unfortunately necessary to work around a terra bug
    87    111   -- it can't seem to handle forward-declarations of structs in C
................................................................................
   118    142   end
   119    143   
   120    144   terra convo:reroute(dest: rawstring) self:reroute_cookie(dest,nil) end
   121    145    
   122    146   terra convo:complain(code: uint16, title: rawstring, msg: rawstring)
   123    147   	var hdrs = array(lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' })
   124    148   
   125         -	var ti: lib.str.acc ti:compose('error :: ', title) defer ti:free()
   126         -	var bo: lib.str.acc bo:compose('<div class="message"><img class="icon" src="/s/warn.webp"><h1>error</h1><p>',msg,'</p></div>') defer bo:free()
          149  +	var ti: lib.str.acc ti:compose('error :: ', title)
          150  +	var bo: lib.str.acc bo:compose('<div class="message"><img class="icon" src="/s/warn.webp"><h1>',title,'</h1><p>',msg,'</p></div>')
   127    151   	var body = data.view.docskel {
   128         -		instance = self.srv.cfg.instance.ptr;
   129         -		title = ti.buf;
   130         -		body = bo.buf;
   131         -		class = 'error';
   132         -		navlinks = lib.coalesce(self.navbar.ptr, '');
          152  +		instance = self.srv.cfg.instance;
          153  +		title = ti:finalize();
          154  +		body = bo:finalize();
          155  +		class = lib.str.plit 'error';
          156  +		navlinks = lib.coalesce(self.navbar, [lib.mem.ptr(int8)]{ptr='',ct=0});
   133    157   	}
   134    158   
   135         -	if body.body == nil then
   136         -		body.body = "i'm sorry, dave. i can't let you do that"
          159  +	if body.body.ptr == nil then
          160  +		body.body = lib.str.plit"i'm sorry, dave. i can't let you do that"
   137    161   	end
   138    162   
   139    163   	body:send(self.con, code, [lib.mem.ptr(lib.http.header)] {
   140    164   		ptr = &hdrs[0], ct = [hdrs.type.N]
   141    165   	})
          166  +
          167  +	body.title:free()
          168  +	body.body:free()
   142    169   end
   143    170   
   144    171   -- CALL ONLY ONCE PER VAR
   145    172   terra convo:postv(name: rawstring)
   146    173   	if self.varbuf.ptr == nil then
   147    174   		self.varbuf = lib.mem.heapa(int8, self.msg.body.len + self.msg.query.len)
   148    175   		self.vbofs = self.varbuf.ptr
   149    176   	end
   150    177   	var o = lib.net.mg_http_get_var(&self.msg.body, name, self.vbofs, self.varbuf.ct - (self.vbofs - self.varbuf.ptr))
   151    178   	if o > 0 then
   152    179   		var r = self.vbofs
   153         -		self.vbofs = self.vbofs + o
   154         -		return r, o
          180  +		self.vbofs = self.vbofs + o + 1
          181  +		@(self.vbofs - 1) = 0
          182  +		var norm = lib.str.normalize([lib.mem.ptr(int8)]{ptr = r, ct = o})
          183  +		return norm.ptr, norm.ct
   155    184   	else return nil, 0 end
   156    185   end
   157    186   
   158    187   terra convo:getv(name: rawstring)
   159    188   	if self.varbuf.ptr == nil then
   160    189   		self.varbuf = lib.mem.heapa(int8, self.msg.query.len + self.msg.body.len)
   161    190   		self.vbofs = self.varbuf.ptr
   162    191   	end
   163    192   	var o = lib.net.mg_http_get_var(&self.msg.query, name, self.vbofs, self.varbuf.ct - (self.vbofs - self.varbuf.ptr))
   164    193   	if o > 0 then
   165    194   		var r = self.vbofs
   166         -		self.vbofs = self.vbofs + o
   167         -		return r, o
          195  +		self.vbofs = self.vbofs + o + 1
          196  +		@(self.vbofs - 1) = 0
          197  +		var norm = lib.str.normalize([lib.mem.ptr(int8)]{ptr = r, ct = o})
          198  +		return norm.ptr, norm.ct
   168    199   	else return nil, 0 end
   169    200   end
   170    201   
   171    202   local urimatch = macro(function(uri, ptn)
   172    203   	return `lib.net.mg_globmatch(ptn, [#ptn], uri.ptr, uri.ct+1)
   173    204   end)
   174    205   
................................................................................
   223    254   				var msg = [&lib.net.mg_http_message](p)
   224    255   				var co = convo {
   225    256   					con = con, srv = server, msg = msg;
   226    257   					aid = 0, who = nil, peer = peer;
   227    258   					reqtype = lib.http.mime.none;
   228    259   				} co.varbuf.ptr = nil
   229    260   				  co.navbar.ptr = nil
          261  +				  co.actorcache.top = 0
          262  +				  co.actorcache.cur = 0
   230    263   
   231    264   				-- first, check for an accept header. if it's there, we need to
   232    265   				-- iterate over the values and pick the highest-priority one
   233    266   				do var acc = lib.http.findheader(msg, 'Accept')
   234    267   					-- TODO handle q-value
   235    268   					if acc.ptr ~= nil then
   236    269   						var [mimevar] = [lib.mem.ref(int8)] { ptr = acc.ptr }
................................................................................
   344    377   				else
   345    378   					co:complain(400,'unknown method','you have submitted an invalid http request')
   346    379   				end
   347    380   
   348    381   				if co.aid ~= 0 then lib.mem.heapf(co.who) end
   349    382   				if co.varbuf.ptr ~= nil then co.varbuf:free() end
   350    383   				if co.navbar.ptr ~= nil then co.navbar:free() end
          384  +				co.actorcache:free()
   351    385   			end
   352    386   		end
   353    387   	end;
   354    388   }
   355    389   
   356    390   local terra cfg(s: &srv, befile: rawstring)
   357    391   	lib.report('configuring backends from ', befile)

Modified static/style.scss from [7905a3444d] to [1dafd96ed6].

     1      1   $color: hsl(323,100%,65%);
     2      2   %sans { font-family: "Alegreya Sans", "Open Sans", sans-serif; }
     3      3   %serif { font-family: Alegreya, GaramondNo8, "Garamond Premier Pro", "Adobe Garamond", Garamond, Junicode, serif; }
     4      4   %teletype { font-family: "Inconsolata LGC", Inconsolata, monospace; font-size: 12pt !important; }
     5      5   
     6         -@function tone($pct) { @return adjust-color($color, $lightness: $pct) }
            6  +@function tone($pct, $alpha: 0) { @return adjust-color($color, $lightness: $pct, $alpha: $alpha) }
     7      7   
     8      8   body {
     9      9   	@extend %sans;
    10         -	background-color: adjust-color($color, $lightness: -55%);
    11         -	color: adjust-color($color, $lightness: 25%);
           10  +	background-color: tone(-55%);
           11  +	color: tone(25%);
    12     12   	font-size: 14pt;
    13     13   	margin: 0;
    14     14   	padding: 0;
    15     15   }
    16     16   a[href] {
    17         -	color: adjust-color($color, $lightness: 10%);
           17  +	color: tone(10%);
           18  +	text-decoration-color: adjust-color($color, $lightness: 10%, $alpha: -0.5);
    18     19   	&:hover {
    19     20   		color: white;
    20         -		text-shadow: 0 0 15px adjust-color($color, $lightness: 20%);
           21  +		text-shadow: 0 0 15px tone(20%);
           22  +		text-decoration-color: adjust-color($color, $lightness: 10%, $alpha: -0.1);
    21     23   	}
    22     24   }
    23     25   a[href^="//"],
    24     26   a[href^="http://"],
    25     27   a[href^="https://"] { // external link
    26     28   	&:hover::after {
    27     29   		color: black;
................................................................................
   410    412   		padding: 0.1in;
   411    413   		&:hover { font-weight: bold; }
   412    414   	}
   413    415   }
   414    416   
   415    417   code {
   416    418   	@extend %teletype;
   417         -	background: adjust-color($color, $lightness: -50%);
   418         -	border: 1px solid adjust-color($color, $lightness: -20%);
          419  +	background: tone(-50%);
          420  +	border: 1px solid tone(-20%);
   419    421   	padding: 2px 6px;
   420    422   	text-shadow: 2px 2px black;
   421    423   }
          424  +
          425  +div.post {
          426  +	@extend %box;
          427  +	display: grid;
          428  +	grid-template-columns: 1in 1fr max-content;
          429  +	grid-template-rows: 1fr max-content;
          430  +	margin-bottom: 0.1in;
          431  +	>.avatar {
          432  +		grid-column: 1/2; grid-row: 1/2;
          433  +		width: 1in;
          434  +	}
          435  +	>a[href].username {
          436  +		display: block;
          437  +		grid-column: 1/3;
          438  +		grid-row: 2/3;
          439  +		text-align: left;
          440  +		text-decoration: none;
          441  +		padding: 0.1in;
          442  +		padding-left: 0.15in;
          443  +		>.nym { font-weight: bold; }
          444  +		color: tone(0%,-0.4);
          445  +		> span.nym { color: tone(10%) }
          446  +		> span.handle { color: tone(-5%) }
          447  +		background: linear-gradient(to right, tone(-55%), transparent);
          448  +	}
          449  +	>.content {
          450  +		grid-column: 2/4; grid-row: 1/2;
          451  +		padding: 0.2in;
          452  +		@extend %serif;
          453  +		font-size: 110%;
          454  +	}
          455  +	> a[href].permalink {
          456  +		display: block;
          457  +		grid-column: 3/4; grid-row: 2/3;
          458  +		font-size: 80%;
          459  +		text-align: right;
          460  +		padding: 0.1in;
          461  +		padding-right: 0.15in;
          462  +		font-style: italic;
          463  +		background: linear-gradient(to left, tone(-55%,-0.5), transparent);
          464  +	}
          465  +}
          466  +
          467  +a[href].rawlink {
          468  +	@extend %teletype;
          469  +}

Modified store.t from [5ad659a834] to [4782518865].

     1      1   -- vim: ft=terra
     2      2   local m = {
     3         -	timepoint = uint64;
            3  +	timepoint = int64;
     4      4   	scope = lib.enum {
     5      5   		'public', 'private', 'local';
     6      6   		'personal', 'direct', 'circle';
     7      7   	};
     8      8   	notiftype = lib.enum {
     9      9   		'mention', 'like', 'rt', 'react'
    10     10   	};
................................................................................
    62     62   struct m.actor {
    63     63   	id: uint64
    64     64   	nym: str
    65     65   	handle: str
    66     66   	origin: uint64
    67     67   	bio: str
    68     68   	avatar: str
    69         -	knownsince: int64
           69  +	knownsince: m.timepoint
    70     70   	rights: m.rights
    71     71   	key: lib.mem.ptr(uint8)
    72     72   
    73     73   -- ephemera
    74     74   	xid: str
    75     75   	source: &m.source
    76     76   }
................................................................................
    79     79   	posts: intptr
    80     80   	follows: intptr
    81     81   	followers: intptr
    82     82   	mutuals: intptr
    83     83   }
    84     84   
    85     85   struct m.range {
    86         -	time: bool
           86  +	mode: uint8 -- 0 == I->I, 1 == T->I, 2 == I->T, 3 == T->T
    87     87   	union {
    88     88   		from_time: m.timepoint
    89     89   		from_idx: uint64
    90     90   	}
    91     91   	union {
    92     92   		to_time: m.timepoint
    93     93   		to_idx: uint64
................................................................................
    95     95   }
    96     96   
    97     97   struct m.post {
    98     98   	id: uint64
    99     99   	author: uint64
   100    100   	subject: str
   101    101   	body: str
          102  +	acl: str
   102    103   	posted: m.timepoint
   103    104   	discovered: m.timepoint
   104         -	scope: m.scope.t
   105    105   	mentions: lib.mem.ptr(uint64)
   106    106   	circles: lib.mem.ptr(uint64) --only meaningful if scope is set to circle
   107    107   	convo: uint64
   108    108   	parent: uint64
   109         -
          109  +-- ephemera
          110  +	localpost: bool
   110    111   	source: &m.source
   111    112   }
   112    113   
   113    114   local cnf = terralib.memoize(function(ty,rty)
   114    115   	rty = rty or ty
   115    116   	return struct {
   116    117   		enum: {&opaque, uint64, rawstring} -> intptr
................................................................................
   207    208   		-- for determining session validity & caps
   208    209   			-- aid:    uint64
   209    210   			-- origin: inet
   210    211   
   211    212   	actor_conf_str: cnf(rawstring, lib.mem.ptr(int8))
   212    213   	actor_conf_int: cnf(intptr, lib.stat(intptr))
   213    214   
   214         -	post_save: {&m.source, &m.post} -> bool
   215         -	post_create: {&m.source, &m.post} -> bool
          215  +	post_save: {&m.source, &m.post} -> {}
          216  +	post_create: {&m.source, &m.post} -> uint64
   216    217   	actor_post_fetch_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(m.post)
   217    218   	convo_fetch_xid: {&m.source,rawstring} -> lib.mem.ptr(m.post)
   218    219   	convo_fetch_uid: {&m.source,uint64} -> lib.mem.ptr(m.post)
   219    220   
   220         -	actor_timeline_fetch_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(m.post)
   221         -	instance_timeline_fetch: {&m.source, m.range} -> lib.mem.ptr(m.post)
          221  +	actor_timeline_fetch_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post))
          222  +	instance_timeline_fetch: {&m.source, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post))
   222    223   }
   223    224   
   224    225   struct m.source {
   225    226   	backend: &m.backend
   226    227   	id: lib.mem.ptr(int8)
   227    228   	handle: &opaque
   228    229   	string: lib.mem.ptr(int8)

Modified str.t from [c8d105a016] to [c5ea2451a4].

     1      1   -- vim: ft=terra
     2      2   -- string.t: string classes
     3      3   local util = dofile('common.lua')
            4  +local pstr = lib.mem.ptr(int8)
            5  +local pref = lib.mem.ref(int8)
     4      6   
     5      7   local m = {
     6      8   	sz = terralib.externfunction('strlen', rawstring -> intptr);
     7      9   	cmp = terralib.externfunction('strcmp', {rawstring, rawstring} -> int);
     8     10   	ncmp = terralib.externfunction('strncmp', {rawstring, rawstring, intptr} -> int);
     9     11   	cpy = terralib.externfunction('stpcpy',{rawstring, rawstring} -> rawstring);
    10     12   	ncpy = terralib.externfunction('stpncpy',{rawstring, rawstring, intptr} -> rawstring);
................................................................................
    14     16   		terralib.types.funcpointer({&rawstring,rawstring},{int},true));
    15     17   	bfmt = terralib.externfunction('sprintf',
    16     18   		terralib.types.funcpointer({rawstring,rawstring},{int},true));
    17     19   	span = terralib.externfunction('strspn',{rawstring, rawstring} -> rawstring);
    18     20   }
    19     21   
    20     22   do local strptr = (lib.mem.ptr(int8))
           23  +	local strref = (lib.mem.ref(int8))
    21     24   	local byteptr = (lib.mem.ptr(uint8))
    22     25   	strptr.metamethods.__cast = function(from,to,e)
    23     26   		if from == &int8 then
    24     27   			return `strptr {ptr = e, ct = m.sz(e)}
    25     28   		elseif to == &int8 then
    26     29   			return e.ptr
    27     30   		end
    28     31   	end
    29     32   
    30     33   	terra strptr:cmp(other: strptr)
           34  +		if self.ptr == nil and other.ptr == nil then return true end
           35  +		if self.ptr == nil or other.ptr == nil then return false end
           36  +
           37  +		var sz = lib.math.biggest(self.ct, other.ct)
           38  +		for i = 0, sz do
           39  +			if self.ptr[i] == 0 and other.ptr[i] == 0 then return true end
           40  +			if self.ptr[i] ~= other.ptr[i] then return false end
           41  +		end
           42  +		return true
           43  +	end
           44  +	terra strref:cmp(other: strref)
           45  +		if self.ptr == nil and other.ptr == nil then return true end
           46  +		if self.ptr == nil or other.ptr == nil then return false end
           47  +
    31     48   		var sz = lib.math.biggest(self.ct, other.ct)
    32     49   		for i = 0, sz do
    33     50   			if self.ptr[i] == 0 and other.ptr[i] == 0 then return true end
    34     51   			if self.ptr[i] ~= other.ptr[i] then return false end
    35     52   		end
    36     53   		return true
    37     54   	end
           55  +
           56  +	strptr.methods.cmpl = macro(function(self,other)
           57  +		return `self:cmp(strptr { ptr = [other:asvalue()], ct = [#(other:asvalue())] })
           58  +	end)
           59  +	strref.methods.cmpl = macro(function(self,other)
           60  +		return `self:cmp(strref { ptr = [other:asvalue()], ct = [#(other:asvalue())] })
           61  +	end)
    38     62   
    39     63   	terra byteptr:cmp(other: byteptr)
    40     64   		var sz = lib.math.biggest(self.ct, other.ct)
    41     65   		for i = 0, sz do
    42     66   			if self.ptr[i] == 0 and other.ptr[i] == 0 then return true end
    43     67   			if self.ptr[i] ~= other.ptr[i] then return false end
    44     68   		end
    45     69   		return true
    46     70   	end
    47     71   end
           72  +
           73  +terra m.normalize(s: pstr)
           74  +	var c: rawstring = s.ptr
           75  +	var n: rawstring = s.ptr
           76  +	while n < s.ptr + s.ct do
           77  +		while @n == 0 or @n == @'\r' do
           78  +			n = n + 1
           79  +			if n > s.ptr + s.ct then
           80  +				c = c + 1 goto done
           81  +			end
           82  +		end
           83  +		@c = @n
           84  +		c = c + 1
           85  +		n = n + 1
           86  +	end ::done::
           87  +	@c = 0
           88  +	return pstr { ptr = s.ptr, ct = c - s.ptr }
           89  +end
    48     90   
    49     91   struct m.acc {
    50     92   	buf: rawstring
    51     93   	sz: intptr
    52     94   	run: intptr
    53     95   	space: intptr
    54     96   }
................................................................................
    70    112   	--lib.dbg('freeing string accumulator')
    71    113   	if self.buf ~= nil and self.space > 0 then
    72    114   		lib.mem.heapf(self.buf)
    73    115   	end
    74    116   end;
    75    117   
    76    118   terra m.acc:crush()
    77         -	lib.dbg('crushing string accumulator')
          119  +	--lib.dbg('crushing string accumulator')
    78    120   	self.buf = [rawstring](lib.mem.heapr_raw(self.buf, self.sz))
    79    121   	self.space = self.sz
    80    122   	return self
    81    123   end;
    82    124   
    83    125   terra m.acc:finalize()
    84         -	lib.dbg('finalizing string accumulator')
          126  +	--lib.dbg('finalizing string accumulator')
    85    127   	self:crush()
    86    128   	var pt: lib.mem.ptr(int8)
    87    129   	pt.ptr = self.buf
    88    130   	pt.ct = self.sz
    89    131   	self.buf = nil
    90    132   	self.sz = 0
    91    133   	return pt
................................................................................
   114    156   	lib.mem.cpy(self.buf + self.sz, str, len)
   115    157   	self.sz = self.sz + len
   116    158   	self.buf[self.sz] = 0
   117    159   	return self
   118    160   end;
   119    161   
   120    162   m.lit = macro(function(str)
   121         -	return `[lib.mem.ref(int8)] {ptr = [str:asvalue()], ct = [#(str:asvalue())]}
          163  +	if str:asvalue() ~= nil then
          164  +		return `[lib.mem.ref(int8)] {ptr = [str:asvalue()], ct = [#(str:asvalue())]}
          165  +	else
          166  +		return `[lib.mem.ref(int8)] {ptr = nil, ct = 0}
          167  +	end
          168  +end)
          169  +
          170  +m.plit = macro(function(str)
          171  +	if str:asvalue() ~= nil then
          172  +		return `[lib.mem.ptr(int8)] {ptr = [str:asvalue()], ct = [#(str:asvalue())]}
          173  +	else
          174  +		return `[lib.mem.ptr(int8)] {ptr = nil, ct = 0}
          175  +	end
   122    176   end)
   123    177   
   124    178   m.acc.methods.lpush = macro(function(self,str)
   125    179   	return `self:push([str:asvalue()], [#(str:asvalue())]) end)
   126    180   m.acc.methods.ppush = terra(self: &m.acc, str: lib.mem.ptr(int8))
   127    181   	self:push(str.ptr, str.ct)            return self end;
   128    182   m.acc.methods.merge = terra(self: &m.acc, str: lib.mem.ptr(int8))
................................................................................
   212    266   			local str,sz = v[1],v[2]
   213    267   			if type(sz) == 'number' then
   214    268   				memreq_const = memreq_const + sz
   215    269   			elseif type(sz:asvalue()) == 'number' then
   216    270   				memreq_const = memreq_const + sz:asvalue()
   217    271   			else memreq_exp = `[sz] + [memreq_exp] end
   218    272   
   219         -			cpy = quote [kp] ; [ptr] = [&int8](lib.mem.cpy([ptr], str, sz)) end
          273  +			cpy = quote [kp] ;
          274  +				--lib.io.fmt('encapsulating string %p → %p [%s] sz %llu\n', str, [ptr], str, sz)
          275  +				[ptr] = [&int8](lib.mem.cpy([ptr], str, sz))
          276  +				--lib.io.fmt(' :: encapsulated string %p [%s]\n', box.obj.[k],box.obj.[k])
          277  +			end
   220    278   			if ty.ptr_basetype then
   221    279   				cpy = quote [cpy]; [box].obj.[k].ct = sz end
   222    280   			end
   223    281   			isnull = `[str] == nil
   224    282   		else
   225    283   			memreq_exp = `(m.sz(v) + 1) + [memreq_exp] -- make room for NUL
   226    284   			isnull = `v == nil

Modified tpl.t from [3cd51c8b03] to [3e776df34f].

    52     52   	local rec = terralib.types.newstruct(string.format('template<%s>',tplspec.id or ''))
    53     53   	local symself = symbol(&rec)
    54     54   	do local kfac = {}
    55     55   		for afterseg,key in pairs(fields) do
    56     56   			if not kfac[key] then
    57     57   				rec.entries[#rec.entries + 1] = {
    58     58   					field = key;
    59         -					type = rawstring;
           59  +					type = lib.mem.ptr(int8);
    60     60   				}
    61     61   			end
    62     62   			kfac[key] = (kfac[key] or 0) + 1
    63     63   		end
    64     64   		for key, fac in pairs(kfac) do
    65     65   			tallyup[#tallyup + 1] = quote
    66         -				[runningtally] = [runningtally] + lib.str.sz([symself].[key])*fac
           66  +				[runningtally] = [runningtally] + ([symself].[key].ct)*fac
    67     67   			end
    68     68   		end
    69     69   	end
    70     70   
    71     71   	local copiers = {}
    72     72   	local senders = {}
    73     73   	local appenders = {}
................................................................................
    76     76   	local accumulator = symbol(&lib.str.acc)
    77     77   	local destcon = symbol(&lib.net.mg_connection)
    78     78   	for idx, seg in ipairs(segs) do
    79     79   		copiers[#copiers+1] = quote [cpypos] = lib.mem.cpy([cpypos], [&opaque]([seg]), [#seg]) end
    80     80   		senders[#senders+1] = quote lib.net.mg_send([destcon], [seg], [#seg]) end
    81     81   		appenders[#appenders+1] = quote [accumulator]:push([seg], [#seg]) end
    82     82   		if fields[idx] then
           83  +			--local fsz = `lib.str.sz(symself.[fields[idx]])
           84  +			local fval = `symself.[fields[idx]].ptr
           85  +			local fsz = `symself.[fields[idx]].ct
    83     86   			copiers[#copiers+1] = quote
    84         -				[cpypos] = lib.mem.cpy([cpypos],
    85         -					[&opaque](symself.[fields[idx]]),
    86         -					lib.str.sz(symself.[fields[idx]]))
           87  +				[cpypos] = lib.mem.cpy([cpypos], [&opaque]([fval]), [fsz])
    87     88   			end
    88     89   			senders[#senders+1] = quote
    89         -				lib.net.mg_send([destcon],
    90         -					symself.[fields[idx]],
    91         -					lib.str.sz(symself.[fields[idx]]))
           90  +				lib.net.mg_send([destcon], [fval], [fsz])
           91  +			end
           92  +			appenders[#appenders+1] = quote
           93  +				[accumulator]:push([fval], [fsz])
    92     94   			end
    93     95   		end
    94     96   	end
    95     97   
    96     98   	local tid = tplspec.id or '<anonymous>'
    97     99   	rec.methods.tostr = terra([symself])
    98    100   		lib.dbg(['compiling template ' .. tid])

Modified view/tweet.tpl from [4ad2e9eaa9] to [e1b2184d52].

     1      1   <div class="post">
     2         -	<div class="detail">
     3         -		<a class="username" href="@link">
     4         -			<span class="nym">@nym</span> <span class="handle">[@xid]</span>
     5         -		</div>
     6         -		<div class="when">@when</div>
     7         -	</div>
            2  +	<img class="avatar" src="@avatar">
            3  +	<a class="username" href="/@acctlink">
            4  +		<span class="nym">@nym</span> [<span class="handle">@xid</span>]
            5  +	</a>
     8      6   	<div class="content">
     9         -		<img class="avatar" src="@avatar">
            7  +		<div class="subject">@subject</div>
    10      8   		<div class="text">@text</div>
    11      9   	</div>
           10  +	<a class="permalink" href="@permalink">@when</a>
    12     11   </div>