parsav  Check-in [7bd78f9b1c]

Overview
Comment:improve sql, js, docs, other tweaks
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 7bd78f9b1c391894b305c8b3412d9c975212ec4306e31b4ed7d0d56d996d008c
User & Date: lexi on 2021-01-05 22:10:42
Other Links: manifest | tags
Context
2021-01-05
22:13
clean up debug code check-in: 7239d174b1 user: lexi tags: trunk
22:10
improve sql, js, docs, other tweaks check-in: 7bd78f9b1c user: lexi tags: trunk
2021-01-04
20:33
more jabbascript improvements check-in: b6c2a79945 user: lexi tags: trunk
Changes

Modified backend/pgsql.t from [2e62d4947d] to [7305c1c258].

    22     22   			delete from parsav_config where
    23     23   				key = $1::text 
    24     24   		]];
    25     25   	};
    26     26   
    27     27   	actor_fetch_uid = {
    28     28   		params = {uint64}, sql = [[
    29         -			select a.id, a.nym, a.handle, a.origin, a.bio,
    30         -			       a.avataruri, a.rank, a.quota, a.key, a.epithet,
    31         -			       extract(epoch from a.knownsince)::bigint,
    32         -				   coalesce(a.handle || '@' || s.domain,
    33         -				            '@' || a.handle) as xid,
    34         -			       a.invites
    35         -
    36         -			from      parsav_actors  as a
    37         -			left join parsav_servers as s
    38         -				on a.origin = s.id
    39         -			where a.id = $1::bigint
           29  +			select (pg_temp.parsavpg_translate_actor(a)).*
           30  +				from parsav_actors as a
           31  +				where a.id = $1::bigint
    40     32   		]];
    41     33   	};
    42     34   
    43     35   	actor_fetch_xid = {
    44     36   		params = {pstring}, sql = [[
    45         -			select a.id, a.nym, a.handle, a.origin, a.bio,
    46         -			       a.avataruri, a.rank, a.quota, a.key, a.epithet,
    47         -			       extract(epoch from a.knownsince)::bigint,
    48         -				   coalesce(a.handle || '@' || s.domain,
    49         -				            '@' || a.handle) as xid,
    50         -			       a.invites,
    51         -
    52         -				coalesce(s.domain,
    53         -				        (select value from parsav_config
    54         -							where key='domain' limit 1)) as domain
    55         -
    56         -			from      parsav_actors  as a
    57         -			left join parsav_servers as s
    58         -				on a.origin = s.id
    59         -
    60         -			where $1::text = (a.handle || '@' || domain) or
    61         -			      $1::text = ('@' || a.handle || '@' || domain) or
           37  +			with txd as (
           38  +				select (pg_temp.parsavpg_translate_actor(a)).* from parsav_actors as a
           39  +			)
           40  +			select * from txd as a where $1::text = xid or
    62     41   				  (a.origin is null and
    63     42   					  $1::text = a.handle or
    64         -					  $1::text = ('@' || a.handle))
           43  +					  $1::text = (a.handle ||'@'||
           44  +				        (select value from parsav_config where key='domain')))
    65     45   		]];
    66     46   	};
    67     47   
    68     48   	actor_purge_uid = {
    69     49   		params = {uint64}, cmd = true, sql = [[
    70     50   			with d as ( -- cheating
    71     51   				delete from parsav_sanctions where victim = $1::bigint
................................................................................
   102     82   	};
   103     83   
   104     84   	actor_create = {
   105     85   		params = {
   106     86   			rawstring, rawstring, uint64, lib.store.timepoint,
   107     87   			rawstring, rawstring, lib.mem.ptr(uint8),
   108     88   			rawstring, uint16, uint32, uint32
   109         -		};
   110         -		sql = [[
           89  +		}, sql = [[
   111     90   			insert into parsav_actors (
   112     91   				nym,handle,
   113     92   				origin,knownsince,
   114     93   				bio,avataruri,key,
   115     94   				epithet,rank,quota,
   116         -				invites
           95  +				invites,authtime
   117     96   			) values ($1::text, $2::text,
   118     97   				case when $3::bigint = 0 then null
   119     98   				     else $3::bigint end,
   120         -				to_timestamp($4::bigint),
           99  +				$4::bigint,
   121    100   				$5::bigint, $6::bigint, $7::bytea,
   122    101   				$8::text, $9::smallint, $10::integer,
   123         -				$11::integer
          102  +				$11::integer,$4::bigint
   124    103   			) returning id
   125    104   		]];
   126    105   	};
   127    106   
   128    107   	actor_auth_pw = {
   129    108   		params = {pstring,rawstring,pstring,lib.store.inet}, sql = [[
   130    109   			select a.aid, a.uid, a.name from parsav_auth as a
................................................................................
   138    117   		]];
   139    118   	};
   140    119   
   141    120   	actor_enum_local = {
   142    121   		params = {}, sql = [[
   143    122   			select id, nym, handle, origin, bio,
   144    123   			       null::text, rank, quota, key, epithet,
   145         -			       extract(epoch from knownsince)::bigint,
          124  +			       knownsince::bigint,
   146    125   					'@' || handle,
   147    126   				   invites
   148    127   			from parsav_actors where origin is null
   149    128   			order by nullif(rank,0) nulls last, handle
   150    129   		]];
   151    130   	};
   152    131   
   153    132   	actor_enum = {
   154    133   		params = {}, sql = [[
   155         -			select a.id, a.nym, a.handle, a.origin, a.bio,
   156         -			       a.avataruri, a.rank, a.quota, a.key, a.epithet,
   157         -			       extract(epoch from a.knownsince)::bigint,
   158         -				   coalesce(a.handle || '@' || s.domain,
   159         -				            '@' || a.handle) as xid,
   160         -				   invites
   161         -			from parsav_actors a
   162         -			left join parsav_servers s on s.id = a.origin
          134  +			select (pg_temp.parsavpg_translate_actor(a)).*
          135  +				from parsav_actors as a
          136  +
   163    137   			order by nullif(a.rank,0) nulls last, a.handle, a.origin
   164    138   		]];
   165    139   	};
   166    140   
   167    141   	actor_stats = {
   168         -		params = {uint64}, sql = ([[
          142  +		params = {uint64}, sql = [[
   169    143   			with tweets as (
   170    144   				select from parsav_posts where author = $1::bigint
   171    145   			),
   172    146   			follows as (
   173    147   				select relatee as user from parsav_rels
   174         -					where relator = $1::bigint and kind = <follow>
          148  +					where relator = $1::bigint and kind = <rel:follow>
   175    149   			),
   176    150   			followers as (
   177    151   				select relator as user from parsav_rels
   178         -					where relatee = $1::bigint and kind = <follow>
          152  +					where relatee = $1::bigint and kind = <rel:follow>
   179    153   			),
   180    154   			mutuals as (
   181    155   				select * from follows  intersect  select * from followers
   182    156   			)
   183    157   
   184    158   			values (
   185    159   				(select count(tweets.*)::bigint from tweets),
   186    160   				(select count(follows.*)::bigint from follows),
   187    161   				(select count(followers.*)::bigint from followers),
   188    162   				(select count(mutuals.*)::bigint from mutuals)
   189    163   			)
   190         -		]]):gsub('<(%w+)>',function(r) return tostring(lib.store.relation.idvmap[r]) end)
          164  +		]]
   191    165   	};
   192    166   
   193    167   	actor_auth_how = {
   194    168   		params = {rawstring, lib.store.inet}, sql = [[
   195    169   		with mts as (select a.kind from parsav_auth as a
   196    170   			left join parsav_actors as u on u.id = a.uid
   197    171   			where (a.uid is null or u.handle = $1::text or (
................................................................................
   206    180   				(select count(*) from mts where kind like 'challenge-%') > 0,
   207    181   				(select count(*) from mts where kind = 'trust') > 0
   208    182   		]]; -- cheat
   209    183   	};
   210    184   
   211    185   	actor_session_fetch = {
   212    186   		params = {uint64, lib.store.inet, int64}, sql = [[
   213         -			select a.id, a.nym, a.handle, a.origin, a.bio,
   214         -			       a.avataruri, a.rank, a.quota, a.key, a.epithet,
   215         -			       extract(epoch from a.knownsince)::bigint,
   216         -				   coalesce(a.handle || '@' || s.domain,
   217         -				            '@' || a.handle) as xid,
          187  +			select (pg_temp.parsavpg_translate_actor(a)).*,
   218    188   
   219    189   			       au.restrict,
   220    190   						array['post'    ] <@ au.restrict,
   221    191   						array['edit'    ] <@ au.restrict,
   222    192   						array['account' ] <@ au.restrict,
   223    193   						array['upload'  ] <@ au.restrict,
   224    194   						array['moderate'] <@ au.restrict,
   225    195   						array['admin'   ] <@ au.restrict
   226    196   
   227    197   			from      parsav_auth au
   228    198   			left join parsav_actors a     on au.uid = a.id
   229         -			left join parsav_servers s    on a.origin = s.id
   230    199   
   231    200   			where au.aid = $1::bigint and au.blacklist = false and
   232    201   				(au.netmask is null or au.netmask >> $2::inet) and
   233    202   				($3::bigint = 0 or --slightly abusing the epoch time fmt here, but
   234         -					((a.authtime   is null or a.authtime   <= to_timestamp($3::bigint)) and
   235         -					 (au.valperiod is null or au.valperiod <= to_timestamp($3::bigint))))
          203  +					((a.authtime   is null or a.authtime   <= $3::bigint) and
          204  +					 (au.valperiod is null or au.valperiod <= $3::bigint)))
   236    205   		]];
   237    206   	};
   238    207   
   239    208   	actor_powers_fetch = {
   240    209   		params = {uint64}, sql = [[
   241    210   			select key, allow from parsav_rights where actor = $1::bigint
   242    211   		]]
................................................................................
   253    222   	actor_power_delete = {
   254    223   		params = {uint64,lib.mem.ptr(int8)}, cmd = true, sql = [[
   255    224   			delete from parsav_rights where
   256    225   				actor = $1::bigint and
   257    226   				key = $2::text
   258    227   		]]
   259    228   	};
          229  +
          230  +	actor_rel_create = {
          231  +		params = {uint16,uint64, uint64}, cmd = true, sql = [[
          232  +			insert into parsav_rels (kind,relator,relatee)
          233  +				values($1::smallint, $2::bigint, $3::bigint)
          234  +			on conflict do nothing
          235  +		]];
          236  +	};
          237  +
          238  +	actor_rel_destroy = {
          239  +		params = {uint16,uint64, uint64}, cmd = true, sql = [[
          240  +			delete from parsav_rels where
          241  +				kind  = $1::smallint and
          242  +				relator = $2::bigint and
          243  +				relatee = $3::bigint
          244  +		]];
          245  +	};
          246  +
          247  +	actor_rel_enum = {
          248  +		params = {uint64, uint64}, sql = [[
          249  +			select kind from parsav_rels where
          250  +				relator = $1::bigint and
          251  +				relatee = $2::bigint
          252  +		]];
          253  +	};
          254  +
          255  +	actor_notice_enum = {
          256  +		params = {uint64}, sql = [[
          257  +			select (notice).* from pg_temp.parsavpg_notices
          258  +			where rcpt = $1::bigint
          259  +		]];
          260  +	};
   260    261   
   261    262   	auth_sigtime_user_fetch = {
   262    263   		params = {uint64}, sql = [[
   263         -			select extract(epoch from authtime)::bigint
          264  +			select authtime::bigint
   264    265   			from parsav_actors where id = $1::bigint
   265    266   		]];
   266    267   	};
   267    268   
   268    269   	auth_sigtime_user_alter = {
   269    270   		params = {uint64,int64}, cmd = true, sql = [[
   270    271   			update parsav_actors set
   271         -				authtime = to_timestamp($2::bigint)
          272  +				authtime = $2::bigint
   272    273   				where id = $1::bigint
   273    274   		]];
   274    275   	};
   275    276   
   276    277   	auth_create_pw = {
   277         -		params = {uint64, binblob, pstring}, cmd = true, sql = [[
   278         -			insert into parsav_auth (uid, name, kind, cred, comment) values (
          278  +		params = {uint64, binblob, int64, pstring}, cmd = true, sql = [[
          279  +			insert into parsav_auth (uid, name, kind, cred, valperiod, comment) values (
   279    280   				$1::bigint,
   280    281   				(select handle from parsav_actors where id = $1::bigint),
   281    282   				'pw-sha256', $2::bytea,
   282         -				$3::text
          283  +				$3::bigint, $4::text
   283    284   			)
   284    285   		]]
   285    286   	};
   286    287   
   287    288   	auth_purge_type = {
   288    289   		params = {rawstring, uint64, rawstring}, cmd = true, sql = [[
   289    290   			delete from parsav_auth where
................................................................................
   310    311   			rawstring, rawstring, rawstring;
   311    312   		}, cmd = true, sql = [[
   312    313   			update parsav_posts set
   313    314   				subject = $4::text,
   314    315   				acl = $5::text,
   315    316   				body = $6::text,
   316    317   				chgcount = $2::integer,
   317         -				edited = to_timestamp($3::bigint)
          318  +				edited = $3::bigint
   318    319   			where id = $1::bigint
   319    320   		]]
   320    321   	};
   321    322   
   322    323   	post_create = {
   323    324   		params = {
   324    325   			uint64, rawstring, rawstring, rawstring,
................................................................................
   327    328   			insert into parsav_posts (
   328    329   				author, subject, acl, body,
   329    330   				parent, posted, discovered,
   330    331   				circles, mentions, convoheaduri
   331    332   			) values (
   332    333   				$1::bigint, case when $2::text = '' then null else $2::text end,
   333    334   				$3::text, $4::text, 
   334         -				$5::bigint, to_timestamp($6::bigint), now(),
          335  +				$5::bigint, $6::bigint, $6::bigint,
   335    336   				array[]::bigint[], array[]::bigint[], $7::text
   336    337   			) returning id
   337    338   		]]; -- TODO array handling
   338    339   	};
   339    340   
   340    341   	post_destroy_prepare = {
   341    342   		params = {uint64}, cmd = true, sql = [[
................................................................................
   349    350   		params = {uint64}, cmd = true, sql = [[
   350    351   			delete from parsav_posts where id = $1::bigint
   351    352   		]]
   352    353   	};
   353    354   	
   354    355   	post_fetch = {
   355    356   		params = {uint64}, sql = [[
   356         -			with counts as (
   357         -				select a.kind, p.id as subject, count(*) as ct from parsav_acts as a
   358         -					inner join parsav_posts as p on p.id = a.subject
   359         -				group by a.kind, p.id
   360         -			)
   361         -
   362         -			select a.origin is null,
   363         -				p.id, p.author, p.subject, p.acl, p.body,
   364         -				extract(epoch from p.posted    )::bigint,
   365         -				extract(epoch from p.discovered)::bigint,
   366         -				extract(epoch from p.edited    )::bigint,
   367         -				p.parent, p.convoheaduri, p.chgcount,
   368         -				coalesce(c.value, -1)::smallint, 0::bigint, 0::bigint,
   369         -				coalesce((select ct from counts where kind = 'like' and counts.subject = p.id),0)::integer,
   370         -				coalesce((select ct from counts where kind = 'rt' and counts.subject = p.id),0)::integer
   371         -
   372         -			from parsav_posts as p
   373         -				inner join parsav_actors          as a on p.author = a.id
   374         -				left join  parsav_actor_conf_ints as c on c.uid    = a.id and c.key = 'ui-accent'
   375         -			where p.id = $1::bigint
   376         -		]];
          357  +			select (p.post).*
          358  +			from pg_temp.parsavpg_known_content as p
          359  +				where (p.post).id = $1::bigint and (p.post).rtdby = 0
          360  +		]]
   377    361   	};
   378    362   
   379    363   	post_enum_parent = {
   380    364   		params = {uint64}, sql = [[
   381         -			with counts as (
   382         -				select a.kind, p.id as subject, count(*) as ct from parsav_acts as a
   383         -					inner join parsav_posts as p on p.id = a.subject
   384         -				group by a.kind, p.id
   385         -			)
   386         -
   387         -			select a.origin is null,
   388         -				p.id, p.author, p.subject, p.acl, p.body,
   389         -				extract(epoch from p.posted    )::bigint,
   390         -				extract(epoch from p.discovered)::bigint,
   391         -				extract(epoch from p.edited    )::bigint,
   392         -				p.parent, p.convoheaduri, p.chgcount,
   393         -				coalesce(c.value, -1)::smallint, 0::bigint, 0::bigint,
   394         -				coalesce((select ct from counts where kind = 'like' and counts.subject = p.id),0)::integer,
   395         -				coalesce((select ct from counts where kind = 'rt' and counts.subject = p.id),0)::integer
   396         -
   397         -			from parsav_posts as p
   398         -				inner join parsav_actors as a on a.id = p.author
   399         -				left join  parsav_actor_conf_ints as c on c.uid = a.id and c.key = 'ui-accent'
   400         -			where p.parent = $1::bigint
   401         -			order by p.posted, p.discovered asc
   402         -		]]
          365  +			select (p.post).*
          366  +			from pg_temp.parsavpg_known_content as p
          367  +				where (p.post).parent = $1::bigint and (p.post).rtdby = 0
          368  +				order by (p.post).posted, (p.post).discovered asc
          369  +		]];
   403    370   	};
   404    371   
   405    372   	thread_latest_arrival_calc = {
   406    373   		params = {uint64}, sql = [[
   407    374   			with recursive posts(id) as (
   408    375   				select id from parsav_posts where parent = $1::bigint
   409    376   			union
................................................................................
   414    381   			maxes as (
   415    382   				select unnest(array[max(p.posted), max(p.discovered), max(p.edited)]) as m
   416    383   					from posts
   417    384   					inner join parsav_posts as p
   418    385   						on p.id = posts.id
   419    386   			)
   420    387   
   421         -			select extract(epoch from max(m))::bigint from maxes
          388  +			select max(m)::bigint from maxes
   422    389   		]];
   423    390   	};
   424    391   
   425    392   	post_react_simple = {
   426         -		params = {uint64, uint64, pstring}, sql = [[
   427         -			insert into parsav_acts (kind,actor,subject) values (
   428         -				$3::text, $1::bigint, $2::bigint
          393  +		params = {uint64, uint64, pstring, int64}, sql = [[
          394  +			insert into parsav_acts (kind,actor,subject,time) values (
          395  +				$3::text, $1::bigint, $2::bigint, $4::bigint
   429    396   			) returning id
   430    397   		]];
   431    398   	};
   432    399   
   433    400   	post_react_cancel = {
   434    401   		params = {uint64, uint64, pstring}, cmd = true, sql = [[
   435    402   			delete from parsav_acts where
................................................................................
   446    413   				($2::bigint = 0 or subject = $2::bigint) and
   447    414   				($3::text is null or kind  = $3::text  )
   448    415   		]]
   449    416   	};
   450    417   
   451    418   	post_enum_author_uid = {
   452    419   		params = {uint64,uint64,uint64,uint64, uint64}, sql = [[
   453         -			with ownposts as (
   454         -				select *, 0::bigint as rtid from parsav_posts as p
   455         -				where p.author = $5::bigint and
   456         -					($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and
   457         -					($2::bigint = 0 or to_timestamp($2::bigint) < p.posted)
   458         -			),
          420  +			select (c.post).*
          421  +			from pg_temp.parsavpg_known_content as c
   459    422   
   460         -			retweets as (
   461         -				select p.*, a.id as rtid from parsav_acts as a
   462         -					inner join parsav_posts as p on a.subject = p.id
   463         -				where a.actor = $5::bigint and
   464         -					  a.kind = 'rt' and
   465         -					  ($1::bigint = 0 or a.time <= to_timestamp($1::bigint)) and
   466         -					  ($2::bigint = 0 or to_timestamp($2::bigint) < a.time)
   467         -			),
          423  +			where c.promoter = $5::bigint and
          424  +				($1::bigint = 0 or c.tltime   <= $1::bigint) and
          425  +				($2::bigint = 0 or $2::bigint <  c.tltime)
          426  +			order by c.tltime desc
   468    427   
   469         -			allposts as (select *, 0::bigint  as retweeter from ownposts
   470         -			      union  select *, $5::bigint as retweeter from retweets),
   471         -
   472         -			counts as (
   473         -				select a.kind, p.id as subject, count(*) as ct from parsav_acts as a
   474         -					inner join parsav_posts as p on p.id = a.subject
   475         -				group by a.kind, p.id
   476         -			)
   477         -
   478         -			select a.origin is null,
   479         -				p.id, p.author, p.subject, p.acl, p.body,
   480         -				extract(epoch from p.posted    )::bigint,
   481         -				extract(epoch from p.discovered)::bigint,
   482         -				extract(epoch from p.edited    )::bigint,
   483         -				p.parent, p.convoheaduri, p.chgcount,
   484         -				coalesce(c.value,-1)::smallint,
   485         -				p.retweeter, p.rtid,
   486         -				coalesce((select ct from counts where kind = 'like' and counts.subject = p.id),0)::integer,
   487         -				coalesce((select ct from counts where kind = 'rt' and counts.subject = p.id),0)::integer
   488         -			from allposts as p
   489         -				inner join parsav_actors as a on p.author = a.id
   490         -				left  join parsav_actor_conf_ints as c
   491         -					on c.key = 'ui-accent' and
   492         -					   c.uid = a.id
   493         -			order by (p.posted, p.discovered) desc
   494    428   			limit case when $3::bigint = 0 then null
   495    429   					   else $3::bigint end
   496    430   			offset $4::bigint
   497         -		]]
          431  +		]];
   498    432   	};
   499    433   
   500    434   	-- maybe there's some way to unify these two, idk, im tired
   501    435   
   502    436   	timeline_instance_fetch = {
   503    437   		params = {uint64, uint64, uint64, uint64}, sql = [[
   504         -			with posts as (
   505         -				select true,
   506         -					p.id, p.author, p.subject, p.acl, p.body,
   507         -					extract(epoch from p.posted    )::bigint,
   508         -					extract(epoch from p.discovered)::bigint,
   509         -					extract(epoch from p.edited    )::bigint,
   510         -					p.parent, null::text, p.chgcount,
   511         -					coalesce(c.value, -1)::smallint, 0::bigint, 0::bigint
   512         -
   513         -				from parsav_posts as p
   514         -					inner join parsav_actors          as a on p.author = a.id
   515         -					left join  parsav_actor_conf_ints as c on c.uid    = a.id and c.key = 'ui-accent'
   516         -				where
   517         -					($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and
   518         -					($2::bigint = 0 or to_timestamp($2::bigint) < p.posted) and
   519         -					(a.origin is null)
   520         -				order by (p.posted, p.discovered) desc
   521         -				limit case when $3::bigint = 0 then null
   522         -						   else $3::bigint end
   523         -				offset $4::bigint
   524         -			), counts as (
   525         -				select a.kind, p.id as subject, count(*) as ct from parsav_acts as a
   526         -					inner join parsav_posts as p on p.id = a.subject
   527         -				group by a.kind, p.id
          438  +			select (c.post).*
          439  +			from pg_temp.parsavpg_known_content as c
          440  +
          441  +			where (c.post).localpost = true and
          442  +				($1::bigint = 0 or c.tltime   <= $1::bigint) and
          443  +				($2::bigint = 0 or $2::bigint <  c.tltime)
          444  +			order by c.tltime desc
          445  +
          446  +			limit case when $3::bigint = 0 then null
          447  +					   else $3::bigint end
          448  +			offset $4::bigint
          449  +		]];
          450  +	};
          451  +
          452  +	timeline_actor_fetch = {
          453  +		params = {uint64, uint64, uint64, uint64, uint64}, sql = [[
          454  +			with followed as (
          455  +				select relatee from parsav_rels where
          456  +					kind = <rel:follow> and
          457  +					relator = $1::bigint
          458  +			), avoided as (
          459  +				select relatee as avoidee from parsav_rels where
          460  +					kind = <rel:avoid> or kind = <rel:mute> and
          461  +					relator = $1::bigint
          462  +				union select relator as avoidee from parsav_rels where
          463  +					kind = <rel:exclude> and
          464  +					relatee = $1::bigint
   528    465   			)
   529    466   
   530         -			select *,
   531         -				coalesce((select ct from counts as c where kind = 'like' and c.subject = posts.id),0)::integer,
   532         -				coalesce((select ct from counts as c where kind = 'rt' and c.subject = posts.id),0)::integer
   533         -			from posts
   534         -		]]
          467  +			select (c.post).*
          468  +			from pg_temp.parsavpg_known_content as c
          469  +
          470  +			where ($2::bigint = 0 or c.tltime   <= $2::bigint) and
          471  +				  ($3::bigint = 0 or $3::bigint <  c.tltime) and
          472  +				  (c.promoter in (table followed) or
          473  +				   c.promoter = $1::bigint) and
          474  +				  not ((c.post).author in (table avoided))
          475  +			order by c.tltime desc
          476  +
          477  +			limit case when $4::bigint = 0 then null
          478  +					   else $4::bigint end
          479  +			offset $5::bigint
          480  +		]];
   535    481   	};
   536    482   
   537    483   	artifact_instantiate = {
   538    484   		params = {binblob, binblob, pstring}, sql = [[
   539    485   			insert into parsav_artifacts (content,hash,mime) values (
   540    486   				$1::bytea, $2::bytea, $3::text
   541    487   			) on conflict do nothing returning id
................................................................................
   744    690   			buf[2] = tycode
   745    691   			buf[3] = sz
   746    692   			for j=0,sz do buf[4 + j] = i.v6[j] end -- 😬
   747    693   			return buf
   748    694   		end
   749    695   	end;
   750    696   }
          697  +
          698  +local sqlvars = {}
          699  +for i, n in ipairs(lib.store.noticetype.members) do
          700  +	sqlvars['notice:' .. n] = lib.store.noticetype[n]
          701  +end
          702  +
          703  +for i, n in ipairs(lib.store.relation.members) do
          704  +	sqlvars['rel:' .. n] = lib.store.relation.idvmap[n]
          705  +end
   751    706   
   752    707   local con = symbol(&lib.pq.PGconn)
   753         -local prep = {}
   754    708   local function sqlsquash(s) return s
   755    709   	:gsub('%%include (.-)%%',function(f)
   756    710   		return sqlsquash(lib.util.ingest('backend/schema/' .. f))
   757    711   	end) -- include dependencies
   758    712   	:gsub('%-%-.-\n','') -- remove disruptive line comments
   759    713   	:gsub('%-%-.-$','') -- remove unnecessary terminal comments
          714  +	:gsub('<(%g-)>',function(r) return tostring(sqlvars[r]) end)
   760    715   	:gsub('%s+',' ') -- remove whitespace
   761    716   	:gsub('^%s*(.-)%s*$','%1') -- chomp
   762    717   end
          718  +
          719  +-- to simplify queries and reduce development headaches in general, we
          720  +-- offload as much logic as possible into views. to avoid versioning
          721  +-- difficulties, these views are not part of the schema, but are rather
          722  +-- uploaded to the database at the start of a parsav connection, visible
          723  +-- only to the connecting parsav instance, stored in memory, and dropped
          724  +-- as soon as the connection session ends.
          725  +
          726  +local tempviews = sqlsquash(lib.util.ingest 'backend/schema/pgsql-views.sql')
          727  +local prep = { quote
          728  +	var res = lib.pq.PQexec([con], tempviews)
          729  +	if lib.pq.PQresultStatus(res) == lib.pq.PGRES_COMMAND_OK then
          730  +		lib.dbg('uploading pgsql session views')
          731  +	else
          732  +		lib.bail('backend pgsql - failed to upload session views: \n', lib.pq.PQresultErrorMessage(res))
          733  +	end
          734  +end }
   763    735   
   764    736   for k,q in pairs(queries) do
   765    737   	local qt = sqlsquash(q.sql)
   766    738   	local stmt = 'parsavpg_' .. k
   767    739   	terra q.prep([con])
   768    740   		var res = lib.pq.PQprepare([con], stmt, qt, [#q.params], nil)
   769    741   		defer lib.pq.PQclear(res)
................................................................................
   979    951   		lib.dbg(['searching for hashed password credentials in format SHA' .. tostring(hash)])
   980    952   		var [out]
   981    953   		[vdrs]
   982    954   		lib.dbg(['could not find password hash'])
   983    955   	end
   984    956   end
   985    957   
   986         -local schema = sqlsquash(lib.util.ingest('backend/schema/pgsql.sql'))
   987         -local obliterator = sqlsquash(lib.util.ingest('backend/schema/pgsql-drop.sql'))
          958  +local schema = sqlsquash(lib.util.ingest 'backend/schema/pgsql.sql')
          959  +local obliterator = sqlsquash(lib.util.ingest 'backend/schema/pgsql-drop.sql')
   988    960   
   989    961   local privupdate = terra(
   990    962   	src: &lib.store.source,
   991    963   	ac: &lib.store.actor
   992    964   ): {}
   993    965   	var pdef: lib.store.powerset pdef:clear()
   994    966   	var map = array([privmap])
................................................................................
  1081   1053   		return con
  1082   1054   	end];
  1083   1055   
  1084   1056   	close = [terra(src: &lib.store.source) lib.pq.PQfinish([&lib.pq.PGconn](src.handle)) end];
  1085   1057   
  1086   1058   	tx_enter = txdo, tx_complete = txdone;
  1087   1059   
  1088         -	conprep = [terra(src: &lib.store.source, mode: lib.store.prepmode.t)
         1060  +	conprep = [terra(src: &lib.store.source, mode: lib.store.prepmode.t): {}
  1089   1061   		var [con] = [&lib.pq.PGconn](src.handle)
  1090   1062   		if mode == lib.store.prepmode.full then [prep]
  1091   1063   		elseif mode == lib.store.prepmode.conf or
  1092   1064   		       mode == lib.store.prepmode.admin then 
  1093   1065   			queries.conf_get.prep(con)
  1094   1066   			queries.conf_set.prep(con)
  1095   1067   			queries.conf_reset.prep(con)
  1096   1068   			if mode == lib.store.prepmode.admin then 
  1097   1069   			end
  1098   1070   		else lib.bail('unsupported connection preparation mode') end
  1099   1071   	end];
  1100   1072   
  1101         -	dbsetup = [terra(src: &lib.store.source)
         1073  +	dbsetup = [terra(src: &lib.store.source): bool
  1102   1074   		var res = lib.pq.PQexec([&lib.pq.PGconn](src.handle), schema)
  1103   1075   		if lib.pq.PQresultStatus(res) == lib.pq.PGRES_COMMAND_OK then
  1104   1076   			lib.report('successfully instantiated schema in database')
  1105   1077   			return true
  1106   1078   		else
  1107   1079   			lib.warn('backend pgsql - failed to initialize database: \n', lib.pq.PQresultErrorMessage(res))
  1108   1080   			return false
  1109   1081   		end
  1110   1082   	end];
  1111   1083   
  1112         -	obliterate_everything = [terra(src: &lib.store.source)
         1084  +	obliterate_everything = [terra(src: &lib.store.source): bool
  1113   1085   		var res = lib.pq.PQexec([&lib.pq.PGconn](src.handle), obliterator)
  1114   1086   		if lib.pq.PQresultStatus(res) == lib.pq.PGRES_COMMAND_OK then
  1115   1087   			lib.report('successfully wiped out everything parsav-related in database')
  1116   1088   			return true
  1117   1089   		else
  1118   1090   			lib.warn('backend pgsql - failed to obliterate database: \n', lib.pq.PQresultErrorMessage(res))
  1119   1091   			return false
................................................................................
  1242   1214   
  1243   1215   			var a = row_to_actor(&r, 0)
  1244   1216   			a.ptr.source = src
  1245   1217   
  1246   1218   			var au = [lib.stat(lib.store.auth)] { ok = true }
  1247   1219   			au.val.aid = aid
  1248   1220   			au.val.uid = a.ptr.id
  1249         -			if not r:null(0,14) then -- restricted?
         1221  +			if not r:null(0,13) then -- restricted?
  1250   1222   				au.val.privs:clear()
  1251         -				(au.val.privs.post    << r:bool(0,15)) 
  1252         -				(au.val.privs.edit    << r:bool(0,16))
  1253         -				(au.val.privs.account << r:bool(0,17))
  1254         -				(au.val.privs.upload  << r:bool(0,18))
  1255         -				(au.val.privs.moderate<< r:bool(0,19))
  1256         -				(au.val.privs.admin   << r:bool(0,20))
         1223  +				(au.val.privs.post    << r:bool(0,14)) 
         1224  +				(au.val.privs.edit    << r:bool(0,15))
         1225  +				(au.val.privs.account << r:bool(0,16))
         1226  +				(au.val.privs.upload  << r:bool(0,17))
         1227  +				(au.val.privs.moderate<< r:bool(0,18))
         1228  +				(au.val.privs.admin   << r:bool(0,19))
  1257   1229   			else au.val.privs:fill() end
  1258   1230   
  1259   1231   			return au, a
  1260   1232   		end
  1261   1233   
  1262   1234   		::fail:: return [lib.stat   (lib.store.auth) ] { ok = false        },
  1263   1235   			            [lib.mem.ptr(lib.store.actor)] { ptr = nil, ct = 0 }
................................................................................
  1301   1273   
  1302   1274   	post_retweet = [terra(
  1303   1275   		src: &lib.store.source,
  1304   1276   		uid: uint64,
  1305   1277   		post: uint64,
  1306   1278   		undo: bool
  1307   1279   	): {}
         1280  +		var time = lib.osclock.time(nil)
  1308   1281   		if not undo then
  1309         -			queries.post_react_simple.exec(src,uid,post,"rt")
         1282  +			queries.post_react_simple.exec(src,uid,post,"rt",time)
  1310   1283   		else
  1311   1284   			queries.post_react_cancel.exec(src,uid,post,"rt")
  1312   1285   		end
  1313   1286   	end];
  1314   1287   	post_like = [terra(
  1315   1288   		src: &lib.store.source,
  1316   1289   		uid: uint64,
  1317   1290   		post: uint64,
  1318   1291   		undo: bool
  1319   1292   	): {}
         1293  +		var time = lib.osclock.time(nil)
  1320   1294   		if not undo then
  1321         -			queries.post_react_simple.exec(src,uid,post,"like")
         1295  +			queries.post_react_simple.exec(src,uid,post,"like",time)
  1322   1296   		else
  1323   1297   			queries.post_react_cancel.exec(src,uid,post,"like")
  1324   1298   		end
  1325   1299   	end];
  1326   1300   	post_liked_uid = [terra(
  1327   1301   		src: &lib.store.source,
  1328   1302   		uid: uint64,
................................................................................
  1391   1365   		-- check against default rights, insert records for wherever powers differ
  1392   1366   		lib.dbg('created new actor, establishing powers')
  1393   1367   		privupdate(src,ac)
  1394   1368   
  1395   1369   		lib.dbg('powers established')
  1396   1370   		return ac.id
  1397   1371   	end];
         1372  +
         1373  +	actor_rel_create = [terra(
         1374  +		src: &lib.store.source,
         1375  +		kind:    uint16,
         1376  +		relator: uint64,
         1377  +		relatee: uint64
         1378  +	): {} queries.actor_rel_create.exec(src,kind,relator,relatee) end];
         1379  +
         1380  +	actor_rel_destroy = [terra(
         1381  +		src: &lib.store.source,
         1382  +		kind:    uint16,
         1383  +		relator: uint64,
         1384  +		relatee: uint64
         1385  +	): {} queries.actor_rel_destroy.exec(src,kind,relator,relatee) end];
         1386  +
         1387  +	actor_rel_calc = [terra(
         1388  +		src: &lib.store.source,
         1389  +		relator: uint64,
         1390  +		relatee: uint64
         1391  +	): lib.store.relationship
         1392  +		var r = lib.store.relationship {
         1393  +			agent = relator, patient = relatee
         1394  +		} r.rel:clear()
         1395  +		  r.recip:clear()
         1396  +
         1397  +		var res = queries.actor_rel_enum.exec(src,relator,relatee)
         1398  +		var recip = queries.actor_rel_enum.exec(src,relatee,relator)
         1399  +
         1400  +		if res.sz > 0 then defer res:free()
         1401  +			for i = 0, res.sz do
         1402  +				var bit = res:int(uint16, i, 0)-1
         1403  +				if bit < [#lib.store.relation.members] then r.rel:setbit(bit, true)
         1404  +					else lib.warn('unknown relationship type in database') end
         1405  +			end
         1406  +		end
         1407  +
         1408  +		if recip.sz > 0 then defer recip:free()
         1409  +			for i = 0, recip.sz do
         1410  +				var bit = recip:int(uint16, i, 0)-1
         1411  +				if bit < [#lib.store.relation.members] then r.recip:setbit(bit, true)
         1412  +					else lib.warn('unknown relationship type in database') end
         1413  +			end
         1414  +		end
         1415  +
         1416  +		return r
         1417  +	end];
  1398   1418   
  1399   1419   	actor_purge_uid = [terra(
  1400   1420   		src: &lib.store.source,
  1401   1421   		uid: uint64
  1402   1422   	) queries.actor_purge_uid.exec(src,uid) end];
  1403   1423   
  1404   1424   	auth_enum_uid = [terra(
................................................................................
  1432   1452   	): {}
  1433   1453   		var hash: uint8[lib.crypt.algsz.sha256]
  1434   1454   		if lib.md.mbedtls_md(lib.md.mbedtls_md_info_from_type(lib.crypt.alg.sha256.id),
  1435   1455   			[&uint8](pw.ptr), pw.ct, &hash[0]) ~= 0 then
  1436   1456   			lib.bail('cannot hash password')
  1437   1457   		end
  1438   1458   		if reset then queries.auth_purge_type.exec(src, nil, uid, 'pw-%') end
  1439         -		queries.auth_create_pw.exec(src, uid, binblob {ptr = &hash[0], ct = [hash.type.N]}, comment)
         1459  +		queries.auth_create_pw.exec(src, uid, binblob {ptr = &hash[0], ct = [hash.type.N]}, lib.osclock.time(nil), comment)
  1440   1460   	end];
  1441   1461   
  1442   1462   	auth_purge_pw = [terra(src: &lib.store.source, uid: uint64, handle: rawstring): {}
  1443   1463   		queries.auth_purge_type.exec(src, handle, uid, 'pw-%')
  1444   1464   	end];
  1445   1465   
  1446   1466   	auth_purge_otp = [terra(src: &lib.store.source, uid: uint64, handle: rawstring): {}

Modified backend/schema/pgsql-auth.sql from [8bbbf24f23] to [09d655e8a5].

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

Modified backend/schema/pgsql-drop.sql from [fa02548662] to [6ff3d1c365].

     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      6   drop table if exists parsav_actor_conf_strs cascade;
     7      7   drop table if exists parsav_actor_conf_ints cascade;
     8      8   drop table if exists parsav_rights cascade;
     9      9   drop table if exists parsav_posts cascade;
    10         -drop table if exists parsav_conversations cascade;
    11     10   drop table if exists parsav_rels cascade;
    12     11   drop table if exists parsav_acts cascade;
    13     12   drop table if exists parsav_log cascade;
    14     13   drop table if exists parsav_artifacts cascade;
    15     14   drop table if exists parsav_artifact_claims cascade;
    16     15   drop table if exists parsav_circles cascade;
    17     16   drop table if exists parsav_rooms cascade;
    18     17   drop table if exists parsav_room_members cascade;
    19     18   drop table if exists parsav_invites cascade;
    20     19   drop table if exists parsav_sanctions cascade;
    21     20   drop table if exists parsav_auth cascade;

Added backend/schema/pgsql-views.sql version [47dae6d49a].

            1  +-- these views are not part of the schema proper, but rather are uploaded
            2  +-- into postgres' memory by parsav at the beginning of a connection. they
            3  +-- are not visible to other clients and politely disappear once the
            4  +-- connection terminates, allowing us to simultaneously avoid versioning
            5  +-- headaches, limit the amount of data we need to send to the server, and
            6  +-- reduce the compilation time of our prepared queries.
            7  +
            8  +create or replace temp view parsavpg_post_react_counts as (
            9  +	with counts as (
           10  +		select a.kind, p.id as subject, count(*) as ct from parsav_acts as a
           11  +			inner join parsav_posts as p on p.id = a.subject
           12  +		group by a.kind, p.id
           13  +	)
           14  +
           15  +	select p.id as post,
           16  +		coalesce((select counts.ct from counts where counts.subject = p.id
           17  +			and counts.kind = 'like'),0)::integer as likes,
           18  +		coalesce((select counts.ct from counts where counts.subject = p.id
           19  +			and counts.kind = 'rt'  ),0)::integer as rts
           20  +	from parsav_posts as p
           21  +);
           22  +
           23  +create type pg_temp.parsavpg_intern_notice as (
           24  +	kind	smallint,
           25  +	"when"	bigint,
           26  +	who		bigint,
           27  +	what	bigint,
           28  +	reply	bigint,
           29  +	reaction text
           30  +);
           31  +
           32  +create type pg_temp.parsavpg_intern_actor as (
           33  +	id			bigint,
           34  +	nym			text,
           35  +	handle		text,
           36  +	origin		bigint,
           37  +	bio			text,
           38  +	avataruri	text,
           39  +	rank		smallint,
           40  +	quota		integer,
           41  +	key			bytea,
           42  +	epithet		text,
           43  +	knownsince	bigint,
           44  +	xid			text,
           45  +	invites		integer
           46  +);
           47  +
           48  +create or replace function
           49  +pg_temp.parsavpg_translate_actor(parsav_actors)
           50  +returns pg_temp.parsavpg_intern_actor as $$
           51  +	select
           52  +		($1).id,        ($1).nym,  ($1).handle, ($1).origin, ($1).bio,
           53  +		($1).avataruri, ($1).rank, ($1).quota,  ($1).key,    ($1).epithet,
           54  +		($1).knownsince::bigint,
           55  +		coalesce(($1).handle || '@' ||
           56  +				(select domain from parsav_servers as s where s.id = ($1).origin),
           57  +			'@' || ($1).handle) as xid,
           58  +		($1).invites
           59  +$$ language sql;
           60  +
           61  +--drop type if exists pg_temp.parsavpg_intern_post;
           62  +create type pg_temp.parsavpg_intern_post as (
           63  +	-- order is crucially important, and must match the order used
           64  +	-- in row_to_actor. names don't matter
           65  +	localpost	bool,
           66  +	id			bigint,
           67  +	author		bigint,
           68  +	subject		text,
           69  +	acl			text,
           70  +	body		text,
           71  +	posted		bigint,
           72  +	discovered	bigint,
           73  +	edited		bigint,
           74  +	parent		bigint,
           75  +	convoheaduri text,
           76  +	chgcount	integer,
           77  +	accent		smallint,
           78  +	rtdby		bigint, -- note that these must be 0 if the record
           79  +	rtid		bigint, -- in question does not represent an RT!
           80  +	n_likes		integer,
           81  +	n_rts		integer
           82  +);
           83  +
           84  +create or replace function
           85  +pg_temp.parsavpg_translate_post(parsav_posts,bigint,bigint)
           86  +returns pg_temp.parsavpg_intern_post as $$
           87  +	select a.origin is null,
           88  +		($1).id,     ($1).author,
           89  +		($1).subject,($1).acl,         ($1).body,
           90  +		($1).posted, ($1).discovered,  ($1).edited,
           91  +		($1).parent, ($1).convoheaduri,($1).chgcount,
           92  +		coalesce(c.value, -1)::smallint,
           93  +		$2 as rtdby, $3 as rtid,
           94  +		re.likes, re.rts
           95  +	from parsav_actors as a 
           96  +		left join parsav_actor_conf_ints as c
           97  +		          on c.key = 'ui-accent' and
           98  +		             c.uid = a.id
           99  +		left join pg_temp.parsavpg_post_react_counts as re
          100  +		          on re.post = ($1).id
          101  +	where a.id = ($1).author
          102  +$$ language sql;
          103  +
          104  +create or replace temp view parsavpg_known_content as (
          105  +	with posts as (
          106  +		select p as orig,
          107  +			null::bigint as promoter,
          108  +			null::bigint as promotion,
          109  +			coalesce(p.posted,p.discovered) as promotime
          110  +		from parsav_posts as p
          111  +	),
          112  +
          113  +	rts as (
          114  +		select p as orig,
          115  +			a.actor as promoter,
          116  +			a.id    as promotion,
          117  +			a.time as  promotime
          118  +		from parsav_acts as a
          119  +			inner join parsav_posts as p on a.subject = p.id
          120  +		where a.kind = 'rt'
          121  +	),
          122  +
          123  +	content as (select * from posts union select * from rts)
          124  +
          125  +	select pg_temp.parsavpg_translate_post(cn.orig,
          126  +			coalesce(cn.promoter,0), coalesce(cn.promotion,0)
          127  +		) as post,
          128  +		cn.promotime::bigint as tltime,
          129  +		coalesce(cn.promoter, (cn.orig).author) as promoter
          130  +	from content as cn
          131  +	order by cn.promotime desc
          132  +);
          133  +
          134  +--
          135  +--create temp view parsavpg_post_threads as (
          136  +--
          137  +--);
          138  +--
          139  +create temp view parsavpg_notices as (
          140  + -- TODO add mentions
          141  +	with ntimes as (
          142  +		select uid, value as when from parsav_actor_conf_ints where key = 'notice-clear-time'
          143  +	), acts as (
          144  +		select row(
          145  +				kmap.kind::smallint,
          146  +				a.time,
          147  +				a.actor,
          148  +				a.subject,
          149  +				null::bigint,
          150  +				null::text
          151  +			)::pg_temp.parsavpg_intern_notice as notice,
          152  +			p.author as rcpt
          153  +		from parsav_acts as a
          154  +			inner join parsav_posts as p on a.subject = p.id
          155  +			inner join (values
          156  +				('rt',    4 ),
          157  +				('like',  3 ),
          158  +				('react', 5 )
          159  +			) as kmap(kstr,kind) on kmap.kstr = a.kind
          160  +			left  join ntimes as nt on nt.uid = p.author
          161  +		where a.time >= coalesce(nt.when,0)
          162  +	), replies as (
          163  +		select row(
          164  +				2::smallint,
          165  +				coalesce(p.posted,p.discovered),
          166  +				p.author,
          167  +				p.parent,
          168  +				p.id,
          169  +				null::text
          170  +			)::pg_temp.parsavpg_intern_notice as notice,
          171  +			par.author as rcpt
          172  +		from parsav_posts as p
          173  +			inner join parsav_posts as par on p.parent = par.id
          174  +			left  join ntimes as nt on nt.uid = p.author
          175  +		where p.discovered >= coalesce(nt.when,0)
          176  +	), allnotices as (select * from acts union select * from replies)
          177  +
          178  +	table allnotices order by (notice).when desc
          179  +);
          180  +

Modified backend/schema/pgsql.sql from [347a4ab533] to [29d1b18fbc].

    12     12   --	('policy-self-register',:'regpol'),
    13     13   --	('master',:'admin'),
    14     14   
    15     15   -- note that valid ids should always > 0, as 0 is reserved for null
    16     16   -- on the client side, vastly simplifying code
    17     17   create table parsav_servers (
    18     18   	id     bigint primary key default (1+random()*(2^63-1))::bigint,
    19         -	domain text not null,
           19  +	domain text not null unique,
    20     20   	key    bytea,
    21         -	knownsince timestamp,
           21  +	knownsince bigint,
    22     22   	parsav boolean -- whether to use parsav protocol extensions
    23     23   );
    24     24   
    25     25   create table parsav_actors (
    26     26   	id        bigint primary key default (1+random()*(2^63-1))::bigint,
    27     27   	nym       text,
    28     28   	handle    text not null, -- nym [@handle@origin] 
    29     29   	origin    bigint references parsav_servers(id)
    30     30   		on delete cascade, -- null origin = local actor
    31         -	knownsince timestamp not null default now(),
           31  +	knownsince bigint not null,
    32     32   	bio       text,
    33     33   	avatarid  bigint, -- artifact id, null if remote
    34     34   	avataruri text, -- null if local
    35     35   	rank      smallint not null default 0,
    36     36   	quota     integer not null default 1000,
    37     37   	invites   integer not null default 0,
    38     38   	key       bytea, -- private if localactor; public if remote
    39     39   	epithet   text,
    40         -	authtime  timestamp not null default now(), -- cookies earlier than this timepoint will not be accepted
           40  +	authtime  bigint not null, -- cookies earlier than this timepoint will not be accepted
    41     41   	
    42     42   	unique (handle,origin)
    43     43   );
    44     44   
    45     45   create table parsav_rights (
    46     46   	key text,
    47     47   	actor bigint references parsav_actors(id)
    48     48   		on delete cascade,
    49     49   	allow boolean not null,
    50     50   	scope bigint, -- for future expansion
    51     51   
    52     52   	primary key (key,actor)
    53     53   );
           54  +create index on parsav_rights (actor);
    54     55   
    55     56   create table parsav_posts (
    56     57   	id         bigint primary key default (1+random()*(2^63-1))::bigint,
    57         -	author     bigint references parsav_actors(id)
    58         -		on delete cascade,
           58  +	author     bigint references parsav_actors(id) on delete cascade,
    59     59   	subject    text,
    60     60   	acl        text not null default 'all', -- just store the script raw 🤷
    61     61   	body       text,
    62         -	posted     timestamp not null,
    63         -	discovered timestamp not null,
           62  +	posted     bigint not null,
           63  +	discovered bigint not null,
    64     64   	chgcount   integer not null default 0,
    65         -	edited     timestamp,
           65  +	edited     bigint,
    66     66   	parent     bigint not null default 0, -- if post: part of conversation; if chatroom: top-level post
    67     67   	circles    bigint[], -- TODO at edit or creation, iterate through each circle
    68     68   	mentions   bigint[], -- a user has, check if it can see her post, and if so add
    69     69   	artifacts  bigint[],
    70     70   
    71     71   	convoheaduri text
    72     72   	-- only used for tracking foreign conversations and tying them to post heads;
    73     73   	-- local conversations are tracked directly and mapped to URIs based on the
    74     74   	-- head's ID. null if native tweet or not the first tweet in convo
    75     75   );
           76  +create index on parsav_posts (author);
           77  +create index on parsav_posts (parent);
    76     78   
    77     79   create table parsav_rels (
    78     80   	relator bigint references parsav_actors(id)
    79     81   		on delete cascade, -- e.g. follower
    80     82   	relatee bigint references parsav_actors(id)
    81     83   		on delete cascade, -- e.g. followed
    82     84   	kind    smallint, -- e.g. follow, block, mute
................................................................................
    83     85   
    84     86   	primary key (relator, relatee, kind)
    85     87   );
    86     88   
    87     89   create table parsav_acts (
    88     90   	id      bigint primary key default (1+random()*(2^63-1))::bigint,
    89     91   	kind    text not null, -- like, rt, react, so on
    90         -	time    timestamp not null default now(),
    91         -	actor   bigint references parsav_actors(id)
    92         -		on delete cascade,
           92  +	time    bigint not null,
           93  +	actor   bigint references parsav_actors(id) on delete cascade,
    93     94   	subject bigint, -- may be post or act, depending on kind
    94     95   	body	text -- emoji, if react
    95     96   );
           97  +create index on parsav_acts (subject);
           98  +create index on parsav_acts (actor);
           99  +create index on parsav_acts (time);
    96    100   
    97    101   create table parsav_log (
    98    102   	-- accesses are tracked for security & sending delete acts
    99    103   	id    bigint primary key default (1+random()*(2^63-1))::bigint,
   100         -	time  timestamp not null default now(),
          104  +	time  bigint not null,
   101    105   	actor bigint references parsav_actors(id)
   102    106   		on delete cascade,
   103    107   	post  bigint not null
   104    108   );
   105    109   
   106    110   create table parsav_artifacts (
   107    111   	id          bigint primary key default (1+random()*(2^63-1))::bigint,
   108         -	birth       timestamp not null default now(),
          112  +	birth       bigint not null,
   109    113   	content     bytea, -- if null, this is a "ban record" preventing content matching the hash from being re-uploaded
   110    114   	hash		bytea unique not null, -- sha256 hash of content
   111    115   	-- it would be cool to use a computed column for this, but i don't want
   112    116   	-- to lock people into PG12 or drag in the pgcrypto extension just for this
   113    117   	mime        text -- null if unknown, will be reported as x-octet-stream
   114    118   );
   115    119   create index on parsav_artifacts (mime);
   116    120   
   117    121   create table parsav_artifact_claims (
   118         -	birth timestamp not null default now(),
          122  +	birth bigint not null,
   119    123   	uid bigint references parsav_actors(id) on delete cascade,
   120    124   	rid bigint references parsav_artifacts(id) on delete cascade,
   121    125   	description text,
   122    126   	folder text,
   123    127   
   124    128   	unique (uid,rid)
   125    129   );
   126    130   create index on parsav_artifact_claims (uid);
          131  +create index on parsav_artifact_claims (uid,folder);
   127    132   
   128    133   create table parsav_circles (
   129    134   	id          bigint primary key default (1+random()*(2^63-1))::bigint,
   130    135   	owner       bigint not null references parsav_actors(id) on delete cascade,
   131    136   	name        text not null,
   132    137   	members     bigint[] not null default array[]::bigint[],
   133    138   
   134    139   	unique (owner,name)
   135    140   );
          141  +create index on parsav_circles (owner);
   136    142   
   137    143   create table parsav_rooms (
   138    144   	id          bigint primary key default (1+random()*(2^63-1))::bigint,
   139    145   	origin		bigint references parsav_servers(id) on delete cascade,
   140    146   	name		text not null,
   141    147   	description text not null,
   142    148   	policy      smallint not null
................................................................................
   146    152   	room   bigint not null references parsav_rooms(id) on delete cascade,
   147    153   	member bigint not null references parsav_actors(id) on delete cascade,
   148    154   	rank   smallint not null default 0,
   149    155   	admin  boolean not null default false, -- non-admins with rank can only moderate + invite
   150    156   	title  text, -- admin-granted title like reddit flair
   151    157   	vouchedby bigint references parsav_actors(id) on delete set null
   152    158   );
          159  +create index on parsav_room_members (member);
          160  +create index on parsav_room_members (room);
   153    161   
   154    162   create table parsav_invites (
   155    163   	id          bigint primary key default (1+random()*(2^63-1))::bigint,
   156    164   	-- when a user is created from an invite, the invite is deleted and the invite
   157    165   	-- ID becomes the user ID. privileges granted on the invite ID during the invite
   158    166   	-- process are thus inherited by the user
   159    167   	issuer bigint references parsav_actors(id) on delete set null,
................................................................................
   164    172   
   165    173   create table parsav_sanctions (
   166    174   	id     bigint primary key default (1+random()*(2^63-1))::bigint,
   167    175   	issuer bigint references parsav_actors(id) on delete set null,
   168    176   	scope  bigint, -- can be null or room for local actions
   169    177   	nature smallint not null, -- silence, suspend, disemvowel, censor, noreply, etc
   170    178   	victim bigint not null, -- can be user, room, or post
   171         -	expire timestamp, -- auto-expires if set
   172         -	review timestamp,  -- brings up for review at given time if set
          179  +	expire bigint, -- auto-expires if set
          180  +	review bigint,  -- brings up for review at given time if set
   173    181   	reason text, -- visible to victim if set
   174         -	context text -- admin-only note
          182  +	context text, -- admin-only note
          183  +	appeal text -- null if no appeal lodged
   175    184   );
          185  +create index on parsav_sanctions (victim,scope);
          186  +create index on parsav_sanctions (issuer);
   176    187   
   177    188   create table parsav_actor_conf_strs (
   178    189   	uid bigint not null references parsav_actors(id) on delete cascade,
   179    190   	key text not null, value text not null, unique (uid,key)
   180    191   );
   181    192   create table parsav_actor_conf_ints (
   182    193   	uid bigint not null references parsav_actors(id) on delete cascade,
   183    194   	key text not null, value bigint not null, unique (uid,key)
   184    195   );
   185    196   
   186    197   -- create a temporary managed auth table; we can delete this later
   187    198   -- if it ends up being replaced with a view
   188    199   %include pgsql-auth.sql%

Modified doc/usr.md from [61c1a08ae0] to [614bf60d7c].

    49     49    * **crier:** the user can promote content to the instance page (and thus the archives). note that by default only one post can be promoted per crier per day, though this can be changed (see [server configuration](srvcfg)).
    50     50    * **elevate:** the user can increase the rank of lower-ranking actors up to one rank below his own, and can grant powers that he already possesses.
    51     51    * **demote:** the user can decrease the rank of lower-ranking actors or strip them of rank entirely, and can revoke powers that she too possesses.
    52     52    * **censor:** the user can eliminate undesirable content, remove posts from the instance page, and respond to badthink reports, whether by dismissing the report, by suppressing (but not deleting) the post in question, or by referring the matter upwards to someone with the discipline power. on smaller instances, moderators should probably hold this power and the discipline power simultaneously; on larger ones, it may be best to separate the two.
    53     53    * **discipline:** the user can place *sanctions* on lower-ranking actors and cancel pending invites. sanctions are (usually temporary) [punishments](discipline) that strip certain abilities (or suspend certain conversations), and are intended as a less extreme, more flexible means of dealing with toxic behavior. most moderators should possess this power rather than `elevate` or `demote`, as sanctions leave a paper trail and can be summarily vacated by users of equal or higher rank with the `vacate` power. `discipline` also grants various other disciplinary abilities, such as issuing *demerits,* which can result in various penalties
    54     54    * **vacate:** the user can rehabilitate disciplined actors, vacating sanctions, voiding demerits, and issuing temporary reprieves from restrictions.
    55     55    * **purge:** the user can completely destroy lower-ranking accounts and all associated content, removing them entirely from the instance. best to keep this one for yourself.
    56         - * **invite:** the user can issue invites without depleting their invite supply, even if they have none at all. users with both the `invite` and `elevate` powers can grant invites to others.
           56  + * **invite:** the user can issue invites and create accounts without depleting their invite supply, even if they have none at all. users with both the `invite` and `elevate` powers can grant invites to others.
    57     57    * **cred:** the user can add, change, and remove the credentials of lower-ranking users (think password resets).
    58     58    * **config:** grants access to technical and security-related server settings, like `bind` or `domain`. be aware that changes made by users with this power affect *all* users, regardless of rank, and may affect how certain other powers function.
    59     59    * **rebrand:** grants access to server settings that affect the appearance and livery of the site, like the `ui-accent` setting, the instance name, or the content of the instance page.
    60     60   
    61     61   powers can be granted and revoked through the online interface, in the `users` section. they can also be controlled using the command line tool, with the commands `parsav user <handle> grant <power>…` and `revoke <power>…` (`all` can be used instead of a list of powers to grant or strip all powers simultaneously)
    62     62   
    63     63   ### recommendations

Modified parsav.t from [2bbe093dad] to [5b9672ebb4].

   183    183   	var now = lib.osclock.time(nil)
   184    184   	var diff = now - lib.noise.lasttime
   185    185   	if diff > 30 then -- print cur time
   186    186   		lib.noise.lasttime = now
   187    187   		var curtime: int8[26]
   188    188   		lib.osclock.ctime_r(&now, &curtime[0])
   189    189   		for i=0,26 do if curtime[i] == @'\n' then curtime[i] = 0 break end end -- :/
   190         -		[ lib.emit(false, 2, '\27[1m[', `&curtime[0], ']\27[;36m\n +00 ') ]
          190  +		[ lib.emit(false, 2, '\27[1m', `&curtime[0], '\27[;36m\n +00 ') ]
   191    191   	else -- print time since last msg
   192    192   		var dfs = arrayof(int8, 0x30 + diff/10, 0x30 + diff%10, 0x20, 0)
   193    193   		[ lib.emit(false, 2, ' \27[36m+', `&dfs[0]) ]
   194    194   	end
   195    195   end
   196    196   
   197    197   local defrep = function(level,n,code)
................................................................................
   235    235   	return n
   236    236   end)
   237    237   lib.enum = function(tbl)
   238    238   	local ty = uint8
   239    239   	if #tbl >= 2^32 then ty = uint64 -- hey, can't be too safe
   240    240   	elseif #tbl >= 2^16 then ty = uint32
   241    241   	elseif #tbl >= 2^8 then ty = uint16 end
   242         -	local o = { t = ty }
          242  +	local o = { t = ty, members = tbl }
   243    243   	local strings = {}
   244    244   	for i, name in ipairs(tbl) do
   245    245   		o[name] = i - 1
   246    246   		strings[i] = `[lib.mem.ref(int8)]{ptr=[name], ct=[#name]}
   247    247   	end
   248    248   	o._str = terra(val: ty)
   249    249   		var l = array([strings])
................................................................................
   271    271   			if (self._store[i/8] and (1 << i % 8)) ~= 0 then ct = ct + 1 end
   272    272   		end
   273    273   		return ct
   274    274   	end
   275    275   	set.methods.dump = macro(function(self)
   276    276   		local q = quote lib.io.say('dumping set:\n') end
   277    277   		for i,v in ipairs(tbl) do
   278         -			q = quote
   279         -				[q]
          278  +			q = quote [q]
   280    279   				if [bool](self.[v])
   281    280   					then lib.io.say([' - ' .. v .. ': true\n'])
   282    281   					else lib.io.say([' - ' .. v .. ': false\n'])
   283    282   				end
   284    283   			end
   285    284   		end
   286    285   		return q

Modified render/conf/users.t from [4bed391611] to [0f343f8c98].

    14     14   		case [uint16](2) then acc:lpush('🔱') end
    15     15   		case [uint16](3) then acc:lpush('⚜️') end
    16     16   		case [uint16](4) then acc:lpush('🗡') end
    17     17   		case [uint16](5) then acc:lpush('🗝') end
    18     18   		else acc:lpush('🕴')
    19     19   	end
    20     20   end
           21  +
           22  +local rnd = lib.crypt.random
           23  +local terra 
           24  +suggest_handle(a: &lib.str.acc)
           25  +	var start = a.sz
           26  +	var puncts = array('.','_','-')
           27  +	var xXx = rnd(uint8, 0, 9) == 0
           28  +	var leet = rnd(uint8, 0, 8) == 0
           29  +	var caps = rnd(uint8, 0, 5)
           30  +	var punct: rawstring = nil
           31  +	var useadj = rnd(uint8, 0, 4) == 0
           32  +	if rnd(uint8, 0, 4) == 0 then
           33  +		punct = puncts[rnd(intptr,0,[puncts.type.N])]
           34  +	end
           35  +
           36  +	var nouns = array(
           37  +		'thunder','bride','blaze','doom','squad','gun','lord','blaster',
           38  +		'fuck','hell','hound','piss','shit','killa','terror', 'horror',
           39  +		'fear', 'slaughter','murder','general','commander', 'commissar',
           40  +		'terrorist','infinity','slut','cunt','whore','bitch', 'bastard',
           41  +		'cock','prince','princess','pimp','gay','cop','slayer', 'vampire',
           42  +		'vampyre','blood','pain','brute','wolf','sword','star','sun','moon',
           43  +		'killer','murderer','thief','arson','fire','ice','frost','hack',
           44  +		'hacker','god','master','mistress','slave','rage','freeze','flayer',
           45  +		'pirate','ninja','shadow','fog','mist','misery','glory','bear',
           46  +		'king','queen','empress','emperor','majesty','space','martian',
           47  +		'winter','fall','monk','katana','420','warrior','banana','demon',
           48  +		'devil','ghost','wraith','cuck','legend','hero','heroine','goblin',
           49  +		'gremlin','troll','dragon','evil','overlord','radiance'
           50  +	)
           51  +	var adjs = array(
           52  +		'dark','super','supreme','ultra','ultimate','total','infinite',
           53  +		'omnipotent','crazy','final','deathless','immortal', 'elite',
           54  +		'leet','1337','bloody','fearless','headless','screaming','insane',
           55  +		'brutal','legendary','space','frozen','flaming','burning',
           56  +		'mighty','flayed','hidden','secret','lost','mystery','glorious',
           57  +		'nude','naked','bare','first','radiant','martian','fallen',
           58  +		'wandering','dank','demonic','satanic','invisible','based','woke',
           59  +		'deadly','lethal','heroic','evil','majestic','luminous'
           60  +	)
           61  +
           62  +	if xXx then a:lpush('xXx_') end
           63  +
           64  +	if useadj then
           65  +		var len = rnd(uint8,1,3) 
           66  +		for i = 0, len do
           67  +			var sz = a.sz
           68  +			a:push(adjs[rnd(intptr,0,[adjs.type.N])], 0)
           69  +			if punct ~= nil then a:push(punct, 1) end
           70  +			if caps == 1 then
           71  +				a.buf[sz] = lib.str.cupcase(a.buf[sz])
           72  +			end
           73  +		end
           74  +	end
           75  +	var nounct = rnd(uint8,1,3) 
           76  +	for i = 0, nounct do
           77  +		var sz = a.sz
           78  +		a:push(nouns[rnd(intptr,0,[nouns.type.N])], 0)
           79  +		if punct ~= nil and i+1 ~= nounct then a:push(punct, 1) end
           80  +		if caps == 1 then
           81  +			a.buf[sz] = lib.str.cupcase(a.buf[sz])
           82  +		end
           83  +	end
           84  +
           85  +	if leet or caps == 2 then for i=start, a.sz do
           86  +		if caps == 2 and rnd(uint8,0,5)==0 then
           87  +			a.buf[i] = lib.str.cupcase(a.buf[i])
           88  +		end
           89  +		if leet then 
           90  +			switch lib.str.cdowncase(a.buf[i]) do
           91  +				case [uint8]([string.byte('e')]) then a.buf[i] = @'3' end
           92  +				case [uint8]([string.byte('i')]) then a.buf[i] = @'1' end
           93  +				case [uint8]([string.byte('l')]) then a.buf[i] = @'1' end
           94  +				case [uint8]([string.byte('t')]) then a.buf[i] = @'7' end
           95  +				case [uint8]([string.byte('s')]) then a.buf[i] = @'5' end
           96  +				case [uint8]([string.byte('o')]) then a.buf[i] = @'0' end
           97  +				case [uint8]([string.byte('b')]) then a.buf[i] = @'6' end
           98  +			end
           99  +		end
          100  +	end end
          101  +
          102  +	if (nounct == 1 and not useadj) or rnd(uint8, 0, 5) == 0 then
          103  +		if punct ~= nil then a:push(punct, 1) end
          104  +		a:ipush(rnd(uint16,0,65535))
          105  +	end
          106  +
          107  +	if xXx then a:lpush('_xXx') end
          108  +
          109  +end
    21    110   
    22    111   local push_num_field = macro(function(acc,name,lbl,min,max,value,disable)
    23    112   	name = name:asvalue()
    24    113   	lbl = lbl:asvalue()
    25    114   	local start = '<div class="elem small">'
    26    115   	local enabled = start .. string.format('<label for="%s">%s</label><input type="number" id="%s" name="%s" min="', name, lbl, name, name)
    27    116   	local disabled = start .. string.format('<label>%s</label><div class="txtbox">', lbl)
................................................................................
   219    308   			else
   220    309   				ulst:lpush('<span class="id">')
   221    310   				lib.render.nym(usr, 0, &ulst, false)
   222    311   				ulst:lpush('</span></li>')
   223    312   			end
   224    313   		::skip::end
   225    314   		ulst:lpush('</ul>')
          315  +
          316  +		if co.who.rights.powers.invite() or co.who.rights.invites > 0 then
          317  +			ulst:lpush('<details><summary>create new user</summary><form method="post"><div class="elem"><label for="handle">handle</label><input type="text" name="handle" id="handle" placeholder="')
          318  +			suggest_handle(&ulst)
          319  +			ulst:lpush('"></div><button name="act" value="create">create</button></form></details>')
          320  +		end
          321  +		ulst:lpush('<details><summary>instantiate remote actor</summary><form method="post"><div class="elem"><label for="xid">xid</label><input type="text" name="xid" id="xid" placeholder="tweetlord@website.tld"></div><button name="act" value="inst">instantiate</button></form></details>')
          322  +
   226    323   		return ulst:finalize()
   227    324   	end
   228    325   	do return pstr.null() end
   229    326   	::e404:: co:complain(404, 'not found', 'there is no user or resource by that identifier on this server') goto quit
   230    327   	::e403:: co:complain(403, 'forbidden', 'you do not have sufficient authority to control that resource')
   231    328   
   232    329   	::quit:: return pstr.null()
   233    330   end
   234    331   
   235    332   return render_conf_users

Modified render/profile.t from [531b5ac1cf] to [edefc21455].

     1      1   -- vim: ft=terra
     2      2   local pstr = lib.mem.ptr(int8)
     3      3   local terra cs(s: rawstring)
     4      4   	return pstr { ptr = s, ct = lib.str.sz(s) }
     5      5   end
     6      6   
     7      7   local terra 
     8         -render_profile(co: &lib.srv.convo, actor: &lib.store.actor)
            8  +render_profile(
            9  +	co: &lib.srv.convo,
           10  +	actor: &lib.store.actor,
           11  +	relationship: &lib.store.relationship
           12  +): pstr
     9     13   	var aux: lib.str.acc
    10     14   	var followed = false -- FIXME
    11     15   	if co.aid ~= 0 and co.who.id == actor.id then
    12     16   		aux:compose('<a accesskey="a" class="button" href="/conf/profile?go=/@',actor.handle,'">alter</a>')
    13     17   	elseif co.aid ~= 0 then
    14         -		if not followed then
           18  +		if not relationship.rel.follow() then
    15     19   			aux:compose('<button accesskey="f" method="post" class="pos" name="act" value="follow">follow</button>')
    16         -		elseif followed then
           20  +		elseif relationship.rel.follow() then
    17     21   			aux:compose('<button accesskey="f" method="post" class="neg" name="act" value="unfollow">unfollow</button>')
    18     22   		end
    19     23   		aux:lpush('<a accesskey="h" class="button" href="/'):push(actor.xid,0):lpush('/chat">chat</a>')
    20     24   		if co.who.rights.powers:affect_users() and co.who:overpowers(actor) then
    21     25   			aux:lpush('<a accesskey="n" class="button" href="/'):push(actor.xid,0):lpush('/ctl">control</a>')
    22     26   		end
    23     27   	else
................................................................................
    67     71   		end
    68     72   
    69     73   		if co.who:outranks(actor) then
    70     74   			comments:lpush('<li style="--co:50">underling</li>')
    71     75   		elseif actor:outranks(co.who) then
    72     76   			comments:lpush('<li style="--co:-50">outranks you</li>')
    73     77   		end
           78  +
           79  +		if relationship.recip.follow() then
           80  +			comments:lpush('<li style="--co:30">follows you</li>')
           81  +		end
    74     82   	end
    75     83   
    76     84   	var profile = data.view.profile {
    77     85   		nym = fullname;
    78     86   		bio = bio;
    79     87   		xid = cs(actor.xid);
    80     88   		avatar = cs(actor.avatar);

Modified render/tweet.t from [d7574b82d8] to [8bee6c6ab8].

     7      7   local terra 
     8      8   push_promo_header(co: &lib.srv.convo, acc: &lib.str.acc, rter: &lib.store.actor, rid: uint64)
     9      9   	acc:lpush('<div class="lede"><div class="promo"><img src="'):push(rter.avatar,0):lpush('"><a href="/')
    10     10   	if rter.origin ~= 0 then acc:lpush('@') end
    11     11   	acc:push(rter.xid,0):lpush('" class="username">')
    12     12   	lib.render.nym(rter, 0, acc, true)
    13     13   	acc:lpush('</a> retweeted</div>')
    14         -	if co.who.id == rter.id then
           14  +	if co.aid ~= 0 and co.who.id == rter.id then
    15     15   		acc:lpush('<a href="/post/'):shpush(rid):lpush('/del" class="del">✖</a>')
    16     16   	end
    17     17   end
    18     18   			
    19     19   local terra 
    20     20   render_tweet(co: &lib.srv.convo, p: &lib.store.post, acc: &lib.str.acc)
    21     21   	var author: &lib.store.actor = nil
................................................................................
    35     35   	end
    36     36   
    37     37   	::foundauth::
    38     38   	var avistr: lib.str.acc if author.origin == 0 then
    39     39   		avistr:compose('/avi/',author.handle)
    40     40   	end
    41     41   	var timestr: int8[26] lib.osclock.ctime_r(&p.posted, &timestr[0])
           42  +	for i=0,26 do if timestr[i] == @'\n' then timestr[i] = 0 break end end -- 🙄
    42     43   
    43     44   	var bhtml = lib.smackdown.html([lib.mem.ptr(int8)] {ptr=p.body,ct=0})
    44     45   	defer bhtml:free()
    45     46   
    46     47   	var idbuf: int8[lib.math.shorthand.maxlen]
    47     48   	var idlen = lib.math.shorthand.gen(p.id, idbuf)
    48     49   	var permalink: lib.str.acc permalink:compose('/post/',{idbuf,idlen})
................................................................................
    57     58   		permalink = permalink:finalize();
    58     59   		attr = pstr{'',0};
    59     60   		stats = pstr{'',0};
    60     61   	}
    61     62   	if p.rts + p.likes > 0 then
    62     63   		var s: lib.str.acc s:init(128)
    63     64   		s:lpush('<div class="stats">')
    64         -		if p.rts   > 0 then s:lpush('<div class="rt">'  ):ipush(p.rts  ):lpush('</div>') end
    65         -		if p.likes > 0 then s:lpush('<div class="like">'):ipush(p.likes):lpush('</div>') end
           65  +		if p.rts   > 0 then s:lpush('<div class="rt">'  ):dpush(p.rts  ):lpush('</div>') end
           66  +		if p.likes > 0 then s:lpush('<div class="like">'):dpush(p.likes):lpush('</div>') end
    66     67   		s:lpush('</div>')
    67     68   		tpl.stats = s:finalize()
    68     69   	end
    69     70   
    70         -	var attrbuf: int8[48]
           71  +	var attrbuf: int8[64]
    71     72   	var attrcur = &attrbuf[0]
    72     73   	if p.accent ~= -1 and p.accent ~= co.ui_hue then
    73     74   		var hdecbuf: int8[21]
    74     75   		var hdec = lib.math.decstr(p.accent, &hdecbuf[20])
    75     76   		attrcur = lib.str.cpy(attrcur,' style="--hue:')
    76     77   		attrcur = lib.str.cpy(attrcur, hdec)
    77     78   		-- var len = &hdecbuf[20] - hdec 
    78     79   		attrcur = lib.str.cpy(attrcur, '"')
    79     80   	end
    80         -	if p.author == co.who.id then attrcur = lib.str.cpy(attrcur, ' data-own') end
           81  +	if co.aid ~= 0 and p.author == co.who.id then attrcur = lib.str.cpy(attrcur, ' data-own') end
           82  +	if retweeter ~= nil then attrcur = lib.str.cpy(attrcur, ' data-rt') end
    81     83   
    82     84   	if attrcur ~= &attrbuf[0] then tpl.attr = &attrbuf[0] end
    83     85   
    84     86   	defer tpl.permalink:free()
    85     87   	if acc ~= nil then
    86     88   		if retweeter ~= nil then push_promo_header(co, acc, retweeter, p.rtact) end
    87     89   		tpl:append(acc)

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

     1      1   -- vim: ft=terra
     2      2   local terra 
     3         -render_userpage(co: &lib.srv.convo, actor: &lib.store.actor)
            3  +render_userpage(
            4  +	co          : &lib.srv.convo,
            5  +	actor       : &lib.store.actor,
            6  +	relationship: &lib.store.relationship
            7  +): {}
     4      8   	var ti: lib.str.acc 
     5      9   	if co.aid ~= 0 and co.who.id == actor.id then
     6     10   		ti:compose('my profile')
     7     11   	else
     8     12   		ti:compose('profile :: ', actor.handle)
     9     13   	end
    10     14   	var tiptr = ti:finalize()
    11     15   
    12     16   	var acc: lib.str.acc acc:init(1024)
    13         -	var pftxt = lib.render.profile(co,actor) defer pftxt:free()
           17  +	var pftxt = lib.render.profile(co,actor,relationship) defer pftxt:free()
    14     18   	acc:ppush(pftxt)
    15     19   
    16     20   	var stoptime = lib.osclock.time(nil)
    17     21   	var posts = co.srv:post_enum_author_uid(actor.id, lib.store.range {
    18     22   		mode = 1; -- T->I
    19     23   		from_time = stoptime;
    20     24   		to_idx = 64;

Modified route.t from [666eb021ed] to [cc5cf170ae].

     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      9   terra meth_get(meth: method.t) return (meth == method.get) or (meth == method.head) end
           10  +
           11  +terra http.actor_profile(co: &lib.srv.convo, actor: &lib.store.actor, meth: method.t)
           12  +	var rel: lib.store.relationship
           13  +	if co.aid ~= 0 then
           14  +		rel = co.srv:actor_rel_calc(co.who.id, actor.id)
           15  +		if meth == method.post then
           16  +			var act = co:ppostv('act')
           17  +			if act:cmp(lib.str.plit 'follow') and not rel.rel.follow() then
           18  +				if rel.recip.block() then
           19  +					co:complain(403,'blocked','you cannot follow a user you are blocked by') return
           20  +				end
           21  +				(rel.rel.follow << true)
           22  +				co.srv:actor_rel_create([lib.store.relation.idvmap.follow], co.who.id, actor.id)
           23  +			elseif act:cmp(lib.str.plit 'unfollow') and rel.rel.follow() then
           24  +				(rel.rel.follow << false)
           25  +				co.srv:actor_rel_destroy([lib.store.relation.idvmap.follow], co.who.id, actor.id)
           26  +			end
           27  +		end
           28  +	else
           29  +		rel.rel:clear()
           30  +		rel.recip:clear()
           31  +	end
           32  +
           33  +	lib.render.user_page(co, actor, &rel)
           34  +end
           35  +
    10     36   terra http.actor_profile_xid(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t)
    11     37   	var handle = [lib.mem.ptr(int8)] { ptr = &uri.ptr[2], ct = 0 }
    12     38   	for i=2,uri.ct do
    13     39   		if uri.ptr[i] == @'/' or uri.ptr[i] == 0 then handle.ct = i - 2 break end
    14     40   	end
    15     41   	if handle.ct == 0 then
    16     42   		handle.ct = uri.ct - 2
................................................................................
    27     53   	var actor = co.srv:actor_fetch_xid(handle)
    28     54   	if actor.ptr == nil then
    29     55   		co:complain(404,'no such user','no such user known to this server')
    30     56   		return
    31     57   	end
    32     58   	defer actor:free()
    33     59   
    34         -	lib.render.user_page(co, actor.ptr)
           60  +	http.actor_profile(co,actor.ptr,meth)
    35     61   end
    36     62   
    37     63   terra http.actor_profile_uid (
    38     64   	co: &lib.srv.convo,
    39     65   	path: lib.mem.ptr(lib.mem.ref(int8)),
    40     66   	meth: method.t
    41     67   )
................................................................................
    53     79   	var actor = co.srv:actor_fetch_uid(uid)
    54     80   	if actor.ptr == nil then
    55     81   		co:complain(404, 'no such user', 'no user by that ID is known to this instance')
    56     82   		return
    57     83   	end
    58     84   	defer actor:free()
    59     85   
    60         -	lib.render.user_page(co, actor.ptr)
           86  +	http.actor_profile(co,actor.ptr,meth)
    61     87   end
    62     88   
    63     89   terra http.login_form(co: &lib.srv.convo, meth: method.t)
    64     90   	if meth_get(meth) then
    65     91   		-- request a username
    66     92   		lib.render.login(co, nil, nil, lib.str.plit(nil))
    67     93   	elseif meth == method.post then
................................................................................
   222    248   					return
   223    249   				else goto badop end
   224    250   			end
   225    251   		else goto badurl end
   226    252   	end
   227    253   
   228    254   	if meth == method.post then
          255  +		if co.aid == 0 then goto noauth end
   229    256   		var act = co:ppostv('act')
   230    257   		if act:cmp(lib.str.plit 'like') and not co.srv:post_liked_uid(co.who.id,pid) then
   231    258   			co.srv:post_like(co.who.id, pid, false)
   232    259   			post.ptr.likes = post.ptr.likes + 1
   233    260   		elseif act:cmp(lib.str.plit 'dislike') and co.srv:post_liked_uid(co.who.id,pid) then
   234    261   			co.srv:post_like(co.who.id, pid, true)
   235    262   			post.ptr.likes = post.ptr.likes - 1
................................................................................
   253    280   	end
   254    281   
   255    282   	lib.render.tweet_page(co, path, post.ptr)
   256    283   	do return end
   257    284   
   258    285   	::badurl:: do co:complain(404, 'invalid URL', 'this URL does not reference extant content or functionality') return end
   259    286   	::badop :: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
          287  +	::noauth:: do co:complain(401, 'unauthorized', 'you have not supplied the necessary credentials to perform this operation') return end
   260    288   end
   261    289   
   262    290   terra http.configure(co: &lib.srv.convo, path: hpath, meth: method.t)
   263    291   	var msg = pstring.null()
   264    292   	-- first things first, do priv checks
   265    293   	if path.ct >= 1 then
   266    294   		if not co.who.rights.powers.config() and (
................................................................................
   323    351   			if act:cmp(lib.str.plit 'invalidate') then
   324    352   				lib.dbg('setting user\'s cookie validation time to now')
   325    353   				co.who.source:auth_sigtime_user_alter(co.who.id, lib.osclock.time(nil))
   326    354   				-- the current session has been invalidated as well, so we need to immediately install a new authentication cookie with the same aid so the user doesn't need to log back in all over again
   327    355   				co:installkey('/conf/sec',co.aid)
   328    356   				return
   329    357   			end
   330         -		elseif path(1):cmp(lib.str.lit 'users') and path.ct >= 2 then
   331         -			var userid, ok = lib.math.shorthand.parse(path(2).ptr, path(2).ct)
   332         -			if ok then
   333         -				var usr = co.srv:actor_fetch_uid(userid) defer usr:free()
   334         -				if not co.who:overpowers(usr.ptr) then goto nopriv end
          358  +		elseif path(1):cmp(lib.str.lit 'users') then
          359  +			if path.ct >= 3 then
          360  +				var userid, ok = lib.math.shorthand.parse(path(2).ptr, path(2).ct)
          361  +				if ok then
          362  +					var usr = co.srv:actor_fetch_uid(userid)
          363  +					if usr:ref() then defer usr:free()
          364  +						if not co.who:overpowers(usr.ptr) then goto nopriv end
          365  +					end
          366  +				end
          367  +			elseif path.ct == 2 then
   335    368   			end
   336    369   		end
   337    370   
   338    371   		if user_refresh then -- refresh the user info for the renderer
   339    372   			var usr = co.srv:actor_fetch_uid(co.who.id)
   340    373   			lib.mem.heapf(co.who)
   341    374   			co.who = usr.ptr

Modified static/live.js from [f01ba9ae01] to [45ade869ea].

    34     34   		})
    35     35   	}
    36     36   
    37     37   	/* div-based like and rt aren't very keyboard-friendly. add a replacement */
    38     38   	if (document.querySelector('body.timeline, body.profile, body.post') != null) {
    39     39   		onkey(window, function(event) {
    40     40   			if (focused()) {return;}
    41         -			let cururl = window._liveTweetMap.cur;
           41  +			let tmap = window._liveTweetMap;
           42  +			let cururl = tmap.cur;
    42     43   			let nexturl = null;
    43     44   			if (event.key == 'j') { // down
    44     45   				if (cururl == null) {
    45         -					nexturl = window._liveTweetMap.first
           46  +					nexturl = tmap.first;
    46     47   				} else {
    47         -					nexturl = window._liveTweetMap.map.get(cururl).next
           48  +					nexturl = tmap.map.get(cururl).next;
    48     49   				}
    49     50   			} else if (event.key == 'k') { // up
    50     51   				if (cururl == null) {
    51         -					nexturl = window._liveTweetMap.last
           52  +					nexturl = tmap.last;
    52     53   				} else {
    53         -					nexturl = window._liveTweetMap.map.get(cururl).prev
           54  +					nexturl = tmap.map.get(cururl).prev;
    54     55   				}
    55     56   			} else if (cururl != null) {
    56         -				let post = window._liveTweetMap.map.get(cururl).me
           57  +				let post = tmap.map.get(cururl).me;
           58  +				let root = tmap.map.get(cururl).go;
    57     59   				if (event.key == 'f') { // fave
    58         -					postReq(cururl, 'like', post.querySelector('.stats>.like'))
           60  +					postReq(root, 'like', post.querySelector('.stats>.like'))
    59     61   				} else if (event.key == 'r') { // rt
    60         -					postReq(cururl, 'rt', post.querySelector('.stats>.rt'))
    61         -				} else if (event.key == 'd') { // rt
    62         -					if (post.attributes.getNamedItem('data-own')) {
    63         -						window.location = cururl + '/del';
    64         -					}
           62  +					postReq(root, 'rt', post.querySelector('.stats>.rt'))
    65     63   				} else if (event.key == 'Enter') { // nav
    66         -					window.location = cururl;
           64  +					window.location = root;
    67     65   					return;
           66  +				} else if (post.attributes.getNamedItem('data-own')) {
           67  +					if      (event.key == 'd') { window.location = root + '/del';  }
           68  +					else if (event.key == 'e') { window.location = root + '/edit'; }
           69  +					else if (event.key == 'u' && root != cururl) { window.location = cururl; } // detweet
    68     70   				}
    69     71   			}
    70     72   			if (nexturl != null) {
    71     73   				if (cururl != null) {
    72     74   					let cur = window._liveTweetMap.map.get(cururl);
    73     75   					cur.me.classList.remove('live-selected')
    74     76   				}
................................................................................
   105    107   			}
   106    108   		});
   107    109   	}
   108    110   
   109    111   	function attachButtons() {
   110    112   		let last = null;
   111    113   		let newmap = { cur: null, first: null, last: null, map: new Map() }
   112         -		document.querySelectorAll('main article.post').forEach(function(post){
   113         -			let url = posturl(post);
          114  +		document.querySelectorAll('main article.post:not([data-rt]), main div.lede').forEach(function(post){
          115  +			let ert = post.querySelector('article.post[data-rt]');
          116  +			let lede = null;
          117  +			if (ert != null) { lede = post; post = ert; }
          118  +			let purl = posturl(post);
          119  +			let url = null;
          120  +			if (lede == null) {url = purl;} else {
          121  +				url = lede.querySelector('a[href].del').
          122  +					attributes.getNamedItem('href').value;
          123  +			}
          124  +			console.log('post',post,'lede',lede,url);
          125  +
   114    126   			if (last == null) { newmap.first = url; } else {
   115         -				newmap.map.get(last).next = url
          127  +				newmap.map.get(last).next = url;
   116    128   			}
   117         -			newmap.map.set(url, {me: post, prev: last, next: null})
   118         -			last = url
   119         -			if (window._liveTweetMap && window._liveTweetMap.cur == url) {
          129  +			newmap.map.set(url, {me: post, go: purl, prev: last, next: null})
          130  +			last = url;
          131  +			if (window._liveTweetMap &&
          132  +				window._liveTweetMap.cur == url
          133  +			) {
   120    134   				post.classList.add('live-selected');
   121    135   			}
   122    136   
   123    137   			let stats = post.querySelector('.stats');
   124    138   			if (stats == null) {
   125    139   				/* no stats box; create one */
   126    140   				let n = mk('div');

Modified static/style.scss from [d34b63e467] to [5326e6a4a7].

   544    544   		justify-content: center;
   545    545   		> .like, > .rt {
   546    546   			margin: 0.5em 0.3em;
   547    547   			padding-left: 1.3em;
   548    548   			background-size: 1.1em;
   549    549   			background-repeat: no-repeat;
   550    550   			min-width: 0.3em;
          551  +			&:focus {
          552  +				outline: none;
          553  +				opacity: 0.9 !important;
          554  +				filter: brightness(1.7) drop-shadow(0 0 15px rgb(255,150,200));
          555  +			}
   551    556   			&:empty {
   552    557   				transition: 0.3s;
   553    558   				opacity: 0.0001; // qutebrowser won't show hints if opacity=0 :(
   554    559   				&:hover, &:focus { opacity: 0.6 !important; }
   555    560   			}
   556    561   		}
   557    562   		> .like { background-image: url(/s/heart.webp); }
................................................................................
   789    794   }
   790    795   
   791    796   ul.remarks {
   792    797   	margin: 0; padding: 0;
   793    798   	list-style-type: none;
   794    799   	li {
   795    800   		border-top: 1px solid otone(-22%);
   796         -		border-bottom: 2px solid otone(-55%);
          801  +		border-bottom: 1px solid otone(-53%);
          802  +		box-shadow: 0 1px 1px otone(-57%);
          803  +		text-shadow: 1px 1px otone(-60%);
   797    804   		border-radius: 3px;
   798    805   		background: otone(-25%,-0.4);
   799    806   		color: otone(25%);
   800    807   		text-align: center;
   801    808   		padding: 0.3em 0;
   802    809   		margin: 0.2em 0.1em;
   803    810   		cursor: default;
................................................................................
   911    918   div.lede {
   912    919   	display: grid;
   913    920   	grid-template-columns: 1fr min-content;
   914    921   	grid-template-rows: 1.5em 1fr;
   915    922   	padding: 0.1in 0.3in;
   916    923   	margin: 0 -0.2in;
   917    924   	margin-top: 0.2in;
          925  +	& + & { margin-top: 0; }
   918    926   	border-radius: 3px;
   919    927   	background: linear-gradient(to bottom, tone(-40%,-0.5), transparent);
   920    928   	border-top: 1px solid tone(-5%,-0.7);
   921    929   	> .promo {
   922    930   		grid-row: 1/2; grid-column: 1/2;
   923    931   		font-style: italic;
   924    932   		font-size: 90%;

Modified store.t from [6a465decce] to [194a7e6d10].

     1      1   -- vim: ft=terra
     2      2   local m = {
     3      3   	timepoint = lib.osclock.time_t;
     4      4   	scope = lib.enum {
     5      5   		'public', 'private', 'local';
     6      6   		'personal', 'direct', 'circle';
     7      7   	};
     8         -	notiftype = lib.enum {
     9         -		'none', 'mention', 'like', 'rt', 'react'
            8  +	noticetype = lib.enum {
            9  +		'none', 'mention', 'reply', 'like', 'rt', 'react'
    10     10   	};
    11     11   
    12     12   	relation = lib.set {
           13  +		'follow',
           14  +		'mute', -- posts will be completely hidden at all times
           15  +		'block', -- no interactions will be permitted, but posts will remain visible
    13     16   		'silence', -- messages will not be accepted
    14     17   		'collapse', -- posts will be collapsed by default
    15     18   		'disemvowel', -- posts will be ritually humiliated, but shown
    16     19   		'avoid', -- posts will be kept out of the timeline but will show on users' posts and in conversations
    17         -		'follow',
    18         -		'mute', -- posts will be completely hidden at all times
    19         -		'block', -- no interactions will be permitted, but posts will remain visible
           20  +		'exclude', -- own posts will not be visible to this user
    20     21   	};
    21     22   	credset = lib.set {
    22     23   		'pw', 'otp', 'challenge', 'trust'
    23     24   	};
    24     25   	privset = lib.set {
    25     26   		'post', 'edit', 'account', 'upload', 'moderate', 'admin', 'invite'
    26     27   	};
................................................................................
   242    243   	} do
   243    244   		be.entries[#be.entries+1] = {
   244    245   			field = 'actor_conf_'..n..'_'..k, type = t
   245    246   		}
   246    247   	end
   247    248   end
   248    249   
   249         -struct m.notif {
   250         -	kind: m.notiftype.t
          250  +struct m.notice {
          251  +	kind: m.noticetype.t
   251    252   	when: uint64
          253  +	who: uint64
          254  +	what: uint64
   252    255   	union {
   253         -		post: uint64
          256  +		reply: uint64
   254    257   		reaction: int8[16]
   255    258   	}
   256    259   }
   257    260   
   258    261   struct m.inet {
   259    262   	pv: uint8 -- 0 = null, 4 = ipv4, 6 = ipv6
   260    263   	union {
................................................................................
   343    346   
   344    347   	actor_create: {&m.source, &m.actor} -> uint64
   345    348   	actor_save: {&m.source, &m.actor} -> {}
   346    349   	actor_save_privs: {&m.source, &m.actor} -> {}
   347    350   	actor_purge_uid: {&m.source, uint64} -> {}
   348    351   	actor_fetch_xid: {&m.source, lib.mem.ptr(int8)} -> lib.mem.ptr(m.actor)
   349    352   	actor_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.actor)
   350         -	actor_notif_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.notif)
   351    353   	actor_enum: {&m.source} -> lib.mem.lstptr(m.actor)
   352    354   	actor_enum_local: {&m.source} -> lib.mem.lstptr(m.actor)
   353    355   	actor_stats: {&m.source, uint64} -> m.actor_stats
   354    356   	actor_rel: {&m.source, uint64, uint64} -> m.relationship
   355    357   
   356    358   	actor_auth_how: {&m.source, m.inet, rawstring} -> {m.credset, bool}
   357    359   		-- returns a set of auth method categories that are available for a
................................................................................
   386    388   			-- origin: inet
   387    389   			-- cookie issue time: m.timepoint
   388    390   	actor_auth_register_uid: {&m.source, uint64, uint64} -> {}
   389    391   		-- notifies the backend module of the UID that has been assigned for
   390    392   		-- an authentication ID
   391    393   			-- aid: uint64
   392    394   			-- uid: uint64
   393         -	actor_notifs_fetch: {&m.source, uint64} -> lib.mem.lstptr(m.notif)
          395  +	actor_notice_enum: {&m.source, uint64} -> lib.mem.lstptr(m.notice)
          396  +	actor_rel_create: {&m.source, uint16, uint64, uint64} -> {}
          397  +	actor_rel_destroy: {&m.source, uint16, uint64, uint64} -> {}
          398  +	actor_rel_calc: {&m.source, uint64, uint64} -> m.relationship
   394    399   
   395    400   	auth_enum_uid:    {&m.source, uint64}    -> lib.mem.lstptr(m.auth)
   396    401   	auth_enum_handle: {&m.source, rawstring} -> lib.mem.lstptr(m.auth)
   397    402   	auth_attach_pw: {&m.source, uint64, bool, pstr, pstr} -> {}
   398    403   		-- uid: uint64
   399    404   		-- reset: bool (delete other passwords?)
   400    405   		-- pw: pstring

Modified str.t from [638f6c2759] to [7fbe47e2ae].

   179    179   	end
   180    180   	lib.mem.cpy(self.buf + self.sz, str, len)
   181    181   	self.sz = self.sz + len
   182    182   	self.buf[self.sz] = 0
   183    183   	return self
   184    184   end;
   185    185   
   186         -terra m.acc:ipush(i: intptr)
          186  +terra m.acc:dpush(i: intptr)
   187    187   	var decbuf: int8[21]
   188    188   	var si = lib.math.decstr_friendly(i, &decbuf[20])
   189    189   	var len: intptr = [decbuf.type.N] - (si - &decbuf[0])
   190         -	return self:push(si,len)
          190  +	return self:push(si,len-1)
          191  +end
          192  +
          193  +terra m.acc:ipush(i: intptr)
          194  +	var decbuf: int8[21]
          195  +	var si = lib.math.decstr(i, &decbuf[20])
          196  +	var len: intptr = [decbuf.type.N] - (si - &decbuf[0])
          197  +	return self:push(si,len-1)
   191    198   end
   192    199   
   193    200   terra m.acc:shpush(i: uint64)
   194    201   	var sbuf: int8[lib.math.shorthand.maxlen]
   195    202   	var len = lib.math.shorthand.gen(i,&sbuf[0])
   196    203   	return self:push(&sbuf[0], len)
   197    204   end

Modified view/confirm.tpl from [8b0d6acfba] to [4c878ea4e6].

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

Modified view/profile.tpl from [4ac5403896] to [6a9a509b78].

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