parsav  Check-in [78b0198f09]

Overview
Comment:add likes, retweets, and iterate on a whole bunch of other shit
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 78b0198f099355ea30a888c8100a75cec3ed910e0e6dbc50f5b711fc065a574d
User & Date: lexi on 2021-01-04 06:44:13
Other Links: manifest | tags
Context
2021-01-04
15:29
add like + retweets buttons, keyboard nav check-in: b9cf14c14b user: lexi tags: trunk
06:44
add likes, retweets, and iterate on a whole bunch of other shit check-in: 78b0198f09 user: lexi tags: trunk
2021-01-02
18:32
iterate on user mgmt UI check-in: f09cd18161 user: lexi tags: trunk
Changes

Modified backend/pgsql.t from [962a3e64e0] to [2e62d4947d].

213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
...
349
350
351
352
353
354
355






356
357
358
359
360
361
362


363
364
365
366
367
368
369
370
371
372






373
374
375
376
377
378
379


380
381
382
383
384
385
386
...
401
402
403
404
405
406
407
408


























409
410

























411
412
413
414
415
416
417
418
419


420
421
422
423
424

425
426
427
428
429
430
431
432
433
434
435

436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454










455
456
457
458
459
460
461
...
792
793
794
795
796
797
798




799
800
801
802
803
804
805
....
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
....
1211
1212
1213
1214
1215
1216
1217


































1218
1219
1220
1221
1222
1223
1224
			select a.id, a.nym, a.handle, a.origin, a.bio,
			       a.avataruri, a.rank, a.quota, a.key, a.epithet,
			       extract(epoch from a.knownsince)::bigint,
				   coalesce(a.handle || '@' || s.domain,
				            '@' || a.handle) as xid,

			       au.restrict,
						array['post'  ] <@ au.restrict as can_post,
						array['edit'  ] <@ au.restrict as can_edit,
						array['acct'  ] <@ au.restrict as can_acct,
						array['upload'] <@ au.restrict as can_upload,
						array['censor'] <@ au.restrict as can_censor,
						array['admin' ] <@ au.restrict as can_admin

			from      parsav_auth au
			left join parsav_actors a     on au.uid = a.id
			left join parsav_servers s    on a.origin = s.id

			where au.aid = $1::bigint and au.blacklist = false and
				(au.netmask is null or au.netmask >> $2::inet) and
