parsav  Check-in [d4ecea913f]

Overview
Comment:add lots more shit
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: d4ecea913fb950b3a9a46867432839060f4dfbaab8854363f7d7d63e4866e634
User & Date: lexi on 2020-12-31 00:15:53
Other Links: manifest | tags
Context
2020-12-31
02:18
start work on user mgmt check-in: db4c5fd644 user: lexi tags: trunk
00:15
add lots more shit check-in: d4ecea913f user: lexi tags: trunk
2020-12-30
02:44
enable profile editing check-in: ac4a630ad5 user: lexi tags: trunk
Changes

Modified acl.t from [3939510d0a] to [7cc6c4467d].

     1      1   -- vim: ft=terra
            2  +local m = {
            3  +	agentkind = lib.enum {
            4  +		'user', 'circle'
            5  +	};
            6  +}
            7  +
            8  +struct m.agent {
            9  +	kind: m.agentkind.t
           10  +	id: uint64
           11  +}
           12  +
           13  +terra m.eval(expr: lib.str.t, agent: m.agent)
           14  +
           15  +end
           16  +
           17  +terra lib.store.post:save(ctupdate: bool)
           18  +-- this post handles the messy details of registering a post's
           19  +-- circles and actors, and increments the edit-count if ctupdate
           20  +-- is true, which is should be in almost all cases.
           21  +	if ctupdate then
           22  +		self.chgcount = self.chgcount + 1
           23  +		self.edited = lib.osclock.time(nil)
           24  +	end
           25  +	-- TODO extract mentions from body, circles from acl
           26  +	self.source:post_save(self)
           27  +end
           28  +
           29  +return m

