parsav  Check-in [5b3a03ad34]

Overview
Comment:big ol iteration
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 5b3a03ad34669be0b5e5ecd853db0a2c2fc923886345f08e93cece08a84c357f
User & Date: lexi on 2020-12-25 03:59:32
Other Links: manifest | tags
Context
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
2020-12-22
23:01
milestone check-in: 419d1a1ebe user: lexi tags: trunk
Changes

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

    21     21   				key = $1::text 
    22     22   		]];
    23     23   	};
    24     24   
    25     25   	actor_fetch_uid = {
    26     26   		params = {uint64}, sql = [[
    27     27   			select
    28         -				id, nym, handle, origin,
    29         -				bio, rank, quota, key
           28  +				id, nym, handle, origin, bio,
           29  +				avataruri, rank, quota, key,
           30  +				extract(epoch from knownsince)::bigint
           31  +
    30     32   			from parsav_actors
    31     33   				where id = $1::bigint
    32     34   		]];
    33     35   	};
    34     36   
    35     37   	actor_fetch_xid = {
    36     38   		params = {lib.mem.ptr(int8)}, sql = [[
    37         -			select a.id, a.nym, a.handle, a.origin,
    38         -			       a.bio, a.rank, a.quota, a.key, 
           39  +			select a.id, a.nym, a.handle, a.origin, a.bio,
           40  +			       a.avataruri, a.rank, a.quota, a.key, 
           41  +			       extract(epoch from knownsince)::bigint,
    39     42   				   coalesce(a.handle || '@' || s.domain,
    40     43   				            '@' || a.handle) as xid,
    41     44   
    42     45   				coalesce(s.domain,
    43     46   				        (select value from parsav_config
    44     47   							where key='domain' limit 1)) as domain
    45     48   
................................................................................
    50     53   			where $1::text = (a.handle || '@' || domain) or
    51     54   			      $1::text = ('@' || a.handle || '@' || domain) or
    52     55   				  (a.origin is null and
    53     56   					  $1::text = a.handle or
    54     57   					  $1::text = ('@' || a.handle))
    55     58   		]];
    56     59   	};
           60  +
           61  +	actor_auth_pw = {
           62  +		params = {lib.mem.ptr(int8),rawstring,lib.mem.ptr(int8),lib.store.inet}, sql = [[
           63  +			select a.aid from parsav_auth as a
           64  +				left join parsav_actors as u on u.id = a.uid
           65  +			where (a.uid is null or u.handle = $1::text or (
           66  +					a.uid = 0 and a.name = $1::text
           67  +				)) and
           68  +				(a.kind = 'trust' or (a.kind = $2::text and a.cred = $3::bytea)) and
           69  +				(a.netmask is null or a.netmask >> $4::inet)
           70  +			order by blacklist desc limit 1
           71  +		]];
           72  +	};
    57     73   
    58     74   	actor_enum_local = {
    59     75   		params = {}, sql = [[
    60         -			select id, nym, handle, origin,
    61         -			       bio, rank, quota, key,
           76  +			select id, nym, handle, origin, bio,
           77  +			       null::text, rank, quota, key,
           78  +			       extract(epoch from knownsince)::bigint,
    62     79   				handle ||'@'||
    63     80   				(select value from parsav_config
    64     81   					where key='domain' limit 1) as xid
    65     82   			from parsav_actors where origin is null
    66     83   		]];
    67     84   	};
    68     85   
    69     86   	actor_enum = {
    70     87   		params = {}, sql = [[
    71         -			select a.id, a.nym, a.handle, a.origin,
    72         -			       a.bio, a.rank, a.quota, a.key,
           88  +			select a.id, a.nym, a.handle, a.origin, a.bio,
           89  +			       a.avataruri, a.rank, a.quota, a.key,
           90  +			       extract(epoch from knownsince)::bigint,
    73     91   				   coalesce(a.handle || '@' || s.domain,
    74     92   				            '@' || a.handle) as xid
    75     93   			from parsav_actors a
    76     94   			left join parsav_servers s on s.id = a.origin
    77     95   		]];
    78     96   	};
           97  +
           98  +	actor_stats = {
           99  +		params = {uint64}, sql = ([[
          100  +			with tweets as (
          101  +				select from parsav_posts where author = $1::bigint
          102  +			),
          103  +			follows as (
          104  +				select relatee as user from parsav_rels
          105  +					where relator = $1::bigint and kind = <follow>
          106  +			),
          107  +			followers as (
          108  +				select relator as user from parsav_rels
          109  +					where relatee = $1::bigint and kind = <follow>
          110  +			),
          111  +			mutuals as (select * from follows intersect select * from followers)
          112  +
          113  +			select count(tweets.*)::bigint,
          114  +			       count(follows.*)::bigint,
          115  +				   count(followers.*)::bigint,
          116  +				   count(mutuals.*)::bigint
          117  +			from tweets, follows, followers, mutuals
          118  +		]]):gsub('<(%w+)>',function(r) return tostring(lib.store.relation[r]) end)
          119  +	};
    79    120   
    80    121   	actor_auth_how = {
    81    122   		params = {rawstring, lib.store.inet}, sql = [[
    82    123   		with mts as (select a.kind from parsav_auth as a
    83    124   			left join parsav_actors as u on u.id = a.uid
    84    125   			where (a.uid is null or u.handle = $1::text or (
    85    126   					a.uid = 0 and a.name = $1::text
................................................................................
    93    134   				(select count(*) from mts where kind like 'challenge-%') > 0,
    94    135   				(select count(*) from mts where kind = 'trust') > 0
    95    136   		]]; -- cheat
    96    137   	};
    97    138   
    98    139   	actor_session_fetch = {
    99    140   		params = {uint64, lib.store.inet}, sql = [[
   100         -			select a.id, a.nym, a.handle, a.origin,
   101         -			       a.bio, a.rank, a.quota, a.key,
          141  +			select a.id, a.nym, a.handle, a.origin, a.bio,
          142  +			       a.avataruri, a.rank, a.quota, a.key,
          143  +			       extract(epoch from knownsince)::bigint,
   102    144   				   coalesce(a.handle || '@' || s.domain,
   103    145   				            '@' || a.handle) as xid,
   104    146   
   105    147   			       au.restrict,
   106    148   						array['post'  ] <@ au.restrict as can_post,
   107    149   						array['edit'  ] <@ au.restrict as can_edit,
   108    150   						array['acct'  ] <@ au.restrict as can_acct,
................................................................................
   171    213   	for j=0,sz do i.v6[j] = v[4 + j] end -- 😬
   172    214   	return i
   173    215   end
   174    216   pqr.methods.int = macro(function(self, ty, row, col)
   175    217   	return quote
   176    218   		var i: ty:astype()
   177    219   		var v = lib.pq.PQgetvalue(self.res, row, col)
          220  +		--i = @[&uint64](v)
   178    221   		lib.math.netswap_ip(ty, v, &i)
   179    222   	in i end
   180    223   end)
   181    224   
   182    225   local pqt = {
   183    226   	[lib.store.inet] = function(cidr)
   184    227   		local tycode = cidr and 0x01 or 0x00
................................................................................
   216    259   			end
   217    260   			lib.bail('could not prepare PGSQL statement ',k,': ',lib.pq.PQresultErrorMessage(res))
   218    261   		end
   219    262   		lib.dbg('prepared PGSQL statement ',k) 
   220    263   	end
   221    264   
   222    265   	local args, casts, counters, fixers, ft, yield = {}, {}, {}, {}, {}, {}
          266  +	local dumpers = {}
   223    267   	for i, ty in ipairs(q.params) do
   224    268   		args[i] = symbol(ty)
   225    269   		ft[i] = `1
   226    270   		if ty == rawstring then
   227    271   			counters[i] = `lib.trn([args[i]] == nil, 0, lib.str.sz([args[i]]))
   228    272   			casts[i] = `[&int8]([args[i]])
          273  +			dumpers[#dumpers+1] = `lib.io.fmt([tostring(i)..'. got rawstr %s\n'], [args[i]])
   229    274   		elseif ty == lib.store.inet then -- assume not CIDR
   230    275   			counters[i] = `lib.trn([args[i]].pv == 4,4,16)+4
   231    276   			casts[i] = quote
   232    277   				var ipbuf: int8[20]
   233    278   				;[pqt[lib.store.inet](false)]([args[i]], [&uint8](&ipbuf))
   234    279   			in &ipbuf[0] end
          280  +			dumpers[#dumpers+1] = `lib.io.fmt([tostring(i)..'. got inet\n'])
   235    281   		elseif ty.ptr_basetype == int8 or ty.ptr_basetype == uint8 then
   236    282   			counters[i] = `[args[i]].ct
   237    283   			casts[i] = `[&int8]([args[i]].ptr)
          284  +			dumpers[#dumpers+1] = `lib.io.fmt([tostring(i)..'. got ptr %llu %.*s\n'], [args[i]].ct, [args[i]].ct, [args[i]].ptr)
   238    285   		elseif ty:isintegral() then
   239    286   			counters[i] = ty.bytes
   240    287   			casts[i] = `[&int8](&[args[i]])
          288  +			dumpers[#dumpers+1] = `lib.io.fmt([tostring(i)..'. got int %llu\n'], [args[i]])
   241    289   			fixers[#fixers + 1] = quote
   242    290   				--lib.io.fmt('uid=%llu(%llx)\n',[args[i]],[args[i]])
   243    291   				[args[i]] = lib.math.netswap(ty, [args[i]])
   244    292   			end
   245    293   		end
   246    294   	end
   247    295   
   248    296   	terra q.exec(src: &lib.store.source, [args])
   249    297   		var params = arrayof([&int8], [casts])
   250    298   		var params_sz = arrayof(int, [counters])
   251    299   		var params_ft = arrayof(int, [ft])
   252    300   		[fixers]
          301  +		--[dumpers]
   253    302   		var res = lib.pq.PQexecPrepared([&lib.pq.PGconn](src.handle), stmt,
   254    303   			[#args], params, params_sz, params_ft, 1)
   255    304   		if res == nil then
   256    305   			lib.bail(['grievous error occurred executing '..k..' against database'])
   257    306   		elseif lib.pq.PQresultStatus(res) ~= lib.pq.PGRES_TUPLES_OK then
   258    307   			lib.bail(['PGSQL database procedure '..k..' failed\n'],
   259    308   			lib.pq.PQresultErrorMessage(res))
................................................................................
   268    317   		end
   269    318   	end
   270    319   end
   271    320   
   272    321   local terra row_to_actor(r: &pqr, row: intptr): lib.mem.ptr(lib.store.actor)
   273    322   	var a: lib.mem.ptr(lib.store.actor)
   274    323   
   275         -	if r:cols() >= 8 then 
          324  +	if r:cols() >= 9 then 
   276    325   		a = [ lib.str.encapsulate(lib.store.actor, {
   277    326   			nym = {`r:string(row,1), `r:len(row,1)+1};
   278    327   			bio = {`r:string(row,4), `r:len(row,4)+1};
          328  +			avatar = {`r:string(row,5), `r:len(row,5)+1};
   279    329   			handle = {`r:string(row, 2); `r:len(row,2) + 1};
   280         -			xid = {`r:string(row, 8); `r:len(row,8) + 1};
          330  +			xid = {`r:string(row, 10); `r:len(row,10) + 1};
   281    331   		}) ]
   282    332   	else
   283    333   		a = [ lib.str.encapsulate(lib.store.actor, {
   284    334   			nym = {`r:string(row,1), `r:len(row,1)+1};
   285    335   			bio = {`r:string(row,4), `r:len(row,4)+1};
          336  +			avatar = {`r:string(row,5), `r:len(row,5)+1};
   286    337   			handle = {`r:string(row, 2); `r:len(row,2) + 1};
   287    338   		}) ]
   288    339   		a.ptr.xid = nil
   289    340   	end
   290    341   	a.ptr.id = r:int(uint64, row, 0);
   291    342   	a.ptr.rights = lib.store.rights_default();
   292         -	a.ptr.rights.rank = r:int(uint16, row, 5);
   293         -	a.ptr.rights.quota = r:int(uint32, row, 6);
   294         -	if r:null(row,7) then
          343  +	a.ptr.rights.rank = r:int(uint16, row, 6);
          344  +	a.ptr.rights.quota = r:int(uint32, row, 7);
          345  +	a.ptr.knownsince = r:int(int64,row, 9);
          346  +	if r:null(row,8) then
   295    347   		a.ptr.key.ct = 0 a.ptr.key.ptr = nil
   296    348   	else
   297         -		a.ptr.key = r:bin(row,7)
          349  +		a.ptr.key = r:bin(row,8)
   298    350   	end
   299    351   	if r:null(row,3) then a.ptr.origin = 0
   300    352   	else a.ptr.origin = r:int(uint64,row,3) end
   301    353   	return a
   302    354   end
   303    355   
   304         -local checksha = function(hnd, query, hash, origin, username, pw)
   305         -	local inet_buf = symbol(uint8[4 + 16])
          356  +local checksha = function(src, hash, origin, username, pw)
   306    357   	local validate = function(kind, cred, credlen)
   307    358   		return quote 
   308         -			var osz: intptr if origin.pv == 4 then osz = 4 else osz = 16 end 
   309         -			var formats = arrayof([int], 1,1,1,1)
   310         -			var params = arrayof([&int8], username, kind,
   311         -				[&int8](&cred), [&int8](&inet_buf))
   312         -			var lens = arrayof(int, lib.str.sz(username), [#kind], credlen, osz + 4)
   313         -			var res = lib.pq.PQexecParams([&lib.pq.PGconn](hnd), query, 4, nil,
   314         -				params, lens, formats, 1)
   315         -			if res == nil then
   316         -				lib.bail('grievous failure checking pwhash')
   317         -			elseif lib.pq.PQresultStatus(res) ~= lib.pq.PGRES_TUPLES_OK then
   318         -				lib.warn('pwhash query failed: ', lib.pq.PQresultErrorMessage(res), '\n', query)
   319         -			else
   320         -				var r = pqr {
   321         -					sz = lib.pq.PQntuples(res);
   322         -					res = res;
   323         -				}
   324         -				if r.sz > 0 then -- found a record! stop here
   325         -					var aid = r:int(uint64, 0,0)
   326         -					r:free()
   327         -					return aid
   328         -				end
          359  +			var r = queries.actor_auth_pw.exec(
          360  +				[&lib.store.source](src),
          361  +				username,
          362  +				kind,
          363  +				[lib.mem.ptr(int8)] {ptr=[&int8](cred), ct=credlen},
          364  +				origin)
          365  +			if r.sz > 0 then -- found a record! stop here
          366  +				var aid = r:int(uint64, 0,0)
          367  +				r:free()
          368  +				return aid
   329    369   			end
   330    370   		end
   331    371   	end
   332    372   	
   333    373   	local out = symbol(uint8[64])
   334    374   	local vdrs = {}
   335    375   
   336    376   		local alg = lib.md['MBEDTLS_MD_SHA' .. tostring(hash)]
   337    377   		vdrs[#vdrs+1] = quote
   338    378   			if lib.md.mbedtls_md(lib.md.mbedtls_md_info_from_type(alg),
   339         -				[&uint8](pw), lib.str.sz(pw), out) ~= 0 then
          379  +				[&uint8](pw.ptr), pw.ct, out) ~= 0 then
   340    380   				lib.bail('hashing failure!')
   341    381   			end
   342         -			[ validate(string.format('pw-sha%u', hash), out, hash / 8) ]
          382  +			[ validate(string.format('pw-sha%u', hash), `&out[0], hash / 8) ]
   343    383   		end
   344    384   
   345    385   	return quote
   346    386   		lib.dbg(['searching for hashed password credentials in format SHA' .. tostring(hash)])
   347         -		var [inet_buf]
   348         -		[pqt[lib.store.inet](false)](origin, inet_buf)
   349    387   		var [out]
   350    388   		[vdrs]
   351    389   		lib.dbg(['could not find password hash'])
   352    390   	end
   353    391   end
   354    392   
   355    393   local b = `lib.store.backend {
................................................................................
   439    477   		end
   440    478   	end];
   441    479   
   442    480   	actor_auth_how = [terra(
   443    481   			src: &lib.store.source,
   444    482   			ip: lib.store.inet,
   445    483   			username: rawstring
   446         -		)
          484  +		): {lib.store.credset, bool}
   447    485   		var cs: lib.store.credset cs:clear();
   448    486   		var r = queries.actor_auth_how.exec(src, username, ip) 
   449         -		if r.sz == 0 then return cs end -- just in case
          487  +		if r.sz == 0 then return cs, false end -- just in case
   450    488   		defer r:free()
   451    489   		(cs.pw << r:bool(0,0))
   452    490   		(cs.otp << r:bool(0,1))
   453    491   		(cs.challenge << r:bool(0,2))
   454    492   		(cs.trust << r:bool(0,3))
   455         -		return cs
          493  +		return cs, true
   456    494   	end];
   457    495   	 
   458    496   	actor_auth_pw = [terra(
   459    497   			src: &lib.store.source,
   460    498   			ip: lib.store.inet,
   461         -			username: rawstring,
   462         -			cred: rawstring
   463         -		)
   464         -		var q = [[select a.aid from parsav_auth as a
   465         -			left join parsav_actors as u on u.id = a.uid
   466         -			where (a.uid is null or u.handle = $1::text or (
   467         -					a.uid = 0 and a.name = $1::text
   468         -				)) and
   469         -				(a.kind = 'trust' or (a.kind = $2::text and a.cred = $3::bytea)) and
   470         -				(a.netmask is null or a.netmask >> $4::inet)
   471         -			order by blacklist desc limit 1]]
          499  +			username: lib.mem.ptr(int8),
          500  +			cred: lib.mem.ptr(int8)
          501  +		): uint64
   472    502   
   473         -		[ checksha(`src.handle, q, 256, ip, username, cred) ] -- most common
   474         -		[ checksha(`src.handle, q, 512, ip, username, cred) ] -- most secure
   475         -		[ checksha(`src.handle, q, 384, ip, username, cred) ] -- weird
   476         -		[ checksha(`src.handle, q, 224, ip, username, cred) ] -- weirdest
          503  +		[ checksha(`src, 256, ip, username, cred) ] -- most common
          504  +		[ checksha(`src, 512, ip, username, cred) ] -- most secure
          505  +		[ checksha(`src, 384, ip, username, cred) ] -- weird
          506  +		[ checksha(`src, 224, ip, username, cred) ] -- weirdest
   477    507   
   478    508   		-- TODO: check pbkdf2-hmac
   479    509   		-- TODO: check OTP
   480    510   		return 0
   481    511   	end];
          512  +
          513  +	actor_stats = [terra(src: &lib.store.source, uid: uint64)
          514  +		var r = queries.actor_stats.exec(src, uid)
          515  +		if r.sz == 0 then lib.bail('error fetching actor stats!') end
          516  +		var s: lib.store.actor_stats
          517  +		s.posts = r:int(uint64, 0, 0)
          518  +		s.follows = r:int(uint64, 0, 1)
          519  +		s.followers = r:int(uint64, 0, 2)
          520  +		s.mutuals = r:int(uint64, 0, 3)
          521  +		return s
          522  +	end];
   482    523   
   483    524   	actor_session_fetch = [terra(
   484    525   		src: &lib.store.source,
   485    526   		aid: uint64,
   486    527   		ip : lib.store.inet
   487    528   	): { lib.stat(lib.store.auth), lib.mem.ptr(lib.store.actor) }
   488    529   		var r = queries.actor_session_fetch.exec(src, aid, ip)
................................................................................
   493    534   
   494    535   			var a = row_to_actor(&r, 0)
   495    536   			a.ptr.source = src
   496    537   
   497    538   			var au = [lib.stat(lib.store.auth)] { ok = true }
   498    539   			au.val.aid = aid
   499    540   			au.val.uid = a.ptr.id
   500         -			if not r:null(0,10) then -- restricted?
          541  +			if not r:null(0,12) then -- restricted?
   501    542   				au.val.privs:clear()
   502         -				(au.val.privs.post   << r:bool(0,11)) 
   503         -				(au.val.privs.edit   << r:bool(0,12))
   504         -				(au.val.privs.acct   << r:bool(0,13))
   505         -				(au.val.privs.upload << r:bool(0,14))
   506         -				(au.val.privs.censor << r:bool(0,15))
   507         -				(au.val.privs.admin  << r:bool(0,16))
          543  +				(au.val.privs.post   << r:bool(0,13)) 
          544  +				(au.val.privs.edit   << r:bool(0,14))
          545  +				(au.val.privs.acct   << r:bool(0,15))
          546  +				(au.val.privs.upload << r:bool(0,16))
          547  +				(au.val.privs.censor << r:bool(0,17))
          548  +				(au.val.privs.admin  << r:bool(0,18))
   508    549   			else au.val.privs:fill() end
   509    550   
   510    551   			return au, a
   511    552   		end
   512    553   
   513    554   		::fail:: return [lib.stat   (lib.store.auth) ] { ok = false        },
   514    555   			            [lib.mem.ptr(lib.store.actor)] { ptr = nil, ct = 0 }
   515    556   	end];
   516    557   }
   517    558   
   518    559   return b

Modified config.lua from [7cb566b503] to [dc89401662].

    27     27   	dist      = default('parsav_dist', coalesce(
    28     28   		os.getenv('NIX_PATH')  and 'nixos',
    29     29   		os.getenv('NIX_STORE') and 'nixos',
    30     30   	''));
    31     31   	tgttrip   = default('parsav_arch_triple'); -- target triple, used in xcomp
    32     32   	tgtcpu    = default('parsav_arch_cpu'); -- target cpu, used in xcomp
    33     33   	tgthf     = u.tobool(default('parsav_arch_armhf',true)); 
           34  +	outform   = default('parsav_emit_type', 'o');
    34     35   	endian    = default('parsav_arch_endian', 'little');
    35     36   	build     = {
    36     37   		id = u.rndstr(6);
    37     38   		release = u.ingest('release');
    38     39   		when = os.date();
    39     40   	};
    40     41   	feat = {};
    41     42   	backends = defaultlist('parsav_backends', 'pgsql');
    42     43   	braingeniousmode = false;
    43     44   	embeds = {
    44     45   		{'style.css', 'text/css'};
           46  +		{'default-avatar.webp', 'image/webp'};
           47  +		{'padlock.webp', 'image/webp'};
           48  +		{'warn.webp', 'image/webp'};
    45     49   	};
    46     50   }
    47     51   if u.ping '.fslckout' or u.ping '_FOSSIL_' then
    48     52   	if u.ping '_FOSSIL_' then default_os = 'windows' end
    49     53   	conf.build.branch = u.exec { 'fossil', 'branch', 'current' }
    50     54   	conf.build.checkout = (u.exec { 'fossil', 'sql',
    51     55   		[[select value from localdb.vvar where name = 'checkout-hash']]

Modified http.t from [e5e590a634] to [654249752e].

     1      1   -- vim: ft=terra
     2      2   local m = {}
     3      3   local util = dofile('common.lua')
     4      4   
     5      5   m.method = lib.enum { 'get', 'post', 'head', 'options', 'put', 'delete' }
            6  +m.mime = lib.enum {
            7  +	'html'; -- default
            8  +	'json';
            9  +	'mkdown';
           10  +	'text';
           11  +	'ansi';
           12  +	'none';
           13  +}
     6     14   
     7     15   m.findheader = terralib.externfunction('mg_http_get_header', {&lib.net.mg_http_message, rawstring} -> &lib.mem.ref(int8)) -- unfortunately necessary to access this function, as its return type conflicts with a function name
     8     16   
     9     17   struct m.header {
    10     18   	key: rawstring
    11     19   	value: rawstring
    12     20   }

Modified makefile from [2d2acfe121] to [3210eb684d].

     1      1   dl = git
     2      2   dbg-flags = $(if $(dbg),-g)
     3      3   
     4         -parsav: parsav.t config.lua pkgdata.lua
            4  +images = $(addsuffix .webp, $(basename $(wildcard static/*.svg)))
            5  +styles = $(addsuffix .css, $(basename $(wildcard static/*.scss)))
            6  +
            7  +parsav: parsav.t config.lua pkgdata.lua $(images) $(styles)
     5      8   	terra $(dbg-flags) $<
     6         -parsav.o: parsav.t config.lua pkgdata.lua
            9  +parsav.o: parsav.t config.lua pkgdata.lua $(images) $(styles)
     7     10   	env parsav_link=no terra $(dbg-flags) $<
           11  +parsav.ll: parsav.t config.lua pkgdata.lua $(images) $(styles)
           12  +	env parsav_emit_type=ll parsav_link=no terra $(dbg-flags) $<
           13  +parsav.s: parsav.ll
           14  +	llc --march=$(target) $<
           15  +
           16  +static/%.webp: static/%.png
           17  +	cwebp -q 90 $< -o $@
           18  +static/%.png: static/%.svg
           19  +	inkscape -f $< -C -d 180 -e $@
           20  +static/%.css: static/%.scss
           21  +	sassc -t compressed $< $@
     8     22   
     9     23   clean:
    10     24   	rm parsav parsav.o
    11     25   
    12     26   install: parsav
    13     27   	mkdir $(prefix)/bin
    14     28   	cp $< $(prefix)/bin/
................................................................................
    39     53   	cd lib/json-c && cmake .
    40     54   lib/json-c/libjson-c.a: lib/json-c/Makefile
    41     55   	$(MAKE) -C lib/json-c
    42     56   lib/mbedtls/library/%.a: lib/mbedtls 
    43     57   	$(MAKE) -C lib/mbedtls/library $*.a
    44     58   
    45     59   ifeq ($(dl), git)
           60  +clone = git clone --depth 1 # save time
    46     61   lib/mongoose: lib
    47         -	cd lib && git clone https://github.com/cesanta/mongoose.git
           62  +	cd lib && $(clone) https://github.com/cesanta/mongoose.git
    48     63   lib/mbedtls: lib
    49         -	cd lib && git clone https://github.com/ARMmbed/mbedtls.git
           64  +	cd lib && $(clone) https://github.com/ARMmbed/mbedtls.git
    50     65   lib/json-c: lib
    51         -	cd lib && git clone https://github.com/json-c/json-c.git
           66  +	cd lib && $(clone) https://github.com/json-c/json-c.git
    52     67   else
    53     68   lib/%: lib/%.tar.gz
    54     69   	cd lib && tar zxf $*.tar.gz
    55     70   	mv lib/$$(tar tf $< | head -n1) $@
    56     71   
    57     72   ifeq ($(dl), wget)
    58     73       dlfile = wget "$(1)" -O "$(2)"

Modified math.t from [f661c3d77e] to [573a13128c].

   145    145   		buf = buf + 1
   146    146   	end
   147    147   end
   148    148   
   149    149   terra m.b32str(a: lib.mem.ptr(uint64))
   150    150   	
   151    151   end
          152  +
          153  +terra m.decstr(val: intptr, buf: &int8): rawstring
          154  +-- works backwards to avoid copies. log10(2^64) ≈ 19.2 and we
          155  +-- need a byte for NUL so buf MUST point to THE END OF a buffer
          156  +-- at least 21 bytes long
          157  +	@buf = 0
          158  +	if val > 0 then while val > 0 do
          159  +		buf = buf - 1
          160  +		var dgt = val % 10
          161  +		val = val / 10
          162  +		@buf = 0x30 + dgt
          163  +	end else
          164  +		buf = buf - 1
          165  +		@buf = 0x30
          166  +	end
          167  +	return buf
          168  +end
          169  +
          170  +terra m.decstr_friendly(val: intptr, buf: &int8): rawstring
          171  +-- as above except needs size-28 buffers, on account of all the commas
          172  +	@buf = 0
          173  +	var dgtct: uint8 = 0
          174  +	if val > 0 then while val > 0 do
          175  +		buf = buf - 1
          176  +		var dgt = val % 10
          177  +		val = val / 10
          178  +		@buf = 0x30 + dgt
          179  +		if dgtct == 2 and val > 0 then
          180  +			buf = buf - 1 @buf = @',' 
          181  +			dgtct = 0
          182  +		else dgtct = dgtct + 1 end
          183  +	end else
          184  +		buf = buf - 1
          185  +		@buf = 0x30
          186  +	end
          187  +	return buf
          188  +end
   152    189   
   153    190   return m

Modified parsav.md from [93a3706cc3] to [eb5d145ae6].

    11     11   
    12     12   * mongoose
    13     13   * json-c
    14     14   * mbedtls
    15     15   * **postgresql backend:**
    16     16     * postgresql-libs 
    17     17   
           18  +additional build-time dependencies are necessary if you are building directly from trunk, rather than from a release tarball that includes certain build artifacts which need to be embedded in the binary:
           19  +
           20  +* inkscape, for rendering out UI graphics
           21  +* cwebp (libwebp package), for transforming inkscape PNGs to webp
           22  +* sassc, for compiling the SCSS stylesheet into its final CSS
           23  +
           24  +all builds require terra, which, unfortunately, requires installing an older version of llvm, v9 at the latest (which i develop parsav under). with any luck, your distro will be clever enough to package terra and its dependencies properly (it's trivial on nix, tho you'll need to tweak the terra expression to select a more recent llvm package); Arch Linux is one of those distros which is not so clever, and whose (AUR) terra package is totally broken. due to these unfortunate circumstances, terra is distributed not just in source form, but also in the the form of LLVM IR. distributions will also be made in the form of tarballed object code and assembly listings for various common platforms, currently including x86-32/64, arm7hf, aarch64, riscv, mips32/64, and ppc64/64le.
           25  +
           26  +i've noticed that terra (at least with llvm9) seems to get a bit cantankerous and trigger llvm to fail with bizarre errors when you try to cross-compile parsav from x86-64 to any other platform, even x86-32. i don't know if this problem exists on other architectures or in what form, but as a workaround, the current cross-compile process consists of generating LLVM IR (ostensible for x86-64, though this is in reality an architecture-independent language), and then compiling that down to an object file with llc. this is an enormous hassle; hopefully the terra people will fix this eventually.
           27  +
           28  +also note that, while parsav has a flag to build with ASAN, ASAN has proven unusable for most purposes as it routinely reports false positive buffer-heap-overflows. if you figure out how to defuckulate this, i will be overjoyed.
           29  +
    18     30   ## building
    19     31   
    20         -first, either install any missing dependencies as shared libraries, or build them as static libraries with the command `make dep.$LIBRARY`. as a shortcut, `make dep` will build all dependencies as static libraries. note that if the build system finds a static version of a librari in the `lib/` folder, it will use that instead of any system library.
           32  +first, either install any missing dependencies as shared libraries, or build them as static libraries with the command `make dep.$LIBRARY`. as a shortcut, `make dep` will build all dependencies as static libraries. note that if the build system finds a static version of a library in the `lib/` folder, it will use that instead of any system library. note that these commands require GNU make (it may be installed as `gmake` on your system), although this is a fairly soft dependency -- if you really need to build it on BSD make, you can probably translate it with a minute or so of work; you'll just have to do some of the various gmake functions' work manually. this may be worthwhile if you're packaging for a BSD.
    21     33   
    22     34   postgresql-libs must be installed systemwide, as `parsav` does not currently provide for statically compiling and linking it
    23     35   
    24     36   ## configuring
    25     37   
    26     38   the `parsav` configuration is comprised of two components: the backends list and the config store. the backends list is a simple text file that tells `parsav` which data sources to draw from. the config store is a key-value store which contains the rest of the server's configuration, and is loaded from the backends. the configuration store can be spread across the backends; backends will be checked for configuration keys according to the order in which they are listed. changes to the configuration store affect parsav in real time; you only need to restart the server if you make a change to the backend list.
    27     39   

Modified parsav.t from [41c6f93980] to [11ad9b3025].

   197    197   end)
   198    198   lib.enum = function(tbl)
   199    199   	local ty = uint8
   200    200   	if #tbl >= 2^32 then ty = uint64 -- hey, can't be too safe
   201    201   	elseif #tbl >= 2^16 then ty = uint32
   202    202   	elseif #tbl >= 2^8 then ty = uint16 end
   203    203   	local o = { t = ty }
          204  +	local strings = {}
   204    205   	for i, name in ipairs(tbl) do
   205         -		o[name] = i
          206  +		o[name] = i - 1
          207  +		strings[i] = `[lib.mem.ref(int8)]{ptr=[name], ct=[#name]}
          208  +	end
          209  +	o._str = terra(val: ty)
          210  +		var l = array([strings])
          211  +		return l[val]
   206    212   	end
   207    213   	return o
   208    214   end
   209    215   lib.set = function(tbl)
   210    216   	local bytes = math.ceil(#tbl / 8)
   211    217   	local o = {}
   212    218   	for i, name in ipairs(tbl) do o[name] = i end
................................................................................
   214    220   	local struct bit { _v: intptr _set: &set}
   215    221   	terra set:clear() for i=0,bytes do self._store[i] = 0 end end
   216    222   	terra set:fill() for i=0,bytes do self._store[i] = 0xFF end end
   217    223   	set.members = tbl
   218    224   	set.name = string.format('set<%s>', table.concat(tbl, '|'))
   219    225   	set.metamethods.__entrymissing = macro(function(val, obj)
   220    226   		if o[val] == nil then error('value ' .. val .. ' not in set') end
   221         -		return `bit { _v=[o[val] - 1], _set = &obj }
          227  +		return `bit { _v=[o[val] - 1], _set = &(obj) }
   222    228   	end)
          229  +	terra set:sz()
          230  +		var ct: intptr = 0
          231  +		for i = 0, [#tbl] do
          232  +			if (self._store[i/8] and (1 << i % 8)) ~= 0 then ct = ct + 1 end
          233  +		end
          234  +		return ct
          235  +	end
   223    236   	set.methods.dump = macro(function(self)
   224    237   		local q = quote lib.io.say('dumping set:\n') end
   225    238   		for i,v in ipairs(tbl) do
   226    239   			q = quote
   227    240   				[q]
   228    241   				if [bool](self.[v])
   229    242   					then lib.io.say([' - ' .. v .. ': true\n'])
................................................................................
   306    319   for k,v in pairs(data.view) do
   307    320   	local t = lib.tpl.mk { body = v, id = 'view/'..k }
   308    321   	data.view[k] = t
   309    322   end
   310    323   
   311    324   lib.load {
   312    325   	'srv';
          326  +	'render:nav';
          327  +	'render:login';
   313    328   	'render:profile';
   314    329   	'render:userpage';
          330  +	'render:compose';
   315    331   	'route';
   316    332   }
   317    333   
   318    334   do
   319    335   	local p = string.format('parsav: %s\nbuilt on %s\n', config.build.str, config.build.when)
   320    336   	terra version() lib.io.send(1, p, [#p]) end
   321    337   end
................................................................................
   427    443   
   428    444   if bflag('dump-config','C') then
   429    445   	print(util.dump(config))
   430    446   	os.exit(0)
   431    447   end
   432    448   
   433    449   local holler = print
   434         -local out = config.exe and 'parsav' or 'parsav.o'
   435         -local linkargs = {}
          450  +local out = config.exe and 'parsav' or ('parsav.' .. config.outform)
          451  +local linkargs = {'-O4'}
   436    452   
   437    453   if bflag('quiet','q') then holler = function() end end
   438    454   if bflag('asan','s') then linkargs[#linkargs+1] = '-fsanitize=address' end
   439    455   if bflag('lsan','S') then linkargs[#linkargs+1] = '-fsanitize=leak' end
   440    456   
   441    457   if config.posix then
   442    458   	linkargs[#linkargs+1] = '-pthread'

Added render/compose.t version [7a7e8f43ac].

            1  +-- vim: ft=terra
            2  +local terra 
            3  +render_compose(co: &lib.srv.convo, edit: &lib.store.post)
            4  +	var target, tgtlen = co:getv('to')
            5  +	var form: data.view.compose
            6  +	if edit == nil then
            7  +		form = data.view.compose {
            8  +			content = lib.coalesce(target, '');
            9  +			acl = lib.trn(target == nil, 'all', 'mentioned'); -- TODO default acl setting?
           10  +			handle = co.who.handle;
           11  +		}
           12  +	end
           13  +	var cotxt = form:tostr() defer cotxt:free()
           14  +
           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;
           21  +	}
           22  +
           23  +	var hdrs = array(
           24  +		lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' }
           25  +	)
           26  +	doc:send(co.con,200,[lib.mem.ptr(lib.http.header)] {ct = 1, ptr = &hdrs[0]})
           27  +end
           28  +
           29  +return render_compose

Added render/login.t version [0d69ec17a3].

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

Added render/nav.t version [2d4aa38bec].

            1  +-- vim: ft=terra
            2  +local terra 
            3  +render_nav(co: &lib.srv.convo)
            4  +	var t: lib.str.acc t:init(64)
            5  +	if co.who ~= nil or co.srv.cfg.pol_sec == lib.srv.secmode.public then
            6  +		t:lpush('<a href="/">timeline</a>')
            7  +	end
            8  +	if co.who ~= nil then
            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>')
           11  +	else
           12  +		t:lpush('<a href="/login">log in</a>')
           13  +	end
           14  +	return t:finalize()
           15  +end
           16  +return render_nav

Modified render/profile.t from [a405db9158] to [ecfc7ba460].

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

Modified render/userpage.t from [052285d84c] to [cdf1e65d22].

     3      3   render_userpage(co: &lib.srv.convo, actor: &lib.store.actor)
     4      4   	var ti: lib.str.acc defer ti:free()
     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         -	var pftxt = lib.render.profile(actor) defer pftxt:free()
           10  +	var pftxt = lib.render.profile(co,actor) defer pftxt:free()
    11     11   
    12     12   	var doc = data.view.docskel {
    13     13   		instance = co.srv.cfg.instance.ptr;
    14     14   		title = ti.buf;
    15     15   		body = pftxt.ptr;
    16     16   		class = 'profile';
           17  +		navlinks = co.navbar.ptr;
    17     18   	}
    18     19   
    19     20   	var hdrs = array(
    20     21   		lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' }
    21     22   	)
    22     23   	doc:send(co.con,200,[lib.mem.ptr(lib.http.header)] {ct = 1, ptr = &hdrs[0]})
    23     24   end
    24     25   
    25     26   return render_userpage

Modified route.t from [70d0da6b8e] to [d7d680b0a3].

    53     53   		co:complain(404, 'no such user', 'no user by that ID is known to this instance')
    54     54   		return
    55     55   	end
    56     56   	defer actor:free()
    57     57   
    58     58   	lib.render.userpage(co, actor.ptr)
    59     59   end
           60  +
           61  +terra http.login_form(co: &lib.srv.convo, meth: method.t)
           62  +	if meth == method.get then
           63  +		-- request a username
           64  +		lib.render.login(co, nil, nil, nil)
           65  +	elseif meth == method.post then
           66  +		var usn, usnl = co:postv('user')
           67  +		lib.dbg('got name ',{usn,usnl})
           68  +		lib.io.fmt('name len %llu\n',usnl)
           69  +		var am, aml = co:postv('authmethod')
           70  +		var chrs, chrsl = co:postv('response')
           71  +		var cs, authok = co.srv:actor_auth_how(co.peer, usn)
           72  +		var act = co.srv:actor_fetch_xid([lib.mem.ptr(int8)] {
           73  +			ptr = usn, ct = usnl
           74  +		})
           75  +		if authok == false then
           76  +			lib.render.login(co, nil, nil, 'access denied')
           77  +			return
           78  +		end
           79  +		var fakeact = false
           80  +		var fakeactor: lib.store.actor
           81  +		if act.ptr == nil then
           82  +			-- the user is known to us but has not yet claimed an
           83  +			-- account on the server. create a template for the
           84  +			-- account that will be created once they log in
           85  +			fakeact = true
           86  +			fakeactor = lib.store.actor {
           87  +				id = 0, handle = usn, nym = usn;
           88  +				origin = 0, bio = nil;
           89  +				key = [lib.mem.ptr(uint8)] {ptr=nil, ct=0}
           90  +			}
           91  +			act.ct = 1
           92  +			act.ptr = &fakeactor
           93  +			act.ptr.rights = lib.store.rights_default()
           94  +		end
           95  +		if am == nil then
           96  +			-- pick an auth method
           97  +			lib.render.login(co, act.ptr, &cs, nil)
           98  +		else var aid: uint64 = 0
           99  +			lib.dbg('authentication attempt beginning')
          100  +			-- attempt login with provided method
          101  +			if lib.str.ncmp('pw', am, lib.math.biggest(2,aml)) == 0 and chrs ~= nil then
          102  +				aid = co.srv:actor_auth_pw(co.peer,
          103  +					[lib.mem.ptr(int8)]{ptr=usn,ct=usnl},
          104  +					[lib.mem.ptr(int8)]{ptr=chrs,ct=chrsl})
          105  +			elseif lib.str.ncmp('otp', am, lib.math.biggest(2,aml)) == 0 and chrs ~= nil then
          106  +				lib.dbg('using otp auth')
          107  +				-- ··· --
          108  +			else
          109  +				lib.dbg('invalid auth method')
          110  +			end
          111  +
          112  +			lib.io.fmt('login got aid = %llu\n', aid)
          113  +			-- error out
          114  +			if aid == 0 then
          115  +				lib.render.login(co, nil, nil, 'authentication failure')
          116  +			else
          117  +				var sesskey: int8[lib.session.maxlen + #lib.session.cookiename + #"=; Path=/" + 1]
          118  +				do var p = &sesskey[0]
          119  +					p = lib.str.ncpy(p, [lib.session.cookiename .. '='], [#lib.session.cookiename + 1])
          120  +					p = p + lib.session.cookie_gen(co.srv.cfg.secret, aid, lib.osclock.time(nil), p)
          121  +					lib.dbg('sending cookie',&sesskey[0])
          122  +					p = lib.str.ncpy(p, '; Path=/', 9)
          123  +				end
          124  +				co:reroute_cookie('/', &sesskey[0])
          125  +			end
          126  +		end
          127  +		if act.ptr ~= nil and fakeact == false then act:free() end
          128  +	else
          129  +		::wrongmeth:: co:complain(405, 'method not allowed', 'that method is not meaningful for this endpoint') do return end
          130  +	end
          131  +	return
          132  +end
          133  +
          134  +terra http.post_compose(co: &lib.srv.convo, meth: method.t)
          135  +	if meth == method.get then
          136  +		lib.render.compose(co, nil)
          137  +	elseif meth == method.post then
          138  +		if co.who.rights.powers.post() == false then
          139  +			co:complain(401,'insufficient privileges','you lack the <strong>post</strong> power and cannot perform this action') return
          140  +		end
          141  +
          142  +	end
          143  +end
    60    144   
    61    145   do local branches = quote end
    62    146   	local filename, flen = symbol(&int8), symbol(intptr)
    63    147   	local page = symbol(lib.http.page)
    64    148   	local send = label()
    65    149   	local storage = data.stmap
    66    150   	for i,e in ipairs(config.embeds) do local id,mime = e[1],e[2]
................................................................................
    86    170   		}
    87    171   		[branches]
    88    172   		do return false end
    89    173   		::[send]:: page:send(co.con) return true
    90    174   	end
    91    175   end
    92    176   
    93         -http.static_content:printpretty()
          177  +
          178  +terra http.local_avatar(co: &lib.srv.convo, handle: lib.mem.ptr(int8))
          179  +	-- TODO retrieve user avatars
          180  +	co:reroute('/s/default-avatar.webp')
          181  +end
    94    182   
    95    183   -- entry points
    96    184   terra r.dispatch_http(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t)
          185  +	lib.dbg('handling URI of form ', {uri.ptr,uri.ct})
          186  +	co.navbar = lib.render.nav(co)
          187  +	-- some routes are non-hierarchical, and can be resolved with a simple strcmp
          188  +	-- we run through those first before giving up and parsing the URI
    97    189   	if uri.ptr[0] ~= @'/' then
    98    190   		co:complain(404, 'what the hell', 'how did you do that')
          191  +		return
          192  +	elseif uri.ct == 1 then -- root
          193  +		lib.io.fmt('root directory, aid is %llu\n', co.aid)
          194  +		if (co.srv.cfg.pol_sec == lib.srv.secmode.private or
          195  +		   co.srv.cfg.pol_sec == lib.srv.secmode.lockdown) and co.aid == 0 then
          196  +		   http.login_form(co, meth)
          197  +		else
          198  +			-- FIXME display home screen
          199  +			goto notfound
          200  +		end
          201  +		return
    99    202   	elseif uri.ptr[1] == @'@' then
   100    203   		http.actor_profile_xid(co, uri, meth)
          204  +		return
   101    205   	elseif uri.ptr[1] == @'s' and uri.ptr[2] == @'/' and uri.ct > 3 then
   102    206   		if meth ~= method.get then goto wrongmeth end
   103    207   		if not http.static_content(co, uri.ptr + 3, uri.ct - 3) then goto notfound end
   104         -	else
          208  +		return
          209  +	elseif lib.str.ncmp('/avi/', uri.ptr, 5) == 0 then
          210  +		http.local_avatar(co, [lib.mem.ptr(int8)] {ptr = uri.ptr + 5, ct = uri.ct - 5})
          211  +		return
          212  +	elseif lib.str.ncmp('/compose', uri.ptr, lib.math.biggest(uri.ct,8)) == 0 then
          213  +		if co.aid == 0 then co:reroute('/login') return end
          214  +		http.post_compose(co,meth)
          215  +		return
          216  +	elseif lib.str.ncmp('/login', uri.ptr, lib.math.biggest(uri.ct,6)) == 0 then
          217  +		if co.aid == 0
          218  +			then http.login_form(co, meth)
          219  +			else co:reroute('/')
          220  +		end
          221  +		return
          222  +	elseif lib.str.ncmp('/logout', uri.ptr, lib.math.biggest(uri.ct,7)) == 0 then
          223  +		if co.aid == 0
          224  +			then goto notfound
          225  +			else co:reroute_cookie('/','auth=; Path=/')
          226  +		end
          227  +		return
          228  +	else -- hierarchical routes
   105    229   		var path = lib.http.hier(uri) defer path:free()
   106    230   		if path.ptr[0]:cmp(lib.str.lit('user')) then
   107    231   			http.actor_profile_uid(co, path, meth)
   108    232   		else goto notfound end
          233  +		return
   109    234   	end
   110    235   
   111         -	::wrongmeth:: co:complain(405, 'method not allowed', 'that method is not meaningful for this path') do return end
          236  +	::wrongmeth:: co:complain(405, 'method not allowed', 'that method is not meaningful for this endpoint') do return end
   112    237   	::notfound:: co:complain(404, 'not found', 'no such resource available') do return end
   113    238   end

Modified schema.sql from [636689e0dd] to [a3359b8b76].

     1      1   \prompt 'domain name: ' domain
     2      2   \prompt 'instance name: ' inst
     3      3   \prompt 'bind to socket: ' bind
     4         -\qecho 'by default, parsav tracks rights on its own. you can override this later by replacing the rights table with a view, but you''ll then need to set appropriate rules on the view to allow administrators to modify rights from the web UI, or set the rights-readonly flag in the config table to true. for now, enter the name of an actor who will be granted full rights when she logs in.'
     5         -\prompt 'admin actor: ' admin
            4  +\qecho 'how locked down should this server be? public = anyone can see public timeline and tweets, private = anyone can see tweets with a link but login required for everything else, lockdown = login required for all activities, isolate = like lockdown but with federation protocols completely disabled'
            5  +\prompt 'security mode: ' secmode
            6  +\qecho 'should user self-registration be allowed? yes or no'
            7  +\prompt 'registration: ' regpol
            8  +\qecho 'by default, parsav tracks rights on its own. you can override this later by replacing the rights table with a view, but you''ll then need to set appropriate rules on the view to allow administrators to modify rights from the web UI, or set the rights-readonly flag in the config table to true. for now, enter the name of an actor who will be granted full rights when she logs in and identified as the server owner.'
            9  +\prompt 'master actor: ' admin
     6     10   \qecho 'you will need to create an authentication view named parsav_auth mapping your user database to something parsav can understand; see auth.sql for an example.'
     7     11   
     8     12   begin;
     9     13   
    10     14   drop table if exists parsav_config;
    11     15   create table if not exists parsav_config (
    12         -	key text primary key,
           16  +	key   text primary key,
    13     17   	value text
    14     18   );
    15     19   
    16     20   insert into parsav_config (key,value) values
    17     21   	('bind',:'bind'),
    18     22   	('domain',:'domain'),
    19     23   	('instance-name',:'inst'),
    20         -	('administrator',:'admin'),
           24  +	('policy-security',:'secmode'),
           25  +	('policy-self-register',:'regpol'),
           26  +	('master',:'admin'),
    21     27   	('server-secret', encode(
    22     28   			digest(int8send((2^63 * (random()*2 - 1))::bigint),
    23     29   		'sha512'), 'base64'));
    24     30   
    25     31   -- note that valid ids should always > 0, as 0 is reserved for null
    26     32   -- on the client side, vastly simplifying code
    27     33   drop table if exists parsav_servers cascade;
    28     34   create table parsav_servers (
    29         -	id bigint primary key default (1+random()*(2^63-1))::bigint,
           35  +	id     bigint primary key default (1+random()*(2^63-1))::bigint,
    30     36   	domain text not null,
    31         -	key bytea
           37  +	key    bytea,
           38  +	parsav boolean -- whether to use parsav protocol extensions
    32     39   );
           40  +
    33     41   drop table if exists parsav_actors cascade;
    34     42   create table parsav_actors (
    35         -	id bigint primary key default (1+random()*(2^63-1))::bigint,
    36         -	nym text,
    37         -	handle text not null, -- nym [@handle@origin] 
    38         -	origin bigint references parsav_servers(id)
           43  +	id        bigint primary key default (1+random()*(2^63-1))::bigint,
           44  +	nym       text,
           45  +	handle    text not null, -- nym [@handle@origin] 
           46  +	origin    bigint references parsav_servers(id)
    39     47   		on delete cascade, -- null origin = local actor
    40         -	bio text,
    41         -	rank smallint not null default 0,
    42         -	quota integer not null default 1000,
    43         -	key bytea, -- private if localactor; public if remote
           48  +	bio       text,
           49  +	avataruri text, -- null if local
           50  +	rank      smallint not null default 0,
           51  +	quota     integer not null default 1000,
           52  +	key       bytea, -- private if localactor; public if remote
           53  +	title     text
    44     54   	
    45     55   	unique (handle,origin)
    46     56   );
    47     57   
    48     58   drop table if exists parsav_rights cascade;
    49     59   create table parsav_rights (
    50     60   	key text,
................................................................................
    63     73   		('censor',true),
    64     74   		('suspend',true),
    65     75   		('rebrand',true)
    66     76   	) as a;
    67     77   
    68     78   drop table if exists parsav_posts cascade;
    69     79   create table parsav_posts (
    70         -	id bigint primary key default (1+random()*(2^63-1))::bigint,
    71         -	author bigint references parsav_actors(id)
           80  +	id         bigint primary key default (1+random()*(2^63-1))::bigint,
           81  +	author     bigint references parsav_actors(id)
    72     82   		on delete cascade,
    73         -	subject text,
    74         -	body text,
    75         -	posted timestamp not null,
           83  +	subject    text,
           84  +	acl        text not null default 'all', -- just store the script raw 🤷
           85  +	body       text,
           86  +	posted     timestamp not null,
    76     87   	discovered timestamp not null,
    77         -	scope smallint not null,
    78         -	convo bigint, parent bigint,
    79         -	circles bigint[], mentions bigint[]
           88  +	scope      smallint not null,
           89  +	convo      bigint,
           90  +	parent     bigint,
           91  +	circles    bigint[],
           92  +	mentions   bigint[]
    80     93   );
    81     94   
    82     95   drop table if exists parsav_conversations cascade;
    83     96   create table parsav_conversations (
    84         -	id bigint primary key default (1+random()*(2^63-1))::bigint,
    85         -	uri text not null,
           97  +	id         bigint primary key default (1+random()*(2^63-1))::bigint,
           98  +	uri        text      not null,
    86     99   	discovered timestamp not null,
    87         -	head bigint references parsav_posts(id)
          100  +	head       bigint references parsav_posts(id)
    88    101   );
    89    102   
    90    103   drop table if exists parsav_rels cascade;
    91    104   create table parsav_rels (
    92    105   	relator bigint references parsav_actors(id)
    93    106   		on delete cascade, -- e.g. follower
    94    107   	relatee bigint references parsav_actors(id)
    95         -		on delete cascade, -- e.g. follower
    96         -	kind smallint, -- e.g. follow, block, mute
          108  +		on delete cascade, -- e.g. followed
          109  +	kind    smallint, -- e.g. follow, block, mute
    97    110   
    98    111   	primary key (relator, relatee, kind)
    99    112   );
   100    113   
   101    114   drop table if exists parsav_acts cascade;
   102    115   create table parsav_acts (
   103         -	id bigint primary key default (1+random()*(2^63-1))::bigint,
   104         -	kind text not null, -- like, react, so on
   105         -	time timestamp not null,
   106         -	actor bigint references parsav_actors(id)
          116  +	id      bigint primary key default (1+random()*(2^63-1))::bigint,
          117  +	kind    text not null, -- like, react, so on
          118  +	time    timestamp not null default now(),
          119  +	actor   bigint references parsav_actors(id)
   107    120   		on delete cascade,
   108    121   	subject bigint -- may be post or act, depending on kind
   109    122   );
   110    123   
   111    124   drop table if exists parsav_log cascade;
   112    125   create table parsav_log (
   113    126   	-- accesses are tracked for security & sending delete acts
   114         -	id bigint primary key default (1+random()*(2^63-1))::bigint,
   115         -	time timestamp not null,
          127  +	id    bigint primary key default (1+random()*(2^63-1))::bigint,
          128  +	time  timestamp not null default now(),
   116    129   	actor bigint references parsav_actors(id)
   117    130   		on delete cascade,
   118         -	post bigint not null
          131  +	post  bigint not null
          132  +);
          133  +
          134  +drop table if exists parsav_attach cascade;
          135  +create table parsav_attach (
          136  +	id          bigint primary key default (1+random()*(2^63-1))::bigint,
          137  +	birth       timestamp not null default now(),
          138  +	content     bytea not null,
          139  +	mime        text, -- null if unknown, will be reported as x-octet-stream
          140  +	description text,
          141  +	parent      bigint -- post id, or userid for avatars
          142  +);
          143  +
          144  +drop table if exists parsav_circles cascade;
          145  +create table parsav_circles (
          146  +	id          bigint primary key default (1+random()*(2^63-1))::bigint,
          147  +	owner       bigint not null references parsav_actors(id),
          148  +	name        text not null,
          149  +	members     bigint[] not null default array[],
          150  +
          151  +	unique (owner,name)
          152  +);
          153  +
          154  +drop table if exists parsav_rooms cascade;
          155  +create table parsav_rooms (
          156  +	id          bigint primary key default (1+random()*(2^63-1))::bigint,
          157  +	origin		bigint references parsav_servers(id),
          158  +	name		text not null,
          159  +	description text not null,
          160  +	policy      smallint not null
          161  +);
          162  +
          163  +drop table if exists parsav_room_members cascade;
          164  +create table parsav_room_members (
          165  +	room   bigint references parsav_rooms(id),
          166  +	member bigint references parsav_actors(id),
          167  +	rank   smallint not null default 0,
          168  +	admin  boolean not null default false, -- non-admins with rank can only moderate + invite
          169  +	title  text -- admin-granted title like reddit flair
          170  +);
          171  +
          172  +drop table if exists parsav_invites cascade;
          173  +create table parsav_invites (
          174  +	id          bigint primary key default (1+random()*(2^63-1))::bigint,
          175  +	-- when a user is created from an invite, the invite is deleted and the invite
          176  +	-- ID becomes the user ID. privileges granted on the invite ID during the invite
          177  +	-- process are thus inherited by the user
          178  +	handle text, -- admin can lock invite to specific handle
          179  +	rank   smallint not null default 0,
          180  +	quota  integer not null  default 1000
          181  +};
          182  +
          183  +drop table if exists parsav_interventions cascade;
          184  +create table parsav_interventions (
          185  +	id     bigint primary key default (1+random()*(2^63-1))::bigint,
          186  +	issuer bigint references parsav_actors(id) not null,
          187  +	scope  bigint, -- can be null or room for local actions
          188  +	nature smallint not null, -- silence, suspend, disemvowel, etc
          189  +	victim bigint not null, -- could potentially target group as well
          190  +	expire timestamp -- auto-expires if set
   119    191   );
          192  +
   120    193   end;

Modified session.t from [58f0eab21d] to [e8a79576f0].

     3      3   -- are tracked by storing an encrypted cookie which contains an authid,
     4      4   -- a login epoch time, and a truncated hmac code authenticating both, all
     5      5   -- encoded using Shorthand. we need functions to generate and parse these
     6      6   
     7      7   local m = {
     8      8   	maxlen = lib.math.shorthand.maxlen*3 + 2;
     9      9   	maxage = 2 * 60 * 60; -- 2 hours
           10  +	cookiename = 'auth';
    10     11   }
    11     12   
    12     13   terra m.cookie_gen(secret: lib.mem.ptr(int8), authid: uint64, time: uint64, out: &int8): intptr
    13     14   	var ptr = out
    14     15   	ptr = ptr + lib.math.shorthand.gen(authid, ptr)
    15     16   	@ptr = @'.' ptr = ptr + 1
    16     17   	ptr = ptr + lib.math.shorthand.gen(time, ptr)

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

     1      1   -- vim: ft=terra
     2      2   local util = dofile 'common.lua'
     3         -
            3  +local secmode = lib.enum { 'public', 'private', 'lockdown', 'isolate' }
     4      4   local struct srv
     5      5   local struct cfgcache {
     6      6   	secret: lib.mem.ptr(int8)
     7      7   	instance: lib.mem.ptr(int8)
     8      8   	overlord: &srv
            9  +	pol_sec: secmode.t
           10  +	pol_reg: bool
     9     11   }
    10     12   local struct srv {
    11     13   	sources: lib.mem.ptr(lib.store.source)
    12     14   	webmgr: lib.net.mg_mgr
    13     15   	webcon: &lib.net.mg_connection
    14     16   	cfg: cfgcache
    15     17   }
................................................................................
    68     70   
    69     71   local struct convo {
    70     72   	srv: &srv
    71     73   	con: &lib.net.mg_connection
    72     74   	msg: &lib.net.mg_http_message
    73     75   	aid: uint64 -- 0 if logged out
    74     76   	who: &lib.store.actor -- who we're logged in as, if aid ~= 0
           77  +	peer: lib.store.inet
           78  +	reqtype: lib.http.mime.t -- negotiated content type
           79  +-- cache
           80  +	navbar: lib.mem.ptr(int8)
           81  +-- private
           82  +	varbuf: lib.mem.ptr(int8)
           83  +	vbofs: &int8
    75     84   }
    76     85   
    77     86   -- this is unfortunately necessary to work around a terra bug
    78     87   -- it can't seem to handle forward-declarations of structs in C
    79     88   
    80     89   local getpeer
    81     90   do local struct strucheader {
................................................................................
    84     93   		peer: lib.net.mg_addr
    85     94   	}
    86     95   	terra getpeer(con: &lib.net.mg_connection)
    87     96   		return [&strucheader](con).peer
    88     97   	end
    89     98   end
    90     99   
          100  +terra convo:reroute_cookie(dest: rawstring, cookie: rawstring)
          101  +	var hdrs = array(
          102  +		lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' },
          103  +		lib.http.header { key = 'Location',     value = dest },
          104  +		lib.http.header { key = 'Set-Cookie',   value = cookie }
          105  +	)
          106  +
          107  +	var body = data.view.docskel {
          108  +		instance = self.srv.cfg.instance.ptr;
          109  +		title = 'rerouting';
          110  +		body = 'you are being redirected';
          111  +		class = 'error';
          112  +		navlinks = '';
          113  +	}
          114  +
          115  +	body:send(self.con, 303, [lib.mem.ptr(lib.http.header)] {
          116  +		ptr = &hdrs[0], ct = [hdrs.type.N] - lib.trn(cookie == nil,1,0)
          117  +	})
          118  +end
          119  +
          120  +terra convo:reroute(dest: rawstring) self:reroute_cookie(dest,nil) end
          121  + 
    91    122   terra convo:complain(code: uint16, title: rawstring, msg: rawstring)
    92    123   	var hdrs = array(lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' })
    93    124   
    94    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()
    95    127   	var body = data.view.docskel {
    96    128   		instance = self.srv.cfg.instance.ptr;
    97    129   		title = ti.buf;
    98         -		body = msg;
          130  +		body = bo.buf;
    99    131   		class = 'error';
          132  +		navlinks = lib.coalesce(self.navbar.ptr, '');
   100    133   	}
   101    134   
   102    135   	if body.body == nil then
   103    136   		body.body = "i'm sorry, dave. i can't let you do that"
   104    137   	end
   105    138   
   106    139   	body:send(self.con, code, [lib.mem.ptr(lib.http.header)] {
   107    140   		ptr = &hdrs[0], ct = [hdrs.type.N]
   108    141   	})
   109    142   end
          143  +
          144  +-- CALL ONLY ONCE PER VAR
          145  +terra convo:postv(name: rawstring)
          146  +	if self.varbuf.ptr == nil then
          147  +		self.varbuf = lib.mem.heapa(int8, self.msg.body.len + self.msg.query.len)
          148  +		self.vbofs = self.varbuf.ptr
          149  +	end
          150  +	var o = lib.net.mg_http_get_var(&self.msg.body, name, self.vbofs, self.varbuf.ct - (self.vbofs - self.varbuf.ptr))
          151  +	if o > 0 then
          152  +		var r = self.vbofs
          153  +		self.vbofs = self.vbofs + o
          154  +		return r, o
          155  +	else return nil, 0 end
          156  +end
          157  +
          158  +terra convo:getv(name: rawstring)
          159  +	if self.varbuf.ptr == nil then
          160  +		self.varbuf = lib.mem.heapa(int8, self.msg.query.len + self.msg.body.len)
          161  +		self.vbofs = self.varbuf.ptr
          162  +	end
          163  +	var o = lib.net.mg_http_get_var(&self.msg.query, name, self.vbofs, self.varbuf.ct - (self.vbofs - self.varbuf.ptr))
          164  +	if o > 0 then
          165  +		var r = self.vbofs
          166  +		self.vbofs = self.vbofs + o
          167  +		return r, o
          168  +	else return nil, 0 end
          169  +end
   110    170   
   111    171   local urimatch = macro(function(uri, ptn)
   112    172   	return `lib.net.mg_globmatch(ptn, [#ptn], uri.ptr, uri.ct+1)
   113    173   end)
   114    174   
   115    175   local route = {} -- these are defined in route.t, as they need access to renderers
   116    176   terra route.dispatch_http ::  {&convo, lib.mem.ptr(int8), lib.http.method.t} -> {}
          177  +
          178  +local mimetypes = {
          179  +	{'html', 'text/html'};
          180  +	{'json', 'application/json'};
          181  +	{'mkdown', 'text/markdown'};
          182  +	{'text', 'text/plain'};
          183  +	{'ansi', 'text/x-ansi'};
          184  +}
          185  +
          186  +local mimevar = symbol(lib.mem.ref(int8))
          187  +local mimeneg = `lib.http.mime.none
          188  +
          189  +for i, t in ipairs(mimetypes) do
          190  +	local name, mime = t[1], t[2]
          191  +	mimeneg = quote
          192  +		var ret: lib.http.mime.t
          193  +		if lib.str.ncmp(mimevar.ptr, mime, lib.math.biggest(mimevar.ct, [#mime])) == 0 then
          194  +			ret = [lib.http.mime[name]]
          195  +		else ret = [mimeneg] end
          196  +	in ret end
          197  +end
   117    198   
   118    199   local handle = {
   119    200   	http = terra(con: &lib.net.mg_connection, event: int, p: &opaque, ext: &opaque)
   120    201   		var server = [&srv](ext)
   121    202   		var mgpeer = getpeer(con)
   122    203   		var peer = lib.store.inet { port = mgpeer.port; }
   123    204   		if mgpeer.is_ip6 then peer.pv = 6 else peer.pv = 4 end
................................................................................
   128    209   		end
   129    210   		-- the peer property is currently broken and there is precious
   130    211   		-- little i can do about this -- it always reports a peer v4 IP
   131    212   		-- of 0.0.0.0, altho the port seems to come through correctly.
   132    213   		-- for now i'm leaving it as is, but note that netmask restrictions
   133    214   		-- WILL NOT WORK until upstream gets its shit together. FIXME
   134    215   
          216  +		-- needs to check for an X-Forwarded-For header from nginx and
          217  +		-- use that instead of the peer iff peer is ::1/127.1 FIXME
          218  +		-- maybe also haproxy support?
          219  +
   135    220   		switch event do
   136    221   			case lib.net.MG_EV_HTTP_MSG then
   137    222   				lib.dbg('routing HTTP request')
   138    223   				var msg = [&lib.net.mg_http_message](p)
   139    224   				var co = convo {
   140    225   					con = con, srv = server, msg = msg;
   141         -					aid = 0, who = nil;
   142         -				}
          226  +					aid = 0, who = nil, peer = peer;
          227  +					reqtype = lib.http.mime.none;
          228  +				} co.varbuf.ptr = nil
          229  +				  co.navbar.ptr = nil
          230  +
          231  +				-- first, check for an accept header. if it's there, we need to
          232  +				-- iterate over the values and pick the highest-priority one
          233  +				do var acc = lib.http.findheader(msg, 'Accept')
          234  +					-- TODO handle q-value
          235  +					if acc.ptr ~= nil then
          236  +						var [mimevar] = [lib.mem.ref(int8)] { ptr = acc.ptr }
          237  +						var i = 0 while i < acc.ct do
          238  +							if acc.ptr[i] == @',' or acc.ptr[i] == @';' then
          239  +								mimevar.ct = (acc.ptr+i) - mimevar.ptr
          240  +								var t = [mimeneg]
          241  +								if t ~= lib.http.mime.none then
          242  +									co.reqtype = t
          243  +									goto foundtype
          244  +								end
          245  +
          246  +								if acc.ptr[i] == @';' then -- fast-forward over q
          247  +									for j=i+1,acc.ct do i=j
          248  +										if acc.ptr[j] == @',' then break end
          249  +									end
          250  +								end
          251  +								
          252  +								while i < acc.ct and -- fast-forward over ws
          253  +									acc.ptr[i+1] == @' ' or
          254  +									acc.ptr[i+1] == @'\t'
          255  +								do i=i+1 end
          256  +
          257  +								mimevar.ptr = acc.ptr + i + 1
          258  +							end
          259  +							i=i+1
          260  +						end
          261  +						if co.reqtype == lib.http.mime.none then
          262  +							mimevar.ct = acc.ct - (mimevar.ptr - acc.ptr)
          263  +							co.reqtype = [mimeneg]
          264  +							if co.reqtype == lib.http.mime.none then
          265  +								co.reqtype = lib.http.mime.html
          266  +							end
          267  +						end
          268  +					else co.reqtype = lib.http.mime.html end
          269  +				::foundtype::end
   143    270   
   144    271   				-- we need to check if there's any cookies sent with the request,
   145    272   				-- and if so, whether they contain any credentials. this will be
   146    273   				-- used to set the auth parameters in the http conversation
   147    274   				var cookies_p = lib.http.findheader(msg, 'Cookie')
   148    275   				if cookies_p ~= nil then
   149    276   					var cookies = cookies_p.ptr
................................................................................
   158    285   								key.ct = (cookies + i) - key.ptr
   159    286   								val.ptr = cookies + i + 1
   160    287   							end
   161    288   							i = i + 1
   162    289   						else
   163    290   							if cookies[i] == @';' then
   164    291   								val.ct = (cookies + i) - val.ptr
   165         -								if lib.str.ncmp(key.ptr, 'auth', key.ct) == 0 then
          292  +								if lib.str.ncmp(key.ptr, lib.session.cookiename, lib.math.biggest([#lib.session.cookiename],key.ct)) == 0 then
   166    293   									goto foundcookie
   167    294   								end
   168    295   
   169    296   								i = i + 1
   170    297   								i = lib.str.ffw(cookies + i, cookies_p.ct - i) - cookies
   171    298   								key.ptr = cookies + i
   172    299   								val.ptr = nil
   173    300   							else i = i + 1 end
   174    301   						end
   175    302   					end
   176    303   					if val.ptr == nil then goto nocookie end
   177    304   					val.ct = (cookies + i) - val.ptr
   178         -					if lib.str.ncmp(key.ptr, 'auth', key.ct) ~= 0 then
          305  +					if lib.str.ncmp(key.ptr, lib.session.cookiename, lib.math.biggest([#lib.session.cookiename], key.ct)) ~= 0 then
   179    306   						goto nocookie
   180    307   					end
   181    308   					::foundcookie:: do
   182    309   						var aid = lib.session.cookie_interpret(server.cfg.secret,
   183    310   							[lib.mem.ptr(int8)]{ptr=val.ptr,ct=val.ct},
   184    311   							lib.osclock.time(nil))
   185    312   						if aid ~= 0 then co.aid = aid end
................................................................................
   204    331   					end
   205    332   					uri.ct = msg.uri.len
   206    333   				else uri.ct = urideclen end
   207    334   				lib.dbg('routing URI ', {uri.ptr, uri.ct})
   208    335   				
   209    336   				if lib.str.ncmp('GET', msg.method.ptr, msg.method.len) == 0 then
   210    337   					route.dispatch_http(&co, uri, [lib.http.method.get])
          338  +				elseif lib.str.ncmp('POST', msg.method.ptr, msg.method.len) == 0 then
          339  +					route.dispatch_http(&co, uri, [lib.http.method.post])
          340  +				elseif lib.str.ncmp('HEAD', msg.method.ptr, msg.method.len) == 0 then
          341  +					route.dispatch_http(&co, uri, [lib.http.method.head])
          342  +				elseif lib.str.ncmp('OPTIONS', msg.method.ptr, msg.method.len) == 0 then
          343  +					route.dispatch_http(&co, uri, [lib.http.method.options])
   211    344   				else
   212    345   					co:complain(400,'unknown method','you have submitted an invalid http request')
   213    346   				end
   214    347   
   215    348   				if co.aid ~= 0 then lib.mem.heapf(co.who) end
          349  +				if co.varbuf.ptr ~= nil then co.varbuf:free() end
          350  +				if co.navbar.ptr ~= nil then co.navbar:free() end
   216    351   			end
   217    352   		end
   218    353   	end;
   219    354   }
   220    355   
   221    356   local terra cfg(s: &srv, befile: rawstring)
   222    357   	lib.report('configuring backends from ', befile)
................................................................................
   292    427   	if c.sz > 0 then
   293    428   		s.sources = c:crush()
   294    429   	else
   295    430   		s.sources.ptr = nil
   296    431   		s.sources.ct = 0
   297    432   	end
   298    433   end
          434  +
          435  +terra srv:actor_stats(uid: uint64)
          436  +	var stats = lib.store.actor_stats {
          437  +		posts = 0, mutuals = 0;
          438  +		follows = 0, followers = 0;
          439  +	}
          440  +	for i=0,self.sources.ct do
          441  +		var s = self.sources.ptr[i]:actor_stats(uid)
          442  +		stats.posts     = stats.posts     + s.posts
          443  +		stats.mutuals   = stats.mutuals   + s.mutuals
          444  +		stats.followers = stats.followers + s.followers
          445  +		stats.follows   = stats.follows   + s.follows
          446  +	end
          447  +	return stats
          448  +end
   299    449   
   300    450   terra srv:actor_auth_how(ip: lib.store.inet, usn: rawstring)
   301    451   	var cs: lib.store.credset cs:clear()
          452  +	var ok = false
   302    453   	for i=0,self.sources.ct do
   303         -		var set: lib.store.credset = self.sources.ptr[i]:actor_auth_how(ip, usn)
   304         -		cs = cs + set
          454  +		var set, iok = self.sources.ptr[i]:actor_auth_how(ip, usn)
          455  +		if iok then
          456  +			cs = cs + set
          457  +			ok = iok
          458  +		end
   305    459   	end
   306         -	return cs
          460  +	return cs, ok
   307    461   end
   308    462   
   309    463   terra cfgcache.methods.load :: {&cfgcache} -> {}
   310    464   terra cfgcache:init(o: &srv)
   311    465   	self.overlord = o
   312    466   	self:load()
   313    467   end
................................................................................
   336    490   		bind = dbbind.ptr
   337    491   	else bind = '[::]:10917' end
   338    492   
   339    493   	lib.report('binding to ', bind)
   340    494   	lib.net.mg_mgr_init(&self.webmgr)
   341    495   	self.webcon = lib.net.mg_http_listen(&self.webmgr, bind, handle.http, self)
   342    496   
   343         -	var buf: int8[lib.session.maxlen]
   344         -	var len = lib.session.cookie_gen(self.cfg.secret, 9139084444658983115ULL, lib.osclock.time(nil), &buf[0])
   345         -	buf[len] = 0
   346         -	
   347         -	var authid = lib.session.cookie_interpret(self.cfg.secret, [lib.mem.ptr(int8)] {ptr=buf, ct=len}, lib.osclock.time(nil))
   348         -	lib.io.fmt('generated cookie %s -- got authid %llu\n', buf, authid)
   349         -
   350    497   	if dbbind.ptr ~= nil then dbbind:free() end
   351    498   end
   352    499   
   353    500   srv.methods.poll = terra(self: &srv)
   354    501   	lib.net.mg_mgr_poll(&self.webmgr,1000)
   355    502   end
   356    503   
................................................................................
   362    509   	end
   363    510   	self.sources:free()
   364    511   end
   365    512   
   366    513   terra cfgcache:load()
   367    514   	self.instance = self.overlord:conf_get('instance-name')
   368    515   	self.secret = self.overlord:conf_get('server-secret')
          516  +
          517  +	self.pol_reg = false
          518  +	var sreg = self.overlord:conf_get('policy-self-register')
          519  +	if sreg.ptr ~= nil then
          520  +		if lib.str.cmp(sreg.ptr, 'on') == 0
          521  +			then self.pol_reg = true
          522  +			else self.pol_reg = false
          523  +		end
          524  +	end
          525  +	sreg:free()
          526  +	
          527  +	self.pol_sec = secmode.lockdown
          528  +	var smode = self.overlord:conf_get('policy-security')
          529  +	if smode.ptr ~= nil then
          530  +		if lib.str.cmp(smode.ptr, 'public') == 0 then
          531  +			self.pol_sec = secmode.public
          532  +		elseif lib.str.cmp(smode.ptr, 'private') == 0 then
          533  +			self.pol_sec = secmode.private
          534  +		elseif lib.str.cmp(smode.ptr, 'lockdown') == 0 then
          535  +			self.pol_sec = secmode.lockdown
          536  +		elseif lib.str.cmp(smode.ptr, 'isolate') == 0 then
          537  +			self.pol_sec = secmode.isolate
          538  +		end
          539  +	end
          540  +	smode:free()
   369    541   end
   370    542   
   371    543   return {
   372    544   	overlord = srv;
   373    545   	convo = convo;
   374    546   	route = route;
          547  +	secmode = secmode;
   375    548   }

Added static/default-avatar.svg version [2764158102].

            1  +<?xml version="1.0" encoding="UTF-8" standalone="no"?>
            2  +<!-- Created with Inkscape (http://www.inkscape.org/) -->
            3  +
            4  +<svg
            5  +   xmlns:dc="http://purl.org/dc/elements/1.1/"
            6  +   xmlns:cc="http://creativecommons.org/ns#"
            7  +   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
            8  +   xmlns:svg="http://www.w3.org/2000/svg"
            9  +   xmlns="http://www.w3.org/2000/svg"
           10  +   xmlns:xlink="http://www.w3.org/1999/xlink"
           11  +   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
           12  +   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
           13  +   width="25.4mm"
           14  +   height="25.4mm"
           15  +   viewBox="0 0 25.4 25.400001"
           16  +   version="1.1"
           17  +   id="svg8"
           18  +   sodipodi:docname="default-avatar.svg"
           19  +   inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
           20  +   inkscape:export-filename="/home/lexi/dev/parsav/static/default-avatar.png"
           21  +   inkscape:export-xdpi="128"
           22  +   inkscape:export-ydpi="128">
           23  +  <defs
           24  +     id="defs2">
           25  +    <linearGradient
           26  +       id="linearGradient5138"
           27  +       inkscape:collect="always">
           28  +      <stop
           29  +         id="stop5134"
           30  +         offset="0"
           31  +         style="stop-color:#ffffff;stop-opacity:1" />
           32  +      <stop
           33  +         id="stop5136"
           34  +         offset="1"
           35  +         style="stop-color:#000000;stop-opacity:0.98260868" />
           36  +    </linearGradient>
           37  +    <linearGradient
           38  +       inkscape:collect="always"
           39  +       id="linearGradient5126">
           40  +      <stop
           41  +         style="stop-color:#ff648d;stop-opacity:0.68235296"
           42  +         offset="0"
           43  +         id="stop5122" />
           44  +      <stop
           45  +         style="stop-color:#ff628a;stop-opacity:0.52549022"
           46  +         offset="1"
           47  +         id="stop5124" />
           48  +    </linearGradient>
           49  +    <linearGradient
           50  +       inkscape:collect="always"
           51  +       id="linearGradient5075">
           52  +      <stop
           53  +         style="stop-color:#100004;stop-opacity:0"
           54  +         offset="0"
           55  +         id="stop5071" />
           56  +      <stop
           57  +         style="stop-color:#100004;stop-opacity:0.59130436"
           58  +         offset="1"
           59  +         id="stop5073" />
           60  +    </linearGradient>
           61  +    <radialGradient
           62  +       inkscape:collect="always"
           63  +       xlink:href="#linearGradient5075"
           64  +       id="radialGradient5077"
           65  +       cx="12.7"
           66  +       cy="284.29998"
           67  +       fx="12.7"
           68  +       fy="284.29998"
           69  +       r="12.7"
           70  +       gradientUnits="userSpaceOnUse" />
           71  +    <radialGradient
           72  +       inkscape:collect="always"
           73  +       xlink:href="#linearGradient5126"
           74  +       id="radialGradient5128"
           75  +       cx="47.583008"
           76  +       cy="72.722656"
           77  +       fx="47.583008"
           78  +       fy="72.722656"
           79  +       r="29.078285"
           80  +       gradientTransform="matrix(1,0,0,0.74354777,0,18.649887)"
           81  +       gradientUnits="userSpaceOnUse" />
           82  +    <linearGradient
           83  +       inkscape:collect="always"
           84  +       xlink:href="#linearGradient5138"
           85  +       id="linearGradient5132"
           86  +       x1="47.611866"
           87  +       y1="62.544083"
           88  +       x2="47.611866"
           89  +       y2="83.615517"
           90  +       gradientUnits="userSpaceOnUse"
           91  +       gradientTransform="matrix(0.26458333,0,0,0.26458333,0,271.59998)" />
           92  +    <linearGradient
           93  +       inkscape:collect="always"
           94  +       xlink:href="#linearGradient5138"
           95  +       id="linearGradient5146"
           96  +       gradientUnits="userSpaceOnUse"
           97  +       x1="47.611866"
           98  +       y1="62.544083"
           99  +       x2="47.611866"
          100  +       y2="83.615517" />
          101  +    <linearGradient
          102  +       inkscape:collect="always"
          103  +       xlink:href="#linearGradient5138"
          104  +       id="linearGradient5152"
          105  +       gradientUnits="userSpaceOnUse"
          106  +       x1="47.611866"
          107  +       y1="62.544083"
          108  +       x2="47.611866"
          109  +       y2="83.615517" />
          110  +    <mask
          111  +       maskUnits="userSpaceOnUse"
          112  +       id="mask5148">
          113  +      <path
          114  +         id="path5150"
          115  +         d="m 39.580078,61.101544 c -4.433597,0.63549 -9.840689,2.053607 -13.849609,5.18555 -8.081221,6.313436 -7.197266,18.056655 -7.197266,18.056655 h 58.099611 c 0,0 0.883952,-11.743219 -7.197268,-18.056655 -3.931143,-3.071206 -9.20211,-4.489247 -13.585935,-5.142576 -2.90603,2.777877 -5.844385,4.437505 -8.111331,4.437505 -2.278666,0 -5.237507,-1.676296 -8.158202,-4.480479 z"
          116  +         style="fill:url(#linearGradient5152);fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
          117  +         inkscape:connector-curvature="0" />
          118  +    </mask>
          119  +    <radialGradient
          120  +       inkscape:collect="always"
          121  +       xlink:href="#linearGradient5126"
          122  +       id="radialGradient843"
          123  +       gradientUnits="userSpaceOnUse"
          124  +       gradientTransform="matrix(1,0,0,0.74354777,0,18.649887)"
          125  +       cx="47.583008"
          126  +       cy="72.722656"
          127  +       fx="47.583008"
          128  +       fy="72.722656"
          129  +       r="29.078285" />
          130  +  </defs>
          131  +  <sodipodi:namedview
          132  +     id="base"
          133  +     pagecolor="#1a1a1a"
          134  +     bordercolor="#666666"
          135  +     borderopacity="1.0"
          136  +     inkscape:pageopacity="0"
          137  +     inkscape:pageshadow="2"
          138  +     inkscape:zoom="5.6"
          139  +     inkscape:cx="41.777193"
          140  +     inkscape:cy="59.103277"
          141  +     inkscape:document-units="mm"
          142  +     inkscape:current-layer="layer1"
          143  +     showgrid="false"
          144  +     units="mm"
          145  +     inkscape:window-width="1920"
          146  +     inkscape:window-height="1042"
          147  +     inkscape:window-x="0"
          148  +     inkscape:window-y="38"
          149  +     inkscape:window-maximized="0" />
          150  +  <metadata
          151  +     id="metadata5">
          152  +    <rdf:RDF>
          153  +      <cc:Work
          154  +         rdf:about="">
          155  +        <dc:format>image/svg+xml</dc:format>
          156  +        <dc:type
          157  +           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
          158  +        <dc:title />
          159  +      </cc:Work>
          160  +    </rdf:RDF>
          161  +  </metadata>
          162  +  <g
          163  +     inkscape:label="Layer 1"
          164  +     inkscape:groupmode="layer"
          165  +     id="layer1"
          166  +     transform="translate(0,-271.59998)">
          167  +    <rect
          168  +       style="fill:url(#radialGradient5077);fill-opacity:1;stroke:none;stroke-width:1.05821478;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
          169  +       id="rect4518"
          170  +       width="25.4"
          171  +       height="25.4"
          172  +       x="-2.220446e-16"
          173  +       y="271.59998" />
          174  +    <g
          175  +       id="g841"
          176  +       transform="translate(0.11032924)">
          177  +      <path
          178  +         mask="url(#mask5148)"
          179  +         transform="matrix(0.26458333,0,0,0.26458333,0,271.59998)"
          180  +         id="path5086"
          181  +         d="m 39.580078,61.101562 c -4.433597,0.635491 -9.840689,2.053587 -13.849609,5.185547 -8.081221,6.313437 -7.197266,18.056641 -7.197266,18.056641 h 58.099609 c 0,0 0.883954,-11.743204 -7.197265,-18.056641 -3.931145,-3.071199 -9.202111,-4.489236 -13.585938,-5.142578 -2.906029,2.777894 -5.844384,4.4375 -8.111328,4.4375 -2.278667,0 -5.237509,-1.67631 -8.158203,-4.480469 z"
          182  +         style="fill:url(#radialGradient843);fill-opacity:1;stroke:none;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
          183  +         inkscape:connector-curvature="0" />
          184  +      <path
          185  +         sodipodi:nodetypes="saszs"
          186  +         inkscape:connector-curvature="0"
          187  +         d="m 17.503541,281.31806 c 0.203041,-3.87199 -2.372092,-5.70473 -4.872977,-5.70473 -2.500884,0 -5.0760174,1.83274 -4.8729766,5.70473 0.2030395,3.87195 3.2371566,7.26639 4.8729766,7.26639 1.63582,0 4.669938,-3.39444 4.872977,-7.26639 z"
          188  +         style="fill:#ffb6c8;fill-opacity:0.86086958;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
          189  +         id="path5081" />
          190  +    </g>
          191  +  </g>
          192  +</svg>

Added static/padlock.svg version [a621b7bd06].

            1  +<?xml version="1.0" encoding="UTF-8" standalone="no"?>
            2  +<!-- Created with Inkscape (http://www.inkscape.org/) -->
            3  +
            4  +<svg
            5  +   xmlns:dc="http://purl.org/dc/elements/1.1/"
            6  +   xmlns:cc="http://creativecommons.org/ns#"
            7  +   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
            8  +   xmlns:svg="http://www.w3.org/2000/svg"
            9  +   xmlns="http://www.w3.org/2000/svg"
           10  +   xmlns:xlink="http://www.w3.org/1999/xlink"
           11  +   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
           12  +   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
           13  +   width="0.2in"
           14  +   height="0.2in"
           15  +   viewBox="0 0 5.0800002 5.0800002"
           16  +   version="1.1"
           17  +   id="svg8"
           18  +   inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
           19  +   sodipodi:docname="padlock.svg"
           20  +   inkscape:export-filename="/home/lexi/dev/parsav/static/padlock.png"
           21  +   inkscape:export-xdpi="240"
           22  +   inkscape:export-ydpi="240">
           23  +  <defs
           24  +     id="defs2">
           25  +    <linearGradient
           26  +       inkscape:collect="always"
           27  +       id="linearGradient884">
           28  +      <stop
           29  +         style="stop-color:#d2a7b2;stop-opacity:1"
           30  +         offset="0"
           31  +         id="stop880" />
           32  +      <stop
           33  +         style="stop-color:#bd7c8d;stop-opacity:1"
           34  +         offset="1"
           35  +         id="stop882" />
           36  +    </linearGradient>
           37  +    <linearGradient
           38  +       inkscape:collect="always"
           39  +       id="linearGradient863">
           40  +      <stop
           41  +         style="stop-color:#894657;stop-opacity:1"
           42  +         offset="0"
           43  +         id="stop859" />
           44  +      <stop
           45  +         id="stop867"
           46  +         offset="0.35023043"
           47  +         style="stop-color:#733a49;stop-opacity:1" />
           48  +      <stop
           49  +         style="stop-color:#884556;stop-opacity:1"
           50  +         offset="1"
           51  +         id="stop861" />
           52  +    </linearGradient>
           53  +    <linearGradient
           54  +       inkscape:collect="always"
           55  +       xlink:href="#linearGradient863"
           56  +       id="linearGradient865"
           57  +       x1="4.1353216"
           58  +       y1="294.38614"
           59  +       x2="4.1353216"
           60  +       y2="296.71402"
           61  +       gradientUnits="userSpaceOnUse"
           62  +       gradientTransform="translate(1.0345579e-4,-0.15310986)" />
           63  +    <radialGradient
           64  +       inkscape:collect="always"
           65  +       xlink:href="#linearGradient884"
           66  +       id="radialGradient886"
           67  +       cx="2.0557182"
           68  +       cy="292.57834"
           69  +       fx="2.0557182"
           70  +       fy="292.57834"
           71  +       r="1.4498034"
           72  +       gradientTransform="matrix(1,0,0,0.99890953,0,0.47312247)"
           73  +       gradientUnits="userSpaceOnUse" />
           74  +    <radialGradient
           75  +       inkscape:collect="always"
           76  +       xlink:href="#linearGradient884"
           77  +       id="radialGradient921"
           78  +       gradientUnits="userSpaceOnUse"
           79  +       gradientTransform="matrix(1,0,0,0.99890953,5.2916667,0.47312243)"
           80  +       cx="2.0557182"
           81  +       cy="292.57834"
           82  +       fx="2.0557182"
           83  +       fy="292.57834"
           84  +       r="1.4498034" />
           85  +    <radialGradient
           86  +       inkscape:collect="always"
           87  +       xlink:href="#linearGradient884"
           88  +       id="radialGradient925"
           89  +       gradientUnits="userSpaceOnUse"
           90  +       gradientTransform="matrix(1,0,0,0.99890953,4.6302084,-0.40803457)"
           91  +       cx="2.0557182"
           92  +       cy="292.57834"
           93  +       fx="2.0557182"
           94  +       fy="292.57834"
           95  +       r="1.4498034" />
           96  +    <radialGradient
           97  +       inkscape:collect="always"
           98  +       xlink:href="#linearGradient884"
           99  +       id="radialGradient938"
          100  +       gradientUnits="userSpaceOnUse"
          101  +       gradientTransform="matrix(0.92374945,0,0,0.92274213,0.19377986,22.690922)"
          102  +       cx="2.0557182"
          103  +       cy="292.57834"
          104  +       fx="2.0557182"
          105  +       fy="292.57834"
          106  +       r="1.4498034" />
          107  +    <radialGradient
          108  +       inkscape:collect="always"
          109  +       xlink:href="#linearGradient884"
          110  +       id="radialGradient943"
          111  +       gradientUnits="userSpaceOnUse"
          112  +       gradientTransform="matrix(1,0,0,0.99890953,-5.2916667,0.47312247)"
          113  +       cx="2.0557182"
          114  +       cy="292.57834"
          115  +       fx="2.0557182"
          116  +       fy="292.57834"
          117  +       r="1.4498034" />
          118  +  </defs>
          119  +  <sodipodi:namedview
          120  +     id="base"
          121  +     pagecolor="#4b002d"
          122  +     bordercolor="#666666"
          123  +     borderopacity="1.0"
          124  +     inkscape:pageopacity="0"
          125  +     inkscape:pageshadow="2"
          126  +     inkscape:zoom="11.2"
          127  +     inkscape:cx="25.660286"
          128  +     inkscape:cy="1.1491243"
          129  +     inkscape:document-units="mm"
          130  +     inkscape:current-layer="layer1"
          131  +     showgrid="false"
          132  +     inkscape:window-width="1920"
          133  +     inkscape:window-height="1042"
          134  +     inkscape:window-x="0"
          135  +     inkscape:window-y="38"
          136  +     inkscape:window-maximized="0"
          137  +     units="in" />
          138  +  <metadata
          139  +     id="metadata5">
          140  +    <rdf:RDF>
          141  +      <cc:Work
          142  +         rdf:about="">
          143  +        <dc:format>image/svg+xml</dc:format>
          144  +        <dc:type
          145  +           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
          146  +        <dc:title></dc:title>
          147  +      </cc:Work>
          148  +    </rdf:RDF>
          149  +  </metadata>
          150  +  <g
          151  +     inkscape:label="Layer 1"
          152  +     inkscape:groupmode="layer"
          153  +     id="layer1"
          154  +     transform="translate(0,-291.91998)">
          155  +    <path
          156  +       style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
          157  +       d="m 2.5400001,292.32047 c -1.3794528,0 -1.3389364,1.46451 -1.3389364,1.46451 v 0.44804 H 0.94474863 v 2.32802 H 4.1352514 v -2.32802 H 3.8794531 v -0.44804 c 0,0 0.04,-1.46451 -1.339453,-1.46451 z m 0,0.5209 c 0.8656493,0 0.8402588,0.9493 0.8402588,0.9493 v 0.44235 H 1.6997413 v -0.44235 c 0,0 -0.02539,-0.9493 0.8402588,-0.9493 z"
          158  +       id="path945"
          159  +       inkscape:connector-curvature="0" />
          160  +    <path
          161  +       id="path936"
          162  +       style="fill:url(#radialGradient938);fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
          163  +       d="m 3.8792355,293.78476 c 0,0 0.040321,-1.46443 -1.3391321,-1.46443 -1.3794528,0 -1.339132,1.46443 -1.339132,1.46443 v 0.75607 l 0.4987852,0.006 v -0.75607 c 0,0 -0.025302,-0.94927 0.8403468,-0.94927 0.8656493,0 0.8403469,0.94927 0.8403469,0.94927 v 0.75607 l 0.4987852,-0.006 z"
          164  +       inkscape:connector-curvature="0"
          165  +       sodipodi:nodetypes="ccccccccccc" />
          166  +    <rect
          167  +       style="fill:url(#linearGradient865);fill-opacity:1;stroke:none;stroke-width:0.69999999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
          168  +       id="rect857"
          169  +       width="3.1906431"
          170  +       height="2.3278909"
          171  +       x="0.94478196"
          172  +       y="294.23303" />
          173  +    <path
          174  +       style="fill:#281419;fill-opacity:1;stroke:#000000;stroke-width:0.09348611;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
          175  +       d="m 2.5400001,294.87847 c -0.1706687,1e-5 -0.3090221,0.13836 -0.3090249,0.30903 1.516e-4,0.1325 0.084759,0.25016 0.2103229,0.29248 v 0 l -0.00938,0.74317 h 0.108082 0.1085944 l -0.010409,-0.74317 v 0 c 0.1257675,-0.0422 0.2106132,-0.15984 0.2108396,-0.29248 -2.8e-6,-0.17067 -0.1383562,-0.30902 -0.309025,-0.30903 z"
          176  +       id="path910"
          177  +       inkscape:connector-curvature="0"
          178  +       sodipodi:nodetypes="ccccccccccc" />
          179  +  </g>
          180  +</svg>

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

            1  +$color: hsl(323,100%,65%);
            2  +%sans { font-family: "Alegreya Sans", "Open Sans", sans-serif; }
            3  +%serif { font-family: Alegreya, GaramondNo8, "Garamond Premier Pro", "Adobe Garamond", Garamond, Junicode, serif; }
            4  +%teletype { font-family: "Inconsolata LGC", Inconsolata, monospace; font-size: 12pt !important; }
            5  +
            6  +@function tone($pct) { @return adjust-color($color, $lightness: $pct) }
            7  +
            8  +body {
            9  +	@extend %sans;
           10  +	background-color: adjust-color($color, $lightness: -55%);
           11  +	color: adjust-color($color, $lightness: 25%);
           12  +	font-size: 14pt;
           13  +	margin: 0;
           14  +	padding: 0;
           15  +}
           16  +a[href] {
           17  +	color: adjust-color($color, $lightness: 10%);
           18  +	&:hover {
           19  +		color: white;
           20  +		text-shadow: 0 0 15px adjust-color($color, $lightness: 20%);
           21  +	}
           22  +}
           23  +a[href^="//"],
           24  +a[href^="http://"],
           25  +a[href^="https://"] { // external link
           26  +	&:hover::after {
           27  +		color: black;
           28  +		background-color: white;
           29  +	}
           30  +	&::after {
           31  +		content: "↗";
           32  +		display: inline-block;
           33  +		color: black;
           34  +		margin-left: 4pt;
           35  +		background-color: adjust-color($color, $lightness: 10%);
           36  +		padding: 0 4px;
           37  +		text-shadow: none;
           38  +		padding-right: 5px;
           39  +		vertical-align: baseline;
           40  +		font-size: 80%;
           41  +	}
           42  +}
           43  +
           44  +%content {
           45  +	width: 8in;
           46  +	margin: auto;
           47  +}
           48  +
           49  +%glow {
           50  +	box-shadow: 0 0 20px adjust-color($color, $alpha: -0.8);
           51  +}
           52  +
           53  +%button {
           54  +	@extend %sans;
           55  +	font-size: 14pt;
           56  +	padding: 0.1in 0.2in;
           57  +	border: 1px solid black;
           58  +	color: adjust-color($color, $lightness: 25%);
           59  +	text-shadow: 1px 1px black;
           60  +	text-decoration: none;
           61  +	text-align: center;
           62  +	background: linear-gradient(to bottom,
           63  +		adjust-color($color, $lightness: -45%),
           64  +		adjust-color($color, $lightness: -50%) 15%,
           65  +		adjust-color($color, $lightness: -50%) 75%,
           66  +		adjust-color($color, $lightness: -55%)
           67  +	);
           68  +	&:hover, &:focus {
           69  +		@extend %glow;
           70  +		outline: none;
           71  +		color: adjust-color($color, $lightness: -55%);
           72  +		text-shadow: none;
           73  +		background: linear-gradient(to bottom,
           74  +			adjust-color($color, $lightness: -25%),
           75  +			adjust-color($color, $lightness: -30%) 15%,
           76  +			adjust-color($color, $lightness: -30%) 75%,
           77  +			adjust-color($color, $lightness: -35%)
           78  +		);
           79  +	}
           80  +	&:active {
           81  +		color: black;
           82  +		padding-bottom: calc(0.1in - 2px);
           83  +		padding-top: calc(0.1in + 2px);
           84  +		background: linear-gradient(to top,
           85  +			adjust-color($color, $lightness: -25%),
           86  +			adjust-color($color, $lightness: -30%) 15%,
           87  +			adjust-color($color, $lightness: -30%) 75%,
           88  +			adjust-color($color, $lightness: -35%)
           89  +		);
           90  +	}
           91  +}
           92  +
           93  +button { @extend %button;
           94  +	&:first-of-type {
           95  +		@extend %button;
           96  +		color: white;
           97  +		box-shadow: inset 0 1px  adjust-color($color, $lightness: -25%),
           98  +		            inset 0 -1px adjust-color($color, $lightness: -50%);
           99  +		background: linear-gradient(to bottom,
          100  +			adjust-color($color, $lightness: -35%),
          101  +			adjust-color($color, $lightness: -40%) 15%,
          102  +			adjust-color($color, $lightness: -40%) 75%,
          103  +			adjust-color($color, $lightness: -45%)
          104  +		);
          105  +		&:hover, &:focus {
          106  +			box-shadow: inset 0 1px  adjust-color($color, $lightness: -15%),
          107  +						inset 0 -1px adjust-color($color, $lightness: -40%);
          108  +		}
          109  +		&:active {
          110  +			box-shadow: inset 0 1px  adjust-color($color, $lightness: -50%),
          111  +						inset 0 -1px adjust-color($color, $lightness: -25%);
          112  +			background: linear-gradient(to top,
          113  +				adjust-color($color, $lightness: -30%),
          114  +				adjust-color($color, $lightness: -35%) 15%,
          115  +				adjust-color($color, $lightness: -35%) 75%,
          116  +				adjust-color($color, $lightness: -40%)
          117  +			);
          118  +		}
          119  +	}
          120  +	&:hover { font-weight: bold; }
          121  +}
          122  +
          123  +$grad-ui-focus: linear-gradient(to bottom,
          124  +	adjust-color($color, $lightness: -50%),
          125  +	adjust-color($color, $lightness: -35%)
          126  +);
          127  +
          128  +input[type='text'], input[type='password'], textarea {
          129  +	@extend %serif;
          130  +	padding: 0.08in 0.1in;
          131  +	border: 1px solid black;
          132  +	background: linear-gradient(to bottom,
          133  +		adjust-color($color, $lightness: -55%),
          134  +		adjust-color($color, $lightness: -40%)
          135  +	);
          136  +	font-size: 16pt;
          137  +	color: adjust-color($color, $lightness: 25%);
          138  +	box-shadow: inset 0 0 20px -3px adjust-color($color, $lightness: -55%);
          139  +	&:focus {
          140  +		color: white;
          141  +		border-image: linear-gradient(to bottom,
          142  +			adjust-color($color, $lightness: -10%),
          143  +			adjust-color($color, $lightness: -30%)
          144  +		) 1 / 1px;
          145  +		background: $grad-ui-focus;
          146  +		outline: none;
          147  +		@extend %glow;
          148  +	}
          149  +}
          150  +
          151  +@mixin glass {
          152  +	@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
          153  +		backdrop-filter: blur(40px);
          154  +		-webkit-backdrop-filter: blur(40px);
          155  +		background-color: adjust-color($color, $lightness: -53%, $alpha: -0.7);
          156  +	}
          157  +	@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
          158  +		background-color: adjust-color($color, $lightness: -53%, $alpha: -0.1);
          159  +	}
          160  +}
          161  +
          162  +header {
          163  +	position: fixed;
          164  +	height: min-content;
          165  +	width: 100vw;
          166  +	margin: 0;
          167  +	padding: 0;
          168  +	border-bottom: 1px solid black;
          169  +	z-index: 1;
          170  +	@include glass;
          171  +	background: linear-gradient(to bottom, transparent, rgba(0,0,0,0.3));
          172  +	> div {
          173  +		position: relative;
          174  +		max-width: 10in;
          175  +		margin: auto;
          176  +
          177  +		display: grid;
          178  +		grid-template-columns: 1fr max-content;
          179  +		grid-template-rows: 1fr;
          180  +		h1 {
          181  +			all: unset;
          182  +			display: block;
          183  +			font-size: 1.4em;
          184  +			padding: 0.25in 0;
          185  +			text-shadow: 2px 2px 1px black;
          186  +			grid-column: 1/2; grid-row: 1/2;
          187  +		}
          188  +		nav {
          189  +			all: unset;
          190  +			display: flex;
          191  +			justify-content: flex-end;
          192  +			align-items: center;
          193  +			grid-column: 2/3; grid-row: 1/2;
          194  +			> a[href] {
          195  +				display: block;
          196  +				padding: 0.25in 0.15in;
          197  +				//padding: calc((25% - 1em)/2) 0.15in;
          198  +				&, &::after { transition: 0.3s; }
          199  +				text-shadow: 1px 1px 1px black;
          200  +				&:hover{
          201  +					transform: scale(120%);
          202  +				}
          203  +			}
          204  +		}
          205  +	}
          206  +}
          207  +
          208  +main {
          209  +	@extend %content;
          210  +	display: block;
          211  +	position: relative;
          212  +	min-height: calc(100vh - 1.1in);
          213  +	margin-top: 0;
          214  +	margin-bottom: 0;
          215  +	padding: 0 0.4in;
          216  +	padding-top: 1.1in;
          217  +	background-color: adjust-color($color, $lightness: -45%, $alpha: 0.4);
          218  +	border: {
          219  +		left: 1px solid black;
          220  +		right: 1px solid black;
          221  +	}
          222  +}
          223  +
          224  +div.profile {
          225  +	@extend %box;
          226  +	padding: 0.1in;
          227  +	position: relative;
          228  +	display: grid;
          229  +	grid-template-columns: 2fr 1fr;
          230  +	grid-template-rows: 1fr 1fr;
          231  +	width: 100%;
          232  +	> .banner {
          233  +		grid-column: 1 / 3;
          234  +		grid-row: 1 / 2;
          235  +		display: grid;
          236  +		grid-template-columns: 1.1in 1fr;
          237  +		grid-template-rows: 0.3in 1fr;
          238  +		> .avatar {
          239  +			display: block;
          240  +			width: 1in; height: 1in;
          241  +			grid-column: 1 / 2;
          242  +			grid-row: 1 / 3;
          243  +			border: 1px solid black;
          244  +		}
          245  +		> .id {
          246  +			grid-column: 2 / 3;
          247  +			grid-row: 1 / 2;
          248  +			color: adjust-color($color, $lightness: 25%, $alpha: -0.4);
          249  +			> .nym {
          250  +				font-weight: bold;
          251  +				color: adjust-color($color, $lightness: 25%);
          252  +			}
          253  +			> .xid {
          254  +				color: adjust-color($color, $lightness: 20%, $alpha: -0.1);
          255  +				font-size: 80%;
          256  +				vertical-align: text-top;
          257  +			}
          258  +		}
          259  +		> .bio {
          260  +			grid-column: 2 / 3;
          261  +			grid-row: 2 / 3;
          262  +		}
          263  +	}
          264  +	> .stats {
          265  +		grid-column: 3 / 4;
          266  +		grid-row: 1 / 3;
          267  +	}
          268  +	> .menu {
          269  +		grid-column: 1 / 3;
          270  +		grid-row: 2 / 3;
          271  +		display: flex;
          272  +		justify-content: center;
          273  +		align-items: center;
          274  +		> a[href] {
          275  +			@extend %button;
          276  +			display: block;
          277  +			margin: 0 0.05in;
          278  +		}
          279  +		> hr {
          280  +			all: unset;
          281  +			display: block;
          282  +			height: 0.3in;
          283  +			width: 1px;
          284  +			border-left: 1px solid rgba(0,0,0,0.6);
          285  +		}
          286  +	}
          287  +}
          288  +
          289  +%box {
          290  +	margin: auto;
          291  +	border: 1px solid adjust-color($color, $lightness: -55%);
          292  +	border-bottom: 3px solid black;
          293  +	box-shadow: 0 0 1px black;
          294  +	border-image: linear-gradient(to bottom,
          295  +		adjust-color($color, $lightness: -40%),
          296  +		adjust-color($color, $lightness: -52%) 10%,
          297  +		adjust-color($color, $lightness: -55%) 90%,
          298  +		adjust-color($color, $lightness: -60%)
          299  +	) 1 / 1px;
          300  +	background: linear-gradient(to bottom,
          301  +		adjust-color($color, $lightness: -58%),
          302  +		adjust-color($color, $lightness: -55%) 10%,
          303  +		adjust-color($color, $lightness: -50%) 80%,
          304  +		adjust-color($color, $lightness: -45%)
          305  +	);
          306  +	// outline: 1px solid black;
          307  +}
          308  +
          309  +body.error .message {
          310  +	@extend %box;
          311  +	width: 4in;
          312  +	margin:auto;
          313  +	padding: 0.5in;
          314  +	text-align: center;
          315  +}
          316  +
          317  +div.login {
          318  +	@extend %box;
          319  +	width: 4in;
          320  +	padding: 0.4in;
          321  +	> .msg {
          322  +		text-align: center;
          323  +		padding: 0.3in;
          324  +	}
          325  +	> .msg:first-child { padding-top: 0; }
          326  +	> .user {
          327  +		width: min-content; margin: auto;
          328  +		background: adjust-color($color, $lightness: -20%, $alpha: -0.3);
          329  +		border: 1px solid black;
          330  +		color: adjust-color($color, $lightness: -50%);
          331  +		padding: 0.1in;
          332  +		> img { width: 1in; height: 1in; border: 1px solid black; }
          333  +		> .name { @extend %serif; text-align: center; font-size: 130%; font-weight: bold; margin-top: 0.08in; }
          334  +	}
          335  +	>form {
          336  +		display: grid;
          337  +		grid-template-columns: 1fr 1fr;
          338  +		grid-template-rows: 1.2em 1fr 1fr;
          339  +		grid-gap: 5px;
          340  +		> label, input, button { display: block; }
          341  +		> label { grid-column: 1 / 3; grid-row: 1/2; font-weight: bold }
          342  +		> input { grid-column: 1 / 3; grid-row: 2/3; }
          343  +		> button { grid-column: 2 / 3; grid-row: 3/4; }
          344  +		> a { @extend %button; grid-column: 1 / 2; grid-row: 3/4; }
          345  +	}
          346  +}
          347  +
          348  +form.compose {
          349  +	@extend %box;
          350  +	display: grid;
          351  +	grid-template-columns: 1.1in 2fr min-content 1fr;
          352  +	grid-template-rows: 1fr min-content;
          353  +	grid-gap: 2px;
          354  +	padding: 0.1in;
          355  +	> img { grid-column: 1/2; grid-row: 1/3; width: 1in; height: 1in;}
          356  +	> textarea { grid-column: 2/5; grid-row: 1/2; height: 3in;}
          357  +	> input[name="acl"] { grid-column: 2/3; grid-row: 2/3; }
          358  +	> button { grid-column: 4/5; grid-row: 2/3; }
          359  +	a.help[href] { margin-right: 0.05in }
          360  +}
          361  +
          362  +a.help[href] {
          363  +	display: block;
          364  +	text-align: center;
          365  +	padding: 0.09in 0.2in;
          366  +	background: tone(-40%);
          367  +	border: 1px solid black;
          368  +	font-weight: bold;
          369  +	text-decoration: none;
          370  +	cursor: help;
          371  +}
          372  +
          373  +input.acl {
          374  +	@extend %teletype;
          375  +	background: url(/s/padlock.webp) no-repeat;
          376  +	background-size: 20pt;
          377  +	background-position: 0.05in 50%;
          378  +	&:focus {
          379  +		background: url(/s/padlock.webp) no-repeat, $grad-ui-focus;
          380  +		background-size: 20pt;
          381  +		background-position: 0.05in 50%;
          382  +	};
          383  +	padding-left: 0.40in;
          384  +}
          385  +
          386  +div.modal {
          387  +	@extend %box;
          388  +	position: fixed;
          389  +	display: none;
          390  +	left: 0; right: 0; bottom: 0; top: 0;
          391  +	max-width: 7in;
          392  +	margin: 1in auto;
          393  +	padding: 0.2in 0.3in;
          394  +	&:target { display: block; }
          395  +	box-shadow: 0 0 4in 5in rgba(0,0,0,0.5);
          396  +	z-index: 2;
          397  +	> div {
          398  +		height: 100%;
          399  +		overflow-y: scroll;
          400  +		>p:first-of-type { margin-top: 0; }
          401  +	}
          402  +	>a[href="#0"] { // close link
          403  +		@extend %button;
          404  +		cursor: default;
          405  +		display: block;
          406  +		position: absolute;
          407  +		top: -0.3in;
          408  +		right: 0.1in;
          409  +		margin: 0.1in;
          410  +		padding: 0.1in;
          411  +		&:hover { font-weight: bold; }
          412  +	}
          413  +}
          414  +
          415  +code {
          416  +	@extend %teletype;
          417  +	background: adjust-color($color, $lightness: -50%);
          418  +	border: 1px solid adjust-color($color, $lightness: -20%);
          419  +	padding: 2px 6px;
          420  +	text-shadow: 2px 2px black;
          421  +}

Added static/warn.svg version [a819eb65cc].

            1  +<?xml version="1.0" encoding="UTF-8" standalone="no"?>
            2  +<!-- Created with Inkscape (http://www.inkscape.org/) -->
            3  +
            4  +<svg
            5  +   xmlns:dc="http://purl.org/dc/elements/1.1/"
            6  +   xmlns:cc="http://creativecommons.org/ns#"
            7  +   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
            8  +   xmlns:svg="http://www.w3.org/2000/svg"
            9  +   xmlns="http://www.w3.org/2000/svg"
           10  +   xmlns:xlink="http://www.w3.org/1999/xlink"
           11  +   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
           12  +   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
           13  +   width="1in"
           14  +   height="1in"
           15  +   viewBox="0 0 25.4 25.400001"
           16  +   version="1.1"
           17  +   id="svg8"
           18  +   inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
           19  +   sodipodi:docname="warn.svg"
           20  +   inkscape:export-filename="/home/lexi/dev/parsav/static/warn.png"
           21  +   inkscape:export-xdpi="200"
           22  +   inkscape:export-ydpi="200">
           23  +  <defs
           24  +     id="defs2">
           25  +    <linearGradient
           26  +       inkscape:collect="always"
           27  +       id="linearGradient2322">
           28  +      <stop
           29  +         style="stop-color:#ca0050;stop-opacity:1;"
           30  +         offset="0"
           31  +         id="stop2318" />
           32  +      <stop
           33  +         style="stop-color:#7b0031;stop-opacity:1"
           34  +         offset="1"
           35  +         id="stop2320" />
           36  +    </linearGradient>
           37  +    <linearGradient
           38  +       inkscape:collect="always"
           39  +       id="linearGradient2302">
           40  +      <stop
           41  +         style="stop-color:#ffffff;stop-opacity:1;"
           42  +         offset="0"
           43  +         id="stop2298" />
           44  +      <stop
           45  +         style="stop-color:#ffffff;stop-opacity:0;"
           46  +         offset="1"
           47  +         id="stop2300" />
           48  +    </linearGradient>
           49  +    <linearGradient
           50  +       inkscape:collect="always"
           51  +       id="linearGradient1228">
           52  +      <stop
           53  +         style="stop-color:#8c0037;stop-opacity:1"
           54  +         offset="0"
           55  +         id="stop1224" />
           56  +      <stop
           57  +         style="stop-color:#ff1a75;stop-opacity:1"
           58  +         offset="1"
           59  +         id="stop1226" />
           60  +    </linearGradient>
           61  +    <linearGradient
           62  +       inkscape:collect="always"
           63  +       id="linearGradient851">
           64  +      <stop
           65  +         style="stop-color:#ffffff;stop-opacity:0"
           66  +         offset="0"
           67  +         id="stop847" />
           68  +      <stop
           69  +         style="stop-color:#ffa9c6;stop-opacity:1"
           70  +         offset="1"
           71  +         id="stop849" />
           72  +    </linearGradient>
           73  +    <radialGradient
           74  +       inkscape:collect="always"
           75  +       xlink:href="#linearGradient851"
           76  +       id="radialGradient853"
           77  +       cx="12.699999"
           78  +       cy="285.82184"
           79  +       fx="12.699999"
           80  +       fy="285.82184"
           81  +       r="1.7905753"
           82  +       gradientTransform="matrix(2.2137788,-2.5697531e-5,6.7771541e-5,5.8383507,-15.43436,-1382.906)"
           83  +       gradientUnits="userSpaceOnUse" />
           84  +    <linearGradient
           85  +       inkscape:collect="always"
           86  +       xlink:href="#linearGradient1228"
           87  +       id="linearGradient1230"
           88  +       x1="87.388672"
           89  +       y1="90.857147"
           90  +       x2="67.185623"
           91  +       y2="-29.734592"
           92  +       gradientUnits="userSpaceOnUse" />
           93  +    <filter
           94  +       inkscape:collect="always"
           95  +       style="color-interpolation-filters:sRGB"
           96  +       id="filter2294"
           97  +       x="-0.38068429"
           98  +       width="1.7613686"
           99  +       y="-0.089708175"
          100  +       height="1.1794164">
          101  +      <feGaussianBlur
          102  +         inkscape:collect="always"
          103  +         stdDeviation="0.5243555"
          104  +         id="feGaussianBlur2296" />
          105  +    </filter>
          106  +    <radialGradient
          107  +       inkscape:collect="always"
          108  +       xlink:href="#linearGradient2302"
          109  +       id="radialGradient2304"
          110  +       cx="12.58085"
          111  +       cy="285.25314"
          112  +       fx="12.58085"
          113  +       fy="285.25314"
          114  +       r="10.573204"
          115  +       gradientTransform="matrix(1.1874875,-1.9679213e-8,1.9699479e-8,1.1887104,-2.3811363,-53.650199)"
          116  +       gradientUnits="userSpaceOnUse" />
          117  +    <linearGradient
          118  +       inkscape:collect="always"
          119  +       xlink:href="#linearGradient2322"
          120  +       id="linearGradient2324"
          121  +       x1="20.298828"
          122  +       y1="97.196846"
          123  +       x2="20.298828"
          124  +       y2="12.911131"
          125  +       gradientUnits="userSpaceOnUse"
          126  +       gradientTransform="translate(1.3858268e-6)" />
          127  +  </defs>
          128  +  <sodipodi:namedview
          129  +     id="base"
          130  +     pagecolor="#313131"
          131  +     bordercolor="#666666"
          132  +     borderopacity="1.0"
          133  +     inkscape:pageopacity="0"
          134  +     inkscape:pageshadow="2"
          135  +     inkscape:zoom="1.4"
          136  +     inkscape:cx="-71.210763"
          137  +     inkscape:cy="60.089308"
          138  +     inkscape:document-units="mm"
          139  +     inkscape:current-layer="layer1"
          140  +     showgrid="false"
          141  +     units="in"
          142  +     inkscape:window-width="1920"
          143  +     inkscape:window-height="1042"
          144  +     inkscape:window-x="0"
          145  +     inkscape:window-y="38"
          146  +     inkscape:window-maximized="0" />
          147  +  <metadata
          148  +     id="metadata5">
          149  +    <rdf:RDF>
          150  +      <cc:Work
          151  +         rdf:about="">
          152  +        <dc:format>image/svg+xml</dc:format>
          153  +        <dc:type
          154  +           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
          155  +        <dc:title></dc:title>
          156  +      </cc:Work>
          157  +    </rdf:RDF>
          158  +  </metadata>
          159  +  <g
          160  +     inkscape:label="Layer 1"
          161  +     inkscape:groupmode="layer"
          162  +     id="layer1"
          163  +     transform="translate(0,-271.59998)">
          164  +    <path
          165  +       id="rect829"
          166  +       style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.26499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
          167  +       d="m 11.543949,291.41721 -7e-6,0.52521 0.893337,0.89333 0.525444,2.4e-4 0.893329,-0.89333 -2.34e-4,-0.52545 -0.893338,-0.89333 h -0.525203 z m 1.995206,-2.42709 0.813727,-9.44222 -0.826441,-0.74021 h -1.652884 l -0.826441,0.74021 0.813726,9.44222 0.419578,0.5079 h 0.839157 z"
          168  +       inkscape:connector-curvature="0" />
          169  +    <path
          170  +       style="fill:url(#linearGradient1230);fill-opacity:1;stroke:none;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
          171  +       d="M 48.013672 8.5 A 46.78571 46.78571 0 0 0 44.353516 8.6621094 L 8.6132812 80.511719 A 46.78571 46.78571 0 0 0 14.115234 87.5 L 81.867188 87.5 A 46.78571 46.78571 0 0 0 87.388672 80.460938 L 51.685547 8.6855469 A 46.78571 46.78571 0 0 0 48.013672 8.5 z M 44.876953 27.242188 L 51.123047 27.242188 L 54.248047 30.039062 L 51.171875 65.726562 L 49.585938 67.646484 L 46.414062 67.646484 L 44.828125 65.726562 L 41.751953 30.039062 L 44.876953 27.242188 z M 47.007812 71.523438 L 48.992188 71.523438 L 52.369141 74.900391 L 52.369141 76.884766 L 48.992188 80.261719 L 47.007812 80.261719 L 43.630859 76.884766 L 43.630859 74.900391 L 47.007812 71.523438 z "
          172  +       id="path817"
          173  +       transform="matrix(0.26458333,0,0,0.26458333,0,271.59998)" />
          174  +    <path
          175  +       inkscape:connector-curvature="0"
          176  +       id="path838"
          177  +       d="m -29.39374,273.80193 a 12.378719,12.378719 0 0 0 -0.968416,0.0424 l -9.456272,19.01021 a 12.378719,12.378719 0 0 0 1.455725,1.84899 h 17.926038 a 12.378719,12.378719 0 0 0 1.460894,-1.86242 l -9.446452,-18.99057 a 12.378719,12.378719 0 0 0 -0.971517,-0.0486 z"
          178  +       style="fill:#ca0050;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
          179  +    <path
          180  +       id="path1146"
          181  +       d="m 12.703617,273.84894 c -0.323241,0.002 -0.646295,0.016 -0.968416,0.0429 l -9.4562703,19.01021 c 0.425424,0.66116 0.9128677,1.28028 1.4557249,1.84898 H 21.660694 c 0.545337,-0.57272 1.034537,-1.19637 1.460892,-1.86242 l -9.446452,-18.99058 c -0.32307,-0.0291 -0.64716,-0.0455 -0.971517,-0.0491 z"
          182  +       style="fill:none;fill-opacity:1;stroke:url(#radialGradient2304);stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
          183  +       inkscape:connector-curvature="0"
          184  +       sodipodi:nodetypes="ccccccccc" />
          185  +    <path
          186  +       id="path1232"
          187  +       style="opacity:1;fill:#ffe7f0;fill-opacity:1;stroke:none;stroke-width:0.26499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
          188  +       d="m 12.134766,279.49414 -0.375,0.33594 0.647507,8.88867 0.07813,0.12618 h 0.429595 l 0.07813,-0.12618 0.647507,-8.88867 -0.375,-0.33594 z m 0.06945,12.07312 -3e-6,0.22524 0.383115,0.38311 0.225341,1.1e-4 0.383112,-0.38311 -1.01e-4,-0.22535 -0.383115,-0.38311 h -0.225238 z"
          189  +       inkscape:connector-curvature="0"
          190  +       sodipodi:nodetypes="cccccccccccccccccc" />
          191  +    <path
          192  +       inkscape:connector-curvature="0"
          193  +       d="m 11.543949,291.41721 -7e-6,0.52521 0.893337,0.89333 0.525444,2.4e-4 0.893329,-0.89333 -2.34e-4,-0.52545 -0.893338,-0.89333 h -0.525203 z m 1.995206,-2.42709 0.813727,-9.44222 -0.826441,-0.74021 h -1.652884 l -0.826441,0.74021 0.813726,9.44222 0.419578,0.5079 h 0.839157 z"
          194  +       style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.26499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter2294)"
          195  +       id="path1856" />
          196  +  </g>
          197  +</svg>

Modified store.t from [213b3d2729] to [5ad659a834].

    12     12   		'follow', 'mute', 'block'
    13     13   	};
    14     14   	credset = lib.set {
    15     15   		'pw', 'otp', 'challenge', 'trust'
    16     16   	};
    17     17   	privset = lib.set {
    18     18   		'post', 'edit', 'acct', 'upload', 'censor', 'admin'
           19  +	};
           20  +	powerset = lib.set {
           21  +		-- user powers -- default on
           22  +		'login', 'visible', 'post', 'shout',
           23  +		'propagate', 'upload', 'acct', 'edit';
           24  +
           25  +		-- admin powers -- default off
           26  +		'purge', 'config', 'censor', 'suspend',
           27  +		'cred', 'elevate', 'demote', 'rebrand' -- modify site's brand identity
    19     28   	}
    20     29   }
           30  +
           31  +terra m.powerset:affect_users()
           32  +	return self.purge() or self.censor() or self.suspend() or
           33  +	       self.elevate() or self.demote() or self.rebrand() or
           34  +		   self.cred()
           35  +end
    21     36   
    22     37   local str = rawstring --lib.mem.ptr(int8)
    23     38   
    24     39   struct m.source
    25     40   
    26     41   struct m.rights {
    27     42   	rank: uint16 -- lower = more powerful except 0 = regular user
    28     43   	-- creating staff automatically assigns rank immediately below you
    29     44   	quota: uint32 -- # of allowed tweets per day; 0 = no limit
    30     45   	
    31         -	-- user powers -- default on
    32         -	login: bool
    33         -	visible: bool
    34         -	post: bool
    35         -	shout: bool
    36         -	propagate: bool
    37         -	upload: bool
    38         -	acct: bool
    39         -	edit: bool
    40         -
    41         -	-- admin powers -- default off
    42         -	ban: bool
    43         -	config: bool
    44         -	censor: bool
    45         -	suspend: bool
    46         -	rebrand: bool -- modify site's brand identity
           46  +	powers: m.powerset
    47     47   }
    48     48   
    49     49   terra m.rights_default()
    50         -	return m.rights {
    51         -		rank = 0, quota = 1000;
    52         -		
    53         -		login = true, visible = true, post = true;
    54         -		shout = true, propagate = true, upload = true;
    55         -
    56         -		ban = false, config = false, censor = false;
    57         -		suspend = false, rebrand = false;
    58         -	}
           50  +	var pow: m.powerset pow:fill()
           51  +	(pow.purge << false)
           52  +	(pow.config << false)
           53  +	(pow.censor << false)
           54  +	(pow.suspend << false)
           55  +	(pow.elevate << false)
           56  +	(pow.demote << false)
           57  +	(pow.cred << false)
           58  +	(pow.rebrand << false)
           59  +	return m.rights { rank = 0, quota = 1000, powers = pow; }
    59     60   end
    60     61   
    61     62   struct m.actor {
    62     63   	id: uint64
    63     64   	nym: str
    64     65   	handle: str
    65     66   	origin: uint64
    66     67   	bio: str
           68  +	avatar: str
           69  +	knownsince: int64
    67     70   	rights: m.rights
    68     71   	key: lib.mem.ptr(uint8)
    69     72   
           73  +-- ephemera
    70     74   	xid: str
    71         -
    72     75   	source: &m.source
    73     76   }
           77  +
           78  +struct m.actor_stats {
           79  +	posts: intptr
           80  +	follows: intptr
           81  +	followers: intptr
           82  +	mutuals: intptr
           83  +}
    74     84   
    75     85   struct m.range {
    76     86   	time: bool
    77     87   	union {
    78     88   		from_time: m.timepoint
    79     89   		from_idx: uint64
    80     90   	}
................................................................................
   165    175   	actor_save: {&m.source, m.actor} -> bool
   166    176   	actor_create: {&m.source, m.actor} -> bool
   167    177   	actor_fetch_xid: {&m.source, lib.mem.ptr(int8)} -> lib.mem.ptr(m.actor)
   168    178   	actor_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.actor)
   169    179   	actor_notif_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.notif)
   170    180   	actor_enum: {&m.source} -> lib.mem.ptr(&m.actor)
   171    181   	actor_enum_local: {&m.source} -> lib.mem.ptr(&m.actor)
          182  +	actor_stats: {&m.source, uint64} -> m.actor_stats
   172    183   
   173         -	actor_auth_how: {&m.source, m.inet, rawstring} -> m.credset
          184  +	actor_auth_how: {&m.source, m.inet, rawstring} -> {m.credset, bool}
   174    185   		-- returns a set of auth method categories that are available for a
   175    186   		-- given user from a certain origin
   176    187   			-- origin: inet
   177         -			-- handle: rawstring
          188  +			-- username: rawstring
   178    189   	actor_auth_otp: {&m.source, m.inet, rawstring, rawstring} -> uint64
   179         -	actor_auth_pw: {&m.source, m.inet, rawstring, rawstring} -> uint64
          190  +	actor_auth_pw: {&m.source, m.inet, lib.mem.ptr(int8), lib.mem.ptr(int8) } -> uint64
   180    191   		-- handles password-based logins against hashed passwords
   181    192   			-- origin: inet
   182    193   			-- handle: rawstring
   183    194   			-- token:  rawstring
   184    195   	actor_auth_tls:    {&m.source, m.inet, rawstring} -> uint64
   185    196   		-- handles implicit authentication performed as part of an TLS connection
   186    197   			-- origin: inet
................................................................................
   219    230   terra m.source:free()
   220    231   	self.id:free()
   221    232   	self.string:free()
   222    233   end
   223    234   m.source.metamethods.__methodmissing = macro(function(meth, obj, ...)
   224    235   	local q = {...}
   225    236   	-- syntax sugar to forward unrecognized calls onto the backend
   226         -	return `obj.backend.[meth](&obj, [q])
          237  +	return quote var r = obj.backend.[meth](&obj, [q]) in r end
   227    238   end)
   228    239   
   229    240   return m

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

    13     13   	fmt = terralib.externfunction('asprintf',
    14     14   		terralib.types.funcpointer({&rawstring,rawstring},{int},true));
    15     15   	bfmt = terralib.externfunction('sprintf',
    16     16   		terralib.types.funcpointer({rawstring,rawstring},{int},true));
    17     17   	span = terralib.externfunction('strspn',{rawstring, rawstring} -> rawstring);
    18     18   }
    19     19   
    20         -(lib.mem.ptr(int8)).metamethods.__cast = function(from,to,e)
    21         -	if from == &int8 then
    22         -		return `[lib.mem.ptr(int8)]{ptr = e, ct = m.sz(e)}
    23         -	elseif to == &int8 then
    24         -		return e.ptr
           20  +do local strptr = (lib.mem.ptr(int8))
           21  +	local byteptr = (lib.mem.ptr(uint8))
           22  +	strptr.metamethods.__cast = function(from,to,e)
           23  +		if from == &int8 then
           24  +			return `strptr {ptr = e, ct = m.sz(e)}
           25  +		elseif to == &int8 then
           26  +			return e.ptr
           27  +		end
           28  +	end
           29  +
           30  +	terra strptr:cmp(other: strptr)
           31  +		var sz = lib.math.biggest(self.ct, other.ct)
           32  +		for i = 0, sz do
           33  +			if self.ptr[i] == 0 and other.ptr[i] == 0 then return true end
           34  +			if self.ptr[i] ~= other.ptr[i] then return false end
           35  +		end
           36  +		return true
           37  +	end
           38  +
           39  +	terra byteptr:cmp(other: byteptr)
           40  +		var sz = lib.math.biggest(self.ct, other.ct)
           41  +		for i = 0, sz do
           42  +			if self.ptr[i] == 0 and other.ptr[i] == 0 then return true end
           43  +			if self.ptr[i] ~= other.ptr[i] then return false end
           44  +		end
           45  +		return true
    25     46   	end
    26     47   end
    27     48   
    28     49   struct m.acc {
    29     50   	buf: rawstring
    30     51   	sz: intptr
    31     52   	run: intptr
................................................................................
    65     86   	var pt: lib.mem.ptr(int8)
    66     87   	pt.ptr = self.buf
    67     88   	pt.ct = self.sz
    68     89   	self.buf = nil
    69     90   	self.sz = 0
    70     91   	return pt
    71     92   end;
           93  +
           94  +terra m.acc:cue(sz: intptr)
           95  +	if sz <= self.run then return end
           96  +	self.run = sz
           97  +	if self.space - self.sz < self.run then
           98  +		self.space = self.sz + self.run
           99  +		self.buf = [rawstring](lib.mem.heapr_raw(self.buf, self.space))
          100  +	end
          101  +end
    72    102   
    73    103   terra m.acc:push(str: rawstring, len: intptr)
    74    104   	--var llen = len
    75    105   	if str == nil then return self end
    76    106   	--if str[len - 1] == 0xA then llen = llen - 1 end -- don't display newlines in debug output
    77    107   	-- lib.dbg('pushing "',{str,llen},'" onto accumulator')
    78    108   	if self.buf == nil then self:init(self.run) end
................................................................................
    87    117   	return self
    88    118   end;
    89    119   
    90    120   m.lit = macro(function(str)
    91    121   	return `[lib.mem.ref(int8)] {ptr = [str:asvalue()], ct = [#(str:asvalue())]}
    92    122   end)
    93    123   
          124  +m.acc.methods.lpush = macro(function(self,str)
          125  +	return `self:push([str:asvalue()], [#(str:asvalue())]) end)
    94    126   m.acc.methods.ppush = terra(self: &m.acc, str: lib.mem.ptr(int8))
    95    127   	self:push(str.ptr, str.ct)            return self end;
    96    128   m.acc.methods.merge = terra(self: &m.acc, str: lib.mem.ptr(int8))
    97    129   	self:push(str.ptr, str.ct) str:free() return self end;
    98    130   m.acc.methods.compose = macro(function(self, ...)
    99    131   	local minlen = 0
   100    132   	local pstrs = {}

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

    27     27   	local segs = {}
    28     28   	local constlen = 0
    29     29   	-- strip out all irrelevant whitespace to tidy things up
    30     30   	-- TODO: find way to exclude <pre> tags?
    31     31   	str = str:gsub('[\n^]%s+','')
    32     32   	str = str:gsub('%s+[\n$]','')
    33     33   	str = str:gsub('\n','')
           34  +	str = str:gsub('</a><a ','</a> <a ') -- keep nav links from getting smooshed
    34     35   	for start, key, stop in string.gmatch(str,'()'..tplchar..'(%w+)()') do
    35     36   		if string.sub(str,start-1,start-1) ~= '\\' then
    36     37   			segs[#segs+1] = string.sub(str,last,start-1)
    37     38   			fields[#segs] = key
    38     39   			last = stop
    39     40   		end
    40     41   	end
................................................................................
    65     66   				[runningtally] = [runningtally] + lib.str.sz([symself].[key])*fac
    66     67   			end
    67     68   		end
    68     69   	end
    69     70   
    70     71   	local copiers = {}
    71     72   	local senders = {}
           73  +	local appenders = {}
    72     74   	local symtxt = symbol(lib.mem.ptr(int8))
    73     75   	local cpypos = symbol(&opaque)
           76  +	local accumulator = symbol(&lib.str.acc)
    74     77   	local destcon = symbol(&lib.net.mg_connection)
    75     78   	for idx, seg in ipairs(segs) do
    76     79   		copiers[#copiers+1] = quote [cpypos] = lib.mem.cpy([cpypos], [&opaque]([seg]), [#seg]) end
    77     80   		senders[#senders+1] = quote lib.net.mg_send([destcon], [seg], [#seg]) end
           81  +		appenders[#appenders+1] = quote [accumulator]:push([seg], [#seg]) end
    78     82   		if fields[idx] then
    79     83   			copiers[#copiers+1] = quote
    80     84   				[cpypos] = lib.mem.cpy([cpypos],
    81     85   					[&opaque](symself.[fields[idx]]),
    82     86   					lib.str.sz(symself.[fields[idx]]))
    83     87   			end
    84     88   			senders[#senders+1] = quote
................................................................................
    94     98   		lib.dbg(['compiling template ' .. tid])
    95     99   		[tallyup]
    96    100   		var [symtxt] = lib.mem.heapa(int8, [runningtally])
    97    101   		var [cpypos] = [&opaque](symtxt.ptr)
    98    102   		[copiers]
    99    103   		@[&int8](cpypos) = 0
   100    104   		return symtxt
          105  +	end
          106  +	rec.methods.append = terra([symself], [accumulator])
          107  +		lib.dbg(['appending template ' .. tid])
          108  +		[tallyup]
          109  +		accumulator:cue([runningtally])
          110  +		[appenders]
          111  +		return accumulator
   101    112   	end
   102    113   	rec.methods.send = terra([symself], [destcon], code: uint16, hd: lib.mem.ptr(lib.http.header))
   103    114   		lib.dbg(['transmitting template ' .. tid])
   104    115   		[tallyup]
   105    116   		lib.net.mg_printf([destcon], 'HTTP/1.1 %s', lib.http.codestr(code))
   106    117   		for i = 0, hd.ct do
   107    118   			lib.net.mg_printf([destcon], '%s: %s\r\n', hd.ptr[i].key, hd.ptr[i].value)

Added view/compose.tpl version [09c6180294].

            1  +<form class="compose" method="post">
            2  +	<img src="/avi/@handle">
            3  +	<textarea autofocus name="post" placeholder="it was a dark and stormy night…">@content</textarea>
            4  +	<input required type="text" name="acl" class="acl" value="@acl">
            5  +		<a href="#aclhelp" class="help">?</a>
            6  +	<button type="submit">commit</button>
            7  +</form>
            8  +
            9  +<div id="aclhelp" class="modal"> <a href="#0">close</a> <div>
           10  +	<p>to control who can see your post (and how far it is propagated), <code>parsav</code> uses <strong>ACL expressions</strong>. this is roughly equivalent to scoping in pleroma or GNU social terms. an ACL expression consists of one or more space-separated terms, each of which match a certain set of users. a term can be negated by prefixing it with <code>~</code>, a tilde character, so <code>~all</code> matches nobody, and <code>~followed</code> matches users you do not follow.</p>
           11  +	<ul>
           12  +		<li><strong>all</strong>: matches any and all users</li>
           13  +		<li><strong>local</strong>: matches users who belong to this instance</li>
           14  +		<li><strong>mutuals</strong>: matches users you follow who also follow you</li>
           15  +		<li><strong>followed</strong>: matches users you follow</li>
           16  +		<li><strong>followers</strong>: matches users who follow you</li>
           17  +		<li><strong>groupies</strong>: matches users who follow you, but whom you do not follow</li>
           18  +		<li><strong>mentioned</strong>: matches users who are mentioned in the post</li>
           19  +		<li><strong>staff</strong>: matches instance staff (equivalent to <code>~%0</code>)</li>
           20  +		<li><strong>admin</strong>: matches the individual named as the instance administrator, if any</li>
           21  +		<li><strong>\@</strong><em>handle</em>: matches the user <em>handle</em></li>
           22  +		<li><strong>+</strong><em>circle</em>: matches users you have categorized under <em>circle</em></li>
           23  +		<li><strong>#</strong><em>room</em>: matches users who are members of <em>room</em></li>
           24  +		<li><strong>%</strong><em>rank</em>: matches users of <em>rank</em> or higher (e.g. <code>%3</code> matches users of rank 3, 2, and 1). as a special case, <code>%0</code> matches ordinary users</li>
           25  +		<li><strong>#</strong><em>room</em><strong>%</strong><em>rank</em>: matches users who hold <em>rank</em> in <em>room</em></li>
           26  +		<li><strong>&lt;</strong><em>title</em><strong>&gt;</strong>: matches peers of the net who have been created <em>title</em> by the sovereign</li>
           27  +		<li><strong>#</strong><em>room</em><strong>&lt;</strong><em>title</em><strong>&gt;</strong>: matches peers of the chat who have been created <em>title</em> by <em>room</em> staff</li>
           28  +	</ul>
           29  +	<p>to evaluate an ACL expression, <code>parsav</code> reads each term from start to finish. for each term, it considers whether it describes the user who is attempting to access the content. if the term matches, its policy is applied and the expression completes. if the term doesn't match, the server proceeds on to the next term and the process repeats until it finds a matching term or runs out of terms, applying the fallback policy.</p>
           30  +	<p><strong>policy</strong> is whether a term grants or denies access. the default term policy is <strong>allow</strong>, but you can control the policy with the keywords <code>allow</code> and <code>deny</code>. if a term finishes evaluating without any match being found, a fallback policy is applied; this fallback is the opposite of whatever the current policy is. this sounds confusing but makes ACL expressions much more intuitive; <code>allow \@bob</code> and <code>deny trent</code> do exactly what you'd expect &em; the former allows bob and only bob in; the latter denies access only to trent, but grants access to the rest of the world.</p>
           31  +	<p>expressions must contain at least one term to be valid. if they consist only of policy keywords, they will be rejected.</p>
           32  +	<p>in effect, this all means that an ACL expression can be treated as a simple list of who is allowed to view your post. for instance, an expression of <code>local</code> means only local users can view it. however, much more complex expressions are possible.</p>
           33  +	<ul>
           34  +		<li><code>deny groupies allow +illuminati</code>: permits access to the illuminati, but excluding those members who are groupies</li>
           35  +		<li><code>+illuminati deny groupies</code>: allows access to everyone but groupies (unless they're in the illuminati)</li>
           36  +		<li><code>\@eve \@alice\@nowhere.tld deny \@bob \@trent\@witches.live</code>: grants access to eve and alice, but locks out bob and trent</li>
           37  +		<li><code>&lt;grand duke&gt; #4th-intl&lt;comrade&gt;</code>: restricts the post to the eyes of the Fourth International's secret cabal of anointed comrades and the grand dukes of the Empire</li>
           38  +		<li><code>deny ~%3</code>: blocks a post from being seen by anyone with a staff rank level below 3</li>
           39  +	</ul>
           40  +	<p><strong>limitations:</strong> to inhibit potential denial-of-service attacks, ACL expressions can be a maximum of 128 characters, can contain at most 16 words, and cannot trigger queries against other servers. all information needed to evaluate an ACL expression must be known locally. this is particularly relevant with respect to rooms.</p>
           41  +</div></div>

Modified view/docskel.tpl from [004398018e] to [cb4a31dcc6].

     1      1   <!doctype html>
     2      2   <html>
     3      3   	<head>
     4      4   		<title>@instance :: @title</title>
     5      5   		<link rel="stylesheet" href="/s/style.css">
     6      6   	</head>
     7      7   	<body class="@class">
     8         -		<h1>@title</h1>
     9         -		@body
            8  +		<header><div>
            9  +			<h1>@title</h1>
           10  +			<nav>
           11  +				<a href="/instance">instance</a>
           12  +				@navlinks
           13  +			</nav>
           14  +		</div></header>
           15  +		<main>
           16  +			@body
           17  +		</main>
    10     18   	</body>
    11     19   </html>

Modified view/load.lua from [53cfafaa7c] to [1503c625ad].

     3      3   -- copies them into a data structure we can then
     4      4   -- create templates from when we return to terra
     5      5   local path = ...
     6      6   local sources = {
     7      7   	'docskel';
     8      8   	'tweet';
     9      9   	'profile';
           10  +	'compose';
           11  +	'login-username';
           12  +	'login-challenge';
    10     13   }
    11     14   
    12     15   local ingest = function(filename)
    13     16   	local hnd = io.open(path..'/'..filename)
    14     17   	local txt = hnd:read('*a')
    15     18   	io.close(hnd)
    16     19   	txt = txt:gsub('([^\\])!%b[]', '%1')
................................................................................
    18     21   	txt = txt:gsub('\\(!%b[])', '%1')
    19     22   	txt = txt:gsub('\\(!!)', '%1')
    20     23   	return txt
    21     24   end
    22     25   
    23     26   
    24     27   local views = {}
    25         -for _,n in pairs(sources) do views[n] = ingest(n .. '.tpl') end
           28  +for _,n in pairs(sources) do views[n:gsub('-','_')] = ingest(n .. '.tpl') end
    26     29   return views

Added view/login-challenge.tpl version [c8511de2b7].

            1  +<div class="login">
            2  +	<div class="user">
            3  +		<img src="/avi/@handle">
            4  +		<div class="name">@name</div>
            5  +	</div>
            6  +	<div class="msg">@challenge</div>
            7  +	<form action="/login" method="post">
            8  +		<label for="response">@label</label>
            9  +		<input type="hidden" name="user" value="@handle">
           10  +		<input type="password" name="response" id="response" autofocus required>
           11  +		<button type="submit" name="authmethod" value="@method">authenticate</button>
           12  +		<a href="/login">cancel</a>
           13  +	</form>
           14  +</div>

Added view/login-username.tpl version [4dc628d5ef].

            1  +<div class="login">
            2  +	<div class="msg">@loginmsg</div>
            3  +	<form action="/login" method="post">
            4  +		<label for="user">local handle</label>
            5  +		<input type="text" name="user" id="user" autofocus required>
            6  +		<button type="submit">log on</button>
            7  +	</form>
            8  +</div>

Modified view/profile.tpl from [abc7153f4d] to [6c61b2a3c1].

     1      1   <div class="profile">
     2      2   	<div class="banner">
     3      3   		<img class="avatar" src="@avatar">
     4         -		<div class="id">@nym [@xid]</div>
            4  +		<div class="id"><span class="nym">@nym</span> [<span class="xid">@xid</span>]</div>
     5      5   		<div class="bio">
     6      6   			@bio
     7      7   		</div>
     8      8   	</div>
     9      9   	<table class="stats">
    10     10   		<tr><th>posts</th> <td>@nposts</td></tr>
    11     11   		<tr><th>following</th> <td>@nfollows</td></tr>
    12     12   		<tr><th>followers</th> <td>@nfollowers</td></tr>
    13     13   		<tr><th>mutuals</th> <td>@nmutuals</td></tr>
    14         -		<tr><th>account created</th> <td>@tweetday</td></tr>
           14  +		<tr><th>@timephrase</th> <td>@tweetday</td></tr>
    15     15   	</table>
    16     16   	<div class="menu">
    17         -		<a href="/\@@xid">posts</a>
    18         -		<a href="/\@@xid/media">media</a>
    19         -		<a href="/\@@xid/follows">follows</a>
    20         -		<a href="/\@@xid/chat">chat</a>
           17  +		<a href="/@xid">posts</a>
           18  +		<a href="/@xid/media">media</a>
           19  +		<a href="/@xid/social">associates</a>
           20  +		<hr>
           21  +		@auxbtn
    21     22   	</div>
    22     23   </div>