................................................................................
		params = {uint64}, cmd = true, sql = [[
			delete from parsav_posts where id = $1::bigint
		]]
	};
	
	post_fetch = {
		params = {uint64}, sql = [[






			select a.origin is null,
				p.id, p.author, p.subject, p.acl, p.body,
				extract(epoch from p.posted    )::bigint,
				extract(epoch from p.discovered)::bigint,
				extract(epoch from p.edited    )::bigint,
				p.parent, p.convoheaduri, p.chgcount,
				coalesce(c.value, -1)::smallint



			from parsav_posts as p
				inner join parsav_actors          as a on p.author = a.id
				left join  parsav_actor_conf_ints as c on c.uid    = a.id and c.key = 'ui-accent'
			where p.id = $1::bigint
		]];
	};

	post_enum_parent = {
		params = {uint64}, sql = [[






			select a.origin is null,
				p.id, p.author, p.subject, p.acl, p.body,
				extract(epoch from p.posted    )::bigint,
				extract(epoch from p.discovered)::bigint,
				extract(epoch from p.edited    )::bigint,
				p.parent, p.convoheaduri, p.chgcount,
				coalesce(c.value, -1)::smallint



			from parsav_posts as p
				inner join parsav_actors as a on a.id = p.author
				left join  parsav_actor_conf_ints as c on c.uid = a.id and c.key = 'ui-accent'
			where p.parent = $1::bigint
			order by p.posted, p.discovered asc
		]]
................................................................................
					inner join parsav_posts as p
						on p.id = posts.id
			)

			select extract(epoch from max(m))::bigint from maxes
		]];
	};



























	post_enum_author_uid = {
		params = {uint64,uint64,uint64,uint64, uint64}, sql = [[

























			select a.origin is null,
				p.id, p.author, p.subject, p.acl, p.body,
				extract(epoch from p.posted    )::bigint,
				extract(epoch from p.discovered)::bigint,
				extract(epoch from p.edited    )::bigint,
				p.parent, p.convoheaduri, p.chgcount,
				coalesce((select value from parsav_actor_conf_ints as c where
					c.uid = $1::bigint and c.key = 'ui-accent'),-1)::smallint



			from parsav_posts as p
				inner join parsav_actors as a on p.author = a.id
			where p.author = $5::bigint and
				($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and
				($2::bigint = 0 or to_timestamp($2::bigint) < p.posted)

			order by (p.posted, p.discovered) desc
			limit case when $3::bigint = 0 then null
			           else $3::bigint end
			offset $4::bigint
		]]
	};

	-- maybe there's some way to unify these two, idk, im tired

	timeline_instance_fetch = {
		params = {uint64, uint64, uint64, uint64}, sql = [[

			select true,
				p.id, p.author, p.subject, p.acl, p.body,
				extract(epoch from p.posted    )::bigint,
				extract(epoch from p.discovered)::bigint,
				extract(epoch from p.edited    )::bigint,
				p.parent, null::text, p.chgcount,
				coalesce(c.value, -1)::smallint

			from parsav_posts as p
				inner join parsav_actors          as a on p.author = a.id
				left join  parsav_actor_conf_ints as c on c.uid    = a.id and c.key = 'ui-accent'
			where
				($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and
				($2::bigint = 0 or to_timestamp($2::bigint) < p.posted) and
				(a.origin is null)
			order by (p.posted, p.discovered) desc
			limit case when $3::bigint = 0 then null
			           else $3::bigint end
			offset $4::bigint










		]]
	};

	artifact_instantiate = {
		params = {binblob, binblob, pstring}, sql = [[
			insert into parsav_artifacts (content,hash,mime) values (
				$1::bytea, $2::bytea, $3::text
................................................................................
	end
	p.ptr.parent = r:int(uint64,row,9)
	if r:null(row,11)
		then p.ptr.chgcount = 0
		else p.ptr.chgcount = r:int(uint32,row,11)
	end 
	p.ptr.accent = r:int(int16,row,12)




	p.ptr.localpost = r:bool(row,0)

	return p
end
local terra row_to_actor(r: &pqr, row: intptr): lib.mem.ptr(lib.store.actor)
	var a: lib.mem.ptr(lib.store.actor)
	var av: rawstring, avlen: intptr
................................................................................
			a.ptr.source = src

			var au = [lib.stat(lib.store.auth)] { ok = true }
			au.val.aid = aid
			au.val.uid = a.ptr.id
			if not r:null(0,14) then -- restricted?
				au.val.privs:clear()
				(au.val.privs.post   << r:bool(0,15)) 
				(au.val.privs.edit   << r:bool(0,16))
				(au.val.privs.acct   << r:bool(0,17))
				(au.val.privs.upload << r:bool(0,18))
				(au.val.privs.censor << r:bool(0,19))
				(au.val.privs.admin  << r:bool(0,20))
			else au.val.privs:fill() end

			return au, a
		end

		::fail:: return [lib.stat   (lib.store.auth) ] { ok = false        },
			            [lib.mem.ptr(lib.store.actor)] { ptr = nil, ct = 0 }
................................................................................
	): lib.mem.ptr(lib.store.post)
		var r = queries.post_fetch.exec(src, post)
		if r.sz == 0 then return [lib.mem.ptr(lib.store.post)].null() end
		var p = row_to_post(&r, 0)
		p.ptr.source = src
		return p
	end];



































	timeline_instance_fetch = [terra(src: &lib.store.source, rg: lib.store.range)
		var r = pqr { sz = 0 }
		var A,B,C,D = rg:matrix() -- :/
		r = queries.timeline_instance_fetch.exec(src,A,B,C,D)
		
		var ret: lib.mem.ptr(lib.mem.ptr(lib.store.post)) ret:init(r.sz)







|
|
|
|
|
|







 







>
>
>
>
>
>






|
>
>










>
>
>
>
>
>






|
>
>







 








>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>


>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>






|
|
<
>
>
|

<
|
|
>


|








>
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
>
>
>
>
>
>
>
>
>
>







 







>
>
>
>







 







|
|
|
|
|
|







 







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
...
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
...
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485

486
487
488
489

490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
...
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
....
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
....
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
			select a.id, a.nym, a.handle, a.origin, a.bio,
			       a.avataruri, a.rank, a.quota, a.key, a.epithet,
			       extract(epoch from a.knownsince)::bigint,
				   coalesce(a.handle || '@' || s.domain,
				            '@' || a.handle) as xid,

			       au.restrict,
						array['post'    ] <@ au.restrict,
						array['edit'    ] <@ au.restrict,
						array['account' ] <@ au.restrict,
						array['upload'  ] <@ au.restrict,
						array['moderate'] <@ au.restrict,
						array['admin'   ] <@ au.restrict

			from      parsav_auth au
			left join parsav_actors a     on au.uid = a.id
			left join parsav_servers s    on a.origin = s.id

			where au.aid = $1::bigint and au.blacklist = false and
				(au.netmask is null or au.netmask >> $2::inet) and
................................................................................
		params = {uint64}, cmd = true, sql = [[
			delete from parsav_posts where id = $1::bigint
		]]
	};
	
	post_fetch = {
		params = {uint64}, sql = [[
			with counts as (
				select a.kind, p.id as subject, count(*) as ct from parsav_acts as a
					inner join parsav_posts as p on p.id = a.subject
				group by a.kind, p.id
			)

			select a.origin is null,
				p.id, p.author, p.subject, p.acl, p.body,
				extract(epoch from p.posted    )::bigint,
				extract(epoch from p.discovered)::bigint,
				extract(epoch from p.edited    )::bigint,
				p.parent, p.convoheaduri, p.chgcount,
				coalesce(c.value, -1)::smallint, 0::bigint, 0::bigint,
				coalesce((select ct from counts where kind = 'like' and counts.subject = p.id),0)::integer,
				coalesce((select ct from counts where kind = 'rt' and counts.subject = p.id),0)::integer

			from parsav_posts as p
				inner join parsav_actors          as a on p.author = a.id
				left join  parsav_actor_conf_ints as c on c.uid    = a.id and c.key = 'ui-accent'
			where p.id = $1::bigint
		]];
	};

	post_enum_parent = {
		params = {uint64}, sql = [[
			with counts as (
				select a.kind, p.id as subject, count(*) as ct from parsav_acts as a
					inner join parsav_posts as p on p.id = a.subject
				group by a.kind, p.id
			)

			select a.origin is null,
				p.id, p.author, p.subject, p.acl, p.body,
				extract(epoch from p.posted    )::bigint,
				extract(epoch from p.discovered)::bigint,
				extract(epoch from p.edited    )::bigint,
				p.parent, p.convoheaduri, p.chgcount,
				coalesce(c.value, -1)::smallint, 0::bigint, 0::bigint,
				coalesce((select ct from counts where kind = 'like' and counts.subject = p.id),0)::integer,
				coalesce((select ct from counts where kind = 'rt' and counts.subject = p.id),0)::integer

			from parsav_posts as p
				inner join parsav_actors as a on a.id = p.author
				left join  parsav_actor_conf_ints as c on c.uid = a.id and c.key = 'ui-accent'
			where p.parent = $1::bigint
			order by p.posted, p.discovered asc
		]]
................................................................................
					inner join parsav_posts as p
						on p.id = posts.id
			)

			select extract(epoch from max(m))::bigint from maxes
		]];
	};

	post_react_simple = {
		params = {uint64, uint64, pstring}, sql = [[
			insert into parsav_acts (kind,actor,subject) values (
				$3::text, $1::bigint, $2::bigint
			) returning id
		]];
	};

	post_react_cancel = {
		params = {uint64, uint64, pstring}, cmd = true, sql = [[
			delete from parsav_acts where
				actor = $1::bigint and
				subject = $2::bigint and
				kind = $3::text
		]];
	};

	post_reacts_fetch_uid = {
		params = {uint64, uint64, pstring}, sql = [[
			select id, actor, subject, kind, body, time from parsav_acts where
				($1::bigint = 0 or actor   = $1::bigint) and
				($2::bigint = 0 or subject = $2::bigint) and
				($3::text is null or kind  = $3::text  )
		]]
	};

	post_enum_author_uid = {
		params = {uint64,uint64,uint64,uint64, uint64}, sql = [[
			with ownposts as (
				select *, 0::bigint as rtid from parsav_posts as p
				where p.author = $5::bigint and
					($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and
					($2::bigint = 0 or to_timestamp($2::bigint) < p.posted)
			),

			retweets as (
				select p.*, a.id as rtid from parsav_acts as a
					inner join parsav_posts as p on a.subject = p.id
				where a.actor = $5::bigint and
					  a.kind = 'rt' and
					  ($1::bigint = 0 or a.time <= to_timestamp($1::bigint)) and
					  ($2::bigint = 0 or to_timestamp($2::bigint) < a.time)
			),

			allposts as (select *, 0::bigint  as retweeter from ownposts
			      union  select *, $5::bigint as retweeter from retweets),

			counts as (
				select a.kind, p.id as subject, count(*) as ct from parsav_acts as a
					inner join parsav_posts as p on p.id = a.subject
				group by a.kind, p.id
			)

			select a.origin is null,
				p.id, p.author, p.subject, p.acl, p.body,
				extract(epoch from p.posted    )::bigint,
				extract(epoch from p.discovered)::bigint,
				extract(epoch from p.edited    )::bigint,
				p.parent, p.convoheaduri, p.chgcount,
				coalesce(c.value,-1)::smallint,
				p.retweeter, p.rtid,

				coalesce((select ct from counts where kind = 'like' and counts.subject = p.id),0)::integer,
				coalesce((select ct from counts where kind = 'rt' and counts.subject = p.id),0)::integer
			from allposts as p
				inner join parsav_actors as a on p.author = a.id

				left  join parsav_actor_conf_ints as c
					on c.key = 'ui-accent' and
					   c.uid = a.id
			order by (p.posted, p.discovered) desc
			limit case when $3::bigint = 0 then null
					   else $3::bigint end
			offset $4::bigint
		]]
	};

	-- maybe there's some way to unify these two, idk, im tired

	timeline_instance_fetch = {
		params = {uint64, uint64, uint64, uint64}, sql = [[
			with posts as (
				select true,
					p.id, p.author, p.subject, p.acl, p.body,
					extract(epoch from p.posted    )::bigint,
					extract(epoch from p.discovered)::bigint,
					extract(epoch from p.edited    )::bigint,
					p.parent, null::text, p.chgcount,
					coalesce(c.value, -1)::smallint, 0::bigint, 0::bigint

				from parsav_posts as p
					inner join parsav_actors          as a on p.author = a.id
					left join  parsav_actor_conf_ints as c on c.uid    = a.id and c.key = 'ui-accent'
				where
					($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and
					($2::bigint = 0 or to_timestamp($2::bigint) < p.posted) and
					(a.origin is null)
				order by (p.posted, p.discovered) desc
				limit case when $3::bigint = 0 then null
						   else $3::bigint end
				offset $4::bigint
			), counts as (
				select a.kind, p.id as subject, count(*) as ct from parsav_acts as a
					inner join parsav_posts as p on p.id = a.subject
				group by a.kind, p.id
			)

			select *,
				coalesce((select ct from counts as c where kind = 'like' and c.subject = posts.id),0)::integer,
				coalesce((select ct from counts as c where kind = 'rt' and c.subject = posts.id),0)::integer
			from posts
		]]
	};

	artifact_instantiate = {
		params = {binblob, binblob, pstring}, sql = [[
			insert into parsav_artifacts (content,hash,mime) values (
				$1::bytea, $2::bytea, $3::text
................................................................................
	end
	p.ptr.parent = r:int(uint64,row,9)
	if r:null(row,11)
		then p.ptr.chgcount = 0
		else p.ptr.chgcount = r:int(uint32,row,11)
	end 
	p.ptr.accent = r:int(int16,row,12)
	p.ptr.rtdby = r:int(uint64,row,13)
	p.ptr.rtact = r:int(uint64,row,14)
	p.ptr.likes = r:int(uint32,row,15)
	p.ptr.rts = r:int(uint32,row,16)
	p.ptr.localpost = r:bool(row,0)

	return p
end
local terra row_to_actor(r: &pqr, row: intptr): lib.mem.ptr(lib.store.actor)
	var a: lib.mem.ptr(lib.store.actor)
	var av: rawstring, avlen: intptr
................................................................................
			a.ptr.source = src

			var au = [lib.stat(lib.store.auth)] { ok = true }
			au.val.aid = aid
			au.val.uid = a.ptr.id
			if not r:null(0,14) then -- restricted?
				au.val.privs:clear()
				(au.val.privs.post    << r:bool(0,15)) 
				(au.val.privs.edit    << r:bool(0,16))
				(au.val.privs.account << r:bool(0,17))
				(au.val.privs.upload  << r:bool(0,18))
				(au.val.privs.moderate<< r:bool(0,19))
				(au.val.privs.admin   << r:bool(0,20))
			else au.val.privs:fill() end

			return au, a
		end

		::fail:: return [lib.stat   (lib.store.auth) ] { ok = false        },
			            [lib.mem.ptr(lib.store.actor)] { ptr = nil, ct = 0 }
................................................................................
	): lib.mem.ptr(lib.store.post)
		var r = queries.post_fetch.exec(src, post)
		if r.sz == 0 then return [lib.mem.ptr(lib.store.post)].null() end
		var p = row_to_post(&r, 0)
		p.ptr.source = src
		return p
	end];

	post_retweet = [terra(
		src: &lib.store.source,
		uid: uint64,
		post: uint64,
		undo: bool
	): {}
		if not undo then
			queries.post_react_simple.exec(src,uid,post,"rt")
		else
			queries.post_react_cancel.exec(src,uid,post,"rt")
		end
	end];
	post_like = [terra(
		src: &lib.store.source,
		uid: uint64,
		post: uint64,
		undo: bool
	): {}
		if not undo then
			queries.post_react_simple.exec(src,uid,post,"like")
		else
			queries.post_react_cancel.exec(src,uid,post,"like")
		end
	end];
	post_liked_uid = [terra(
		src: &lib.store.source,
		uid: uint64,
		post: uint64
	): bool
		var q = queries.post_reacts_fetch_uid.exec(src,uid,post,'like')
		if q.sz > 0 then q:free() return true end
		return false
	end];

	timeline_instance_fetch = [terra(src: &lib.store.source, rg: lib.store.range)
		var r = pqr { sz = 0 }
		var A,B,C,D = rg:matrix() -- :/
		r = queries.timeline_instance_fetch.exec(src,A,B,C,D)
		
		var ret: lib.mem.ptr(lib.mem.ptr(lib.store.post)) ret:init(r.sz)

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

82
83
84
85
86
87
88
89
90
91
92
93

94
95
96
97
98
99
100
	kind    smallint, -- e.g. follow, block, mute

	primary key (relator, relatee, kind)
);

create table parsav_acts (
	id      bigint primary key default (1+random()*(2^63-1))::bigint,
	kind    text not null, -- like, react, so on
	time    timestamp not null default now(),
	actor   bigint references parsav_actors(id)
		on delete cascade,
	subject bigint -- may be post or act, depending on kind

);

create table parsav_log (
	-- accesses are tracked for security & sending delete acts
	id    bigint primary key default (1+random()*(2^63-1))::bigint,
	time  timestamp not null default now(),
	actor bigint references parsav_actors(id)







|



|
>







82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
	kind    smallint, -- e.g. follow, block, mute

	primary key (relator, relatee, kind)
);

create table parsav_acts (
	id      bigint primary key default (1+random()*(2^63-1))::bigint,
	kind    text not null, -- like, rt, react, so on
	time    timestamp not null default now(),
	actor   bigint references parsav_actors(id)
		on delete cascade,
	subject bigint, -- may be post or act, depending on kind
	body	text -- emoji, if react
);

create table parsav_log (
	-- accesses are tracked for security & sending delete acts
	id    bigint primary key default (1+random()*(2^63-1))::bigint,
	time  timestamp not null default now(),
	actor bigint references parsav_actors(id)

Modified config.lua from [5a4f5a8d5b] to [8efc0b984f].

53
54
55
56
57
58
59


60
61
62
63
64
65
66
		-- the damn things before compiling (also making the binary smaller)
		{'style.css', 'text/css'};
		{'live.js', 'text/javascript'}; -- rrrrrrrr
		{'default-avatar.webp', 'image/webp'}; -- needs inkscape-exclusive svg features
		{'padlock.svg', 'image/svg+xml'};
		{'warn.svg', 'image/svg+xml'};
		{'query.webp', 'image/webp'};


	};
	default_ui_accent = tonumber(default('parsav_ui_default_accent',323));
}
if os.getenv('parsav_let_me_be_an_idiot') == "i know what i'm doing" then
	conf.braingeniousmode = true -- SOUND GENERAL QUARTERS
end
if u.ping '.fslckout' or u.ping '_FOSSIL_' then







>
>







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

Modified doc/load.lua from [68f6a9a498] to [783a358256].

1
2
3
4
5
6
7



8
9
10
11
12
13
14
local path = ...
local sources = {
-- user section
	acl = {title = 'access control lists'};
-- admin section
	--overview = {title = 'server overview', priv = 'config'};
	invocation = {title = 'daemon invocation', priv = 'config'};



	--backends = {title = 'storage backends', priv = 'config'};
		--pgsql = {title = 'pgsql', priv = 'config', parent = 'backends'};
}

local util = dofile 'common.lua'
local ingest = function(filename)
	return (util.exec { 'cmark', '--smart', '--unsafe', (path..'/'..filename) }):gsub('\n','')







>
>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
local path = ...
local sources = {
-- user section
	acl = {title = 'access control lists'};
-- admin section
	--overview = {title = 'server overview', priv = 'config'};
	invocation = {title = 'daemon invocation', priv = 'config'};
	usr = {title = 'user accounting', priv = {'elevate','demote','purge','herald'}};
	--srvcfg = {title = 'server configuration policies', priv = 'config'};
	--discipline = {title = 'disciplinary measures', priv = 'discipline'};
	--backends = {title = 'storage backends', priv = 'config'};
		--pgsql = {title = 'pgsql', priv = 'config', parent = 'backends'};
}

local util = dofile 'common.lua'
local ingest = function(filename)
	return (util.exec { 'cmark', '--smart', '--unsafe', (path..'/'..filename) }):gsub('\n','')

Added doc/usr.md version [61c1a08ae0].











































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# user accounting
parsav comes with sophisticated user management tools. you can manage users either from the command line (if you have shell and database access on the host), or using the web UI. both methods will be described in depth here.

## core concepts
in parsav, users are a subset of *actors,* entities that post content on the fediverse. users are the actors that a given parsav instance publishes. some aspects of parsav administration apply to all actors, meaning that remote actors as well as local users are subject to them; others apply only only to users. in the database, users are distinguished only from other actors in that they are marked as belonging to the local instance.

every actor has several properties, most of which are fairly standard social media concepts. the first of these is the handle, the username that uniquely identifies a user on an instance and on the fediverse. due to technical limitations in the design of activitypub, handles cannot be changed, and are indelibly associated with a specific account. (as parsav is also intended to have a non-federating mode, and perhaps a mode to federate only with other parsav instances, it should be possible for non-federating instances to allow handle changes at some point.) the second is the nym, or "display name" in twitter parlance, a string that the user can set and change at will to identify himself, and which is displayed before the handle on posts. and of course, a user can give herself a *bio,* a block of markdown-formatted text that is displayed on her profile. neither the display name nor the bio can be changed by administrators.

actors can also have an *epithet,* a secondary title which is displayed in emphasized fashion after the handle and nym. how you use epithets is entirely up to you; you might use them to indicate specific staff roles ("founder", "admin", "moderator" or so on), or to attach humorous labels to well-known users as a mark of respect (or disrespect). the key thing about the epithet is that it can be set only by administrators (specifically, users with the `herald` power), so bearing an epithet indicates some kind of status recognized by the operator of the instance. (at some point it may also be made possible to change the color of a user's epithet as well, but for now they all display alike.) epithets can also be assigned to members of a chatroom by chatroom staff, but these are scoped to the chatroom and do not display outside of it (nor can they be modified by instance administrators). epithets can be set from the command line with the `parsav actor <xid> bestow <epithet>` command.

finally, every actor can be assigned a *rank.* this determines their level of authority on the server. when users are given powers, they can only exercise these powers over actors without rank or with lower ranks than their own; for instance, users of rank 2 can affect actors of rank 3 and 4, but not rank 1. rank 1 is special, being the highest possible rank, in that rank 1 users can affect other rank 1 users — this exception is to prevent the situation where a root user forgets their password and nobody else can reset it. however, the best practice is still to reserve rank 1 for the server owner and use lower ranks for all other users. it is important to note that all actors, not just local users, can be given ranks; while remote users cannot exercise power locally, they can be exempted from the power of lower-ranking administrators. rank can be set with the `parsav actor <xid> rank <number>` command; `degrade` will remove an actor's rank

local users have additional properties, a set of *powers* that governs their ability to use the instance. some of these powers relate only to normal use (logging in, posting, editing one's posts, and so on), and are given to all users by default. others grant power over other users, such as `elevate`, `demote`, and `discipline` (see "powers" for a list). administrative powers can be exercised only over users of lower rank; users without rank cannot make any use of most administrative powers (though `herald` can be granted to allow a user to change his own epithet). local users also have a "quota" (defaulting to 1000) that governs how many posts they can publish each day. if you run a restricted instance that requires invitations to join, users are also assigned a certain number of those invitations, and once they run out this count must be increased by an administrator before they can invite more users.

## creating administrative accounts
when you first install parsav and initialize its database, there will be no user accounts (unless you're using postgresql and unmanaged authentication) and user self-registration will not be allowed. in order to begin using your new instance, you will need to create yourself a user with which to administrate it. in order to do this, you will need to use the `parsav` utility you used to initialize the database in the first place. (note that depending on your configuration, you may need to run `parsav` as the same user the `parsavd` process runs as for it to be able to connect to the database.)

initial account creation is handled with the `parsav mkroot <handle>` command, where `<handle>` is the handle of your new user. issuing this command will create a new account with the given name, grant all powers to that account, assign it to rank 1, and generate a password you can use to log in. you can run this command multiple times and create multiple root users if you want, but note that these users have absolute power over the instance, including other root users! in most cases, there should be a single root account beloning to the instance owner, with lower-ranking accounts given out to moderators and other staff. (see "core concepts")

`mkroot` is purely a convenience function, and is almost identical to the effect of the commands `user <handle> create`, `actor <xid> rank 1`, `user <handle> grant all`, `user <handle> auth pw new`, and `conf set master <handle>` issued in sequence. the only difference is that `mkroot` also `bestow`s a silly title on the new account.

## creating user accounts
the command `parsav user` is used to manage local user accounts, and you can create a new standard user with the command `parsav user <handle> create`. for example, a user named `@eve` could be created with `parsav user eve create`.  this user will have no rank, default rights, default settings, and will not be able to log in.

you can enable the user to log in by creating a credential for them. for instance, to issue `@eve` a password, you could use the command `parsav user eve auth pw new` or `pw reset`. (the difference between the two is that `reset` deletes existing credentials of that type, whereas `new` creates a new credential without disabling any others). this will generate a temporary password that `@eve` can use to log in.

you can also create new users through the HTTP interface. log in to your administrative account, navigate to the configuration screen, and click "users" in the menu. (see "emergency recovery" if you don't have this option in your menu.) unfold the "new user" interface, and enter a handle for the new account; it will be created and you will be taken to a page where you can set its properties and create authentication tokens. note that administrators cannot edit other users' display names or bios; these are exclusively the prerogative of the user herself.

nota bene: if you want to give an account to another user, creating an invitation link is generally the best way of doing it, rather than manually adding a new account.

## powers
the abilities a user can exercise on a server are governed by their *powers,* a set of flags administrators can set on their accounts.

these powers are intended for ordinary users, and default to on:

 * **login:** the user can log in to the instance. revoking this power is equivalent to banning the user.
 * **visible:** the user and his posts can be seen by other users without navigating directly to his profile page
 * **post:** the user can publish new posts
 * **shout:** the user is visible on the local timeline
 * **propagate:** the user's posts federate
 * **artifact:** the user can upload artifacts and attach them to posts. users without this power can still add artifacts uploaded by others to their account, but cannot upload their own.
 * **account:** the user can configure their own account and profile
 * **edit:** the user can edit her own posts
 * **snitch:** the user can submit reports asking for moderator action

these powers are intended for staff, and default to off:

 * **herald:** the user can change the epithets of lower-ranking actors, grant them badges, or revoke their badges. note that badges can also be restricted such that only heralds of a certain rank can grant or revoke them.
 * **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)).
 * **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.
 * **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.
 * **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.
 * **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
 * **vacate:** the user can rehabilitate disciplined actors, vacating sanctions, voiding demerits, and issuing temporary reprieves from restrictions.
 * **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.
 * **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.
 * **cred:** the user can add, change, and remove the credentials of lower-ranking users (think password resets).
 * **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.
 * **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.

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)

### recommendations
on smaller servers, it is highly recommended that the `config`, `rebrand`, `purge`, `elevate`, and `demote` powers all rest with a single user. other administrators and moderators should be given `censor`, `discipline`, `vacate`, and possibly `invite` and `herald` depending on your intentions for the site. you should be the only rank-1 user, and other staff should be given rank 2. rank 3 might be useful to limit the damage new staff can do during a "probation period." `herald` and `crier` are useful powers to combine, as they create a "moderator" with powers related mostly to promotion of users and their work.

on larger servers, it may be necessary to have more levels of administrative abstraction, or even to increase the maximum number of ranks from its default of 10. in this case, certain exceptional powers such as `rebrand` and `purge` should still remain exclusively with the founder, but it may be necessary to (carefully!) apportion out access to powers like `elevate` and `demote`. it may also be desirable to have a broader class of less-trusted moderators who can take minimally destructive measures on their own (say, `censor` and `herald`) to filter through the bulk of reports, with a smaller corps of highly trusted commissars who have powers like `discipline` and `vacate` to handle the small number of reports that censors believe deserve their attention.

in both cases, it's very, very important to keep in mind that 99% of community management is social. parsav tries to provide you with effective tools for when use of force becomes unavoidable, but most of the time a good community leader can accomplish his goals with words alone (remember, IRC has none of this fancy shit, and they manage just fine most of the time!). apart from those relatively rare cases where you are faced with true bad-faith actors (in which cases immediate brutality is the only solution), a community can be handled effectively with just with judicious use of symbolic measures like rank, badges, and epithets. a gentle indication that a high-status user disapproves of her conduct is often all it takes to convince a lower-status user who truly cares about her community to shape up. all the power in the world won't give you a drop of authority, and if you're new to running communities, you may be surprised how much authority you can endow other members with without giving them anything besides maybe a fancy title (though even that is just a convenience) so long as the people in your community like, trust, and respect you.

and if your users don't respect you, you might as well pack up right now.

## emergencies
shit happens. sometimes this shit results in getting locked out of your own instance. if so, don't panic quite yet. as long as you can get shell access to the host to run the `parsav` utility, you can resolve the situation. (note that `parsavd` does not need to be running to use commands that control the database, and for some backends such as sqlite `parsavd` may need to be shut down first.)

### locked out
if you are locked out of your administrator account, the fix is simple, as long as you can modify the underlying database: the `parsav` utility does not use instance credentials, but rather directly modifies the DB and sends IPC signals through the kernel. if you're locked out because you've forgotten your password or all your credentials have been deleted somehow, just issue yourself a new temporary password like you would for any other user, with the `parsav user <handle> auth pw reset` command. 

### missing privileges
if you've been stripped of the `login` privilege by a bug or a rogue admin, you can restore it with `parsav user <handle> grant login`, and it may be worthwhile to issue a `revoke demote` to keep that rogue admin from immediately locking you out again. keep in mind that this won't affect sanctions that have been issued against your account; see below for these.

### sanctions
users with the `discipline` privilege cannot change user powers outright, but can issue sanctions that temporarily limit these powers in various ways, for instance preventing a user from posting for a few hours until they've cooled down. users with `discipline` can only affect users of lower rank unless they're rank 1, in which case they can affect all users. if you've fallen afoul of one of these users and need to get your instance back, you'll need to vacate all the sanctions against your account. this can be done with the `parsav actor <xid> sanction all vacate` command. alternately, you can list individual sanctions with `sanction`, and then delete them individually with `sanction <sid> vacate`.

### lost account
if your account has been completely deleted, rather than just suspended, things are decidedly more serious. everything associated with your account — posts, media, circles, relationships, all of it — is gone, irreversibly, unless you have a database backup around somewhere. (the `purge` power is so named because it is *serious business,* to be treated as the equivalent of a concealed carry permit — you should give it out to other users only out of specific justified need in exceptional circumstances, and revoke it proactively when it is no longer absolutely necessary, rather than as punishment for misuse. hopefully you have now learned this lesson.)

Modified makefile from [eedbd28993] to [5c5158676d].

1
2
3
4
5
6
7
8
9
10
11
dl = git
dbg-flags = $(if $(dbg),-g)

images = static/default-avatar.webp static/query.webp
#$(addsuffix .webp, $(basename $(wildcard static/*.svg)))
styles = $(addsuffix .css, $(basename $(wildcard static/*.scss)))

parsav parsavd: parsav.t config.lua pkgdata.lua $(images) $(styles)
	terra $(dbg-flags) $<
parsav.o parsavd.o: parsav.t config.lua pkgdata.lua $(images) $(styles)
	env parsav_link=no terra $(dbg-flags) $<



|







1
2
3
4
5
6
7
8
9
10
11
dl = git
dbg-flags = $(if $(dbg),-g)

images = static/default-avatar.webp static/query.webp static/heart.webp static/retweet.webp
#$(addsuffix .webp, $(basename $(wildcard static/*.svg)))
styles = $(addsuffix .css, $(basename $(wildcard static/*.scss)))

parsav parsavd: parsav.t config.lua pkgdata.lua $(images) $(styles)
	terra $(dbg-flags) $<
parsav.o parsavd.o: parsav.t config.lua pkgdata.lua $(images) $(styles)
	env parsav_link=no terra $(dbg-flags) $<

Modified mgtool.t from [ee3dfa15a8] to [4f69a41277].

385
386
387
388
389
390
391
392

393
394
395
396
397
398
399
					root.handle = cfmode.arglist(0)
					var epithets = array(
						'root', 'god', 'regional jehovah', 'titan king',
						'king of olympus', 'cyberpharaoh', 'electric ellimist',
						"rampaging c'tan", 'deathless tweetlord', 'postmaster',
						'faerie queene', 'lord of the posts', 'ruthless cybercrat',
						'general secretary', 'commissar', 'kwisatz haderach',
						'dedicated hyperturing'

						-- feel free to add more
					)
					root.epithet = epithets[lib.crypt.random(intptr,0,[epithets.type.N])]
					root.rights.powers:fill() -- grant omnipotence
					root.rights.rank = 1
					var ruid = dlg:actor_create(&root)
					dlg:conf_set('master',root.handle)







|
>







385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
					root.handle = cfmode.arglist(0)
					var epithets = array(
						'root', 'god', 'regional jehovah', 'titan king',
						'king of olympus', 'cyberpharaoh', 'electric ellimist',
						"rampaging c'tan", 'deathless tweetlord', 'postmaster',
						'faerie queene', 'lord of the posts', 'ruthless cybercrat',
						'general secretary', 'commissar', 'kwisatz haderach',
						'dedicated hyperturing', 'grand inquisitor', 'reverend mother',
						'cyberpope', 'verified®', 'patent pending'
						-- feel free to add more
					)
					root.epithet = epithets[lib.crypt.random(intptr,0,[epithets.type.N])]
					root.rights.powers:fill() -- grant omnipotence
					root.rights.rank = 1
					var ruid = dlg:actor_create(&root)
					dlg:conf_set('master',root.handle)

Modified render/conf/users.t from [09fa445b20] to [4bed391611].

95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110








111
112
113
114
115
116
117
...
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
...
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
				then sanitized = pstr {ptr='', ct=0}
				else sanitized = lib.html.sanitize(cs(user.ptr.epithet),true)
			end
			cinp:lpush('<div class="elem"><label for="epithet">epithet</label><input type="text" id="epithet" name="epithet" value="'):ppush(sanitized):lpush('"></div>')
			if user.ptr.epithet ~= nil then sanitized:free() end
		end
		if co.who.rights.powers.invite() or co.who.rights.powers.discipline() then
			var min = 0
			if not (co.who.rights.powers.discipline() or
				co.who.rights.powers.demote() and co.who.rights.powers.invite())
					then min = user.ptr.rights.invites end
			var max = co.srv.cfg.maxinvites
			if not co.who.rights.powers.invite() then max = user.ptr.rights.invites end

			push_num_field(cinp, 'invites', 'invites', min, max, user.ptr.rights.invites, false)
		end








		cinp:lpush('</div><div class="elem"><div class="check-panel">')

		if user.ptr.id ~= co.who.id and
		   ((user.ptr.rights.rank == 0 and co.who.rights.powers.elevate()) or
		    (user.ptr.rights.rank >  0 and co.who.rights.powers.demote())) then
			push_checkbox(&cinp, 'staff', pstr.null(), 'site staff member', user.ptr.rights.rank > 0, true, pstr.null())
		end
................................................................................
		var unym: lib.str.acc unym:init(64)
		unym:lpush('<a href="/')
		if user(0).origin ~= 0 then unym:lpush('@') end
		do var sanxid = lib.html.sanitize(user(0).xid, true)
			unym:ppush(sanxid)
			sanxid:free() end
		unym:lpush('" class="id">')
		lib.render.nym(user.ptr,0,&unym)
		unym:lpush('</a>')
		var pg = data.view.conf_user_ctl {
			name = unym:finalize();
			inputcontent = cinpp;
			linkcontent = clnkp;
		}
		var ret = pg:tostr()
................................................................................
			if usr.rights.rank ~= 0 then
				ulst:lpush('<span class="regalia">')
				regalia(&ulst, usr.rights.rank)
				ulst:lpush('</span>')
			end
			if co.who:overpowers(usr) then
				ulst:lpush('<a class="id" href="users/'):push(&idbuf[0],idlen):lpush('">')
				lib.render.nym(usr, 0, &ulst)
				ulst:lpush('</a></li>')
			else
				ulst:lpush('<span class="id">')
				lib.render.nym(usr, 0, &ulst)
				ulst:lpush('</span></li>')
			end
		::skip::end
		ulst:lpush('</ul>')
		return ulst:finalize()
	end
	do return pstr.null() end







|



|




>
>
>
>
>
>
>
>







 







|







 







|



|







95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
...
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
...
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
				then sanitized = pstr {ptr='', ct=0}
				else sanitized = lib.html.sanitize(cs(user.ptr.epithet),true)
			end
			cinp:lpush('<div class="elem"><label for="epithet">epithet</label><input type="text" id="epithet" name="epithet" value="'):ppush(sanitized):lpush('"></div>')
			if user.ptr.epithet ~= nil then sanitized:free() end
		end
		if co.who.rights.powers.invite() or co.who.rights.powers.discipline() then
			var min: uint32 = 0
			if not (co.who.rights.powers.discipline() or
				co.who.rights.powers.demote() and co.who.rights.powers.invite())
					then min = user.ptr.rights.invites end
			var max: uint32 = co.srv.cfg.maxinvites
			if not co.who.rights.powers.invite() then max = user.ptr.rights.invites end

			push_num_field(cinp, 'invites', 'invites', min, max, user.ptr.rights.invites, false)
		end
		if co.who.rights.powers.elevate() or co.who.rights.powers.demote() then
			var max: uint32 = 5000
			if not co.who.rights.powers.elevate() then max = user.ptr.rights.quota end
			var min: uint32 = 0
			if not co.who.rights.powers.demote() then min = user.ptr.rights.quota end

			push_num_field(cinp, 'quota', 'quota', min, max, user.ptr.rights.quota, user.ptr.id == co.who.id and co.who.rights.rank ~= 1)
		end
		cinp:lpush('</div><div class="elem"><div class="check-panel">')

		if user.ptr.id ~= co.who.id and
		   ((user.ptr.rights.rank == 0 and co.who.rights.powers.elevate()) or
		    (user.ptr.rights.rank >  0 and co.who.rights.powers.demote())) then
			push_checkbox(&cinp, 'staff', pstr.null(), 'site staff member', user.ptr.rights.rank > 0, true, pstr.null())
		end
................................................................................
		var unym: lib.str.acc unym:init(64)
		unym:lpush('<a href="/')
		if user(0).origin ~= 0 then unym:lpush('@') end
		do var sanxid = lib.html.sanitize(user(0).xid, true)
			unym:ppush(sanxid)
			sanxid:free() end
		unym:lpush('" class="id">')
		lib.render.nym(user.ptr,0,&unym,false)
		unym:lpush('</a>')
		var pg = data.view.conf_user_ctl {
			name = unym:finalize();
			inputcontent = cinpp;
			linkcontent = clnkp;
		}
		var ret = pg:tostr()
................................................................................
			if usr.rights.rank ~= 0 then
				ulst:lpush('<span class="regalia">')
				regalia(&ulst, usr.rights.rank)
				ulst:lpush('</span>')
			end
			if co.who:overpowers(usr) then
				ulst:lpush('<a class="id" href="users/'):push(&idbuf[0],idlen):lpush('">')
				lib.render.nym(usr, 0, &ulst, false)
				ulst:lpush('</a></li>')
			else
				ulst:lpush('<span class="id">')
				lib.render.nym(usr, 0, &ulst, false)
				ulst:lpush('</span></li>')
			end
		::skip::end
		ulst:lpush('</ul>')
		return ulst:finalize()
	end
	do return pstr.null() end

Modified render/docpage.t from [148acf7303] to [97c704e199].

67
68
69
70
71
72
73
74
75
76
77
78
79
80
81

local terra 
pushbranches(list: &lib.str.acc, idx: intptr, ps: lib.store.powerset): {}
	var [pages] = array([allpages])
	var started = false
	for i=0,[pages.type.N] do
		if pages[i].parent == idx+1 and (pages[i].priv:sz() == 0 or 
				(ps and pages[i].priv) == pages[i].priv) then
			if not started then
				started = true
				list:lpush('<ul>')
			end
			list:lpush('<li><a href="/doc/'):rpush(pages[i].name):lpush('">')
				:rpush(pages[i].title):lpush('</a>')
			pushbranches(list, i, ps)







|







67
68
69
70
71
72
73
74
75
76
77
78
79
80
81

local terra 
pushbranches(list: &lib.str.acc, idx: intptr, ps: lib.store.powerset): {}
	var [pages] = array([allpages])
	var started = false
	for i=0,[pages.type.N] do
		if pages[i].parent == idx+1 and (pages[i].priv:sz() == 0 or 
				(ps and pages[i].priv):sz() ~= 0) then
			if not started then
				started = true
				list:lpush('<ul>')
			end
			list:lpush('<li><a href="/doc/'):rpush(pages[i].name):lpush('">')
				:rpush(pages[i].title):lpush('</a>')
			pushbranches(list, i, ps)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
..
18
19
20
21
22
23
24

25
26
27
28

29
30
31
32
33
34
35
36
37
-- vim: ft=terra
local pstr = lib.str.t
local terra cs(s: rawstring)
	return pstr { ptr = s, ct = lib.str.sz(s) }
end

local terra 
render_nym(who: &lib.store.actor, scope: uint64, tgt: &lib.str.acc)
	var acc: lib.str.acc
	var n: &lib.str.acc
	if tgt ~= nil then n = tgt else
		n = &acc
		n:init(128)
	end
	var xidsan = lib.html.sanitize(cs(who.xid),false)
................................................................................
		n:lpush('<span class="nym">'):ppush(nymsan)
			:lpush('</span> [<span class="handle">'):ppush(xidsan)
			:lpush('</span>]')
		nymsan:free()
	else n:lpush('<span class="handle">'):ppush(xidsan):lpush('</span>') end
	xidsan:free()


	if who.epithet ~= nil then
		var episan = lib.html.sanitize(cs(who.epithet),false)
		n:lpush('<span class="epithet">'):ppush(episan):lpush('</span>')
		episan:free()

	end
	
	-- TODO: if scope == chat room then lookup titles in room member db
	if tgt == nil then
		return n:finalize()
	else return pstr.null() end
end

return render_nym







|







 







>
|
|
|
|
>









1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
..
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
-- vim: ft=terra
local pstr = lib.str.t
local terra cs(s: rawstring)
	return pstr { ptr = s, ct = lib.str.sz(s) }
end

local terra 
render_nym(who: &lib.store.actor, scope: uint64, tgt: &lib.str.acc, minimal: bool)
	var acc: lib.str.acc
	var n: &lib.str.acc
	if tgt ~= nil then n = tgt else
		n = &acc
		n:init(128)
	end
	var xidsan = lib.html.sanitize(cs(who.xid),false)
................................................................................
		n:lpush('<span class="nym">'):ppush(nymsan)
			:lpush('</span> [<span class="handle">'):ppush(xidsan)
			:lpush('</span>]')
		nymsan:free()
	else n:lpush('<span class="handle">'):ppush(xidsan):lpush('</span>') end
	xidsan:free()

	if not minimal then
		if who.epithet ~= nil then
			var episan = lib.html.sanitize(cs(who.epithet),false)
			n:lpush('<span class="epithet">'):ppush(episan):lpush('</span>')
			episan:free()
		end
	end
	
	-- TODO: if scope == chat room then lookup titles in room member db
	if tgt == nil then
		return n:finalize()
	else return pstr.null() end
end

return render_nym

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

32
33
34
35
36
37
38
39
40
41
42



43
44
45
46
47
48
49
		var sn_follows   = cs(lib.math.decstr_friendly(stats.follows, sn_posts.ptr - 1))
		var sn_followers = cs(lib.math.decstr_friendly(stats.followers, sn_follows.ptr - 1))
		var sn_mutuals   = cs(lib.math.decstr_friendly(stats.mutuals, sn_followers.ptr - 1))
	var bio = lib.str.plit "<em>tall, dark, and mysterious</em>"
	if actor.bio ~= nil then
		bio = lib.smackdown.html(cs(actor.bio))
	end
	var fullname = lib.render.nym(actor,0,nil) defer fullname:free()
	var comments: lib.str.acc comments:init(64)
	-- this is really more what epithets are for, i think
	--if actor.rights.rank > 0 then comments:lpush('<li>staff member</li>') end



	if co.aid ~= 0 and actor.rights.rank ~= 0 then
		if co.who:outranks(actor) then
			comments:lpush('<li style="--co:50">underling</li>')
		elseif actor:outranks(co.who) then
			comments:lpush('<li style="--co:-50">outranks you</li>')
		end
	end







|



>
>
>







32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
		var sn_follows   = cs(lib.math.decstr_friendly(stats.follows, sn_posts.ptr - 1))
		var sn_followers = cs(lib.math.decstr_friendly(stats.followers, sn_follows.ptr - 1))
		var sn_mutuals   = cs(lib.math.decstr_friendly(stats.mutuals, sn_followers.ptr - 1))
	var bio = lib.str.plit "<em>tall, dark, and mysterious</em>"
	if actor.bio ~= nil then
		bio = lib.smackdown.html(cs(actor.bio))
	end
	var fullname = lib.render.nym(actor,0,nil,false) defer fullname:free()
	var comments: lib.str.acc comments:init(64)
	-- this is really more what epithets are for, i think
	--if actor.rights.rank > 0 then comments:lpush('<li>staff member</li>') end
	if co.srv.cfg.master == actor.id then
		comments:lpush('<li style="--co:-70">founder</li>')
	end
	if co.aid ~= 0 and actor.rights.rank ~= 0 then
		if co.who:outranks(actor) then
			comments:lpush('<li style="--co:50">underling</li>')
		elseif actor:outranks(co.who) then
			comments:lpush('<li style="--co:-50">outranks you</li>')
		end
	end

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

31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
	var livetime = co.srv:thread_latest_arrival_calc(p.id)

	var pg: lib.str.acc pg:init(256)
	lib.render.tweet(co, p, &pg)

	if co.aid ~= 0 then
		pg:lpush('<form class="action-bar" method="post">')
		var liked = false -- FIXME
		var rtd = false
		if not liked
			then pg:lpush('<button class="pos" name="act" value="like">like</button>')
			else pg:lpush('<button class="neg" name="act" value="dislike">dislike</button>')
		end
		if not rtd
			then pg:lpush('<button class="pos" name="act" value="rt">retweet</button>')
			else pg:lpush('<button class="neg" name="act" value="unrt">detweet</button>')
		end
		if p.author == co.who.id then
			pg:lpush('<a class="button" href="/post/'):rpush(path(1)):lpush('/edit">edit</a><a class="neg button" href="/post/'):rpush(path(1)):lpush('/del">delete</a>')
		end
		-- TODO list user's chosen reaction emoji
		pg:lpush('</form>')

	end







|
<
<



<
|
<
<







31
32
33
34
35
36
37
38


39
40
41

42


43
44
45
46
47
48
49
	var livetime = co.srv:thread_latest_arrival_calc(p.id)

	var pg: lib.str.acc pg:init(256)
	lib.render.tweet(co, p, &pg)

	if co.aid ~= 0 then
		pg:lpush('<form class="action-bar" method="post">')
		if not co.srv:post_liked_uid(co.who.id, p.id)


			then pg:lpush('<button class="pos" name="act" value="like">like</button>')
			else pg:lpush('<button class="neg" name="act" value="dislike">dislike</button>')
		end

		pg:lpush('<button class="pos" name="act" value="rt">retweet</button>')


		if p.author == co.who.id then
			pg:lpush('<a class="button" href="/post/'):rpush(path(1)):lpush('/edit">edit</a><a class="neg button" href="/post/'):rpush(path(1)):lpush('/del">delete</a>')
		end
		-- TODO list user's chosen reaction emoji
		pg:lpush('</form>')

	end

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

1
2
3
4
5
6
7












8
9

10
11
12

13
14
15

16




17
18
19
20
21
22
23
24

25
26
27
28
29
30
31
32
33
34
35
36
37
38

39








40
41
42
43
44
45
46
47
48
49
50
51
52





53








54

55
56

57
-- vim: ft=terra
local pstr = lib.mem.ptr(int8)
local terra cs(s: rawstring)
	return pstr { ptr = s, ct = lib.str.sz(s) }
end

local terra 












render_tweet(co: &lib.srv.convo, p: &lib.store.post, acc: &lib.str.acc)
	var author: &lib.store.actor

	for j = 0, co.actorcache.top do
		if p.author == co.actorcache(j).ptr.id then
			author = co.actorcache(j).ptr

			goto foundauth
		end
	end

	author = co.actorcache:insert(co.srv:actor_fetch_uid(p.author)).ptr





	::foundauth::
	var avistr: lib.str.acc if author.origin == 0 then
		avistr:compose('/avi/',author.handle)
	end
	var timestr: int8[26] lib.osclock.ctime_r(&p.posted, &timestr[0])

	var bhtml = lib.smackdown.html([lib.mem.ptr(int8)] {ptr=p.body,ct=0}) defer bhtml:free()


	var idbuf: int8[lib.math.shorthand.maxlen]
	var idlen = lib.math.shorthand.gen(p.id, idbuf)
	var permalink: lib.str.acc permalink:compose('/post/',{idbuf,idlen})
	var fullname = lib.render.nym(author,0,nil) defer fullname:free()
	var tpl = data.view.tweet {
		text = bhtml;
		subject = cs(lib.coalesce(p.subject,''));
		nym = fullname;
		when = cs(&timestr[0]);
		avatar = cs(author.avatar);
		acctlink = cs(author.xid);
		permalink = permalink:finalize();
		attr = ''

	}









	var attrbuf: int8[32]
	if p.accent ~= -1 and p.accent ~= co.ui_hue then
		var hdecbuf: int8[21]
		var hdec = lib.math.decstr(p.accent, &hdecbuf[20])
		lib.str.cpy(&attrbuf[0], ' style="--hue:')
		lib.str.cpy(&attrbuf[14], hdec)
		var len = &hdecbuf[20] - hdec 
		lib.str.cpy(&attrbuf[14] + len, '"')
		tpl.attr = &attrbuf[0]
	end

	defer tpl.permalink:free()





	if acc ~= nil then tpl:append(acc) return [lib.mem.ptr(int8)]{ptr=nil,ct=0} end








	var txt = tpl:tostr()

	return txt
end

return render_tweet







>
>
>
>
>
>
>
>
>
>
>
>

|
>

|
|
>



>
|
>
>
>
>







|
>




|








|
>

>
>
>
>
>
>
>
>













>
>
>
>
>
|
>
>
>
>
>
>
>
>
|
>
|
|
>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
-- vim: ft=terra
local pstr = lib.mem.ptr(int8)
local terra cs(s: rawstring)
	return pstr { ptr = s, ct = lib.str.sz(s) }
end

local terra 
push_promo_header(co: &lib.srv.convo, acc: &lib.str.acc, rter: &lib.store.actor, rid: uint64)
	acc:lpush('<div class="lede"><div class="promo"><img src="'):push(rter.avatar,0):lpush('"><a href="/')
	if rter.origin ~= 0 then acc:lpush('@') end
	acc:push(rter.xid,0):lpush('" class="username">')
	lib.render.nym(rter, 0, acc, true)
	acc:lpush('</a> retweeted</div>')
	if co.who.id == rter.id then
		acc:lpush('<a href="/post/'):shpush(rid):lpush('/del" class="del">✖</a>')
	end
end
			
local terra 
render_tweet(co: &lib.srv.convo, p: &lib.store.post, acc: &lib.str.acc)
	var author: &lib.store.actor = nil
	var retweeter: &lib.store.actor = nil
	for j = 0, co.actorcache.top do
		if p.author == co.actorcache(j).ptr.id then author    = co.actorcache(j).ptr end
		if p.rtdby  == co.actorcache(j).ptr.id then retweeter = co.actorcache(j).ptr end
		if author ~= nil and (p.rtdby == 0 or retweeter ~= nil) then
			goto foundauth
		end
	end
	if author == nil then
		author = co.actorcache:insert(co.srv:actor_fetch_uid(p.author)).ptr
	end
	if p.rtdby ~= 0 and retweeter == nil then
		retweeter = co.actorcache:insert(co.srv:actor_fetch_uid(p.rtdby)).ptr
	end

	::foundauth::
	var avistr: lib.str.acc if author.origin == 0 then
		avistr:compose('/avi/',author.handle)
	end
	var timestr: int8[26] lib.osclock.ctime_r(&p.posted, &timestr[0])

	var bhtml = lib.smackdown.html([lib.mem.ptr(int8)] {ptr=p.body,ct=0})
	defer bhtml:free()

	var idbuf: int8[lib.math.shorthand.maxlen]
	var idlen = lib.math.shorthand.gen(p.id, idbuf)
	var permalink: lib.str.acc permalink:compose('/post/',{idbuf,idlen})
	var fullname = lib.render.nym(author,0,nil, false) defer fullname:free()
	var tpl = data.view.tweet {
		text = bhtml;
		subject = cs(lib.coalesce(p.subject,''));
		nym = fullname;
		when = cs(&timestr[0]);
		avatar = cs(author.avatar);
		acctlink = cs(author.xid);
		permalink = permalink:finalize();
		attr = pstr{'',0};
		stats = pstr{'',0};
	}
	if p.rts + p.likes > 0 then
		var s: lib.str.acc s:init(128)
		s:lpush('<div class="stats">')
		if p.rts   > 0 then s:lpush('<div class="rt">'  ):ipush(p.rts  ):lpush('</div>') end
		if p.likes > 0 then s:lpush('<div class="like">'):ipush(p.likes):lpush('</div>') end
		s:lpush('</div>')
		tpl.stats = s:finalize()
	end

	var attrbuf: int8[32]
	if p.accent ~= -1 and p.accent ~= co.ui_hue then
		var hdecbuf: int8[21]
		var hdec = lib.math.decstr(p.accent, &hdecbuf[20])
		lib.str.cpy(&attrbuf[0], ' style="--hue:')
		lib.str.cpy(&attrbuf[14], hdec)
		var len = &hdecbuf[20] - hdec 
		lib.str.cpy(&attrbuf[14] + len, '"')
		tpl.attr = &attrbuf[0]
	end

	defer tpl.permalink:free()
	if acc ~= nil then
		if retweeter ~= nil then push_promo_header(co, acc, retweeter, p.rtact) end
		tpl:append(acc)
		if retweeter ~= nil then acc:lpush('</div>') end
		if p.rts + p.likes > 0 then tpl.stats:free() end
		return [lib.mem.ptr(int8)]{ptr=nil,ct=0}
	end

	if retweeter ~= nil then
		var rta: lib.str.acc rta:init(512)
		push_promo_header(co, &rta, retweeter, p.rtact)
		tpl:append(&rta) rta:lpush('</div>')
		return rta:finalize()
	else
		var txt = tpl:tostr()
		if p.rts + p.likes > 0 then tpl.stats:free() end
		return txt
	end
end
return render_tweet

Modified route.t from [2f7668c3df] to [666eb021ed].

222
223
224
225
226
227
228











229
230
231
232
233
234
235
236
237
238
239
240

241
242
243
244
245
246
247
...
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
					return
				else goto badop end
			end
		else goto badurl end
	end

	if meth == method.post then











		var replytext = co:ppostv('post')
		var acl = co:ppostv('acl')
		var subj = co:ppostv('subject')
		if not acl then acl = lib.str.plit 'all' end
		if not replytext then goto badop end
		
		var reply = lib.store.post {
			author = co.who.id, parent = pid;
			subject = subj.ptr, acl = acl.ptr, body = replytext.ptr;
		}

		reply:publish(co.srv)

	end

	lib.render.tweet_page(co, path, post.ptr)
	do return end

	::badurl:: do co:complain(404, 'invalid URL', 'this URL does not reference extant content or functionality') return end
	::badop :: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
................................................................................
			path(1):cmp(lib.str.lit 'emoji')
		) then goto nopriv

		elseif not co.who.rights.powers.rebrand() and (
			path(1):cmp(lib.str.lit 'brand')
		) then goto nopriv

		elseif not co.who.rights.powers.acct() and (
			path(1):cmp(lib.str.lit 'profile') or
			path(1):cmp(lib.str.lit 'acct')
		) then goto nopriv

		elseif not co.who.rights.powers:affect_users() and (
			path(1):cmp(lib.str.lit 'users')
		) then goto nopriv end







>
>
>
>
>
>
>
>
>
>
>
|
|
|
|
|
|
|
|
|
|

|
>







 







|







222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
...
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
					return
				else goto badop end
			end
		else goto badurl end
	end

	if meth == method.post then
		var act = co:ppostv('act')
		if act:cmp(lib.str.plit 'like') and not co.srv:post_liked_uid(co.who.id,pid) then
			co.srv:post_like(co.who.id, pid, false)
			post.ptr.likes = post.ptr.likes + 1
		elseif act:cmp(lib.str.plit 'dislike') and co.srv:post_liked_uid(co.who.id,pid) then
			co.srv:post_like(co.who.id, pid, true)
			post.ptr.likes = post.ptr.likes - 1
		elseif act:cmp(lib.str.plit 'rt') then
			co.srv:post_retweet(co.who.id, pid, false)
			post.ptr.rts = post.ptr.rts + 1
		elseif act:cmp(lib.str.plit 'post') then
			var replytext = co:ppostv('post')
			var acl = co:ppostv('acl')
			var subj = co:ppostv('subject')
			if not acl then acl = lib.str.plit 'all' end
			if not replytext then goto badop end
			
			var reply = lib.store.post {
				author = co.who.id, parent = pid;
				subject = subj.ptr, acl = acl.ptr, body = replytext.ptr;
			}

			reply:publish(co.srv)
		else goto badop end
	end

	lib.render.tweet_page(co, path, post.ptr)
	do return end

	::badurl:: do co:complain(404, 'invalid URL', 'this URL does not reference extant content or functionality') return end
	::badop :: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
................................................................................
			path(1):cmp(lib.str.lit 'emoji')
		) then goto nopriv

		elseif not co.who.rights.powers.rebrand() and (
			path(1):cmp(lib.str.lit 'brand')
		) then goto nopriv

		elseif not co.who.rights.powers.account() and (
			path(1):cmp(lib.str.lit 'profile') or
			path(1):cmp(lib.str.lit 'acct')
		) then goto nopriv

		elseif not co.who.rights.powers:affect_users() and (
			path(1):cmp(lib.str.lit 'users')
		) then goto nopriv end

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

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- sessions are implemented so as to avoid any local data storage. they
-- are tracked by storing an encrypted cookie which contains an authid,
-- a login epoch time, and a truncated hmac code authenticating both, all
-- encoded using Shorthand. we need functions to generate and parse these

local m = {
	maxlen = lib.math.shorthand.maxlen*3 + 2;
	maxage = 2 * 60 * 60; -- 2 hours
	cookiename = 'auth';
}

terra m.cookie_gen(secret: lib.mem.ptr(int8), authid: uint64, time: uint64, out: &int8): intptr
	var ptr = out
	ptr = ptr + lib.math.shorthand.gen(authid, ptr)
	@ptr = @'.' ptr = ptr + 1







|







2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- sessions are implemented so as to avoid any local data storage. they
-- are tracked by storing an encrypted cookie which contains an authid,
-- a login epoch time, and a truncated hmac code authenticating both, all
-- encoded using Shorthand. we need functions to generate and parse these

local m = {
	maxlen = lib.math.shorthand.maxlen*3 + 2;
	maxage = 16 * 60 * 60; -- 16 hours
	cookiename = 'auth';
}

terra m.cookie_gen(secret: lib.mem.ptr(int8), authid: uint64, time: uint64, out: &int8): intptr
	var ptr = out
	ptr = ptr + lib.math.shorthand.gen(authid, ptr)
	@ptr = @'.' ptr = ptr + 1

Modified srv.t from [34fad9fa1a] to [e0d52a1828].

10
11
12
13
14
15
16

17
18
19
20
21
22
23
...
800
801
802
803
804
805
806











807
808
809
810
811
812
813
814
	credmgd: bool
	maxupsz: intptr
	instance: lib.mem.ptr(int8)
	overlord: &srv
	ui_hue: uint16
	nranks: uint16
	maxinvites: uint16

}
local struct srv {
	sources: lib.mem.ptr(lib.store.source)
	webmgr: lib.net.mg_mgr
	webcon: &lib.net.mg_connection
	cfg: cfgcache
	id: rawstring
................................................................................
		end
		smode:free()
	end

	self.ui_hue = self:cfint('ui-accent',config.default_ui_accent)
	self.nranks = self:cfint('user-ranks',10)
	self.maxinvites = self:cfint('max-invites',64)











end

return {
	overlord = srv;
	convo = convo;
	route = route;
	secmode = secmode;
}







>







 







>
>
>
>
>
>
>
>
>
>
>








10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
...
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
	credmgd: bool
	maxupsz: intptr
	instance: lib.mem.ptr(int8)
	overlord: &srv
	ui_hue: uint16
	nranks: uint16
	maxinvites: uint16
	master: uint64
}
local struct srv {
	sources: lib.mem.ptr(lib.store.source)
	webmgr: lib.net.mg_mgr
	webcon: &lib.net.mg_connection
	cfg: cfgcache
	id: rawstring
................................................................................
		end
		smode:free()
	end

	self.ui_hue = self:cfint('ui-accent',config.default_ui_accent)
	self.nranks = self:cfint('user-ranks',10)
	self.maxinvites = self:cfint('max-invites',64)
	
	var webmaster = self.overlord:conf_get('master')
	if webmaster:ref() then defer webmaster:free()
		var wma = self.overlord:actor_fetch_xid(webmaster)
		if not wma then
			lib.warn('the webmaster specified in the configuration store does not seem to exist or is not known to this instance; preceding as if no master defined. if the master is a remote user, you can rectify this with the `actor ',{webmaster.ptr,webmaster.ct},' instantiate` and `conf refresh` commands')
		else
			self.master = wma(0).id
			wma:free()
		end
	end
end

return {
	overlord = srv;
	convo = convo;
	route = route;
	secmode = secmode;
}

Added static/heart.svg version [c2edd21438].





































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->

<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:xlink="http://www.w3.org/1999/xlink"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   width="20"
   height="20"
   viewBox="0 0 5.2916668 5.2916668"
   version="1.1"
   id="svg8"
   inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
   sodipodi:docname="heart.svg"
   inkscape:export-filename="/home/lexi/dev/parsav/static/heart.png"
   inkscape:export-xdpi="406.39999"
   inkscape:export-ydpi="406.39999">
  <defs
     id="defs2">
    <linearGradient
       id="linearGradient1395"
       inkscape:collect="always">
      <stop
         id="stop1391"
         offset="0"
         style="stop-color:#ff1616;stop-opacity:1" />
      <stop
         id="stop1393"
         offset="1"
         style="stop-color:#ff1d1d;stop-opacity:0" />
    </linearGradient>
    <linearGradient
       inkscape:collect="always"
       id="linearGradient1383">
      <stop
         style="stop-color:#980000;stop-opacity:1;"
         offset="0"
         id="stop1379" />
      <stop
         style="stop-color:#980000;stop-opacity:0;"
         offset="1"
         id="stop1381" />
    </linearGradient>
    <linearGradient
       inkscape:collect="always"
       id="linearGradient832">
      <stop
         style="stop-color:#ffcfcf;stop-opacity:1;"
         offset="0"
         id="stop828" />
      <stop
         style="stop-color:#ffcfcf;stop-opacity:0;"
         offset="1"
         id="stop830" />
    </linearGradient>
    <radialGradient
       inkscape:collect="always"
       xlink:href="#linearGradient832"
       id="radialGradient834"
       cx="3.2286437"
       cy="286.62921"
       fx="3.2286437"
       fy="286.62921"
       r="1.0866126"
       gradientTransform="matrix(1.8608797,0.8147617,-0.38242057,0.87343168,106.71446,33.692223)"
       gradientUnits="userSpaceOnUse" />
    <filter
       inkscape:collect="always"
       style="color-interpolation-filters:sRGB"
       id="filter1356"
       x="-0.044539396"
       width="1.0890788"
       y="-0.04671235"
       height="1.0934247">
      <feGaussianBlur
         inkscape:collect="always"
         stdDeviation="0.040330888"
         id="feGaussianBlur1358" />
    </filter>
    <radialGradient
       inkscape:collect="always"
       xlink:href="#linearGradient1383"
       id="radialGradient1385"
       cx="4.1787109"
       cy="286.89261"
       fx="4.1787109"
       fy="286.89261"
       r="1.2260786"
       gradientTransform="matrix(1.7016464,0,0,1.6348586,-2.9319775,-182.10895)"
       gradientUnits="userSpaceOnUse" />
    <radialGradient
       inkscape:collect="always"
       xlink:href="#linearGradient1395"
       id="radialGradient1389"
       gradientUnits="userSpaceOnUse"
       gradientTransform="matrix(0.66230313,-1.6430738,1.0154487,0.40931507,-290.06307,177.39489)"
       cx="4.02069"
       cy="287.79269"
       fx="4.02069"
       fy="287.79269"
       r="1.0866126" />
  </defs>
  <sodipodi:namedview
     id="base"
     pagecolor="#181818"
     bordercolor="#666666"
     borderopacity="1.0"
     inkscape:pageopacity="0"
     inkscape:pageshadow="2"
     inkscape:zoom="11.2"
     inkscape:cx="13.645085"
     inkscape:cy="22.307499"
     inkscape:document-units="mm"
     inkscape:current-layer="layer1"
     showgrid="false"
     units="px"
     inkscape:window-width="949"
     inkscape:window-height="1028"
     inkscape:window-x="963"
     inkscape:window-y="44"
     inkscape:window-maximized="0"
     showguides="false"
     fit-margin-top="0"
     fit-margin-left="0"
     fit-margin-right="0"
     fit-margin-bottom="0" />
  <metadata
     id="metadata5">
    <rdf:RDF>
      <cc:Work
         rdf:about="">
        <dc:format>image/svg+xml</dc:format>
        <dc:type
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
        <dc:title></dc:title>
      </cc:Work>
    </rdf:RDF>
  </metadata>
  <g
     inkscape:label="Layer 1"
     inkscape:groupmode="layer"
     id="layer1"
     transform="translate(-2.9526324,-283.47435)">
    <path
       sodipodi:type="inkscape:offset"
       inkscape:radius="0.14186843"
       inkscape:original="M 3.625 286.55273 C 3.0632316 286.5586 3.0996094 286.98633 3.0996094 286.98633 C 3.0996094 286.98633 3.0113255 287.32746 3.4589844 287.69727 C 3.9066436 288.06708 4.1796875 288.625 4.1796875 288.625 C 4.1796875 288.625 4.4507783 288.06708 4.8984375 287.69727 C 5.3460971 287.32746 5.2578125 286.98633 5.2578125 286.98633 C 5.2578125 286.98633 5.2941901 286.55873 4.7324219 286.55273 C 4.1706518 286.54711 4.1796875 287.19141 4.1796875 287.19141 C 4.1796875 287.19141 4.1867679 286.54673 3.625 286.55273 z "
       style="fill:url(#radialGradient1385);fill-opacity:1;stroke:none;stroke-width:0.12017766px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
       id="path822"
       d="m 3.6230469,286.41016 c -0.3169165,0.003 -0.5116816,0.14302 -0.5957031,0.29101 -0.084022,0.14799 -0.068359,0.29688 -0.068359,0.29688 l 0.00391,-0.0469 c 0,0 -0.02958,0.12684 0.011719,0.28711 0.041298,0.16028 0.1506583,0.3669 0.3945312,0.56836 0.4154821,0.34323 0.6835938,0.88086 0.6835938,0.88086 a 0.14188261,0.14188261 0 0 0 0.2539062,0 c 0,0 0.2663147,-0.53776 0.6816406,-0.88086 0.2438734,-0.20146 0.3532329,-0.40808 0.3945313,-0.56836 0.041298,-0.16027 0.011719,-0.28711 0.011719,-0.28711 l 0.00391,0.0469 c 0,0 0.015663,-0.14891 -0.068359,-0.29688 -0.084023,-0.14797 -0.2787972,-0.28763 -0.5957031,-0.29101 -0.2850618,-0.003 -0.4543151,0.15732 -0.5546875,0.32226 -0.100738,-0.16498 -0.2713805,-0.32531 -0.5566406,-0.32226 z"
       transform="matrix(2.157964,0,0,2.2461218,-3.4190419,-359.83766)" />
    <path
       id="path819"
       style="fill:#ff8080;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
       d="m 4.0453212,286.36379 c -0.9660318,-0.83062 -0.7766137,-1.59419 -0.7766137,-1.59419 0,0 -0.075768,-0.96249 1.1365076,-0.97567 1.2122749,-0.0127 1.1933329,1.43711 1.1933329,1.43711 0,0 -0.018944,-1.44975 1.1933326,-1.43711 1.2122754,0.0127 1.1365076,0.97567 1.1365076,0.97567 0,0 0.1894205,0.76357 -0.7766137,1.59419 -0.9660323,0.83065 -1.5532265,2.08431 -1.5532265,2.08431 0,0 -0.5871945,-1.25366 -1.5532268,-2.08431 z"
       inkscape:connector-curvature="0"
       sodipodi:nodetypes="scccccscs" />
    <path
       sodipodi:nodetypes="scccccscs"
       inkscape:connector-curvature="0"
       d="m 3.4589842,287.69653 c -0.4476589,-0.36981 -0.3598826,-0.70976 -0.3598826,-0.70976 0,0 -0.035111,-0.42851 0.5266574,-0.43438 0.5617679,-0.006 0.5529901,0.63982 0.5529901,0.63982 0,0 -0.00878,-0.64544 0.5529901,-0.63982 0.5617682,0.006 0.5266574,0.43438 0.5266574,0.43438 0,0 0.087777,0.33995 -0.3598826,0.70976 -0.4476592,0.36981 -0.7197649,0.92795 -0.7197649,0.92795 0,0 -0.2721057,-0.55814 -0.7197649,-0.92795 z"
       style="fill:url(#radialGradient834);fill-opacity:1;stroke:none;stroke-width:0.12017766px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter1356)"
       id="path826"
       transform="matrix(2.157964,0,0,2.2461218,-3.4190419,-359.83766)" />
    <path
       id="path1387"
       style="fill:url(#radialGradient1389);fill-opacity:1;stroke:none;stroke-width:0.12017766px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter1356)"
       d="m 3.4589842,287.69653 c -0.4476589,-0.36981 -0.3598826,-0.70976 -0.3598826,-0.70976 0,0 -0.035111,-0.42851 0.5266574,-0.43438 0.5617679,-0.006 0.5529901,0.63982 0.5529901,0.63982 0,0 -0.00878,-0.64544 0.5529901,-0.63982 0.5617682,0.006 0.5266574,0.43438 0.5266574,0.43438 0,0 0.087777,0.33995 -0.3598826,0.70976 -0.4476592,0.36981 -0.7197649,0.92795 -0.7197649,0.92795 0,0 -0.2721057,-0.55814 -0.7197649,-0.92795 z"
       inkscape:connector-curvature="0"
       sodipodi:nodetypes="scccccscs"
       transform="matrix(2.157964,0,0,2.2461218,-3.4190419,-359.83766)" />
  </g>
</svg>

Modified static/live.js from [86bdd64b84] to [682908b4c8].

1
2
3
4
5


















































6
7
8
9
10
11
12
..
38
39
40
41
42
43
44
45

46
47
48
49


50
/* first things first, we need to scan over the document and see
 * if there are any UI elements unfortunate enough to need
 * interactivity beyond what native HTML+CSS can provide. if so,
 * we attach the appropriate listeners to them. */
window.addEventListener('load', function() {


















































	/* update hue-picker background when slider is adjusted */
	document.querySelectorAll('.color-picker').forEach(function(box) {
		let slider = box.querySelector('[data-color-pick]');
		box.style.setProperty('--hue', slider.value);
		slider.addEventListener('input', function(e) {
			box.style.setProperty('--hue', e.target.value);
		});
................................................................................
					return;
				}
				container._liveLastArrival = newest

				resp.text().then(function(htmlbody) {
					var parser = new DOMParser();
					var newdoc = parser.parseFromString(htmlbody,'text/html')
					container.innerHTML = newdoc.getElementById(container.id).innerHTML

				})
			})
		}, interv)
	});


});





>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







 







|
>




>
>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
..
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
/* first things first, we need to scan over the document and see
 * if there are any UI elements unfortunate enough to need
 * interactivity beyond what native HTML+CSS can provide. if so,
 * we attach the appropriate listeners to them. */
window.addEventListener('load', function() {
	/* social media is less fun when you can't just click on a tweet
	 * to insta-like or -retweet it. this is unfortunately not possible
	 * (except in various hideously shitty ways) without javascript. */
	function mk(elt) { return document.createElement(elt); }
	function attachButtons() {
		document.querySelectorAll('body:not(.post) main div.post').forEach(function(post){
			let url = post.querySelector('.permalink').attributes.getNamedItem('href').value;
			function postReq(act,elt) {
				fetch(new Request(url, {
					method: 'POST',
					body: 'act='+act
				})).then(function(resp) {
					if (resp.ok && resp.status == 200) {
						var i = parseInt(elt.innerHTML)
						if (isNaN(i)) {i=0}
						elt.innerHTML = (i+1).toString()
					}
				})
			}

			var stats = post.querySelector('.stats');
			if (stats == null) {
				/* no stats box; create one */
				var n = mk('div');
				n.classList.add('stats');
				post.appendChild(n);
				stats = n
			}
			function ensureElt(cls, before) {
				let s = stats.querySelector('.' + cls);
				if (s == null) {
					var n = mk('div');
					n.classList.add(cls);
					if (before == null) { stats.appendChild(n) } else {
						stats.insertBefore(n,stats.querySelector(before))
					}
					return n
				} else { return s }
			}
			var like = ensureElt('like', null);
			var rt   = ensureElt('rt','.like');
			function activate(elt,name) {
				elt.addEventListener('click', function(e) { postReq(name,elt) });
				elt.style.setProperty('cursor','pointer');
			}
			activate(like,'like');
			activate(rt,'rt');
		});
	}

	/* update hue-picker background when slider is adjusted */
	document.querySelectorAll('.color-picker').forEach(function(box) {
		let slider = box.querySelector('[data-color-pick]');
		box.style.setProperty('--hue', slider.value);
		slider.addEventListener('input', function(e) {
			box.style.setProperty('--hue', e.target.value);
		});
................................................................................
					return;
				}
				container._liveLastArrival = newest

				resp.text().then(function(htmlbody) {
					var parser = new DOMParser();
					var newdoc = parser.parseFromString(htmlbody,'text/html')
					container.innerHTML = newdoc.getElementById(container.id).innerHTML;
					attachButtons();
				})
			})
		}, interv)
	});

	attachButtons();
});

