parsav  Check-in [0324d62546]

Overview
Comment:continued iteration
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 0324d625467b68d51407a9ab6432c30e71eed46459b9cd666a5355f14b585143
User & Date: lexi on 2020-12-30 00:43:11
Other Links: manifest | tags
Context
2020-12-30
02:44
enable profile editing check-in: ac4a630ad5 user: lexi tags: trunk
00:43
continued iteration check-in: 0324d62546 user: lexi tags: trunk
2020-12-29
15:48
enable remote control of running instances check-in: f8816b0ab5 user: lexi tags: trunk
Changes

Modified backend/pgsql.t from [0f1425913d] to [30de1dd276].

   131    131   				select relatee as user from parsav_rels
   132    132   					where relator = $1::bigint and kind = <follow>
   133    133   			),
   134    134   			followers as (
   135    135   				select relator as user from parsav_rels
   136    136   					where relatee = $1::bigint and kind = <follow>
   137    137   			),
   138         -			mutuals as (select * from follows intersect select * from followers)
          138  +			mutuals as (
          139  +				select * from follows  intersect  select * from followers
          140  +			)
   139    141   
   140         -			select count(tweets.*)::bigint,
   141         -			       count(follows.*)::bigint,
   142         -				   count(followers.*)::bigint,
   143         -				   count(mutuals.*)::bigint
   144         -			from tweets, follows, followers, mutuals
          142  +			values (
          143  +				(select count(tweets.*)::bigint from tweets),
          144  +				(select count(follows.*)::bigint from follows),
          145  +				(select count(followers.*)::bigint from followers),
          146  +				(select count(mutuals.*)::bigint from mutuals)
          147  +			)
   145    148   		]]):gsub('<(%w+)>',function(r) return tostring(lib.store.relation[r]) end)
   146    149   	};
   147    150   
   148    151   	actor_auth_how = {
   149    152   		params = {rawstring, lib.store.inet}, sql = [[
   150    153   		with mts as (select a.kind from parsav_auth as a
   151    154   			left join parsav_actors as u on u.id = a.uid
................................................................................
   278    281   				(a.origin is null)
   279    282   			order by (p.posted, p.discovered) desc
   280    283   			limit case when $3::bigint = 0 then null
   281    284   			           else $3::bigint end
   282    285   			offset $4::bigint
   283    286   		]]
   284    287   	};
          288  +
          289  +	artifact_instantiate = {
          290  +		params = {binblob, binblob, pstring}, sql = [[
          291  +			insert into parsav_artifacts (content,hash,mime) values (
          292  +				$1::bytea, $2::bytea, $3::text
          293  +			) on conflict do nothing returning id
          294  +		]];
          295  +	};
          296  +	artifact_expropriate = {
          297  +		params = {uint64, uint64, pstring}, cmd = true, sql = [[
          298  +			insert into parsav_artifact_claims (uid,rid,description,folder) values (
          299  +				$1::bigint, $2::bigint, $3::text, 'new'
          300  +			) on conflict do nothing
          301  +		]];
          302  +	};
          303  +	artifact_quicksearch = {
          304  +		params = {binblob}, sql = [[
          305  +			select id, (content is null) from parsav_artifacts where hash = $1::bytea
          306  +				limit 1
          307  +		]];
          308  +	};
          309  +	artifact_disclaim = {
          310  +		params = {uint64, uint64}, cmd = true, sql = [[
          311  +			delete from parsav_artifact_claims where
          312  +				uid = $1::bigint and
          313  +				rid = $2::bigint
          314  +		]];
          315  +	};
          316  +	artifact_excise_forget = {
          317  +		-- delete the blasted thing and pretend it never existed
          318  +		params = {uint64}, cmd=true, sql = [[
          319  +			delete from parsav_artifacts where id = $1::bigint
          320  +		]];
          321  +	};
          322  +	artifact_excise_suppress_nullify = {
          323  +		-- banish the thing into the outer darkness, preventing
          324  +		-- it from ever being admitted into our databases, and
          325  +		-- tabulate a -- list of the degenerates who befouled
          326  +		-- their accounts with such wanton and execrable filth,
          327  +		-- the better to ensure their long-overdue punishment
          328  +		params = {uint64}, cmd=true, sql = [[
          329  +			update parsav_artifacts
          330  +				set content = null
          331  +				where id = $1::bigint;
          332  +		]];
          333  +	};
          334  +	artifact_excise_suppress_breaklinks = {
          335  +	-- "ERROR:  cannot insert multiple commands into a prepared
          336  +	--  statement" are you fucking shitting me with this shit
          337  +		params = {uint64}, sql = [[
          338  +			delete from parsav_artifact_claims where
          339  +				rid = $1::bigint
          340  +			returning uid, description, birth, folder;
          341  +		]];
          342  +	};
          343  +	post_attach_ctl_ins = {
          344  +		params = {uint64, uint64}, cmd=true, sql = [[
          345  +			update parsav_posts set
          346  +				artifacts = artifacts || $2::bigint
          347  +			where id = $1::bigint and not
          348  +				artifacts @> array[$2::bigint]
          349  +		]];
          350  +	};
          351  +	post_attach_ctl_del = {
          352  +		params = {uint64, uint64}, cmd=true, sql = [[
          353  +			update parsav_posts set
          354  +				artifacts = array_remove(artifacts, $2::bigint)
          355  +			where id = $1::bigint and
          356  +				artifacts @> array[$2::bigint]
          357  +		]];
          358  +	};
   285    359   }
   286         -				--($5::bool = false or p.parent is null) and
   287    360   
   288    361   local struct pqr {
   289    362   	sz: intptr
   290    363   	res: &lib.pq.PGresult
   291    364   }
   292    365   terra pqr:free() if self.sz > 0 then lib.pq.PQclear(self.res) end end
   293    366   terra pqr:null(row: intptr, col: intptr)
