parsav  Check-in [7129658e1d]

Overview
Comment:work on admin ui
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 7129658e1d31700f1ac316f1d4d2962d31f39dc951a2f2d34c5830cf8aed0a84
User & Date: lexi on 2021-01-02 04:47:03
Other Links: manifest | tags
Context
2021-01-02
18:32
iterate on user mgmt UI check-in: f09cd18161 user: lexi tags: trunk
04:47
work on admin ui check-in: 7129658e1d user: lexi tags: trunk
2021-01-01
16:42
handle (some) deletions in live.js check-in: 53ef86f7ff user: lexi tags: trunk
Changes

Modified backend/pgsql.t from [30375d8380] to [962a3e64e0].

    26     26   
    27     27   	actor_fetch_uid = {
    28     28   		params = {uint64}, sql = [[
    29     29   			select a.id, a.nym, a.handle, a.origin, a.bio,
    30     30   			       a.avataruri, a.rank, a.quota, a.key, a.epithet,
    31     31   			       extract(epoch from a.knownsince)::bigint,
    32     32   				   coalesce(a.handle || '@' || s.domain,
    33         -				            '@' || a.handle) as xid
           33  +				            '@' || a.handle) as xid,
           34  +			       a.invites
    34     35   
    35     36   			from      parsav_actors  as a
    36     37   			left join parsav_servers as s
    37     38   				on a.origin = s.id
    38     39   			where a.id = $1::bigint
    39     40   		]];
    40     41   	};
