parsav  Check-in [24ec409083]

Overview
Comment:add live updates, system to only update when necessary almost works
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 24ec4090837417ad087eff63c16c1336dffc58f941ab2786f4175be4b558a7ad
User & Date: lexi on 2021-01-01 04:33:10
Other Links: manifest | tags
Context
2021-01-01
16:24
move from webp to svg except where necessary check-in: aa17a03321 user: lexi tags: trunk
04:33
add live updates, system to only update when necessary almost works check-in: 24ec409083 user: lexi tags: trunk
2020-12-31
02:18
start work on user mgmt check-in: db4c5fd644 user: lexi tags: trunk
Changes

Modified acl.t from [7cc6c4467d] to [db020952e6].

    10     10   	id: uint64
    11     11   }
    12     12   
    13     13   terra m.eval(expr: lib.str.t, agent: m.agent)
    14     14   
    15     15   end
    16     16   
           17  +
           18  +terra lib.store.post:comp()
           19  + -- TODO extract mentions from body, circles from acl
           20  +	self.mentions = [lib.mem.ptr(uint64)].null()
           21  +	self.circles = [lib.mem.ptr(uint64)].null()
           22  +	self.convoheaduri = nil
           23  +end
           24  +
    17     25   terra lib.store.post:save(ctupdate: bool)
    18         --- this post handles the messy details of registering a post's
    19         --- circles and actors, and increments the edit-count if ctupdate
    20         --- is true, which is should be in almost all cases.
           26  + -- this post handles the messy details of registering a post's
           27  + -- circles and actors, and increments the edit-count if ctupdate
           28  + -- is true, which is should be in almost all cases.
    21     29   	if ctupdate then
    22     30   		self.chgcount = self.chgcount + 1
    23     31   		self.edited = lib.osclock.time(nil)
    24     32   	end
    25         -	-- TODO extract mentions from body, circles from acl
           33  +	self:comp()
    26     34   	self.source:post_save(self)
    27     35   end
    28     36   
    29     37   return m