................................................................................
   685    758   			lib.report('successfully wiped out everything parsav-related in database')
   686    759   			return true
   687    760   		else
   688    761   			lib.warn('backend pgsql - failed to obliterate database: \n', lib.pq.PQresultErrorMessage(res))
   689    762   			return false
   690    763   		end
   691    764   	end];
          765  +
          766  +	tx_enter = [terra(src: &lib.store.source)
          767  +		var res = lib.pq.PQexec([&lib.pq.PGconn](src.handle), 'begin')
          768  +		if lib.pq.PQresultStatus(res) == lib.pq.PGRES_COMMAND_OK then
          769  +			lib.dbg('beginning pgsql transaction')
          770  +			return true
          771  +		else
          772  +			lib.warn('backend pgsql - failed to begin transaction: \n', lib.pq.PQresultErrorMessage(res))
          773  +			return false
          774  +		end
          775  +	end];
          776  +
          777  +	tx_complete = [terra(src: &lib.store.source)
          778  +		var res = lib.pq.PQexec([&lib.pq.PGconn](src.handle), 'end')
          779  +		if lib.pq.PQresultStatus(res) == lib.pq.PGRES_COMMAND_OK then
          780  +			lib.dbg('completing pgsql transaction')
          781  +			return true
          782  +		else
          783  +			lib.warn('backend pgsql - failed to complete transaction: \n', lib.pq.PQresultErrorMessage(res))
          784  +			return false
          785  +		end
          786  +	end];
   692    787   
   693    788   	conf_get = [terra(src: &lib.store.source, key: rawstring)
   694    789   		var r = queries.conf_get.exec(src, key)
   695    790   		if r.sz == 0 then return [lib.mem.ptr(int8)] { ptr = nil, ct = 0 } else
   696    791   			defer r:free()
   697    792   			return r:String(0,0)
   698    793   		end
................................................................................
   905   1000   		queries.auth_purge_type.exec(src, handle, uid, 'otp-%')
   906   1001   	end];
   907   1002   
   908   1003   	auth_purge_trust = [terra(src: &lib.store.source, uid: uint64, handle: rawstring): {}
   909   1004   		queries.auth_purge_type.exec(src, handle, uid, 'trust')
   910   1005   	end];
   911   1006   
   912         -	actor_auth_register_uid = nil; -- not necessary for view-based auth
         1007  +	artifact_quicksearch = [terra(
         1008  +		src: &lib.store.source,
         1009  +		hash: binblob
         1010  +	): {uint64, bool}
         1011  +		var srec = queries.artifact_quicksearch.exec(src, hash)
         1012  +		if srec.sz > 0 then
         1013  +			defer srec:free()
         1014  +			var id = srec:int(uint64,0,0)
         1015  +			var ban = srec:bool(0,1)
         1016  +			return id, ban
         1017  +		else return 0, false end
         1018  +	end];
         1019  +
         1020  +	artifact_instantiate = [terra(
         1021  +		src: &lib.store.source,
         1022  +		artifact: binblob,
         1023  +		mime: pstring
         1024  +	): uint64
         1025  +		var arthash: uint8[lib.crypt.algsz.sha256]
         1026  +		if lib.md.mbedtls_md(lib.md.mbedtls_md_info_from_type(lib.crypt.alg.sha256.id),
         1027  +			artifact.ptr, artifact.ct, &arthash[0]) ~= 0 then
         1028  +			lib.bail('could not hash artifact to be instantiated')
         1029  +		end
         1030  +		var hashb = binblob{ptr=&arthash[0],ct=[arthash.type.N]}
         1031  +
         1032  +		var srec = queries.artifact_quicksearch.exec(src, hashb)
         1033  +		if srec.sz > 0 then
         1034  +			defer srec:free()
         1035  +			var ban = srec:bool(0,1)
         1036  +			if ban then
         1037  +				lib.report('user attempted to instantiate forsaken artifact')
         1038  +				return 0
         1039  +			end
         1040  +			var oldid = srec:int(uint64,0,0)
         1041  +			return oldid
         1042  +		else -- not in db, insert
         1043  +			var nrec = queries.artifact_instantiate.exec(src, artifact, hashb, mime)
         1044  +			if nrec.sz == 0 then
         1045  +				lib.warn('failed to instantiate artifact -- are you running out of storage?')
         1046  +				return 0
         1047  +			else defer nrec:free()
         1048  +				var newid = nrec:int(uint64,0,0)
         1049  +				return newid
         1050  +			end
         1051  +		end
         1052  +	end];
         1053  +
         1054  +	post_attach_ctl = [terra(
         1055  +		src: &lib.store.source,
         1056  +		post: uint64,
         1057  +		artifact: uint64,
         1058  +		detach: bool
         1059  +	): {}
         1060  +		if detach
         1061  +			then queries.post_attach_ctl_del.exec(src,post,artifact)
         1062  +			else queries.post_attach_ctl_ins.exec(src,post,artifact)
         1063  +		end
         1064  +	end];
   913   1065   
         1066  +	actor_auth_register_uid = nil; -- TODO better support non-view based auth
   914   1067   }
   915   1068   
   916   1069   return b

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

     5      5   drop table if exists parsav_actors cascade;
     6      6   drop table if exists parsav_rights cascade;
     7      7   drop table if exists parsav_posts cascade;
     8      8   drop table if exists parsav_conversations cascade;
     9      9   drop table if exists parsav_rels cascade;
    10     10   drop table if exists parsav_acts cascade;
    11     11   drop table if exists parsav_log cascade;
    12         -drop table if exists parsav_attach cascade;
           12  +drop table if exists parsav_artifacts cascade;
           13  +drop table if exists parsav_artifact_claims cascade;
    13     14   drop table if exists parsav_circles cascade;
    14     15   drop table if exists parsav_rooms cascade;
    15     16   drop table if exists parsav_room_members cascade;
    16     17   drop table if exists parsav_invites cascade;
    17         -drop table if exists parsav_interventions cascade;
           18  +drop table if exists parsav_sanctions cascade;
    18     19   drop table if exists parsav_auth cascade;