Added static/retweet.svg version [c3fa459a24].





























































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->

<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:xlink="http://www.w3.org/1999/xlink"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   width="20"
   height="20"
   viewBox="0 0 5.2916664 5.2916665"
   version="1.1"
   id="svg8"
   inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
   sodipodi:docname="retweet.svg">
  <defs
     id="defs2">
    <linearGradient
       id="linearGradient2866"
       inkscape:collect="always">
      <stop
         id="stop2862"
         offset="0"
         style="stop-color:#9a57ff;stop-opacity:1" />
      <stop
         id="stop2864"
         offset="1"
         style="stop-color:#ab73ff;stop-opacity:0" />
    </linearGradient>
    <linearGradient
       inkscape:collect="always"
       id="linearGradient1966">
      <stop
         style="stop-color:#f0e7ff;stop-opacity:1;"
         offset="0"
         id="stop1962" />
      <stop
         style="stop-color:#f0e7ff;stop-opacity:0;"
         offset="1"
         id="stop1964" />
    </linearGradient>
    <linearGradient
       inkscape:collect="always"
       id="linearGradient1468">
      <stop
         style="stop-color:#9955ff;stop-opacity:1;"
         offset="0"
         id="stop1464" />
      <stop
         style="stop-color:#9955ff;stop-opacity:0;"
         offset="1"
         id="stop1466" />
    </linearGradient>
    <linearGradient
       inkscape:collect="always"
       id="linearGradient1403">
      <stop
         style="stop-color:#ccaaff;stop-opacity:1;"
         offset="0"
         id="stop1399" />
      <stop
         style="stop-color:#ccaaff;stop-opacity:0;"
         offset="1"
         id="stop1401" />
    </linearGradient>
    <linearGradient
       id="linearGradient1395"
       inkscape:collect="always">
      <stop
         id="stop1391"
         offset="0"
         style="stop-color:#ff1616;stop-opacity:1" />
      <stop
         id="stop1393"
         offset="1"
         style="stop-color:#ff1d1d;stop-opacity:0" />
    </linearGradient>
    <linearGradient
       inkscape:collect="always"
       id="linearGradient1383">
      <stop
         style="stop-color:#980000;stop-opacity:1;"
         offset="0"
         id="stop1379" />
      <stop
         style="stop-color:#980000;stop-opacity:0;"
         offset="1"
         id="stop1381" />
    </linearGradient>
    <linearGradient
       inkscape:collect="always"
       id="linearGradient832">
      <stop
         style="stop-color:#ffcfcf;stop-opacity:1;"
         offset="0"
         id="stop828" />
      <stop
         style="stop-color:#ffcfcf;stop-opacity:0;"
         offset="1"
         id="stop830" />
    </linearGradient>
    <radialGradient
       inkscape:collect="always"
       xlink:href="#linearGradient832"
       id="radialGradient834"
       cx="3.2286437"
       cy="286.62921"
       fx="3.2286437"
       fy="286.62921"
       r="1.0866126"
       gradientTransform="matrix(1.8608797,0.8147617,-0.38242057,0.87343168,106.71446,33.692223)"
       gradientUnits="userSpaceOnUse" />
    <radialGradient
       inkscape:collect="always"
       xlink:href="#linearGradient1383"
       id="radialGradient1385"
       cx="4.1787109"
       cy="286.89261"
       fx="4.1787109"
       fy="286.89261"
       r="1.2260786"
       gradientTransform="matrix(1.7016464,0,0,1.6348586,-2.9319775,-182.10895)"
       gradientUnits="userSpaceOnUse" />
    <radialGradient
       inkscape:collect="always"
       xlink:href="#linearGradient1395"
       id="radialGradient1389"
       gradientUnits="userSpaceOnUse"
       gradientTransform="matrix(0.66230313,-1.6430738,1.0154487,0.40931507,-290.06307,177.39489)"
       cx="4.02069"
       cy="287.79269"
       fx="4.02069"
       fy="287.79269"
       r="1.0866126" />
    <linearGradient
       inkscape:collect="always"
       xlink:href="#linearGradient1403"
       id="linearGradient1405"
       x1="8.3939333"
       y1="288.1091"
       x2="7.0158253"
       y2="287.32819"
       gradientUnits="userSpaceOnUse" />
    <filter
       inkscape:collect="always"
       style="color-interpolation-filters:sRGB"
       id="filter2508"
       x="-0.24674278"
       width="1.4934856"
       y="-0.13581935"
       height="1.2716388">
      <feGaussianBlur
         inkscape:collect="always"
         stdDeviation="0.056246184"
         id="feGaussianBlur2510" />
    </filter>
    <filter
       inkscape:collect="always"
       style="color-interpolation-filters:sRGB"
       id="filter3064"
       x="-0.07694713"
       width="1.1538943"
       y="-0.14115551"
       height="1.282311">
      <feGaussianBlur
         inkscape:collect="always"
         stdDeviation="0.065422039"
         id="feGaussianBlur3066" />
    </filter>
    <linearGradient
       inkscape:collect="always"
       xlink:href="#linearGradient2866"
       id="linearGradient1533"
       gradientUnits="userSpaceOnUse"
       x1="8.3939333"
       y1="288.1091"
       x2="7.0097656"
       y2="287.25977" />
    <radialGradient
       inkscape:collect="always"
       xlink:href="#linearGradient1468"
       id="radialGradient1535"
       gradientUnits="userSpaceOnUse"
       gradientTransform="matrix(1,0,0,1.5198212,0,-149.75763)"
       cx="8.525074"
       cy="288.10031"
       fx="8.525074"
       fy="288.10031"
       r="0.43142718" />
    <linearGradient
       inkscape:collect="always"
       xlink:href="#linearGradient1403"
       id="linearGradient1537"
       gradientUnits="userSpaceOnUse"
       x1="8.3939333"
       y1="288.1091"
       x2="7.0158253"
       y2="287.32819" />
    <radialGradient
       inkscape:collect="always"
       xlink:href="#linearGradient1966"
       id="radialGradient1539"
       gradientUnits="userSpaceOnUse"
       gradientTransform="matrix(1,0,0,1.2339206,0,-67.391253)"
       cx="8.7198324"
       cy="288.09686"
       fx="8.7198324"
       fy="288.09686"
       r="0.27354568" />
  </defs>
  <sodipodi:namedview
     id="base"
     pagecolor="#181818"
     bordercolor="#666666"
     borderopacity="1.0"
     inkscape:pageopacity="0"
     inkscape:pageshadow="2"
     inkscape:zoom="7.919596"
     inkscape:cx="7.7101412"
     inkscape:cy="36.101286"
     inkscape:document-units="mm"
     inkscape:current-layer="layer1"
     showgrid="false"
     units="px"
     inkscape:window-width="949"
     inkscape:window-height="1028"
     inkscape:window-x="963"
     inkscape:window-y="44"
     inkscape:window-maximized="0"
     showguides="false"
     fit-margin-top="0"
     fit-margin-left="0"
     fit-margin-right="0"
     fit-margin-bottom="0" />
  <metadata
     id="metadata5">
    <rdf:RDF>
      <cc:Work
         rdf:about="">
        <dc:format>image/svg+xml</dc:format>
        <dc:type
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
        <dc:title></dc:title>
      </cc:Work>
    </rdf:RDF>
  </metadata>
  <g
     inkscape:label="Layer 1"
     inkscape:groupmode="layer"
     id="layer1"
     transform="translate(-2.6134661,-283.36966)">
    <g
       id="g1452"
       transform="matrix(2.0546825,0,0,1.965062,-10.834174,-279.0744)"
       style="stroke-width:0.49766773">
      <path
         sodipodi:type="inkscape:offset"
         inkscape:radius="0.051069338"
         inkscape:original="M 7.015625 287.32812 L 6.5898438 287.41211 C 6.5898438 287.41211 6.7325506 288.06384 6.7910156 288.16406 C 6.8494806 288.26426 8.5195312 288.33789 8.5195312 288.33789 L 8.5273438 287.88867 C 8.5273438 287.88867 7.158409 287.98057 7.125 287.88867 C 7.0915917 287.7968 7.015625 287.32812 7.015625 287.32812 z "
         style="fill:url(#linearGradient1533);fill-opacity:1;stroke:none;stroke-width:0.13167457px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3064)"
         id="path2512"
         d="m 7.0058594,287.27734 -0.4257813,0.084 a 0.05107445,0.05107445 0 0 0 -0.041016,0.0625 c 0,0 0.036909,0.16168 0.080078,0.33789 0.021585,0.0881 0.044485,0.17955 0.066406,0.25586 0.021922,0.0763 0.038348,0.13382 0.060547,0.17187 0.016645,0.0285 0.035211,0.0334 0.054687,0.041 0.019476,0.008 0.043217,0.0134 0.070313,0.0195 0.054191,0.0123 0.125713,0.0226 0.2089843,0.0332 0.1665426,0.0212 0.3808629,0.0411 0.59375,0.0566 0.4257743,0.031 0.84375,0.0488 0.84375,0.0488 a 0.05107445,0.05107445 0 0 0 0.052734,-0.0508 l 0.00781,-0.44922 a 0.05107445,0.05107445 0 0 0 -0.054687,-0.0508 c 0,0 -0.3402778,0.0237 -0.6855469,0.0352 -0.1726345,0.006 -0.3476553,0.008 -0.4785156,0.004 -0.06543,-0.002 -0.1194572,-0.006 -0.15625,-0.0117 -0.015583,-0.002 -0.025917,-0.006 -0.033203,-0.008 -0.00622,-0.0201 -0.015078,-0.0559 -0.025391,-0.10547 -0.011663,-0.0561 -0.025573,-0.12344 -0.037109,-0.1875 -0.023072,-0.12812 -0.041016,-0.24414 -0.041016,-0.24414 a 0.05107445,0.05107445 0 0 0 -0.060547,-0.043 z" />
      <path
         d="m 8.3984375,287.4707 a 0.12639828,0.12639828 0 0 0 -0.125,0.10157 c -0.077254,0.36258 -0.064406,0.70957 -0.00195,1.04296 a 0.12639828,0.12639828 0 0 0 0.234375,0.0391 c 0.1095654,-0.19397 0.2246157,-0.35162 0.4375,-0.44532 a 0.12639828,0.12639828 0 0 0 -0.011719,-0.23632 c -0.170448,-0.055 -0.3069446,-0.1953 -0.421875,-0.42969 a 0.12639828,0.12639828 0 0 0 -0.1113281,-0.0723 z"
         id="path1430"
         style="fill:url(#radialGradient1535);fill-opacity:1;stroke:none;stroke-width:0.13167457px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
         inkscape:original="M 8.3964844 287.59766 C 8.3232974 287.94116 8.3363034 288.27053 8.3964844 288.5918 C 8.5106543 288.38968 8.6461782 288.2022 8.8925781 288.09375 C 8.6798713 288.02515 8.5201615 287.84989 8.3964844 287.59766 z "
         inkscape:radius="0.12638564"
         sodipodi:type="inkscape:offset" />
      <path
         inkscape:connector-curvature="0"
         id="path1397"
         d="m 8.519216,288.33878 c 0,0 -1.6704347,-0.0752 -1.7288997,-0.1754 -0.058465,-0.10022 -0.200452,-0.75169 -0.200452,-0.75169 l 0.4259608,-0.0835 c 0,0 0.07517,0.46773 0.1085783,0.5596 0.033409,0.0919 1.403165,0 1.403165,0 z"
         style="fill:url(#linearGradient1537);fill-opacity:1;stroke:none;stroke-width:0.13167457px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
      <path
         sodipodi:nodetypes="cccc"
         inkscape:connector-curvature="0"
         d="m 8.3960186,287.59752 c 0.1236771,0.25223 0.2842532,0.42835 0.49696,0.49695 -0.2463999,0.10845 -0.3827901,0.29483 -0.49696,0.49695 -0.060181,-0.32127 -0.073187,-0.6504 0,-0.9939 z"
         style="fill:#d8c0ff;fill-opacity:1;stroke:none;stroke-width:0.13167457px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
         id="path1409" />
      <path
         id="path1960"
         style="fill:url(#radialGradient1539);fill-opacity:1;stroke:none;stroke-width:0.13167457px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter2508)"
         d="m 8.3960186,287.59752 c 0.1236771,0.25223 0.2842532,0.42835 0.49696,0.49695 -0.2463999,0.10845 -0.3827901,0.29483 -0.49696,0.49695 -0.060181,-0.32127 -0.073187,-0.6504 0,-0.9939 z"
         inkscape:connector-curvature="0"
         sodipodi:nodetypes="cccc" />
    </g>
    <use
       x="0"
       y="0"
       xlink:href="#g1452"
       id="use1460"
       transform="rotate(180,5.2593307,286.0155)"
       width="100%"
       height="100%"
       style="stroke-width:0.49766773" />
  </g>