................................................................................
    42     43   	actor_fetch_xid = {
    43     44   		params = {pstring}, sql = [[
    44     45   			select a.id, a.nym, a.handle, a.origin, a.bio,
    45     46   			       a.avataruri, a.rank, a.quota, a.key, a.epithet,
    46     47   			       extract(epoch from a.knownsince)::bigint,
    47     48   				   coalesce(a.handle || '@' || s.domain,
    48     49   				            '@' || a.handle) as xid,
           50  +			       a.invites,
    49     51   
    50     52   				coalesce(s.domain,
    51     53   				        (select value from parsav_config
    52     54   							where key='domain' limit 1)) as domain
    53     55   
    54     56   			from      parsav_actors  as a
    55     57   			left join parsav_servers as s
................................................................................
    58     60   			where $1::text = (a.handle || '@' || domain) or
    59     61   			      $1::text = ('@' || a.handle || '@' || domain) or
    60     62   				  (a.origin is null and
    61     63   					  $1::text = a.handle or
    62     64   					  $1::text = ('@' || a.handle))
    63     65   		]];
    64     66   	};
           67  +
           68  +	actor_purge_uid = {
           69  +		params = {uint64}, cmd = true, sql = [[
           70  +			with d as ( -- cheating
           71  +				delete from parsav_sanctions where victim = $1::bigint
           72  +			)
           73  +			delete from parsav_actors where id = $1::bigint
           74  +		]];
           75  +	};
    65     76   
    66     77   	actor_save = {
    67     78   		params = {
    68     79   			uint64, --id
    69     80   			rawstring, --nym
    70     81   			rawstring, --handle
    71     82   			rawstring, --bio 
    72     83   			rawstring, --epithet
    73     84   			rawstring, --avataruri
    74     85   			uint64, --avatarid
    75     86   			uint16, --rank
    76         -			uint32 --quota
           87  +			uint32, --quota
           88  +			uint32 --invites
    77     89   		}, cmd = true, sql = [[
    78     90   			update parsav_actors set
    79     91   				nym = $2::text,
    80     92   				handle = $3::text,
    81     93   				bio = $4::text,
    82     94   				epithet = $5::text,
    83     95   				avataruri = $6::text,
    84     96   				avatarid = $7::bigint,
    85     97   				rank = $8::smallint,
    86         -				quota = $9::integer
    87         -				--invites are controlled by their own specialized routines
           98  +				quota = $9::integer,
           99  +				invites = $10::integer
    88    100   			where id = $1::bigint
    89    101   		]];
    90    102   	};
    91    103   
    92    104   	actor_create = {
    93    105   		params = {
    94    106   			rawstring, rawstring, uint64, lib.store.timepoint,
    95    107   			rawstring, rawstring, lib.mem.ptr(uint8),
    96         -			rawstring, uint16, uint32
          108  +			rawstring, uint16, uint32, uint32
    97    109   		};
    98    110   		sql = [[
    99    111   			insert into parsav_actors (
   100    112   				nym,handle,
   101    113   				origin,knownsince,
   102    114   				bio,avataruri,key,
   103         -				epithet,rank,quota
          115  +				epithet,rank,quota,
          116  +				invites
   104    117   			) values ($1::text, $2::text,
   105    118   				case when $3::bigint = 0 then null
   106    119   				     else $3::bigint end,
   107    120   				to_timestamp($4::bigint),
   108    121   				$5::bigint, $6::bigint, $7::bytea,
   109         -				$8::text, $9::smallint, $10::integer
          122  +				$8::text, $9::smallint, $10::integer,
          123  +				$11::integer
   110    124   			) returning id
   111    125   		]];
   112    126   	};
   113    127   
   114    128   	actor_auth_pw = {
   115    129   		params = {pstring,rawstring,pstring,lib.store.inet}, sql = [[
   116    130   			select a.aid, a.uid, a.name from parsav_auth as a
................................................................................
   125    139   	};
   126    140   
   127    141   	actor_enum_local = {
   128    142   		params = {}, sql = [[
   129    143   			select id, nym, handle, origin, bio,
   130    144   			       null::text, rank, quota, key, epithet,
   131    145   			       extract(epoch from knownsince)::bigint,
   132         -				handle ||'@'||
   133         -				(select value from parsav_config
   134         -					where key='domain' limit 1) as xid
          146  +					'@' || handle,
          147  +				   invites
   135    148   			from parsav_actors where origin is null
          149  +			order by nullif(rank,0) nulls last, handle
   136    150   		]];
   137    151   	};
   138    152   
   139    153   	actor_enum = {
   140    154   		params = {}, sql = [[
   141    155   			select a.id, a.nym, a.handle, a.origin, a.bio,
   142    156   			       a.avataruri, a.rank, a.quota, a.key, a.epithet,
   143    157   			       extract(epoch from a.knownsince)::bigint,
   144    158   				   coalesce(a.handle || '@' || s.domain,
   145         -				            '@' || a.handle) as xid
          159  +				            '@' || a.handle) as xid,
          160  +				   invites
   146    161   			from parsav_actors a
   147    162   			left join parsav_servers s on s.id = a.origin
          163  +			order by nullif(a.rank,0) nulls last, a.handle, a.origin
   148    164   		]];
   149    165   	};
   150    166   
   151    167   	actor_stats = {
   152    168   		params = {uint64}, sql = ([[
   153    169   			with tweets as (
   154    170   				select from parsav_posts where author = $1::bigint
................................................................................
   786    802   end
   787    803   local terra row_to_actor(r: &pqr, row: intptr): lib.mem.ptr(lib.store.actor)
   788    804   	var a: lib.mem.ptr(lib.store.actor)
   789    805   	var av: rawstring, avlen: intptr
   790    806   	var nym: rawstring, nymlen: intptr
   791    807   	var bio: rawstring, biolen: intptr
   792    808   	var epi: rawstring, epilen: intptr
   793         -	if r:null(row,5) then avlen = 0 av = nil else
          809  +	var origin: uint64 = 0
          810  +	var handle = r:_string(row, 2)
          811  +	if not r:null(row,3) then origin = r:int(uint64,row,3) end
          812  +
          813  +	var avia = lib.str.acc {buf=nil}
          814  +	if origin == 0 then
          815  +		avia:compose('/avi/',handle)
          816  +		av = avia.buf
          817  +		avlen = avia.sz+1
          818  +	elseif r:null(row,5) then
   794    819   		av = r:string(row,5)
   795    820   		avlen = r:len(row,5)+1
          821  +	else
          822  +		av = '/s/default-avatar.webp'
          823  +		avlen = 22
   796    824   	end
          825  +
   797    826   	if r:null(row,1) then nymlen = 0 nym = nil else
   798    827   		nym = r:string(row,1)
   799    828   		nymlen = r:len(row,1)+1
   800    829   	end
   801    830   	if r:null(row,4) then biolen = 0 bio = nil else
   802    831   		bio = r:string(row,4)
   803    832   		biolen = r:len(row,4)+1
................................................................................
   807    836   		epilen = r:len(row,9)+1
   808    837   	end
   809    838   	a = [ lib.str.encapsulate(lib.store.actor, {
   810    839   		nym = {`nym, `nymlen};
   811    840   		bio = {`bio, `biolen};
   812    841   		epithet = {`epi, `epilen};
   813    842   		avatar = {`av,`avlen};
   814         -		handle = {`r:string(row, 2); `r:len(row,2) + 1};
          843  +		handle = {`handle.ptr, `handle.ct + 1};
   815    844   		xid = {`r:string(row, 11); `r:len(row,11) + 1};
   816    845   	}) ]
   817    846   	a.ptr.id = r:int(uint64, row, 0);
   818    847   	a.ptr.rights = lib.store.rights_default();
   819    848   	a.ptr.rights.rank = r:int(uint16, row, 6);
   820    849   	a.ptr.rights.quota = r:int(uint32, row, 7);
          850  +	a.ptr.rights.invites = r:int(uint32, row, 12);
   821    851   	a.ptr.knownsince = r:int(int64,row, 10);
   822    852   	if r:null(row,8) then
   823    853   		a.ptr.key.ct = 0 a.ptr.key.ptr = nil
   824    854   	else
   825    855   		a.ptr.key = r:bin(row,8)
   826    856   	end
   827         -	if r:null(row,3) then a.ptr.origin = 0
   828         -	else a.ptr.origin = r:int(uint64,row,3) end
          857  +	a.ptr.origin = origin
          858  +	if avia.buf ~= nil then avia:free() end
   829    859   	return a
   830    860   end
   831    861   
   832    862   local privmap = lib.store.privmap
   833    863   
   834    864   local checksha = function(src, hash, origin, username, pw)
   835    865   	local validate = function(kind, cred, credlen)
................................................................................
   873    903   local schema = sqlsquash(lib.util.ingest('backend/schema/pgsql.sql'))
   874    904   local obliterator = sqlsquash(lib.util.ingest('backend/schema/pgsql-drop.sql'))
   875    905   
   876    906   local privupdate = terra(
   877    907   	src: &lib.store.source,
   878    908   	ac: &lib.store.actor
   879    909   ): {}
   880         -	var pdef = lib.store.rights_default().powers
          910  +	var pdef: lib.store.powerset pdef:clear()
   881    911   	var map = array([privmap])
   882    912   	for i=0, [map.type.N] do
   883    913   		var d = pdef and map[i].priv
   884    914   		var u = ac.rights.powers and map[i].priv
   885    915   		queries.actor_power_delete.exec(src, ac.id, map[i].name)
   886    916   		if d:sz() > 0 and u:sz() == 0 then
   887    917   			lib.dbg('blocking power ', {map[i].name.ptr, map[i].name.ct})
................................................................................
  1042   1072   			return a
  1043   1073   		end
  1044   1074   	end];
  1045   1075   
  1046   1076   	actor_enum = [terra(src: &lib.store.source)
  1047   1077   		var r = queries.actor_enum.exec(src)
  1048   1078   		if r.sz == 0 then
  1049         -			return [lib.mem.ptr(&lib.store.actor)] { ct = 0, ptr = nil }
         1079  +			return [lib.mem.lstptr(lib.store.actor)].null()
  1050   1080   		else defer r:free()
  1051         -			var mem = lib.mem.heapa([&lib.store.actor], r.sz)
         1081  +			var mem = lib.mem.heapa([lib.mem.ptr(lib.store.actor)], r.sz)
  1052   1082   			for i=0,r.sz do
  1053         -				mem.ptr[i] = row_to_actor(&r, i).ptr
  1054         -				mem.ptr[i].source = src
         1083  +				mem.ptr[i] = row_to_actor(&r, i)
         1084  +				mem(i).ptr.source = src
  1055   1085   			end
  1056         -			return [lib.mem.ptr(&lib.store.actor)] { ct = r.sz, ptr = mem.ptr }
         1086  +			return [lib.mem.lstptr(lib.store.actor)] { ct = r.sz, ptr = mem.ptr }
  1057   1087   		end
  1058   1088   	end];
  1059   1089   
  1060   1090   	actor_enum_local = [terra(src: &lib.store.source)
  1061   1091   		var r = queries.actor_enum_local.exec(src)
  1062   1092   		if r.sz == 0 then
  1063         -			return [lib.mem.ptr(&lib.store.actor)] { ct = 0, ptr = nil }
         1093  +			return [lib.mem.lstptr(lib.store.actor)].null()
  1064   1094   		else defer r:free()
  1065         -			var mem = lib.mem.heapa([&lib.store.actor], r.sz)
         1095  +			var mem = lib.mem.heapa([lib.mem.ptr(lib.store.actor)], r.sz)
  1066   1096   			for i=0,r.sz do
  1067         -				mem.ptr[i] = row_to_actor(&r, i).ptr
  1068         -				mem.ptr[i].source = src
         1097  +				mem.ptr[i] = row_to_actor(&r, i)
         1098  +				mem(i).ptr.source = src
  1069   1099   			end
  1070         -			return [lib.mem.ptr(&lib.store.actor)] { ct = r.sz, ptr = mem.ptr }
         1100  +			return [lib.mem.lstptr(lib.store.actor)] { ct = r.sz, ptr = mem.ptr }
  1071   1101   		end
  1072   1102   	end];
  1073   1103   
  1074   1104   	actor_auth_how = [terra(
  1075   1105   			src: &lib.store.source,
  1076   1106   			ip: lib.store.inet,
  1077   1107   			username: rawstring
................................................................................
  1129   1159   
  1130   1160   			var a = row_to_actor(&r, 0)
  1131   1161   			a.ptr.source = src
  1132   1162   
  1133   1163   			var au = [lib.stat(lib.store.auth)] { ok = true }
  1134   1164   			au.val.aid = aid
  1135   1165   			au.val.uid = a.ptr.id
  1136         -			if not r:null(0,13) then -- restricted?
         1166  +			if not r:null(0,14) then -- restricted?
  1137   1167   				au.val.privs:clear()
  1138         -				(au.val.privs.post   << r:bool(0,14)) 
  1139         -				(au.val.privs.edit   << r:bool(0,15))
  1140         -				(au.val.privs.acct   << r:bool(0,16))
  1141         -				(au.val.privs.upload << r:bool(0,17))
  1142         -				(au.val.privs.censor << r:bool(0,18))
  1143         -				(au.val.privs.admin  << r:bool(0,19))
         1168  +				(au.val.privs.post   << r:bool(0,15)) 
         1169  +				(au.val.privs.edit   << r:bool(0,16))
         1170  +				(au.val.privs.acct   << r:bool(0,17))
         1171  +				(au.val.privs.upload << r:bool(0,18))
         1172  +				(au.val.privs.censor << r:bool(0,19))
         1173  +				(au.val.privs.admin  << r:bool(0,20))
  1144   1174   			else au.val.privs:fill() end
  1145   1175   
  1146   1176   			return au, a
  1147   1177   		end
  1148   1178   
  1149   1179   		::fail:: return [lib.stat   (lib.store.auth) ] { ok = false        },
  1150   1180   			            [lib.mem.ptr(lib.store.actor)] { ptr = nil, ct = 0 }
................................................................................
  1219   1249   	end];
  1220   1250   
  1221   1251   	actor_powers_fetch = getpow;
  1222   1252   	actor_save = [terra(
  1223   1253   		src: &lib.store.source,
  1224   1254   		ac: &lib.store.actor
  1225   1255   	): {}
         1256  +		var avatar = ac.avatar
         1257  +		if ac.origin == 0 then avatar = nil end
  1226   1258   		queries.actor_save.exec(src,
  1227   1259   			ac.id, ac.nym, ac.handle,
  1228         -			ac.bio, ac.epithet, ac.avatar,
  1229         -			ac.avatarid, ac.rights.rank, ac.rights.quota)
         1260  +			ac.bio, ac.epithet, avatar,
         1261  +			ac.avatarid, ac.rights.rank, ac.rights.quota, ac.rights.invites)
  1230   1262   	end];
  1231   1263   
  1232   1264   	actor_save_privs = privupdate;
  1233   1265   
  1234   1266   	actor_create = [terra(
  1235   1267   		src: &lib.store.source,
  1236   1268   		ac: &lib.store.actor
  1237   1269   	): uint64
  1238         -		var r = queries.actor_create.exec(src,ac.nym, ac.handle, ac.origin, ac.knownsince, ac.bio, ac.avatar, ac.key, ac.epithet, ac.rights.rank, ac.rights.quota)
         1270  +		var r = queries.actor_create.exec(src,ac.nym, ac.handle, ac.origin, ac.knownsince, ac.bio, ac.avatar, ac.key, ac.epithet, ac.rights.rank, ac.rights.quota,ac.rights.invites)
  1239   1271   		if r.sz == 0 then lib.bail('failed to create actor!') end
  1240   1272   		ac.id = r:int(uint64,0,0)
  1241   1273   
  1242   1274   		-- check against default rights, insert records for wherever powers differ
  1243   1275   		lib.dbg('created new actor, establishing powers')
  1244   1276   		privupdate(src,ac)
  1245   1277   
  1246   1278   		lib.dbg('powers established')
  1247   1279   		return ac.id
  1248   1280   	end];
  1249   1281   
         1282  +	actor_purge_uid = [terra(
         1283  +		src: &lib.store.source,
         1284  +		uid: uint64
         1285  +	) queries.actor_purge_uid.exec(src,uid) end];
         1286  +
  1250   1287   	auth_enum_uid = [terra(
  1251   1288   		src: &lib.store.source,
  1252   1289   		uid: uint64
  1253   1290   	): lib.mem.ptr(lib.mem.ptr(lib.store.auth))
  1254   1291   		var r = queries.auth_enum_uid.exec(src,uid)
  1255   1292   		if r.sz == 0 then return [lib.mem.ptr(lib.mem.ptr(lib.store.auth))].null() end
  1256   1293   		var ret = lib.mem.heapa([lib.mem.ptr(lib.store.auth)], r.sz)

Modified backend/schema/pgsql.sql from [b4d8dee98e] to [72a3e65e6e].

    30     30   		on delete cascade, -- null origin = local actor
    31     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  +	invites   integer not null default 0,
    37     38   	key       bytea, -- private if localactor; public if remote
    38     39   	epithet   text,
    39     40   	authtime  timestamp not null default now(), -- cookies earlier than this timepoint will not be accepted
    40     41   	
    41     42   	unique (handle,origin)
    42     43   );
    43     44   
................................................................................
   142    143   
   143    144   create table parsav_room_members (
   144    145   	room   bigint not null references parsav_rooms(id) on delete cascade,
   145    146   	member bigint not null references parsav_actors(id) on delete cascade,
   146    147   	rank   smallint not null default 0,
   147    148   	admin  boolean not null default false, -- non-admins with rank can only moderate + invite
   148    149   	title  text, -- admin-granted title like reddit flair
   149         -	vouchedby bigint references parsav_actors(id)
          150  +	vouchedby bigint references parsav_actors(id) on delete set null
   150    151   );
   151    152   
   152    153   create table parsav_invites (
   153    154   	id          bigint primary key default (1+random()*(2^63-1))::bigint,
   154    155   	-- when a user is created from an invite, the invite is deleted and the invite
   155    156   	-- ID becomes the user ID. privileges granted on the invite ID during the invite
   156    157   	-- process are thus inherited by the user

Modified config.lua from [6a4b9180fe] to [5a4f5a8d5b].

    52     52   		-- we should add support for content-encoding headers and pre-compress
    53     53   		-- the damn things before compiling (also making the binary smaller)
    54     54   		{'style.css', 'text/css'};
    55     55   		{'live.js', 'text/javascript'}; -- rrrrrrrr
    56     56   		{'default-avatar.webp', 'image/webp'}; -- needs inkscape-exclusive svg features
    57     57   		{'padlock.svg', 'image/svg+xml'};
    58     58   		{'warn.svg', 'image/svg+xml'};
    59         -		{'query.svg', 'image/svg+xml'};
           59  +		{'query.webp', 'image/webp'};
    60     60   	};
    61     61   	default_ui_accent = tonumber(default('parsav_ui_default_accent',323));
    62     62   }
    63     63   if os.getenv('parsav_let_me_be_an_idiot') == "i know what i'm doing" then
    64     64   	conf.braingeniousmode = true -- SOUND GENERAL QUARTERS
    65     65   end
    66     66   if u.ping '.fslckout' or u.ping '_FOSSIL_' then

Modified makefile from [e6a5371547] to [eedbd28993].

     1      1   dl = git
     2      2   dbg-flags = $(if $(dbg),-g)
     3      3   
     4         -images = static/default-avatar.webp
            4  +images = static/default-avatar.webp static/query.webp
     5      5   #$(addsuffix .webp, $(basename $(wildcard static/*.svg)))
     6      6   styles = $(addsuffix .css, $(basename $(wildcard static/*.scss)))
     7      7   
     8      8   parsav parsavd: parsav.t config.lua pkgdata.lua $(images) $(styles)
     9      9   	terra $(dbg-flags) $<
    10     10   parsav.o parsavd.o: parsav.t config.lua pkgdata.lua $(images) $(styles)
    11     11   	env parsav_link=no terra $(dbg-flags) $<

Modified mem.t from [1f9397ac82] to [a0c3213659].

   123    123   		end
   124    124   	end
   125    125   	return t
   126    126   end
   127    127   
   128    128   m.ptr = terralib.memoize(function(ty) return mkptr(ty, true) end)
   129    129   m.ref = terralib.memoize(function(ty) return mkptr(ty, false) end)
          130  +m.lstptr = function(ty) return m.ptr(m.ptr(ty)) end -- make code more readable
   130    131   
   131    132   m.vec = terralib.memoize(function(ty)
   132    133   	local v = terralib.types.newstruct(string.format('vec<%s>', ty.name))
   133    134   	v.entries = {
   134    135   		{field = 'storage', type = m.ptr(ty)};
   135    136   		{field = 'sz', type = intptr};
   136    137   		{field = 'run', type = intptr};

Modified mgtool.t from [39281cf1cf] to [ee3dfa15a8].

    21     21   
    22     22   local ctlcmds = {
    23     23   	{ 'start', 'start a new instance of the server' };
    24     24   	{ 'stop', 'stop a running instance' };
    25     25   	{ 'ls', 'list all running instances' };
    26     26   	{ 'attach', 'capture log output from a running instance' };
    27     27   	{ 'db', 'set up and manage the database' };
    28         -	{ 'user', 'manage users, privileges, and credentials'};
           28  +	{ 'user', 'create and manage users, privileges, and credentials'};
           29  +	{ 'actor', 'manage and purge actors, epithets, and ranks'};
    29     30   	{ 'mkroot <handle>', 'establish a new root user with the given handle' };
    30         -	{ 'actor <xid> purge-all', 'remove all traces of a user from the database (except local user credentials -- use \27[1mauth all purge\27[m to prevent a user from accessing the instance)' };
    31         -	{ 'actor <xid> create', 'instantiate a new actor' };
    32         -	{ 'actor <xid> bestow <epithet>', 'bestow an epithet upon an actor' };
    33     31   	{ 'conf', 'manage the server configuration'};
           32  +	{ 'grow <count> [<acl>]', 'grant a new round of invites to all users, or those who match the given ACL' };
    34     33   	{ 'serv dl', 'initiate an update cycle over foreign actors' };
    35     34   	{ 'tl', 'print the current local timeline to standard out' };
    36     35   	{ 'be pgsql setup-auth (managed|unmanaged)', '(PGSQL backends) select the authentication strategy to use' };
    37     36   }
    38     37   
    39     38   local cmdhelp = function(tbl)
    40     39   	local str = '\ncommands:\n'
................................................................................
   130    129   		if acks(i).success then
   131    130   			lib.report('instance #',num,' reports successful ',rep)
   132    131   		else
   133    132   			lib.report('instance #',num,' reports failed ',rep)
   134    133   		end
   135    134   	end
   136    135   end
          136  +
          137  +local terra gen_cfstr(cfmstr: rawstring, seed: intptr)
          138  +	var confirmstrs = array(
          139  +		'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'eta', 'nu', 'kappa',
          140  +		'emerald', 'carnelian', 'sapphire', 'ruby', 'amethyst'
          141  +	)
          142  +	var tdx = lib.osclock.time(nil) / 60
          143  +	cfmstr[0] = 0
          144  +	for i=0,3 do
          145  +		if i ~= 0 then lib.str.cat(cfmstr, '-') end
          146  +		lib.str.cat(cfmstr, confirmstrs[(seed ^ tdx ^ (173*i)) % [confirmstrs.type.N]])
          147  +	end
          148  +end
   137    149   
   138    150   local emp = lib.ipc.global_emperor
   139    151   local terra entry_mgtool(argc: int, argv: &rawstring): int
   140    152   	if argc < 1 then lib.bail('bad invocation!') end
   141    153   
   142    154   	lib.noise.init(2)
   143    155   	[lib.init]
................................................................................
   272    284   				return 1
   273    285   			end
   274    286   			if dbmode.arglist.ct < 1 then goto cmderr end
   275    287   
   276    288   			srv:setup(cnf) 
   277    289   			if lib.str.cmp(dbmode.arglist(0),'init') == 0 and dbmode.arglist.ct == 2 then
   278    290   				lib.report('initializing new database structure for domain ', dbmode.arglist(1))
          291  +				dlg:tx_enter()
   279    292   				if dlg:dbsetup() then
   280    293   					srv:conprep(lib.store.prepmode.conf)
   281    294   					dlg:conf_set('instance-name', dbmode.arglist(1))
          295  +					dlg:conf_set('domain', dbmode.arglist(1))
   282    296   					do var sec: int8[65] gensec(&sec[0])
          297  +						dlg:conf_set('server-secret', &sec[0])
   283    298   						dlg:conf_set('server-secret', &sec[0])
   284    299   					end
   285    300   					lib.report('database setup complete; use mkroot to create an administrative user')
   286    301   				else lib.bail('initialization process interrupted') end
          302  +				dlg:tx_complete()
   287    303   			elseif lib.str.cmp(dbmode.arglist(0),'obliterate') == 0 then
   288         -				var confirmstrs = array(
   289         -					'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'eta', 'nu', 'kappa',
   290         -					'emerald', 'carnelian', 'sapphire', 'ruby', 'amethyst'
   291         -				)
   292         -				var cfmstr: int8[64] cfmstr[0] = 0
   293         -				var tdx = lib.osclock.time(nil) / 60
   294         -				for i=0,3 do
   295         -					if i ~= 0 then lib.str.cat(&cfmstr[0], '-') end
   296         -					lib.str.cat(&cfmstr[0], confirmstrs[(tdx ^ (173*i)) % [confirmstrs.type.N]])
   297         -				end
          304  +				var cfmstr: int8[64] gen_cfstr(&cfmstr[0],0)
   298    305   
   299    306   				if dbmode.arglist.ct == 1 then
   300    307   					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])
   301    308   				elseif dbmode.arglist.ct == 2 then
   302    309   					if lib.str.cmp(dbmode.arglist(1), cfmstr) == 0 then
   303    310   						lib.warn('completely obliterating all data!')
   304    311   						dlg:obliterate_everything()
................................................................................
   392    399   					dlg:conf_set('master',root.handle)
   393    400   					lib.report('created new administrator')
   394    401   					if mg then
   395    402   						var tmppw: int8[33]
   396    403   						pwset(dlg, &tmppw, ruid, false)
   397    404   						lib.report('temporary root pw: ', {&tmppw[0], 32})
   398    405   					end
          406  +				else goto cmderr end
          407  +			elseif lib.str.cmp(mode.arglist(0),'actor') == 0 then
          408  +				var umode: pbasic umode:parse(mode.arglist.ct, &mode.arglist(0))
          409  +				if umode.help then
          410  +					[ lib.emit(false, 1, 'usage: ', `argv[0], ' actor ', umode.type.helptxt.flags, ' <xid> <cmd> [<args>…]', umode.type.helptxt.opts, cmdhelp {
          411  +						{ 'actor <xid> rank <value>', 'set an actor\'s rank to <value> (remote actors cannot exercise rank-related powers, but benefit from rank immunities)' };
          412  +						{ 'actor <xid> degrade', 'alias for `actor <xid> rank 0`' };
          413  +						{ 'actor <xid> bestow <epithet>', 'bestow an epithet upon an actor' };
          414  +						{ 'actor <xid> instantiate', 'instantiate a remote actor, retrieving their profile and posts even if no one follows them' };
          415  +						{ 'actor <xid> proscribe', 'globally ban an actor from interacting with your server' };
          416  +						{ 'actor <xid> rehabilitate', 'lift a proscription on an actor' };
          417  +						{ 'actor <xid> purge-all <confirm-str>', 'remove all traces of a user from the database (except local user credentials -- use \27[1mauth all purge\27[m to prevent a user from accessing the instance)' };
          418  +					}) ]
          419  +					return 1
          420  +				end
          421  +				if umode.arglist.ct >= 2 then
          422  +					var degrade = lib.str.cmp(umode.arglist(1),'degrade') == 0
          423  +					var xid = umode.arglist(0)
          424  +					var usr = dlg:actor_fetch_xid(pstr {ptr=xid, ct=lib.str.sz(xid)})
          425  +					if not usr then lib.bail('no such actor') end
          426  +					if degrade or lib.str.cmp(umode.arglist(1),'rank') == 0 then
          427  +						var rank: uint16
          428  +						if degrade and umode.arglist.ct == 2 then
          429  +							rank = 0
          430  +						elseif (not degrade) and umode.arglist.ct == 3 then
          431  +							var r, ok = lib.math.decparse(pstr {
          432  +								ptr = umode.arglist(2);
          433  +								ct = lib.str.sz(umode.arglist(2));
          434  +							})
          435  +							if not ok then goto cmderr end
          436  +							rank = r
          437  +						else goto cmderr end
          438  +						usr.ptr.rights.rank = rank
          439  +						dlg:actor_save(usr.ptr)
          440  +						lib.report('set user rank')
          441  +					elseif umode.arglist.ct == 3 and lib.str.cmp(umode.arglist(1),'bestow') == 0 then
          442  +						if umode.arglist(2)[0] == 0
          443  +							then usr.ptr.epithet = nil
          444  +							else usr.ptr.epithet = umode.arglist(2)
          445  +						end
          446  +						dlg:actor_save(usr.ptr)
          447  +						lib.report('bestowed a new epithet on ', usr.ptr.xid)
          448  +					elseif lib.str.cmp(umode.arglist(1),'purge-all') == 0 then
          449  +						var cfmstr: int8[64] gen_cfstr(&cfmstr[0],usr.ptr.id)
          450  +						if umode.arglist.ct == 2 then
          451  +							lib.bail('you are attempting to completely purge the actor ', usr.ptr.xid, ' and all related content from the database! if you really want to do this, pass the confirmation string ', &cfmstr[0])
          452  +						elseif umode.arglist.ct == 3 then
          453  +							if lib.str.ncmp(&cfmstr[0],umode.arglist(2),64) ~= 0 then
          454  +								lib.bail('you have supplied an invalid confirmation string; if you really want to purge this actor, pass ', &cfmstr[0])
          455  +							end
          456  +							lib.warn('completely purging actor ', usr.ptr.xid, ' and all related content from database')
          457  +							dlg:actor_purge_uid(usr.ptr.id)
          458  +							lib.report('actor purged')
          459  +						else goto cmderr end
          460  +					else goto cmderr end
   399    461   				else goto cmderr end
   400    462   			elseif lib.str.cmp(mode.arglist(0),'user') == 0 then
   401    463   				var umode: pbasic umode:parse(mode.arglist.ct, &mode.arglist(0))
   402    464   				if umode.help then
   403    465   					[ lib.emit(false, 1, 'usage: ', `argv[0], ' user ', umode.type.helptxt.flags, ' <handle> <cmd> [<args>…]', umode.type.helptxt.opts, cmdhelp {
          466  +						{ 'user <handle> create', 'add a new user' };
   404    467   						{ 'user <handle> auth <type> new', '(where applicable, managed auth only) create a new authentication token of the given type for a user' };
   405    468   						{ 'user <handle> auth <type> reset', '(where applicable, managed auth only) delete all of a user\'s authentication tokens of the given type and issue a new one' };
   406    469   						{ 'user <handle> auth (<type>|all) purge', 'delete all credentials that would allow this user to log in (where possible)' };
   407    470   						{ 'user <handle> (grant|revoke) (<priv>|all)', 'grant or revoke a specific power to or from a user' };
   408         -						{ 'user <handle> emasculate', 'strip all administrative powers from a user' };
          471  +						{ 'user <handle> emasculate', 'strip all administrative powers and rank from a user' };
   409    472   						{ 'user <handle> forgive', 'restore all default powers to a user' };
   410    473   						{ 'user <handle> suspend [<timespec>]', '(e.g. \27[1muser jokester suspend 5d 6h 7m 3s\27[m to suspend "jokester" for five days, six hours, seven minutes, and three seconds) suspend a user'};
   411    474   					}) ]
   412    475   					return 1
   413    476   				end
   414         -				if umode.arglist.ct >= 3 then
          477  +				var handle = umode.arglist(0)
          478  +				var usr = dlg:actor_fetch_xid(pstr {ptr=handle, ct=lib.str.sz(handle)})
          479  +				if umode.arglist.ct == 2 and lib.str.cmp(umode.arglist(1),'create')==0 then
          480  +					if usr:ref() then lib.bail('that user already exists') end
          481  +					if not lib.store.actor.handle_validate(handle) then
          482  +						lib.bail('invalid user handle') end
          483  +					var kbuf: uint8[lib.crypt.const.maxdersz]
          484  +					var na = lib.store.actor.mk(&kbuf[0])
          485  +					na.handle = handle
          486  +					dlg:actor_create(&na)
          487  +					lib.report('created new user @',na.handle,'; assign credentials to enable login')
          488  +				elseif umode.arglist.ct >= 3 then
   415    489   					var grant = lib.str.cmp(umode.arglist(1),'grant') == 0
   416         -					var handle = umode.arglist(0)
   417         -					var usr = dlg:actor_fetch_xid(pstr {ptr=handle, ct=lib.str.sz(handle)})
          490  +					if not usr then lib.bail('no such user') end
   418    491   					if grant or lib.str.cmp(umode.arglist(1),'revoke') == 0 then
   419         -						if not usr then lib.bail('unknown handle') end
   420    492   						var newprivs = usr.ptr.rights.powers
   421    493   						var map = array([lib.store.privmap])
   422    494   						if umode.arglist.ct == 3 and lib.str.cmp(umode.arglist(2),'all') == 0 then
   423    495   							if grant
   424    496   								then newprivs:fill()
   425    497   								else newprivs:clear()
   426    498   							end

Modified parsav.t from [4b4b2876e4] to [2bbe093dad].

   394    394   	'http', 'html', 'session', 'tpl', 'store', 'acl';
   395    395   
   396    396   	'smackdown'; -- md-alike parser
   397    397   }
   398    398   
   399    399   local be = {}
   400    400   for _, b in pairs(config.backends) do
   401         -	be[#be+1] = terralib.loadfile('backend/' .. b .. '.t')()
          401  +	be[#be+1] = terralib.loadfile(string.format('backend/%s.t',b))()
   402    402   end
   403    403   lib.store.backends = global(`array([be]))
   404    404   
   405    405   lib.cmdparse = terralib.loadfile('cmdparse.t')()
   406    406   
   407    407   do local collate = function(path,f, ...)
   408    408   	return loadfile(path..'/'..f..'.lua')(path, ...)

Modified render/conf.t from [fa178f73b5] to [60d6b764a8].

    11     11   	{url = 'qnt', title = 'quarantine', render = 'quarantine'};
    12     12   	{url = 'acl', title = 'access control shortcuts', render = 'acl'};
    13     13   	{url = 'rooms', title = 'chatrooms', render = 'rooms'};
    14     14   	{url = 'circles', title = 'circles', render = 'circles'};
    15     15   
    16     16   	{url = 'srv', title = 'server settings', render = 'srv'};
    17     17   	{url = 'brand', title = 'instance branding', render = 'rebrand'};
           18  +	{url = 'badge', title = 'user badges', render = 'badge'};
           19  +	{url = 'emoji', title = 'custom emoji packs', render = 'emojo'};
    18     20   	{url = 'censor', title = 'censorship &amp; badthink suppression', render = 'rebrand'};
    19     21   	{url = 'users', title = 'user accounting', render = 'users'};
    20     22   
    21     23   }
    22     24   
    23     25   local path = symbol(lib.mem.ptr(pref))
    24     26   local co = symbol(&lib.srv.convo)
................................................................................
    41     43   
    42     44   local terra 
    43     45   render_conf([co], [path], notify: pstr)
    44     46   	var menu: lib.str.acc menu:init(64):lpush('<hr>') defer menu:free()
    45     47   
    46     48   	-- build menu
    47     49   	do var p = co.who.rights.powers
    48         -		if p.config() then menu:lpush '<a href="/conf/srv">server settings</a>' end
    49         -		if p.rebrand() then menu:lpush '<a href="/conf/brand">instance branding</a>' end
           50  +		if p:affect_users() then menu:lpush '<a href="/conf/users">users</a>' end
    50     51   		if p.censor() then menu:lpush '<a href="/conf/censor">badthink alerts</a>' end
    51         -		if p:affect_users() then menu:lpush '<a href="/conf/users">users</a>' end
           52  +		if p.config() then menu:lpush([
           53  +			'<a href="/conf/srv">server &amp; policy</a>' ..
           54  +			'<a href="/conf/badge">badges</a>' ..
           55  +			'<a href="/conf/emoji">emoji packs</a>'
           56  +		]) end
           57  +		if p.rebrand() then menu:lpush '<a href="/conf/brand">instance branding</a>' end
    52     58   	end
    53     59   
    54     60   	-- select the appropriate panel
    55     61   	var [panel] = pstr { ptr = ''; ct = 0 }
    56     62   	if path.ct >= 2 then [invoker] end
    57     63   
    58     64   	-- avoid the hr if we didn't add any elements

Modified render/conf/users.t from [6e4ba75dd2] to [4494d99300].

     1      1   -- vim: ft=terra
     2      2   local pstr = lib.mem.ptr(int8)
     3      3   local pref = lib.mem.ref(int8)
            4  +local P = lib.str.plit
     4      5   
     5      6   local terra cs(s: rawstring)
     6      7   	return pstr { ptr = s, ct = lib.str.sz(s) }
     7      8   end
     8      9   
     9     10   local terra 
           11  +regalia(acc: &lib.str.acc, rank: uint16)
           12  +	switch rank do -- TODO customizability
           13  +		case [uint16](1) then acc:lpush('👑') end
           14  +		case [uint16](2) then acc:lpush('🔱') end
           15  +		case [uint16](3) then acc:lpush('⚜️') end
           16  +		case [uint16](4) then acc:lpush('🗡') end
           17  +		case [uint16](5) then acc:lpush('🗝') end
           18  +		else acc:lpush('🕴')
           19  +	end
           20  +end
           21  +
           22  +local num_field = macro(function(acc,name,lbl,min,max,value)
           23  +	name = name:asvalue()
           24  +	lbl = lbl:asvalue()
           25  +	return quote
           26  +		var decbuf: int8[21]
           27  +	in acc:lpush([string.format('<div class="elem small"><label for="%s">%s</label><input type="number" id="%s" name="%s" min="', name, lbl, name, name)])
           28  +		:push(lib.math.decstr(min, &decbuf[20]),0)
           29  +		:lpush('" max="'):push(lib.math.decstr(max, &decbuf[20]),0)
           30  +		:lpush('" value="'):push(lib.math.decstr(value, &decbuf[20]),0):lpush('"></div>')
           31  +	end
           32  +end)
           33  +
           34  +local terra 
           35  +push_checkbox(acc: &lib.str.acc, name: pstr, lbl: pstr, on: bool, enabled: bool)
           36  +	acc:lpush('<label><input type="checkbox" name="'):ppush(name):lpush('"')
           37  +	if on then acc:lpush(' checked') end
           38  +	if not enabled then acc:lpush(' disabled') end
           39  +	acc:lpush('> '):ppush(lbl):lpush('</label>')
           40  +end
           41  +
           42  +local mode_local, mode_remote, mode_staff, mode_peers, mode_peons, mode_all = 0,1,2,3,4,5
           43  +local terra 
    10     44   render_conf_users(co: &lib.srv.convo, path: lib.mem.ptr(pref)): pstr
    11         -	if path.ct == 2 then
    12         -		var uid, ok = lib.math.shorthand.parse(path(1).ptr,path(1).ct)
           45  +	if path.ct == 3 then
           46  +		var uid, ok = lib.math.shorthand.parse(path(2).ptr,path(2).ct)
           47  +		if not ok then goto e404 end
    13     48   		var user = co.srv:actor_fetch_uid(uid)
           49  +		-- FIXME allow xids as well, for manual queries
    14     50   		if not user then goto e404 end
           51  +		defer user:free()
           52  +		if not co.who:overpowers(user.ptr) then goto e403 end
           53  +
    15     54   		var islinkct = false
    16         -		var cinp: lib.str.acc
           55  +		var cinp: lib.str.acc cinp:init(128)
    17     56   		var clnk: lib.str.acc clnk:compose('<hr>')
           57  +		cinp:lpush('<div class="elem-group">')
           58  +		if co.who.rights.powers.herald() then
           59  +			var sanitized: pstr
           60  +			if user.ptr.epithet == nil
           61  +				then sanitized = pstr {ptr='', ct=0}
           62  +				else sanitized = lib.html.sanitize(cs(user.ptr.epithet),true)
           63  +			end
           64  +			cinp:lpush('<div class="elem"><label for="epithet">epithet</label><input type="text" id="epithet" name="epithet" value="'):ppush(sanitized):lpush('"></div>')
           65  +			if user.ptr.epithet ~= nil then sanitized:free() end
           66  +		end
           67  +		if user.ptr.rights.rank > 0 and (co.who.rights.powers.elevate() or co.who.rights.powers.demote()) then
           68  +			var max = co.who.rights.rank
           69  +			if not co.who.rights.powers.elevate() then max = user.ptr.rights.rank end
           70  +			var min = co.srv.cfg.nranks
           71  +			if not co.who.rights.powers.demote() then min = user.ptr.rights.rank end
           72  +
           73  +			num_field(cinp, 'rank', 'rank', max, min, user.ptr.rights.rank)
           74  +		end
           75  +		if co.who.rights.powers.invite() or co.who.rights.powers.discipline() then
           76  +			var min = 0
           77  +			if not (co.who.rights.powers.discipline() or
           78  +				co.who.rights.powers.demote() and co.who.rights.powers.invite())
           79  +					then min = user.ptr.rights.invites end
           80  +			var max = co.srv.cfg.maxinvites
           81  +			if not co.who.rights.powers.invite() then max = user.ptr.rights.invites end
           82  +
           83  +			num_field(cinp, 'invites', 'invites', min, max, user.ptr.rights.invites)
           84  +		end
           85  +		cinp:lpush('</div><div class="check-panel">')
           86  +
           87  +		if (user.ptr.rights.rank == 0 and co.who.rights.powers.elevate()) or
           88  +		   (user.ptr.rights.rank >  0 and co.who.rights.powers.demote()) then
           89  +			push_checkbox(&cinp, 'staff', 'site staff member', user.ptr.rights.rank > 0, true)
           90  +		end
           91  +
           92  +		cinp:lpush('</div>')
           93  +
           94  +		if co.who.rights.powers.elevate() or
           95  +		   co.who.rights.powers.demote() then
           96  +			var map = array([lib.store.privmap])
           97  +			cinp:lpush('<label>powers</label><div class="check-panel">')
           98  +				for i=0, [map.type.N] do
           99  +					if (co.who.rights.powers and map[i].priv) == map[i].priv then
          100  +						var name: int8[64]
          101  +						var on = (user.ptr.rights.powers and map[i].priv) == map[i].priv
          102  +						var enabled = (on and co.who.rights.powers.demote()) or
          103  +									  ((not on) and co.who.rights.powers.elevate())
          104  +						lib.str.cpy(&name[0], 'allow-')
          105  +						lib.str.ncpy(&name[6], map[i].name.ptr, map[i].name.ct)
          106  +						push_checkbox(&cinp, pstr{ptr=&name[0],ct=map[i].name.ct+6},
          107  +							map[i].name, on, enabled)
          108  +					end
          109  +				end
          110  +			cinp:lpush('</div>')
          111  +		end
          112  +
          113  +		-- TODO black mark system? e.g. resolution option for badthink reports
          114  +		-- adds a black mark to the offending user; they can be automatically banned
          115  +		-- or brought up for review after a certain number of offenses; possibly lower
          116  +		-- set of default privs for marked users
    18    117   
    19    118   		var cinpp = cinp:finalize() defer cinpp:free()
    20    119   		var clnkp: pstr
    21    120   		if islinkct then clnkp = clnk:finalize() else
    22    121   			clnk:free()
    23    122   			clnkp = pstr { ptr='', ct=0 }
    24    123   		end
          124  +		var unym: lib.str.acc unym:init(64)
          125  +		unym:lpush('<a href="/')
          126  +		if user(0).origin ~= 0 then unym:lpush('@') end
          127  +		do var sanxid = lib.html.sanitize(user(0).xid, true)
          128  +			unym:ppush(sanxid)
          129  +			sanxid:free() end
          130  +		unym:lpush('" class="id">')
          131  +		lib.render.nym(user.ptr,0,&unym)
          132  +		unym:lpush('</a>')
    25    133   		var pg = data.view.conf_user_ctl {
    26         -			name = cs(user(0).handle);
          134  +			name = unym:finalize();
    27    135   			inputcontent = cinpp;
    28    136   			linkcontent = clnkp;
    29    137   		}
    30    138   		var ret = pg:tostr()
          139  +		pg.name:free()
    31    140   		if islinkct then clnkp:free() end
    32    141   		return ret
    33    142   	else
    34         -
          143  +		var modes = array(P'local', P'remote', P'staff', P'titled', P'peons', P'all')
          144  +		var idbuf: int8[lib.math.shorthand.maxlen]
          145  +		var ulst: lib.str.acc ulst:init(256)
          146  +		var mode: uint8 = mode_local
          147  +		var modestr = co:pgetv('show')
          148  +		ulst:lpush('<div style="text-align: right"><em>showing ')
          149  +		for i=0,[modes.type.N] do
          150  +			if modestr:ref() and modes[i]:cmp(modestr) then mode = i end
          151  +		end
          152  +		for i=0,[modes.type.N] do
          153  +			if i > 0 then ulst:lpush(' · ') end
          154  +			if mode == i then
          155  +				ulst:lpush('<strong>'):ppush(modes[i]):lpush('</strong>')
          156  +			else
          157  +				ulst:lpush('<a href="?show='):ppush(modes[i]):lpush('">')
          158  +					:ppush(modes[i]):lpush('</a>')
          159  +			end
          160  +		end
          161  +		var users: lib.mem.lstptr(lib.store.actor)
          162  +		if mode == mode_local then
          163  +			users = co.srv:actor_enum_local()
          164  +		else
          165  +			users = co.srv:actor_enum()
          166  +		end
          167  +		ulst:lpush('</em></div>')
          168  +		ulst:lpush('<ul class="user-list">')
          169  +		for i=0,users.ct do var usr = users(i).ptr
          170  +			if mode == mode_staff and usr.rights.rank == 0 then goto skip
          171  +			elseif mode == mode_peons and usr.rights.rank ~= 0 then goto skip
          172  +			elseif mode == mode_remote and usr.origin == 0 then goto skip 
          173  +			elseif mode == mode_peers and usr.epithet == nil then goto skip end
          174  +			var idlen = lib.math.shorthand.gen(usr.id, &idbuf[0])
          175  +			ulst:lpush('<li>')
          176  +			if usr.rights.rank ~= 0 then
          177  +				ulst:lpush('<span class="regalia">')
          178  +				regalia(&ulst, usr.rights.rank)
          179  +				ulst:lpush('</span>')
          180  +			end
          181  +			if co.who:overpowers(usr) then
          182  +				ulst:lpush('<a class="id" href="users/'):push(&idbuf[0],idlen):lpush('">')
          183  +				lib.render.nym(usr, 0, &ulst)
          184  +				ulst:lpush('</a></li>')
          185  +			else
          186  +				ulst:lpush('<span class="id">')
          187  +				lib.render.nym(usr, 0, &ulst)
          188  +				ulst:lpush('</span></li>')
          189  +			end
          190  +		::skip::end
          191  +		ulst:lpush('</ul>')
          192  +		return ulst:finalize()
    35    193   	end
    36    194   	do return pstr.null() end
    37         -	::e404:: co:complain(404, 'not found', 'there is no user or resource by that identifier on this server')
          195  +	::e404:: co:complain(404, 'not found', 'there is no user or resource by that identifier on this server') goto quit
          196  +	::e403:: co:complain(403, 'forbidden', 'you do not have sufficient authority to control that resource')
    38    197   
    39         -	do return pstr.null() end
          198  +	::quit:: return pstr.null()
    40    199   end
    41    200   
    42    201   return render_conf_users

Modified render/nym.t from [0d2437aadd] to [74775ce158].

     1      1   -- vim: ft=terra
     2         -local pstr = lib.mem.ptr(int8)
            2  +local pstr = lib.str.t
     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         -render_nym(who: &lib.store.actor, scope: uint64)
     9         -	var n: lib.str.acc n:init(128)
            8  +render_nym(who: &lib.store.actor, scope: uint64, tgt: &lib.str.acc)
            9  +	var acc: lib.str.acc
           10  +	var n: &lib.str.acc
           11  +	if tgt ~= nil then n = tgt else
           12  +		n = &acc
           13  +		n:init(128)
           14  +	end
    10     15   	var xidsan = lib.html.sanitize(cs(who.xid),false)
    11     16   	if who.nym ~= nil and who.nym[0] ~= 0 then
    12     17   		var nymsan = lib.html.sanitize(cs(who.nym),false)
    13         -		n:compose('<span class="nym">',nymsan,'</span> [<span class="handle">',
    14         -			xidsan,'</span>]')
           18  +		n:lpush('<span class="nym">'):ppush(nymsan)
           19  +			:lpush('</span> [<span class="handle">'):ppush(xidsan)
           20  +			:lpush('</span>]')
    15     21   		nymsan:free()
    16         -	else n:compose('<span class="handle">',xidsan,'</span>') end
           22  +	else n:lpush('<span class="handle">'):ppush(xidsan):lpush('</span>') end
    17     23   	xidsan:free()
    18     24   
    19     25   	if who.epithet ~= nil then
    20     26   		var episan = lib.html.sanitize(cs(who.epithet),false)
    21         -		n:lpush(' <span class="epithet">'):ppush(episan):lpush('</span>')
           27  +		n:lpush('<span class="epithet">'):ppush(episan):lpush('</span>')
    22     28   		episan:free()
    23     29   	end
    24     30   	
    25     31   	-- TODO: if scope == chat room then lookup titles in room member db
    26         -	return n:finalize()
           32  +	if tgt == nil then
           33  +		return n:finalize()
           34  +	else return pstr.null() end
    27     35   end
    28     36   
    29     37   return render_nym

Modified render/profile.t from [5ac1497f7a] to [5d5ed1c86e].

     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  +	var followed = false -- FIXME
    11     11   	if co.aid ~= 0 and co.who.id == actor.id then
    12         -		aux:compose('<a class="button" href="/conf/profile?go=/',actor.xid,'">alter</a>')
           12  +		aux:compose('<a class="button" href="/conf/profile?go=/@',actor.handle,'">alter</a>')
    13     13   	elseif co.aid ~= 0 then
    14     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>')
           15  +			aux:compose('<button method="post" class="pos" name="act" value="follow">follow</button>')
           16  +		elseif followed then
           17  +			aux:compose('<button method="post" class="neg" name="act" value="unfollow">unfollow</button>')
    18     18   		end
    19         -		aux:lpush('<a href="/'):push(actor.xid,0):lpush('/chat">chat</a>')
    20         -		if co.who.rights.powers:affect_users() then
           19  +		aux:lpush('<a class="button" href="/'):push(actor.xid,0):lpush('/chat">chat</a>')
           20  +		if co.who.rights.powers:affect_users() and co.who:overpowers(actor) then
    21     21   			aux:lpush('<a class="button" href="/'):push(actor.xid,0):lpush('/ctl">control</a>')
    22     22   		end
    23     23   	else
    24     24   		aux:compose('<a class="button" href="/', actor.xid, '/follow">remote follow</a>')
    25     25   	end
    26     26   	var auxp = aux:finalize()
    27         -	var avistr: lib.str.acc if actor.origin == 0 then
    28         -		avistr:compose('/avi/',actor.handle)
    29         -	end
    30     27   	var timestr: int8[26] lib.osclock.ctime_r(&actor.knownsince, &timestr[0])
    31     28   
    32     29   	var strfbuf: int8[28*4]
    33     30   	var stats = co.srv:actor_stats(actor.id)
    34     31   		var sn_posts     = cs(lib.math.decstr_friendly(stats.posts, &strfbuf[ [strfbuf.type.N - 1] ]))
    35     32   		var sn_follows   = cs(lib.math.decstr_friendly(stats.follows, sn_posts.ptr - 1))
    36     33   		var sn_followers = cs(lib.math.decstr_friendly(stats.followers, sn_follows.ptr - 1))
    37     34   		var sn_mutuals   = cs(lib.math.decstr_friendly(stats.mutuals, sn_followers.ptr - 1))
    38     35   	var bio = lib.str.plit "<em>tall, dark, and mysterious</em>"
    39     36   	if actor.bio ~= nil then
    40     37   		bio = lib.smackdown.html(cs(actor.bio))
    41     38   	end
    42         -	var fullname = lib.render.nym(actor,0) defer fullname:free()
           39  +	var fullname = lib.render.nym(actor,0,nil) defer fullname:free()
           40  +	var comments: lib.str.acc comments:init(64)
           41  +	-- this is really more what epithets are for, i think
           42  +	--if actor.rights.rank > 0 then comments:lpush('<li>staff member</li>') end
           43  +	if co.aid ~= 0 and actor.rights.rank ~= 0 then
           44  +		if co.who:outranks(actor) then
           45  +			comments:lpush('<li style="--co:50">underling</li>')
           46  +		elseif actor:outranks(co.who) then
           47  +			comments:lpush('<li style="--co:-50">outranks you</li>')
           48  +		end
           49  +	end
           50  +
    43     51   	var profile = data.view.profile {
    44     52   		nym = fullname;
    45     53   		bio = bio;
    46     54   		xid = cs(actor.xid);
    47         -		avatar = lib.trn(actor.origin == 0, pstr{ptr=avistr.buf,ct=avistr.sz},
    48         -			cs(lib.coalesce(actor.avatar, '/s/default-avatar.svg')));
           55  +		avatar = cs(actor.avatar);
    49     56   
    50     57   		nposts = sn_posts, nfollows = sn_follows;
    51     58   		nfollowers = sn_followers, nmutuals = sn_mutuals;
    52     59   		tweetday = cs(timestr);
    53     60   		timephrase = lib.trn(actor.origin == 0, lib.str.plit'joined', lib.str.plit'known since');
           61  +
           62  +		remarks = '';
    54     63   
    55     64   		auxbtn = auxp;
    56     65   	}
           66  +	if comments.sz > 0 then profile.remarks = comments:finalize() end
    57     67   
    58     68   	var ret = profile:tostr()
    59         -	if actor.origin == 0 then avistr:free() end
    60     69   	auxp:free() 
    61     70   	if actor.bio ~= nil then bio:free() end
           71  +	if comments.sz > 0 then profile.remarks:free() end
    62     72   	return ret
    63     73   end
    64     74   
    65     75   return render_profile

Modified render/tweet.t from [2b64155fcc] to [43aca48007].

    22     22   	var timestr: int8[26] lib.osclock.ctime_r(&p.posted, &timestr[0])
    23     23   
    24     24   	var bhtml = lib.smackdown.html([lib.mem.ptr(int8)] {ptr=p.body,ct=0}) defer bhtml:free()
    25     25   
    26     26   	var idbuf: int8[lib.math.shorthand.maxlen]
    27     27   	var idlen = lib.math.shorthand.gen(p.id, idbuf)
    28     28   	var permalink: lib.str.acc permalink:compose('/post/',{idbuf,idlen})
    29         -	var fullname = lib.render.nym(author,0) defer fullname:free()
           29  +	var fullname = lib.render.nym(author,0,nil) defer fullname:free()
    30     30   	var tpl = data.view.tweet {
    31     31   		text = bhtml;
    32     32   		subject = cs(lib.coalesce(p.subject,''));
    33     33   		nym = fullname;
    34     34   		when = cs(&timestr[0]);
    35         -		avatar = cs(lib.trn(author.origin == 0, avistr.buf,
    36         -			lib.coalesce(author.avatar, '/s/default-avatar.svg')));
           35  +		avatar = cs(author.avatar);
    37     36   		acctlink = cs(author.xid);
    38     37   		permalink = permalink:finalize();
    39     38   		attr = ''
    40     39   	}
    41     40   
    42     41   	var attrbuf: int8[32]
    43     42   	if p.accent ~= -1 and p.accent ~= co.ui_hue then

Modified route.t from [35b3cb0b8a] to [2f7668c3df].

   245    245   
   246    246   	::badurl:: do co:complain(404, 'invalid URL', 'this URL does not reference extant content or functionality') return end
   247    247   	::badop :: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
   248    248   end
   249    249   
   250    250   terra http.configure(co: &lib.srv.convo, path: hpath, meth: method.t)
   251    251   	var msg = pstring.null()
          252  +	-- first things first, do priv checks
          253  +	if path.ct >= 1 then
          254  +		if not co.who.rights.powers.config() and (
          255  +			path(1):cmp(lib.str.lit 'srv')   or
          256  +			path(1):cmp(lib.str.lit 'badge') or
          257  +			path(1):cmp(lib.str.lit 'emoji')
          258  +		) then goto nopriv
          259  +
          260  +		elseif not co.who.rights.powers.rebrand() and (
          261  +			path(1):cmp(lib.str.lit 'brand')
          262  +		) then goto nopriv
          263  +
          264  +		elseif not co.who.rights.powers.acct() and (
          265  +			path(1):cmp(lib.str.lit 'profile') or
          266  +			path(1):cmp(lib.str.lit 'acct')
          267  +		) then goto nopriv
          268  +
          269  +		elseif not co.who.rights.powers:affect_users() and (
          270  +			path(1):cmp(lib.str.lit 'users')
          271  +		) then goto nopriv end
          272  +	end
          273  +
   252    274   	if meth == method.post and path.ct >= 1 then
   253    275   		var user_refresh = false var fail = false
   254    276   		if path(1):cmp(lib.str.lit 'profile') then
   255    277   			lib.dbg('updating profile')
   256    278   			co.who.bio = co:postv('bio')._0
   257    279   			co.who.nym = co:postv('nym')._0
   258    280   			if co.who.bio ~= nil and @co.who.bio == 0 then co.who.bio = nil end
................................................................................
   279    301   			if resethue then
   280    302   				co.srv:actor_conf_int_reset(co.who.id, 'ui-accent')
   281    303   				co.ui_hue = co.srv.cfg.ui_hue
   282    304   			end
   283    305   
   284    306   			msg = lib.str.plit 'profile changes saved'
   285    307   			--user_refresh = true -- not really necessary here, actually
   286         -		elseif path(1):cmp(lib.str.lit 'srv') then
   287         -			if not co.who.rights.powers.config() then goto nopriv end
   288         -		elseif path(1):cmp(lib.str.lit 'brand') then
   289         -			if not co.who.rights.powers.rebrand() then goto nopriv end
   290         -		elseif path(1):cmp(lib.str.lit 'users') then
   291         -			if not co.who.rights.powers:affect_users() then goto nopriv end
   292    308   
   293    309   		elseif path(1):cmp(lib.str.lit 'sec') then
   294    310   			var act = co:ppostv('act')
   295    311   			if act:cmp(lib.str.plit 'invalidate') then
   296    312   				lib.dbg('setting user\'s cookie validation time to now')
   297    313   				co.who.source:auth_sigtime_user_alter(co.who.id, lib.osclock.time(nil))
   298    314   				-- 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
   299    315   				co:installkey('/conf/sec',co.aid)
   300    316   				return
          317  +			end
          318  +		elseif path(1):cmp(lib.str.lit 'users') and path.ct >= 2 then
          319  +			var userid, ok = lib.math.shorthand.parse(path(2).ptr, path(2).ct)
          320  +			if ok then
          321  +				var usr = co.srv:actor_fetch_uid(userid) defer usr:free()
          322  +				if not co.who:overpowers(usr.ptr) then goto nopriv end
   301    323   			end
   302    324   		end
   303    325   
   304    326   		if user_refresh then -- refresh the user info for the renderer
   305    327   			var usr = co.srv:actor_fetch_uid(co.who.id)
   306    328   			lib.mem.heapf(co.who)
   307    329   			co.who = usr.ptr

Modified srv.t from [b092edff32] to [6be667433b].

     8      8   	pol_sec: secmode.t
     9      9   	pol_reg: bool
    10     10   	credmgd: bool
    11     11   	maxupsz: intptr
    12     12   	instance: lib.mem.ptr(int8)
    13     13   	overlord: &srv
    14     14   	ui_hue: uint16
           15  +	nranks: uint16
           16  +	maxinvites: uint16
    15     17   }
    16     18   local struct srv {
    17     19   	sources: lib.mem.ptr(lib.store.source)
    18     20   	webmgr: lib.net.mg_mgr
    19     21   	webcon: &lib.net.mg_connection
    20     22   	cfg: cfgcache
    21     23   	id: rawstring
................................................................................
   644    646   		   self.sources(i).backend.actor_auth_pw ~= nil then
   645    647   			var aid,uid,newhnd = self.sources(i):actor_auth_pw(ip,user,pw)
   646    648   			if aid ~= 0 then
   647    649   				if uid == 0 then
   648    650   					lib.dbg('new user just logged in, creating account entry')
   649    651   					var kbuf: uint8[lib.crypt.const.maxdersz]
   650    652   					var na = lib.store.actor.mk(&kbuf[0])
          653  +					na.handle = newhnd.ptr
   651    654   					var newuid: uint64
   652    655   					if self.sources(i).backend.actor_create ~= nil then
   653    656   						newuid = self.sources(i):actor_create(&na)
   654    657   					else newuid = self:actor_create(&na) end
   655    658   
   656    659   					if self.sources(i).backend.actor_auth_register_uid ~= nil then
   657    660   						self.sources(i):actor_auth_register_uid(aid,newuid)
................................................................................
   723    726   	lib.net.mg_mgr_free(&self.webmgr)
   724    727   	for i=0,self.sources.ct do var src = self.sources.ptr + i
   725    728   		lib.report('closing data source ', src.id.ptr, '(', src.backend.id, ')')
   726    729   		src:close()
   727    730   	end
   728    731   	self.sources:free()
   729    732   end
          733  +
          734  +terra cfgcache:cfint(name: rawstring, default: intptr)
          735  +	var str = self.overlord:conf_get(name)
          736  +	if str.ptr ~= nil then
          737  +		var i,ok = lib.math.decparse(str)
          738  +		if ok then default = i else
          739  +			lib.warn('invalid configuration setting ',name,'="',{str.ptr,str.ct},'", expected integer; using default value instead')
          740  +		end
          741  +		str:free()
          742  +	end
          743  +	return default
          744  +end
          745  +
          746  +terra cfgcache:cfbool(name: rawstring, default: bool)
          747  +	var str = self.overlord:conf_get(name)
          748  +	if str.ptr ~= nil then
          749  +		if str:cmp(lib.str.plit 'true') or str:cmp(lib.str.plit 'on') or
          750  +		   str:cmp(lib.str.plit 'yes')  or str:cmp(lib.str.plit '1') then
          751  +			default = true
          752  +		elseif str:cmp(lib.str.plit 'false') or str:cmp(lib.str.plit 'off') or
          753  +		       str:cmp(lib.str.plit 'no')    or str:cmp(lib.str.plit '0') then
          754  +			default = false
          755  +		else
          756  +			lib.warn('invalid configuration setting ',name,'="',{str.ptr,str.ct},'", expected boolean; using default value instead')
          757  +		end
          758  +		str:free()
          759  +	end
          760  +	return default
          761  +end
   730    762   
   731    763   terra cfgcache:load()
   732    764   	self.instance = self.overlord:conf_get('instance-name')
   733    765   	self.secret = self.overlord:conf_get('server-secret')
   734    766   
   735         -	do self.pol_reg = false
   736         -	var sreg = self.overlord:conf_get('policy-self-register')
   737         -	if sreg:ref() then
   738         -		if lib.str.cmp(sreg.ptr, 'on') == 0
   739         -			then self.pol_reg = true
   740         -			else self.pol_reg = false
   741         -		end
   742         -		sreg:free()
   743         -	end end
          767  +	self.pol_reg = self:cfbool('policy-self-register', false)
   744    768   
   745    769   	do self.credmgd = false
   746    770   	var sreg = self.overlord:conf_get('credential-store')
   747    771   	if sreg:ref() then
   748    772   		if lib.str.cmp(sreg.ptr, 'managed') == 0
   749    773   			then self.credmgd = true
   750    774   			else self.credmgd = false
................................................................................
   773    797   			self.pol_sec = secmode.lockdown
   774    798   		elseif lib.str.cmp(smode.ptr, 'isolate') == 0 then
   775    799   			self.pol_sec = secmode.isolate
   776    800   		end
   777    801   		smode:free()
   778    802   	end
   779    803   
   780         -	self.ui_hue = config.default_ui_accent
   781         -	var shue = self.overlord:conf_get('ui-accent')
   782         -	if shue.ptr ~= nil then
   783         -		var hue,ok = lib.math.decparse(shue)
   784         -		if ok then self.ui_hue = hue end
   785         -		shue:free()
   786         -	end
          804  +	self.ui_hue = self:cfint('ui-accent',config.default_ui_accent)
          805  +	self.nranks = self:cfint('user-ranks',10)
          806  +	self.maxinvites = self:cfint('max-invites',64)
   787    807   end
   788    808   
   789    809   return {
   790    810   	overlord = srv;
   791    811   	convo = convo;
   792    812   	route = route;
   793    813   	secmode = secmode;
   794    814   }

Modified static/style.scss from [a256539ae3] to [f40a20016f].

     1      1   $default-color: hsl(323,100%,65%);
     2      2   %sans { font-family: "Alegreya Sans", "Open Sans", sans-serif; }
     3      3   %serif { font-family: Alegreya, GaramondNo8, "Garamond Premier Pro", "Adobe Garamond", Garamond, Junicode, serif; }
     4      4   %teletype { font-family: "Inconsolata LGC", Inconsolata, monospace; font-size: 12pt !important; }
     5      5   
     6      6   // @function tone($pct, $alpha: 0) { @return adjust-color($color, $lightness: $pct, $alpha: $alpha) }
     7         -@function tone($pct, $alpha: 0) {
     8         - @return hsla(var(--hue), 100%, 65% + $pct, 1 + $alpha)
     9         -}
            7  +@function tone($pct, $alpha: 0)         { @return hsla(var(--hue),                   100%, 65% + $pct, 1 + $alpha) }
            8  +@function vtone($pct, $vary, $alpha: 0) { @return hsla(calc(var(--hue) + $vary),     100%, 65% + $pct, 1 + $alpha) }
            9  +@function otone($pct, $alpha: 0)        { @return hsla(calc(var(--hue) + var(--co)), 100%, 65% + $pct, 1 + $alpha) }
    10     10   
    11         -:root { --hue: 323; }
           11  +:root { --hue: 323; --co: 0; }
    12     12   body {
    13     13   	@extend %sans;
    14     14   	background-color: tone(-55%);
    15     15   	color: tone(25%);
    16     16   	font-size: 14pt;
    17     17   	margin: 0;
    18     18   	padding: 0;
................................................................................
    24     24   ::placeholder {
    25     25   	color: tone(0,-0.3);
    26     26   	font-style: italic;
    27     27   }
    28     28   a[href] {
    29     29   	color: tone(10%);
    30     30   	text-decoration-color: tone(10%,-0.5);
           31  +	text-decoration-skip-ink: all;
           32  +	text-decoration-thickness: 1px;
           33  +	text-underline-offset: 0.1em;
    31     34   	&:hover, &:focus {
    32     35   		color: white;
    33     36   		text-shadow: 0 0 15px tone(20%);
    34     37   		text-decoration-color: tone(10%,-0.1);
    35     38   		outline: none;
    36     39   	}
    37     40   	&.button { @extend %button; }
................................................................................
    68     71   
    69     72   %button {
    70     73   	@extend %sans;
    71     74   	font-size: 14pt;
    72     75   	box-sizing: border-box;
    73     76   	padding: 0.1in 0.2in;
    74     77   	border: 1px solid black;
    75         -	color: tone(25%);
           78  +	color: otone(25%);
    76     79   	text-shadow: 1px 1px black;
    77     80   	text-decoration: none;
    78     81   	text-align: center;
    79     82   	cursor: default;
    80     83   	user-select: none;
    81     84   	-webkit-user-drag: none;
    82     85   	-webkit-app-region: no-drag;
    83     86   	background: linear-gradient(to bottom,
    84         -		tone(-47%),
    85         -		tone(-50%) 15%,
    86         -		tone(-50%) 75%,
    87         -		tone(-53%)
           87  +		otone(-47%),
           88  +		otone(-50%) 15%,
           89  +		otone(-50%) 75%,
           90  +		otone(-53%)
    88     91   	);
    89     92   	&:hover, &:focus {
    90     93   		@extend %glow;
    91     94   		outline: none;
    92     95   		color: tone(-55%);
    93     96   		text-shadow: none;
    94     97   		background: linear-gradient(to bottom,
    95         -			tone(-27%),
    96         -			tone(-30%) 15%,
    97         -			tone(-30%) 75%,
    98         -			tone(-35%)
           98  +			otone(-27%),
           99  +			otone(-30%) 15%,
          100  +			otone(-30%) 75%,
          101  +			otone(-35%)
    99    102   		);
   100    103   	}
   101    104   	&:active {
   102    105   		color: black;
   103    106   		padding-bottom: calc(0.1in - 2px);
   104    107   		padding-top: calc(0.1in + 2px);
   105    108   		background: linear-gradient(to top,
   106         -			tone(-25%),
   107         -			tone(-30%) 15%,
   108         -			tone(-30%) 75%,
   109         -			tone(-35%)
          109  +			otone(-25%),
          110  +			otone(-30%) 15%,
          111  +			otone(-30%) 75%,
          112  +			otone(-35%)
   110    113   		);
   111    114   	}
   112    115   }
   113    116   
   114    117   button { @extend %button;
   115    118   	&:first-of-type {
   116    119   		@extend %button;
   117    120   		color: white;
   118         -		box-shadow: inset 0 1px  tone(-25%),
   119         -		            inset 0 -1px tone(-50%);
          121  +		box-shadow: inset 0 1px  otone(-25%),
          122  +		            inset 0 -1px otone(-50%);
   120    123   		background: linear-gradient(to bottom,
   121         -			tone(-35%),
   122         -			tone(-40%) 15%,
   123         -			tone(-40%) 75%,
   124         -			tone(-45%)
          124  +			otone(-35%),
          125  +			otone(-40%) 15%,
          126  +			otone(-40%) 75%,
          127  +			otone(-45%)
   125    128   		);
   126    129   		&:hover, &:focus {
   127         -			box-shadow: inset 0 1px  tone(-15%),
   128         -						inset 0 -1px tone(-40%);
          130  +			box-shadow: inset 0 1px  otone(-15%),
          131  +						inset 0 -1px otone(-40%);
   129    132   		}
   130    133   		&:active {
   131         -			box-shadow: inset 0 1px  tone(-50%),
   132         -						inset 0 -1px tone(-25%);
          134  +			box-shadow: inset 0 1px  otone(-50%),
          135  +						inset 0 -1px otone(-25%);
   133    136   			background: linear-gradient(to top,
   134         -				tone(-30%),
   135         -				tone(-35%) 15%,
   136         -				tone(-35%) 75%,
   137         -				tone(-40%)
          137  +				otone(-30%),
          138  +				otone(-35%) 15%,
          139  +				otone(-35%) 75%,
          140  +				otone(-40%)
   138    141   			);
   139    142   		}
   140    143   	}
   141         -	&:hover { font-weight: bold; }
          144  +	//&:hover { font-weight: bold; }
   142    145   }
   143    146   
   144    147   $grad-ui-focus: linear-gradient(to bottom,
   145    148   	tone(-50%),
   146    149   	tone(-35%)
   147    150   );
   148    151   
   149         -input[type='text'], input[type='password'], textarea, select {
          152  +input[type='text'], input[type='number'], input[type='password'], textarea, select {
   150    153   	@extend %serif;
   151    154   	padding: 0.08in 0.1in;
   152    155   	box-sizing: border-box;
   153    156   	border: 1px solid black;
   154    157   	background: linear-gradient(to bottom, tone(-55%), tone(-40%));
   155    158   	font-size: 16pt;
   156    159   	color: tone(25%);
................................................................................
   236    239   	padding-bottom: 0.1in;
   237    240   	background-color: tone(-45%,-0.3);
   238    241   	border: {
   239    242   		left: 1px solid black;
   240    243   		right: 1px solid black;
   241    244   	}
   242    245   }
          246  +
          247  +.id {
          248  +	color: tone(25%,-0.4);
          249  +	> .nym {
          250  +		font-weight: bold;
          251  +		color: tone(25%);
          252  +	}
          253  +	> .xid {
          254  +		color: tone(20%,-0.1);
          255  +		font-size: 80%;
          256  +		vertical-align: text-top;
          257  +	}
          258  +}
   243    259   
   244    260   div.profile {
   245    261   	padding: 0.1in;
   246    262   	position: relative;
   247    263   	display: grid;
   248    264   	margin-bottom: 0.4in;
   249    265   	grid-template-columns: 2fr 1fr;
................................................................................
   261    277   			grid-column: 1 / 2;
   262    278   			grid-row: 1 / 3;
   263    279   			border: 1px solid black;
   264    280   		}
   265    281   		> .id {
   266    282   			grid-column: 2 / 3;
   267    283   			grid-row: 1 / 2;
   268         -			color: tone(25%,-0.4);
   269         -			> .nym {
   270         -				font-weight: bold;
   271         -				color: tone(25%);
   272         -			}
   273         -			> .xid {
   274         -				color: tone(20%,-0.1);
   275         -				font-size: 80%;
   276         -				vertical-align: text-top;
   277         -			}
   278    284   		}
   279    285   		> .bio {
   280    286   			grid-column: 2 / 3;
   281    287   			grid-row: 2 / 3;
   282    288   		}
   283    289   	}
   284    290   	> .stats {
   285    291   		grid-column: 3 / 4;
   286    292   		grid-row: 1 / 3;
          293  +		display: flex;
          294  +		flex-flow: column;
          295  +		> * { flex-grow: 1; }
          296  +		table { td, th { text-align: center; } }
   287    297   	}
   288    298   	> form.actions {
   289    299   		grid-column: 1 / 3; grid-row: 2 / 3;
   290    300   		padding-top: 0.075in;
   291    301   		flex-wrap: wrap;
   292    302   		display: flex;
   293    303   		justify-content: center;
................................................................................
   606    616   		}
   607    617   		input, textarea, .txtbox {
   608    618   			display: block;
   609    619   			width: 100%;
   610    620   		}
   611    621   		textarea { resize: vertical; min-height: 2in; }
   612    622   	}
   613         -	.elem + %button { margin-left: 50%; width: 50%; }
          623  +	:is(.elem,.elem-group) + %button { margin-left: 50%; width: 50%; }
          624  +	.elem-group {
          625  +		display: flex;
          626  +		flex-flow: row;
          627  +		> .elem {
          628  +			flex-shrink: 1;
          629  +			flex-grow: 1;
          630  +			margin-left: 0.1in;
          631  +			&:first-child { margin-left: 0; }
          632  +		}
          633  +		> .small { flex-shrink: 5; }
          634  +	}
   614    635   }
   615    636   
   616    637   menu.choice {
   617    638   	display: flex;
   618    639   	&.horizontal {
   619    640   		flex-flow: row-reverse wrap;
   620    641   		justify-content: space-evenly;
................................................................................
   713    734   
   714    735   .color-picker {
   715    736   	/* implemented using javascript, alas */
   716    737   	@extend %box;
   717    738   	label { text-shadow: 1px 1px black; }
   718    739   	padding: 0.1in;
   719    740   }
          741  +
          742  +ul.user-list {
          743  +	list-style-type: none;
          744  +	margin: 0.5em 0;
          745  +	padding: 0;
          746  +	box-shadow: 0 0 10px -3px black inset;
          747  +	border: 1px solid tone(-50%);
          748  +	li {
          749  +		background-color: tone(-20%, -0.8);
          750  +		padding: 0.5em;
          751  +		.regalia { margin-right: 0.3em; vertical-align: bottom; }
          752  +		&:nth-child(odd) {
          753  +			background-color: tone(-30%, -0.8);
          754  +		}
          755  +	}
          756  +}
          757  +
          758  +ul.remarks {
          759  +	margin: 0; padding: 0;
          760  +	list-style-type: none;
          761  +	li {
          762  +		border-top: 1px solid otone(-22%);
          763  +		border-bottom: 2px solid otone(-55%);
          764  +		border-radius: 3px;
          765  +		background: otone(-25%,-0.4);
          766  +		color: otone(25%);
          767  +		text-align: center;
          768  +		padding: 0.3em 0;
          769  +		margin: 0.2em 0.1em;
          770  +		cursor: default;
          771  +	}
          772  +}
          773  +
          774  +:is(%button, a[href]).neg { --co:  60 }
          775  +:is(%button, a[href]).pos { --co: -30 }

Modified store.t from [e7b33e5534] to [da1c9184b0].

    22     22   		'pw', 'otp', 'challenge', 'trust'
    23     23   	};
    24     24   	privset = lib.set {
    25     25   		'post', 'edit', 'acct', 'upload', 'censor', 'admin', 'invite'
    26     26   	};
    27     27   	powerset = lib.set {
    28     28   		-- user powers -- default on
    29         -		'login', 'visible', 'post', 'shout',
    30         -		'propagate', 'upload', 'acct', 'edit';
           29  +		'login', -- not locked out
           30  +		'visible', -- account & posts can be seen by others
           31  +		'post', -- can do poasts
           32  +		'shout', -- posts show up on local timeline
           33  +		'propagate', -- posts are sent to other instances
           34  +		'artifact', -- upload, claim, and manage artifacts
           35  +		'acct', -- configure own account
           36  +		'edit'; -- edit own poasts
    31     37   
    32     38   		-- admin powers -- default off
    33         -		'purge', 'config', 'censor', 'suspend',
    34         -		'cred', 'elevate', 'demote', 'rebrand', -- modify site's brand identity
    35         -		'herald', -- grant serverwide epithets
           39  +		'purge', -- permanently delete users
           40  +		'config', -- change daemon policy & config UI
           41  +		'censor', -- dispose of badthink
           42  +		'discipline', -- enforced timeouts, stripping badges and epithets, punitive actions that do not permanently deprive of powers; can remove own injunctions but not others'
           43  +		'vacate', -- can remove others' injunctions, but not apply them
           44  +		'cred', -- alter credentials
           45  +		'elevate', 'demote', -- change user rank, give and take powers, including the ability to log in
           46  +		'rebrand', -- modify site's brand identity
           47  +		'herald', -- grant serverwide epithets and badges
    36     48   		'invite' -- *unlimited* invites
    37     49   	};
    38     50   	prepmode = lib.enum {
    39     51   		'full','conf','admin'
    40     52   	}
    41     53   }
    42     54   
................................................................................
    46     58   	m.privmap[#m.privmap + 1] = quote
    47     59   		var ps: m.powerset ps:clear()
    48     60   		(ps.[v] << true)
    49     61   	in pt {name = lib.str.plit(v), priv = ps} end
    50     62   end end
    51     63   
    52     64   terra m.powerset:affect_users()
    53         -	return self.purge() or self.censor() or self.suspend() or
           65  +	return self.purge() or self.discipline() or self.herald() or
    54     66   	       self.elevate() or self.demote() or self.cred()
    55     67   end
    56     68   
    57     69   local str = rawstring
    58     70   local pstr = lib.mem.ptr(int8)
    59     71   
    60     72   struct m.source
................................................................................
    64     76   	-- creating staff automatically assigns rank immediately below you
    65     77   	quota: uint32 -- # of allowed tweets per day; 0 = no limit
    66     78   	invites: uint32 -- # of people left this user can invite
    67     79   	
    68     80   	powers: m.powerset
    69     81   }
    70     82   
    71         -terra m.rights_default()
           83  +terra m.rights_default() -- TODO make configurable
    72     84   	var pow: m.powerset pow:clear()
    73     85   	(pow.login     << true)
    74     86   	(pow.visible   << true)
    75     87   	(pow.post      << true)
    76     88   	(pow.shout     << true)
    77     89   	(pow.propagate << true)
    78         -	(pow.upload    << true)
           90  +	(pow.artifact  << true)
    79     91   	(pow.acct      << true)
    80     92   	(pow.edit      << true)
    81     93   	return m.rights { rank = 0, quota = 1000, invites = 0, powers = pow; }
    82     94   end
    83     95   
    84     96   struct m.actor {
    85     97   	id: uint64
................................................................................
    94    106   	rights: m.rights
    95    107   	key: lib.mem.ptr(uint8)
    96    108   
    97    109   -- ephemera
    98    110   	xid: str
    99    111   	source: &m.source
   100    112   }
          113  +
          114  +terra m.actor:outranks(other: &m.actor)
          115  + -- this predicate determines where two users stand relative to
          116  + -- each other in the formal staff hierarchy. it is used in
          117  + -- authority calculations, but this function should only be
          118  + -- used directly in rendering code and by other predicates.
          119  + -- do not use it in authority calculation, as there are special
          120  + -- cases where formal rank does not fully determine a user's
          121  + -- capabilities (e.g. roots have the same rank, but can
          122  + -- exercise power over each other, unlike lower ranks)
          123  +	if self.rights.rank == 0 then
          124  +	 -- peons never outrank anybody
          125  +		return false
          126  +	end
          127  +	if other.rights.rank == 0 then
          128  +	 -- everybody outranks peons
          129  +		return true
          130  +	end
          131  +	return self.rights.rank < other.rights.rank
          132  +	-- rank 1 is the highest possible, rank 2 is second-highest, and so on
          133  +end
          134  +
          135  +terra m.actor:overpowers(other: &m.actor)
          136  + -- this predicate determines whether one user may exercise their
          137  + -- powers over another user. it does not affect what those powers
          138  + -- actually are (for instance, you cannot revoke a power you do
          139  + -- not have, no matter how much you outrank someone)
          140  +	if self.rights.rank == 1 and other.rights.rank == 1 then
          141  +	 -- special case: root users always overpower each other
          142  +	 -- otherwise, nobody could reset their passwords
          143  +	 -- (also dissuades people from giving root lightly)
          144  +		return true
          145  +	end
          146  +	return self:outranks(other)
          147  +end
          148  +
          149  +terra m.actor.methods.handle_validate(hnd: rawstring)
          150  +	if hnd[0] == 0 then
          151  +		return false
          152  +	end
          153  +	-- TODO validate fully
          154  +	return true
          155  +end
   101    156   
   102    157   terra m.actor.methods.mk(kbuf: &uint8)
   103    158   	var newkp = lib.crypt.genkp()
   104    159   	var privsz = lib.crypt.der(false,&newkp,kbuf)
   105    160   	return m.actor {
   106    161   		id = 0; nym = nil; handle = nil;
   107    162   		origin = 0; bio = nil; avatar = nil;
................................................................................
   280    335   	conf_get: {&m.source, rawstring} -> lib.mem.ptr(int8)
   281    336   	conf_set: {&m.source, rawstring, rawstring} -> {}
   282    337   	conf_reset: {&m.source, rawstring} -> {}
   283    338   
   284    339   	actor_create: {&m.source, &m.actor} -> uint64
   285    340   	actor_save: {&m.source, &m.actor} -> {}
   286    341   	actor_save_privs: {&m.source, &m.actor} -> {}
          342  +	actor_purge_uid: {&m.source, uint64} -> {}
   287    343   	actor_fetch_xid: {&m.source, lib.mem.ptr(int8)} -> lib.mem.ptr(m.actor)
   288    344   	actor_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.actor)
   289    345   	actor_notif_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.notif)
   290         -	actor_enum: {&m.source} -> lib.mem.ptr(&m.actor)
   291         -	actor_enum_local: {&m.source} -> lib.mem.ptr(&m.actor)
          346  +	actor_enum: {&m.source} -> lib.mem.lstptr(m.actor)
          347  +	actor_enum_local: {&m.source} -> lib.mem.lstptr(m.actor)
   292    348   	actor_stats: {&m.source, uint64} -> m.actor_stats
   293    349   	actor_rel: {&m.source, uint64, uint64} -> m.relationship
   294    350   
   295    351   	actor_auth_how: {&m.source, m.inet, rawstring} -> {m.credset, bool}
   296    352   		-- returns a set of auth method categories that are available for a
   297    353   		-- given user from a certain origin
   298    354   			-- origin: inet
................................................................................
   326    382   			-- cookie issue time: m.timepoint
   327    383   	actor_auth_register_uid: {&m.source, uint64, uint64} -> {}
   328    384   		-- notifies the backend module of the UID that has been assigned for
   329    385   		-- an authentication ID
   330    386   			-- aid: uint64
   331    387   			-- uid: uint64
   332    388   
   333         -	auth_enum_uid:    {&m.source, uint64}    -> lib.mem.ptr(lib.mem.ptr(m.auth))
   334         -	auth_enum_handle: {&m.source, rawstring} -> lib.mem.ptr(lib.mem.ptr(m.auth))
          389  +	auth_enum_uid:    {&m.source, uint64}    -> lib.mem.lstptr(m.auth)
          390  +	auth_enum_handle: {&m.source, rawstring} -> lib.mem.lstptr(m.auth)
   335    391   	auth_attach_pw: {&m.source, uint64, bool, pstr, pstr} -> {}
   336    392   		-- uid: uint64
   337    393   		-- reset: bool (delete other passwords?)
   338    394   		-- pw: pstring
   339    395   		-- comment: pstring
   340    396   	auth_purge_pw: {&m.source, uint64, rawstring} -> {}
   341    397   	auth_purge_otp: {&m.source, uint64, rawstring} -> {}
................................................................................
   350    406   			-- uid: uint64
   351    407   			-- timestamp: timepoint
   352    408   
   353    409   	post_save: {&m.source, &m.post} -> {}
   354    410   	post_create: {&m.source, &m.post} -> uint64
   355    411   	post_destroy: {&m.source, uint64} -> {}
   356    412   	post_fetch: {&m.source, uint64} -> lib.mem.ptr(m.post)
   357         -	post_enum_author_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post))
   358         -	post_enum_parent: {&m.source, uint64} -> lib.mem.ptr(lib.mem.ptr(m.post))
          413  +	post_enum_author_uid: {&m.source, uint64, m.range} -> lib.mem.lstptr(m.post)
          414  +	post_enum_parent: {&m.source, uint64} -> lib.mem.lstptr(m.post)
   359    415   	post_attach_ctl: {&m.source, uint64, uint64, bool} -> {}
   360    416   		-- attaches or detaches an existing database artifact
   361    417   			-- post id: uint64
   362    418   			-- artifact id: uint64
   363    419   			-- detach: bool
   364    420   
   365    421   	thread_latest_arrival_calc: {&m.source, uint64} -> m.timepoint
................................................................................
   402    458   			-- proto: kompromat (null for all records, or a prototype describing the records to return)
   403    459   	nkvd_sanction_issue:  {&m.source, &m.sanction} -> uint64
   404    460   	nkvd_sanction_vacate: {&m.source, uint64} -> {}
   405    461   	nkvd_sanction_enum_target: {&m.source, uint64} -> {}
   406    462   	nkvd_sanction_enum_issuer: {&m.source, uint64} -> {}
   407    463   	nkvd_sanction_review: {&m.source, m.timepoint} -> {}
   408    464   
   409         -	timeline_actor_fetch_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post))
   410         -	timeline_instance_fetch: {&m.source, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post))
          465  +	timeline_actor_fetch_uid: {&m.source, uint64, m.range} -> lib.mem.lstptr(m.post)
          466  +	timeline_instance_fetch: {&m.source, m.range} -> lib.mem.lstptr(m.post)
   411    467   }
   412    468   
   413    469   m.user_conf_funcs(m.backend, 'str', rawstring, lib.mem.ptr(int8))
   414    470   m.user_conf_funcs(m.backend, 'int', intptr, intptr, bool)
   415    471   
   416    472   struct m.source {
   417    473   	backend: &m.backend

Modified view/profile.tpl from [694fa2eec6] to [4ac5403896].

     2      2   	<div class="banner">
     3      3   		<img class="avatar" src="@:avatar">
     4      4   		<div class="id">@nym</div>
     5      5   		<div class="bio">
     6      6   			@bio
     7      7   		</div>
     8      8   	</div>
     9         -	<table class="stats">
    10         -		<tr><th>posts</th> <td>@nposts</td></tr>
    11         -		<tr><th>following</th> <td>@nfollows</td></tr>
    12         -		<tr><th>followers</th> <td>@nfollowers</td></tr>
    13         -		<tr><th>mutuals</th> <td>@nmutuals</td></tr>
    14         -		<tr><th>@timephrase</th> <td>@tweetday</td></tr>
    15         -	</table>
            9  +	<div class="stats">
           10  +		<table>
           11  +			<tr><th>posts</th> <th>mutuals</th></tr>
           12  +			<tr><td>@nposts</td> <td>@nmutuals</td></tr>
           13  +			<tr><th>following</th> <th>followers</th></tr>
           14  +			<tr><td>@nfollows</td> <td>@nfollowers</td></tr>
           15  +			<tr><th>@timephrase</th> <td>@tweetday</td></tr>
           16  +		</table>
           17  +		<ul class="remarks">@remarks</ul>
           18  +	</div>
    16     19   	<form class="actions">
    17     20   		<a class="button" href="/@:xid">posts</a>
    18     21   		<a class="button" href="/@:xid/arc">archive</a>
    19     22   		<a class="button" href="/@:xid/media">media</a>
    20     23   		<a class="button" href="/@:xid/social">associates</a>
    21     24   		<hr>
    22     25   		@auxbtn
    23     26   	</form>
    24     27   </div>