Modified backend/schema/pgsql.sql from [0ef43163b5] to [cb277a93b1].

    26     26   	id        bigint primary key default (1+random()*(2^63-1))::bigint,
    27     27   	nym       text,
    28     28   	handle    text not null, -- nym [@handle@origin] 
    29     29   	origin    bigint references parsav_servers(id)
    30     30   		on delete cascade, -- null origin = local actor
    31     31   	knownsince timestamp,
    32     32   	bio       text,
           33  +	avatarid  bigint, -- artifact id, null if remote
    33     34   	avataruri text, -- null if local
    34     35   	rank      smallint not null default 0,
    35     36   	quota     integer not null default 1000,
    36     37   	key       bytea, -- private if localactor; public if remote
    37     38   	epithet   text,
    38     39   	authtime  timestamp not null default now(), -- cookies earlier than this timepoint will not be accepted
    39     40   	
................................................................................
    55     56   	author     bigint references parsav_actors(id)
    56     57   		on delete cascade,
    57     58   	subject    text,
    58     59   	acl        text not null default 'all', -- just store the script raw 🤷
    59     60   	body       text,
    60     61   	posted     timestamp not null,
    61     62   	discovered timestamp not null,
    62         -	parent     bigint not null default 0,
           63  +	parent     bigint not null default 0, -- if post: part of conversation; if chatroom: top-level post
    63     64   	circles    bigint[], -- TODO at edit or creation, iterate through each circle
    64     65   	mentions   bigint[], -- a user has, check if it can see her post, and if so add
           66  +	artifacts  bigint[],
    65     67   
    66     68   	convoheaduri text
    67     69   	-- only used for tracking foreign conversations and tying them to post heads;
    68     70   	-- local conversations are tracked directly and mapped to URIs based on the
    69     71   	-- head's ID. null if native tweet or not the first tweet in convo
    70     72   );
    71     73   