</svg>

Modified static/style.scss from [4fd9d6949f] to [322d30fa17].

79
80
81
82
83
84
85

86
87
88
89
90
91
92
93
...
482
483
484
485
486
487
488










489
490
491

492
493
494
495
496
497
498
499
...
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535














536











537
538
539
540
541
542
543
...
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
...
870
871
872
873
874
875
876






























	text-shadow: 1px 1px black;
	text-decoration: none;
	text-align: center;
	cursor: default;
	user-select: none;
	-webkit-user-drag: none;
	-webkit-app-region: no-drag;

	background: linear-gradient(to bottom,
		otone(-47%),
		otone(-50%) 15%,
		otone(-50%) 75%,
		otone(-53%)
	);
	&:hover, &:focus {
		@extend %glow;
................................................................................
}

div.thread {
	margin-left: 0.3in;
	& + div.post { margin-top: 0.3in; }
}











div.post {
	@extend %box;
	display: grid;

	grid-template-columns: 1in 1fr max-content;
	grid-template-rows: min-content max-content;
	margin-bottom: 0.1in;
	>.avatar {
		grid-column: 1/2; grid-row: 1/2;
		img { display: block; width: 1in; height: 1in; margin:0; }
		background: linear-gradient(to bottom, tone(-53%), tone(-57%));
	}
................................................................................
		display: block;
		grid-column: 1/3;
		grid-row: 2/3;
		text-align: left;
		text-decoration: none;
		padding: 0.1in;
		padding-left: 0.15in;
		>.nym { font-weight: bold; }
		color: tone(0%,-0.4);
		> span.nym { color: tone(10%) }
		> span.handle { color: tone(-5%) }
		background: linear-gradient(to right, tone(-55%), transparent);
		&:hover {
			> span.nym { color: white; }
			> span.handle { color: tone(15%) }
		}
	}
	>.content {
		grid-column: 2/4; grid-row: 1/2;
		padding: 0.2in;
		@extend %serif;
		font-size: 110%;
		text-align: justify;
		color: tone(25%);
	}
	> a[href].permalink {
		display: block;
		grid-column: 3/4; grid-row: 2/3;
		font-size: 80%;
		text-align: right;
		padding: 0.1in;
		padding-right: 0.15in;
		font-style: italic;
		background: linear-gradient(to left, tone(-55%,-0.5), transparent);
	}














}