Modified backend/pgsql.t from [d54604496b] to [af6b4187ca].

   167    167   
   168    168   			values (
   169    169   				(select count(tweets.*)::bigint from tweets),
   170    170   				(select count(follows.*)::bigint from follows),
   171    171   				(select count(followers.*)::bigint from followers),
   172    172   				(select count(mutuals.*)::bigint from mutuals)
   173    173   			)
   174         -		]]):gsub('<(%w+)>',function(r) return tostring(lib.store.relation[r]) end)
          174  +		]]):gsub('<(%w+)>',function(r) return tostring(lib.store.relation.idvmap[r]) end)
   175    175   	};
   176    176   
   177    177   	actor_auth_how = {
   178    178   		params = {rawstring, lib.store.inet}, sql = [[
   179    179   		with mts as (select a.kind from parsav_auth as a
   180    180   			left join parsav_actors as u on u.id = a.uid
   181    181   			where (a.uid is null or u.handle = $1::text or (
................................................................................
   189    189   				(select count(*) from mts where kind like 'otp-%') > 0,
   190    190   				(select count(*) from mts where kind like 'challenge-%') > 0,
   191    191   				(select count(*) from mts where kind = 'trust') > 0
   192    192   		]]; -- cheat
   193    193   	};
   194    194   
   195    195   	actor_session_fetch = {
   196         -		params = {uint64, lib.store.inet}, sql = [[
          196  +		params = {uint64, lib.store.inet, int64}, sql = [[
   197    197   			select a.id, a.nym, a.handle, a.origin, a.bio,
   198    198   			       a.avataruri, a.rank, a.quota, a.key, a.epithet,
   199    199   			       extract(epoch from a.knownsince)::bigint,
   200    200   				   coalesce(a.handle || '@' || s.domain,
   201    201   				            '@' || a.handle) as xid,
   202    202   
   203    203   			       au.restrict,
................................................................................
   209    209   						array['admin' ] <@ au.restrict as can_admin
   210    210   
   211    211   			from      parsav_auth au
   212    212   			left join parsav_actors a     on au.uid = a.id
   213    213   			left join parsav_servers s    on a.origin = s.id
   214    214   
   215    215   			where au.aid = $1::bigint and au.blacklist = false and
   216         -				(au.netmask is null or au.netmask >> $2::inet)
          216  +				(au.netmask is null or au.netmask >> $2::inet) and
          217  +				($3::bigint = 0 or --slightly abusing the epoch time fmt here, but
          218  +					((a.authtime   is null or a.authtime   <= to_timestamp($3::bigint)) and
          219  +					 (au.valperiod is null or au.valperiod <= to_timestamp($3::bigint))))
   217    220   		]];
   218    221   	};
   219    222   
   220    223   	actor_powers_fetch = {
   221    224   		params = {uint64}, sql = [[
   222    225   			select key, allow from parsav_rights where actor = $1::bigint
   223    226   		]]
................................................................................
   234    237   	actor_power_delete = {
   235    238   		params = {uint64,lib.mem.ptr(int8)}, cmd = true, sql = [[
   236    239   			delete from parsav_rights where
   237    240   				actor = $1::bigint and
   238    241   				key = $2::text
   239    242   		]]
   240    243   	};
          244  +
          245  +	auth_sigtime_user_fetch = {
          246  +		params = {uint64}, sql = [[
          247  +			select extract(epoch from authtime)::bigint
          248  +			from parsav_actors where id = $1::bigint
          249  +		]];
          250  +	};
          251  +
          252  +	auth_sigtime_user_alter = {
          253  +		params = {uint64,int64}, cmd = true, sql = [[
          254  +			update parsav_actors set
          255  +				authtime = to_timestamp($2::bigint)
          256  +				where id = $1::bigint
          257  +		]];
          258  +	};
   241    259   
   242    260   	auth_create_pw = {
   243    261   		params = {uint64, lib.mem.ptr(uint8)}, cmd = true, sql = [[
   244    262   			insert into parsav_auth (uid, name, kind, cred) values (
   245    263   				$1::bigint,
   246    264   				(select handle from parsav_actors where id = $1::bigint),
   247    265   				'pw-sha256', $2::bytea
................................................................................
   252    270   	auth_purge_type = {
   253    271   		params = {rawstring, uint64, rawstring}, cmd = true, sql = [[
   254    272   			delete from parsav_auth where
   255    273   				((uid = 0 and name = $1::text) or uid = $2::bigint) and
   256    274   				kind like $3::text
   257    275   		]]
   258    276   	};
          277  +
          278  +	post_save = {
          279  +		params = {
          280  +			uint64, uint32, int64;
          281  +			rawstring, rawstring, rawstring;
          282  +		}, cmd = true, sql = [[
          283  +			update parsav_posts set
          284  +				subject = $4::text,
          285  +				acl = $5::text,
          286  +				body = $6::text,
          287  +				chgcount = $2::integer,
          288  +				edited = to_timestamp($3::bigint)
          289  +			where id = $1::bigint
          290  +		]]
          291  +	};
   259    292   
   260    293   	post_create = {
   261    294   		params = {uint64, rawstring, rawstring, rawstring}, sql = [[
   262    295   			insert into parsav_posts (
   263    296   				author, subject, acl, body,
   264    297   				posted, discovered,
   265    298   				circles, mentions
................................................................................
   266    299   			) values (
   267    300   				$1::bigint, case when $2::text = '' then null else $2::text end,
   268    301   				$3::text, $4::text, 
   269    302   				now(), now(), array[]::bigint[], array[]::bigint[]
   270    303   			) returning id
   271    304   		]]; -- TODO array handling
   272    305   	};
          306  +
          307  +	post_fetch = {
          308  +		params = {uint64}, sql = [[
          309  +			select a.origin is null,
          310  +				p.id, p.author, p.subject, p.acl, p.body,
          311  +				extract(epoch from p.posted    )::bigint,
          312  +				extract(epoch from p.discovered)::bigint,
          313  +				extract(epoch from p.edited    )::bigint,
          314  +				p.parent, p.convoheaduri, p.chgcount
          315  +			from parsav_posts as p
          316  +				inner join parsav_actors as a on p.author = a.id
          317  +			where p.id = $1::bigint
          318  +		]];
          319  +	};
   273    320   
   274    321   	post_enum_author_uid = {
   275    322   		params = {uint64,uint64,uint64,uint64, uint64}, sql = [[
   276    323   			select a.origin is null,
   277    324   				p.id, p.author, p.subject, p.acl, p.body,
   278    325   				extract(epoch from p.posted    )::bigint,
   279    326   				extract(epoch from p.discovered)::bigint,
   280         -				p.parent, p.convoheaduri
          327  +				extract(epoch from p.edited    )::bigint,
          328  +				p.parent, p.convoheaduri, p.chgcount
   281    329   			from parsav_posts as p
   282    330   				inner join parsav_actors as a on p.author = a.id
   283    331   			where p.author = $5::bigint and
   284    332   				($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and
   285    333   				($2::bigint = 0 or to_timestamp($2::bigint) < p.posted)
   286    334   			order by (p.posted, p.discovered) desc
   287    335   			limit case when $3::bigint = 0 then null
................................................................................
   294    342   
   295    343   	timeline_instance_fetch = {
   296    344   		params = {uint64, uint64, uint64, uint64}, sql = [[
   297    345   			select true,
   298    346   				p.id, p.author, p.subject, p.acl, p.body,
   299    347   				extract(epoch from p.posted    )::bigint,
   300    348   				extract(epoch from p.discovered)::bigint,
   301         -				p.parent, null::text
          349  +				extract(epoch from p.edited    )::bigint,
          350  +				p.parent, null::text, p.chgcount
   302    351   			from parsav_posts as p
   303    352   				inner join parsav_actors as a on p.author = a.id
   304    353   			where
   305    354   				($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and
   306    355   				($2::bigint = 0 or to_timestamp($2::bigint) < p.posted) and
   307    356   				(a.origin is null)
   308    357   			order by (p.posted, p.discovered) desc
................................................................................
   565    614   local terra row_to_post(r: &pqr, row: intptr): lib.mem.ptr(lib.store.post)
   566    615   	var subj: rawstring, sblen: intptr
   567    616   	var cvhu: rawstring, cvhlen: intptr
   568    617   	if r:null(row,3)
   569    618   		then subj = nil sblen = 0
   570    619   		else subj = r:string(row,3) sblen = r:len(row,3)+1
   571    620   	end
   572         -	if r:null(row,9)
          621  +	if r:null(row,10)
   573    622   		then cvhu = nil cvhlen = 0
   574         -		else cvhu = r:string(row,9) cvhlen = r:len(row,9)+1
          623  +		else cvhu = r:string(row,10) cvhlen = r:len(row,10)+1
   575    624   	end
   576    625   	var p = [ lib.str.encapsulate(lib.store.post, {
   577    626   		subject = { `subj, `sblen };
   578    627   		acl = {`r:string(row,4), `r:len(row,4)+1};
   579    628   		body = {`r:string(row,5), `r:len(row,5)+1};
   580    629   		convoheaduri = { `cvhu, `cvhlen }; --FIXME
   581    630   	}) ]
   582    631   	p.ptr.id = r:int(uint64,row,1)
   583    632   	p.ptr.author = r:int(uint64,row,2)
   584    633   	p.ptr.posted = r:int(uint64,row,6)
   585    634   	p.ptr.discovered = r:int(uint64,row,7)
   586         -	if r:null(row,8)
          635  +	p.ptr.edited = r:int(uint64,row,8)
          636  +	if r:null(row,9)
   587    637   		then p.ptr.parent = 0
   588         -		else p.ptr.parent = r:int(uint64,row,8)
          638  +		else p.ptr.parent = r:int(uint64,row,9)
          639  +	end 
          640  +	if r:null(row,11)
          641  +		then p.ptr.chgcount = 0
          642  +		else p.ptr.chgcount = r:int(uint32,row,11)
   589    643   	end 
   590    644   	p.ptr.localpost = r:bool(row,0)
   591    645   
   592    646   	return p
   593    647   end
   594    648   local terra row_to_actor(r: &pqr, row: intptr): lib.mem.ptr(lib.store.actor)
   595    649   	var a: lib.mem.ptr(lib.store.actor)
................................................................................
   918    972   		s.mutuals = r:int(uint64, 0, 3)
   919    973   		return s
   920    974   	end];
   921    975   
   922    976   	actor_session_fetch = [terra(
   923    977   		src: &lib.store.source,
   924    978   		aid: uint64,
   925         -		ip : lib.store.inet
          979  +		ip : lib.store.inet,
          980  +		issuetime: lib.store.timepoint
   926    981   	): { lib.stat(lib.store.auth), lib.mem.ptr(lib.store.actor) }
   927         -		var r = queries.actor_session_fetch.exec(src, aid, ip)
          982  +		var r = queries.actor_session_fetch.exec(src, aid, ip, issuetime)
   928    983   		if r.sz == 0 then goto fail end
   929    984   		do defer r:free()
   930    985   
   931    986   			if r:null(0,0) then goto fail end
   932    987   
   933    988   			var a = row_to_actor(&r, 0)
   934    989   			a.ptr.source = src
................................................................................
   959   1014   	): uint64
   960   1015   		var r = queries.post_create.exec(src,post.author,post.subject,post.acl,post.body) 
   961   1016   		if r.sz == 0 then return 0 end
   962   1017   		defer r:free()
   963   1018   		var id = r:int(uint64,0,0)
   964   1019   		return id
   965   1020   	end];
         1021  +
         1022  +	post_fetch = [terra(
         1023  +		src: &lib.store.source,
         1024  +		post: uint64
         1025  +	): lib.mem.ptr(lib.store.post)
         1026  +		var r = queries.post_fetch.exec(src, post)
         1027  +		if r.sz == 0 then return [lib.mem.ptr(lib.store.post)].null() end
         1028  +		var p = row_to_post(&r, 0)
         1029  +		p.ptr.source = src
         1030  +		return p
         1031  +	end];
   966   1032   
   967   1033   	timeline_instance_fetch = [terra(src: &lib.store.source, rg: lib.store.range)
   968   1034   		var r = pqr { sz = 0 }
   969   1035   		var A,B,C,D = rg:matrix() -- :/
   970   1036   		r = queries.timeline_instance_fetch.exec(src,A,B,C,D)
   971   1037   		
   972   1038   		var ret: lib.mem.ptr(lib.mem.ptr(lib.store.post)) ret:init(r.sz)
................................................................................
  1106   1172   		detach: bool
  1107   1173   	): {}
  1108   1174   		if detach
  1109   1175   			then queries.post_attach_ctl_del.exec(src,post,artifact)
  1110   1176   			else queries.post_attach_ctl_ins.exec(src,post,artifact)
  1111   1177   		end
  1112   1178   	end];
         1179  +	
         1180  +	post_save = [terra(
         1181  +		src: &lib.store.source,
         1182  +		post: &lib.store.post
         1183  +	): {}
         1184  +		queries.post_save.exec(src,
         1185  +			post.id, post.chgcount, post.edited,
         1186  +			post.subject, post.acl, post.body)
         1187  +	end];
         1188  +
         1189  +	auth_sigtime_user_fetch = [terra(
         1190  +		src: &lib.store.source,
         1191  +		uid: uint64
         1192  +	): lib.store.timepoint
         1193  +		var r = queries.auth_sigtime_user_fetch.exec(src, uid)
         1194  +		if r.sz > 0 then defer r:free()
         1195  +			var t = r:int(int64,0,0)
         1196  +			return t
         1197  +		else return 0 end
         1198  +	end];
         1199  +
         1200  +	auth_sigtime_user_alter = [terra(
         1201  +		src: &lib.store.source,
         1202  +		uid: uint64,
         1203  +		time: lib.store.timepoint
         1204  +	): {} queries.auth_sigtime_user_alter.exec(src, uid, time) end];
  1113   1205   
  1114   1206   	actor_auth_register_uid = nil; -- TODO better support non-view based auth
  1115   1207   }
  1116   1208   
  1117   1209   return b

Modified backend/schema/pgsql-auth.sql from [1170b3857b] to [8bbbf24f23].

    42     42   		-- if the credential matches, access will be denied, even if
    43     43   		-- non-blacklisted credentials match. most useful with
    44     44   		-- uid = null, kind = trust, cidr = (untrusted IP range)
    45     45   
    46     46   	valperiod timestamp default now(),
    47     47   		-- cookies bearing timestamps earlier than this point in time
    48     48   		-- will be considered invalid and will not grant access
           49  +	
           50  +	comment text,
           51  +		-- a field the user can use to identify the specific credential,
           52  +		-- in order to aid credential management
    49     53   
    50     54   	unique(name,kind,cred)
    51     55   );

Modified backend/schema/pgsql.sql from [cb277a93b1] to [135f2b367a].

    24     24   
    25     25   create table parsav_actors (
    26     26   	id        bigint primary key default (1+random()*(2^63-1))::bigint,
    27     27   	nym       text,
    28     28   	handle    text not null, -- nym [@handle@origin] 
    29     29   	origin    bigint references parsav_servers(id)
    30     30   		on delete cascade, -- null origin = local actor
    31         -	knownsince timestamp,
           31  +	knownsince timestamp not null default now(),
    32     32   	bio       text,
    33     33   	avatarid  bigint, -- artifact id, null if remote
    34     34   	avataruri text, -- null if local
    35     35   	rank      smallint not null default 0,
    36     36   	quota     integer not null default 1000,
    37     37   	key       bytea, -- private if localactor; public if remote
    38     38   	epithet   text,
................................................................................
    56     56   	author     bigint references parsav_actors(id)
    57     57   		on delete cascade,
    58     58   	subject    text,
    59     59   	acl        text not null default 'all', -- just store the script raw 🤷
    60     60   	body       text,
    61     61   	posted     timestamp not null,
    62     62   	discovered timestamp not null,
           63  +	chgcount   integer not null default 0,
           64  +	edited     timestamp,
    63     65   	parent     bigint not null default 0, -- if post: part of conversation; if chatroom: top-level post
    64     66   	circles    bigint[], -- TODO at edit or creation, iterate through each circle
    65     67   	mentions   bigint[], -- a user has, check if it can see her post, and if so add
    66     68   	artifacts  bigint[],
    67     69   
    68     70   	convoheaduri text
    69     71   	-- only used for tracking foreign conversations and tying them to post heads;

Modified cmdparse.t from [49c267b075] to [dec7841af5].

    21     21   		for o,desc in pairs(tbl) do
    22     22   			local consume = desc.consume or 0
    23     23   			local incr = desc.inc or 0
    24     24   			options.entries[#options.entries + 1] = {
    25     25   				field = o, type = (consume > 0) and &rawstring or
    26     26   				                  (incr    > 0) and uint       or bool
    27     27   			}
    28         -			helpstr = helpstr .. string.format('    -%s --%s: %s\n',
    29         -				desc[1], sanitize(o), desc[2])
           28  +			if desc[1] then
           29  +				helpstr = helpstr .. string.format('    -%s --%s: %s\n',
           30  +					desc[1], sanitize(o), desc[2])
           31  +			else
           32  +				helpstr = helpstr .. string.format('       --%s: %s\n',
           33  +					sanitize(o), desc[2])
           34  +			end
    30     35   		end
    31     36   		for o,desc in pairs(tbl) do
    32     37   			local flag = desc[1]
    33     38   			local consume = desc.consume or 0
    34     39   			local incr = desc.inc or 0
    35     40   			init[#init + 1] = quote [self].[o] = [
    36     41   				(consume > 0 and `nil) or 
................................................................................
    48     53   					end
    49     54   				end
    50     55   			elseif incr > 0 then
    51     56   				ch = quote [self].[o] = [self].[o] + incr end
    52     57   			else ch = quote
    53     58   				[self].[o] = true
    54     59   			end end
    55         -			shortcases[#shortcases + 1] = quote
    56         -				case [int8]([string.byte(flag)]) then [ch] end
           60  +			if flag ~= nil then
           61  +				shortcases[#shortcases + 1] = quote
           62  +					case [int8]([string.byte(flag)]) then [ch] end
           63  +				end
    57     64   			end
    58     65   			longcases[#longcases + 1] = quote
    59     66   				if lib.str.cmp([arg]+2, [sanitize(o)]) == 0 then [ch] goto [skip] end
    60     67   			end
    61     68   		end
    62     69   		terra options:free() self.arglist:free() end
    63     70   		options.methods.parse = terra([self], [argc], [argv])

Modified config.lua from [931922a3e1] to [3d0e5432a4].

    44     44   		when = os.date();
    45     45   	};
    46     46   	feat = {};
    47     47   	debug = u.tobool(default('parsav_enable_debug',true)); 
    48     48   	backends = defaultlist('parsav_backends', 'pgsql');
    49     49   	braingeniousmode = false;
    50     50   	embeds = {
           51  +		-- TODO with gzip compression, svg is dramatically superior to webp
           52  +		-- we should have a build-time option to serve svg so instances
           53  +		-- proxied behind nginx can serve svgz, or possibly just straight-up
           54  +		-- add support for content-encoding headers and pre-compress the
           55  +		-- damn things before compiling
    51     56   		{'style.css', 'text/css'};
    52     57   		{'default-avatar.webp', 'image/webp'};
    53     58   		{'padlock.webp', 'image/webp'};
    54     59   		{'warn.webp', 'image/webp'};
           60  +		{'query.webp', 'image/webp'};
    55     61   	};
    56     62   }
    57     63   if os.getenv('parsav_let_me_be_an_idiot') == "i know what i'm doing" then
    58     64   	conf.braingeniousmode = true -- SOUND GENERAL QUARTERS
    59     65   end
    60     66   if u.ping '.fslckout' or u.ping '_FOSSIL_' then
    61     67   	if u.ping '_FOSSIL_' then default_os = 'windows' end

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

     1      1   -- vim: ft=terra
     2      2   local m = {
     3      3   	shorthand = {maxlen = 14}
     4      4   }
            5  +
            6  +local pstring = lib.mem.ptr(int8)
     5      7   
     6      8   -- swap in place -- faster on little endian
     7      9   m.netswap_ip = macro(function(ty, src, dest)
     8     10   	if ty:astype().type ~= 'integer' then error('bad type') end
     9     11   	local bytes = ty:astype().bytes
    10     12   	src = `[&uint8](src)
    11     13   	dest = `[&uint8](dest)
................................................................................
   182    184   		else dgtct = dgtct + 1 end
   183    185   	end else
   184    186   		buf = buf - 1
   185    187   		@buf = 0x30
   186    188   	end
   187    189   	return buf
   188    190   end
          191  +
          192  +terra m.ndigits(n: intptr, base: intptr): intptr
          193  +	var c = base
          194  +	var i = 1
          195  +	while true do
          196  +		if n < c then return i end
          197  +		c = c * base
          198  +		i = i + 1
          199  +	end
          200  +end
          201  +
          202  +terra m.fsz_parse(f: pstring): {intptr, bool}
          203  +-- take a string representing a file size and return {nbytes, true}
          204  +-- or {0, false} if the parse fails
          205  +	if f.ct == 0 then f.ct = lib.str.sz(f.ptr) end
          206  +	var sz: intptr = 0
          207  +	for i = 0, f.ct do
          208  +		if f(i) == @',' then goto skip end
          209  +		if f(i) >= 0x30 and f(i) <= 0x39 then
          210  +			sz = sz * 10
          211  +			sz = sz + f(i) - 0x30
          212  +		else
          213  +			if i+1 == f.ct or f(i) == 0 then return sz, true end
          214  +			if i+2 == f.ct or f(i+1) == 0 then
          215  +				if f(i) == @'b' then return sz/8, true end -- bits
          216  +			else
          217  +				var s: intptr = 0
          218  +				if i+3 == f.ct or f(i+2) == 0 then 
          219  +					s = i + 1
          220  +				elseif (i+4 == f.ct or f(i+3) == 0) and f(i+1) == @'i' then
          221  +				-- grudgingly tolerate ~mebibits~ and its ilk, without
          222  +				-- affecting the result in any way
          223  +					s = i + 2
          224  +				else return 0, false end
          225  +
          226  +				if f(s) == @'b' then sz = sz/8 -- bits
          227  +				elseif f(s) ~= @'B' then return 0, false end -- wth
          228  +			end
          229  +			var c = f(i)
          230  +			if c >= @'A' and c <= @'Z' then c = c - 0x20 end
          231  +			switch c do -- normal char literal syntax doesn't work here, leads to llvm error (!!)
          232  +				case [uint8]([string.byte('k')]) then return sz * [1024ULL ^ 1], true end
          233  +				case [uint8]([string.byte('m')]) then return sz * [1024ULL ^ 2], true end
          234  +				case [uint8]([string.byte('g')]) then return sz * [1024ULL ^ 3], true end
          235  +				case [uint8]([string.byte('t')]) then return sz * [1024ULL ^ 4], true end
          236  +				case [uint8]([string.byte('e')]) then return sz * [1024ULL ^ 5], true end
          237  +				case [uint8]([string.byte('y')]) then return sz * [1024ULL ^ 6], true end
          238  +				else return sz, true
          239  +			end
          240  +		end
          241  +	::skip::end
          242  +	return sz, true
          243  +end
   189    244   
   190    245   return m

Modified mgtool.t from [03b0c72484] to [faf7450a82].

   297    297   
   298    298   				if dbmode.arglist.ct == 1 then
   299    299   					lib.bail('you are attempting to completely obliterate all data! make sure you have selected your target correctly. if you really want to do this, pass the confirmation string ', &cfmstr[0])
   300    300   				elseif dbmode.arglist.ct == 2 then
   301    301   					if lib.str.cmp(dbmode.arglist(1), cfmstr) == 0 then
   302    302   						lib.warn('completely obliterating all data!')
   303    303   						dlg:obliterate_everything()
          304  +					elseif lib.str.cmp(dbmode.arglist(1), 'print-confirmation-string') == 0 then
          305  +						lib.io.send(1, cfmstr, lib.str.sz(cfmstr))
   304    306   					else
   305    307   						lib.bail('you passed an incorrect confirmation string; pass ', &cfmstr[0], ' if you really want to destroy everything')
   306    308   					end
   307    309   				else goto cmderr end
   308    310   			else goto cmderr end
   309    311   		elseif lib.str.cmp(mode.arglist(0),'be') == 0 then
   310    312   			srv:setup(cnf) 
................................................................................
   329    331   			if cfmode.arglist.ct < 1 then goto cmderr end
   330    332   
   331    333   			if cfmode.arglist.ct == 1 then
   332    334   				if lib.str.cmp(cfmode.arglist(0),'chsec') == 0 then
   333    335   					var sec: int8[65] gensec(&sec[0])
   334    336   					dlg:conf_set('server-secret', &sec[0])
   335    337   					lib.report('server secret reset')
   336         -					-- FIXME notify server to reload its config
   337    338   				elseif lib.str.cmp(cfmode.arglist(0),'refresh') == 0 then
   338         -					-- TODO notify server to reload config
          339  +					cfmode.no_notify = false -- duh
   339    340   				else goto cmderr end
   340    341   			elseif cfmode.arglist.ct == 3 and
   341    342   				lib.str.cmp(cfmode.arglist(0),'set') == 0 then
   342    343   				dlg:conf_set(cfmode.arglist(1),cfmode.arglist(2))
   343    344   				lib.report('parameter set')
   344    345   			else goto cmderr end
          346  +
          347  +			-- successful commands fall through
          348  +			if not cfmode.no_notify then
          349  +				dlg:ipc_send(lib.ipc.cmd.cfgrefresh,0)
          350  +			end
   345    351   		else
   346    352   			srv:setup(cnf) 
   347    353   			srv:conprep(lib.store.prepmode.full)
   348    354   			if lib.str.cmp(mode.arglist(0),'mkroot') == 0 then
   349    355   				var cfmode: pbasic cfmode:parse(mode.arglist.ct, &mode.arglist(0))
   350    356   				if cfmode.help then
   351    357   					[ lib.emit(false, 1, 'usage: ', `argv[0], ' mkroot ', cfmode.type.helptxt.flags, ' <handle>', cfmode.type.helptxt.opts) ]
................................................................................
   364    370   					var root = lib.store.actor.mk(&kbuf[0])
   365    371   					root.handle = cfmode.arglist(0)
   366    372   					var epithets = array(
   367    373   						'root', 'god', 'regional jehovah', 'titan king',
   368    374   						'king of olympus', 'cyberpharaoh', 'electric ellimist',
   369    375   						"rampaging c'tan", 'deathless tweetlord', 'postmaster',
   370    376   						'faerie queene', 'lord of the posts', 'ruthless cybercrat',
   371         -						'general secretary', 'commissar', 'kwisatz haderach'
          377  +						'general secretary', 'commissar', 'kwisatz haderach',
          378  +						'dedicated hyperturing'
   372    379   						-- feel free to add more
   373    380   					)
   374    381   					root.epithet = epithets[lib.crypt.random(intptr,0,[epithets.type.N])]
   375    382   					root.rights.powers:fill() -- grant omnipotence
   376    383   					root.rights.rank = 1
   377    384   					var ruid = dlg:actor_create(&root)
   378    385   					dlg:conf_set('master',root.handle)

Modified parsav.t from [dc97a3f269] to [022b1bf037].

   125    125   			var [val]
   126    126   			[exp]
   127    127   		in val end
   128    128   		return q
   129    129   	end);
   130    130   	proc = {
   131    131   		fork = terralib.externfunction('fork', {} -> int);
   132         -		daemonize = terralib.externfunction('daemon', {int,int} -> {});
   133    132   		exit = terralib.externfunction('exit', int -> {});
   134    133   		getenv = terralib.externfunction('getenv', rawstring -> rawstring);
   135    134   		exec = terralib.externfunction('execv', {rawstring,&rawstring} -> int);
   136    135   		execp = terralib.externfunction('execvp', {rawstring,&rawstring} -> int);
   137    136   	};
   138    137   	io = {
   139    138   		send = terralib.externfunction('write', {int, rawstring, intptr} -> ptrdiff);
   140    139   		recv = terralib.externfunction('read',  {int, rawstring, intptr} -> ptrdiff);
   141    140   		close = terralib.externfunction('close', {int} -> int);
   142    141   		say = macro(function(msg) return `lib.io.send(2, msg, [#(msg:asvalue())]) end);
   143    142   		fmt = terralib.externfunction('printf',
   144    143   			terralib.types.funcpointer({rawstring},{int},true));
          144  +		ttyp = terralib.externfunction('isatty', int -> int);
   145    145   	};
   146    146   	str = { sz = terralib.externfunction('strlen', rawstring -> intptr) };
   147    147   	copy = function(tbl)
   148    148   		local new = {}
   149    149   		for k,v in pairs(tbl) do new[k] = v end
   150    150   		setmetatable(new, getmetatable(tbl))
   151    151   		return new
................................................................................
   273    273   					then lib.io.say([' - ' .. v .. ': true\n'])
   274    274   					else lib.io.say([' - ' .. v .. ': false\n'])
   275    275   				end
   276    276   			end
   277    277   		end
   278    278   		return q
   279    279   	end)
          280  +	terra set:setbit(i: intptr, val: bool)
          281  +		if val then
          282  +			self._store[i/8] = self._store[i/8] or (1 << (i % 8))
          283  +		else
          284  +			self._store[i/8] = self._store[i/8] and not (1 << (i % 8))
          285  +		end
          286  +	end
          287  +	set.bits = {}
          288  +	set.idvmap = {}
          289  +	for i,v in ipairs(tbl) do
          290  +		set.idvmap[v] = i
          291  +		set.bits[v] = quote var b: set b:clear() b:setbit(i, true) in b end
          292  +	end
   280    293   	set.metamethods.__add = macro(function(self,other)
   281    294   		local new = symbol(set)
   282    295   		local q = quote var [new] new:clear() end
   283    296   		for i = 0, bytes - 1 do
   284    297   			q = quote [q]
   285    298   				new._store[i] = self._store[i] or other._store[i]
   286    299   			end
................................................................................
   292    305   		local q = quote var [new] new:clear() end
   293    306   		for i = 0, bytes - 1 do
   294    307   			q = quote [q]
   295    308   				new._store[i] = self._store[i] and other._store[i]
   296    309   			end
   297    310   		end
   298    311   		return quote [q] in new end
          312  +	end)
          313  +	set.metamethods.__eq = macro(function(self,other)
          314  +		local rt = symbol(bool)
          315  +		local fb if #tbl % 8 == 0 then fb = bytes - 1 else fb = bytes - 2 end
          316  +		local q = quote rt = true end
          317  +		for i = 0, fb do
          318  +			q = quote
          319  +				if self._store[i] ~= other._store[i] then rt = false else [q] end
          320  +			end
          321  +		end
          322  +		-- we need to mask out any extraneous bits the values might have, as we
          323  +		-- don't want the kind of noise introduced by :fill() to affect comparison
          324  +		if #tbl % 8 ~= 0 then
          325  +			local last = #tbl-1
          326  +			local msk = (2 ^ (#tbl % 8)) - 1
          327  +			q = quote
          328  +				if (self._store [last] and [uint8](msk)) ~=
          329  +				   (other._store[last] and [uint8](msk)) then rt = false else [q] end
          330  +			end
          331  +		end
          332  +		return quote var [rt]; [q] in rt end
   299    333   	end)
   300    334   	set.metamethods.__not = macro(function(self)
   301    335   		local new = symbol(set)
   302    336   		local q = quote var [new] new:clear() end
   303    337   		for i = 0, bytes - 1 do
   304    338   			q = quote [q]
   305    339   				new._store[i] = not self._store[i] 
................................................................................
   345    379   lib.md = lib.loadlib('mbedtls','mbedtls/md.h')
   346    380   lib.b64 = lib.loadlib('mbedtls','mbedtls/base64.h')
   347    381   lib.net = lib.loadlib('mongoose','mongoose.h')
   348    382   lib.pq = lib.loadlib('libpq','libpq-fe.h')
   349    383   
   350    384   lib.load {
   351    385   	'mem', 'math', 'str', 'file', 'crypt', 'ipc';
   352         -	'http', 'html', 'session', 'tpl', 'store';
          386  +	'http', 'html', 'session', 'tpl', 'store', 'acl';
   353    387   
   354    388   	'smackdown'; -- md-alike parser
   355    389   }
   356    390   
   357    391   local be = {}
   358    392   for _, b in pairs(config.backends) do
   359    393   	be[#be+1] = terralib.loadfile('backend/' .. b .. '.t')()
................................................................................
   387    421   
   388    422   lib.load {
   389    423   	'srv';
   390    424   	'render:nav';
   391    425   	'render:nym';
   392    426   	'render:login';
   393    427   	'render:profile';
   394         -
   395    428   	'render:compose';
   396    429   	'render:tweet';
   397         -	'render:userpage';
          430  +	'render:tweet-page';
          431  +	'render:user-page';
   398    432   	'render:timeline';
   399    433   
   400    434   	'render:docpage';
   401    435   
   402    436   	'render:conf:profile';
          437  +	'render:conf:sec';
   403    438   	'render:conf';
   404    439   	'route';
   405    440   }
   406    441   
   407    442   do
   408    443   	local p = string.format('parsav: %s\nbuilt on %s\n', config.build.str, config.build.when)
   409    444   	terra version() lib.io.send(1, p, [#p]) end

Modified render/compose.t from [cb3a66bab9] to [13509724e6].

     1      1   -- vim: ft=terra
     2      2   local terra 
     3         -render_compose(co: &lib.srv.convo, edit: &lib.store.post)
            3  +render_compose(co: &lib.srv.convo, edit: &lib.store.post, acc: &lib.str.acc)
     4      4   	var target, tgtlen = co:getv('to')
     5      5   	var form: data.view.compose
            6  +	form = data.view.compose {
            7  +		handle = co.who.handle;
            8  +		circles = ''; -- TODO: list user's circles, rooms, and saved aclexps
            9  +	}
     6     10   	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         -			circles = ''; -- TODO: list user's circles, rooms, and saved aclexps
    12         -		}
           11  +		form.content = lib.coalesce(target, '')
           12  +		form.acl = lib.trn(target == nil, 'all', 'mentioned') -- TODO default acl setting?
           13  +	else
           14  +		form.content = lib.coalesce(edit.body, '')
           15  +		form.acl = edit.acl
    13     16   	end
           17  +	if acc ~= nil then form:append(acc) return end 
           18  +
    14     19   	var cotxt = form:tostr() defer cotxt:free()
    15     20   
    16         -	var doc = data.view.docskel {
    17         -		instance = co.srv.cfg.instance;
           21  +	var doc = [lib.srv.convo.page] {
    18     22   		title = lib.str.plit 'compose';
    19     23   		body = cotxt;
    20     24   		class = lib.str.plit 'compose';
    21         -		navlinks = co.navbar;
           25  +		cache = true;
    22     26   	}
    23     27   
    24         -	var hdrs = array(
    25         -		lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' }
    26         -	)
    27         -	doc:send(co.con,200,[lib.mem.ptr(lib.http.header)] {ct = 1, ptr = &hdrs[0]})
           28  +	co:stdpage(doc)
    28     29   end
    29     30   
    30     31   return render_compose

Modified render/conf/profile.t from [248ab207d4] to [7f970c2f4a].

     4      4   
     5      5   local terra cs(s: rawstring)
     6      6   	return pstr { ptr = s, ct = lib.str.sz(s) }
     7      7   end
     8      8   
     9      9   local terra 
    10     10   render_conf_profile(co: &lib.srv.convo, path: lib.mem.ptr(pref)): pstr
    11         -
    12     11   	var c = data.view.conf_profile {
    13     12   		handle = cs(co.who.handle);
    14     13   		nym = cs(lib.coalesce(co.who.nym,''));
    15     14   		bio = cs(lib.coalesce(co.who.bio,''));
    16     15   	}
    17     16   	return c:tostr()
    18     17   end
    19     18   
    20     19   return render_conf_profile

Added render/conf/sec.t version [2ed8642241].

            1  +-- vim: ft=terra
            2  +local pstr = lib.mem.ptr(int8)
            3  +local pref = lib.mem.ref(int8)
            4  +local terra 
            5  +render_conf_sec(co: &lib.srv.convo, path: lib.mem.ptr(pref)): pstr
            6  +	var time: lib.store.timepoint = co.who.source:auth_sigtime_user_fetch(co.who.id)
            7  +	var tstr: int8[26]
            8  +	lib.osclock.ctime_r(&time, &tstr[0])
            9  +	var body = data.view.conf_sec {
           10  +		lastreset = pstr {
           11  +			ptr = &tstr[0], ct = lib.str.sz(&tstr[0])
           12  +		}
           13  +	}
           14  +	
           15  +	if co.srv.cfg.credmgd then
           16  +		var a: lib.str.acc a:init(768)
           17  +		body:append(&a)
           18  +		var credmgr = data.view.conf_sec_credmg {
           19  +			credlist = '<option>your password</option>'
           20  +		}
           21  +		credmgr:append(&a)
           22  +		return a:finalize()
           23  +	else return body:tostr() end
           24  +end
           25  +return render_conf_sec

Modified render/docpage.t from [3eda6110a6] to [148acf7303].

    67     67   
    68     68   local terra 
    69     69   pushbranches(list: &lib.str.acc, idx: intptr, ps: lib.store.powerset): {}
    70     70   	var [pages] = array([allpages])
    71     71   	var started = false
    72     72   	for i=0,[pages.type.N] do
    73     73   		if pages[i].parent == idx+1 and (pages[i].priv:sz() == 0 or 
    74         -			(ps and pages[i].priv):sz() > 0) then
           74  +				(ps and pages[i].priv) == pages[i].priv) then
    75     75   			if not started then
    76     76   				started = true
    77     77   				list:lpush('<ul>')
    78     78   			end
    79     79   			list:lpush('<li><a href="/doc/'):rpush(pages[i].name):lpush('">')
    80     80   				:rpush(pages[i].title):lpush('</a>')
    81     81   			pushbranches(list, i, ps)

Modified render/profile.t from [19457b4b7c] to [ae13f6f2b7].

     3      3   local terra cs(s: rawstring)
     4      4   	return pstr { ptr = s, ct = lib.str.sz(s) }
     5      5   end
     6      6   
     7      7   local terra 
     8      8   render_profile(co: &lib.srv.convo, actor: &lib.store.actor)
     9      9   	var aux: lib.str.acc
           10  +	var followed = true -- FIXME
    10     11   	if co.aid ~= 0 and co.who.id == actor.id then
    11         -		aux:compose('<a href="/conf/profile?go=/',actor.xid,'">alter</a>')
           12  +		aux:compose('<a class="button" href="/conf/profile?go=/',actor.xid,'">alter</a>')
    12     13   	elseif co.aid ~= 0 then
    13         -		aux:compose('<a href="/', actor.xid, '/follow">follow</a><a href="/',
    14         -			actor.xid, '/chat">chat</a>')
           14  +		if not followed then
           15  +			aux:compose('<button method="post" name="act" value="follow">follow</a>')
           16  +		elseif not followed then
           17  +			aux:compose('<button method="post" name="act" value="unfollow">unfollow</a>')
           18  +		end
           19  +		aux:lpush('<a href="/'):push(actor.xid,0):lpush('/chat">chat</a>')
    15     20   		if co.who.rights.powers:affect_users() then
    16         -			aux:lpush('<a href="/'):push(actor.xid,0):lpush('/ctl">control</a>')
           21  +			aux:lpush('<a class="button" href="/'):push(actor.xid,0):lpush('/ctl">control</a>')
    17     22   		end
    18     23   	else
    19         -		aux:compose('<a href="/', actor.xid, '/follow">remote follow</a>')
           24  +		aux:compose('<a class="button" href="/', actor.xid, '/follow">remote follow</a>')
    20     25   	end
    21     26   	var auxp = aux:finalize()
    22     27   	var avistr: lib.str.acc if actor.origin == 0 then
    23     28   		avistr:compose('/avi/',actor.handle)
    24     29   	end
    25     30   	var timestr: int8[26] lib.osclock.ctime_r(&actor.knownsince, &timestr[0])
    26     31   

Added render/tweet-page.t version [ad592d16bd].

            1  +-- vim: ft=terra
            2  +local pstr = lib.mem.ptr(int8)
            3  +local pref = lib.mem.ref(int8)
            4  +local terra cs(s: rawstring)
            5  +	return pstr { ptr = s, ct = lib.str.sz(s) }
            6  +end
            7  +
            8  +local terra 
            9  +render_tweet_page(
           10  +	co: &lib.srv.convo,
           11  +	path: lib.mem.ptr(pref),
           12  +	p: &lib.store.post
           13  +): {}
           14  +	var pg: lib.str.acc pg:init(256)
           15  +	lib.render.tweet(co, p, &pg)
           16  +	pg:lpush('<form class="action-bar" method="post">')
           17  +
           18  +	if co.aid ~= 0 then
           19  +		var liked = false -- FIXME
           20  +		var rtd = false
           21  +		if not liked
           22  +			then pg:lpush('<button class="pos" name="act" value="like">like</button>')
           23  +			else pg:lpush('<button class="neg" name="act" value="dislike">dislike</button>')
           24  +		end
           25  +		if not rtd
           26  +			then pg:lpush('<button class="pos" name="act" value="rt">retweet</button>')
           27  +			else pg:lpush('<button class="neg" name="act" value="unrt">detweet</button>')
           28  +		end
           29  +		if p.author == co.who.id then
           30  +			pg:lpush('<a class="button" href="/post/'):rpush(path(1)):lpush('/edit">edit</a><a class="neg button" href="/post/'):rpush(path(1)):lpush('/del">delete</a>')
           31  +		end
           32  +		-- TODO list user's chosen reaction emoji
           33  +		pg:lpush('</form>')
           34  +
           35  +		if co.who.rights.powers.post() then
           36  +			lib.render.compose(co, nil, &pg)
           37  +		end
           38  +	end
           39  +
           40  +	var ppg = pg:finalize() defer ppg:free()
           41  +	co:stdpage([lib.srv.convo.page] {
           42  +		title = lib.str.plit 'post'; cache = false;
           43  +		class = lib.str.plit 'post'; body = ppg;
           44  +	})
           45  +
           46  +	-- TODO display conversation
           47  +	-- perhaps display descendant nodes here, and have a link to the top of the whole tree?
           48  +end
           49  +
           50  +return render_tweet_page

Name change from render/userpage.t to render/user-page.t.


Modified route.t from [ae96c17fe6] to [2469fad253].

    10     10   	var handle = [lib.mem.ptr(int8)] { ptr = &uri.ptr[2], ct = 0 }
    11     11   	for i=2,uri.ct do
    12     12   		if uri.ptr[i] == @'/' or uri.ptr[i] == 0 then handle.ct = i - 2 break end
    13     13   	end
    14     14   	if handle.ct == 0 then
    15     15   		handle.ct = uri.ct - 2
    16     16   		uri:advance(uri.ct)
    17         -	else
    18         -		if handle.ct + 2 < uri.ct then
    19         -			uri:advance(handle.ct + 2)
    20         -			--uri.ptr = uri.ptr + (handle.ct + 2)
    21         -			--uri.ct = uri.ct - (handle.ct + 2)
    22         -		end
    23         -	end
           17  +	elseif handle.ct + 2 < uri.ct then uri:advance(handle.ct + 2) end
    24     18   
    25     19   	lib.dbg('looking up user by xid "', {handle.ptr,handle.ct} ,'", path: ', {uri.ptr,uri.ct})
    26     20   
    27     21   	var path = lib.http.hier(uri) defer path:free()
    28     22   	for i=0,path.ct do
    29     23   		lib.dbg('got path component ', {path.ptr[i].ptr, path.ptr[i].ct})
    30     24   	end
................................................................................
    32     26   	var actor = co.srv:actor_fetch_xid(handle)
    33     27   	if actor.ptr == nil then
    34     28   		co:complain(404,'no such user','no such user known to this server')
    35     29   		return
    36     30   	end
    37     31   	defer actor:free()
    38     32   
    39         -	lib.render.userpage(co, actor.ptr)
           33  +	lib.render.user_page(co, actor.ptr)
    40     34   end
    41     35   
    42         -terra http.actor_profile_uid(co: &lib.srv.convo, path: lib.mem.ptr(lib.mem.ref(int8)), meth: method.t)
           36  +terra http.actor_profile_uid (
           37  +	co: &lib.srv.convo,
           38  +	path: lib.mem.ptr(lib.mem.ref(int8)),
           39  +	meth: method.t
           40  +)
    43     41   	if path.ct < 2 then
    44     42   		co:complain(404,'bad url','invalid user url')
    45     43   		return
    46     44   	end
    47     45   
    48     46   	var uid, ok = lib.math.shorthand.parse(path.ptr[1].ptr, path.ptr[1].ct)
    49     47   	if not ok then
................................................................................
    54     52   	var actor = co.srv:actor_fetch_uid(uid)
    55     53   	if actor.ptr == nil then
    56     54   		co:complain(404, 'no such user', 'no user by that ID is known to this instance')
    57     55   		return
    58     56   	end
    59     57   	defer actor:free()
    60     58   
    61         -	lib.render.userpage(co, actor.ptr)
           59  +	lib.render.user_page(co, actor.ptr)
    62     60   end
    63     61   
    64     62   terra http.login_form(co: &lib.srv.convo, meth: method.t)
    65     63   	if meth == method.get then
    66     64   		-- request a username
    67     65   		lib.render.login(co, nil, nil, lib.str.plit(nil))
    68     66   	elseif meth == method.post then
................................................................................
   103    101   			if lib.str.ncmp('pw', am, lib.math.biggest(2,aml)) == 0 and chrs ~= nil then
   104    102   				aid = co.srv:actor_auth_pw(co.peer,
   105    103   					[lib.mem.ptr(int8)]{ptr=usn,ct=usnl},
   106    104   					[lib.mem.ptr(int8)]{ptr=chrs,ct=chrsl})
   107    105   			elseif lib.str.ncmp('otp', am, lib.math.biggest(2,aml)) == 0 and chrs ~= nil then
   108    106   				lib.dbg('using otp auth')
   109    107   				-- ··· --
   110         -			else
   111         -				lib.dbg('invalid auth method')
   112         -			end
          108  +			else lib.dbg('invalid auth method') end
   113    109   
   114    110   			-- error out
   115    111   			if aid == 0 then
   116    112   				lib.render.login(co, nil, nil, lib.str.plit 'authentication failure')
   117    113   			else
   118         -				var sesskey: int8[lib.session.maxlen + #lib.session.cookiename + #"=; Path=/" + 1]
   119         -				do var p = &sesskey[0]
   120         -					p = lib.str.ncpy(p, [lib.session.cookiename .. '='], [#lib.session.cookiename + 1])
   121         -					p = p + lib.session.cookie_gen(co.srv.cfg.secret, aid, lib.osclock.time(nil), p)
   122         -					lib.dbg('sending cookie ',{&sesskey[0],15})
   123         -					p = lib.str.ncpy(p, '; Path=/', 9)
   124         -				end
   125         -				co:reroute_cookie('/', &sesskey[0])
          114  +				co:installkey('/',aid)
   126    115   			end
   127    116   		end
   128    117   		if act.ptr ~= nil and fakeact == false then act:free() end
   129    118   	else
   130    119   		::wrongmeth:: co:complain(405, 'method not allowed', 'that method is not meaningful for this endpoint') do return end
   131    120   	end
   132    121   	return
................................................................................
   134    123   
   135    124   terra http.post_compose(co: &lib.srv.convo, meth: method.t)
   136    125   	if not co:assertpow('post') then return end
   137    126   	--if co.who.rights.powers.post() == false then
   138    127   		--co:complain(403,'insufficient privileges','you lack the <strong>post</strong> power and cannot perform this action')
   139    128   
   140    129   	if meth == method.get then
   141         -		lib.render.compose(co, nil)
          130  +		lib.render.compose(co, nil, nil)
   142    131   	elseif meth == method.post then
   143    132   		var text, textlen = co:postv("post")
   144    133   		var acl, acllen = co:postv("acl")
   145    134   		var subj, subjlen = co:postv("subject")
   146    135   		if text == nil or acl == nil then
   147    136   			co:complain(405, 'invalid post', 'every post must have at least body text and an ACL')
   148    137   			return
................................................................................
   171    160   		lib.render.docpage(co,path(1))
   172    161   	elseif path.ct == 1 then
   173    162   		lib.render.docpage(co, rstring.null())
   174    163   	else
   175    164   		co:complain(404, 'no such documentation', 'invalid documentation URL')
   176    165   	end
   177    166   end
          167  +
          168  +terra http.tweet_page(co: &lib.srv.convo, path: hpath, meth: method.t)
          169  +	var pid, ok = lib.math.shorthand.parse(path(1).ptr, path(1).ct)
          170  +	if not ok then
          171  +		co:complain(400, 'bad post ID', 'that post ID is not valid')
          172  +		return
          173  +	end
          174  +	var post = co.srv:post_fetch(pid)
          175  +	if not post then
          176  +		co:complain(404, 'post not found', 'no such post is known to this server')
          177  +		return
          178  +	end
          179  +	defer post:free()
          180  +
          181  +	if path.ct == 3 then
          182  +		if path(2):cmp(lib.str.lit 'edit') then
          183  +			if post(0).author ~= co.who.id then
          184  +				co:complain(403, 'forbidden', 'you cannot edit other people\'s posts')
          185  +				return
          186  +			end
          187  +
          188  +			if meth == method.get then
          189  +				lib.render.compose(co, post.ptr, nil)
          190  +				return
          191  +			elseif meth == method.post then
          192  +				var newbody = co:postv('post')._0
          193  +				var newacl = co:postv('acl')._0
          194  +				var newsubj = co:postv('subject')._0
          195  +				if newbody ~= nil then post(0).body = newbody end
          196  +				if newacl  ~= nil then post(0).acl = newacl end
          197  +				if newsubj ~= nil then post(0).subject = newsubj end
          198  +				post(0):save(true)
          199  +
          200  +				var lnk: lib.str.acc lnk:compose('/post/', path(1))
          201  +				co:reroute(lnk.buf)
          202  +				lnk:free()
          203  +			end
          204  +			return
          205  +		else goto badurl end
          206  +	end
          207  +
          208  +	if meth == method.post then
          209  +		co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful')
          210  +		return
          211  +	end
          212  +
          213  +	lib.render.tweet_page(co, path, post.ptr)
          214  +	do return end
          215  +
          216  +	::badurl:: co:complain(404, 'invalid URL', 'this URL does not reference extant content or functionality')
          217  +end
   178    218   
   179    219   terra http.configure(co: &lib.srv.convo, path: hpath, meth: method.t)
   180    220   	var msg = pstring.null()
   181    221   	if meth == method.post and path.ct >= 1 then
   182    222   		var user_refresh = false var fail = false
   183    223   		if path(1):cmp(lib.str.lit 'profile') then
          224  +			lib.dbg('updating profile')
   184    225   			co.who.bio = co:postv('bio')._0
   185    226   			co.who.nym = co:postv('nym')._0
   186    227   			if co.who.bio ~= nil and @co.who.bio == 0 then co.who.bio = nil end
   187    228   			if co.who.nym ~= nil and @co.who.nym == 0 then co.who.nym = nil end
   188    229   			co.who.source:actor_save(co.who)
   189    230   			msg = lib.str.plit 'profile changes saved'
   190    231   			--user_refresh = true -- not really necessary here, actually
   191    232   		elseif path(1):cmp(lib.str.lit 'srv') then
   192    233   		elseif path(1):cmp(lib.str.lit 'users') then
   193         -
          234  +		elseif path(1):cmp(lib.str.lit 'sec') then
          235  +			var act = co:ppostv('act')
          236  +			if act:cmp(lib.str.plit 'invalidate') then
          237  +				lib.dbg('setting user\'s cookie validation time to now')
          238  +				co.who.source:auth_sigtime_user_alter(co.who.id, lib.osclock.time(nil))
          239  +				-- the current session has been invalidated as well, so we need to immediately install a new authentication cookie with the same aid so the user doesn't need to log back in all over again
          240  +				co:installkey('/conf/sec',co.aid)
          241  +				return
          242  +			end
   194    243   		end
   195    244   
   196    245   		if user_refresh then -- refresh the user info for the renderer
   197    246   			var usr = co.srv:actor_fetch_uid(co.who.id)
   198    247   			lib.mem.heapf(co.who)
   199    248   			co.who = usr.ptr
   200    249   		end
................................................................................
   222    271   					ct  = storage[([i-1])].ct;
   223    272   				}
   224    273   				goto [send]
   225    274   			end
   226    275   		end
   227    276   	end
   228    277   	terra http.static_content(co: &lib.srv.convo, [filename], [flen])
   229         -		var hdrs = array(lib.http.header{'Content-Type',nil})
          278  +		var hdrs = array(
          279  +		lib.http.header{'Content-Type',nil})
   230    280   		var [page] = lib.http.page {
   231    281   			respcode = 200;
   232    282   			headers = [lib.mem.ptr(lib.http.header)] {
   233    283   				ptr = &hdrs[0], ct = 1
   234    284   			}
   235    285   		}
   236    286   		[branches]
................................................................................
   288    338   		if co.aid == 0
   289    339   			then goto notfound
   290    340   			else co:reroute_cookie('/','auth=; Path=/')
   291    341   		end
   292    342   		return
   293    343   	else -- hierarchical routes
   294    344   		var path = lib.http.hier(uri) defer path:free()
   295         -		if path.ptr[0]:cmp(lib.str.lit('user')) then
          345  +		if path.ct > 1 and path(0):cmp(lib.str.lit('user')) then
   296    346   			http.actor_profile_uid(co, path, meth)
   297         -		elseif path.ptr[0]:cmp(lib.str.lit('tl')) then
          347  +		elseif path.ct > 1 and path(0):cmp(lib.str.lit('post')) then
          348  +			http.tweet_page(co, path, meth)
          349  +		elseif path(0):cmp(lib.str.lit('tl')) then
   298    350   			http.timeline(co, path)
   299         -		elseif path.ptr[0]:cmp(lib.str.lit('doc')) then
          351  +		elseif path(0):cmp(lib.str.lit('doc')) then
   300    352   			if meth ~= method.get and meth ~= method.head then goto wrongmeth end
   301    353   			http.documentation(co, path)
   302         -		elseif path.ptr[0]:cmp(lib.str.lit('conf')) then
          354  +		elseif path(0):cmp(lib.str.lit('conf')) then
   303    355   			if co.aid == 0 then goto unauth end
   304    356   			http.configure(co,path,meth)
   305    357   		else goto notfound end
   306    358   		return
   307    359   	end
   308    360   
   309    361   	::wrongmeth:: co:complain(405, 'method not allowed', 'that method is not meaningful for this endpoint') do return end
   310    362   	::notfound:: co:complain(404, 'not found', 'no such resource available') do return end
   311    363   	::unauth:: co:complain(401, 'unauthorized', 'this content is not available at your clearance level') do return end
   312    364   end

Modified session.t from [e8a79576f0] to [78b2aad470].

    22     22   		[lib.mem.ptr(uint8)] {ptr = [&uint8](secret.ptr), ct = secret.ct},
    23     23   		[lib.mem.ptr( int8)] {ptr = out, ct = len},
    24     24   	&hash[0])
    25     25   	ptr = ptr + lib.math.shorthand.gen(lib.math.truncate64(hash, [hash.type.N]), ptr)
    26     26   	return ptr - out
    27     27   end
    28     28   
    29         -terra m.cookie_interpret(secret: lib.mem.ptr(int8), c: lib.mem.ptr(int8), now: uint64): uint64 -- returns either 0 or a valid authid
           29  +terra m.cookie_interpret(secret: lib.mem.ptr(int8), c: lib.mem.ptr(int8), now: uint64) -- returns either 0,0 or a valid {authid, timepoint}
    30     30   	var authid_sz = lib.str.cspan(c.ptr, lib.str.lit '.', c.ct)
    31         -	if authid_sz == 0 then return 0 end
    32         -	if authid_sz + 1 > c.ct then return 0 end
           31  +	if authid_sz == 0 then return 0,0 end
           32  +	if authid_sz + 1 > c.ct then return 0,0 end
    33     33   	var time_sz = lib.str.cspan(c.ptr+authid_sz+1, lib.str.lit '.', c.ct - (authid_sz+1))
    34         -	if time_sz == 0 then return 0 end
    35         -	if (authid_sz + time_sz + 2) > c.ct then return 0 end
           34  +	if time_sz == 0 then return 0,0 end
           35  +	if (authid_sz + time_sz + 2) > c.ct then return 0,0 end
    36     36   	var hash_sz = c.ct - (authid_sz + time_sz + 2)
    37     37   
    38     38   	var knownhash: uint8[lib.crypt.algsz.sha256]
    39     39   	lib.crypt.hmac(lib.crypt.alg.sha256,
    40     40   		[lib.mem.ptr(uint8)] {ptr = [&uint8](secret.ptr), ct = secret.ct},
    41     41   		[lib.mem.ptr( int8)] {ptr = c.ptr, ct = c.ct - hash_sz},
    42     42   	&knownhash[0])
    43     43   
    44     44   	var authid, authok = lib.math.shorthand.parse(c.ptr, authid_sz)
    45     45   	var time, timeok = lib.math.shorthand.parse(c.ptr + authid_sz + 1, time_sz)
    46     46   	var hash, hashok = lib.math.shorthand.parse(c.ptr + c.ct - hash_sz, hash_sz)
    47         -	if not (timeok and authok and hashok) then return 0 end
    48         -	if lib.math.truncate64(knownhash, [knownhash.type.N]) ~= hash then return 0 end
    49         -	if now - time > m.maxage then return 0 end
           47  +	if not (timeok and authok and hashok) then return 0,0 end
           48  +	if lib.math.truncate64(knownhash, [knownhash.type.N]) ~= hash then return 0,0 end
           49  +	if now - time > m.maxage then return 0,0 end
    50     50   
    51         -	return authid
           51  +	return authid, time
    52     52   end
    53     53   
    54     54   return m

Modified srv.t from [4721bcd333] to [9e0d1b7489].

     1      1   -- vim: ft=terra
     2      2   local util = lib.util
     3      3   local secmode = lib.enum { 'public', 'private', 'lockdown', 'isolate' }
     4      4   local pstring = lib.mem.ptr(int8)
     5      5   local struct srv
     6      6   local struct cfgcache {
     7      7   	secret: lib.mem.ptr(int8)
     8         -	instance: lib.mem.ptr(int8)
     9         -	overlord: &srv
    10      8   	pol_sec: secmode.t
    11      9   	pol_reg: bool
           10  +	credmgd: bool
           11  +	maxupsz: intptr
           12  +	instance: lib.mem.ptr(int8)
           13  +	overlord: &srv
    12     14   }
    13     15   local struct srv {
    14     16   	sources: lib.mem.ptr(lib.store.source)
    15     17   	webmgr: lib.net.mg_mgr
    16     18   	webcon: &lib.net.mg_connection
    17     19   	cfg: cfgcache
    18     20   	id: rawstring
................................................................................
   108    110   end)
   109    111   
   110    112   local struct convo {
   111    113   	srv: &srv
   112    114   	con: &lib.net.mg_connection
   113    115   	msg: &lib.net.mg_http_message
   114    116   	aid: uint64 -- 0 if logged out
          117  +	aid_issue: lib.store.timepoint
   115    118   	who: &lib.store.actor -- who we're logged in as, if aid ~= 0
   116    119   	peer: lib.store.inet
   117    120   	reqtype: lib.http.mime.t -- negotiated content type
   118    121   -- cache
   119    122   	navbar: lib.mem.ptr(int8)
   120    123   	actorcache: lib.mem.cache(lib.mem.ptr(lib.store.actor),32) -- naive cache to avoid unnecessary queries
   121    124   -- private
................................................................................
   154    157   
   155    158   	body:send(self.con, 303, [lib.mem.ptr(lib.http.header)] {
   156    159   		ptr = &hdrs[0], ct = [hdrs.type.N] - lib.trn(cookie == nil,1,0)
   157    160   	})
   158    161   end
   159    162   
   160    163   terra convo:reroute(dest: rawstring) self:reroute_cookie(dest,nil) end
          164  +
          165  +terra convo:installkey(dest: rawstring, aid: uint64)
          166  +	var sesskey: int8[lib.session.maxlen + #lib.session.cookiename + #"=; Path=/" + 1]
          167  +	do var p = &sesskey[0]
          168  +		p = lib.str.ncpy(p, [lib.session.cookiename .. '='], [#lib.session.cookiename + 1])
          169  +		p = p + lib.session.cookie_gen(self.srv.cfg.secret, aid, lib.osclock.time(nil), p)
          170  +		lib.dbg('sending cookie ',{&sesskey[0],15})
          171  +		p = lib.str.ncpy(p, '; Path=/', 9)
          172  +	end
          173  +	self:reroute_cookie(dest, &sesskey[0])
          174  +end
   161    175    
   162    176   terra convo:complain(code: uint16, title: rawstring, msg: rawstring)
   163    177   	var hdrs = array(
   164    178   		lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' },
   165    179   		lib.http.header { key = 'Cache-Control', value = 'no-store' }
   166    180   	)
   167    181   
................................................................................
   232    246   		var r = self.vbofs
   233    247   		self.vbofs = self.vbofs + o + 1
   234    248   		@(self.vbofs - 1) = 0
   235    249   		var norm = lib.str.normalize([lib.mem.ptr(int8)]{ptr = r, ct = o})
   236    250   		return norm.ptr, norm.ct
   237    251   	else return nil, 0 end
   238    252   end
          253  +terra convo:ppostv(name: rawstring)
          254  +	var s,l = self:postv(name)
          255  +	return pstring { ptr = s, ct = l }
          256  +end
   239    257   
   240    258   terra convo:getv(name: rawstring)
   241    259   	if self.varbuf.ptr == nil then
   242    260   		self.varbuf = lib.mem.heapa(int8, self.msg.query.len + self.msg.body.len)
   243    261   		self.vbofs = self.varbuf.ptr
   244    262   	end
   245    263   	var o = lib.net.mg_http_get_var(&self.msg.query, name, self.vbofs, self.varbuf.ct - (self.vbofs - self.varbuf.ptr))
................................................................................
   247    265   		var r = self.vbofs
   248    266   		self.vbofs = self.vbofs + o + 1
   249    267   		@(self.vbofs - 1) = 0
   250    268   		var norm = lib.str.normalize([lib.mem.ptr(int8)]{ptr = r, ct = o})
   251    269   		return norm.ptr, norm.ct
   252    270   	else return nil, 0 end
   253    271   end
          272  +terra convo:pgetv(name: rawstring)
          273  +	var s,l = self:getv(name)
          274  +	return pstring { ptr = s, ct = l }
          275  +end
   254    276   
   255    277   local urimatch = macro(function(uri, ptn)
   256    278   	return `lib.net.mg_globmatch(ptn, [#ptn], uri.ptr, uri.ct+1)
   257    279   end)
   258    280   
   259    281   local route = {} -- these are defined in route.t, as they need access to renderers
   260    282   terra route.dispatch_http ::  {&convo, lib.mem.ptr(int8), lib.http.method.t} -> {}
................................................................................
   303    325   
   304    326   		switch event do
   305    327   			case lib.net.MG_EV_HTTP_MSG then
   306    328   				lib.dbg('routing HTTP request')
   307    329   				var msg = [&lib.net.mg_http_message](p)
   308    330   				var co = convo {
   309    331   					con = con, srv = server, msg = msg;
   310         -					aid = 0, who = nil, peer = peer;
          332  +					aid = 0, aid_issue = 0, who = nil;
   311    333   					reqtype = lib.http.mime.none;
          334  +					peer = peer;
   312    335   				} co.varbuf.ptr = nil
   313    336   				  co.navbar.ptr = nil
   314    337   				  co.actorcache.top = 0
   315    338   				  co.actorcache.cur = 0
   316    339   
   317    340   				-- first, check for an accept header. if it's there, we need to
   318    341   				-- iterate over the values and pick the highest-priority one
................................................................................
   388    411   					end
   389    412   					if val.ptr == nil then goto nocookie end
   390    413   					val.ct = (cookies + i) - val.ptr
   391    414   					if lib.str.ncmp(key.ptr, lib.session.cookiename, lib.math.biggest([#lib.session.cookiename], key.ct)) ~= 0 then
   392    415   						goto nocookie
   393    416   					end
   394    417   					::foundcookie:: do
   395         -						var aid = lib.session.cookie_interpret(server.cfg.secret,
          418  +						var aid, tp = lib.session.cookie_interpret(server.cfg.secret,
   396    419   							[lib.mem.ptr(int8)]{ptr=val.ptr,ct=val.ct},
   397    420   							lib.osclock.time(nil))
   398         -						if aid ~= 0 then co.aid = aid end
          421  +						if aid ~= 0 then co.aid = aid co.aid_issue = tp end
   399    422   					end ::nocookie::;
   400    423   				end
   401    424   
   402    425   				if co.aid ~= 0 then
   403         -					var sess, usr = co.srv:actor_session_fetch(co.aid, peer)
   404         -					if sess.ok == false then co.aid = 0 else
          426  +					var sess, usr = co.srv:actor_session_fetch(co.aid, peer, co.aid_issue)
          427  +					if sess.ok == false then co.aid = 0 co.aid_issue = 0 else
   405    428   						co.who = usr.ptr
   406    429   						co.who.rights.powers = server:actor_powers_fetch(co.who.id)
   407    430   					end
   408    431   				end
   409    432   
   410    433   				var uridec = lib.mem.heapa(int8, msg.uri.len) defer uridec:free()
   411    434   				var urideclen = lib.net.mg_url_decode(msg.uri.ptr, msg.uri.len, uridec.ptr, uridec.ct, 1)
................................................................................
   640    663   	self.sources:free()
   641    664   end
   642    665   
   643    666   terra cfgcache:load()
   644    667   	self.instance = self.overlord:conf_get('instance-name')
   645    668   	self.secret = self.overlord:conf_get('server-secret')
   646    669   
   647         -	self.pol_reg = false
          670  +	do self.pol_reg = false
   648    671   	var sreg = self.overlord:conf_get('policy-self-register')
   649         -	if sreg.ptr ~= nil then
          672  +	if sreg:ref() then
   650    673   		if lib.str.cmp(sreg.ptr, 'on') == 0
   651    674   			then self.pol_reg = true
   652    675   			else self.pol_reg = false
   653    676   		end
   654    677   	end
   655         -	sreg:free()
          678  +	sreg:free() end
          679  +
          680  +	do self.credmgd = false
          681  +	var sreg = self.overlord:conf_get('credential-store')
          682  +	if sreg:ref() then
          683  +		if lib.str.cmp(sreg.ptr, 'managed') == 0
          684  +			then self.credmgd = true
          685  +			else self.credmgd = false
          686  +		end
          687  +	end
          688  +	sreg:free() end
          689  +
          690  +	do self.maxupsz = [1024 * 100] -- 100 kilobyte default
          691  +	var sreg = self.overlord:conf_get('maximum-artifact-size')
          692  +	if sreg:ref() then
          693  +		var sz, ok = lib.math.fsz_parse(sreg)
          694  +		if ok then self.maxupsz = sz else
          695  +			lib.warn('invalid configuration value for maximum-artifact-size; keeping default 100K upload limit')
          696  +		end
          697  +	end
          698  +	sreg:free() end
   656    699   	
   657    700   	self.pol_sec = secmode.lockdown
   658    701   	var smode = self.overlord:conf_get('policy-security')
   659    702   	if smode.ptr ~= nil then
   660    703   		if lib.str.cmp(smode.ptr, 'public') == 0 then
   661    704   			self.pol_sec = secmode.public
   662    705   		elseif lib.str.cmp(smode.ptr, 'private') == 0 then

Added static/query.svg version [eb8b842615].

            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="query.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  +       id="linearGradient1362"
           27  +       inkscape:collect="always">
           28  +      <stop
           29  +         id="stop1358"
           30  +         offset="0"
           31  +         style="stop-color:#b64bc7;stop-opacity:1" />
           32  +      <stop
           33  +         id="stop1360"
           34  +         offset="1"
           35  +         style="stop-color:#7932a4;stop-opacity:1" />
           36  +    </linearGradient>
           37  +    <linearGradient
           38  +       inkscape:collect="always"
           39  +       id="linearGradient973">
           40  +      <stop
           41  +         style="stop-color:#9b3bd7;stop-opacity:1;"
           42  +         offset="0"
           43  +         id="stop969" />
           44  +      <stop
           45  +         style="stop-color:#5d267e;stop-opacity:1"
           46  +         offset="1"
           47  +         id="stop971" />
           48  +    </linearGradient>
           49  +    <linearGradient
           50  +       inkscape:collect="always"
           51  +       id="linearGradient2322">
           52  +      <stop
           53  +         style="stop-color:#ca0050;stop-opacity:1;"
           54  +         offset="0"
           55  +         id="stop2318" />
           56  +      <stop
           57  +         style="stop-color:#7b0031;stop-opacity:1"
           58  +         offset="1"
           59  +         id="stop2320" />
           60  +    </linearGradient>
           61  +    <linearGradient
           62  +       inkscape:collect="always"
           63  +       id="linearGradient2302">
           64  +      <stop
           65  +         style="stop-color:#ffffff;stop-opacity:1;"
           66  +         offset="0"
           67  +         id="stop2298" />
           68  +      <stop
           69  +         style="stop-color:#ffffff;stop-opacity:0;"
           70  +         offset="1"
           71  +         id="stop2300" />
           72  +    </linearGradient>
           73  +    <linearGradient
           74  +       inkscape:collect="always"
           75  +       id="linearGradient851">
           76  +      <stop
           77  +         style="stop-color:#ffffff;stop-opacity:0"
           78  +         offset="0"
           79  +         id="stop847" />
           80  +      <stop
           81  +         style="stop-color:#ffa9c6;stop-opacity:1"
           82  +         offset="1"
           83  +         id="stop849" />
           84  +    </linearGradient>
           85  +    <radialGradient
           86  +       inkscape:collect="always"
           87  +       xlink:href="#linearGradient851"
           88  +       id="radialGradient853"
           89  +       cx="12.699999"
           90  +       cy="285.82184"
           91  +       fx="12.699999"
           92  +       fy="285.82184"
           93  +       r="1.7905753"
           94  +       gradientTransform="matrix(2.2137788,-2.5697531e-5,6.7771541e-5,5.8383507,-15.43436,-1382.906)"
           95  +       gradientUnits="userSpaceOnUse" />
           96  +    <radialGradient
           97  +       inkscape:collect="always"
           98  +       xlink:href="#linearGradient2302"
           99  +       id="radialGradient2304"
          100  +       cx="7.2493401"
          101  +       cy="278.65524"
          102  +       fx="7.2493401"
          103  +       fy="278.65524"
          104  +       r="10.573204"
          105  +       gradientTransform="matrix(1.1874875,-1.9679213e-8,1.9699479e-8,1.1887104,-2.3813503,-53.649456)"
          106  +       gradientUnits="userSpaceOnUse" />
          107  +    <linearGradient
          108  +       inkscape:collect="always"
          109  +       xlink:href="#linearGradient2322"
          110  +       id="linearGradient2324"
          111  +       x1="20.298828"
          112  +       y1="97.196846"
          113  +       x2="20.298828"
          114  +       y2="12.911131"
          115  +       gradientUnits="userSpaceOnUse"
          116  +       gradientTransform="translate(1.3858268e-6)" />
          117  +    <radialGradient
          118  +       inkscape:collect="always"
          119  +       xlink:href="#linearGradient2302"
          120  +       id="radialGradient2304-8"
          121  +       cx="12.58085"
          122  +       cy="285.25314"
          123  +       fx="12.58085"
          124  +       fy="285.25314"
          125  +       r="10.573204"
          126  +       gradientTransform="matrix(4.4881418,-7.4378129e-8,7.4454725e-8,4.4927638,-17.038663,-1237.2864)"
          127  +       gradientUnits="userSpaceOnUse" />
          128  +    <radialGradient
          129  +       inkscape:collect="always"
          130  +       xlink:href="#linearGradient2302"
          131  +       id="radialGradient960"
          132  +       gradientUnits="userSpaceOnUse"
          133  +       gradientTransform="matrix(0.77290068,-0.9015271,0.4066395,0.34862174,-111.85097,215.11933)"
          134  +       cx="23.48217"
          135  +       cy="269.32919"
          136  +       fx="23.48217"
          137  +       fy="269.32919"
          138  +       r="10.573204" />
          139  +    <linearGradient
          140  +       inkscape:collect="always"
          141  +       xlink:href="#linearGradient973"
          142  +       id="linearGradient975"
          143  +       x1="5.8321538"
          144  +       y1="275.4801"
          145  +       x2="21.037828"
          146  +       y2="292.65216"
          147  +       gradientUnits="userSpaceOnUse"
          148  +       gradientTransform="matrix(3.7795276,0,0,3.7795276,0,-1026.5196)" />
          149  +    <linearGradient
          150  +       inkscape:collect="always"
          151  +       xlink:href="#linearGradient973"
          152  +       id="linearGradient1014"
          153  +       gradientUnits="userSpaceOnUse"
          154  +       x1="5.8321538"
          155  +       y1="275.4801"
          156  +       x2="21.037828"
          157  +       y2="292.65216"
          158  +       gradientTransform="translate(-42.333335)" />
          159  +    <radialGradient
          160  +       inkscape:collect="always"
          161  +       xlink:href="#linearGradient2302"
          162  +       id="radialGradient1016"
          163  +       gradientUnits="userSpaceOnUse"
          164  +       gradientTransform="matrix(1.1874875,-1.9679213e-8,1.9699479e-8,1.1887104,-44.714686,-53.649456)"
          165  +       cx="7.2493401"
          166  +       cy="278.65524"
          167  +       fx="7.2493401"
          168  +       fy="278.65524"
          169  +       r="10.573204" />
          170  +    <radialGradient
          171  +       inkscape:collect="always"
          172  +       xlink:href="#linearGradient2302"
          173  +       id="radialGradient1018"
          174  +       gradientUnits="userSpaceOnUse"
          175  +       gradientTransform="matrix(0.77290068,-0.9015271,0.4066395,0.34862174,-154.18433,215.11933)"
          176  +       cx="23.48217"
          177  +       cy="269.32919"
          178  +       fx="23.48217"
          179  +       fy="269.32919"
          180  +       r="10.573204" />
          181  +    <filter
          182  +       inkscape:collect="always"
          183  +       style="color-interpolation-filters:sRGB"
          184  +       id="filter1222"
          185  +       x="-0.12898994"
          186  +       width="1.2579799"
          187  +       y="-0.05840071"
          188  +       height="1.1168014">
          189  +      <feGaussianBlur
          190  +         inkscape:collect="always"
          191  +         stdDeviation="0.33911411"
          192  +         id="feGaussianBlur1224" />
          193  +    </filter>
          194  +    <filter
          195  +       inkscape:collect="always"
          196  +       style="color-interpolation-filters:sRGB"
          197  +       id="filter1352"
          198  +       x="-0.15389959"
          199  +       width="1.3077992"
          200  +       y="-0.077627083"
          201  +       height="1.1552542">
          202  +      <feGaussianBlur
          203  +         inkscape:collect="always"
          204  +         stdDeviation="0.50074934"
          205  +         id="feGaussianBlur1354" />
          206  +    </filter>
          207  +    <radialGradient
          208  +       inkscape:collect="always"
          209  +       xlink:href="#linearGradient1362"
          210  +       id="radialGradient1356"
          211  +       cx="6.5823746"
          212  +       cy="278.22546"
          213  +       fx="6.5823746"
          214  +       fy="278.22546"
          215  +       r="9.8748493"
          216  +       gradientTransform="matrix(2.0818304,0,0,2.0818304,-8.516817,-303.27685)"
          217  +       gradientUnits="userSpaceOnUse" />
          218  +    <meshgradient
          219  +       y="277.31741"
          220  +       x="5.7174305"
          221  +       gradientUnits="userSpaceOnUse"
          222  +       id="meshgradient5802"
          223  +       inkscape:collect="always">
          224  +      <meshrow
          225  +         id="meshrow5804">
          226  +        <meshpatch
          227  +           id="meshpatch5806">
          228  +          <stop
          229  +             path="c 3.85637,-3.85637  10.1088,-3.85637  13.9651,0"
          230  +             style="stop-color:#eccaff;stop-opacity:1"
          231  +             id="stop5808" />
          232  +          <stop
          233  +             path="c 3.85637,3.85637  3.85637,10.1088  0,13.9651"
          234  +             style="stop-color:#800080;stop-opacity:1"
          235  +             id="stop5810" />
          236  +          <stop
          237  +             path="c -3.85637,3.85637  -10.1088,3.85637  -13.9651,0"
          238  +             style="stop-color:#ef9aff;stop-opacity:1"
          239  +             id="stop5812" />
          240  +          <stop
          241  +             path="c -3.85637,-3.85637  -3.85637,-10.1088  0,-13.9651"
          242  +             style="stop-color:#800080;stop-opacity:1"
          243  +             id="stop5814" />
          244  +        </meshpatch>
          245  +      </meshrow>
          246  +    </meshgradient>
          247  +  </defs>
          248  +  <sodipodi:namedview
          249  +     id="base"
          250  +     pagecolor="#313131"
          251  +     bordercolor="#666666"
          252  +     borderopacity="1.0"
          253  +     inkscape:pageopacity="0"
          254  +     inkscape:pageshadow="2"
          255  +     inkscape:zoom="3.959798"
          256  +     inkscape:cx="81.587655"
          257  +     inkscape:cy="1.9529273"
          258  +     inkscape:document-units="mm"
          259  +     inkscape:current-layer="layer1"
          260  +     showgrid="false"
          261  +     units="in"
          262  +     inkscape:window-width="1920"
          263  +     inkscape:window-height="1042"
          264  +     inkscape:window-x="0"
          265  +     inkscape:window-y="38"
          266  +     inkscape:window-maximized="0" />
          267  +  <metadata
          268  +     id="metadata5">
          269  +    <rdf:RDF>
          270  +      <cc:Work
          271  +         rdf:about="">
          272  +        <dc:format>image/svg+xml</dc:format>
          273  +        <dc:type
          274  +           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
          275  +        <dc:title></dc:title>
          276  +      </cc:Work>
          277  +    </rdf:RDF>
          278  +  </metadata>
          279  +  <g
          280  +     inkscape:label="Layer 1"
          281  +     inkscape:groupmode="layer"
          282  +     id="layer1"
          283  +     transform="translate(0,-271.59998)">
          284  +    <circle
          285  +       style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:url(#radialGradient2304);stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
          286  +       id="path910"
          287  +       cx="12.7"
          288  +       cy="284.29999"
          289  +       r="9.8746281" />
          290  +    <path
          291  +       id="rect829"
          292  +       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"
          293  +       d="m 11.543949,290.50847 -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"
          294  +       inkscape:connector-curvature="0"
          295  +       sodipodi:nodetypes="ccccccccc" />
          296  +    <g
          297  +       aria-label="?"
          298  +       style="font-style:normal;font-weight:normal;font-size:10.58333302px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.12330705;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
          299  +       id="text877"
          300  +       transform="matrix(2.1597213,0,0,2.1385442,0.73403254,-313.27713)">
          301  +      <path
          302  +         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Averia Serif Libre';-inkscape-font-specification:'Averia Serif Libre';vector-effect:none;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.12330705;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
          303  +         d="m 7.6924933,277.43408 c 0.1165795,0.95131 -0.6562552,1.66432 -1.3319477,2.19936 -0.6322781,0.5146 -0.3926261,1.03197 -0.696752,1.4212 -0.7264746,0.20404 -0.708357,-0.94298 -0.5084803,-1.3771 0.3220556,-0.77195 1.5189026,-1.18082 1.6106521,-2.0731 0.1697248,-0.85219 -1.0638516,-1.33257 -1.6164509,-0.77546 -0.4346018,0.40863 -1.0937506,0.72692 -1.0597346,-0.0803 0.061275,-0.68615 0.5563323,-0.90025 1.144623,-0.96295 0.9913021,-0.15129 2.2140894,0.34092 2.4316458,1.40994 l 0.018295,0.11853 z"
          304  +         id="path884"
          305  +         inkscape:connector-curvature="0"
          306  +         sodipodi:nodetypes="ccccccccccc" />
          307  +    </g>
          308  +    <path
          309  +       style="opacity:1;vector-effect:none;fill:url(#meshgradient5802);fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
          310  +       d="M 12.7,274.42513 A 9.874628,9.874628 0 0 0 2.8251504,284.29998 9.874628,9.874628 0 0 0 12.7,294.17483 9.874628,9.874628 0 0 0 22.574849,284.29998 9.874628,9.874628 0 0 0 12.7,274.42513 Z m 0.160197,2.02055 c 1.946164,0.016 4.019091,1.07178 4.43022,3.07216 l 0.03979,0.25373 0.01757,0.25632 c 0.251779,2.03442 -1.41752,3.55938 -2.876827,4.70359 -1.365545,1.10049 -0.847991,2.2067 -1.504818,3.03909 -1.568983,0.43635 -1.529802,-2.01666 -1.098124,-2.94504 0.69555,-1.65085 3.280704,-2.52514 3.478857,-4.43332 0.366559,-1.82245 -2.297799,-2.8497 -3.491259,-1.6583 -0.938619,0.87387 -2.3622147,1.55471 -2.2887497,-0.17156 0.132337,-1.46737 1.2016567,-1.92522 2.4722007,-2.05931 0.267617,-0.0404 0.543115,-0.0596 0.821139,-0.0574 z m -0.422714,13.16922 h 0.525033 l 0.893485,0.89349 v 0.52555 l -0.893485,0.89348 -0.525033,-5.2e-4 -0.893485,-0.89348 v -0.52503 z"
          311  +       id="circle967"
          312  +       inkscape:connector-curvature="0" />
          313  +    <circle
          314  +       r="9.8746281"
          315  +       cy="284.29999"
          316  +       cx="12.7"
          317  +       id="circle958"
          318  +       style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:url(#radialGradient960);stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
          319  +    <g
          320  +       aria-label="?"
          321  +       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:31.11434174px;line-height:1.25;font-family:'Averia Serif Libre';-inkscape-font-specification:'Averia Serif Libre';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:0.26458335"
          322  +       id="text979" />
          323  +    <circle
          324  +       r="9.8746281"
          325  +       cy="284.29999"
          326  +       cx="-29.633333"
          327  +       id="circle998"
          328  +       style="opacity:1;vector-effect:none;fill:url(#linearGradient1014);fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
          329  +    <g
          330  +       transform="translate(-42.333335,-0.90874193)"
          331  +       id="g1006">
          332  +      <path
          333  +         id="path1000"
          334  +         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"
          335  +         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"
          336  +         inkscape:connector-curvature="0"
          337  +         sodipodi:nodetypes="ccccccccc" />
          338  +      <g
          339  +         aria-label="?"
          340  +         style="font-style:normal;font-weight:normal;font-size:10.58333302px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.12330705;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
          341  +         id="g1004"
          342  +         transform="matrix(2.1597213,0,0,2.1385442,0.73403254,-312.36839)">
          343  +        <path
          344  +           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Averia Serif Libre';-inkscape-font-specification:'Averia Serif Libre';vector-effect:none;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.12330705;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
          345  +           d="m 7.6924933,277.43408 c 0.1165795,0.95131 -0.6562552,1.66432 -1.3319477,2.19936 -0.6322781,0.5146 -0.3926261,1.03197 -0.696752,1.4212 -0.7264746,0.20404 -0.708357,-0.94298 -0.5084803,-1.3771 0.3220556,-0.77195 1.5189026,-1.18082 1.6106521,-2.0731 0.1697248,-0.85219 -1.0638516,-1.33257 -1.6164509,-0.77546 -0.4346018,0.40863 -1.0937506,0.72692 -1.0597346,-0.0803 0.061275,-0.68615 0.5563323,-0.90025 1.144623,-0.96295 0.9913021,-0.15129 2.2140894,0.34092 2.4316458,1.40994 l 0.018295,0.11853 z"
          346  +           id="path1002"
          347  +           inkscape:connector-curvature="0"
          348  +           sodipodi:nodetypes="ccccccccccc" />
          349  +      </g>
          350  +    </g>
          351  +    <circle
          352  +       r="9.8746281"
          353  +       cy="284.29999"
          354  +       cx="-29.633333"
          355  +       id="circle1008"
          356  +       style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:url(#radialGradient1016);stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
          357  +    <circle
          358  +       style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:url(#radialGradient1018);stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
          359  +       id="circle1010"
          360  +       cx="-29.633333"
          361  +       cy="284.29999"
          362  +       r="9.8746281" />
          363  +    <g
          364  +       id="g1012"
          365  +       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:31.11434174px;line-height:1.25;font-family:'Averia Serif Libre';-inkscape-font-specification:'Averia Serif Libre';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:0.26458335"
          366  +       aria-label="?"
          367  +       transform="translate(-42.333335)" />
          368  +    <g
          369  +       transform="matrix(2.1597213,0,0,2.1385442,0.73403254,-313.27713)"
          370  +       id="g1024"
          371  +       style="font-style:normal;font-weight:normal;font-size:10.58333302px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.12330705;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
          372  +       aria-label="?" />
          373  +    <g
          374  +       transform="translate(-3.3333335e-8,-0.90874193)"
          375  +       id="g1039"
          376  +       style="opacity:0.64600004;filter:url(#filter1352)">
          377  +      <path
          378  +         id="path1033"
          379  +         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"
          380  +         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"
          381  +         inkscape:connector-curvature="0"
          382  +         sodipodi:nodetypes="ccccccccc" />
          383  +      <g
          384  +         aria-label="?"
          385  +         style="font-style:normal;font-weight:normal;font-size:10.58333302px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.12330705;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
          386  +         id="g1037"
          387  +         transform="matrix(2.1597213,0,0,2.1385442,0.73403254,-312.36839)">
          388  +        <path
          389  +           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Averia Serif Libre';-inkscape-font-specification:'Averia Serif Libre';vector-effect:none;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.12330705;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
          390  +           d="m 7.6924933,277.43408 c 0.1165795,0.95131 -0.6562552,1.66432 -1.3319477,2.19936 -0.6322781,0.5146 -0.3926261,1.03197 -0.696752,1.4212 -0.7264746,0.20404 -0.708357,-0.94298 -0.5084803,-1.3771 0.3220556,-0.77195 1.5189026,-1.18082 1.6106521,-2.0731 0.1697248,-0.85219 -1.0638516,-1.33257 -1.6164509,-0.77546 -0.4346018,0.40863 -1.0937506,0.72692 -1.0597346,-0.0803 0.061275,-0.68615 0.5563323,-0.90025 1.144623,-0.96295 0.9913021,-0.15129 2.2140894,0.34092 2.4316458,1.40994 l 0.018295,0.11853 z"
          391  +           id="path1035"
          392  +           inkscape:connector-curvature="0"
          393  +           sodipodi:nodetypes="ccccccccccc" />
          394  +      </g>
          395  +    </g>
          396  +    <path
          397  +       d="m 12.150391,277.24219 a 0.748792,0.748792 0 0 1 -0.0332,0.004 c -0.568389,0.06 -1.027224,0.19774 -1.306641,0.39649 -0.277209,0.19717 -0.442118,0.42727 -0.494141,0.97851 -7.25e-4,0.16027 0.0015,0.16112 0.01172,0.19922 0.194129,-0.0613 0.650675,-0.29317 1.017578,-0.63476 l -0.01953,0.0195 c 0.869948,-0.86845 2.158273,-0.89534 3.158203,-0.45899 0.990823,0.43239 1.8253,1.49756 1.589844,2.76368 -0.148683,1.24943 -1.014306,2.08321 -1.78711,2.75976 -0.782208,0.68479 -1.493175,1.28446 -1.730468,1.84766 a 0.748792,0.748792 0 0 1 -0.0098,0.0254 c -0.113045,0.24312 -0.238287,1.0468 -0.144531,1.52344 0.02878,0.14631 0.0631,0.17601 0.09961,0.24218 0.06761,-0.20148 0.132148,-0.34938 0.222656,-0.73047 0.140154,-0.5901 0.460972,-1.37295 1.275391,-2.02929 a 0.748792,0.748792 0 0 1 0.0078,-0.008 c 0.71169,-0.55801 1.441989,-1.19038 1.9375,-1.86914 0.495512,-0.67875 0.758472,-1.35793 0.660157,-2.15234 a 0.748792,0.748792 0 0 1 -0.0039,-0.041 l -0.01562,-0.2246 -0.0293,-0.19141 c -0.185302,-0.89101 -0.77691,-1.53674 -1.607422,-1.96484 -0.83249,-0.42913 -1.892004,-0.59212 -2.798828,-0.45508 z m 0.548828,13.16797 -0.359375,0.36132 0.359375,0.35938 0.361328,-0.35938 z"
          398  +       id="path1212"
          399  +       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.58333302px;line-height:1.25;font-family:'Averia Serif Libre';-inkscape-font-specification:'Averia Serif Libre';letter-spacing:0px;word-spacing:0px;vector-effect:none;fill:#fa93ff;fill-opacity:1;stroke:none;stroke-width:0.26500002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter1222)"
          400  +       inkscape:original="M 12.039062 276.50195 C 10.768518 276.63604 9.6987432 277.09514 9.5664062 278.5625 C 9.4929412 280.28878 10.91685 279.60825 11.855469 278.73438 C 13.048929 277.54298 15.712261 278.57013 15.345703 280.39258 C 15.14755 282.30076 12.562739 283.17532 11.867188 284.82617 C 11.43551 285.75456 11.397815 288.20783 12.966797 287.77148 C 13.623624 286.93909 13.105159 285.83097 14.470703 284.73047 C 15.930011 283.58626 17.599435 282.06176 17.347656 280.02734 L 17.330078 279.77148 L 17.291016 279.51758 C 16.821155 277.23144 14.179998 276.17841 12.039062 276.50195 z M 12.4375 289.61523 L 11.544922 290.50781 L 11.542969 291.0332 L 12.4375 291.92773 L 12.962891 291.92773 L 13.855469 291.0332 L 13.855469 290.50781 L 12.962891 289.61523 L 12.4375 289.61523 z "
          401  +       inkscape:radius="-0.74871713"
          402  +       sodipodi:type="inkscape:offset" />
          403  +    <path
          404  +       sodipodi:type="inkscape:offset"
          405  +       inkscape:radius="-0.16717836"
          406  +       inkscape:original="M -14.34375 277.00195 C -14.35155 277.00395 -14.359347 277.00486 -14.367188 277.00586 C -14.957202 277.06816 -15.456819 277.209 -15.789062 277.44531 C -16.11815 277.67939 -16.326757 277.98861 -16.384766 278.59375 C -16.396596 278.94282 -16.323264 279.09764 -16.308594 279.11523 C -16.293644 279.13313 -16.320444 279.13779 -16.197266 279.12109 C -15.950904 279.08739 -15.37302 278.75785 -14.949219 278.36328 L -14.960938 278.375 C -14.179326 277.59474 -12.995641 277.56495 -12.070312 277.96875 C -11.152622 278.36922 -10.401691 279.31598 -10.617188 280.46484 C -10.74706 281.62372 -11.562681 282.41436 -12.332031 283.08789 C -13.106385 283.7658 -13.851986 284.37545 -14.125 285.02344 C -14.1275 285.02844 -14.130113 285.03396 -14.132812 285.03906 C -14.279093 285.35366 -14.401281 286.17218 -14.294922 286.71289 C -14.241742 286.98325 -14.139292 287.17048 -14.054688 287.24414 C -14.009657 287.28334 -13.897079 287.25733 -13.828125 287.26562 C -13.685143 287.00234 -13.611598 286.71247 -13.498047 286.23438 C -13.363381 285.66737 -13.078496 284.95798 -12.306641 284.33594 C -12.304641 284.33494 -12.302781 284.33303 -12.300781 284.33203 C -11.583283 283.76946 -10.83545 283.12505 -10.316406 282.41406 C -9.7973609 281.70308 -9.5060504 280.95629 -9.6132812 280.08984 C -9.6147813 280.08084 -9.6160875 280.0716 -9.6171875 280.0625 L -9.6328125 279.82812 L -9.6640625 279.61914 C -9.8643945 278.64441 -10.518063 277.93724 -11.400391 277.48242 C -12.28272 277.02761 -13.384016 276.85692 -14.34375 277.00195 z "
          407  +       xlink:href="#path1199"
          408  +       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.58333302px;line-height:1.25;font-family:'Averia Serif Libre';-inkscape-font-specification:'Averia Serif Libre';letter-spacing:0px;word-spacing:0px;vector-effect:none;fill:#ffe2fb;fill-opacity:1;stroke:none;stroke-width:0.26500002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
          409  +       id="path5733"
          410  +       inkscape:href="#path1199"
          411  +       d="m -13.605469,277.11914 c -0.24345,-0.003 -0.482418,0.014 -0.71289,0.0488 -0.01058,0.002 -0.02313,0.003 -0.02734,0.004 a 0.16719508,0.16719508 0 0 1 -0.0039,0 c -0.57365,0.0606 -1.048495,0.20154 -1.341797,0.41015 -0.297394,0.21154 -0.469187,0.45403 -0.525391,1.01758 -3.1e-5,9.2e-4 3e-5,0.001 0,0.002 -0.0052,0.16054 0.0082,0.27296 0.02344,0.3418 0.07849,-0.0163 0.262351,-0.0861 0.46875,-0.20898 0.218361,-0.13004 0.460956,-0.30687 0.662109,-0.49415 a 0.16719508,0.16719508 0 0 1 0.05273,-0.0312 c 0.0011,-0.001 0.0028,-9.3e-4 0.0039,-0.002 0.838825,-0.77648 2.051057,-0.80558 3.001953,-0.39062 0.975575,0.42573 1.78232,1.4453 1.550781,2.67968 -0.141936,1.22067 -0.998063,2.04142 -1.769531,2.7168 -0.776996,0.68022 -1.502751,1.2928 -1.748047,1.875 a 0.16719508,0.16719508 0 0 1 -0.0039,0.01 c -7.97e-4,0.002 -0.0037,0.007 -0.0059,0.0117 -0.05727,0.12317 -0.127965,0.40183 -0.162109,0.70117 -0.03414,0.29933 -0.03604,0.62635 0.01172,0.86914 0.0462,0.2349 0.143766,0.38461 0.177734,0.41992 0.0013,-2e-5 0.0045,4e-5 0.0059,0 0.109999,-0.22507 0.183634,-0.46863 0.28711,-0.9043 0.139671,-0.58808 0.443488,-1.34184 1.248047,-1.99023 a 0.16719508,0.16719508 0 0 1 0.0078,-0.004 c 0.0021,-0.002 0.0037,-0.004 0.0059,-0.006 a 0.16719508,0.16719508 0 0 1 0.002,-0.002 c 0.71045,-0.55752 1.444993,-1.19161 1.945312,-1.87695 0.5037376,-0.69002 0.7731349,-1.38688 0.6718751,-2.20508 -0.00154,-0.01 -0.00277,-0.0199 -0.00391,-0.0293 a 0.16719508,0.16719508 0 0 1 0,-0.008 l -0.015625,-0.22852 -0.029297,-0.19336 -0.00195,-0.004 c -0.1901449,-0.91782 -0.7983519,-1.58039 -1.6464839,-2.01758 -0.635335,-0.32749 -1.398557,-0.50322 -2.128907,-0.51172 z"
          412  +       transform="translate(26.458334)" />
          413  +    <path
          414  +       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.58333302px;line-height:1.25;font-family:'Averia Serif Libre';-inkscape-font-specification:'Averia Serif Libre';letter-spacing:0px;word-spacing:0px;vector-effect:none;fill:#ffe2fb;fill-opacity:1;stroke:none;stroke-width:0.26500002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
          415  +       d="m -14.343099,277.00195 c -0.0078,0.002 -0.0156,0.003 -0.02344,0.004 -0.590014,0.0623 -1.089633,0.20314 -1.421876,0.43945 -0.329088,0.23408 -0.537694,0.5433 -0.595703,1.14844 -0.01183,0.34907 0.0615,0.50389 0.07617,0.52148 0.01495,0.0179 -0.01185,0.0227 0.111328,0.006 0.246362,-0.0337 0.824247,-0.36324 1.248048,-0.75781 l -0.01172,0.0117 c 0.781612,-0.78026 1.965296,-0.81005 2.890625,-0.40625 0.91769,0.40047 1.668621,1.34723 1.453124,2.49609 -0.129872,1.15888 -0.945493,1.94952 -1.714843,2.62305 -0.774354,0.67791 -1.519955,1.28756 -1.792969,1.93555 -0.0025,0.005 -0.0051,0.0105 -0.0078,0.0156 -0.14628,0.3146 -0.268469,1.13312 -0.16211,1.67383 0.05318,0.27036 0.155631,0.45759 0.240235,0.53125 0.04503,0.0392 0.157608,0.0132 0.226562,0.0215 0.142982,-0.26329 0.216528,-0.55315 0.330079,-1.03124 0.134666,-0.56701 0.419551,-1.2764 1.191406,-1.89844 0.002,-10e-4 0.0039,-0.003 0.0059,-0.004 0.717498,-0.56257 1.46533,-1.20698 1.984374,-1.91797 0.5190453,-0.71098 0.8103562,-1.45777 0.7031253,-2.32422 -0.0015,-0.009 -0.0028,-0.0182 -0.0039,-0.0273 l -0.01563,-0.23438 -0.03125,-0.20898 c -0.200332,-0.97473 -0.8539993,-1.6819 -1.7363273,-2.13672 -0.882329,-0.45481 -1.983626,-0.6255 -2.94336,-0.48047 z"
          416  +       id="path1199"
          417  +       inkscape:connector-curvature="0"
          418  +       sodipodi:nodetypes="ccccccccccscccccccccccccccc" />
          419  +    <path
          420  +       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"
          421  +       d="m 12.204228,290.65853 -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"
          422  +       id="path1232"
          423  +       inkscape:connector-curvature="0" />
          424  +  </g>
          425  +</svg>

Modified static/style.scss from [2a06f65525] to [9b25bded91].

     9      9   	@extend %sans;
    10     10   	background-color: tone(-55%);
    11     11   	color: tone(25%);
    12     12   	font-size: 14pt;
    13     13   	margin: 0;
    14     14   	padding: 0;
    15     15   }
           16  +::selection {
           17  +	color: tone(-60%);
           18  +	background-color: tone(-10%);
           19  +}
           20  +::placeholder {
           21  +	color: tone(0,-0.3);
           22  +	font-style: italic;
           23  +}
    16     24   a[href] {
    17     25   	color: tone(10%);
    18     26   	text-decoration-color: tone(10%,-0.5);
    19     27   	&:hover {
    20     28   		color: white;
    21     29   		text-shadow: 0 0 15px tone(20%);
    22     30   		text-decoration-color: tone(10%,-0.1);
    23     31   	}
           32  +	&.button { @extend %button; }
    24     33   }
    25     34   a[href^="//"],
    26     35   a[href^="http://"],
    27     36   a[href^="https://"] { // external link
    28     37   	&:hover::after {
    29     38   		color: black;
    30     39   		background-color: white;
................................................................................
    51     60   %glow {
    52     61   	box-shadow: 0 0 20px tone(0%,-0.8);
    53     62   }
    54     63   
    55     64   %button {
    56     65   	@extend %sans;
    57     66   	font-size: 14pt;
           67  +	box-sizing: border-box;
    58     68   	padding: 0.1in 0.2in;
    59     69   	border: 1px solid black;
    60     70   	color: tone(25%);
    61     71   	text-shadow: 1px 1px black;
    62     72   	text-decoration: none;
    63     73   	text-align: center;
    64     74   	cursor: default;
................................................................................
   124    134   }
   125    135   
   126    136   $grad-ui-focus: linear-gradient(to bottom,
   127    137   	tone(-50%),
   128    138   	tone(-35%)
   129    139   );
   130    140   
   131         -input[type='text'], input[type='password'], textarea {
          141  +input[type='text'], input[type='password'], textarea, select {
   132    142   	@extend %serif;
   133    143   	padding: 0.08in 0.1in;
   134    144   	box-sizing: border-box;
   135    145   	border: 1px solid black;
   136    146   	background: linear-gradient(to bottom, tone(-55%), tone(-40%));
   137    147   	font-size: 16pt;
   138    148   	color: tone(25%);
................................................................................
   141    151   		color: white;
   142    152   		border-image: linear-gradient(to bottom, tone(-10%), tone(-30%)) 1 / 1px;
   143    153   		background: $grad-ui-focus;
   144    154   		outline: none;
   145    155   		@extend %glow;
   146    156   	}
   147    157   }
          158  +select { width: 100%; }
   148    159   
   149    160   @mixin glass {
   150    161   	@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
   151    162   		backdrop-filter: blur(40px);
   152    163   		-webkit-backdrop-filter: blur(40px);
   153    164   		background-color: tone(-53%, -0.7);
   154    165   	}
................................................................................
   262    273   			grid-row: 2 / 3;
   263    274   		}
   264    275   	}
   265    276   	> .stats {
   266    277   		grid-column: 3 / 4;
   267    278   		grid-row: 1 / 3;
   268    279   	}
   269         -	> .menu {
          280  +	> form.actions {
   270    281   		grid-column: 1 / 3; grid-row: 2 / 3;
   271    282   		padding-top: 0.075in;
   272    283   		flex-wrap: wrap;
   273    284   		display: flex;
   274    285   		justify-content: center;
   275    286   		align-items: center;
   276    287   		> a[href] {
   277         -			@extend %button;
   278    288   			display: block;
   279    289   			margin: 0.025in 0.05in;
   280    290   		}
   281    291   		> hr {
   282    292   			all: unset;
   283    293   			display: block;
   284    294   			height: 0.3in;
................................................................................
   289    299   }
   290    300   
   291    301   .epithet {
   292    302   	display: inline-block;
   293    303   	background: tone(20%);
   294    304   	color: tone(-45%);
   295    305   	text-shadow: 0 0 3px tone(-30%, -0.4);
   296         -	border-radius: 3px;
          306  +	border-radius: 2px;
   297    307   	padding: 6px;
   298    308   	padding-top: 2px;
   299    309   	padding-bottom: 4px;
   300    310   	font-size: 80%;
   301    311   	vertical-align: top;
   302    312   	font-weight: 300;
   303    313   	letter-spacing: 0.5px;
................................................................................
   321    331   		tone(-55%) 10%,
   322    332   		tone(-50%) 80%,
   323    333   		tone(-45%)
   324    334   	);
   325    335   	// outline: 1px solid black;
   326    336   }
   327    337   
   328         -body.error .message {
          338  +//body.error .message {
          339  +.message {
   329    340   	@extend %box;
          341  +	display: block;
   330    342   	width: 4in;
   331    343   	margin:auto;
   332    344   	padding: 0.5in;
   333    345   	text-align: center;
   334    346   }
   335    347   
   336    348   div.login {
................................................................................
   469    481   		padding: 0.1in;
   470    482   		padding-left: 0.15in;
   471    483   		>.nym { font-weight: bold; }
   472    484   		color: tone(0%,-0.4);
   473    485   		> span.nym { color: tone(10%) }
   474    486   		> span.handle { color: tone(-5%) }
   475    487   		background: linear-gradient(to right, tone(-55%), transparent);
          488  +		&:hover {
          489  +			> span.nym { color: white; }
          490  +			> span.handle { color: tone(15%) }
          491  +		}
   476    492   	}
   477    493   	>.content {
   478    494   		grid-column: 2/4; grid-row: 1/2;
   479    495   		padding: 0.2in;
   480    496   		@extend %serif;
   481    497   		font-size: 110%;
   482    498   		text-align: justify;
................................................................................
   510    526   	}
   511    527   }
   512    528   
   513    529   body.conf main {
   514    530   	display: grid;
   515    531   	grid-template-columns: 2in 1fr;
   516    532   	grid-template-rows: max-content 1fr;
   517         -	> .menu {
          533  +	> menu {
   518    534   		margin-left: -0.25in;
   519    535   		grid-column: 1/2; grid-row: 1/2;
   520    536   		background: linear-gradient(to bottom, tone(-45%),tone(-55%));
   521    537   		border: 1px solid black;
   522    538   		padding: 0.1in;
   523    539   		> a[href] {
   524    540   			@extend %button;
................................................................................
   546    562   			border-left: none;
   547    563   			text-shadow: 1px 1px 0 black;
   548    564   		}
   549    565   	}
   550    566   
   551    567   }
   552    568   
          569  +hr {
          570  +	border: none;
          571  +	border-top: 1px solid tone(-30%);
          572  +	border-bottom: 1px solid tone(-55%);
          573  +}
   553    574   form {
          575  +	margin: 0.15in 0;
          576  +	> p:first-child { margin-top: 0; }
          577  +	> p:last-child { margin-bottom: 0; }
   554    578   	.elem {
   555    579   		margin: 0.1in 0;
   556    580   		label { display:block; font-weight: bold; padding: 0.03in 0; }
   557    581   		.txtbox {
   558    582   			@extend %serif;
   559    583   			box-sizing: border-box;
   560    584   			padding: 0.08in 0.1in;
   561    585   			border: 1px solid black;
   562    586   			background: tone(-55%);
   563    587   		}
   564         -		textarea { resize: vertical; min-height: 2in; }
   565    588   		input, textarea, .txtbox {
   566    589   			display: block;
   567    590   			width: 100%;
   568    591   		}
   569         -		button { float: right; width: 50%; }
          592  +		textarea { resize: vertical; min-height: 2in; }
          593  +	}
          594  +	.elem + %button { margin-left: 50%; width: 50%; }
          595  +}
          596  +
          597  +menu.choice {
          598  +	display: flex;
          599  +	&.horizontal {
          600  +		flex-flow: row-reverse wrap;
          601  +		justify-content: space-evenly;
          602  +	}
          603  +	&.vertical {
          604  +		flex-flow: column;
          605  +		margin-left: 50%;
          606  +	}
          607  +	&.vertical-float {
          608  +		flex-flow: column;
          609  +		float: right;
          610  +		width: 40%;
          611  +		margin-left: 0.1in;
          612  +	}
          613  +	> %button { display: block; margin: 2px; flex-grow: 1 }
          614  +}
          615  +
          616  +.check-panel {
          617  +	display: flex;
          618  +	flex-flow: row wrap;
          619  +	> label {
          620  +		display: block;
          621  +		box-sizing: border-box;
          622  +		width: calc(50% - 0.2in);
          623  +		padding: 0.1in 0.1in;
          624  +		margin: 0.1in 0.1in;
          625  +		background: tone(-45%);
          626  +		border: 1px solid black;
          627  +		text-shadow: 1px 1px black;
          628  +		flex-grow: 1;
          629  +		&:focus-within {
          630  +			border: 1px inset tone(-10%);
          631  +			background: tone(-50%);
          632  +		}
          633  +	}
          634  +	input[type="checkbox"] {
          635  +		-webkit-appearance: none;
          636  +		padding: 0.5em;
          637  +		background: tone(-35%);
          638  +		border: 1px outset tone(-50%);
          639  +		vertical-align: bottom;
          640  +		box-shadow: 0 1px tone(-50%);
          641  +		&:checked {
          642  +			border: 1px inset tone(-35%);
          643  +			background: tone(-60%);
          644  +			box-shadow: 0 1px tone(-40%);
          645  +		}
          646  +		&:focus {
          647  +			border-color: tone(10%);
          648  +			outline: none;
          649  +		}
   570    650   	}
   571    651   }
   572    652   
   573    653   @keyframes flashup {
   574    654   	0% { opacity: 0; transform: scale(0.8); }
   575    655   	10% { opacity: 1; transform: scale(1.1); }
   576    656   	80% { opacity: 1; transform: scale(1); }
................................................................................
   591    671   	border-radius: 3px;
   592    672   	box-shadow: 0 0 50px tone(-55%);
   593    673   	color: white;
   594    674   	animation: ease forwards flashup;
   595    675   	//cubic-bezier(0.4, 0.63, 0.6, 0.31)
   596    676   	animation-duration: 3s;
   597    677   }
          678  +
          679  +form.action-bar {
          680  +	display: flex;
          681  +	> * {
          682  +		flex-grow: 1;
          683  +		flex-basis: 0;
          684  +		margin-left: 0.1in;
          685  +	}
          686  +	> *:first-child {
          687  +		margin-left: 0;
          688  +	}
          689  +}

Modified store.t from [f53ab94a55] to [004846cca6].

     1      1   -- vim: ft=terra
     2      2   local m = {
     3         -	timepoint = int64;
            3  +	timepoint = lib.osclock.time_t;
     4      4   	scope = lib.enum {
     5      5   		'public', 'private', 'local';
     6      6   		'personal', 'direct', 'circle';
     7      7   	};
     8      8   	notiftype = lib.enum {
     9      9   		'mention', 'like', 'rt', 'react'
    10     10   	};
    11     11   
    12         -	relation = lib.enum {
    13         -		'follow', 'mute', 'block'
           12  +	relation = lib.set {
           13  +		'silence', -- messages will not be accepted
           14  +		'collapse', -- posts will be collapsed by default
           15  +		'disemvowel', -- posts will be ritually humiliated, but shown
           16  +		'avoid', -- posts will be kept out of the timeline but will show on users' posts and in conversations
           17  +		'follow',
           18  +		'mute', -- posts will be completely hidden at all times
           19  +		'block', -- no interactions will be permitted, but posts will remain visible
    14     20   	};
    15     21   	credset = lib.set {
    16     22   		'pw', 'otp', 'challenge', 'trust'
    17     23   	};
    18     24   	privset = lib.set {
    19     25   		'post', 'edit', 'acct', 'upload', 'censor', 'admin', 'invite'
    20     26   	};
................................................................................
   142    148   	id: uint64
   143    149   	author: uint64
   144    150   	subject: str
   145    151   	body: str
   146    152   	acl: str
   147    153   	posted: m.timepoint
   148    154   	discovered: m.timepoint
          155  +	edited: m.timepoint
          156  +	chgcount: uint
   149    157   	mentions: lib.mem.ptr(uint64)
   150    158   	circles: lib.mem.ptr(uint64) --only meaningful if scope is set to circle
   151    159   	convoheaduri: str
   152    160   	parent: uint64
   153    161   -- ephemera
   154    162   	localpost: bool
   155    163   	source: &m.source
          164  +
          165  +	-- save :: bool -> {} (defined in acl.t due to dep. hell)
   156    166   }
   157    167   
   158    168   local cnf = terralib.memoize(function(ty,rty)
   159    169   	rty = rty or ty
   160    170   	return struct {
   161    171   		enum: {&opaque, uint64, rawstring} -> intptr
   162    172   		get: {&opaque, uint64, rawstring} -> rty
................................................................................
   227    237   	aid: uint64
   228    238   	uid: uint64
   229    239   	aname: str
   230    240   	netmask: m.inet
   231    241   	privs: m.privset
   232    242   	blacklist: bool
   233    243   }
          244  +
          245  +struct m.relationship {
          246  +	agent: uint64
          247  +	patient: uint64
          248  +	rel: m.relation -- agent → patient
          249  +	recip: m.relation -- patient → agent
          250  +}
   234    251   
   235    252   -- backends only handle content on the local server
   236    253   struct m.backend { id: rawstring
   237    254   	open: &m.source -> &opaque
   238    255   	close: &m.source -> {}
   239    256   	dbsetup: &m.source -> bool -- creates the schema needed to call conprep (called only once per database e.g. with `parsav db init`)
   240    257   	conprep: {&m.source, m.prepmode.t} -> {} -- prepares queries and similar tasks that require the schema to already be in place
................................................................................
   256    273   	actor_save_privs: {&m.source, &m.actor} -> {}
   257    274   	actor_fetch_xid: {&m.source, lib.mem.ptr(int8)} -> lib.mem.ptr(m.actor)
   258    275   	actor_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.actor)
   259    276   	actor_notif_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.notif)
   260    277   	actor_enum: {&m.source} -> lib.mem.ptr(&m.actor)
   261    278   	actor_enum_local: {&m.source} -> lib.mem.ptr(&m.actor)
   262    279   	actor_stats: {&m.source, uint64} -> m.actor_stats
          280  +	actor_rel: {&m.source, uint64, uint64} -> m.relationship
   263    281   
   264    282   	actor_auth_how: {&m.source, m.inet, rawstring} -> {m.credset, bool}
   265    283   		-- returns a set of auth method categories that are available for a
   266    284   		-- given user from a certain origin
   267    285   			-- origin: inet
   268    286   			-- username: rawstring
   269    287   	actor_auth_otp: {&m.source, m.inet, rawstring, rawstring}
................................................................................
   283    301   			-> {uint64, uint64, pstr}
   284    302   		-- handles API authentication
   285    303   			-- origin: inet
   286    304   			-- handle: rawstring
   287    305   			-- key:    rawstring (X-API-Key)
   288    306   	actor_auth_record_fetch: {&m.source, uint64} -> lib.mem.ptr(m.auth)
   289    307   	actor_powers_fetch: {&m.source, uint64} -> m.powerset
   290         -	actor_session_fetch: {&m.source, uint64, m.inet} -> {lib.stat(m.auth), lib.mem.ptr(m.actor)}
          308  +	actor_session_fetch: {&m.source, uint64, m.inet, m.timepoint} -> {lib.stat(m.auth), lib.mem.ptr(m.actor)}
   291    309   		-- retrieves an auth record + actor combo suitable by AID suitable
   292    310   		-- for determining session validity & caps
   293    311   			-- aid:    uint64
   294    312   			-- origin: inet
          313  +			-- cookie issue time: m.timepoint
   295    314   	actor_auth_register_uid: {&m.source, uint64, uint64} -> {}
   296    315   		-- notifies the backend module of the UID that has been assigned for
   297    316   		-- an authentication ID
   298    317   			-- aid: uint64
   299    318   			-- uid: uint64
   300    319   
   301    320   	actor_conf_str: cnf(rawstring, lib.mem.ptr(int8))
................................................................................
   304    323   	auth_create_pw: {&m.source, uint64, bool, lib.mem.ptr(int8)} -> {}
   305    324   		-- uid: uint64
   306    325   		-- reset: bool (delete other passwords?)
   307    326   		-- pw: pstring
   308    327   	auth_purge_pw: {&m.source, uint64, rawstring} -> {}
   309    328   	auth_purge_otp: {&m.source, uint64, rawstring} -> {}
   310    329   	auth_purge_trust: {&m.source, uint64, rawstring} -> {}
          330  +	auth_sigtime_user_fetch: {&m.source, uint64} -> m.timepoint
          331  +		-- authentication tokens and accounts have a property that controls
          332  +		-- whether auth cookies dated to a certain point are valid. cookies
          333  +		-- that are generated before the timepoint are considered invalid.
          334  +		-- this is used primarily to lock out untrusted sessions.
          335  +			-- uid: uint64
          336  +	auth_sigtime_user_alter: {&m.source, uint64, m.timepoint} -> {}
          337  +			-- uid: uint64
          338  +			-- timestamp: timepoint
   311    339   
   312    340   	post_save: {&m.source, &m.post} -> {}
   313    341   	post_create: {&m.source, &m.post} -> uint64
          342  +	post_fetch: {&m.source, uint64} -> lib.mem.ptr(m.post)
   314    343   	post_enum_author_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post))
   315    344   	post_attach_ctl: {&m.source, uint64, uint64, bool} -> {}
   316    345   		-- attaches or detaches an existing database artifact
   317    346   			-- post id: uint64
   318    347   			-- artifact id: uint64
   319    348   			-- detach: bool
   320    349   	artifact_instantiate: {&m.source, lib.mem.ptr(uint8), lib.mem.ptr(int8)} -> uint64

Modified str.t from [f2457b558f] to [265a0fa659].

     1      1   -- vim: ft=terra
     2      2   -- string.t: string classes
     3      3   local util = lib.util
     4      4   local pstr = lib.mem.ptr(int8)
     5      5   local pref = lib.mem.ref(int8)
     6      6   
     7      7   local m = {
            8  +	t = pstr, ref = pref;
     8      9   	sz = terralib.externfunction('strlen', rawstring -> intptr);
     9     10   	cmp = terralib.externfunction('strcmp', {rawstring, rawstring} -> int);
    10     11   	ncmp = terralib.externfunction('strncmp', {rawstring, rawstring, intptr} -> int);
    11     12   	cpy = terralib.externfunction('stpcpy',{rawstring, rawstring} -> rawstring);
    12     13   	ncpy = terralib.externfunction('stpncpy',{rawstring, rawstring, intptr} -> rawstring);
    13     14   	cat = terralib.externfunction('strcat',{rawstring, rawstring} -> rawstring);
    14     15   	ncat = terralib.externfunction('strncat',{rawstring, rawstring, intptr} -> rawstring);
................................................................................
    92     93   
    93     94   struct m.acc {
    94     95   	buf: rawstring
    95     96   	sz: intptr
    96     97   	run: intptr
    97     98   	space: intptr
    98     99   }
          100  +
          101  +terra m.cdowncase(c: int8)
          102  +	if c >= @'A' and c <= @'Z' then
          103  +		return c + (@'a' - @'A')
          104  +	else return c end
          105  +end
          106  +
          107  +terra m.cupcase(c: int8)
          108  +	if c >= @'a' and c <= @'z' then
          109  +		return c - (@'a' - @'A')
          110  +	else return c end
          111  +end
    99    112   
   100    113   local terra biggest(a: intptr, b: intptr)
   101    114   	if a > b then return a else return b end
   102    115   end
   103    116   
   104    117   terra m.acc:init(run: intptr)
   105    118   	--lib.dbg('initializing string accumulator')

Modified view/conf-profile.tpl from [f1f77d7014] to [d384fd3b9f].

     1      1   <form method="post">
     2         -	<div class="elem"><label>handle</label> <div class="txtbox">@!handle</div>
            2  +	<div class="elem"><label>handle</label> <div class="txtbox">@!handle</div></div>
     3      3   	<div class="elem"><label for="nym">display name</label> <input type="text" name="nym" id="nym" placeholder="j. random poster" value="@:nym"></div>
     4      4   	<div class="elem"><label for="bio">bio</label><textarea name="bio" id="bio" placeholder="tall, dark, and mysterious">@!bio</textarea></div>
     5      5   	<button>commit</button>
     6      6   </form>

Added view/conf-sec-credmg.tpl version [43efff9618].

            1  +<hr>
            2  +<form method="post">
            3  +	<p>your account can currently be accessed with the credentials listed below. if you fear a credential has been compromised, you can revoke or reset it.</p>
            4  +	<select size="6" name="cred">
            5  +		@credlist
            6  +	</select>
            7  +	<menu class="horizontal choice">
            8  +		<button name="act" value="reset">reset</button>
            9  +		<button name="act" value="revoke">revoke</button>
           10  +	</menu>
           11  +</form>
           12  +<hr>
           13  +<form method="post">
           14  +	<p>you can associate extra credentials with your account. you can also limit how much of your authority these credentials can be used to exercise &mdash; for instance, it might be useful to create API keys that can read your timeline, but not post as you or access any administrative powers you may have. if you don't select a capability set, the credential will be able to wield the full scope of your powers.</p>
           15  +	<div class="check-panel">
           16  +		<label><input type="checkbox" name="allow-post"> post</label>
           17  +		<label><input type="checkbox" name="allow-edit"> edit</label>
           18  +		<label><input type="checkbox" name="allow-acct"> manage account</label>
           19  +		<label><input type="checkbox" name="allow-upload"> upload artifacts</label>
           20  +		<label><input type="checkbox" name="allow-censor"> moderation</label>
           21  +		<label><input type="checkbox" name="allow-admin"> other admin powers</label>
           22  +		<label><input type="checkbox" name="allow-invite"> invite</label>
           23  +	</div>
           24  +	<p>you can also specify an IP address range in CIDR format to associate with this credential. if you do so, this credential will only be usable when connecting from an IP address in that range. otherwise, it will be valid when connecting from anywhere on the internet.</p>
           25  +	<div class="elem">
           26  +		<label for="netmask">netmask</label>
           27  +		<input type="text" name="netmask" id="netmask" placeholder="10.0.0.0/8">
           28  +	</div>
           29  +	<menu class="vertical choice">
           30  +		<button name="kind" value="pw">new password</button>
           31  +		<button name="kind" value="otp">new OTP key</button>
           32  +		<button name="kind" value="api">new API token</button>
           33  +		<button name="kind" value="challenge">new challenge key</button>
           34  +	</div>
           35  +</form>

Modified view/conf-sec.tpl from [7ba95a81c5] to [de1cf7e8f0].

     1      1   <form method="post">
     2      2   	<p>if you are concerned that your account may have been compromised, you can terminate all other login sessions by invalidating their session cookies. note that this will not have any effect on API tokens; these must be revoked separately!</p>
     3         -	<label>
     4         -		sessions valid from
            3  +	<div class="elem">
            4  +		<label> sessions valid from </label>
     5      5   		<div class="txtbox">@lastreset</div>
     6         -	</label>
            6  +	</div>
     7      7   	<button type="submit" name="act" value="invalidate">
     8      8   		invalidate other sessions
     9      9   	</button>
    10     10   </form>

Modified view/conf.tpl from [bd130ad9d3] to [09b447ff32].

     1         -<div class="menu">
            1  +<menu>
     2      2   	<a href="/conf/profile">profile</a>
     3      3   	<a href="/conf/avi">avatar</a>
     4      4   	<a href="/conf/sec">security</a>
     5      5   	<a href="/conf/rel">relationships</a>
     6      6   	<a href="/conf/qnt">quarantine</a>
     7      7   	<a href="/conf/acl">ACL shortcuts</a>
     8      8   	<a href="/conf/rooms">chatrooms</a>
     9      9   	<a href="/conf/circles">circles</a>
    10     10   	@menu
    11         -</div>
           11  +</menu>
    12     12   
    13     13   <div class="panel">
    14     14   	@panel
    15     15   </div>

Added view/confirm.tpl version [9198c794e9].

            1  +<form class="message">
            2  +	<img class="icon" src="/s/query.webp">
            3  +	<h1>@title</h1>
            4  +	<p>@query</p>
            5  +	<menu class="horizontal choice">
            6  +		<a class="button" href="@:cancel">cancel</a>
            7  +		<button name="act" value="confirm">confirm</button>
            8  +	</menu>
            9  +</form>

Modified view/load.lua from [f63ef60595] to [212041720e].

     1      1   -- because lua can't scan directories, we need a
     2      2   -- file that indexes the templates manually, and
     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  +	'confirm';
     8      9   	'tweet';
     9     10   	'profile';
    10     11   	'compose';
    11     12   
    12     13   	'login-username';
    13     14   	'login-challenge';
    14     15   
    15     16   	'conf';
    16     17   	'conf-profile';
           18  +	'conf-sec';
           19  +	'conf-sec-credmg';
    17     20   }
    18     21   
    19     22   local ingest = function(filename)
    20     23   	local hnd = io.open(path..'/'..filename)
    21     24   	local txt = hnd:read('*a')
    22     25   	io.close(hnd)
    23     26   	txt = txt:gsub('([^\\])!%b[]', '%1')

Modified view/profile.tpl from [d21ccabbe8] to [cfeb837b05].

     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     14   		<tr><th>@timephrase</th> <td>@tweetday</td></tr>
    15     15   	</table>
    16         -	<div class="menu">
    17         -		<a href="/@:xid">posts</a>
    18         -		<a href="/@:xid/media">media</a>
    19         -		<a href="/@:xid/social">associates</a>
           16  +	<form class="actions">
           17  +		<a class="button" href="/@:xid">posts</a>
           18  +		<a class="button" href="/@:xid/media">media</a>
           19  +		<a class="button" href="/@:xid/social">associates</a>
    20     20   		<hr>
    21     21   		@auxbtn
    22         -	</div>
           22  +	</form>
    23     23   </div>