................................................................................
    93     95   	id    bigint primary key default (1+random()*(2^63-1))::bigint,
    94     96   	time  timestamp not null default now(),
    95     97   	actor bigint references parsav_actors(id)
    96     98   		on delete cascade,
    97     99   	post  bigint not null
    98    100   );
    99    101   
   100         -create table parsav_attach (
          102  +create table parsav_artifacts (
   101    103   	id          bigint primary key default (1+random()*(2^63-1))::bigint,
   102    104   	birth       timestamp not null default now(),
   103         -	content     bytea not null,
   104         -	mime        text, -- null if unknown, will be reported as x-octet-stream
          105  +	content     bytea, -- if null, this is a "ban record" preventing content matching the hash from being re-uploaded
          106  +	hash		bytea unique not null, -- sha256 hash of content
          107  +	-- it would be cool to use a computed column for this, but i don't want
          108  +	-- to lock people into PG12 or drag in the pgcrypto extension just for this
          109  +	mime        text -- null if unknown, will be reported as x-octet-stream
          110  +);
          111  +create index on parsav_artifacts (mime);
          112  +
          113  +create table parsav_artifact_claims (
          114  +	birth timestamp not null default now(),
          115  +	uid bigint references parsav_actors(id) on delete cascade,
          116  +	rid bigint references parsav_artifacts(id) on delete cascade,
   105    117   	description text,
   106         -	parent      bigint -- post id, or userid for avatars
          118  +	folder text,
          119  +
          120  +	unique (uid,rid)
   107    121   );
          122  +create index on parsav_artifact_claims (uid);
   108    123   
   109    124   create table parsav_circles (
   110    125   	id          bigint primary key default (1+random()*(2^63-1))::bigint,
   111         -	owner       bigint not null references parsav_actors(id),
          126  +	owner       bigint not null references parsav_actors(id) on delete cascade,
   112    127   	name        text not null,
   113    128   	members     bigint[] not null default array[]::bigint[],
   114    129   
   115    130   	unique (owner,name)
   116    131   );
   117    132   
   118    133   create table parsav_rooms (
   119    134   	id          bigint primary key default (1+random()*(2^63-1))::bigint,
   120         -	origin		bigint references parsav_servers(id),
          135  +	origin		bigint references parsav_servers(id) on delete cascade,
   121    136   	name		text not null,
   122    137   	description text not null,
   123    138   	policy      smallint not null
   124    139   );
   125    140   
   126    141   create table parsav_room_members (
   127         -	room   bigint references parsav_rooms(id),
   128         -	member bigint references parsav_actors(id),
          142  +	room   bigint not null references parsav_rooms(id) on delete cascade,
          143  +	member bigint not null references parsav_actors(id) on delete cascade,
   129    144   	rank   smallint not null default 0,
   130    145   	admin  boolean not null default false, -- non-admins with rank can only moderate + invite
   131    146   	title  text, -- admin-granted title like reddit flair
   132    147   	vouchedby bigint references parsav_actors(id)
   133    148   );
   134    149   
   135    150   create table parsav_invites (
   136    151   	id          bigint primary key default (1+random()*(2^63-1))::bigint,
   137    152   	-- when a user is created from an invite, the invite is deleted and the invite
   138    153   	-- ID becomes the user ID. privileges granted on the invite ID during the invite
   139    154   	-- process are thus inherited by the user
   140         -	issuer bigint references parsav_actors(id),
          155  +	issuer bigint references parsav_actors(id) on delete set null,
   141    156   	handle text, -- admin can lock invite to specific handle
   142    157   	rank   smallint not null default 0,
   143    158   	quota  integer not null  default 1000
   144    159   );
   145    160   
   146         -create table parsav_interventions (
          161  +create table parsav_sanctions (
   147    162   	id     bigint primary key default (1+random()*(2^63-1))::bigint,
   148         -	issuer bigint references parsav_actors(id) not null,
          163  +	issuer bigint references parsav_actors(id) on delete set null,
   149    164   	scope  bigint, -- can be null or room for local actions
   150         -	nature smallint not null, -- silence, suspend, disemvowel, etc
   151         -	victim bigint not null, -- could potentially target group as well
   152         -	expire timestamp -- auto-expires if set
          165  +	nature smallint not null, -- silence, suspend, disemvowel, censor, noreply, etc
          166  +	victim bigint not null, -- can be user, room, or post
          167  +	expire timestamp, -- auto-expires if set
          168  +	review timestamp,  -- brings up for review at given time if set
          169  +	reason text, -- visible to victim if set
          170  +	context text -- admin-only note
   153    171   );
   154    172   
   155    173   -- create a temporary managed auth table; we can delete this later
   156    174   -- if it ends up being replaced with a view
   157    175   %include pgsql-auth.sql%

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

    67     67   		local fn = f.field or f[1]
    68     68   		local ft = f.type or f[2]
    69     69   		if fn == meth then rt = ft.type.returntype break end
    70     70   	end
    71     71   
    72     72   	return quote
    73     73   		var r: rt
    74         -		if self.all
           74  +		if self.all or (self.srv ~= nil and self.srv.sources.ct == 1)
    75     75   			then r=self.srv:[meth]([expr])
    76     76   			elseif self.src ~= nil then r=self.src:[meth]([expr])
    77     77   			else lib.bail('no data source specified')
    78     78   		end
    79     79   	in r end
    80     80   end)
    81     81   
................................................................................
   271    271   				return 1
   272    272   			end
   273    273   			if dbmode.arglist.ct < 1 then goto cmderr end
   274    274   
   275    275   			srv:setup(cnf) 
   276    276   			if lib.str.cmp(dbmode.arglist(0),'init') == 0 and dbmode.arglist.ct == 2 then
   277    277   				lib.report('initializing new database structure for domain ', dbmode.arglist(1))
   278         -				dlg:dbsetup()
   279         -				srv:conprep(lib.store.prepmode.conf)
   280         -				dlg:conf_set('instance-name', dbmode.arglist(1))
   281         -				do var sec: int8[65] gensec(&sec[0])
   282         -					dlg:conf_set('server-secret', &sec[0])
   283         -				end
   284         -				lib.report('database setup complete; use mkroot to create an administrative user')
          278  +				if dlg:dbsetup() then
          279  +					srv:conprep(lib.store.prepmode.conf)
          280  +					dlg:conf_set('instance-name', dbmode.arglist(1))
          281  +					do var sec: int8[65] gensec(&sec[0])
          282  +						dlg:conf_set('server-secret', &sec[0])
          283  +					end
          284  +					lib.report('database setup complete; use mkroot to create an administrative user')
          285  +				else lib.bail('initialization process interrupted') end
   285    286   			elseif lib.str.cmp(dbmode.arglist(0),'obliterate') == 0 then
   286    287   				var confirmstrs = array(
   287         -					'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'eta', 'nu', 'kappa'
          288  +					'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'eta', 'nu', 'kappa',
          289  +					'emerald', 'carnelian', 'sapphire', 'ruby', 'amethyst'
   288    290   				)
   289    291   				var cfmstr: int8[64] cfmstr[0] = 0
   290    292   				var tdx = lib.osclock.time(nil) / 60
   291    293   				for i=0,3 do
   292    294   					if i ~= 0 then lib.str.cat(&cfmstr[0], '-') end
   293    295   					lib.str.cat(&cfmstr[0], confirmstrs[(tdx ^ (173*i)) % [confirmstrs.type.N]])
   294    296   				end

Modified render/conf.t from [6e08f785f6] to [79b6da76d7].

    62     62   		panel = panel;
    63     63   	}
    64     64   
    65     65   	var pgt = pg:tostr() defer pgt:free()
    66     66   	co:stdpage([lib.srv.convo.page] {
    67     67   		title = 'configure'; body = pgt;
    68     68   		class = lib.str.plit 'conf';
           69  +		cache = false;
    69     70   	})
    70     71   
    71     72   	if panel.ct ~= 0 then panel:free() end
    72     73   end
    73     74   
    74     75   return render_conf

Modified render/docpage.t from [7e1168b387] to [3eda6110a6].

    44     44   		parent = par;
    45     45   		priv = restrict;
    46     46   		title = R(t.meta.title);
    47     47   		content = page {
    48     48   			title = ['documentation :: ' .. t.meta.title];
    49     49   			body = [ t.text ];
    50     50   			class = P'doc article';
           51  +			cache = true;
    51     52   		};
    52     53   	} end
    53     54   end
    54     55   
    55     56   local terra 
    56     57   showpage(co: &lib.srv.convo, id: pref)
    57     58   	var [pages] = array([allpages])
................................................................................
   107    108   		list:lpush('</ul>')
   108    109   
   109    110   		var bp = list:finalize()
   110    111   		co:stdpage(page {
   111    112   			title = 'documentation';
   112    113   			body = bp;
   113    114   			class = P'doc listing';
          115  +			cache = false;
   114    116   		})
   115    117   		bp:free()
   116    118   	else showpage(co, pg) end
   117    119   end
   118    120   
   119    121   return render_docpage