a[href].rawlink {
	@extend %teletype;
}

body.doc main {
	@extend %serif;
................................................................................
		&+label:hover {
			background-color: otone(-35%);
			color: white;
		}
		&:checked+label {
			border-top: 1px solid otone(-10%);
			border-bottom: 1px solid otone(-50%);
			background: linear-gradient(to bottom, otone(-25%,-0.2), otone(-28%,-0.4) 35%, otone(-30%,-0.7));
			color: white;
			box-shadow: 0 0 0 1px tone(-60%);
			&:hover {
				border-top: 1px solid otone(10%);
				border-bottom: 1px solid otone(-60%);
				font-weight: bold;
			}
................................................................................
				transform: rotate(90deg) scale(1.1);
				color: tone(-20%);
				text-shadow: 0 0 8px tone(-30%);
			}
		}
	}
}





































>
|







 







>
>
>
>
>
>
>
>
>
>



>
|







 







<
<
<
<

<
<
<
|
<

|








|







>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>







 







|







 







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
...
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
...
513
514
515
516
517
518
519




520



521

522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
...
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
...
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
	text-shadow: 1px 1px black;
	text-decoration: none;
	text-align: center;
	cursor: default;
	user-select: none;
	-webkit-user-drag: none;
	-webkit-app-region: no-drag;
	--icon: url(/s/heart.webp);
	background-image: linear-gradient(to bottom,
		otone(-47%),
		otone(-50%) 15%,
		otone(-50%) 75%,
		otone(-53%)
	);
	&:hover, &:focus {
		@extend %glow;
................................................................................
}