Modified backend/pgsql.t from [35848d4bf0] to [3e99c3d4ab].

   254    254   			update parsav_actors set
   255    255   				authtime = to_timestamp($2::bigint)
   256    256   				where id = $1::bigint
   257    257   		]];
   258    258   	};
   259    259   
   260    260   	auth_create_pw = {
   261         -		params = {uint64, lib.mem.ptr(uint8)}, cmd = true, sql = [[
   262         -			insert into parsav_auth (uid, name, kind, cred) values (
          261  +		params = {uint64, binblob, pstring}, cmd = true, sql = [[
          262  +			insert into parsav_auth (uid, name, kind, cred, comment) values (
   263    263   				$1::bigint,
   264    264   				(select handle from parsav_actors where id = $1::bigint),
   265         -				'pw-sha256', $2::bytea
          265  +				'pw-sha256', $2::bytea,
          266  +				$3::text
   266    267   			)
   267    268   		]]
   268    269   	};
   269    270   
   270    271   	auth_purge_type = {
   271    272   		params = {rawstring, uint64, rawstring}, cmd = true, sql = [[
   272    273   			delete from parsav_auth where
   273    274   				((uid = 0 and name = $1::text) or uid = $2::bigint) and
   274    275   				kind like $3::text
   275    276   		]]
   276    277   	};
          278  +
          279  +	auth_enum_uid = {
          280  +		params = {uint64}, sql = [[
          281  +			select aid, kind, comment, netmask, blacklist from parsav_auth where uid = $1::bigint
          282  +		]];
          283  +	};
          284  +
          285  +	auth_enum_handle = {
          286  +		params = {rawstring}, sql = [[
          287  +			select aid, kind, comment, netmask, blacklist from parsav_auth where name = $1::text
          288  +		]];
          289  +	};
   277    290   
   278    291   	post_save = {
   279    292   		params = {
   280    293   			uint64, uint32, int64;
   281    294   			rawstring, rawstring, rawstring;
   282    295   		}, cmd = true, sql = [[
   283    296   			update parsav_posts set
................................................................................
   287    300   				chgcount = $2::integer,
   288    301   				edited = to_timestamp($3::bigint)
   289    302   			where id = $1::bigint
   290    303   		]]
   291    304   	};
   292    305   
   293    306   	post_create = {
   294         -		params = {uint64, rawstring, rawstring, rawstring}, sql = [[
          307  +		params = {
          308  +			uint64, rawstring, rawstring, rawstring,
          309  +			uint64, uint64, rawstring
          310  +		}, sql = [[
   295    311   			insert into parsav_posts (
   296    312   				author, subject, acl, body,
   297         -				posted, discovered,
   298         -				circles, mentions
          313  +				parent, posted, discovered,
          314  +				circles, mentions, convoheaduri
   299    315   			) values (
   300    316   				$1::bigint, case when $2::text = '' then null else $2::text end,
   301    317   				$3::text, $4::text, 
   302         -				now(), now(), array[]::bigint[], array[]::bigint[]
          318  +				$5::bigint, to_timestamp($6::bigint), now(),
          319  +				array[]::bigint[], array[]::bigint[], $7::text
   303    320   			) returning id
   304    321   		]]; -- TODO array handling
   305    322   	};
   306    323   
   307    324   	post_destroy_prepare = {
   308    325   		params = {uint64}, cmd = true, sql = [[
   309    326   			update parsav_posts set
................................................................................
   321    338   	post_fetch = {
   322    339   		params = {uint64}, sql = [[
   323    340   			select a.origin is null,
   324    341   				p.id, p.author, p.subject, p.acl, p.body,
   325    342   				extract(epoch from p.posted    )::bigint,
   326    343   				extract(epoch from p.discovered)::bigint,
   327    344   				extract(epoch from p.edited    )::bigint,
   328         -				p.parent, p.convoheaduri, p.chgcount
          345  +				p.parent, p.convoheaduri, p.chgcount,
          346  +				coalesce(c.value, -1)::smallint
          347  +
   329    348   			from parsav_posts as p
   330         -				inner join parsav_actors as a on p.author = a.id
          349  +				inner join parsav_actors          as a on p.author = a.id
          350  +				left join  parsav_actor_conf_ints as c on c.uid    = a.id and c.key = 'ui-accent'
   331    351   			where p.id = $1::bigint
   332    352   		]];
   333    353   	};
          354  +
          355  +	post_enum_parent = {
          356  +		params = {uint64}, sql = [[
          357  +			select a.origin is null,
          358  +				p.id, p.author, p.subject, p.acl, p.body,
          359  +				extract(epoch from p.posted    )::bigint,
          360  +				extract(epoch from p.discovered)::bigint,
          361  +				extract(epoch from p.edited    )::bigint,
          362  +				p.parent, p.convoheaduri, p.chgcount,
          363  +				coalesce(c.value, -1)::smallint
          364  +
          365  +			from parsav_posts as p
          366  +				inner join parsav_actors as a on a.id = p.author
          367  +				left join  parsav_actor_conf_ints as c on c.uid = a.id and c.key = 'ui-accent'
          368  +			where p.parent = $1::bigint
          369  +			order by p.posted, p.discovered asc
          370  +		]]
          371  +	};
          372  +
          373  +	thread_latest_arrival_calc = {
          374  +		params = {uint64}, sql = [[
          375  +			with recursive posts(id) as (
          376  +				select id from parsav_posts where parent = $1::bigint
          377  +			union
          378  +				select p.id from parsav_posts as p
          379  +					inner join posts on posts.id = p.parent
          380  +			), 
          381  +
          382  +			maxes as (
          383  +				select unnest(array[max(p.posted), max(p.discovered), max(p.edited)]) as m
          384  +					from posts
          385  +					inner join parsav_posts as p
          386  +						on p.id = posts.id
          387  +			)
          388  +
          389  +			select extract(epoch from max(m))::bigint from maxes
          390  +		]];
          391  +	};
   334    392   
   335    393   	post_enum_author_uid = {
   336    394   		params = {uint64,uint64,uint64,uint64, uint64}, sql = [[
   337    395   			select a.origin is null,
   338    396   				p.id, p.author, p.subject, p.acl, p.body,
   339    397   				extract(epoch from p.posted    )::bigint,
   340    398   				extract(epoch from p.discovered)::bigint,
   341    399   				extract(epoch from p.edited    )::bigint,
   342         -				p.parent, p.convoheaduri, p.chgcount
          400  +				p.parent, p.convoheaduri, p.chgcount,
          401  +				coalesce((select value from parsav_actor_conf_ints as c where
          402  +					c.uid = $1::bigint and c.key = 'ui-accent'),-1)::smallint
          403  +
   343    404   			from parsav_posts as p
   344    405   				inner join parsav_actors as a on p.author = a.id
   345    406   			where p.author = $5::bigint and
   346    407   				($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and
   347    408   				($2::bigint = 0 or to_timestamp($2::bigint) < p.posted)
   348    409   			order by (p.posted, p.discovered) desc
   349    410   			limit case when $3::bigint = 0 then null
................................................................................
   357    418   	timeline_instance_fetch = {
   358    419   		params = {uint64, uint64, uint64, uint64}, sql = [[
   359    420   			select true,
   360    421   				p.id, p.author, p.subject, p.acl, p.body,
   361    422   				extract(epoch from p.posted    )::bigint,
   362    423   				extract(epoch from p.discovered)::bigint,
   363    424   				extract(epoch from p.edited    )::bigint,
   364         -				p.parent, null::text, p.chgcount
          425  +				p.parent, null::text, p.chgcount,
          426  +				coalesce(c.value, -1)::smallint
          427  +
   365    428   			from parsav_posts as p
   366         -				inner join parsav_actors as a on p.author = a.id
          429  +				inner join parsav_actors          as a on p.author = a.id
          430  +				left join  parsav_actor_conf_ints as c on c.uid    = a.id and c.key = 'ui-accent'
   367    431   			where
   368    432   				($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and
   369    433   				($2::bigint = 0 or to_timestamp($2::bigint) < p.posted) and
   370    434   				(a.origin is null)
   371    435   			order by (p.posted, p.discovered) desc
   372    436   			limit case when $3::bigint = 0 then null
   373    437   			           else $3::bigint end
................................................................................
   430    494   		]];
   431    495   	};
   432    496   	post_attach_ctl_ins = {
   433    497   		params = {uint64, uint64}, cmd=true, sql = [[
   434    498   			update parsav_posts set
   435    499   				artifacts = artifacts || $2::bigint
   436    500   			where id = $1::bigint and not
   437         -				artifacts @> array[$2::bigint]
          501  +				artifacts @> array[$2::bigint] -- prevent duplication
   438    502   		]];
   439    503   	};
   440    504   	post_attach_ctl_del = {
   441    505   		params = {uint64, uint64}, cmd=true, sql = [[
   442    506   			update parsav_posts set
   443    507   				artifacts = array_remove(artifacts, $2::bigint)
   444    508   			where id = $1::bigint and
   445    509   				artifacts @> array[$2::bigint]
   446    510   		]];
   447    511   	};
          512  +
          513  +	actor_conf_str_get = {
          514  +		params = {uint64, rawstring}, sql = [[
          515  +			select value from parsav_actor_conf_strs where
          516  +				uid = $1::bigint and
          517  +				key = $2::text
          518  +			limit 1
          519  +		]];
          520  +	};
          521  +	actor_conf_str_set = {
          522  +		params = {uint64, rawstring, rawstring}, cmd = true, sql = [[
          523  +			insert into parsav_actor_conf_strs (uid,key,value)
          524  +				values ($1::bigint, $2::text, $3::text)
          525  +			on conflict (uid,key) do update set value = $3::text
          526  +		]];
          527  +	};
          528  +	actor_conf_str_enum = {
          529  +		params = {uint64}, sql = [[
          530  +			select value from parsav_actor_conf_strs where uid = $1::bigint
          531  +		]];
          532  +	};
          533  +	actor_conf_str_reset = {
          534  +		params = {uint64, rawstring}, cmd = true, sql = [[
          535  +			delete from parsav_actor_conf_strs where
          536  +				uid = $1::bigint and ($2::text is null or key = $2::text)
          537  +		]]
          538  +	};
          539  +
          540  +	actor_conf_int_get = {
          541  +		params = {uint64, rawstring}, sql = [[
          542  +			select value from parsav_actor_conf_ints where
          543  +				uid = $1::bigint and
          544  +				key = $2::text
          545  +			limit 1
          546  +		]];
          547  +	};
          548  +	actor_conf_int_set = {
          549  +		params = {uint64, rawstring, uint64}, cmd = true, sql = [[
          550  +			insert into parsav_actor_conf_ints (uid,key,value)
          551  +				values ($1::bigint, $2::text, $3::bigint)
          552  +			on conflict (uid,key) do update set value = $3::bigint
          553  +		]];
          554  +	};
          555  +	actor_conf_int_enum = {
          556  +		params = {uint64}, sql = [[
          557  +			select value from parsav_actor_conf_ints where uid = $1::bigint
          558  +		]];
          559  +	};
          560  +	actor_conf_int_reset = {
          561  +		params = {uint64, rawstring}, cmd = true, sql = [[
          562  +			delete from parsav_actor_conf_ints where
          563  +				uid = $1::bigint and ($2::text is null or key = $2::text)
          564  +		]]
          565  +	};
   448    566   }
   449    567   
   450    568   local struct pqr {
   451    569   	sz: intptr
   452    570   	res: &lib.pq.PGresult
   453    571   }
   454    572   terra pqr:free() if self.sz > 0 then lib.pq.PQclear(self.res) end end
................................................................................
   651    769   		then p.ptr.parent = 0
   652    770   		else p.ptr.parent = r:int(uint64,row,9)
   653    771   	end 
   654    772   	if r:null(row,11)
   655    773   		then p.ptr.chgcount = 0
   656    774   		else p.ptr.chgcount = r:int(uint32,row,11)
   657    775   	end 
          776  +	p.ptr.accent = r:int(int16,row,12)
   658    777   	p.ptr.localpost = r:bool(row,0)
   659    778   
   660    779   	return p
   661    780   end
   662    781   local terra row_to_actor(r: &pqr, row: intptr): lib.mem.ptr(lib.store.actor)
   663    782   	var a: lib.mem.ptr(lib.store.actor)
   664    783   	var av: rawstring, avlen: intptr
................................................................................
  1025   1144   			            [lib.mem.ptr(lib.store.actor)] { ptr = nil, ct = 0 }
  1026   1145   	end];
  1027   1146   
  1028   1147   	post_create = [terra(
  1029   1148   		src: &lib.store.source,
  1030   1149   		post: &lib.store.post
  1031   1150   	): uint64
  1032         -		var r = queries.post_create.exec(src,post.author,post.subject,post.acl,post.body) 
         1151  +		var r = queries.post_create.exec(src,
         1152  +			post.author,post.subject,post.acl,post.body,
         1153  +			post.parent,post.posted,post.convoheaduri
         1154  +		) 
  1033   1155   		if r.sz == 0 then return 0 end
  1034   1156   		defer r:free()
  1035   1157   		var id = r:int(uint64,0,0)
         1158  +		post.source = src
  1036   1159   		return id
  1037   1160   	end];
  1038   1161   
  1039   1162   	post_destroy = [terra(
  1040   1163   		src: &lib.store.source,
  1041   1164   		post: uint64
  1042   1165   	): {}
................................................................................
  1114   1237   		lib.dbg('created new actor, establishing powers')
  1115   1238   		privupdate(src,ac)
  1116   1239   
  1117   1240   		lib.dbg('powers established')
  1118   1241   		return ac.id
  1119   1242   	end];
  1120   1243   
  1121         -	auth_create_pw = [terra(
         1244  +	auth_enum_uid = [terra(
         1245  +		src: &lib.store.source,
         1246  +		uid: uint64
         1247  +	): lib.mem.ptr(lib.mem.ptr(lib.store.auth))
         1248  +		var r = queries.auth_enum_uid.exec(src,uid)
         1249  +		if r.sz == 0 then return [lib.mem.ptr(lib.mem.ptr(lib.store.auth))].null() end
         1250  +		var ret = lib.mem.heapa([lib.mem.ptr(lib.store.auth)], r.sz)
         1251  +		for i=0, r.sz do
         1252  +			var kind = r:_string(i, 1)
         1253  +			var comment = r:_string(i, 2)
         1254  +			var a = [ lib.str.encapsulate(lib.store.auth, {
         1255  +				kind = {`kind.ptr, `kind.ct};
         1256  +				comment = {`comment.ptr, `comment.ct};
         1257  +			}) ]
         1258  +			a.ptr.aid = r:int(uint64, i, 0)
         1259  +			a.ptr.netmask = r:cidr(i, 3)
         1260  +			a.ptr.blacklist = r:bool(i, 4)
         1261  +			ret.ptr[i] = a
         1262  +		end
         1263  +		return ret
         1264  +	end];
         1265  +
         1266  +	auth_attach_pw = [terra(
  1122   1267   		src: &lib.store.source,
  1123   1268   		uid: uint64,
  1124   1269   		reset: bool,
  1125         -		pw: lib.mem.ptr(int8)
         1270  +		pw: pstring,
         1271  +		comment: pstring
  1126   1272   	): {}
  1127   1273   		var hash: uint8[lib.crypt.algsz.sha256]
  1128   1274   		if lib.md.mbedtls_md(lib.md.mbedtls_md_info_from_type(lib.crypt.alg.sha256.id),
  1129   1275   			[&uint8](pw.ptr), pw.ct, &hash[0]) ~= 0 then
  1130   1276   			lib.bail('cannot hash password')
  1131   1277   		end
  1132   1278   		if reset then queries.auth_purge_type.exec(src, nil, uid, 'pw-%') end
  1133         -		queries.auth_create_pw.exec(src, uid, [lib.mem.ptr(uint8)] {ptr = &hash[0], ct = [hash.type.N]})
         1279  +		queries.auth_create_pw.exec(src, uid, binblob {ptr = &hash[0], ct = [hash.type.N]}, comment)
  1134   1280   	end];
  1135   1281   
  1136   1282   	auth_purge_pw = [terra(src: &lib.store.source, uid: uint64, handle: rawstring): {}
  1137   1283   		queries.auth_purge_type.exec(src, handle, uid, 'pw-%')
  1138   1284   	end];
  1139   1285   
  1140   1286   	auth_purge_otp = [terra(src: &lib.store.source, uid: uint64, handle: rawstring): {}
................................................................................
  1208   1354   		src: &lib.store.source,
  1209   1355   		post: &lib.store.post
  1210   1356   	): {}
  1211   1357   		queries.post_save.exec(src,
  1212   1358   			post.id, post.chgcount, post.edited,
  1213   1359   			post.subject, post.acl, post.body)
  1214   1360   	end];
         1361  +
         1362  +	post_enum_parent = [terra(
         1363  +		src: &lib.store.source,
         1364  +		post: uint64
         1365  +	): lib.mem.ptr(lib.mem.ptr(lib.store.post))
         1366  +		var r = queries.post_enum_parent.exec(src,post)
         1367  +		if r.sz == 0 then
         1368  +			return [lib.mem.ptr(lib.mem.ptr(lib.store.post))].null()
         1369  +		end
         1370  +		defer r:free()
         1371  +		var lst = lib.mem.heapa([lib.mem.ptr(lib.store.post)], r.sz)
         1372  +
         1373  +		for i=0, r.sz do lst.ptr[i] = row_to_post(&r, i) end
         1374  +
         1375  +		return lst
         1376  +	end];
         1377  +
         1378  +	thread_latest_arrival_calc = [terra(
         1379  +		src: &lib.store.source,
         1380  +		post: uint64
         1381  +	): lib.store.timepoint
         1382  +		var r = queries.thread_latest_arrival_calc.exec(src,post)
         1383  +		if r.sz == 0 or r:null(0,0) then return 0 end
         1384  +		var tp: lib.store.timepoint = r:int(int64,0,0)
         1385  +		r:free()
         1386  +		return tp
         1387  +	end];
  1215   1388   
  1216   1389   	auth_sigtime_user_fetch = [terra(
  1217   1390   		src: &lib.store.source,
  1218   1391   		uid: uint64
  1219   1392   	): lib.store.timepoint
  1220   1393   		var r = queries.auth_sigtime_user_fetch.exec(src, uid)
  1221   1394   		if r.sz > 0 then defer r:free()
................................................................................
  1226   1399   
  1227   1400   	auth_sigtime_user_alter = [terra(
  1228   1401   		src: &lib.store.source,
  1229   1402   		uid: uint64,
  1230   1403   		time: lib.store.timepoint
  1231   1404   	): {} queries.auth_sigtime_user_alter.exec(src, uid, time) end];
  1232   1405   
         1406  +	actor_conf_str_enum = nil;
         1407  +	actor_conf_str_get = [terra(src: &lib.store.source, uid: uint64, key: rawstring): pstring
         1408  +			var r = queries.actor_conf_str_get.exec(src, uid, key)
         1409  +			if r.sz > 0 then
         1410  +				var ret = r:String(0,0)
         1411  +				r:free()
         1412  +				return ret
         1413  +			else return pstring.null() end
         1414  +		end];
         1415  +	actor_conf_str_set = [terra(src: &lib.store.source, uid: uint64, key: rawstring, value: rawstring): {}
         1416  +			queries.actor_conf_str_set.exec(src,uid,key,value) end];
         1417  +	actor_conf_str_reset = [terra(src: &lib.store.source, uid: uint64, key: rawstring): {}
         1418  +			queries.actor_conf_str_reset.exec(src,uid,key) end];
         1419  +
         1420  +	actor_conf_int_enum = nil;
         1421  +	actor_conf_int_get = [terra(src: &lib.store.source, uid: uint64, key: rawstring)
         1422  +			var r = queries.actor_conf_int_get.exec(src, uid, key)
         1423  +			if r.sz > 0 then
         1424  +				var ret = r:int(uint64,0,0)
         1425  +				r:free()
         1426  +				return ret, true
         1427  +			end
         1428  +			return 0, false
         1429  +		end];
         1430  +	actor_conf_int_set = [terra(src: &lib.store.source, uid: uint64, key: rawstring, value: uint64): {}
         1431  +			queries.actor_conf_int_set.exec(src,uid,key,value) end];
         1432  +	actor_conf_int_reset = [terra(src: &lib.store.source, uid: uint64, key: rawstring): {}
         1433  +			queries.actor_conf_int_reset.exec(src,uid,key) end];
         1434  +
  1233   1435   	actor_auth_register_uid = nil; -- TODO better support non-view based auth
  1234   1436   }
  1235   1437   
  1236   1438   return b

Modified backend/schema/pgsql-drop.sql from [17a37aa5f6] to [fa02548662].

     1      1   -- destroy absolutely everything
     2      2   
     3      3   drop table if exists parsav_config cascade;
     4      4   drop table if exists parsav_servers cascade;
     5      5   drop table if exists parsav_actors cascade;
            6  +drop table if exists parsav_actor_conf_strs cascade;
            7  +drop table if exists parsav_actor_conf_ints cascade;
     6      8   drop table if exists parsav_rights cascade;
     7      9   drop table if exists parsav_posts cascade;
     8     10   drop table if exists parsav_conversations cascade;
     9     11   drop table if exists parsav_rels cascade;
    10     12   drop table if exists parsav_acts cascade;
    11     13   drop table if exists parsav_log cascade;
    12     14   drop table if exists parsav_artifacts cascade;

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

   167    167   	nature smallint not null, -- silence, suspend, disemvowel, censor, noreply, etc
   168    168   	victim bigint not null, -- can be user, room, or post
   169    169   	expire timestamp, -- auto-expires if set
   170    170   	review timestamp,  -- brings up for review at given time if set
   171    171   	reason text, -- visible to victim if set
   172    172   	context text -- admin-only note
   173    173   );
          174  +
          175  +create table parsav_actor_conf_strs (
          176  +	uid bigint not null references parsav_actors(id) on delete cascade,
          177  +	key text not null, value text not null, unique (uid,key)
          178  +);
          179  +create table parsav_actor_conf_ints (
          180  +	uid bigint not null references parsav_actors(id) on delete cascade,
          181  +	key text not null, value bigint not null, unique (uid,key)
          182  +);
   174    183   
   175    184   -- create a temporary managed auth table; we can delete this later
   176    185   -- if it ends up being replaced with a view
   177    186   %include pgsql-auth.sql%

Modified config.lua from [3d0e5432a4] to [cd48dd2db6].

    50     50   	embeds = {
    51     51   		-- TODO with gzip compression, svg is dramatically superior to webp
    52     52   		-- we should have a build-time option to serve svg so instances
    53     53   		-- proxied behind nginx can serve svgz, or possibly just straight-up
    54     54   		-- add support for content-encoding headers and pre-compress the
    55     55   		-- damn things before compiling
    56     56   		{'style.css', 'text/css'};
           57  +		{'live.js', 'text/javascript'}; -- rrrrrrrr
    57     58   		{'default-avatar.webp', 'image/webp'};
    58     59   		{'padlock.webp', 'image/webp'};
    59     60   		{'warn.webp', 'image/webp'};
    60     61   		{'query.webp', 'image/webp'};
    61     62   	};
           63  +	default_ui_accent = tonumber(default('parsav_ui_default_accent',323));
    62     64   }
    63     65   if os.getenv('parsav_let_me_be_an_idiot') == "i know what i'm doing" then
    64     66   	conf.braingeniousmode = true -- SOUND GENERAL QUARTERS
    65     67   end
    66     68   if u.ping '.fslckout' or u.ping '_FOSSIL_' then
    67     69   	if u.ping '_FOSSIL_' then default_os = 'windows' end
    68     70   	conf.build.branch = u.exec { 'fossil', 'branch', 'current' }

Modified math.t from [bc01716315] to [1d1d177061].

   184    184   		else dgtct = dgtct + 1 end
   185    185   	end else
   186    186   		buf = buf - 1
   187    187   		@buf = 0x30
   188    188   	end
   189    189   	return buf
   190    190   end
          191  +
          192  +terra m.decparse(s: pstring): {intptr, bool}
          193  +	if not s then return 0, false end
          194  +	var val:intptr = 0
          195  +	var c = s.ptr
          196  +	while @c ~= 0 do
          197  +		if @c >= 0x30 and @c <= 0x39 then
          198  +			val = val * 10
          199  +			val = val + (@c - 0x30)
          200  +		else
          201  +			return 0, false
          202  +		end
          203  +
          204  +		c = c + 1
          205  +		if s.ct ~= 0 and (c - s.ptr > s.ct) then lib.dbg('reached end') return val, true end
          206  +	end
          207  +	return val, true
          208  +end
   191    209   
   192    210   terra m.ndigits(n: intptr, base: intptr): intptr
   193    211   	var c = base
   194    212   	var i = 1
   195    213   	while true do
   196    214   		if n < c then return i end
   197    215   		c = c * base

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

    59     59   
    60     60   	sid: uint64
    61     61   	iname: rawstring
    62     62   }
    63     63   idelegate.metamethods.__methodmissing = macro(function(meth, self, ...)
    64     64   	local expr = {...}
    65     65   	local rt
           66  +
    66     67   	for _,f in pairs(lib.store.backend.entries) do
    67     68   		local fn = f.field or f[1]
    68     69   		local ft = f.type or f[2]
    69     70   		if fn == meth then rt = ft.type.returntype break end
    70     71   	end
    71     72   
    72     73   	return quote
................................................................................
   113    114   		if tmppw[i] >= 36 then
   114    115   			tmppw[i] = tmppw[i] + (0x61 - 36)
   115    116   		elseif tmppw[i] >= 10 then
   116    117   			tmppw[i] = tmppw[i] + (0x41 - 10)
   117    118   		else tmppw[i] = tmppw[i] + 0x30 end
   118    119   	end
   119    120   	lib.dbg('assigning temporary password')
   120         -	dlg:auth_create_pw(uid, reset, pstr {
   121         -		ptr = [rawstring](tmppw), ct = 32
   122         -	})
          121  +	dlg:auth_attach_pw(uid, reset,
          122  +		pstr { ptr = [rawstring](tmppw), ct = 32 },
          123  +		lib.str.plit 'temporary password');
   123    124   end
   124    125   
   125    126   local terra ipc_report(acks: lib.mem.ptr(lib.ipc.ack), rep: rawstring)
   126    127   	var decbuf: int8[21]
   127    128   	for i=0,acks.ct do
   128    129   		var num = lib.math.decstr(acks(i).clid, &decbuf[20])
   129    130   		if acks(i).success then
................................................................................
   334    335   				if lib.str.cmp(cfmode.arglist(0),'chsec') == 0 then
   335    336   					var sec: int8[65] gensec(&sec[0])
   336    337   					dlg:conf_set('server-secret', &sec[0])
   337    338   					lib.report('server secret reset')
   338    339   				elseif lib.str.cmp(cfmode.arglist(0),'refresh') == 0 then
   339    340   					cfmode.no_notify = false -- duh
   340    341   				else goto cmderr end
          342  +			elseif cfmode.arglist.ct == 2 and
          343  +				lib.str.cmp(cfmode.arglist(0),'reset') == 0 or
          344  +				lib.str.cmp(cfmode.arglist(0),'clear') == 0 or
          345  +				lib.str.cmp(cfmode.arglist(0),'unset') == 0 then
          346  +				dlg:conf_reset(cfmode.arglist(1))
          347  +				lib.report('parameter cleared')
   341    348   			elseif cfmode.arglist.ct == 3 and
   342    349   				lib.str.cmp(cfmode.arglist(0),'set') == 0 then
   343    350   				dlg:conf_set(cfmode.arglist(1),cfmode.arglist(2))
   344    351   				lib.report('parameter set')
   345    352   			else goto cmderr end
   346    353   
   347    354   			-- successful commands fall through

Modified parsav.md from [409d9b6b60] to [bfa0a26bd5].

    10     10   ## dependencies
    11     11   
    12     12   * runtime
    13     13     * mongoose
    14     14     * json-c
    15     15     * mbedtls
    16     16     * **postgresql backend:**
    17         -    * postgresql-libs 
           17  +     * postgresql-libs 
    18     18   * compile-time
    19     19     * cmark (commonmark implementation), for transformation of the help files, whose source is in commonmark. online documentation transforms these into html and embeds them in the binary; cmark is also used to to produce the troff source which is used to build the offline documentation. disable with `parsav_online_documentation=no parsav_offline_documentation=no`
    20     20     * troff implementation (tested with groff but as far as i know we don't need any groff-specific extensions) to produce PDFs and manpages from the cmark-generated intermediate forms. disable with `parsav_offline_documentation=no`
    21     21   
    22     22   additional preconfigure dependencies are necessary if you are building directly from trunk, rather than from a release tarball that includes certain build artifacts which need to be embedded in the binary:
    23     23   
    24     24   * inkscape, for rendering out UI graphics
    25     25   * cwebp (libwebp package), for transforming inkscape PNGs to webp
    26     26   * sassc, for compiling the SCSS stylesheet into its final CSS
    27     27   
    28     28   all builds require terra, which, unfortunately, requires installing an older version of llvm, v9 at the latest (which i develop parsav under). with any luck, your distro will be clever enough to package terra and its dependencies properly (it's trivial on nix, tho you'll need to tweak the terra expression to select a more recent llvm package); Arch Linux is one of those distros which is not so clever, and whose (AUR) terra package is totally broken. due to these unfortunate circumstances, terra is distributed not just in source form, but also in the the form of LLVM IR. distributions will also be made in the form of tarballed object code and assembly listings for various common platforms, currently including x86-32/64, arm7hf, aarch64, riscv, mips32/64, and ppc64/64le.
    29     29   
    30         -i've noticed that terra (at least with llvm9) seems to get a bit cantankerous and trigger llvm to fail with bizarre errors when you try to cross-compile parsav from x86-64 to any other platform, even x86-32. i don't know if this problem exists on other architectures or in what form, but as a workaround, the current cross-compile process consists of generating LLVM IR (ostensible for x86-64, though this is in reality an architecture-independent language), and then compiling that down to an object file with llc. this is an enormous hassle; hopefully the terra people will fix this eventually.
           30  +i've noticed that terra (at least with llvm9) seems to get a bit cantankerous and trigger llvm to fail with bizarre errors when you try to cross-compile parsav from x86-64 to any other platform, even x86-32. i don't know if this problem exists on other architectures or in what form, but as a workaround, the current cross-compile process consists of generating LLVM IR (ostensibly for x86-64, though this is in reality an architecture-independent language), and then compiling that down to an object file with llc. this is an enormous hassle; hopefully the terra (or llvm?) people will fix this eventually.
    31     31   
    32     32   also note that, while parsav has a flag to build with ASAN, ASAN has proven unusable for most purposes as it routinely reports false positive buffer-heap-overflows. if you figure out how to defuckulate this, i will be overjoyed.
    33     33   
    34     34   ## building
    35     35   
    36     36   first, either install any missing dependencies as shared libraries, or build them as static libraries with the command `make dep.$LIBRARY`. as a shortcut, `make dep` will build all dependencies as static libraries. note that if the build system finds a static version of a library in the `lib/` folder, it will use that instead of any system library. note that these commands require GNU make (it may be installed as `gmake` on your system), although this is a fairly soft dependency -- if you really need to build it on BSD make, you can probably translate it with a minute or so of work; you'll just have to do some of the various gmake functions' work manually. this may be worthwhile if you're packaging for a BSD.
    37     37   

Modified parsav.t from [90c24eca6f] to [4b4b2876e4].

   602    602   	print(util.dump(config))
   603    603   	os.exit(0)
   604    604   end
   605    605   
   606    606   local holler = print
   607    607   local suffix = config.exe and '' or ('.'..config.outform)
   608    608   local out = 'parsavd' .. suffix
   609         -local linkargs = {'-O4'}
          609  +local linkargs = {}
   610    610   local target = config.tgttrip and terralib.newtarget {
   611    611   	Triple = config.tgttrip;
   612    612   	CPU = config.tgtcpu;
   613    613   	FloatABIHard = config.tgthf;
   614    614   } or nil
   615    615   
   616    616   if bflag('quiet','q') then holler = function() end end

Modified render/conf.t from [1b75d5dd6d] to [fa178f73b5].

     1      1   -- vim: ft=terra
     2      2   local pstr = lib.mem.ptr(int8)
     3      3   local pref = lib.mem.ref(int8)
     4      4   
     5      5   local mappings = {
     6      6   	{url = 'profile', title = 'account profile', render = 'profile'};
     7      7   	{url = 'avi', title = 'avatar', render = 'avatar'};
            8  +	{url = 'ui', title = 'user interface', render = 'ui'};
     8      9   	{url = 'sec', title = 'security', render = 'sec'};
     9     10   	{url = 'rel', title = 'relationships', render = 'rel'};
    10     11   	{url = 'qnt', title = 'quarantine', render = 'quarantine'};
    11     12   	{url = 'acl', title = 'access control shortcuts', render = 'acl'};
    12     13   	{url = 'rooms', title = 'chatrooms', render = 'rooms'};
    13     14   	{url = 'circles', title = 'circles', render = 'circles'};
    14     15   

Modified render/conf/profile.t from [7f970c2f4a] to [864a63a85e].

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

Modified render/timeline.t from [2afba48373] to [ab5808172b].

    23     23   			to_idx = 64;
    24     24   		})
    25     25   	elseif mode == modes.fediglobal then
    26     26   	elseif mode == modes.circle then
    27     27   	end
    28     28   
    29     29   	var acc: lib.str.acc acc:init(1024)
           30  +	acc:lpush('<div id="tl" data-live="10">')
           31  +	var newest: lib.store.timepoint = 0
    30     32   	for i = 0, posts.sz do
    31     33   		lib.render.tweet(co, posts(i).ptr, &acc)
           34  +		var t = lib.math.biggest(lib.math.biggest(posts(i).ptr.posted, posts(i).ptr.discovered),posts(i).ptr.edited)
           35  +		if t > newest then newest = t end
    32     36   		posts(i):free()
    33     37   	end
    34     38   	posts:free()
           39  +	acc:lpush('</div>')
    35     40   
    36     41   	var doc = [lib.srv.convo.page] {
    37     42   		title = lib.str.plit'timeline';
    38     43   		body = acc:finalize();
    39     44   		class = lib.str.plit'timeline';
    40     45   		cache = false;
    41     46   	}
    42         -	co:stdpage(doc)
           47  +	co:livepage(doc,newest)
    43     48   	doc.body:free()
    44     49   end
    45     50   return render_timeline

Modified render/tweet-page.t from [ad592d16bd] to [005ba03599].

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

Modified render/tweet.t from [ac0f8e680f] to [77ab77b2db].

    32     32   		subject = cs(lib.coalesce(p.subject,''));
    33     33   		nym = fullname;
    34     34   		when = cs(&timestr[0]);
    35     35   		avatar = cs(lib.trn(author.origin == 0, avistr.buf,
    36     36   			lib.coalesce(author.avatar, '/s/default-avatar.webp')));
    37     37   		acctlink = cs(author.xid);
    38     38   		permalink = permalink:finalize();
           39  +		attr = ''
    39     40   	}
           41  +
           42  +	var attrbuf: int8[32]
           43  +	if p.accent ~= -1 and p.accent ~= co.ui_hue then
           44  +		var hdecbuf: int8[21]
           45  +		var hdec = lib.math.decstr(p.accent, &hdecbuf[20])
           46  +		lib.str.cpy(&attrbuf[0], ' style="--hue:')
           47  +		lib.str.cpy(&attrbuf[14], hdec)
           48  +		var len = &hdecbuf[20] - hdec 
           49  +		lib.str.cpy(&attrbuf[14] + len, '"')
           50  +		tpl.attr = &attrbuf[0]
           51  +	end
           52  +
    40     53   	defer tpl.permalink:free()
    41     54   	if acc ~= nil then tpl:append(acc) return [lib.mem.ptr(int8)]{ptr=nil,ct=0} end
    42     55   	var txt = tpl:tostr()
    43     56   	return txt
    44     57   end
    45     58   return render_tweet

Modified render/user-page.t from [8e478d7a95] to [08cdf2fd9f].

    16     16   	var stoptime = lib.osclock.time(nil)
    17     17   	var posts = co.srv:post_enum_author_uid(actor.id, lib.store.range {
    18     18   		mode = 1; -- T->I
    19     19   		from_time = stoptime;
    20     20   		to_idx = 64;
    21     21   	})
    22     22   
           23  +	acc:lpush('<div id="feed" data-live="10">')
           24  +	var newest: lib.store.timepoint = 0
    23     25   	for i = 0, posts.sz do
    24     26   		lib.render.tweet(co, posts(i).ptr, &acc)
           27  +		var t = lib.math.biggest(lib.math.biggest(posts(i).ptr.posted, posts(i).ptr.discovered),posts(i).ptr.edited)
           28  +		if t > newest then newest = t end
    25     29   		posts(i):free()
    26     30   	end
    27     31   	posts:free()
           32  +	acc:lpush('</div>')
    28     33   
    29     34   	var bdf = acc:finalize()
    30         -	co:stdpage([lib.srv.convo.page] {
           35  +	co:livepage([lib.srv.convo.page] {
    31     36   		title = tiptr; body = bdf;
    32     37   		class = lib.str.plit 'profile';
    33     38   		cache = false;
    34         -	})
           39  +	}, newest)
    35     40   
    36     41   	tiptr:free()
    37     42   	bdf:free()
    38     43   end
    39     44   
    40     45   return render_userpage

Modified route.t from [a9eb70a00e] to [f59188addc].

     2      2   local r = lib.srv.route
     3      3   local method = lib.http.method
     4      4   local pstring = lib.mem.ptr(int8)
     5      5   local rstring = lib.mem.ref(int8)
     6      6   local hpath = lib.mem.ptr(rstring)
     7      7   local http = {}
     8      8   
            9  +terra meth_get(meth: method.t) return (meth == method.get) or (meth == method.head) end
     9     10   terra http.actor_profile_xid(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t)
    10     11   	var handle = [lib.mem.ptr(int8)] { ptr = &uri.ptr[2], ct = 0 }
    11     12   	for i=2,uri.ct do
    12     13   		if uri.ptr[i] == @'/' or uri.ptr[i] == 0 then handle.ct = i - 2 break end
    13     14   	end
    14     15   	if handle.ct == 0 then
    15     16   		handle.ct = uri.ct - 2
................................................................................
    56     57   	end
    57     58   	defer actor:free()
    58     59   
    59     60   	lib.render.user_page(co, actor.ptr)
    60     61   end
    61     62   
    62     63   terra http.login_form(co: &lib.srv.convo, meth: method.t)
    63         -	if meth == method.get then
           64  +	if meth_get(meth) then
    64     65   		-- request a username
    65     66   		lib.render.login(co, nil, nil, lib.str.plit(nil))
    66     67   	elseif meth == method.post then
    67     68   		var usn, usnl = co:postv('user')
    68     69   		var am, aml = co:postv('authmethod')
    69     70   		var chrs, chrsl = co:postv('response')
    70     71   		var cs, authok = co.srv:actor_auth_how(co.peer, usn)
................................................................................
   122    123   end
   123    124   
   124    125   terra http.post_compose(co: &lib.srv.convo, meth: method.t)
   125    126   	if not co:assertpow('post') then return end
   126    127   	--if co.who.rights.powers.post() == false then
   127    128   		--co:complain(403,'insufficient privileges','you lack the <strong>post</strong> power and cannot perform this action')
   128    129   
   129         -	if meth == method.get then
          130  +	if meth_get(meth) then
   130    131   		lib.render.compose(co, nil, nil)
   131    132   	elseif meth == method.post then
   132    133   		var text, textlen = co:postv("post")
   133    134   		var acl, acllen = co:postv("acl")
   134    135   		var subj, subjlen = co:postv("subject")
   135    136   		if text == nil or acl == nil then
   136    137   			co:complain(405, 'invalid post', 'every post must have at least body text and an ACL')
................................................................................
   138    139   		end
   139    140   		if subj == nil then subj = '' end
   140    141   
   141    142   		var p = lib.store.post {
   142    143   			author = co.who.id, acl = acl;
   143    144   			body = text, subject = subj;
   144    145   		}
   145         -		var newid = co.srv:post_create(&p)
          146  +		var newid = p:publish(co.srv)
   146    147   
   147    148   		var idbuf: int8[lib.math.shorthand.maxlen]
   148    149   		var idlen = lib.math.shorthand.gen(newid, idbuf)
   149    150   		var redirto: lib.str.acc redirto:compose('/post/',{idbuf,idlen}) defer redirto:free()
   150    151   		co:reroute(redirto.buf)
   151    152   	end
   152    153   end
................................................................................
   181    182   	if path.ct == 3 then
   182    183   		var lnk: lib.str.acc lnk:compose('/post/', path(1))
   183    184   		var lnkp = lnk:finalize() defer lnkp:free()
   184    185   		if post(0).author ~= co.who.id then
   185    186   			co:complain(403, 'forbidden', 'you cannot alter other people\'s posts')
   186    187   			return
   187    188   		elseif path(2):cmp(lib.str.lit 'edit') then
   188         -			if meth == method.get then
          189  +			if meth_get(meth) then
   189    190   				lib.render.compose(co, post.ptr, nil)
   190    191   				return
   191    192   			elseif meth == method.post then
   192    193   				var newbody = co:postv('post')._0
   193    194   				var newacl = co:postv('acl')._0
   194    195   				var newsubj = co:postv('subject')._0
   195    196   				if newbody ~= nil then post(0).body = newbody end
................................................................................
   196    197   				if newacl  ~= nil then post(0).acl = newacl end
   197    198   				if newsubj ~= nil then post(0).subject = newsubj end
   198    199   				post(0):save(true)
   199    200   				co:reroute(lnkp.ptr)
   200    201   			end
   201    202   			return
   202    203   		elseif path(2):cmp(lib.str.lit 'del') then
   203         -			if meth == method.get then
          204  +			if meth_get(meth) then
   204    205   				var conf = data.view.confirm {
   205    206   					title = lib.str.plit 'delete post';
   206    207   					query = lib.str.plit 'are you sure you want to delete this post?';
   207    208   					cancel = lnkp
   208    209   				}
   209    210   				var body = conf:tostr() defer body:free()
   210    211   				co:stdpage([lib.srv.convo.page] {
................................................................................
   220    221   					co:reroute('/') -- TODO maybe return to parent or conversation if possible
   221    222   					return
   222    223   				else goto badop end
   223    224   			end
   224    225   		else goto badurl end
   225    226   	end
   226    227   
   227         -	if meth == method.post then goto badop end
          228  +	if meth == method.post then
          229  +		var replytext = co:ppostv('post')
          230  +		var acl = co:ppostv('acl')
          231  +		var subj = co:ppostv('subject')
          232  +		if not acl then acl = lib.str.plit 'all' end
          233  +		if not replytext then goto badop end
          234  +		
          235  +		var reply = lib.store.post {
          236  +			author = co.who.id, parent = pid;
          237  +			subject = subj.ptr, acl = acl.ptr, body = replytext.ptr;
          238  +		}
          239  +
          240  +		reply:publish(co.srv)
          241  +	end
   228    242   
   229    243   	lib.render.tweet_page(co, path, post.ptr)
   230    244   	do return end
   231    245   
   232    246   	::badurl:: do co:complain(404, 'invalid URL', 'this URL does not reference extant content or functionality') return end
   233    247   	::badop :: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
   234    248   end
................................................................................
   240    254   		if path(1):cmp(lib.str.lit 'profile') then
   241    255   			lib.dbg('updating profile')
   242    256   			co.who.bio = co:postv('bio')._0
   243    257   			co.who.nym = co:postv('nym')._0
   244    258   			if co.who.bio ~= nil and @co.who.bio == 0 then co.who.bio = nil end
   245    259   			if co.who.nym ~= nil and @co.who.nym == 0 then co.who.nym = nil end
   246    260   			co.who.source:actor_save(co.who)
          261  +
          262  +			var act = co:ppostv('act')
          263  +			var resethue = false
          264  +			if act:ref() then
          265  +				resethue = act:cmp(lib.str.plit 'reset-hue')
          266  +			end
          267  +
          268  +			if not resethue then
          269  +				var shue = co:ppostv('hue')
          270  +				var nhue, okhue = lib.math.decparse(shue)
          271  +				if okhue and nhue ~= co.ui_hue then
          272  +					if nhue == co.srv.cfg.ui_hue
          273  +						then resethue = true
          274  +						else co.srv:actor_conf_int_set(co.who.id, 'ui-accent', nhue)
          275  +					end
          276  +					co.ui_hue = nhue
          277  +				end
          278  +			end
          279  +			if resethue then
          280  +				co.srv:actor_conf_int_reset(co.who.id, 'ui-accent')
          281  +				co.ui_hue = co.srv.cfg.ui_hue
          282  +			end
          283  +
   247    284   			msg = lib.str.plit 'profile changes saved'
   248    285   			--user_refresh = true -- not really necessary here, actually
   249    286   		elseif path(1):cmp(lib.str.lit 'srv') then
   250    287   			if not co.who.rights.powers.config() then goto nopriv end
   251    288   		elseif path(1):cmp(lib.str.lit 'brand') then
   252    289   			if not co.who.rights.powers.rebrand() then goto nopriv end
   253    290   		elseif path(1):cmp(lib.str.lit 'users') then
................................................................................
   322    359   
   323    360   -- entry points
   324    361   terra r.dispatch_http(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t)
   325    362   	lib.dbg('handling URI of form ', {uri.ptr,uri.ct})
   326    363   	co.navbar = lib.render.nav(co)
   327    364   	-- some routes are non-hierarchical, and can be resolved with a simple strcmp
   328    365   	-- we run through those first before giving up and parsing the URI
   329         -	if uri.ptr[0] ~= @'/' then
          366  +	if uri.ptr == nil or uri.ptr[0] ~= @'/' then
   330    367   		co:complain(404, 'what the hell', 'how did you do that')
   331         -		return
   332    368   	elseif uri.ct == 1 then -- root
   333    369   		if (co.srv.cfg.pol_sec == lib.srv.secmode.private or
   334    370   		   co.srv.cfg.pol_sec == lib.srv.secmode.lockdown) and co.aid == 0 then
   335    371   		   http.login_form(co, meth)
   336         -		else
   337         -			-- FIXME display home screen
   338         -			http.timeline(co, hpath {ptr=nil})
   339         -			goto notfound
   340         -		end
   341         -		return
          372  +		else http.timeline(co, hpath {ptr=nil}) end
   342    373   	elseif uri.ptr[1] == @'@' then
   343    374   		http.actor_profile_xid(co, uri, meth)
   344         -		return
   345    375   	elseif uri.ptr[1] == @'s' and uri.ptr[2] == @'/' and uri.ct > 3 then
   346         -		if meth ~= method.get then goto wrongmeth end
          376  +		if not meth_get(meth) then goto wrongmeth end
   347    377   		if not http.static_content(co, uri.ptr + 3, uri.ct - 3) then goto notfound end
   348         -		return
   349    378   	elseif lib.str.ncmp('/avi/', uri.ptr, 5) == 0 then
   350    379   		http.local_avatar(co, [lib.mem.ptr(int8)] {ptr = uri.ptr + 5, ct = uri.ct - 5})
   351         -		return
   352    380   	elseif lib.str.ncmp('/compose', uri.ptr, lib.math.biggest(uri.ct,8)) == 0 then
   353    381   		if co.aid == 0 then co:reroute('/login') return end
   354    382   		http.post_compose(co,meth)
   355         -		return
   356    383   	elseif lib.str.ncmp('/login', uri.ptr, lib.math.biggest(uri.ct,6)) == 0 then
   357    384   		if co.aid == 0
   358    385   			then http.login_form(co, meth)
   359    386   			else co:reroute('/')
   360    387   		end
   361         -		return
   362    388   	elseif lib.str.ncmp('/logout', uri.ptr, lib.math.biggest(uri.ct,7)) == 0 then
   363    389   		if co.aid == 0
   364    390   			then goto notfound
   365    391   			else co:reroute_cookie('/','auth=; Path=/')
   366    392   		end
   367         -		return
   368    393   	else -- hierarchical routes
   369    394   		var path = lib.http.hier(uri) defer path:free()
   370    395   		if path.ct > 1 and path(0):cmp(lib.str.lit('user')) then
   371    396   			http.actor_profile_uid(co, path, meth)
   372    397   		elseif path.ct > 1 and path(0):cmp(lib.str.lit('post')) then
   373    398   			http.tweet_page(co, path, meth)
   374    399   		elseif path(0):cmp(lib.str.lit('tl')) then
   375    400   			http.timeline(co, path)
   376    401   		elseif path(0):cmp(lib.str.lit('doc')) then
   377         -			if meth ~= method.get and meth ~= method.head then goto wrongmeth end
          402  +			if not meth_get(meth) then goto wrongmeth end
   378    403   			http.documentation(co, path)
   379    404   		elseif path(0):cmp(lib.str.lit('conf')) then
   380    405   			if co.aid == 0 then goto unauth end
   381    406   			http.configure(co,path,meth)
   382    407   		else goto notfound end
   383         -		return
   384    408   	end
          409  +	do return end
   385    410   
   386    411   	::wrongmeth:: co:complain(405, 'method not allowed', 'that method is not meaningful for this endpoint') do return end
   387    412   	::notfound:: co:complain(404, 'not found', 'no such resource available') do return end
   388    413   	::unauth:: co:complain(401, 'unauthorized', 'this content is not available at your clearance level') do return end
   389    414   end

Modified srv.t from [9e0d1b7489] to [675eda18a7].

     7      7   	secret: lib.mem.ptr(int8)
     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  +	ui_hue: uint16
    14     15   }
    15     16   local struct srv {
    16     17   	sources: lib.mem.ptr(lib.store.source)
    17     18   	webmgr: lib.net.mg_mgr
    18     19   	webcon: &lib.net.mg_connection
    19     20   	cfg: cfgcache
    20     21   	id: rawstring
................................................................................
   104    105   					if [ok] then break
   105    106   						else r = empty end
   106    107   				end
   107    108   			end
   108    109   		in r end
   109    110   	end
   110    111   end)
          112  +
          113  +terra lib.store.post:publish(s: &srv)
          114  +	self:comp()
          115  +	self.posted = lib.osclock.time(nil)
          116  +	self.discovered = self.posted
          117  +	self.chgcount = 0
          118  +	self.edited = 0
          119  +	self.id = s:post_create(self)
          120  +	return self.id
          121  +end
   111    122   
   112    123   local struct convo {
   113    124   	srv: &srv
   114    125   	con: &lib.net.mg_connection
   115    126   	msg: &lib.net.mg_http_message
   116    127   	aid: uint64 -- 0 if logged out
   117    128   	aid_issue: lib.store.timepoint
   118    129   	who: &lib.store.actor -- who we're logged in as, if aid ~= 0
   119    130   	peer: lib.store.inet
   120    131   	reqtype: lib.http.mime.t -- negotiated content type
          132  +	method: lib.http.method.t
          133  +	live_last: lib.store.timepoint
   121    134   -- cache
          135  +	ui_hue: uint16
   122    136   	navbar: lib.mem.ptr(int8)
   123    137   	actorcache: lib.mem.cache(lib.mem.ptr(lib.store.actor),32) -- naive cache to avoid unnecessary queries
   124    138   -- private
   125    139   	varbuf: lib.mem.ptr(int8)
   126    140   	vbofs: &int8
   127    141   }
          142  +
          143  +struct convo.page {
          144  +	title: pstring
          145  +	body: pstring
          146  +	class: pstring
          147  +	cache: bool
          148  +}
   128    149   
   129    150   -- this is unfortunately necessary to work around a terra bug
   130    151   -- it can't seem to handle forward-declarations of structs in C
   131    152   
   132    153   local getpeer
   133    154   do local struct strucheader {
   134    155   		next: &lib.net.mg_connection
................................................................................
   136    157   		peer: lib.net.mg_addr
   137    158   	}
   138    159   	terra getpeer(con: &lib.net.mg_connection)
   139    160   		return [&strucheader](con).peer
   140    161   	end
   141    162   end
   142    163   
          164  +terra convo:rawpage(code: uint16, pg: convo.page, hdrs: lib.mem.ptr(lib.http.header))
          165  +	var doc = data.view.docskel {
          166  +		instance = self.srv.cfg.instance;
          167  +		title = pg.title;
          168  +		body = pg.body;
          169  +		class = pg.class;
          170  +		navlinks = self.navbar;
          171  +		attr = '';
          172  +	}
          173  +	var attrbuf: int8[32]
          174  +	if self.aid ~= 0 and self.ui_hue ~= 323 then
          175  +		var hdecbuf: int8[21]
          176  +		var hdec = lib.math.decstr(self.ui_hue, &hdecbuf[20])
          177  +		lib.str.cpy(&attrbuf[0], ' style="--hue:')
          178  +		lib.str.cpy(&attrbuf[14], hdec)
          179  +		var len = &hdecbuf[20] - hdec 
          180  +		lib.str.cpy(&attrbuf[14] + len, '"')
          181  +		doc.attr = &attrbuf[0]
          182  +	end
          183  +
          184  +	if self.method == [lib.http.method.head]
          185  +		then doc:head(self.con,code,hdrs)
          186  +		else doc:send(self.con,code,hdrs)
          187  +	end
          188  +end
          189  +
          190  +terra convo:statpage(code: uint16, pg: convo.page)
          191  +	var hdrs = array(
          192  +		lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' },
          193  +		lib.http.header { key = 'Cache-Control', value = 'no-store' }
          194  +	)
          195  +	self:rawpage(code,pg, [lib.mem.ptr(lib.http.header)] {
          196  +		ptr = &hdrs[0];
          197  +		ct = [hdrs.type.N] - lib.trn(pg.cache,1,0);
          198  +	})
          199  +end
          200  +
          201  +terra convo:livepage(pg: convo.page, lastup: lib.store.timepoint)
          202  +	var nbuf: int8[21]
          203  +	var hdrs = array(
          204  +		lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' },
          205  +		lib.http.header { key = 'Cache-Control', value = 'no-store' },
          206  +		lib.http.header {
          207  +			key = 'X-Live-Newest-Artifact';
          208  +			value = lib.math.decstr(lastup, &nbuf[20]);
          209  +		},
          210  +		lib.http.header { key = 'Content-Length', value = '0' }
          211  +	)
          212  +	if self.live_last ~= 0 and self.live_last <= lastup then
          213  +		lib.net.mg_printf(self.con, 'HTTP/1.1 %s', lib.http.codestr(200))
          214  +		for i = 0, [hdrs.type.N] do
          215  +			lib.net.mg_printf(self.con, '%s: %s\r\n', hdrs[i].key, hdrs[i].value)
          216  +		end
          217  +		lib.net.mg_printf(self.con, '\r\n')
          218  +	else
          219  +		self:rawpage(200, pg, [lib.mem.ptr(lib.http.header)] {
          220  +			ptr = &hdrs[0], ct = 3
          221  +		})
          222  +	end
          223  +end
          224  +
          225  +terra convo:stdpage(pg: convo.page) self:statpage(200, pg) end
          226  +
   143    227   terra convo:reroute_cookie(dest: rawstring, cookie: rawstring)
   144    228   	var hdrs = array(
   145    229   		lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' },
   146    230   		lib.http.header { key = 'Location',     value = dest },
   147    231   		lib.http.header { key = 'Set-Cookie',   value = cookie }
   148    232   	)
   149    233   
   150    234   	var body = data.view.docskel {
   151    235   		instance = self.srv.cfg.instance.ptr;
   152    236   		title = 'rerouting';
   153    237   		body = 'you are being redirected';
   154    238   		class = 'error';
   155    239   		navlinks = '';
          240  +		attr = '';
   156    241   	}
   157    242   
   158    243   	body:send(self.con, 303, [lib.mem.ptr(lib.http.header)] {
   159    244   		ptr = &hdrs[0], ct = [hdrs.type.N] - lib.trn(cookie == nil,1,0)
   160    245   	})
   161    246   end
   162    247   
................................................................................
   170    255   		lib.dbg('sending cookie ',{&sesskey[0],15})
   171    256   		p = lib.str.ncpy(p, '; Path=/', 9)
   172    257   	end
   173    258   	self:reroute_cookie(dest, &sesskey[0])
   174    259   end
   175    260    
   176    261   terra convo:complain(code: uint16, title: rawstring, msg: rawstring)
   177         -	var hdrs = array(
   178         -		lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' },
   179         -		lib.http.header { key = 'Cache-Control', value = 'no-store' }
   180         -	)
          262  +	if msg == nil then msg = "i'm sorry, dave. i can't let you do that" end
   181    263   
   182    264   	var ti: lib.str.acc ti:compose('error :: ', title)
   183    265   	var bo: lib.str.acc bo:compose('<div class="message"><img class="icon" src="/s/warn.webp"><h1>',title,'</h1><p>',msg,'</p></div>')
   184         -	var body = data.view.docskel {
   185         -		instance = self.srv.cfg.instance;
          266  +	var body = [convo.page] {
   186    267   		title = ti:finalize();
   187    268   		body = bo:finalize();
   188    269   		class = lib.str.plit 'error';
   189         -		navlinks = lib.coalesce(self.navbar, [lib.mem.ptr(int8)]{ptr='',ct=0});
          270  +		cache = false;
   190    271   	}
   191    272   
   192         -	if body.body.ptr == nil then
   193         -		body.body = lib.str.plit"i'm sorry, dave. i can't let you do that"
   194         -	end
   195         -
   196         -	body:send(self.con, code, [lib.mem.ptr(lib.http.header)] {
   197         -		ptr = &hdrs[0], ct = [hdrs.type.N]
   198         -	})
          273  +	self:statpage(code, body)
   199    274   
   200    275   	body.title:free()
   201    276   	body.body:free()
   202    277   end
   203    278   
   204    279   convo.methods.assertpow = macro(function(self, pow)
   205    280   	return quote
................................................................................
   207    282   		if self.aid == 0 or self.who.rights.powers.[pow:asvalue()]() == false then
   208    283   			ok = false
   209    284   			self:complain(403,'insufficient privileges',['you lack the <strong>'..pow:asvalue()..'</strong> power and cannot perform this action'])
   210    285   		end
   211    286   	in ok end
   212    287   end)
   213    288   
   214         -struct convo.page {
   215         -	title: pstring
   216         -	body: pstring
   217         -	class: pstring
   218         -	cache: bool
   219         -}
   220         -
   221         -terra convo:stdpage(pg: convo.page)
   222         -	var doc = data.view.docskel {
   223         -		instance = self.srv.cfg.instance;
   224         -		title = pg.title;
   225         -		body = pg.body;
   226         -		class = pg.class;
   227         -		navlinks = self.navbar;
   228         -	}
   229         -
   230         -	var hdrs = array(
   231         -		lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' },
   232         -		lib.http.header { key = 'Cache-Control', value = 'no-store' }
   233         -	)
   234         -
   235         -	doc:send(self.con,200,[lib.mem.ptr(lib.http.header)] {ct = [hdrs.type.N] - lib.trn(pg.cache,1,0), ptr = &hdrs[0]})
   236         -end
   237         -
   238    289   -- CALL ONLY ONCE PER VAR
   239    290   terra convo:postv(name: rawstring)
   240    291   	if self.varbuf.ptr == nil then
   241    292   		self.varbuf = lib.mem.heapa(int8, self.msg.body.len + self.msg.query.len)
   242    293   		self.vbofs = self.varbuf.ptr
   243    294   	end
   244    295   	var o = lib.net.mg_http_get_var(&self.msg.body, name, self.vbofs, self.varbuf.ct - (self.vbofs - self.varbuf.ptr))
................................................................................
   299    350   		if lib.str.ncmp(mimevar.ptr, mime, lib.math.biggest(mimevar.ct, [#mime])) == 0 then
   300    351   			ret = [lib.http.mime[name]]
   301    352   		else ret = [mimeneg] end
   302    353   	in ret end
   303    354   end
   304    355   
   305    356   local handle = {
   306         -	http = terra(con: &lib.net.mg_connection, event: int, p: &opaque, ext: &opaque)
   307         -		var server = [&srv](ext)
          357  +	http = terra(con: &lib.net.mg_connection, event_kind: int, event: &opaque, userdata: &opaque)
          358  +		var server = [&srv](userdata)
   308    359   		var mgpeer = getpeer(con)
   309    360   		var peer = lib.store.inet { port = mgpeer.port; }
   310    361   		if mgpeer.is_ip6 then peer.pv = 6 else peer.pv = 4 end
   311    362   		if peer.pv == 6 then
   312    363   			for i = 0, 16 do peer.v6[i] = mgpeer.ip6[i] end
   313    364   		else -- v4
   314    365   			@[&uint32](&peer.v4) = mgpeer.ip
................................................................................
   319    370   		-- for now i'm leaving it as is, but note that netmask restrictions
   320    371   		-- WILL NOT WORK until upstream gets its shit together. FIXME
   321    372   
   322    373   		-- needs to check for an X-Forwarded-For header from nginx and
   323    374   		-- use that instead of the peer iff peer is ::1/127.1 FIXME
   324    375   		-- maybe also haproxy support?
   325    376   
   326         -		switch event do
          377  +		switch event_kind do
   327    378   			case lib.net.MG_EV_HTTP_MSG then
   328    379   				lib.dbg('routing HTTP request')
   329         -				var msg = [&lib.net.mg_http_message](p)
          380  +				var msg = [&lib.net.mg_http_message](event)
   330    381   				var co = convo {
   331    382   					con = con, srv = server, msg = msg;
   332    383   					aid = 0, aid_issue = 0, who = nil;
   333    384   					reqtype = lib.http.mime.none;
   334         -					peer = peer;
          385  +					peer = peer, live_last = 0;
   335    386   				} co.varbuf.ptr = nil
   336    387   				  co.navbar.ptr = nil
   337    388   				  co.actorcache.top = 0
   338    389   				  co.actorcache.cur = 0
          390  +				  co.ui_hue = server.cfg.ui_hue
   339    391   
   340    392   				-- first, check for an accept header. if it's there, we need to
   341    393   				-- iterate over the values and pick the highest-priority one
   342    394   				do var acc = lib.http.findheader(msg, 'Accept')
   343    395   					-- TODO handle q-value
   344         -					if acc.ptr ~= nil then
          396  +					if acc ~= nil and acc.ptr ~= nil then
   345    397   						var [mimevar] = [lib.mem.ref(int8)] { ptr = acc.ptr }
   346    398   						var i = 0 while i < acc.ct do
   347    399   							if acc.ptr[i] == @',' or acc.ptr[i] == @';' then
   348    400   								mimevar.ct = (acc.ptr+i) - mimevar.ptr
   349    401   								var t = [mimeneg]
   350    402   								if t ~= lib.http.mime.none then
   351    403   									co.reqtype = t
................................................................................
   377    429   					else co.reqtype = lib.http.mime.html end
   378    430   				::foundtype::end
   379    431   
   380    432   				-- we need to check if there's any cookies sent with the request,
   381    433   				-- and if so, whether they contain any credentials. this will be
   382    434   				-- used to set the auth parameters in the http conversation
   383    435   				var cookies_p = lib.http.findheader(msg, 'Cookie')
   384         -				if cookies_p ~= nil then
          436  +				if cookies_p ~= nil and cookies_p.ptr ~= nil then
   385    437   					var cookies = cookies_p.ptr
   386    438   					var key = [lib.mem.ref(int8)] {ptr = cookies, ct = 0}
   387    439   					var val = [lib.mem.ref(int8)] {ptr = nil, ct = 0}
   388    440   					var i = 0 while i < cookies_p.ct    and
   389    441   					                cookies[i] ~= 0     and
   390    442   					                cookies[i] ~= @'\r' and
   391    443   									cookies[i] ~= @'\n' do -- cover all the bases
................................................................................
   423    475   				end
   424    476   
   425    477   				if co.aid ~= 0 then
   426    478   					var sess, usr = co.srv:actor_session_fetch(co.aid, peer, co.aid_issue)
   427    479   					if sess.ok == false then co.aid = 0 co.aid_issue = 0 else
   428    480   						co.who = usr.ptr
   429    481   						co.who.rights.powers = server:actor_powers_fetch(co.who.id)
          482  +						var userhue, hueok = server:actor_conf_int_get(co.who.id, 'ui-accent')
          483  +						if hueok then co.ui_hue = userhue end
   430    484   					end
   431    485   				end
          486  +
          487  +				var livelast_p = lib.http.findheader(msg, 'X-Live-Last-Arrival')
          488  +				if livelast_p ~= nil and livelast_p.ptr ~= nil then
          489  +					var ll, ok = lib.math.decparse(pstring{ptr = livelast_p.ptr, ct = livelast_p.ct - 1})
          490  +					if ok then co.live_last = ll end
          491  +				end
          492  +
   432    493   
   433    494   				var uridec = lib.mem.heapa(int8, msg.uri.len) defer uridec:free()
   434    495   				var urideclen = lib.net.mg_url_decode(msg.uri.ptr, msg.uri.len, uridec.ptr, uridec.ct, 1)
   435    496   
   436    497   				var uri = uridec
   437    498   				if urideclen == -1 then
   438    499   					for i = 0,msg.uri.len do
................................................................................
   442    503   						end
   443    504   					end
   444    505   					uri.ct = msg.uri.len
   445    506   				else uri.ct = urideclen end
   446    507   				lib.dbg('routing URI ', {uri.ptr, uri.ct})
   447    508   				
   448    509   				if lib.str.ncmp('GET', msg.method.ptr, msg.method.len) == 0 then
          510  +					co.method = [lib.http.method.get]
   449    511   					route.dispatch_http(&co, uri, [lib.http.method.get])
   450    512   				elseif lib.str.ncmp('POST', msg.method.ptr, msg.method.len) == 0 then
          513  +					co.method = [lib.http.method.get]
   451    514   					route.dispatch_http(&co, uri, [lib.http.method.post])
   452    515   				elseif lib.str.ncmp('HEAD', msg.method.ptr, msg.method.len) == 0 then
          516  +					co.method = [lib.http.method.head]
   453    517   					route.dispatch_http(&co, uri, [lib.http.method.head])
   454    518   				elseif lib.str.ncmp('OPTIONS', msg.method.ptr, msg.method.len) == 0 then
          519  +					co.method = [lib.http.method.options]
   455    520   					route.dispatch_http(&co, uri, [lib.http.method.options])
   456    521   				else
   457    522   					co:complain(400,'unknown method','you have submitted an invalid http request')
   458    523   				end
   459    524   
   460    525   				if co.aid ~= 0 then lib.mem.heapf(co.who) end
   461    526   				if co.varbuf.ptr ~= nil then co.varbuf:free() end
................................................................................
   670    735   	do self.pol_reg = false
   671    736   	var sreg = self.overlord:conf_get('policy-self-register')
   672    737   	if sreg:ref() then
   673    738   		if lib.str.cmp(sreg.ptr, 'on') == 0
   674    739   			then self.pol_reg = true
   675    740   			else self.pol_reg = false
   676    741   		end
   677         -	end
   678         -	sreg:free() end
          742  +		sreg:free()
          743  +	end end
   679    744   
   680    745   	do self.credmgd = false
   681    746   	var sreg = self.overlord:conf_get('credential-store')
   682    747   	if sreg:ref() then
   683    748   		if lib.str.cmp(sreg.ptr, 'managed') == 0
   684    749   			then self.credmgd = true
   685    750   			else self.credmgd = false
   686    751   		end
   687         -	end
   688         -	sreg:free() end
          752  +		sreg:free()
          753  +	end end
   689    754   
   690    755   	do self.maxupsz = [1024 * 100] -- 100 kilobyte default
   691    756   	var sreg = self.overlord:conf_get('maximum-artifact-size')
   692    757   	if sreg:ref() then
   693    758   		var sz, ok = lib.math.fsz_parse(sreg)
   694    759   		if ok then self.maxupsz = sz else
   695    760   			lib.warn('invalid configuration value for maximum-artifact-size; keeping default 100K upload limit')
   696    761   		end
          762  +		sreg:free() end
   697    763   	end
   698         -	sreg:free() end
   699    764   	
   700    765   	self.pol_sec = secmode.lockdown
   701    766   	var smode = self.overlord:conf_get('policy-security')
   702    767   	if smode.ptr ~= nil then
   703    768   		if lib.str.cmp(smode.ptr, 'public') == 0 then
   704    769   			self.pol_sec = secmode.public
   705    770   		elseif lib.str.cmp(smode.ptr, 'private') == 0 then
   706    771   			self.pol_sec = secmode.private
   707    772   		elseif lib.str.cmp(smode.ptr, 'lockdown') == 0 then
   708    773   			self.pol_sec = secmode.lockdown
   709    774   		elseif lib.str.cmp(smode.ptr, 'isolate') == 0 then
   710    775   			self.pol_sec = secmode.isolate
   711    776   		end
          777  +		smode:free()
   712    778   	end
   713         -	smode:free()
          779  +
          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
   714    787   end
   715    788   
   716    789   return {
   717    790   	overlord = srv;
   718    791   	convo = convo;
   719    792   	route = route;
   720    793   	secmode = secmode;
   721    794   }

Added static/live.js version [6fb4c9ec70].

            1  +/* first things first, we need to scan over the document and see
            2  + * if there are any UI elements unfortunate enough to need
            3  + * interactivity beyond what native HTML+CSS can provide. if so,
            4  + * we attach the appropriate listeners to them. */
            5  +window.addEventListener('load', function() {
            6  +	/* update hue-picker background when slider is adjusted */
            7  +	document.querySelectorAll('.color-picker').forEach(function(box) {
            8  +		let slider = box.querySelector('[data-color-pick]');
            9  +		box.style.setProperty('--hue', slider.value);
           10  +		slider.addEventListener('input', function(e) {
           11  +			box.style.setProperty('--hue', e.target.value);
           12  +		});
           13  +	});
           14  +
           15  +	/* the main purpose of this script -- by marking itself with the
           16  +	 * data-live property, an html element registers itself for live
           17  +	 * updates from the server. this is pretty straightforward: we
           18  +	 * retrieve this url from the server as a get request, create a
           19  +	 * tree from its html, find the element in question, ferret out
           20  +	 * any deltas, and apply them. */
           21  +	document.querySelectorAll('*[data-live]').forEach(function(container) {
           22  +		let interv = parseFloat(container.attributes.getNamedItem('data-live').nodeValue) * 1000;
           23  +		container._liveLastArrival = '0'; /* TODO include header for this */
           24  +
           25  +		window.setInterval(function() {
           26  +			var req = new Request(window.location, {
           27  +				method: 'GET',
           28  +				headers: {
           29  +					'X-Live-Last-Arrival': container._liveLastArrival
           30  +				}
           31  +			})
           32  +			
           33  +			fetch(req).then(function(resp) {
           34  +				if (!resp.ok) return;
           35  +				let newest = resp.headers.get('X-Live-Newest-Artifact');
           36  +				if (newest <= container._liveLastArrival) {
           37  +					resp.body.cancel();
           38  +					return;
           39  +				}
           40  +				container._liveLastArrival = newest
           41  +
           42  +				resp.text().then(function(htmlbody) {
           43  +					var parser = new DOMParser();
           44  +					var newdoc = parser.parseFromString(htmlbody,'text/html')
           45  +					// console.log(newdoc.getElementById(container.id).innerHTML)
           46  +					container.innerHTML = newdoc.getElementById(container.id).innerHTML
           47  +				})
           48  +			})
           49  +		}, interv)
           50  +	});
           51  +});

Modified static/style.scss from [ada3763759] to [0e6b10a9e2].

     1         -$color: hsl(323,100%,65%);
            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         -@function tone($pct, $alpha: 0) { @return adjust-color($color, $lightness: $pct, $alpha: $alpha) }
            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     10   
           11  +:root { --hue: 323; }
     8     12   body {
     9     13   	@extend %sans;
    10     14   	background-color: tone(-55%);
    11     15   	color: tone(25%);
    12     16   	font-size: 14pt;
    13     17   	margin: 0;
    14     18   	padding: 0;
................................................................................
    20     24   ::placeholder {
    21     25   	color: tone(0,-0.3);
    22     26   	font-style: italic;
    23     27   }
    24     28   a[href] {
    25     29   	color: tone(10%);
    26     30   	text-decoration-color: tone(10%,-0.5);
    27         -	&:hover {
           31  +	&:hover, &:focus {
    28     32   		color: white;
    29     33   		text-shadow: 0 0 15px tone(20%);
    30     34   		text-decoration-color: tone(10%,-0.1);
           35  +		outline: none;
    31     36   	}
    32     37   	&.button { @extend %button; }
    33     38   }
    34     39   a[href^="//"],
    35     40   a[href^="http://"],
    36     41   a[href^="https://"] { // external link
    37     42   	&:hover::after {
................................................................................
    69     74   	border: 1px solid black;
    70     75   	color: tone(25%);
    71     76   	text-shadow: 1px 1px black;
    72     77   	text-decoration: none;
    73     78   	text-align: center;
    74     79   	cursor: default;
    75     80   	user-select: none;
           81  +	-webkit-user-drag: none;
           82  +	-webkit-app-region: no-drag;
    76     83   	background: linear-gradient(to bottom,
    77     84   		tone(-47%),
    78     85   		tone(-50%) 15%,
    79     86   		tone(-50%) 75%,
    80     87   		tone(-53%)
    81     88   	);
    82     89   	&:hover, &:focus {
................................................................................
   206    213   			> a[href] {
   207    214   				display: block;
   208    215   				padding: 0.25in 0.10in;
   209    216   				//padding: calc((25% - 1em)/2) 0.15in;
   210    217   				&, &::after { transition: 0.3s; }
   211    218   				text-shadow: 1px 1px 1px black;
   212    219   				&:hover{
   213         -					transform: scale(120%);
          220  +					transform: scale(1.2);
   214    221   				}
   215    222   			}
   216    223   		}
   217    224   	}
   218    225   }
   219    226   
   220    227   main {
................................................................................
   341    348   	@extend %box;
   342    349   	display: block;
   343    350   	width: 4in;
   344    351   	margin:auto;
   345    352   	padding: 0.5in;
   346    353   	text-align: center;
   347    354   	menu:first-of-type { margin-top: 0.3in; }
          355  +	img.icon { width: 1.875in; height: 1.875in; }
   348    356   }
   349    357   
   350    358   div.login {
   351    359   	@extend %box;
   352    360   	width: 4in;
   353    361   	padding: 0.4in;
   354    362   	> .msg {
................................................................................
   458    466   	font-size: 1.5ex !important;
   459    467   	letter-spacing: 1.3px;
   460    468   	padding-bottom: 3px;
   461    469   	border-radius: 2px;
   462    470   	vertical-align: baseline;
   463    471   	box-shadow: 1px 1px 1px black;
   464    472   }
          473  +
          474  +div.thread {
          475  +	margin-left: 0.3in;
          476  +	& + div.post { margin-top: 0.3in; }
          477  +}
   465    478   
   466    479   div.post {
   467    480   	@extend %box;
   468    481   	display: grid;
   469    482   	grid-template-columns: 1in 1fr max-content;
   470    483   	grid-template-rows: min-content max-content;
   471    484   	margin-bottom: 0.1in;
   472    485   	>.avatar {
   473    486   		grid-column: 1/2; grid-row: 1/2;
   474         -		img { display: block; width: 1in; margin:0; }
          487  +		img { display: block; width: 1in; height: 1in; margin:0; }
   475    488   		background: linear-gradient(to bottom, tone(-53%), tone(-57%));
   476    489   	}
   477    490   	>a[href].username {
   478    491   		display: block;
   479    492   		grid-column: 1/3;
   480    493   		grid-row: 2/3;
   481    494   		text-align: left;
................................................................................
   494    507   	}
   495    508   	>.content {
   496    509   		grid-column: 2/4; grid-row: 1/2;
   497    510   		padding: 0.2in;
   498    511   		@extend %serif;
   499    512   		font-size: 110%;
   500    513   		text-align: justify;
          514  +		color: tone(25%);
   501    515   	}
   502    516   	> a[href].permalink {
   503    517   		display: block;
   504    518   		grid-column: 3/4; grid-row: 2/3;
   505    519   		font-size: 80%;
   506    520   		text-align: right;
   507    521   		padding: 0.1in;
................................................................................
   523    537   	h1, h2, h3, h4, h5, h6 {
   524    538   		background: linear-gradient(to right, tone(-50%), transparent);
   525    539   		margin-left: -0.4in;
   526    540   		padding-left: 0.2in;
   527    541   		text-shadow: 0 2px 0 black;
   528    542   	}
   529    543   }
          544  +
          545  +%navmenu, body.profile main > menu {
          546  +	margin-left: -0.25in;
          547  +	grid-column: 1/2; grid-row: 1/2;
          548  +	background: linear-gradient(to bottom, tone(-45%),tone(-55%));
          549  +	border: 1px solid black;
          550  +	padding: 0.1in;
          551  +	> a[href] {
          552  +		@extend %button;
          553  +		display: block;
          554  +		text-align: left;
          555  +	}
          556  +	> a[href] + a[href] {
          557  +		border-top: none;
          558  +	}
          559  +	hr {
          560  +		border: none;
          561  +	}
          562  +}
   530    563   
   531    564   menu { all: unset; display: block; }
   532    565   body.conf main {
   533    566   	display: grid;
   534    567   	grid-template-columns: 2in 1fr;
   535    568   	grid-template-rows: max-content 1fr;
   536         -	> menu {
   537         -		margin-left: -0.25in;
   538         -		grid-column: 1/2; grid-row: 1/2;
   539         -		background: linear-gradient(to bottom, tone(-45%),tone(-55%));
   540         -		border: 1px solid black;
   541         -		padding: 0.1in;
   542         -		> a[href] {
   543         -			@extend %button;
   544         -			display: block;
   545         -			text-align: left;
   546         -		}
   547         -		> a[href] + a[href] {
   548         -			border-top: none;
   549         -		}
   550         -		hr {
   551         -			border: none;
   552         -		}
   553         -	}
          569  +	> menu { @extend %navmenu; }
   554    570   	> .panel {
   555    571   		grid-column: 2/3; grid-row: 1/3;
   556    572   		padding-left: 0.15in;
   557    573   		> h1 {
   558    574   			padding-bottom: 0.1in;
   559    575   			margin-bottom: 0.1in;
   560    576   			margin-left: -0.15in;
................................................................................
   610    626   	&.vertical-float {
   611    627   		flex-flow: column;
   612    628   		float: right;
   613    629   		width: 40%;
   614    630   		margin-left: 0.1in;
   615    631   	}
   616    632   	> %button {
   617         -		flex-basis: 0;
          633  +		flex-basis: min-content;
   618    634   		flex-grow: 1;
   619    635   		display: block; margin: 2px;
   620    636   	}
   621    637   }
   622    638   
   623    639   .check-panel {
   624    640   	display: flex;
................................................................................
   664    680   	100% { opacity: 0; transform: scale(0.9) translateY(-0.12in); display: none; }
   665    681   }
   666    682   .flashmsg {
   667    683   	display: block;
   668    684   	position: fixed;
   669    685   	top: 1.3in;
   670    686   	max-width: 3in;
   671         -	padding: 0.5in 0.2in;
          687  +	padding: 0.4in 0.2in;
   672    688   	left: 0; right: 0;
   673    689   	text-align: center;
   674    690   	text-shadow: 0 0 15px tone(10%);
   675    691   	margin: auto;
   676    692   	background: linear-gradient(to bottom, tone(-49%), tone(-43%,-0.1));
   677    693   	border: 1px solid tone(0%);
   678    694   	border-radius: 3px;
   679    695   	box-shadow: 0 0 50px tone(-55%);
   680    696   	color: white;
   681    697   	animation: ease forwards flashup;
   682    698   	//cubic-bezier(0.4, 0.63, 0.6, 0.31)
   683         -	animation-duration: 3s;
          699  +	animation-duration: 2.5s;
   684    700   }
   685    701   
   686    702   form.action-bar {
   687    703   	display: flex;
   688    704   	> * {
   689    705   		flex-grow: 1;
   690    706   		flex-basis: 0;
   691    707   		margin-left: 0.1in;
   692    708   	}
   693    709   	> *:first-child {
   694    710   		margin-left: 0;
   695    711   	}
   696    712   }
          713  +
          714  +.color-picker {
          715  +	/* implemented using javascript, alas */
          716  +	@extend %box;
          717  +	label { text-shadow: 1px 1px black; }
          718  +	padding: 0.1in;
          719  +}

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

   156    156   	chgcount: uint
   157    157   	mentions: lib.mem.ptr(uint64)
   158    158   	circles: lib.mem.ptr(uint64) --only meaningful if scope is set to circle
   159    159   	convoheaduri: str
   160    160   	parent: uint64
   161    161   -- ephemera
   162    162   	localpost: bool
          163  +	accent: int16
          164  +	depth: uint16 -- used in conversations to indicate tree depth
   163    165   	source: &m.source
   164    166   
   165    167   	-- save :: bool -> {} (defined in acl.t due to dep. hell)
   166    168   }
   167    169   
   168         -local cnf = terralib.memoize(function(ty,rty)
          170  +m.user_conf_funcs = function(be,n,ty,rty,rty2)
   169    171   	rty = rty or ty
   170         -	return struct {
   171         -		enum: {&opaque, uint64, rawstring} -> intptr
   172         -		get: {&opaque, uint64, rawstring} -> rty
   173         -		set: {&opaque, uint64, rawstring, ty} -> {}
   174         -		reset: {&opaque, uint64, rawstring} -> {}
   175         -	}
   176         -end)
          172  +	local gt
          173  +	if not rty2 -- what the fuck?
          174  +		then gt = {&m.source, uint64, rawstring} -> rty;
          175  +		else gt = {&m.source, uint64, rawstring} -> {rty, rty2};
          176  +	end
          177  +	for k, t in pairs {
          178  +		enum = {&m.source, uint64, rawstring} -> lib.mem.ptr(rty);
          179  +		get = gt;
          180  +		set = {&m.source, uint64, rawstring, ty} -> {};
          181  +		reset = {&m.source, uint64, rawstring} -> {};
          182  +	} do
          183  +		be.entries[#be.entries+1] = {
          184  +			field = 'actor_conf_'..n..'_'..k, type = t
          185  +		}
          186  +	end
          187  +end
   177    188   
   178    189   struct m.notif {
   179    190   	kind: m.notiftype.t
   180    191   	when: uint64
   181    192   	union {
   182    193   		post: uint64
   183         -		reaction: int8[8]
          194  +		reaction: int8[16]
   184    195   	}
   185    196   }
   186    197   
   187    198   struct m.inet {
   188    199   	pv: uint8 -- 0 = null, 4 = ipv4, 6 = ipv6
   189    200   	union {
   190    201   		v4: uint8[4]
................................................................................
   232    243   	context: str
   233    244   }
   234    245   
   235    246   struct m.auth {
   236    247   -- a credential record
   237    248   	aid: uint64
   238    249   	uid: uint64
          250  +	kind: str
   239    251   	aname: str
          252  +	comment: str
   240    253   	netmask: m.inet
   241    254   	privs: m.privset
   242    255   	blacklist: bool
   243    256   }
   244    257   
   245    258   struct m.relationship {
   246    259   	agent: uint64
................................................................................
   313    326   			-- cookie issue time: m.timepoint
   314    327   	actor_auth_register_uid: {&m.source, uint64, uint64} -> {}
   315    328   		-- notifies the backend module of the UID that has been assigned for
   316    329   		-- an authentication ID
   317    330   			-- aid: uint64
   318    331   			-- uid: uint64
   319    332   
   320         -	actor_conf_str: cnf(rawstring, lib.mem.ptr(int8))
   321         -	actor_conf_int: cnf(intptr, lib.stat(intptr))
   322         -
   323         -	auth_create_pw: {&m.source, uint64, bool, lib.mem.ptr(int8)} -> {}
          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))
          335  +	auth_attach_pw: {&m.source, uint64, bool, pstr, pstr} -> {}
   324    336   		-- uid: uint64
   325    337   		-- reset: bool (delete other passwords?)
   326    338   		-- pw: pstring
          339  +		-- comment: pstring
   327    340   	auth_purge_pw: {&m.source, uint64, rawstring} -> {}
   328    341   	auth_purge_otp: {&m.source, uint64, rawstring} -> {}
   329    342   	auth_purge_trust: {&m.source, uint64, rawstring} -> {}
   330    343   	auth_sigtime_user_fetch: {&m.source, uint64} -> m.timepoint
   331    344   		-- authentication tokens and accounts have a property that controls
   332    345   		-- whether auth cookies dated to a certain point are valid. cookies
   333    346   		-- that are generated before the timepoint are considered invalid.
................................................................................
   338    351   			-- timestamp: timepoint
   339    352   
   340    353   	post_save: {&m.source, &m.post} -> {}
   341    354   	post_create: {&m.source, &m.post} -> uint64
   342    355   	post_destroy: {&m.source, uint64} -> {}
   343    356   	post_fetch: {&m.source, uint64} -> lib.mem.ptr(m.post)
   344    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))
   345    359   	post_attach_ctl: {&m.source, uint64, uint64, bool} -> {}
   346    360   		-- attaches or detaches an existing database artifact
   347    361   			-- post id: uint64
   348    362   			-- artifact id: uint64
   349    363   			-- detach: bool
          364  +
          365  +	thread_latest_arrival_calc: {&m.source, uint64} -> m.timepoint
          366  +
   350    367   	artifact_instantiate: {&m.source, lib.mem.ptr(uint8), lib.mem.ptr(int8)} -> uint64
   351    368   		-- instantiate an artifact in the database, either installing a new
   352    369   		-- artifact or returning the id of an existing artifact with the same hash
   353    370   			-- artifact: bytea
   354    371   			-- mime:     pstring
   355    372   	artifact_quicksearch: {&m.source, lib.mem.ptr(uint8)} -> {uint64,bool}
   356    373   		-- checks whether a hash is already in the database without uploading
................................................................................
   385    402   			-- proto: kompromat (null for all records, or a prototype describing the records to return)
   386    403   	nkvd_sanction_issue:  {&m.source, &m.sanction} -> uint64
   387    404   	nkvd_sanction_vacate: {&m.source, uint64} -> {}
   388    405   	nkvd_sanction_enum_target: {&m.source, uint64} -> {}
   389    406   	nkvd_sanction_enum_issuer: {&m.source, uint64} -> {}
   390    407   	nkvd_sanction_review: {&m.source, m.timepoint} -> {}
   391    408   
   392         -	convo_fetch_xid: {&m.source,rawstring} -> lib.mem.ptr(m.post)
   393         -	convo_fetch_cid: {&m.source,uint64} -> lib.mem.ptr(m.post)
   394         -
   395    409   	timeline_actor_fetch_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post))
   396    410   	timeline_instance_fetch: {&m.source, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post))
   397    411   }
          412  +
          413  +m.user_conf_funcs(m.backend, 'str', rawstring, lib.mem.ptr(int8))
          414  +m.user_conf_funcs(m.backend, 'int', intptr, intptr, bool)
   398    415   
   399    416   struct m.source {
   400    417   	backend: &m.backend
   401    418   	id: lib.mem.ptr(int8)
   402    419   	handle: &opaque
   403    420   	string: lib.mem.ptr(int8)
   404    421   }

Modified tpl.t from [682e534236] to [9f68e11c52].

   156    156   	rec.methods.append = terra([symself], [accumulator])
   157    157   		lib.dbg(['appending template ' .. tid])
   158    158   		[tallyup]
   159    159   		accumulator:cue([runningtally])
   160    160   		[appenders]
   161    161   		return accumulator
   162    162   	end
   163         -	rec.methods.send = terra([symself], [destcon], code: uint16, hd: lib.mem.ptr(lib.http.header))
   164         -		lib.dbg(['transmitting template ' .. tid])
          163  +	rec.methods.head = terra([symself], [destcon], code: uint16, hd: lib.mem.ptr(lib.http.header))
          164  +		lib.dbg(['transmitting template headers ' .. tid])
   165    165   		[tallyup]
   166    166   		lib.net.mg_printf([destcon], 'HTTP/1.1 %s', lib.http.codestr(code))
   167    167   		for i = 0, hd.ct do
   168    168   			lib.net.mg_printf([destcon], '%s: %s\r\n', hd.ptr[i].key, hd.ptr[i].value)
   169    169   		end
   170    170   		lib.net.mg_printf([destcon],'Content-Length: %llu\r\n\r\n', [runningtally] + 1)
          171  +	end
          172  +	rec.methods.send = terra([symself], [destcon], code: uint16, hd: lib.mem.ptr(lib.http.header))
          173  +		lib.dbg(['transmitting template ' .. tid])
          174  +
          175  +		symself:head(destcon,code,hd)
          176  +
   171    177   		[senders]
   172    178   		lib.net.mg_send([destcon],'\r\n',2)
          179  +	end
          180  +	rec.methods.sz = terra([symself])
          181  +		lib.dbg(['tallying template ' .. tid])
          182  +		[tallyup]
          183  +		return [runningtally] + 1
   173    184   	end
   174    185   
   175    186   	return rec
   176    187   end
   177    188   
   178    189   return m

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

     1      1   <form method="post">
     2      2   	<div class="elem"><label>handle</label> <div class="txtbox">@!handle</div></div>
     3      3   	<div class="elem"><label for="nym">display name</label> <input type="text" name="nym" id="nym" placeholder="j. random poster" value="@:nym"></div>
     4      4   	<div class="elem"><label for="bio">bio</label><textarea name="bio" id="bio" placeholder="tall, dark, and mysterious">@!bio</textarea></div>
     5         -	<button>commit</button>
            5  +	<div class="elem color-picker"><label for="hue">accent</label><input type="range" min="0" max="360" value="@hue" name="hue" id="hue" data-color-pick></div>
            6  +	<menu class="choice vertical">
            7  +		<button>commit all</button>
            8  +		<button name="act" value="reset-hue">use server colors</button>
            9  +	</menu> 
     6     10   </form>

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

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

Modified view/docskel.tpl from [cb4a31dcc6] to [3229efd171].

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

Added view/media.tpl version [5a68c18a8e].

            1  +<menu>
            2  +	<a href="/user/@:xid/media">new uploads</a>
            3  +	@folders
            4  +</menu>
            5  +
            6  +<div name="gallery">
            7  +	@images
            8  +</div>
            9  +
           10  +<div name="files">
           11  +	@files
           12  +</div>

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

    11     11   		<tr><th>following</th> <td>@nfollows</td></tr>
    12     12   		<tr><th>followers</th> <td>@nfollowers</td></tr>
    13     13   		<tr><th>mutuals</th> <td>@nmutuals</td></tr>
    14     14   		<tr><th>@timephrase</th> <td>@tweetday</td></tr>
    15     15   	</table>
    16     16   	<form class="actions">
    17     17   		<a class="button" href="/@:xid">posts</a>
           18  +		<a class="button" href="/@:xid/arc">archive</a>
    18     19   		<a class="button" href="/@:xid/media">media</a>
    19     20   		<a class="button" href="/@:xid/social">associates</a>
    20     21   		<hr>
    21     22   		@auxbtn
    22     23   	</form>
    23     24   </div>

Added view/tweet-mini.tpl version [4c5364412a].

            1  +<div class="post-mini">
            2  +	<span class="permalink">[<a href="@permalink">@when</a>]</span>
            3  +	<span class="username">‹<a href="/@:acctlink">@nym</a>›</span> <span class="content">@text</span>
            4  +</div>

Modified view/tweet.tpl from [806c88c01c] to [e117899db4].

     1         -<div class="post">
            1  +<div class="post"@attr>
     2      2   	<div class="avatar"><img src="@:avatar"></div>
     3      3   	<a class="username" href="/@:acctlink">@nym</a>
     4      4   	<div class="content">
     5      5   		<div class="subject">@!subject</div>
     6      6   		<div class="text">@text</div>
     7      7   	</div>
     8      8   	<a class="permalink" href="@permalink">@when</a>
     9      9   </div>