Modified render/login.t from [dd5c50c3e9] to [7ea4ccf2b4].

     1      1   -- vim: ft=terra
     2      2   local pstr = lib.mem.ptr(int8)
     3      3   local P = lib.str.plit
     4      4   local terra 
     5      5   login_form(co: &lib.srv.convo, user: &lib.store.actor, creds: &lib.store.credset, msg: pstr)
     6         -	var doc = data.view.docskel {
     7         -		instance = co.srv.cfg.instance;
            6  +	var doc = [lib.srv.convo.page] {
     8      7   		title = lib.str.plit 'instance logon';
     9      8   		class = lib.str.plit 'login';
    10         -		navlinks = co.navbar;
            9  +		cache = false;
    11     10   	}
    12     11   
    13     12   	if user == nil then
    14     13   		var form = data.view.login_username {
    15     14   			loginmsg = msg;
    16     15   		}
    17     16   		if form.loginmsg.ptr == nil then
................................................................................
    52     51   		end
    53     52   
    54     53   		doc.body = ch:tostr()
    55     54   	else
    56     55   		-- pick a method
    57     56   	end
    58     57   
    59         -	var hdrs = array(
    60         -		lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' }
    61         -	)
    62         -	doc:send(co.con,200,[lib.mem.ptr(lib.http.header)] {ct = 1, ptr = &hdrs[0]})
           58  +	co:stdpage(doc)
    63     59   	doc.body:free()
    64     60   end
    65     61   
    66     62   return login_form

Modified render/nav.t from [27572ae99b] to [4a737bb7ff].

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

Modified render/nym.t from [89e574dd98] to [0d2437aadd].

     1      1   -- vim: ft=terra
     2      2   local pstr = lib.mem.ptr(int8)
            3  +local terra cs(s: rawstring)
            4  +	return pstr { ptr = s, ct = lib.str.sz(s) }
            5  +end
     3      6   
     4      7   local terra 
     5      8   render_nym(who: &lib.store.actor, scope: uint64)
     6      9   	var n: lib.str.acc n:init(128)
           10  +	var xidsan = lib.html.sanitize(cs(who.xid),false)
     7     11   	if who.nym ~= nil and who.nym[0] ~= 0 then
     8         -		n:compose('<span class="nym">',who.nym,'</span> [<span class="handle">',
     9         -			who.xid,'</span>]')
    10         -	else n:compose('<span class="handle">',who.xid,'</span>') end
           12  +		var nymsan = lib.html.sanitize(cs(who.nym),false)
           13  +		n:compose('<span class="nym">',nymsan,'</span> [<span class="handle">',
           14  +			xidsan,'</span>]')
           15  +		nymsan:free()
           16  +	else n:compose('<span class="handle">',xidsan,'</span>') end
           17  +	xidsan:free()
    11     18   
    12     19   	if who.epithet ~= nil then
    13         -		n:lpush(' <span class="epithet">'):push(who.epithet,0):lpush('</span>')
           20  +		var episan = lib.html.sanitize(cs(who.epithet),false)
           21  +		n:lpush(' <span class="epithet">'):ppush(episan):lpush('</span>')
           22  +		episan:free()
    14     23   	end
    15     24   	
    16     25   	-- TODO: if scope == chat room then lookup titles in room member db
    17         -
    18     26   	return n:finalize()
    19     27   end
    20     28   
    21     29   return render_nym

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

    29     29   	var acc: lib.str.acc acc:init(1024)
    30     30   	for i = 0, posts.sz do
    31     31   		lib.render.tweet(co, posts(i).ptr, &acc)
    32     32   		posts(i):free()
    33     33   	end
    34     34   	posts:free()
    35     35   
    36         -	var doc = data.view.docskel {
    37         -		instance = co.srv.cfg.instance;
           36  +	var doc = [lib.srv.convo.page] {
    38     37   		title = lib.str.plit'timeline';
    39     38   		body = acc:finalize();
    40     39   		class = lib.str.plit'timeline';
    41         -		navlinks = co.navbar;
           40  +		cache = false;
    42     41   	}
    43         -	var hdrs = array(
    44         -		lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' }
    45         -	)
    46         -	doc:send(co.con,200,[lib.mem.ptr(lib.http.header)] {ct = 1, ptr = &hdrs[0]})
           42  +	co:stdpage(doc)
    47     43   	doc.body:free()
    48     44   end
    49     45   return render_timeline

Modified render/userpage.t from [edce0da550] to [8e478d7a95].

    26     26   	end
    27     27   	posts:free()
    28     28   
    29     29   	var bdf = acc:finalize()
    30     30   	co:stdpage([lib.srv.convo.page] {
    31     31   		title = tiptr; body = bdf;
    32     32   		class = lib.str.plit 'profile';
           33  +		cache = false;
    33     34   	})
    34     35   
    35     36   	tiptr:free()
    36     37   	bdf:free()
    37     38   end
    38     39   
    39     40   return render_userpage

Modified route.t from [4ebb6db558] to [5e16c3f22b].

   115    115   			if aid == 0 then
   116    116   				lib.render.login(co, nil, nil, lib.str.plit 'authentication failure')
   117    117   			else
   118    118   				var sesskey: int8[lib.session.maxlen + #lib.session.cookiename + #"=; Path=/" + 1]
   119    119   				do var p = &sesskey[0]
   120    120   					p = lib.str.ncpy(p, [lib.session.cookiename .. '='], [#lib.session.cookiename + 1])
   121    121   					p = p + lib.session.cookie_gen(co.srv.cfg.secret, aid, lib.osclock.time(nil), p)
   122         -					lib.dbg('sending cookie',&sesskey[0])
          122  +					lib.dbg('sending cookie ',{&sesskey[0],15})
   123    123   					p = lib.str.ncpy(p, '; Path=/', 9)
   124    124   				end
   125    125   				co:reroute_cookie('/', &sesskey[0])
   126    126   			end
   127    127   		end
   128    128   		if act.ptr ~= nil and fakeact == false then act:free() end
   129    129   	else

Modified srv.t from [7727a773dd] to [4721bcd333].

   156    156   		ptr = &hdrs[0], ct = [hdrs.type.N] - lib.trn(cookie == nil,1,0)
   157    157   	})
   158    158   end
   159    159   
   160    160   terra convo:reroute(dest: rawstring) self:reroute_cookie(dest,nil) end
   161    161    
   162    162   terra convo:complain(code: uint16, title: rawstring, msg: rawstring)
   163         -	var hdrs = array(lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' })
          163  +	var hdrs = array(
          164  +		lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' },
          165  +		lib.http.header { key = 'Cache-Control', value = 'no-store' }
          166  +	)
   164    167   
   165    168   	var ti: lib.str.acc ti:compose('error :: ', title)
   166    169   	var bo: lib.str.acc bo:compose('<div class="message"><img class="icon" src="/s/warn.webp"><h1>',title,'</h1><p>',msg,'</p></div>')
   167    170   	var body = data.view.docskel {
   168    171   		instance = self.srv.cfg.instance;
   169    172   		title = ti:finalize();
   170    173   		body = bo:finalize();
................................................................................
   194    197   	in ok end
   195    198   end)
   196    199   
   197    200   struct convo.page {
   198    201   	title: pstring
   199    202   	body: pstring
   200    203   	class: pstring
          204  +	cache: bool
   201    205   }
   202    206   
   203    207   terra convo:stdpage(pg: convo.page)
   204    208   	var doc = data.view.docskel {
   205    209   		instance = self.srv.cfg.instance;
   206    210   		title = pg.title;
   207    211   		body = pg.body;
   208    212   		class = pg.class;
   209    213   		navlinks = self.navbar;
   210    214   	}
   211    215   
   212    216   	var hdrs = array(
   213         -		lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' }
          217  +		lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' },
          218  +		lib.http.header { key = 'Cache-Control', value = 'no-store' }
   214    219   	)
   215    220   
   216         -	doc:send(self.con,200,[lib.mem.ptr(lib.http.header)] {ct = [hdrs.type.N], ptr = &hdrs[0]})
          221  +	doc:send(self.con,200,[lib.mem.ptr(lib.http.header)] {ct = [hdrs.type.N] - lib.trn(pg.cache,1,0), ptr = &hdrs[0]})
   217    222   end
   218    223   
   219    224   -- CALL ONLY ONCE PER VAR
   220    225   terra convo:postv(name: rawstring)
   221    226   	if self.varbuf.ptr == nil then
   222    227   		self.varbuf = lib.mem.heapa(int8, self.msg.body.len + self.msg.query.len)
   223    228   		self.vbofs = self.varbuf.ptr

Modified static/style.scss from [1f479ddac7] to [a08d589c48].

   127    127   	tone(-50%),
   128    128   	tone(-35%)
   129    129   );
   130    130   
   131    131   input[type='text'], input[type='password'], textarea {
   132    132   	@extend %serif;
   133    133   	padding: 0.08in 0.1in;
          134  +	box-sizing: border-box;
   134    135   	border: 1px solid black;
   135    136   	background: linear-gradient(to bottom, tone(-55%), tone(-40%));
   136    137   	font-size: 16pt;
   137    138   	color: tone(25%);
   138    139   	box-shadow: inset 0 0 20px -3px tone(-55%);
   139    140   	&:focus {
   140    141   		color: white;
................................................................................
   362    363   		> a { @extend %button; grid-column: 1 / 2; grid-row: 3/4; }
   363    364   	}
   364    365   }
   365    366   
   366    367   form.compose {
   367    368   	@extend %box;
   368    369   	display: grid;
   369         -	grid-template-columns: 1.1in 2fr min-content 1fr;
          370  +	grid-template-columns: 1.1in 2fr min-content 1fr 1.5fr;
   370    371   	grid-template-rows: 1fr min-content;
   371    372   	grid-gap: 2px;
   372    373   	padding: 0.1in;
   373    374   	> img { grid-column: 1/2; grid-row: 1/3; width: 1in; height: 1in;}
   374    375   	> textarea {
   375         -		grid-column: 2/5; grid-row: 1/2; height: 3in;
          376  +		grid-column: 2/6; grid-row: 1/2; height: 3in;
   376    377   		resize: vertical;
   377    378   		margin-bottom: 0.08in;
   378    379   	}
   379    380   	> input[name="acl"] { grid-column: 2/3; grid-row: 2/3; }
   380         -	> button { grid-column: 4/5; grid-row: 2/3; }
          381  +	> button[value="post"] { grid-column: 5/6; grid-row: 2/3; }
          382  +	> button[value="attach"] { grid-column: 4/5; grid-row: 2/3; }
   381    383   	a.help[href] { margin-right: 0.05in }
   382    384   }
   383    385   
   384    386   a.help[href] {
   385    387   	display: block;
   386    388   	text-align: center;
   387    389   	padding: 0.09in 0.2in;
................................................................................
   541    543   			padding-top: 0.12in;
   542    544   			background: linear-gradient(to right, tone(-50%), tone(-50%,-0.7));
   543    545   			border: 1px solid tone(-55%);
   544    546   			border-left: none;
   545    547   			text-shadow: 1px 1px 0 black;
   546    548   		}
   547    549   	}
          550  +
          551  +}
          552  +
          553  +form {
          554  +	.elem {
          555  +		margin: 0.1in 0;
          556  +		label { display:block; font-weight: bold; padding: 0.03in 0; }
          557  +		.txtbox {
          558  +			@extend %serif;
          559  +			box-sizing: border-box;
          560  +			padding: 0.08in 0.1in;
          561  +			border: 1px solid black;
          562  +			background: tone(-55%);
          563  +		}
          564  +		input, textarea, .txtbox {
          565  +			display: block;
          566  +			width: 100%;
          567  +		}
          568  +		button { float: right; width: 50%; }
          569  +	}
   548    570   }

Modified store.t from [763fd9ba8a] to [69369cc5c2].

    12     12   	relation = lib.enum {
    13     13   		'follow', 'mute', 'block'
    14     14   	};
    15     15   	credset = lib.set {
    16     16   		'pw', 'otp', 'challenge', 'trust'
    17     17   	};
    18     18   	privset = lib.set {
    19         -		'post', 'edit', 'acct', 'upload', 'censor', 'admin'
           19  +		'post', 'edit', 'acct', 'upload', 'censor', 'admin', 'invite'
    20     20   	};
    21     21   	powerset = lib.set {
    22     22   		-- user powers -- default on
    23     23   		'login', 'visible', 'post', 'shout',
    24     24   		'propagate', 'upload', 'acct', 'edit';
    25     25   
    26     26   		-- admin powers -- default off
    27     27   		'purge', 'config', 'censor', 'suspend',
    28     28   		'cred', 'elevate', 'demote', 'rebrand', -- modify site's brand identity
    29         -		'herald' -- grant serverwide epithets
           29  +		'herald', -- grant serverwide epithets
           30  +		'invite' -- *unlimited* invites
    30     31   	};
    31     32   	prepmode = lib.enum {
    32     33   		'full','conf','admin'
    33     34   	}
    34     35   }
    35     36   
    36     37   m.privmap = {}