div.thread {
	margin-left: 0.3in;
	& + div.post { margin-top: 0.3in; }
}

a[href].username {
	>.nym { font-weight: bold; }
	color: tone(0%,-0.4);
	> span.nym { color: tone(10%) }
	> span.handle { color: tone(-5%) }
	&:hover {
		> span.nym { color: white; }
		> span.handle { color: tone(15%) }
	}
}
div.post {
	@extend %box;
	display: grid;
	margin: unset;
	grid-template-columns: 1in 1fr max-content max-content;
	grid-template-rows: min-content max-content;
	margin-bottom: 0.1in;
	>.avatar {
		grid-column: 1/2; grid-row: 1/2;
		img { display: block; width: 1in; height: 1in; margin:0; }
		background: linear-gradient(to bottom, tone(-53%), tone(-57%));
	}
................................................................................
		display: block;
		grid-column: 1/3;
		grid-row: 2/3;
		text-align: left;
		text-decoration: none;
		padding: 0.1in;
		padding-left: 0.15in;




		background: linear-gradient(to right, tone(-55%), transparent);



	}

	>.content {
		grid-column: 2/5; grid-row: 1/2;
		padding: 0.2in;
		@extend %serif;
		font-size: 110%;
		text-align: justify;
		color: tone(25%);
	}
	> a[href].permalink {
		display: block;
		grid-column: 4/5; grid-row: 2/3;
		font-size: 80%;
		text-align: right;
		padding: 0.1in;
		padding-right: 0.15in;
		font-style: italic;
		background: linear-gradient(to left, tone(-55%,-0.5), transparent);
	}
	div.stats {
		display: flex;
		grid-column: 3/4; grid-row: 2/3;
		justify-content: center;
		> .like, > .rt {
			margin: 0.5em 0.3em;
			padding-left: 1.3em;
			background-size: 1.1em;
			background-repeat: no-repeat;
			min-width: 0.3em;
			&:empty {
				transition: 0.3s;
				opacity: 0.1;
				&:hover { opacity: 0.6 !important; }
			}
		}
		> .like {
			background-image: url(/s/heart.webp);
		}
		> .rt {
			background-image: url(/s/retweet.webp);
		}
	}
}