................................................................................
    52     53   
    53     54   struct m.source
    54     55   
    55     56   struct m.rights {
    56     57   	rank: uint16 -- lower = more powerful except 0 = regular user
    57     58   	-- creating staff automatically assigns rank immediately below you
    58     59   	quota: uint32 -- # of allowed tweets per day; 0 = no limit
           60  +	invites: intptr -- # of people left this user can invite
    59     61   	
    60     62   	powers: m.powerset
    61     63   }
    62     64   
    63     65   terra m.rights_default()
    64         -	var pow: m.powerset pow:fill()
    65         -	(pow.purge << false)
    66         -	(pow.config << false)
    67         -	(pow.censor << false)
    68         -	(pow.suspend << false)
    69         -	(pow.elevate << false)
    70         -	(pow.demote << false)
    71         -	(pow.cred << false)
    72         -	(pow.rebrand << false)
    73         -	return m.rights { rank = 0, quota = 1000, powers = pow; }
           66  +	var pow: m.powerset pow:clear()
           67  +	(pow.login     << true)
           68  +	(pow.visible   << true)
           69  +	(pow.post      << true)
           70  +	(pow.shout     << true)
           71  +	(pow.propagate << true)
           72  +	(pow.upload    << true)
           73  +	(pow.acct      << true)
           74  +	(pow.edit      << true)
           75  +	return m.rights { rank = 0, quota = 1000, invites = 0, powers = pow; }
    74     76   end
    75     77   
    76     78   struct m.actor {
    77     79   	id: uint64
    78     80   	nym: str
    79     81   	handle: str
    80     82   	origin: uint64
................................................................................
   191    193   		var bytes = bits / 8
   192    194   		var hexchs = bytes * 2
   193    195   		var segs = hexchs / 4
   194    196   		var seps = segs - 1
   195    197   		var maxsz = hexchs + seps + 1
   196    198   	else return nil end
   197    199   end
          200  +
          201  +struct m.kompromat {
          202  +-- The Evidence
          203  +	id: uint64
          204  +	perp: uint64 -- whodunnit
          205  +	desc: str
          206  +	post: uint64 -- the post in question, if any
          207  +	reporter: uint64 -- 0 = originated automatically by the System itself
          208  +	resolution: str -- null for unresolved
          209  +	-- as proto: set resolution to empty string to search for resolved incidents
          210  +}
          211  +
          212  +struct m.sanction {
          213  +	id: uint64
          214  +	issuer: uint64
          215  +	scope: uint64
          216  +	nature: uint16
          217  +	victim: uint64
          218  +	autoexpire: bool  expire: m.timepoint
          219  +	timedreview: bool review: m.timepoint
          220  +	reason: str
          221  +	context: str
          222  +}
   198    223   
   199    224   struct m.auth {
          225  +-- a credential record
   200    226   	aid: uint64
   201    227   	uid: uint64
   202    228   	aname: str
   203    229   	netmask: m.inet
   204    230   	privs: m.privset
   205    231   	blacklist: bool
   206    232   }
................................................................................
   209    235   struct m.backend { id: rawstring
   210    236   	open: &m.source -> &opaque
   211    237   	close: &m.source -> {}
   212    238   	dbsetup: &m.source -> bool -- creates the schema needed to call conprep (called only once per database e.g. with `parsav db init`)
   213    239   	conprep: {&m.source, m.prepmode.t} -> {} -- prepares queries and similar tasks that require the schema to already be in place
   214    240   	obliterate_everything: &m.source -> bool -- wipes everything parsav-related out of the database
   215    241   
          242  +	tx_enter: &m.source -> bool
          243  +	tx_complete: &m.source -> bool
          244  +	-- these two functions are special, in that they should be called
          245  +	-- directly on a specific backend, rather than passed down to the
          246  +	-- backends by the server; that is pathological behavior that will
          247  +	-- not have the desired effect
          248  +
   216    249   	conf_get: {&m.source, rawstring} -> lib.mem.ptr(int8)
   217    250   	conf_set: {&m.source, rawstring, rawstring} -> {}
   218    251   	conf_reset: {&m.source, rawstring} -> {}
   219    252   
   220    253   	actor_create: {&m.source, &m.actor} -> uint64
   221    254   	actor_save_privs: {&m.source, &m.actor} -> {}
   222    255   	actor_fetch_xid: {&m.source, lib.mem.ptr(int8)} -> lib.mem.ptr(m.actor)
................................................................................
   273    306   	auth_purge_pw: {&m.source, uint64, rawstring} -> {}
   274    307   	auth_purge_otp: {&m.source, uint64, rawstring} -> {}
   275    308   	auth_purge_trust: {&m.source, uint64, rawstring} -> {}
   276    309   
   277    310   	post_save: {&m.source, &m.post} -> {}
   278    311   	post_create: {&m.source, &m.post} -> uint64
   279    312   	post_enum_author_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post))
          313  +	post_attach_ctl: {&m.source, uint64, uint64, bool} -> {}
          314  +		-- attaches or detaches an existing database artifact
          315  +			-- post id: uint64
          316  +			-- artifact id: uint64
          317  +			-- detach: bool
          318  +	artifact_instantiate: {&m.source, lib.mem.ptr(uint8), lib.mem.ptr(int8)} -> uint64
          319  +		-- instantiate an artifact in the database, either installing a new
          320  +		-- artifact or returning the id of an existing artifact with the same hash
          321  +			-- artifact: bytea
          322  +			-- mime:     pstring
          323  +	artifact_quicksearch: {&m.source, lib.mem.ptr(uint8)} -> {uint64,bool}
          324  +		-- checks whether a hash is already in the database without uploading
          325  +		-- the entire file to the database server
          326  +			-- hash: bytea
          327  +				--> artifact id (0 if null), suppressed?
          328  +	artifact_expropriate: {&m.source, uint64, uint64, lib.mem.ptr(int8)} -> {}
          329  +		-- claims an existing artifact for the user's own collection
          330  +			-- uid:         uint64
          331  +			-- artifact id: uint64
          332  +			-- description: pstring
          333  +	artifact_disclaim: {&m.source, uint64, uint64} -> {}
          334  +		-- a user disclaims their ownership stake in an artifact, removing it from
          335  +		-- the database entirely if they were the only owner, and removing their
          336  +		-- description of it either way
          337  +			-- uid:         uint64
          338  +			-- artifact id: uint64
          339  +	artifact_excise: {&m.source, uint64, bool} -> {}
          340  +		-- (admin action) forcibly excise an artifact from the database, deleting
          341  +		-- all links to it and removing it from users' collections. if "blacklist,"
          342  +		-- the artifact will be banned and attempts to upload it in the future
          343  +		-- will fail, triggering a report. mainly intended for dealing with spam,
          344  +		-- IP violations, That Which Shall Not Be Named, and various other infohazards.
          345  +			-- artifact id: uint64
          346  +			-- blacklist:   bool
          347  +
          348  +	nkvd_report_issue: {&m.source, &m.kompromat} -> {}
          349  +		-- an incidence of Badthink has been detected. report it immediately
          350  +		-- to the Supreme Soviet
          351  +	nkvd_reports_enum: {&m.source, &m.kompromat} -> lib.mem.ptr(m.kompromat)
          352  +		-- search through the Archives
          353  +			-- proto: kompromat (null for all records, or a prototype describing the records to return)
          354  +	nkvd_sanction_issue:  {&m.source, &m.sanction} -> uint64
          355  +	nkvd_sanction_vacate: {&m.source, uint64} -> {}
          356  +	nkvd_sanction_enum_target: {&m.source, uint64} -> {}
          357  +	nkvd_sanction_enum_issuer: {&m.source, uint64} -> {}
          358  +	nkvd_sanction_review: {&m.source, m.timepoint} -> {}
          359  +
   280    360   	convo_fetch_xid: {&m.source,rawstring} -> lib.mem.ptr(m.post)
   281         -	convo_fetch_uid: {&m.source,uint64} -> lib.mem.ptr(m.post)
          361  +	convo_fetch_cid: {&m.source,uint64} -> lib.mem.ptr(m.post)
   282    362   
   283    363   	timeline_actor_fetch_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post))
   284    364   	timeline_instance_fetch: {&m.source, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post))
   285    365   }
   286    366   
   287    367   struct m.source {
   288    368   	backend: &m.backend

Modified view/compose.tpl from [5ccb8d92d6] to [a602850007].

     1      1   <form class="compose" method="post">
     2      2   	<img src="/avi/@handle">
     3      3   	<textarea autofocus name="post" placeholder="it was a dark and stormy night…">@!content</textarea>
     4      4   	<input required autocomplete="on" type="text" name="acl" class="acl" value="@acl" list="scopes" placeholder="access control"> @?acl
     5         -	<button type="submit">commit</button>
            5  +	<button type="submit" name="act" value="post">commit</button>
            6  +	<button type="submit" name="act" value="attach">attach</button>
     6      7   </form>
     7      8   
     8      9   <datalist id="scopes">
     9     10   	<option>all</option>
    10     11   	<option>mentioned</option>
    11     12   	<option>local</option>
    12     13   	<option>mutual</option>

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

     1      1   <form method="post">
     2         -	<label>handle <div class="txtbox">@!handle</div></label>
     3         -	<label>display name <input type="text" name="nym" value="@:nym"></label>
     4         -	<label>bio <textarea name="bio">@!bio</textarea></label>
     5         -	<input type="submit" value="commit">
            2  +	<div class="elem"><label>handle</label> <div class="txtbox">@!handle</div>
            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  +	<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>
     6      6   </form>