div.post:hover div.stats { > .like, > .rt { &:empty {opacity: 0.3;} } }

a[href].rawlink {
	@extend %teletype;
}

body.doc main {
	@extend %serif;
................................................................................
		&+label:hover {
			background-color: otone(-35%);
			color: white;
		}
		&:checked+label {
			border-top: 1px solid otone(-10%);
			border-bottom: 1px solid otone(-50%);
			background: linear-gradient(to bottom, otone(-25%,-0.2), otone(-28%,-0.3) 35%, otone(-30%,-0.5));
			color: white;
			box-shadow: 0 0 0 1px tone(-60%);
			&:hover {
				border-top: 1px solid otone(10%);
				border-bottom: 1px solid otone(-60%);
				font-weight: bold;
			}
................................................................................
				transform: rotate(90deg) scale(1.1);
				color: tone(-20%);
				text-shadow: 0 0 8px tone(-30%);
			}
		}
	}
}

div.lede {
	display: grid;
	grid-template-columns: 1fr min-content;
	grid-template-rows: 1.5em 1fr;
	padding: 0.1in 0.3in;
	margin: 0 -0.2in;
	margin-top: 0.2in;
	border-radius: 3px;
	background: linear-gradient(to bottom, tone(-40%,-0.5), transparent);
	border-top: 1px solid tone(-5%,-0.7);
	> .promo {
		grid-row: 1/2; grid-column: 1/2;
		font-style: italic;
		font-size: 90%;
		color: tone(-10%);
		> img {
			vertical-align: middle;
			margin-right: 0.4em;
			width: 1em; height: 1em;
		}
	}
	> a[href].del {
		grid-row: 1/2; grid-column: 2/3;
		text-decoration: none;
	}
	> .post {
		grid-row: 2/3; grid-column: 1/3;
	}
}

Modified store.t from [da1c9184b0] to [6a465decce].

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
..
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

37
38
39
40
41
42
43
44
45
46
47

48
49
50
51
52
53
54
..
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
...
212
213
214
215
216
217
218
219



220
221
222
223
224
225
226
...
381
382
383
384
385
386
387

388
389
390
391
392
393
394
...
413
414
415
416
417
418
419







420
421
422
423
424
425
426
local m = {
	timepoint = lib.osclock.time_t;
	scope = lib.enum {
		'public', 'private', 'local';
		'personal', 'direct', 'circle';
	};
	notiftype = lib.enum {
		'mention', 'like', 'rt', 'react'
	};

	relation = lib.set {
		'silence', -- messages will not be accepted
		'collapse', -- posts will be collapsed by default
		'disemvowel', -- posts will be ritually humiliated, but shown
		'avoid', -- posts will be kept out of the timeline but will show on users' posts and in conversations
................................................................................
		'mute', -- posts will be completely hidden at all times
		'block', -- no interactions will be permitted, but posts will remain visible
	};
	credset = lib.set {
		'pw', 'otp', 'challenge', 'trust'
	};
	privset = lib.set {
		'post', 'edit', 'acct', 'upload', 'censor', 'admin', 'invite'
	};
	powerset = lib.set {
		-- user powers -- default on
		'login', -- not locked out
		'visible', -- account & posts can be seen by others
		'post', -- can do poasts
		'shout', -- posts show up on local timeline
		'propagate', -- posts are sent to other instances
		'artifact', -- upload, claim, and manage artifacts
		'acct', -- configure own account
		'edit'; -- edit own poasts


		-- admin powers -- default off
		'purge', -- permanently delete users
		'config', -- change daemon policy & config UI
		'censor', -- dispose of badthink
		'discipline', -- enforced timeouts, stripping badges and epithets, punitive actions that do not permanently deprive of powers; can remove own injunctions but not others'
		'vacate', -- can remove others' injunctions, but not apply them
		'cred', -- alter credentials
		'elevate', 'demote', -- change user rank, give and take powers, including the ability to log in
		'rebrand', -- modify site's brand identity
		'herald', -- grant serverwide epithets and badges

		'invite' -- *unlimited* invites
	};
	prepmode = lib.enum {
		'full','conf','admin'
	}
}

................................................................................
	var pow: m.powerset pow:clear()
	(pow.login     << true)
	(pow.visible   << true)
	(pow.post      << true)
	(pow.shout     << true)
	(pow.propagate << true)
	(pow.artifact  << true)
	(pow.acct      << true)
	(pow.edit      << true)
	return m.rights { rank = 0, quota = 1000, invites = 0, powers = pow; }
end

struct m.actor {
	id: uint64
	nym: str
................................................................................
	mentions: lib.mem.ptr(uint64)
	circles: lib.mem.ptr(uint64) --only meaningful if scope is set to circle
	convoheaduri: str
	parent: uint64
-- ephemera
	localpost: bool
	accent: int16
	depth: uint16 -- used in conversations to indicate tree depth



	source: &m.source

	-- save :: bool -> {} (defined in acl.t due to dep. hell)
}

m.user_conf_funcs = function(be,n,ty,rty,rty2)
	rty = rty or ty
................................................................................
			-- origin: inet
			-- cookie issue time: m.timepoint
	actor_auth_register_uid: {&m.source, uint64, uint64} -> {}
		-- notifies the backend module of the UID that has been assigned for
		-- an authentication ID
			-- aid: uint64
			-- uid: uint64


	auth_enum_uid:    {&m.source, uint64}    -> lib.mem.lstptr(m.auth)
	auth_enum_handle: {&m.source, rawstring} -> lib.mem.lstptr(m.auth)
	auth_attach_pw: {&m.source, uint64, bool, pstr, pstr} -> {}
		-- uid: uint64
		-- reset: bool (delete other passwords?)
		-- pw: pstring
................................................................................
	post_enum_author_uid: {&m.source, uint64, m.range} -> lib.mem.lstptr(m.post)
	post_enum_parent: {&m.source, uint64} -> lib.mem.lstptr(m.post)
	post_attach_ctl: {&m.source, uint64, uint64, bool} -> {}
		-- attaches or detaches an existing database artifact
			-- post id: uint64
			-- artifact id: uint64
			-- detach: bool








	thread_latest_arrival_calc: {&m.source, uint64} -> m.timepoint

	artifact_instantiate: {&m.source, lib.mem.ptr(uint8), lib.mem.ptr(int8)} -> uint64
		-- instantiate an artifact in the database, either installing a new
		-- artifact or returning the id of an existing artifact with the same hash
			-- artifact: bytea







|







 







|









|

>











>







 







|







 







|
>
>
>







 







>







 







>
>
>
>
>
>
>







2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
..
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
..
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
...
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
...
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
...
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
local m = {
	timepoint = lib.osclock.time_t;
	scope = lib.enum {
		'public', 'private', 'local';
		'personal', 'direct', 'circle';
	};
	notiftype = lib.enum {
		'none', 'mention', 'like', 'rt', 'react'
	};

	relation = lib.set {
		'silence', -- messages will not be accepted
		'collapse', -- posts will be collapsed by default
		'disemvowel', -- posts will be ritually humiliated, but shown
		'avoid', -- posts will be kept out of the timeline but will show on users' posts and in conversations
................................................................................
		'mute', -- posts will be completely hidden at all times
		'block', -- no interactions will be permitted, but posts will remain visible
	};
	credset = lib.set {
		'pw', 'otp', 'challenge', 'trust'
	};
	privset = lib.set {
		'post', 'edit', 'account', 'upload', 'moderate', 'admin', 'invite'
	};
	powerset = lib.set {
		-- user powers -- default on
		'login', -- not locked out
		'visible', -- account & posts can be seen by others
		'post', -- can do poasts
		'shout', -- posts show up on local timeline
		'propagate', -- posts are sent to other instances
		'artifact', -- upload, claim, and manage artifacts
		'account', -- configure own account
		'edit'; -- edit own poasts
		'snitch'; -- can issue badthink reports

		-- admin powers -- default off
		'purge', -- permanently delete users
		'config', -- change daemon policy & config UI
		'censor', -- dispose of badthink
		'discipline', -- enforced timeouts, stripping badges and epithets, punitive actions that do not permanently deprive of powers; can remove own injunctions but not others'
		'vacate', -- can remove others' injunctions, but not apply them
		'cred', -- alter credentials
		'elevate', 'demote', -- change user rank, give and take powers, including the ability to log in
		'rebrand', -- modify site's brand identity
		'herald', -- grant serverwide epithets and badges
		'crier', -- can promote content to the instance page
		'invite' -- *unlimited* invites
	};
	prepmode = lib.enum {
		'full','conf','admin'
	}
}

................................................................................
	var pow: m.powerset pow:clear()
	(pow.login     << true)
	(pow.visible   << true)
	(pow.post      << true)
	(pow.shout     << true)
	(pow.propagate << true)
	(pow.artifact  << true)
	(pow.account   << true)
	(pow.edit      << true)
	return m.rights { rank = 0, quota = 1000, invites = 0, powers = pow; }
end

struct m.actor {
	id: uint64
	nym: str
................................................................................
	mentions: lib.mem.ptr(uint64)
	circles: lib.mem.ptr(uint64) --only meaningful if scope is set to circle
	convoheaduri: str
	parent: uint64
-- ephemera
	localpost: bool
	accent: int16
	rts: uint32
	likes: uint32
	rtdby: uint64 -- 0 if not rt
	rtact: uint64 -- 0 if not rt, id of rt action otherwise
	source: &m.source

	-- save :: bool -> {} (defined in acl.t due to dep. hell)
}

m.user_conf_funcs = function(be,n,ty,rty,rty2)
	rty = rty or ty
................................................................................
			-- origin: inet
			-- cookie issue time: m.timepoint
	actor_auth_register_uid: {&m.source, uint64, uint64} -> {}
		-- notifies the backend module of the UID that has been assigned for
		-- an authentication ID
			-- aid: uint64
			-- uid: uint64
	actor_notifs_fetch: {&m.source, uint64} -> lib.mem.lstptr(m.notif)

	auth_enum_uid:    {&m.source, uint64}    -> lib.mem.lstptr(m.auth)
	auth_enum_handle: {&m.source, rawstring} -> lib.mem.lstptr(m.auth)
	auth_attach_pw: {&m.source, uint64, bool, pstr, pstr} -> {}
		-- uid: uint64
		-- reset: bool (delete other passwords?)
		-- pw: pstring
................................................................................
	post_enum_author_uid: {&m.source, uint64, m.range} -> lib.mem.lstptr(m.post)
	post_enum_parent: {&m.source, uint64} -> lib.mem.lstptr(m.post)
	post_attach_ctl: {&m.source, uint64, uint64, bool} -> {}
		-- attaches or detaches an existing database artifact
			-- post id: uint64
			-- artifact id: uint64
			-- detach: bool
	post_retweet: {&m.source, uint64, uint64, bool} -> {}
	post_like: {&m.source, uint64, uint64, bool} -> {}
			-- undo: bool
	post_react: {&m.source, uint64, uint64, pstring} -> {}
			-- emoji: pstring (null to delete previous reaction, otherwise adds/changes)
	post_liked_uid: {&m.source, uint64, uint64} -> bool
	post_reacted_uid: {&m.source, uint64, uint64} -> bool

	thread_latest_arrival_calc: {&m.source, uint64} -> m.timepoint

	artifact_instantiate: {&m.source, lib.mem.ptr(uint8), lib.mem.ptr(int8)} -> uint64
		-- instantiate an artifact in the database, either installing a new
		-- artifact or returning the id of an existing artifact with the same hash
			-- artifact: bytea

Modified str.t from [1e93ad8eb5] to [638f6c2759].

178
179
180
181
182
183
184













185
186
187
188
189
190
191
		self.buf = [rawstring](lib.mem.heapr_raw(self.buf, self.space))
	end
	lib.mem.cpy(self.buf + self.sz, str, len)
	self.sz = self.sz + len
	self.buf[self.sz] = 0
	return self
end;














m.lit = macro(function(str)
	if str:asvalue() ~= nil then
		return `[lib.mem.ref(int8)] {ptr = [str:asvalue()], ct = [#(str:asvalue())]}
	else
		return `[lib.mem.ref(int8)] {ptr = nil, ct = 0}
	end







>
>
>
>
>
>
>
>
>
>
>
>
>







178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
		self.buf = [rawstring](lib.mem.heapr_raw(self.buf, self.space))
	end
	lib.mem.cpy(self.buf + self.sz, str, len)
	self.sz = self.sz + len
	self.buf[self.sz] = 0
	return self
end;

terra m.acc:ipush(i: intptr)
	var decbuf: int8[21]
	var si = lib.math.decstr_friendly(i, &decbuf[20])
	var len: intptr = [decbuf.type.N] - (si - &decbuf[0])
	return self:push(si,len)
end

terra m.acc:shpush(i: uint64)
	var sbuf: int8[lib.math.shorthand.maxlen]
	var len = lib.math.shorthand.gen(i,&sbuf[0])
	return self:push(&sbuf[0], len)
end

m.lit = macro(function(str)
	if str:asvalue() ~= nil then
		return `[lib.mem.ref(int8)] {ptr = [str:asvalue()], ct = [#(str:asvalue())]}
	else
		return `[lib.mem.ref(int8)] {ptr = nil, ct = 0}
	end

Modified view/confirm.tpl from [3b921f59eb] to [0d2952df9c].

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

|







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

Modified view/tweet.tpl from [e117899db4] to [aefe28dd4e].

1
2
3
4
5
6
7

8
9
<div class="post"@attr>
	<div class="avatar"><img src="@:avatar"></div>
	<a class="username" href="/@:acctlink">@nym</a>
	<div class="content">
		<div class="subject">@!subject</div>
		<div class="text">@text</div>
	</div>

	<a class="permalink" href="@permalink">@when</a>
</div>







>


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