parsav  Check-in [d228cd7fcb]

Overview
Comment:vastly improve the setup process
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: d228cd7fcb7c10b982f8c2129c50644044573c531e939c97096757c38a4520ae
User & Date: lexi on 2020-12-28 23:42:22
Other Links: manifest | tags
Context
2020-12-29
00:57
add privilege control verbs check-in: a64461061f user: lexi tags: trunk
2020-12-28
23:42
vastly improve the setup process check-in: d228cd7fcb user: lexi tags: trunk
2020-12-27
04:08
look ma, im tweetin check-in: 8f954221a1 user: lexi tags: trunk
Changes

Modified backend/pgsql.t from [2c2a215381] to [0fdc39456b].

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
..
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
..
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
...
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
...
190
191
192
193
194
195
196


















197
198
199
200
201
202
203
...
339
340
341
342
343
344
345







346


347
348
349
350
351
352
353
354
355
356
357
358
359
360

361
362
363
364
365
366
367
...
389
390
391
392
393
394
395


396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
...
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
...
530
531
532
533
534
535
536



537
538
539
540
541
542
543
...
556
557
558
559
560
561
562
563
564
565

566



































567
568
569
570
571
572
573
...
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
...
759
760
761
762
763
764
765
766
767
768



















769
770















771

772
773
774
		params = {rawstring}, sql = [[
			select value from parsav_config
				where key = $1::text limit 1
		]];
	};

	conf_set = {
		params = {rawstring,rawstring}, sql = [[
			insert into parsav_config (key, value)
				values ($1::text, $2::text)
				on conflict (key) do update set value = $2::text
		]];
	};

	conf_reset = {
		params = {rawstring}, sql = [[
			delete from parsav_config where
				key = $1::text 
		]];
	};

	actor_fetch_uid = {
		params = {uint64}, sql = [[
			select a.id, a.nym, a.handle, a.origin, a.bio,
			       a.avataruri, a.rank, a.quota, a.key, 
			       extract(epoch from a.knownsince)::bigint,
				   coalesce(a.handle || '@' || s.domain,
				            '@' || a.handle) as xid

			from      parsav_actors  as a
			left join parsav_servers as s
				on a.origin = s.id
................................................................................
			where a.id = $1::bigint
		]];
	};

	actor_fetch_xid = {
		params = {pstring}, sql = [[
			select a.id, a.nym, a.handle, a.origin, a.bio,
			       a.avataruri, a.rank, a.quota, a.key, 
			       extract(epoch from a.knownsince)::bigint,
				   coalesce(a.handle || '@' || s.domain,
				            '@' || a.handle) as xid,

				coalesce(s.domain,
				        (select value from parsav_config
							where key='domain' limit 1)) as domain
................................................................................
			rawstring, uint16, uint32
		};
		sql = [[
			insert into parsav_actors (
				nym,handle,
				origin,knownsince,
				bio,avataruri,key,
				title,rank,quota
			) values ($1::text, $2::text,
				case when $3::bigint = 0 then null
				     else $3::bigint end,
				to_timestamp($4::bigint),
				$5::bigint, $6::bigint, $7::bytea,
				$8::text, $9::smallint, $10::integer
			) returning id
................................................................................
			order by blacklist desc limit 1
		]];
	};

	actor_enum_local = {
		params = {}, sql = [[
			select id, nym, handle, origin, bio,
			       null::text, rank, quota, key,
			       extract(epoch from knownsince)::bigint,
				handle ||'@'||
				(select value from parsav_config
					where key='domain' limit 1) as xid
			from parsav_actors where origin is null
		]];
	};

	actor_enum = {
		params = {}, sql = [[
			select a.id, a.nym, a.handle, a.origin, a.bio,
			       a.avataruri, a.rank, a.quota, a.key,
			       extract(epoch from a.knownsince)::bigint,
				   coalesce(a.handle || '@' || s.domain,
				            '@' || a.handle) as xid
			from parsav_actors a
			left join parsav_servers s on s.id = a.origin
		]];
	};
................................................................................
				(select count(*) from mts where kind = 'trust') > 0
		]]; -- cheat
	};

	actor_session_fetch = {
		params = {uint64, lib.store.inet}, sql = [[
			select a.id, a.nym, a.handle, a.origin, a.bio,
			       a.avataruri, a.rank, a.quota, a.key,
			       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,
................................................................................
	};

	actor_powers_fetch = {
		params = {uint64}, sql = [[
			select key, allow from parsav_rights where actor = $1::bigint
		]]
	};



















	post_create = {
		params = {uint64, rawstring, rawstring, rawstring}, sql = [[
			insert into parsav_posts (
				author, subject, acl, body,
				posted, discovered,
				circles, mentions
................................................................................
			return buf
		end
	end;
}

local con = symbol(&lib.pq.PGconn)
local prep = {}







local sqlsquash = function(s) return s:gsub('%s+',' '):gsub('^%s*(.-)%s*$','%1') end


for k,q in pairs(queries) do
	local qt = sqlsquash(q.sql)
	local stmt = 'parsavpg_' .. k
	prep[#prep + 1] = quote
		var res = lib.pq.PQprepare([con], stmt, qt, [#q.params], nil)
		defer lib.pq.PQclear(res)
		if res == nil or lib.pq.PQresultStatus(res) ~= lib.pq.PGRES_COMMAND_OK then
			if res == nil then
				lib.bail('grievous error occurred preparing ',k,' statement')
			end
			lib.bail('could not prepare PGSQL statement ',k,': ',lib.pq.PQresultErrorMessage(res))
		end
		lib.dbg('prepared PGSQL statement ',k) 
	end


	local args, casts, counters, fixers, ft, yield = {}, {}, {}, {}, {}, {}
	local dumpers = {}
	for i, ty in ipairs(q.params) do
		args[i] = symbol(ty)
		ft[i] = `1
		if ty == rawstring then
................................................................................
			dumpers[#dumpers+1] = `lib.io.fmt([tostring(i)..'. got int %llu\n'], [args[i]])
			fixers[#fixers + 1] = quote
				[args[i]] = lib.math.netswap(ty, [args[i]])
			end
		end
	end



	terra q.exec(src: &lib.store.source, [args])
		var params = arrayof([&int8], [casts])
		var params_sz = arrayof(int, [counters])
		var params_ft = arrayof(int, [ft])
		[fixers]
		--[dumpers]
		var res = lib.pq.PQexecPrepared([&lib.pq.PGconn](src.handle), stmt,
			[#args], params, params_sz, params_ft, 1)
		if res == nil then
			lib.bail(['grievous error occurred executing '..k..' against database'])
		elseif lib.pq.PQresultStatus(res) ~= lib.pq.PGRES_TUPLES_OK then
			lib.bail(['PGSQL database procedure '..k..' failed\n'],
			lib.pq.PQresultErrorMessage(res))
		end

		var ct = lib.pq.PQntuples(res)
		if ct == 0 then
			lib.pq.PQclear(res)
................................................................................
	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
	var nym: rawstring, nymlen: intptr
	var bio: rawstring, biolen: intptr

	if r:null(row,5) then avlen = 0 av = nil else
		av = r:string(row,5)
		avlen = r:len(row,5)+1
	end
	if r:null(row,1) then nymlen = 0 nym = nil else
		nym = r:string(row,1)
		nymlen = r:len(row,1)+1
	end
	if r:null(row,4) then biolen = 0 bio = nil else
		bio = r:string(row,4)
		biolen = r:len(row,4)+1




	end
	a = [ lib.str.encapsulate(lib.store.actor, {
		nym = {`nym, `nymlen};
		bio = {`bio, `biolen};

		avatar = {`av,`avlen};
		handle = {`r:string(row, 2); `r:len(row,2) + 1};
		xid = {`r:string(row, 10); `r:len(row,10) + 1};
	}) ]
	a.ptr.id = r:int(uint64, row, 0);
	a.ptr.rights = lib.store.rights_default();
	a.ptr.rights.rank = r:int(uint16, row, 6);
	a.ptr.rights.quota = r:int(uint32, row, 7);
	a.ptr.knownsince = r:int(int64,row, 9);
	if r:null(row,8) then
		a.ptr.key.ct = 0 a.ptr.key.ptr = nil
	else
		a.ptr.key = r:bin(row,8)
	end
	if r:null(row,3) then a.ptr.origin = 0
	else a.ptr.origin = r:int(uint64,row,3) end
................................................................................
		lib.dbg(['searching for hashed password credentials in format SHA' .. tostring(hash)])
		var [out]
		[vdrs]
		lib.dbg(['could not find password hash'])
	end
end




local b = `lib.store.backend {
	id = "pgsql";
	open = [terra(src: &lib.store.source): &opaque
		lib.report('connecting to postgres database: ', src.string.ptr)
		var [con] = lib.pq.PQconnectdb(src.string.ptr)
		if lib.pq.PQstatus(con) ~= lib.pq.CONNECTION_OK then
			lib.warn('postgres backend connection failed')
................................................................................
		defer lib.pq.PQclear(res)
		if lib.pq.PQresultStatus(res) ~= lib.pq.PGRES_TUPLES_OK then
			lib.warn('failed to secure postgres connection')
			lib.pq.PQfinish(con)
			return nil
		end

		[prep]
		return con
	end];

	close = [terra(src: &lib.store.source) lib.pq.PQfinish([&lib.pq.PGconn](src.handle)) end];




































	conf_get = [terra(src: &lib.store.source, key: rawstring)
		var r = queries.conf_get.exec(src, key)
		if r.sz == 0 then return [lib.mem.ptr(int8)] { ptr = nil, ct = 0 } else
			defer r:free()
			return r:String(0,0)
		end
................................................................................

			var a = row_to_actor(&r, 0)
			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,12) then -- restricted?
				au.val.privs:clear()
				(au.val.privs.post   << r:bool(0,13)) 
				(au.val.privs.edit   << r:bool(0,14))
				(au.val.privs.acct   << r:bool(0,15))
				(au.val.privs.upload << r:bool(0,16))
				(au.val.privs.censor << r:bool(0,17))
				(au.val.privs.admin  << r:bool(0,18))
			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 }
................................................................................
		return powers
	end];

	actor_create = [terra(
		src: &lib.store.source,
		ac: &lib.store.actor
	): uint64
		var r = queries.actor_create.exec(src,ac.nym, ac.handle, ac.origin, ac.knownsince, ac.bio, ac.avatar, ac.key, ac.title, ac.rights.rank, ac.rights.quota)
		if r.sz == 0 then lib.bail('failed to create actor!') end
		return r:int(uint64,0,0)



















	end];
















	actor_auth_register_uid = nil; -- not necessary for view-based auth

}

return b







|







|








|







 







|







 







|







 







|











|







 







|







 







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







 







>
>
>
>
>
>
>
|
>
>



|










>







 







>
>










|







 







>











>
>
>
>




>


|





|







 







>
>
>







 







<


>

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







 







|

|
|
|
|
|
|







 







|

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


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

>



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
..
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
..
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
...
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
...
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
...
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
...
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
...
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
...
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
...
595
596
597
598
599
600
601

602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
...
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
...
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
		params = {rawstring}, sql = [[
			select value from parsav_config
				where key = $1::text limit 1
		]];
	};

	conf_set = {
		params = {rawstring,rawstring}, cmd=true, sql = [[
			insert into parsav_config (key, value)
				values ($1::text, $2::text)
				on conflict (key) do update set value = $2::text
		]];
	};

	conf_reset = {
		params = {rawstring}, cmd=true, sql = [[
			delete from parsav_config where
				key = $1::text 
		]];
	};

	actor_fetch_uid = {
		params = {uint64}, sql = [[
			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

			from      parsav_actors  as a
			left join parsav_servers as s
				on a.origin = s.id
................................................................................
			where a.id = $1::bigint
		]];
	};

	actor_fetch_xid = {
		params = {pstring}, sql = [[
			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,

				coalesce(s.domain,
				        (select value from parsav_config
							where key='domain' limit 1)) as domain
................................................................................
			rawstring, uint16, uint32
		};
		sql = [[
			insert into parsav_actors (
				nym,handle,
				origin,knownsince,
				bio,avataruri,key,
				epithet,rank,quota
			) values ($1::text, $2::text,
				case when $3::bigint = 0 then null
				     else $3::bigint end,
				to_timestamp($4::bigint),
				$5::bigint, $6::bigint, $7::bytea,
				$8::text, $9::smallint, $10::integer
			) returning id
................................................................................
			order by blacklist desc limit 1
		]];
	};

	actor_enum_local = {
		params = {}, sql = [[
			select id, nym, handle, origin, bio,
			       null::text, rank, quota, key, epithet,
			       extract(epoch from knownsince)::bigint,
				handle ||'@'||
				(select value from parsav_config
					where key='domain' limit 1) as xid
			from parsav_actors where origin is null
		]];
	};

	actor_enum = {
		params = {}, sql = [[
			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
			from parsav_actors a
			left join parsav_servers s on s.id = a.origin
		]];
	};
................................................................................
				(select count(*) from mts where kind = 'trust') > 0
		]]; -- cheat
	};

	actor_session_fetch = {
		params = {uint64, lib.store.inet}, sql = [[
			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,
................................................................................
	};

	actor_powers_fetch = {
		params = {uint64}, sql = [[
			select key, allow from parsav_rights where actor = $1::bigint
		]]
	};

	actor_power_insert = {
		params = {uint64,lib.mem.ptr(int8),uint16}, cmd = true, sql = [[
			insert into parsav_rights (actor, key, allow) values (
				$1::bigint, $2::text, ($3::smallint)::integer::bool
			)
		]]
	};

	auth_create_pw = {
		params = {uint64, lib.mem.ptr(uint8)}, cmd = true, sql = [[
			insert into parsav_auth (uid, name, kind, cred) values (
				$1::bigint,
				(select handle from parsav_actors where id = $1::bigint),
				'pw-sha256', $2::bytea
			)
		]]
	};

	post_create = {
		params = {uint64, rawstring, rawstring, rawstring}, sql = [[
			insert into parsav_posts (
				author, subject, acl, body,
				posted, discovered,
				circles, mentions
................................................................................
			return buf
		end
	end;
}

local con = symbol(&lib.pq.PGconn)
local prep = {}
local function sqlsquash(s) return s
	:gsub('%%include (.-)%%',function(f)
		return sqlsquash(lib.util.ingest('backend/schema/' .. f))
	end) -- include dependencies
	:gsub('%-%-.-\n','') -- remove disruptive line comments
	:gsub('%-%-.-$','') -- remove unnecessary terminal comments
	:gsub('%s+',' ') -- remove whitespace
	:gsub('^%s*(.-)%s*$','%1') -- chomp
end

for k,q in pairs(queries) do
	local qt = sqlsquash(q.sql)
	local stmt = 'parsavpg_' .. k
	terra q.prep([con])
		var res = lib.pq.PQprepare([con], stmt, qt, [#q.params], nil)
		defer lib.pq.PQclear(res)
		if res == nil or lib.pq.PQresultStatus(res) ~= lib.pq.PGRES_COMMAND_OK then
			if res == nil then
				lib.bail('grievous error occurred preparing ',k,' statement')
			end
			lib.bail('could not prepare PGSQL statement ',k,': ',lib.pq.PQresultErrorMessage(res))
		end
		lib.dbg('prepared PGSQL statement ',k) 
	end
	prep[#prep + 1] = quote q.prep([con]) end

	local args, casts, counters, fixers, ft, yield = {}, {}, {}, {}, {}, {}
	local dumpers = {}
	for i, ty in ipairs(q.params) do
		args[i] = symbol(ty)
		ft[i] = `1
		if ty == rawstring then
................................................................................
			dumpers[#dumpers+1] = `lib.io.fmt([tostring(i)..'. got int %llu\n'], [args[i]])
			fixers[#fixers + 1] = quote
				[args[i]] = lib.math.netswap(ty, [args[i]])
			end
		end
	end

	local okconst = lib.pq.PGRES_TUPLES_OK
	if q.cmd then okconst = lib.pq.PGRES_COMMAND_OK end
	terra q.exec(src: &lib.store.source, [args])
		var params = arrayof([&int8], [casts])
		var params_sz = arrayof(int, [counters])
		var params_ft = arrayof(int, [ft])
		[fixers]
		--[dumpers]
		var res = lib.pq.PQexecPrepared([&lib.pq.PGconn](src.handle), stmt,
			[#args], params, params_sz, params_ft, 1)
		if res == nil then
			lib.bail(['grievous error occurred executing '..k..' against database'])
		elseif lib.pq.PQresultStatus(res) ~= okconst then
			lib.bail(['PGSQL database procedure '..k..' failed\n'],
			lib.pq.PQresultErrorMessage(res))
		end

		var ct = lib.pq.PQntuples(res)
		if ct == 0 then
			lib.pq.PQclear(res)
................................................................................
	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
	var nym: rawstring, nymlen: intptr
	var bio: rawstring, biolen: intptr
	var epi: rawstring, epilen: intptr
	if r:null(row,5) then avlen = 0 av = nil else
		av = r:string(row,5)
		avlen = r:len(row,5)+1
	end
	if r:null(row,1) then nymlen = 0 nym = nil else
		nym = r:string(row,1)
		nymlen = r:len(row,1)+1
	end
	if r:null(row,4) then biolen = 0 bio = nil else
		bio = r:string(row,4)
		biolen = r:len(row,4)+1
	end
	if r:null(row,9) then epilen = 0 epi = nil else
		epi = r:string(row,9)
		epilen = r:len(row,9)+1
	end
	a = [ lib.str.encapsulate(lib.store.actor, {
		nym = {`nym, `nymlen};
		bio = {`bio, `biolen};
		epithet = {`epi, `epilen};
		avatar = {`av,`avlen};
		handle = {`r:string(row, 2); `r:len(row,2) + 1};
		xid = {`r:string(row, 11); `r:len(row,11) + 1};
	}) ]
	a.ptr.id = r:int(uint64, row, 0);
	a.ptr.rights = lib.store.rights_default();
	a.ptr.rights.rank = r:int(uint16, row, 6);
	a.ptr.rights.quota = r:int(uint32, row, 7);
	a.ptr.knownsince = r:int(int64,row, 10);
	if r:null(row,8) then
		a.ptr.key.ct = 0 a.ptr.key.ptr = nil
	else
		a.ptr.key = r:bin(row,8)
	end
	if r:null(row,3) then a.ptr.origin = 0
	else a.ptr.origin = r:int(uint64,row,3) end
................................................................................
		lib.dbg(['searching for hashed password credentials in format SHA' .. tostring(hash)])
		var [out]
		[vdrs]
		lib.dbg(['could not find password hash'])
	end
end

local schema = sqlsquash(lib.util.ingest('backend/schema/pgsql.sql'))
local obliterator = sqlsquash(lib.util.ingest('backend/schema/pgsql-drop.sql'))

local b = `lib.store.backend {
	id = "pgsql";
	open = [terra(src: &lib.store.source): &opaque
		lib.report('connecting to postgres database: ', src.string.ptr)
		var [con] = lib.pq.PQconnectdb(src.string.ptr)
		if lib.pq.PQstatus(con) ~= lib.pq.CONNECTION_OK then
			lib.warn('postgres backend connection failed')
................................................................................
		defer lib.pq.PQclear(res)
		if lib.pq.PQresultStatus(res) ~= lib.pq.PGRES_TUPLES_OK then
			lib.warn('failed to secure postgres connection')
			lib.pq.PQfinish(con)
			return nil
		end


		return con
	end];

	close = [terra(src: &lib.store.source) lib.pq.PQfinish([&lib.pq.PGconn](src.handle)) end];

	conprep = [terra(src: &lib.store.source, mode: lib.store.prepmode.t)
		var [con] = [&lib.pq.PGconn](src.handle)
		if mode == lib.store.prepmode.full then [prep]
		elseif mode == lib.store.prepmode.conf or
		       mode == lib.store.prepmode.admin then 
			queries.conf_get.prep(con)
			queries.conf_set.prep(con)
			queries.conf_reset.prep(con)
			if mode == lib.store.prepmode.admin then 
			end
		else lib.bail('unsupported connection preparation mode') end
	end];

	dbsetup = [terra(src: &lib.store.source)
		var res = lib.pq.PQexec([&lib.pq.PGconn](src.handle), schema)
		if lib.pq.PQresultStatus(res) == lib.pq.PGRES_COMMAND_OK then
			lib.report('successfully instantiated schema in database')
			return true
		else
			lib.warn('backend pgsql - failed to initialize database: \n', lib.pq.PQresultErrorMessage(res))
			return false
		end
	end];

	obliterate_everything = [terra(src: &lib.store.source)
		var res = lib.pq.PQexec([&lib.pq.PGconn](src.handle), obliterator)
		if lib.pq.PQresultStatus(res) == lib.pq.PGRES_COMMAND_OK then
			lib.report('successfully wiped out everything parsav-related in database')
			return true
		else
			lib.warn('backend pgsql - failed to obliterate database: \n', lib.pq.PQresultErrorMessage(res))
			return false
		end
	end];

	conf_get = [terra(src: &lib.store.source, key: rawstring)
		var r = queries.conf_get.exec(src, key)
		if r.sz == 0 then return [lib.mem.ptr(int8)] { ptr = nil, ct = 0 } else
			defer r:free()
			return r:String(0,0)
		end
................................................................................

			var a = row_to_actor(&r, 0)
			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,13) then -- restricted?
				au.val.privs:clear()
				(au.val.privs.post   << r:bool(0,14)) 
				(au.val.privs.edit   << r:bool(0,15))
				(au.val.privs.acct   << r:bool(0,16))
				(au.val.privs.upload << r:bool(0,17))
				(au.val.privs.censor << r:bool(0,18))
				(au.val.privs.admin  << r:bool(0,19))
			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 }
................................................................................
		return powers
	end];

	actor_create = [terra(
		src: &lib.store.source,
		ac: &lib.store.actor
	): uint64
		var r = queries.actor_create.exec(src,ac.nym, ac.handle, ac.origin, ac.knownsince, ac.bio, ac.avatar, ac.key, ac.epithet, ac.rights.rank, ac.rights.quota)
		if r.sz == 0 then lib.bail('failed to create actor!') end
		var uid = r:int(uint64,0,0)

		-- check against default rights, insert records for wherever powers differ
		lib.dbg('created new actor, establishing powers')
		var pdef = lib.store.rights_default().powers
		var map = array([privmap])
		for i=0, [map.type.N] do
			var d = pdef and map[i].priv
			var u = ac.rights.powers and map[i].priv
			if d:sz() > 0 and u:sz() == 0 then
				lib.dbg('blocking power ', {map[i].name.ptr, map[i].name.ct})
				queries.actor_power_insert.exec(src, uid, map[i].name, 0)
			elseif d:sz() == 0 and u:sz() > 0 then
				lib.dbg('granting power ', {map[i].name.ptr, map[i].name.ct})
				queries.actor_power_insert.exec(src, uid, map[i].name, 1)
			end
		end

		lib.dbg('powers established')
		return uid
	end];

	auth_create_pw = [terra(
		src: &lib.store.source,
		uid: uint64,
		reset: bool,
		pw: lib.mem.ptr(int8)
	): {}
		-- TODO impl reset support
		var hash: uint8[lib.crypt.algsz.sha256]
		if lib.md.mbedtls_md(lib.md.mbedtls_md_info_from_type(lib.crypt.alg.sha256.id),
			[&uint8](pw.ptr), pw.ct, &hash[0]) ~= 0 then
			lib.bail('cannot hash password')
		end
		queries.auth_create_pw.exec(src, uid, [lib.mem.ptr(uint8)] {ptr = &hash[0], ct = [hash.type.N]})
	end];

	actor_auth_register_uid = nil; -- not necessary for view-based auth

}

return b

Added backend/schema/pgsql-auth.sql version [1170b3857b].







































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
-- in managed-auth configurations, parsav_auth is a table which is directly
-- controlled by the parsav daemon and utilities themselves. in unmanaged
-- configuration, you will need to create your own view with the same fields
-- as this table
create table parsav_auth (
	aid bigint primary key default (1+random()*(2^63-1))::bigint,
		-- the AID is the value that links a session to its credentials,
		-- so the aid needs to be stable over time. if you don't have a
		-- convenient field to rely on in your own datasets, the best
		-- approach is to use digest(str,'sha256') from the pgcrypto
		-- extension to create a value that depends on the values of
		-- kind, cred, and a unique user ID from your own dataset (NOT
		-- uid, as the UID associated with a session will change when
		-- a user logs in for the first time).

	uid bigint,
		-- the UID links a credential set to an actor in the parsav
		-- database. if it is equal to 0 (but not null) a new actor
		-- will be created and associated with the authentication
		-- records bearing its name when that user first logs in 

	name text,
		-- this is the handle of the actor that will be created when
		-- a user first logs in with this as the username and one of
		-- its associated credentials. the field is otherwise unused.

	kind text not null, -- see parsav.md
	cred bytea,
	restrict text[],
		-- per-credential restrictions can be levelled, for instance
		-- to prevent a certain API key from being used to post tweets
		-- as that user, while allowing it to be used to collect data.
		-- if restrict is null, no restrictions will be applied.
		-- otherwise, it should be an array of privileges that will be
		-- permitted when authenticated via this credential.

	netmask cidr,
		-- if not null, the credential will only be valid when logging
		-- in from an IP address contained by this netmask.

	blacklist bool not null default false,
		-- if the credential matches, access will be denied, even if
		-- non-blacklisted credentials match. most useful with
		-- uid = null, kind = trust, cidr = (untrusted IP range)

	valperiod timestamp default now(),
		-- cookies bearing timestamps earlier than this point in time
		-- will be considered invalid and will not grant access

	unique(name,kind,cred)
);

Added backend/schema/pgsql-drop.sql version [e1fb43be2e].





































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- destroy absolutely everything

drop table if exists parsav_config cascade;
drop table if exists parsav_servers cascade;
drop table if exists parsav_actors cascade;
drop table if exists parsav_rights cascade;
drop table if exists parsav_posts cascade;
drop table if exists parsav_conversations cascade;
drop table if exists parsav_rels cascade;
drop table if exists parsav_acts cascade;
drop table if exists parsav_log cascade;
drop table if exists parsav_attach cascade;
drop table if exists parsav_circles cascade;
drop table if exists parsav_rooms cascade;
drop table if exists parsav_room_members cascade;
drop table if exists parsav_invites cascade;
drop table if exists parsav_interventions cascade;
drop table if exists parsav_auth cascade;

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

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
..
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


\prompt 'domain name: ' domain
\prompt 'instance name: ' inst
\prompt 'bind to socket: ' bind
\qecho 'how locked down should this server be? public = anyone can see public timeline and tweets, private = anyone can see tweets with a link but login required for everything else, lockdown = login required for all activities, isolate = like lockdown but with federation protocols completely disabled'
\prompt 'security mode: ' secmode
\qecho 'should user self-registration be allowed? yes or no'
\prompt 'registration: ' regpol
\qecho 'by default, parsav tracks rights on its own. you can override this later by replacing the rights table with a view, but you''ll then need to set appropriate rules on the view to allow administrators to modify rights from the web UI, or set the rights-readonly flag in the config table to true. for now, enter the name of an actor who will be granted full rights when she logs in and identified as the server owner.'
\prompt 'master actor: ' admin
\qecho 'you will need to create an authentication view named parsav_auth mapping your user database to something parsav can understand; see auth.sql for an example.'

begin;

drop table if exists parsav_config;
create table if not exists parsav_config (
	key   text primary key,
	value text
);

insert into parsav_config (key,value) values

	('bind',:'bind'),
	('domain',:'domain'),
	('instance-name',:'inst'),
	('policy-security',:'secmode'),
	('policy-self-register',:'regpol'),
	('master',:'admin'),
	('server-secret', encode(
			digest(int8send((2^63 * (random()*2 - 1))::bigint),
		'sha512'), 'base64'));

-- note that valid ids should always > 0, as 0 is reserved for null
-- on the client side, vastly simplifying code
drop table if exists parsav_servers cascade;
create table parsav_servers (
	id     bigint primary key default (1+random()*(2^63-1))::bigint,
	domain text not null,
	key    bytea,
	knownsince timestamp,
	parsav boolean -- whether to use parsav protocol extensions
);

drop table if exists parsav_actors cascade;
create table parsav_actors (
	id        bigint primary key default (1+random()*(2^63-1))::bigint,
	nym       text,
	handle    text not null, -- nym [@handle@origin] 
	origin    bigint references parsav_servers(id)
		on delete cascade, -- null origin = local actor
	knownsince timestamp,
	bio       text,
	avataruri text, -- null if local
	rank      smallint not null default 0,
	quota     integer not null default 1000,
	key       bytea, -- private if localactor; public if remote
	title     text,

	
	unique (handle,origin)
);

drop table if exists parsav_rights cascade;
create table parsav_rights (
	key text,
	actor bigint references parsav_actors(id)
		on delete cascade,
	allow boolean not null,
	scope bigint, -- for future expansion

	primary key (key,actor)
);

insert into parsav_actors (handle,rank,quota) values (:'admin',1,0);
insert into parsav_rights (actor,key,allow)
	select (select id from parsav_actors where handle=:'admin'), a.column1, a.column2 from (values
		('purge',true),
		('config',true),
		('censor',true),
		('suspend',true),
		('cred',true),
		('elevate',true),
		('demote',true),
		('rebrand',true)
	) as a;

drop table if exists parsav_posts cascade;
create table parsav_posts (
	id         bigint primary key default (1+random()*(2^63-1))::bigint,
	author     bigint references parsav_actors(id)
		on delete cascade,
	subject    text,
	acl        text not null default 'all', -- just store the script raw 🤷
	body       text,
................................................................................

	convoheaduri text
	-- only used for tracking foreign conversations and tying them to post heads;
	-- local conversations are tracked directly and mapped to URIs based on the
	-- head's ID. null if native tweet or not the first tweet in convo
);

drop table if exists parsav_conversations cascade;

drop table if exists parsav_rels cascade;
create table parsav_rels (
	relator bigint references parsav_actors(id)
		on delete cascade, -- e.g. follower
	relatee bigint references parsav_actors(id)
		on delete cascade, -- e.g. followed
	kind    smallint, -- e.g. follow, block, mute

	primary key (relator, relatee, kind)
);

drop table if exists parsav_acts cascade;
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
);

drop table if exists parsav_log cascade;
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)
		on delete cascade,
	post  bigint not null
);

drop table if exists parsav_attach cascade;
create table parsav_attach (
	id          bigint primary key default (1+random()*(2^63-1))::bigint,
	birth       timestamp not null default now(),
	content     bytea not null,
	mime        text, -- null if unknown, will be reported as x-octet-stream
	description text,
	parent      bigint -- post id, or userid for avatars
);

drop table if exists parsav_circles cascade;
create table parsav_circles (
	id          bigint primary key default (1+random()*(2^63-1))::bigint,
	owner       bigint not null references parsav_actors(id),
	name        text not null,
	members     bigint[] not null default array[]::bigint[],

	unique (owner,name)
);

drop table if exists parsav_rooms cascade;
create table parsav_rooms (
	id          bigint primary key default (1+random()*(2^63-1))::bigint,
	origin		bigint references parsav_servers(id),
	name		text not null,
	description text not null,
	policy      smallint not null
);

drop table if exists parsav_room_members cascade;
create table parsav_room_members (
	room   bigint references parsav_rooms(id),
	member bigint references parsav_actors(id),
	rank   smallint not null default 0,
	admin  boolean not null default false, -- non-admins with rank can only moderate + invite
	title  text, -- admin-granted title like reddit flair
	vouchedby bigint references parsav_actors(id)
);

drop table if exists parsav_invites cascade;
create table parsav_invites (
	id          bigint primary key default (1+random()*(2^63-1))::bigint,
	-- when a user is created from an invite, the invite is deleted and the invite
	-- ID becomes the user ID. privileges granted on the invite ID during the invite
	-- process are thus inherited by the user
	issuer bigint references parsav_actors(id),
	handle text, -- admin can lock invite to specific handle
	rank   smallint not null default 0,
	quota  integer not null  default 1000
);

drop table if exists parsav_interventions cascade;
create table parsav_interventions (
	id     bigint primary key default (1+random()*(2^63-1))::bigint,
	issuer bigint references parsav_actors(id) not null,
	scope  bigint, -- can be null or room for local actions
	nature smallint not null, -- silence, suspend, disemvowel, etc
	victim bigint not null, -- could potentially target group as well
	expire timestamp -- auto-expires if set
);

end;


<
<
<
<
<
<
<
<
<
<
<
<
<
<
|




|
>
|
|
|
|
|
|
<
<
<



<








<












|
>




<










<
<
<
<
<
<
<
<
<
<
<
<
<
<







 







<
<
<










<









<









<









<









<








<









<











<









|
>
>














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
..
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














create table parsav_config (
	key   text primary key,
	value text
);

insert into parsav_config (key,value) values ('schema-version','1'),
	('credential-store','managed');
--	('bind',:'bind'),
--	('domain',:'domain'),
--	('instance-name',:'inst'),
--	('policy-security',:'secmode'),
--	('policy-self-register',:'regpol'),
--	('master',:'admin'),




-- note that valid ids should always > 0, as 0 is reserved for null
-- on the client side, vastly simplifying code

create table parsav_servers (
	id     bigint primary key default (1+random()*(2^63-1))::bigint,
	domain text not null,
	key    bytea,
	knownsince timestamp,
	parsav boolean -- whether to use parsav protocol extensions
);


create table parsav_actors (
	id        bigint primary key default (1+random()*(2^63-1))::bigint,
	nym       text,
	handle    text not null, -- nym [@handle@origin] 
	origin    bigint references parsav_servers(id)
		on delete cascade, -- null origin = local actor
	knownsince timestamp,
	bio       text,
	avataruri text, -- null if local
	rank      smallint not null default 0,
	quota     integer not null default 1000,
	key       bytea, -- private if localactor; public if remote
	epithet   text,
	authtime  timestamp not null default now(), -- cookies earlier than this timepoint will not be accepted
	
	unique (handle,origin)
);


create table parsav_rights (
	key text,
	actor bigint references parsav_actors(id)
		on delete cascade,
	allow boolean not null,
	scope bigint, -- for future expansion

	primary key (key,actor)
);















create table parsav_posts (
	id         bigint primary key default (1+random()*(2^63-1))::bigint,
	author     bigint references parsav_actors(id)
		on delete cascade,
	subject    text,
	acl        text not null default 'all', -- just store the script raw 🤷
	body       text,
................................................................................

	convoheaduri text
	-- only used for tracking foreign conversations and tying them to post heads;
	-- local conversations are tracked directly and mapped to URIs based on the
	-- head's ID. null if native tweet or not the first tweet in convo
);




create table parsav_rels (
	relator bigint references parsav_actors(id)
		on delete cascade, -- e.g. follower
	relatee bigint references parsav_actors(id)
		on delete cascade, -- e.g. followed
	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)
		on delete cascade,
	post  bigint not null
);


create table parsav_attach (
	id          bigint primary key default (1+random()*(2^63-1))::bigint,
	birth       timestamp not null default now(),
	content     bytea not null,
	mime        text, -- null if unknown, will be reported as x-octet-stream
	description text,
	parent      bigint -- post id, or userid for avatars
);


create table parsav_circles (
	id          bigint primary key default (1+random()*(2^63-1))::bigint,
	owner       bigint not null references parsav_actors(id),
	name        text not null,
	members     bigint[] not null default array[]::bigint[],

	unique (owner,name)
);


create table parsav_rooms (
	id          bigint primary key default (1+random()*(2^63-1))::bigint,
	origin		bigint references parsav_servers(id),
	name		text not null,
	description text not null,
	policy      smallint not null
);


create table parsav_room_members (
	room   bigint references parsav_rooms(id),
	member bigint references parsav_actors(id),
	rank   smallint not null default 0,
	admin  boolean not null default false, -- non-admins with rank can only moderate + invite
	title  text, -- admin-granted title like reddit flair
	vouchedby bigint references parsav_actors(id)
);


create table parsav_invites (
	id          bigint primary key default (1+random()*(2^63-1))::bigint,
	-- when a user is created from an invite, the invite is deleted and the invite
	-- ID becomes the user ID. privileges granted on the invite ID during the invite
	-- process are thus inherited by the user
	issuer bigint references parsav_actors(id),
	handle text, -- admin can lock invite to specific handle
	rank   smallint not null default 0,
	quota  integer not null  default 1000
);


create table parsav_interventions (
	id     bigint primary key default (1+random()*(2^63-1))::bigint,
	issuer bigint references parsav_actors(id) not null,
	scope  bigint, -- can be null or room for local actions
	nature smallint not null, -- silence, suspend, disemvowel, etc
	victim bigint not null, -- could potentially target group as well
	expire timestamp -- auto-expires if set
);

-- create a temporary managed auth table; we can delete this later
-- if it ends up being replaced with a view
%include pgsql-auth.sql%

Modified cmdparse.t from [50677a3c0c] to [bfedd61eec].

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
..
49
50
51
52
53
54
55

56
57
58
59
60
61
62
..
68
69
70
71
72
73
74




75
76
77
78
79
80
81
82
83
84
85
-- vim: ft=terra
return function(tbl)

	local options = terralib.types.newstruct('options') do
		local flags = '' for _,d in pairs(tbl) do flags = flags .. d[1] end

		local helpstr = 'usage: parsav [-' .. flags .. '] [<arg>...]\n'
		options.entries = {
			{field = 'arglist', type = lib.mem.ptr(rawstring)}
		}
		local shortcases, longcases, init, verifiers = {}, {}, {}, {}
		local self = symbol(&options)
		local arg = symbol(rawstring)
		local idx = symbol(uint)
		local argv = symbol(&rawstring)
		local argc = symbol(int)
		local optstack = symbol(intptr)

		local skip = label()
		local sanitize = function(s) return s:gsub('_','-') end
		for o,desc in pairs(tbl) do
			local consume = desc[3] or 0

			options.entries[#options.entries + 1] = {
				field = o, type = (consume > 0) and &rawstring or bool

			}
			helpstr = helpstr .. string.format('    -%s --%s: %s\n',
				desc[1], sanitize(o), desc[2])
		end
		for o,desc in pairs(tbl) do
			local flag = desc[1]
			local consume = desc[3] or 0

			init[#init + 1] = quote [self].[o] = [(consume > 0 and `nil) or false] end



			local ch if consume > 0 then
				ch = quote
					[self].[o] = argv+(idx+1+optstack)
					optstack = optstack + consume
				end
				verifiers[#verifiers+1] = quote
					var terminus = argv + argc
					if [self].[o] ~= nil and [self].[o] >= terminus then
						lib.bail(['missing argument for command line option ' .. sanitize(o)])
					end
				end


			else ch = quote
				[self].[o] = true
			end end
			shortcases[#shortcases + 1] = quote
				case [int8]([string.byte(flag)]) then [ch] end
			end
			longcases[#longcases + 1] = quote
................................................................................
			end
		end
		terra options:free() self.arglist:free() end
		options.methods.parse = terra([self], [argc], [argv])
			[init]
			var parseopts = true
			var [optstack] = 0

			self.arglist = lib.mem.heapa(rawstring, argc)
			var finalargc = 0
			for [idx]=1,argc do
				var [arg] = argv[idx]
				if optstack > 0 then optstack = optstack - 1 goto [skip] end
				if arg[0] == @'-' and parseopts then
					if arg[1] == @'-' then -- long option
................................................................................
							switch arg[j] do [shortcases] end
							j = j + 1
						end
					end
				else
					self.arglist.ptr[finalargc] = arg
					finalargc = finalargc + 1




				end
				::[skip]::
			end
			[verifiers]
			if finalargc == 0 then self.arglist:free()
							  else self.arglist:resize(finalargc) end
		end
		options.helptxt = helpstr
	end
	return options
end

|
>


>
|










>



|
>

|
>






|
>
|
>
>
>











>
>







 







>







 







>
>
>
>







|



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
..
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
..
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
return function(tbl,opts)
	opts = opts or {}
	local options = terralib.types.newstruct('options') do
		local flags = '' for _,d in pairs(tbl) do flags = flags .. d[1] end
		local flagstr = '[-' .. flags .. ']'
		local helpstr = '\n'
		options.entries = {
			{field = 'arglist', type = lib.mem.ptr(rawstring)}
		}
		local shortcases, longcases, init, verifiers = {}, {}, {}, {}
		local self = symbol(&options)
		local arg = symbol(rawstring)
		local idx = symbol(uint)
		local argv = symbol(&rawstring)
		local argc = symbol(int)
		local optstack = symbol(intptr)
		local subcmd = symbol(intptr)
		local skip = label()
		local sanitize = function(s) return s:gsub('_','-') end
		for o,desc in pairs(tbl) do
			local consume = desc.consume or 0
			local incr = desc.inc or 0
			options.entries[#options.entries + 1] = {
				field = o, type = (consume > 0) and &rawstring or
				                  (incr    > 0) and uint       or bool
			}
			helpstr = helpstr .. string.format('    -%s --%s: %s\n',
				desc[1], sanitize(o), desc[2])
		end
		for o,desc in pairs(tbl) do
			local flag = desc[1]
			local consume = desc.consume or 0
			local incr = desc.inc or 0
			init[#init + 1] = quote [self].[o] = [
				(consume > 0 and `nil) or 
				(incr    > 0 and `0  ) or false
			] end
			local ch if consume > 0 then
				ch = quote
					[self].[o] = argv+(idx+1+optstack)
					optstack = optstack + consume
				end
				verifiers[#verifiers+1] = quote
					var terminus = argv + argc
					if [self].[o] ~= nil and [self].[o] >= terminus then
						lib.bail(['missing argument for command line option ' .. sanitize(o)])
					end
				end
			elseif incr > 0 then
				ch = quote [self].[o] = [self].[o] + incr end
			else ch = quote
				[self].[o] = true
			end end
			shortcases[#shortcases + 1] = quote
				case [int8]([string.byte(flag)]) then [ch] end
			end
			longcases[#longcases + 1] = quote
................................................................................
			end
		end
		terra options:free() self.arglist:free() end
		options.methods.parse = terra([self], [argc], [argv])
			[init]
			var parseopts = true
			var [optstack] = 0
			var [subcmd] = [ opts.subcmd or 0 ]
			self.arglist = lib.mem.heapa(rawstring, argc)
			var finalargc = 0
			for [idx]=1,argc do
				var [arg] = argv[idx]
				if optstack > 0 then optstack = optstack - 1 goto [skip] end
				if arg[0] == @'-' and parseopts then
					if arg[1] == @'-' then -- long option
................................................................................
							switch arg[j] do [shortcases] end
							j = j + 1
						end
					end
				else
					self.arglist.ptr[finalargc] = arg
					finalargc = finalargc + 1
					if subcmd > 0 then
						subcmd = subcmd - 1
						if subcmd == 0 then parseopts = false end
					end
				end
				::[skip]::
			end
			[verifiers]
			if finalargc == 0 then self.arglist:free()
							  else self.arglist:resize(finalargc) end
		end
		options.helptxt = { opts = helpstr, flags = flagstr }
	end
	return options
end

Modified common.lua from [e762fc8997] to [c72e0a0971].

101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
		local kt = {}
		for k,v in pairs(ary) do kt[#kt+1] = k end
		return kt
	end;
	ingest = function(f)
		local h = io.open(f, 'r')
		if h == nil then return nil end
		local txt = f:read('*a') f:close()
		return chomp(txt)
	end;
	parseargs = function(a)
		local raw = false
		local opts, args = {}, {}
		for i,v in ipairs(a) do
			if v == '--' then







|







101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
		local kt = {}
		for k,v in pairs(ary) do kt[#kt+1] = k end
		return kt
	end;
	ingest = function(f)
		local h = io.open(f, 'r')
		if h == nil then return nil end
		local txt = h:read('*a') h:close()
		return chomp(txt)
	end;
	parseargs = function(a)
		local raw = false
		local opts, args = {}, {}
		for i,v in ipairs(a) do
			if v == '--' then

Modified config.lua from [29834379ec] to [0c09bec0e6].

33
34
35
36
37
38
39

40
41
42
43
44
45

46
47
48
49
50
51
52
..
58
59
60
61
62
63
64
65
66
67
68


69
70
71
72
73
74
75
	tgthf     = u.tobool(default('parsav_arch_armhf',true)); 
	doc       = {
		online  = u.tobool(default('parsav_online_documentation',true)); 
		offline = u.tobool(default('parsav_offline_documentation',true)); 
	};
	outform   = default('parsav_emit_type', 'o');
	endian    = default('parsav_arch_endian', 'little');

	build     = {
		id = u.rndstr(6);
		release = u.ingest('release');
		when = os.date();
	};
	feat = {};

	backends = defaultlist('parsav_backends', 'pgsql');
	braingeniousmode = false;
	embeds = {
		{'style.css', 'text/css'};
		{'default-avatar.webp', 'image/webp'};
		{'padlock.webp', 'image/webp'};
		{'warn.webp', 'image/webp'};
................................................................................
if u.ping '.fslckout' or u.ping '_FOSSIL_' then
	if u.ping '_FOSSIL_' then default_os = 'windows' end
	conf.build.branch = u.exec { 'fossil', 'branch', 'current' }
	conf.build.checkout = (u.exec { 'fossil', 'sql',
		[[select value from localdb.vvar where name = 'checkout-hash']]
	}):gsub("^'(.*)'$", '%1')
end
conf.os    = default('parsav_host_os', default_os);
conf.tgtos = default('parsav_target_os', default_os);
conf.posix = posixes[conf.os]
conf.exe   = u.tobool(default('parsav_link',not conf.tgttrip)); -- turn off for partial builds


conf.build.origin = coalesce(
	os.getenv('parsav_builder'),
	string.format('%s@%s', coalesce (
		os.getenv('USER'),
		u.exec{'whoami'}
	), u.exec{'hostname'}) -- whoami and hostname are present on both windows & unix
)







>






>







 







|
|

|
>
>







33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
..
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
	tgthf     = u.tobool(default('parsav_arch_armhf',true)); 
	doc       = {
		online  = u.tobool(default('parsav_online_documentation',true)); 
		offline = u.tobool(default('parsav_offline_documentation',true)); 
	};
	outform   = default('parsav_emit_type', 'o');
	endian    = default('parsav_arch_endian', 'little');
	prefix    = default('parsav_install_prefix', './');
	build     = {
		id = u.rndstr(6);
		release = u.ingest('release');
		when = os.date();
	};
	feat = {};
	debug = u.tobool(default('parsav_enable_debug',true)); 
	backends = defaultlist('parsav_backends', 'pgsql');
	braingeniousmode = false;
	embeds = {
		{'style.css', 'text/css'};
		{'default-avatar.webp', 'image/webp'};
		{'padlock.webp', 'image/webp'};
		{'warn.webp', 'image/webp'};
................................................................................
if u.ping '.fslckout' or u.ping '_FOSSIL_' then
	if u.ping '_FOSSIL_' then default_os = 'windows' end
	conf.build.branch = u.exec { 'fossil', 'branch', 'current' }
	conf.build.checkout = (u.exec { 'fossil', 'sql',
		[[select value from localdb.vvar where name = 'checkout-hash']]
	}):gsub("^'(.*)'$", '%1')
end
conf.os    = default('parsav_host_os', default_os)
conf.tgtos = default('parsav_target_os', default_os)
conf.posix = posixes[conf.os]
conf.exe   = u.tobool(default('parsav_link',not conf.tgttrip)) -- turn off for partial builds
conf.prefix_conf = default('parsav_install_prefix_cfg', conf.prefix)
conf.prefix_static = default('parsav_install_prefix_static', nil)
conf.build.origin = coalesce(
	os.getenv('parsav_builder'),
	string.format('%s@%s', coalesce (
		os.getenv('USER'),
		u.exec{'whoami'}
	), u.exec{'hostname'}) -- whoami and hostname are present on both windows & unix
)

Modified crypt.t from [bf3957f4f4] to [9b6529621c].

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
	sha384 = `hashalg {id = lib.md.MBEDTLS_MD_SHA384; bytes = m.algsz.sha384};
	sha224 = `hashalg {id = lib.md.MBEDTLS_MD_SHA224; bytes = m.algsz.sha224};
	-- md5 = {id = lib.md.MBEDTLS_MD_MD5};-- !!!
};
local callbacks = {}
if config.feat.randomizer == 'kern' then
	local rnd = terralib.externfunction('getrandom', {&opaque, intptr, uint} -> ptrdiff);
	terra callbacks.randomize(ctx: &opaque, dest: &uint8, sz: intptr): int
		return rnd(dest, sz, 0)
	end
elseif config.feat.randomizer == 'devfs' then
	terra callbacks.randomize(ctx: &opaque, dest: &uint8, sz: intptr): int
		var gen = lib.io.open("/dev/urandom",0)
		lib.io.read(gen, dest, sz)
		lib.io.close(gen)
		return sz
	end
elseif config.feat.randomizer == 'libc' then
	local rnd = terralib.externfunction('rand', {} -> int);
	local srnd = terralib.externfunction('srand', uint -> int);
	local time = terralib.includec 'time.h'
	lib.init[#lib.init + 1] = quote srnd(time.time(nil)) end
	print '(warn) using libc soft-rand function for cryptographic purposes, this is very bad!'
	terra callbacks.randomize(ctx: &opaque, dest: &uint8, sz: intptr): int
		for i=0,sz do dest[i] = [uint8](rnd()) end
		return sz
	end
end













terra m.pem(pub: bool, key: &ctx, buf: &uint8): bool
	if pub then
		return lib.pk.mbedtls_pk_write_pubkey_pem(key, buf, const.maxpemsz) == 0
	else
		return lib.pk.mbedtls_pk_write_key_pem(key, buf, const.maxpemsz) == 0
	end







|



|











|




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







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
	sha384 = `hashalg {id = lib.md.MBEDTLS_MD_SHA384; bytes = m.algsz.sha384};
	sha224 = `hashalg {id = lib.md.MBEDTLS_MD_SHA224; bytes = m.algsz.sha224};
	-- md5 = {id = lib.md.MBEDTLS_MD_MD5};-- !!!
};
local callbacks = {}
if config.feat.randomizer == 'kern' then
	local rnd = terralib.externfunction('getrandom', {&opaque, intptr, uint} -> ptrdiff);
	terra m.spray(dest: &uint8, sz: intptr): int
		return rnd(dest, sz, 0)
	end
elseif config.feat.randomizer == 'devfs' then
	terra m.spray(dest: &uint8, sz: intptr): int
		var gen = lib.io.open("/dev/urandom",0)
		lib.io.read(gen, dest, sz)
		lib.io.close(gen)
		return sz
	end
elseif config.feat.randomizer == 'libc' then
	local rnd = terralib.externfunction('rand', {} -> int);
	local srnd = terralib.externfunction('srand', uint -> int);
	local time = terralib.includec 'time.h'
	lib.init[#lib.init + 1] = quote srnd(time.time(nil)) end
	print '(warn) using libc soft-rand function for cryptographic purposes, this is very bad!'
	terra m.spray(dest: &uint8, sz: intptr): int
		for i=0,sz do dest[i] = [uint8](rnd()) end
		return sz
	end
end

m.random = macro(function(typ, from, to)
	local ty = typ:astype()
	return quote
		var v: ty
		m.spray([&uint8](&v), sizeof(ty))
		v = v % (to - from) + from -- only works with unsigned!!
	in v end
end)

terra callbacks.randomize(ctx: &opaque, dest: &uint8, sz: intptr)
	return m.spray(dest,sz) end

terra m.pem(pub: bool, key: &ctx, buf: &uint8): bool
	if pub then
		return lib.pk.mbedtls_pk_write_pubkey_pem(key, buf, const.maxpemsz) == 0
	else
		return lib.pk.mbedtls_pk_write_key_pem(key, buf, const.maxpemsz) == 0
	end

Added html.t version [ee4d50abb4].



































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- vim: ft=terra
local m={}
local pstr = lib.mem.ptr(int8)

terra m.sanitize(txt: pstr, quo: bool)
	var a: lib.str.acc a:init(txt.ct*1.3)
	for i=0,txt.ct do
		if txt(i) == @'<' then a:lpush('&lt;')
		elseif txt(i) == @'>' then a:lpush('&gt;')
		elseif txt(i) == @'&' then a:lpush('&amp;')
		elseif quo and txt(i) == @'"' then a:lpush('&quot;')
		else a:push(&txt(i),1) end
	end
	return a:finalize()
end

return m

Modified makefile from [3210eb684d] to [8946539e56].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
dl = git
dbg-flags = $(if $(dbg),-g)

images = $(addsuffix .webp, $(basename $(wildcard static/*.svg)))
styles = $(addsuffix .css, $(basename $(wildcard static/*.scss)))

parsav: parsav.t config.lua pkgdata.lua $(images) $(styles)
	terra $(dbg-flags) $<
parsav.o: parsav.t config.lua pkgdata.lua $(images) $(styles)
	env parsav_link=no terra $(dbg-flags) $<
parsav.ll: parsav.t config.lua pkgdata.lua $(images) $(styles)
	env parsav_emit_type=ll parsav_link=no terra $(dbg-flags) $<
parsav.s: parsav.ll
	llc --march=$(target) $<

static/%.webp: static/%.png
	cwebp -q 90 $< -o $@
static/%.png: static/%.svg
	inkscape -f $< -C -d 180 -e $@
static/%.css: static/%.scss






|

|

|

|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
dl = git
dbg-flags = $(if $(dbg),-g)

images = $(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) $<
parsav.ll parsavd.ll: parsav.t config.lua pkgdata.lua $(images) $(styles)
	env parsav_emit_type=ll parsav_link=no terra $(dbg-flags) $<
parsav.s parsavd.ss: parsav.ll
	llc --march=$(target) $<

static/%.webp: static/%.png
	cwebp -q 90 $< -o $@
static/%.png: static/%.svg
	inkscape -f $< -C -d 180 -e $@
static/%.css: static/%.scss

Added mgtool.t version [293667feb7].

































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
-- vim: ft=terra
-- provides the functionality of the `parsav` utility that controls `parsavd`
local pstr = lib.mem.ptr(int8)
local ctloptions = lib.cmdparse({
	version = {'V', 'display information about the binary build and exit'};
	verbose = {'v', 'increase logging verbosity', inc=1};
	quiet = {'q', 'do not print to standard out'};
	help = {'h', 'display this list'};
	backend_file = {'B', 'init from specified backend file', consume=1};
	backend = {'b', 'operate on only the selected backend'};
	instance = {'i', 'specify the instance to control by name', consume=1};
	all = {'A', 'affect all running instances'};
}, { subcmd = 1 })

local pbasic = lib.cmdparse {
	help = {'h', 'display this list'}
}
local subcmds = {
}

local ctlcmds = {
	{ 'start', 'start a new instance of the server' };
	{ 'stop', 'stop a running instance' };
	{ 'attach', 'capture log output from a running instance' };
	{ 'db init <domain>', 'initialize backend databases (or a single specified database) with the necessary schema and structures for the given FQDN' };
	{ 'db vacuum', 'delete old remote content from the database' };
	{ 'db extract (<artifact>|<post>/<attachment number>)', 'extracts an attachment artifact from the database and prints it to standard out' };
	{ 'db excise <artifact>', 'extracts an attachment artifact from the database and prints it to standard out' };
	{ 'db obliterate', 'completely purge all parsav-related content and structure from the database, destroying all user content (requires confirmation)' };
	{ 'db insert', 'reads a file from standard in and inserts it into the attachment database, printing the resulting ID' };
	{ 'mkroot <handle>', 'establish a new root user with the given handle' };
	{ 'user <handle> auth <type> new', '(where applicable, managed auth only) create a new authentication token of the given type for a user' };
	{ 'user <handle> auth <type> reset', '(where applicable, managed auth only) delete all of a user\'s authentication tokens of the given type and issue a new one' };
	{ 'user <handle> auth purge-credentials [<type>]', 'delete all credentials that would allow this user to log in (where possible)' };
	{ 'user <handle> (grant|revoke) (<priv>|all)', 'grant or revoke a specific power to or from a user' };
	{ 'user <handle> emasculate', 'strip all administrative powers from a user' };
	{ 'user <handle> suspend [<timespec>]', '(e.g. \27[1muser jokester suspend 5d 6h 7m 3s\27[m to suspend "jokester" for five days, six hours, seven minutes, and three seconds) suspend a user'};
	{ 'actor <xid> purge-all', 'remove all traces of a user from the database (except local user credentials -- use \27[1mauth purge-credentials\27[m to prevent a user from accessing the instance)' };
	{ 'actor <xid> create', 'instantiate a new actor' };
	{ 'actor <xid> bestow <epithet>', 'bestow an epithet upon an actor' };
	{ 'conf set <setting> <value>', 'add or a change a server configuration parameter to the database' };
	{ 'conf get <setting>', 'report the value of a server setting' };
	{ 'conf reset <setting>', 'reset a server setting to its default value' };
	{ 'conf refresh', 'instruct an instance to refresh its configuration cache' };
	{ 'conf chsec', 'reset the server secret, invalidating all authentication cookies' };
	{ 'serv dl', 'initiate an update cycle over foreign actors' };
	{ 'tl', 'print the current local timeline to standard out' };
	{ 'be pgsql setup-auth (managed|unmanaged)', '(PGSQL backends) select the authentication strategy to use' };
}

local ctlcmdhelp = 'commands:\n'
for _, v in ipairs(ctlcmds) do
	ctlcmdhelp = ctlcmdhelp .. string.format (
		'    \27[1m%s\27[m: %s\n', v[1]:gsub('(<%w+>)','\27[36m%1\27[;1m'), v[2]
	)
end

local struct idelegate {
	all: bool
	src: &lib.store.source
	srv: &lib.srv.overlord
}
idelegate.metamethods.__methodmissing = macro(function(meth, self, ...)
	local expr = {...}
	local rt
	for _,f in pairs(lib.store.backend.entries) do
		local fn = f.field or f[1]
		local ft = f.type or f[2]
		if fn == meth then rt = ft.type.returntype break end
	end

	return quote
		var r: rt
		if self.all
			then r=self.srv:[meth]([expr])
			elseif self.src ~= nil then r=self.src:[meth]([expr])
			else lib.bail('no data source specified')
		end
	in r end
end)

local terra gensec(sdest: rawstring)
	var dest = [&uint8](sdest)
	lib.crypt.spray(dest,64)
	for i=0,64 do dest[i] = dest[i] % (0x7e - 0x20) + 0x20 end
	dest[64] = 0
end

local terra entry_mgtool(argc: int, argv: &rawstring): int
	if argc < 1 then lib.bail('bad invocation!') end

	lib.noise_init(2)
	[lib.init]

	var srv: lib.srv.overlord
	var dlg = idelegate { srv = &srv, src = nil }

	var mode: ctloptions
	mode:parse(argc,argv) defer mode:free()
	if mode.version then version() return 0 end
	if mode.help then
		[ lib.emit(false, 1, 'usage: ', `argv[0], ' ', ctloptions.helptxt.flags, ' <cmd> [<args>…]', ctloptions.helptxt.opts, ctlcmdhelp) ]
		return 0
	end
	var cnf: rawstring
	if mode.backend_file ~= nil
		then cnf = @mode.backend_file
		else cnf = lib.proc.getenv('parsav_backend_file')
	end
	if cnf == nil then cnf = "backend.conf" end
	if mode.all then dlg.all = true else
		-- iterate through and pick the right backend
	end

	if mode.arglist.ct == 0 then lib.bail('no command') return 1 end
	if lib.str.cmp(mode.arglist(0),'attach') == 0 then
	elseif lib.str.cmp(mode.arglist(0),'start') == 0 then
	elseif lib.str.cmp(mode.arglist(0),'stop') == 0 then
	else
		if lib.str.cmp(mode.arglist(0),'db') == 0 then
			var dbmode: pbasic dbmode:parse(mode.arglist.ct, &mode.arglist(0))
			if dbmode.help then
				[ lib.emit(false, 1, 'usage: ', `argv[0], ' db ', dbmode.type.helptxt.flags, ' <cmd> [<args>…]', dbmode.type.helptxt.opts) ]
				return 1
			end
			if dbmode.arglist.ct < 1 then goto cmderr end

			srv:setup(cnf) 
			if lib.str.cmp(dbmode.arglist(0),'init') == 0 and dbmode.arglist.ct == 2 then
				lib.report('initializing new database structure for domain ', dbmode.arglist(1))
				dlg:dbsetup()
				srv:conprep(lib.store.prepmode.conf)
				dlg:conf_set('instance-name', dbmode.arglist(1))
				do var sec: int8[65] gensec(&sec[0])
					dlg:conf_set('server-secret', &sec[0])
				end
				lib.report('database setup complete; use mkroot to create an administrative user')
			elseif lib.str.cmp(dbmode.arglist(0),'obliterate') == 0 then
				var confirmstrs = array(
					'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'eta', 'nu', 'kappa'
				)
				var cfmstr: int8[64] cfmstr[0] = 0
				var tdx = lib.osclock.time(nil) / 60
				for i=0,3 do
					if i ~= 0 then lib.str.cat(&cfmstr[0], '-') end
					lib.str.cat(&cfmstr[0], confirmstrs[(tdx + 49*i) % [confirmstrs.type.N]])
				end

				if dbmode.arglist.ct == 1 then
					lib.bail('you are attempting to completely obliterate all data! make sure you have selected your target correctly. if you really want to do this, pass the confirmation string ', &cfmstr[0])
				elseif dbmode.arglist.ct == 2 then
					if lib.str.cmp(dbmode.arglist(1), cfmstr) == 0 then
						lib.warn('completely obliterating all data!')
						dlg:obliterate_everything()
					else
						lib.bail('you passed an incorrect confirmation string; pass ', &cfmstr[0], ' if you really want to destroy everything')
					end
				else goto cmderr end
			else goto cmderr end
		elseif lib.str.cmp(mode.arglist(0),'be') == 0 then
			srv:setup(cnf) 
		elseif lib.str.cmp(mode.arglist(0),'conf') == 0 then
			srv:setup(cnf) 
			srv:conprep(lib.store.prepmode.conf)
			var cfmode: lib.cmdparse {
				help = {'h','display this list'};
				no_notify = {'n', "don't instruct the server to refresh its configuration cache after making changes; useful for \"transactional\" configuration changes."};
			}
			cfmode:parse(mode.arglist.ct, &mode.arglist(0))
			if cfmode.help then
				[ lib.emit(false, 1, 'usage: ', `argv[0], ' conf ', cfmode.type.helptxt.flags, ' <cmd> [<args>…]', cfmode.type.helptxt.opts) ]
				return 1
			end
			if cfmode.arglist.ct < 1 then goto cmderr end

			if cfmode.arglist.ct == 1 then
				if lib.str.cmp(cfmode.arglist(0),'chsec') == 0 then
					var sec: int8[65] gensec(&sec[0])
					dlg:conf_set('server-secret', &sec[0])
					lib.report('server secret reset')
					-- FIXME notify server to reload its config
				elseif lib.str.cmp(cfmode.arglist(0),'refresh') == 0 then
					-- TODO notify server to reload config
				else goto cmderr end
			elseif cfmode.arglist.ct == 3 and
				lib.str.cmp(cfmode.arglist(0),'set') == 0 then
				dlg:conf_set(cfmode.arglist(1),cfmode.arglist(2))
				lib.report('parameter set')
			else goto cmderr end
		else
			srv:setup(cnf) 
			srv:conprep(lib.store.prepmode.full)
			if lib.str.cmp(mode.arglist(0),'mkroot') == 0 then
				var cfmode: pbasic cfmode:parse(mode.arglist.ct, &mode.arglist(0))
				if cfmode.help then
					[ lib.emit(false, 1, 'usage: ', `argv[0], ' mkroot ', cfmode.type.helptxt.flags, ' <handle>', cfmode.type.helptxt.opts) ]
					return 1
				end
				if cfmode.arglist.ct == 1 then
					var am = dlg:conf_get('credential-store')
					var mg: bool
					if (not am) or am:cmp(lib.str.plit 'managed') then
						mg = true
					elseif am:cmp(lib.str.plit 'unmanaged') then
						lib.warn('credential store is unmanaged; you will need to create credentials for the new root user manually!')
						mg = false
					else lib.bail('unknown credential store mode "',{am.ptr,am.ct},'"; should be either "managed" or "unmanaged"') end
					var kbuf: uint8[lib.crypt.const.maxdersz]
					var root = lib.store.actor.mk(&kbuf[0])
					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'
						-- 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)
					lib.report('created new administrator')
					if mg then
						lib.dbg('generating temporary password')
						var tmppw: uint8[33]
						lib.crypt.spray(&tmppw[0],32) tmppw[32] = 0
						for i=0,32 do
							tmppw[i] = tmppw[i] % (10 + 26*2)
							if tmppw[i] >= 36 then
								tmppw[i] = tmppw[i] + (0x61 - 36)
							elseif tmppw[i] >= 10 then
								tmppw[i] = tmppw[i] + (0x41 - 10)
							else tmppw[i] = tmppw[i] + 0x30 end
						end
						lib.dbg('assigning temporary password')
						dlg:auth_create_pw(ruid, false, pstr {
							ptr = [rawstring](&tmppw[0]), ct = 32
						})
						lib.report('temporary root pw: ', {[rawstring](&tmppw[0]), 32})
					end
				else goto cmderr end
			elseif lib.str.cmp(mode.arglist(0),'user') == 0 then
			elseif lib.str.cmp(mode.arglist(0),'actor') == 0 then
			elseif lib.str.cmp(mode.arglist(0),'tl') == 0 then
			elseif lib.str.cmp(mode.arglist(0),'serv') == 0 then
			else goto cmderr end
		end
	end

	do return 0 end
	::cmderr:: lib.bail('invalid command') return 2
end

return entry_mgtool

Modified parsav.md from [ae23203c4b] to [4b27db126a].

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
..
69
70
71
72
73
74
75
76
77
78
79
80








81
82
83
84
85
86
87
..
92
93
94
95
96
97
98
99
100
101
102
103




104
105
106
107
108
109
110

if you use nixos and wish to build the pdf documentation, you're going to have to do a bit of extra work (but you're used to that, aren't you). for some incomprehensible reason, the groff package on nix is split up, seemingly randomly, with many crucial output devices relegated to the "perl" output of the package, which is not installed by default (and `nix-env -iA nixos.groff.perl` doesn't work either; i don't know why either). you'll have to instantiate and install the outputs directly by path, e.g. `nix-env -i /nix/store/*groff*/` to get everything you need into your profile. alas, the battle is not over: you also need to change the environment variables `GROFF_FONT_PATH` and `GROFF_TMAC_PATH` to point at the `font` and `tmac` subdirs of `~/.nix-profile/share/groff/$groff_version/`. once this is done, invoking `groff -Tpdf` will work as expected.

## configuring

the `parsav` configuration is comprised of two components: the backends list and the config store. the backends list is a simple text file that tells `parsav` which data sources to draw from. the config store is a key-value store which contains the rest of the server's configuration, and is loaded from the backends. the configuration store can be spread across the backends; backends will be checked for configuration keys according to the order in which they are listed. changes to the configuration store affect parsav in real time; you only need to restart the server if you make a change to the backend list.

eventually, we'll add a command-line tool `parsav-cfg` to enable easy modification of the configuration store from the command line; for now, you'll need to modify the database by hand or use the online administration menu. the schema.sql file contains commands to prompt for various important values like the name of your administrative user.

by default, parsav looks for a file called `backend.conf` in the current directory when it is launched. you can override this default with the `parsav_backend_file` environment or with the `-b`/`--backend-file` flag. `backend.conf` lists one backend per line, in the form `id type confstring`. for instance, if you had two postgresql databases, you might write a backend file like

    master   pgsql   host=localhost dbname=parsav
	tweets   pgsql   host=420.69.dread.cloud dbname=content

the form the configuration string takes depends on the specific backend.





### postgresql backend

currently, postgres needs to be configured manually before parsav can make use of it to store data. the first step is to create a database for parsav's use. once you've done that, you need to create the database schema with the command `$ psql (-h $host) -d $database -f schema.sql`. you'll be prompted for some crucial settings to install in the configuration store, such as the name of the relation you want to use for authentication (we'll call it `parsav_auth` from here on out).


parsav separates the storage of user credentials from the storage of other user data, in order to facilitate centralized user accounting. you don't need to take advantage of this feature, and if you don't want to, you can just create a `parsav_auth` table and have done. however, `parsav_auth` can also be a view, collecting a list of authorized users and their various credentials from whatever source you please.




`parsav_auth` has the following schema:

    create table parsav_auth (
		aid bigint primary key,
		uid bigint,
		newname text,
................................................................................
		restrict text[],
		netmask cidr,
		blacklist bool
    )

`aid` is a unique value identifying the authentication method. it must be deterministic -- values based on time of creation or a hash of `uid`+`kind`+`cred` are ideal. `uid` is the identifier of the user the row specifies credentials for. `kind` is a string indicating the credential type, and `cred` is the content of that credential.for the meaning of these fields and use of this structure, see **authentication** below.

## authentication 
in the most basic case, an authentication record would be something like `{uid = 123, kind = "pw-sha512", cred = "\x12bf90…a10e"::bytea}`. but `parsav` is not restricted to username-password authentication, and in addition to various hashing styles, it also will support more esoteric forms of authentcation. any individual user can have as many auth rows as she likes. there is also a `restrict` field, which is normally null, but can be specified in order to restrict a particular credential to certain operations, such as posting tweets or updating a bio. `blacklist` indicates that any attempt to authenticate that matches this row will be denied, regardless of whether it matches other rows. if `netmask` is present, this authentication will only succeed if it comes from the specified IP mask.

`uid` can also be `0` (not null, which matches any user!), indicating that there is not yet a record in `parsav_actors` for this account. if this is the case, `name` must contain the handle of the account to be created when someone attempts to log in with this credential. whether `name` is used in the authentication process depends on whether the authentication method accepts a username. all rows with the same `uid` *must* have the same `name`.









below is a full list of authentication types we intend/hope to one day support. contributors should consider this a to-do list. a checked box indicates the scheme has been implemented.

* ☑ pw-sha{512,384,256,224}: an ordinary password, hashed with the appropriate algorithm
* ☐ pw-{sha1,md5,clear} (insecure, must be manually enabled at compile time with the config variable `parsav_let_me_be_a_dumbass="i know what i'm doing"`)
* ☐ pw-pbkdf2-hmac-sha{…}: a password hashed with the Password-Based Key Derivation Function 2 instead of plain SHA2
* ☐ pw-extern-ldap: try to authenticate by binding against an LDAP server
* ☐ pw-extern-cyrus: try to authenticate against saslauthd
................................................................................
* ☐ api-digest-sha{…}: a value that can be hashed with the current epoch to derive a temporary access key without logging in. these are used for API calls, sent in the header `X-API-Key`.
* ☐ otp-time-sha1: a TOTP PSK: the first two bytes represent the step, the third byte the OTP length, and the remaining ten bytes the secret key
* ☐ tls-cert-fp: a fingerprint of a client certificate
* ☐ tls-cert-ca: a value of the form `fp/key=value` where a client certificate with the property `key=value` (e.g. `uid=cyberlord19`) signed by a certificate authority matching the given fingerprint `fp` can authenticate the user
* ☐ challenge-rsa-sha256: an RSA public key. the user is presented with a challenge and must sign it with the corresponding private key using SHA256.
* ☐ challenge-ecc-sha256: a Curve25519 public key. the user is presented with a challenge and must sign it with the corresponding private key using SHA256.
* ☐ challenge-ecc448-sha256: a Curve448 public key. the user is presented with a challenge and must sign it with the corresponding private key using SHA256.
* ☑ trust: authentication always succeeds. only use in combination with netmask!!!

## license

parsav is released under the terms of the EUPL v1.2. copies of this license are included in the repository. dependencies are produced





## future direction

parsav needs more storage backends, as it currently supports only postgres. some possibilities, in order of priority, are:

* plain text/filesystem storage
* lmdb







|








>
>
>
>


<
>

<
>
>
>







 







<


|

>
>
>
>
>
>
>
>







 







|

|

|
>
>
>
>







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
..
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
...
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127

if you use nixos and wish to build the pdf documentation, you're going to have to do a bit of extra work (but you're used to that, aren't you). for some incomprehensible reason, the groff package on nix is split up, seemingly randomly, with many crucial output devices relegated to the "perl" output of the package, which is not installed by default (and `nix-env -iA nixos.groff.perl` doesn't work either; i don't know why either). you'll have to instantiate and install the outputs directly by path, e.g. `nix-env -i /nix/store/*groff*/` to get everything you need into your profile. alas, the battle is not over: you also need to change the environment variables `GROFF_FONT_PATH` and `GROFF_TMAC_PATH` to point at the `font` and `tmac` subdirs of `~/.nix-profile/share/groff/$groff_version/`. once this is done, invoking `groff -Tpdf` will work as expected.

## configuring

the `parsav` configuration is comprised of two components: the backends list and the config store. the backends list is a simple text file that tells `parsav` which data sources to draw from. the config store is a key-value store which contains the rest of the server's configuration, and is loaded from the backends. the configuration store can be spread across the backends; backends will be checked for configuration keys according to the order in which they are listed. changes to the configuration store affect parsav in real time; you only need to restart the server if you make a change to the backend list.

you can directly modify the store from the command line with the `parsav conf` command; see `parsav conf -h` for more information.

by default, parsav looks for a file called `backend.conf` in the current directory when it is launched. you can override this default with the `parsav_backend_file` environment or with the `-b`/`--backend-file` flag. `backend.conf` lists one backend per line, in the form `id type confstring`. for instance, if you had two postgresql databases, you might write a backend file like

    master   pgsql   host=localhost dbname=parsav
	tweets   pgsql   host=420.69.dread.cloud dbname=content

the form the configuration string takes depends on the specific backend.

once you've set up a backend and confirmed parsav can connect succesfully to it, you can initialize the database with the command `parsav db init <domain>`, where `<domain>` is the name of the domain name you will be hosting `parsav` from. this will install all necessary structures and functions in the target and create all necessary files. it will not, however, create any users. you can create an initial administrative user with the `parsav mkroot <handle>` command, where `<handle>` is the handle you want to use on the server. this will also assign a temporary password for the user if possible. you should now be able to log in and administer the server.

by default, parsav binds to [::1]:10917. if you want to change this (to run it on a different port, or make it directly accessible to other servers on the network), you can use the command `parsav conf set bind <address>`, where `address` is a binding specification like `0.0.0.0:80`. it is recommended, however, that `parsavd` be kept accessible only from localhost, and that connections be forwarded to it from nginx, haproxy, or a similar reverse proxy. (this can also be changed with the online configuration UI)

### postgresql backend


a database will need to be created for `parsav`'s use before `parsav db init` will work. this can be accomplished with a command like `$ createdb parsav`. you'll also of course need to set up some way for `parsavd` to authenticate itself to `postgres`. peer auth is the most secure option, and this is what you should use if postgres and `parsavd` are running on the same box. specify the database name to the backend the usual way, with a clause like `dbname=parsav` in your connection string.


the postgresql backend has some extra features that enable it to be integrated with existing authentication databases you may have. when you initialize the database, a table `parsav_auth` will be created to hold the credentials of the instance users and the authentication mode will be set to "managed", which will enable parsav's built-in credential administration tools. if you would prefer to use your own source of credentials, you'll need to set parsav to "unmanaged" mode with the command `parsav be pgsql setup-auth unmanaged`.

this command will reconfigure `parsav` and remove the `parsav_auth` table, making room for you to create a view with the same name.  if you want to go back to managed mode at any time, just run `parsav be psql setup-auth managed`; just be aware that this will delete your auth view!

`parsav_auth` has the following schema:

    create table parsav_auth (
		aid bigint primary key,
		uid bigint,
		newname text,
................................................................................
		restrict text[],
		netmask cidr,
		blacklist bool
    )

`aid` is a unique value identifying the authentication method. it must be deterministic -- values based on time of creation or a hash of `uid`+`kind`+`cred` are ideal. `uid` is the identifier of the user the row specifies credentials for. `kind` is a string indicating the credential type, and `cred` is the content of that credential.for the meaning of these fields and use of this structure, see **authentication** below.


in the most basic case, an authentication record would be something like `{uid = 123, kind = "pw-sha512", cred = "\x12bf90…a10e"::bytea}`. but `parsav` is not restricted to username-password authentication, and in addition to various hashing styles, it also will support more esoteric forms of authentcation. any individual user can have as many auth rows as she likes. there is also a `restrict` field, which is normally null, but can be specified in order to restrict a particular credential to certain operations, such as posting tweets or updating a bio. `blacklist` indicates that any attempt to authenticate that matches this row will be denied, regardless of whether it matches other rows. if `netmask` is present, this authentication will only succeed if it comes from the specified IP mask.

`uid` can also be `0` (emphatically *not* null, which causes the rule to match any user!), indicating that there is not yet a record in `parsav_actors` for this account. if this is the case, `name` must contain the handle of the account to be created when someone attempts to log in with this credential. whether `name` is used in the authentication process depends on whether the authentication method accepts a username. all rows with the same `uid` *must* have the same `name`.

## invoking
the build process generates two binaries, `parsav` and `parsavd`. `parsav` is a driver tool that can be used to set up and start a `parsav` instance, as well as administer it from the command line. it accesses databases directly and uses the same backend configuration file as parsav, but can also send IPC messages directly to running `parsavd` instances.

as a convenience, the `parsav start` command can be used to start and daemonize a `parsav` instance. additionally, the `-l` option to `parsav start` can be used to redirect `parsavd`'s logging output to a file; without `-l`, logging output will be discarded and can be viewed only by connecting to the running instance with `parsav attach`. `parsav start` passes its arguments on to `parsavd`; you can use this to pass options by separating `parsav`'s arguments from `parsavd`'s with `--`. if you launch an instance with `parsav start -- -i chungus`, you can then stop that instance with `parsav -i chungus stop`. `parsav stop` can be used on its own if only one `parsavd` instance is running; otherwise, `parsav -a stop` will cleanly terminate all running instances.

you generally should not invoke `parsavd` directly except for debugging purposes, or in the context of an init daemon (particularly systemd). if you launch `parsavd` directly it will not fork to the background. 

## authentication 
below is a full list of authentication types we intend/hope to one day support. contributors should consider this a to-do list. a checked box indicates the scheme has been implemented.

* ☑ pw-sha{512,384,256,224}: an ordinary password, hashed with the appropriate algorithm
* ☐ pw-{sha1,md5,clear} (insecure, must be manually enabled at compile time with the config variable `parsav_let_me_be_a_dumbass="i know what i'm doing"`)
* ☐ pw-pbkdf2-hmac-sha{…}: a password hashed with the Password-Based Key Derivation Function 2 instead of plain SHA2
* ☐ pw-extern-ldap: try to authenticate by binding against an LDAP server
* ☐ pw-extern-cyrus: try to authenticate against saslauthd
................................................................................
* ☐ api-digest-sha{…}: a value that can be hashed with the current epoch to derive a temporary access key without logging in. these are used for API calls, sent in the header `X-API-Key`.
* ☐ otp-time-sha1: a TOTP PSK: the first two bytes represent the step, the third byte the OTP length, and the remaining ten bytes the secret key
* ☐ tls-cert-fp: a fingerprint of a client certificate
* ☐ tls-cert-ca: a value of the form `fp/key=value` where a client certificate with the property `key=value` (e.g. `uid=cyberlord19`) signed by a certificate authority matching the given fingerprint `fp` can authenticate the user
* ☐ challenge-rsa-sha256: an RSA public key. the user is presented with a challenge and must sign it with the corresponding private key using SHA256.
* ☐ challenge-ecc-sha256: a Curve25519 public key. the user is presented with a challenge and must sign it with the corresponding private key using SHA256.
* ☐ challenge-ecc448-sha256: a Curve448 public key. the user is presented with a challenge and must sign it with the corresponding private key using SHA256.
* ☑ trust: authentication always succeeds (or fails, if blacklisted). only use in combination with netmask!!!

## legal

parsav is released under the terms of the EUPL v1.2. copies of this license are included in the repository. by contributing any intellectual property to this project, you reassign ownership and all attendant rights over that intellectual property to the current maintainer. this is to ensure that the project can be relicensed without difficulty in the unlikely event that it is necessary.

## code of conduct

when hacking on `parsav`, it is absolutely mandatory to wear a wizard hat and burgundy silk summoning cloak. this code of conduct is enforced capriciously by the Fair Folk, and violations are punishable by dancing hex.

## future direction

parsav needs more storage backends, as it currently supports only postgres. some possibilities, in order of priority, are:

* plain text/filesystem storage
* lmdb

Modified parsav.t from [1c10e6f8f8] to [d1470e4b10].

11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
...
175
176
177
178
179
180
181



182
183
184
185
186


187
188
189
190
191
192
193
194
...
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
...
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
403

404
405
406
407
408

409
410
411
412
413
414
415
...
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
			local path = {}
			for m in l:gmatch('([^:]+)') do path[#path+1]=m end
			local tgt = lib
			for i=1,#path-1 do
				if tgt[path[i]] == nil then tgt[path[i]] = {} end
				tgt = tgt[path[i]]
			end
			tgt[path[#path]] = terralib.loadfile(l:gsub(':','/') .. '.t')()
		end
	end;
	loadlib = function(name,hdr)
		local p = config.pkg[name]
		-- for _,v in pairs(p.dylibs) do
		-- 	terralib.linklibrary(p.libdir .. '/' .. v)
		-- end
................................................................................
	else -- print time since last msg
		var dfs = arrayof(int8, 0x30 + diff/10, 0x30 + diff%10, 0x20, 0)
		[ lib.emit(false, 2, ' \27[36m+', `&dfs[0]) ]
	end
end

local defrep = function(level,n,code)



	return macro(function(...)
		local fn = (...).filename
		local ln = tostring((...).linenumber)
		local dbgtag = string.format('\27[35m · \27[34m%s:\27[1m%s\27[m\n', fn,ln)
		local q = lib.emit(level < 3 and true or dbgtag, 2, noise_header(code,n), ...)


		return quote if noise >= level then timehdr(); [q] end end
	end);
end
lib.dbg = defrep(3,'debug', '32')
lib.report = defrep(2,'info', '35')
lib.warn = defrep(1,'warn', '33')
lib.bail = macro(function(...)
	local q = lib.emit(true, 2, noise_header('31','fatal'), ...)
................................................................................
lib.md = lib.loadlib('mbedtls','mbedtls/md.h')
lib.b64 = lib.loadlib('mbedtls','mbedtls/base64.h')
lib.net = lib.loadlib('mongoose','mongoose.h')
lib.pq = lib.loadlib('libpq','libpq-fe.h')

lib.load {
	'mem',  'math', 'str', 'file', 'crypt';
	'http', 'session', 'tpl', 'store';

	'smackdown'; -- md-alike parser
}

local be = {}
for _, b in pairs(config.backends) do
	be[#be+1] = terralib.loadfile('backend/' .. b .. '.t')()
................................................................................
	local t = lib.tpl.mk { body = v, id = 'view/'..k }
	data.view[k] = t
end

lib.load {
	'srv';
	'render:nav';

	'render:login';
	'render:profile';

	'render:compose';
	'render:tweet';
	'render:userpage';
	'render:timeline';

	'render:docpage';



	'route';
}

do
	local p = string.format('parsav: %s\nbuilt on %s\n', config.build.str, config.build.when)
	terra version() lib.io.send(1, p, [#p]) end
end

terra noise_init()
	starttime = lib.osclock.time(nil)
	lastnoisetime = 0
	var n = lib.proc.getenv('parsav_noise')
	if n ~= nil then
		if n[0] >= 0x30 and n[0] <= 0x39 and n[1] == 0 then
			noise = n[0] - 0x30
			return
		end
	end
	noise = 1
end


local options = lib.cmdparse {
	version = {'V', 'display information about the binary build and exit'};

	quiet = {'q', 'do not print to standard out'};
	help = {'h', 'display this list'};
	backend_file = {'b', 'init from specified backend file', 1};
	static_dir = {'S', 'directory with overrides for static content', 1};
	builtin_data = {'B', 'do not load static content overrides at runtime under any circumstances'};

}


local static_setup = quote end
local mapin = quote end
local odir = symbol(rawstring)
local pathbuf = symbol(lib.str.acc)
................................................................................
	[static_setup]
	if mode.builtin_data then return end

	var [odir] = lib.proc.getenv('parsav_override_dir')
	if mode.static_dir ~= nil then
		odir=@mode.static_dir
	end
	if odir == nil then return end





	var [pathbuf] defer pathbuf:free()
	pathbuf:compose(odir,'/')
	[mapin]
end

terra entry(argc: int, argv: &rawstring): int
	if argc < 1 then lib.bail('bad invocation!') end

	noise_init()
	[lib.init]

	-- shut mongoose the fuck up
	lib.net.mg_log_set_callback([terra(msg: &opaque, sz: int, u: &opaque) end], nil)
	var srv: lib.srv.overlord

	do var mode: options
		mode:parse(argc,argv) defer mode:free()
		static_init(&mode)
		if mode.version then version() return 0 end
		if mode.help then
			lib.io.send(1,  [options.helptxt], [#options.helptxt])
			return 0
		end
		var cnf: rawstring
		if mode.backend_file ~= nil
			then cnf = @mode.backend_file
			else cnf = lib.proc.getenv('parsav_backend_file')
		end
		if cnf == nil then cnf = "backend.conf" end

		srv:start(cnf)

	end

	lib.report('listening for requests')
	while true do
		srv:poll()
	end
	srv:shutdown()

	return 0
end


local bflag = function(long,short)
	if short and util.has(buildopts, short) then return true end
	if long and util.has(buildopts, long) then return true end
	return false
end

if bflag('dump-config','C') then
	print(util.dump(config))
	os.exit(0)
end

local holler = print
local out = config.exe and 'parsav' or ('parsav.' .. config.outform)

local linkargs = {}






if bflag('quiet','q') then holler = function() end end
if bflag('asan','s') then linkargs[#linkargs+1] = '-fsanitize=address' end
if bflag('lsan','S') then linkargs[#linkargs+1] = '-fsanitize=leak' end



if config.posix then
	linkargs[#linkargs+1] = '-pthread'
end
for _,p in pairs(config.pkg) do util.append(linkargs, p.linkargs) end
holler('linking with args',util.dump(linkargs))
terralib.saveobj(out, {
		main = entry

	},
	linkargs,
	config.tgttrip and terralib.newtarget {
		Triple = config.tgttrip;
		CPU = config.tgtcpu;
		FloatABIHard = config.tgthf;
	} or nil)







|







 







>
>
>





>
>
|







 







|







 







>


>




>

>
>
>








|









|

>



>


|
|
|
>







 







|
>
>
>
>






|


|











|







|

|
>










>













|
>

>
>
>
>
>





>
>

|

<

<
<
>
|
|
<
<
<
<
<
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
...
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
...
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
...
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
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
...
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
541





			local path = {}
			for m in l:gmatch('([^:]+)') do path[#path+1]=m end
			local tgt = lib
			for i=1,#path-1 do
				if tgt[path[i]] == nil then tgt[path[i]] = {} end
				tgt = tgt[path[i]]
			end
			tgt[path[#path]:gsub('-','_')] = terralib.loadfile(l:gsub(':','/') .. '.t')()
		end
	end;
	loadlib = function(name,hdr)
		local p = config.pkg[name]
		-- for _,v in pairs(p.dylibs) do
		-- 	terralib.linklibrary(p.libdir .. '/' .. v)
		-- end
................................................................................
	else -- print time since last msg
		var dfs = arrayof(int8, 0x30 + diff/10, 0x30 + diff%10, 0x20, 0)
		[ lib.emit(false, 2, ' \27[36m+', `&dfs[0]) ]
	end
end

local defrep = function(level,n,code)
	if level >= 3 and config.debug == false then
		return macro(function(...) return {} end)
	end
	return macro(function(...)
		local fn = (...).filename
		local ln = tostring((...).linenumber)
		local dbgtag = string.format('\27[35m · \27[34m%s:\27[1m%s\27[m\n', fn,ln)
		local q = lib.emit(level < 3 and true or dbgtag, 2, noise_header(code,n), ...)
		return quote
		--lib.io.fmt(['attempting to emit at ' .. fn..':'..ln.. '\n'])
		if noise >= level then timehdr(); [q] end end
	end);
end
lib.dbg = defrep(3,'debug', '32')
lib.report = defrep(2,'info', '35')
lib.warn = defrep(1,'warn', '33')
lib.bail = macro(function(...)
	local q = lib.emit(true, 2, noise_header('31','fatal'), ...)
................................................................................
lib.md = lib.loadlib('mbedtls','mbedtls/md.h')
lib.b64 = lib.loadlib('mbedtls','mbedtls/base64.h')
lib.net = lib.loadlib('mongoose','mongoose.h')
lib.pq = lib.loadlib('libpq','libpq-fe.h')

lib.load {
	'mem',  'math', 'str', 'file', 'crypt';
	'http', 'html', 'session', 'tpl', 'store';

	'smackdown'; -- md-alike parser
}

local be = {}
for _, b in pairs(config.backends) do
	be[#be+1] = terralib.loadfile('backend/' .. b .. '.t')()
................................................................................
	local t = lib.tpl.mk { body = v, id = 'view/'..k }
	data.view[k] = t
end

lib.load {
	'srv';
	'render:nav';
	'render:nym';
	'render:login';
	'render:profile';

	'render:compose';
	'render:tweet';
	'render:userpage';
	'render:timeline';

	'render:docpage';

	'render:conf:profile';
	'render:conf';
	'route';
}

do
	local p = string.format('parsav: %s\nbuilt on %s\n', config.build.str, config.build.when)
	terra version() lib.io.send(1, p, [#p]) end
end

terra lib.noise_init(default_level: uint)
	starttime = lib.osclock.time(nil)
	lastnoisetime = 0
	var n = lib.proc.getenv('parsav_noise')
	if n ~= nil then
		if n[0] >= 0x30 and n[0] <= 0x39 and n[1] == 0 then
			noise = n[0] - 0x30
			return
		end
	end
	noise = default_level
end
lib.load{'mgtool'}

local options = lib.cmdparse {
	version = {'V', 'display information about the binary build and exit'};
	verbose = {'v', 'increase logging verbosity', inc=1};
	quiet = {'q', 'do not print to standard out'};
	help = {'h', 'display this list'};
	backend_file = {'B', 'init from specified backend file', consume=1};
	static_dir = {'S', 'directory with overrides for static content', consume=1};
	builtin_data = {'D', 'do not load static content overrides at runtime under any circumstances'};
	instance = {'i', 'set an instance name to make it easier to control multiple daemons', consume = 1};
}


local static_setup = quote end
local mapin = quote end
local odir = symbol(rawstring)
local pathbuf = symbol(lib.str.acc)
................................................................................
	[static_setup]
	if mode.builtin_data then return end

	var [odir] = lib.proc.getenv('parsav_override_dir')
	if mode.static_dir ~= nil then
		odir=@mode.static_dir
	end
	if odir == nil then [
		config.prefix_static and quote
			odir = [config.prefix_static]
		end or quote return end
	] end

	var [pathbuf] defer pathbuf:free()
	pathbuf:compose(odir,'/')
	[mapin]
end

local terra entry_daemon(argc: int, argv: &rawstring): int
	if argc < 1 then lib.bail('bad invocation!') end

	lib.noise_init(1)
	[lib.init]

	-- shut mongoose the fuck up
	lib.net.mg_log_set_callback([terra(msg: &opaque, sz: int, u: &opaque) end], nil)
	var srv: lib.srv.overlord

	do var mode: options
		mode:parse(argc,argv) defer mode:free()
		static_init(&mode)
		if mode.version then version() return 0 end
		if mode.help then
			[ lib.emit(true, 1, 'usage: ',`argv[0],' ', options.helptxt.flags, ' [<args>…]', options.helptxt.opts) ]
			return 0
		end
		var cnf: rawstring
		if mode.backend_file ~= nil
			then cnf = @mode.backend_file
			else cnf = lib.proc.getenv('parsav_backend_file')
		end
		if cnf == nil then cnf = [config.prefix_conf .. "backend.conf"] end

		srv:setup(cnf)
		srv:start(lib.trn(mode.instance ~= nil, @mode.instance, nil))
	end

	lib.report('listening for requests')
	while true do
		srv:poll()
	end
	srv:shutdown()

	return 0
end


local bflag = function(long,short)
	if short and util.has(buildopts, short) then return true end
	if long and util.has(buildopts, long) then return true end
	return false
end

if bflag('dump-config','C') then
	print(util.dump(config))
	os.exit(0)
end

local holler = print
local suffix = config.exe and '' or ('.'..config.outform)
local out = 'parsavd' .. suffix
local linkargs = {}
local target = config.tgttrip and terralib.newtarget {
	Triple = config.tgttrip;
	CPU = config.tgtcpu;
	FloatABIHard = config.tgthf;
} or nil

if bflag('quiet','q') then holler = function() end end
if bflag('asan','s') then linkargs[#linkargs+1] = '-fsanitize=address' end
if bflag('lsan','S') then linkargs[#linkargs+1] = '-fsanitize=leak' end

for _,p in pairs(config.pkg) do util.append(linkargs, p.linkargs) end
local linkargs_d = linkargs -- controller is not multithreaded
if config.posix then
	linkargs_d[#linkargs_d+1] = '-pthread'
end

holler('linking with args',util.dump(linkargs))



terralib.saveobj('parsavd'..suffix, { main = entry_daemon }, linkargs_d, target)
terralib.saveobj('parsav' ..suffix, { main = lib.mgtool }, linkargs, target)





Modified render/compose.t from [0685338a7e] to [cb3a66bab9].

4
5
6
7
8
9
10

11
12
13
14
15
16
17
	var target, tgtlen = co:getv('to')
	var form: data.view.compose
	if edit == nil then
		form = data.view.compose {
			content = lib.coalesce(target, '');
			acl = lib.trn(target == nil, 'all', 'mentioned'); -- TODO default acl setting?
			handle = co.who.handle;

		}
	end
	var cotxt = form:tostr() defer cotxt:free()

	var doc = data.view.docskel {
		instance = co.srv.cfg.instance;
		title = lib.str.plit 'compose';







>







4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
	var target, tgtlen = co:getv('to')
	var form: data.view.compose
	if edit == nil then
		form = data.view.compose {
			content = lib.coalesce(target, '');
			acl = lib.trn(target == nil, 'all', 'mentioned'); -- TODO default acl setting?
			handle = co.who.handle;
			circles = ''; -- TODO: list user's circles, rooms, and saved aclexps
		}
	end
	var cotxt = form:tostr() defer cotxt:free()

	var doc = data.view.docskel {
		instance = co.srv.cfg.instance;
		title = lib.str.plit 'compose';

Added render/conf.t version [6e08f785f6].





















































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
-- vim: ft=terra
local pstr = lib.mem.ptr(int8)
local pref = lib.mem.ref(int8)

local mappings = {
	{url = 'profile', title = 'account profile', render = 'profile'};
	{url = 'avi', title = 'avatar', render = 'avatar'};
	{url = 'sec', title = 'security', render = 'sec'};
	{url = 'rel', title = 'relationships', render = 'rel'};
	{url = 'qnt', title = 'quarantine', render = 'quarantine'};
	{url = 'acl', title = 'access control shortcuts', render = 'acl'};
	{url = 'rooms', title = 'chatrooms', render = 'rooms'};
	{url = 'circles', title = 'circles', render = 'circles'};

	{url = 'srv', title = 'server settings', render = 'srv'};
	{url = 'brand', title = 'instance branding', render = 'rebrand'};
	{url = 'censor', title = 'censorship &amp; badthink suppression', render = 'rebrand'};
	{url = 'users', title = 'user accounting', render = 'users'};

}

local path = symbol(lib.mem.ptr(pref))
local co = symbol(&lib.srv.convo)
local panel = symbol(pstr)
local invoker = quote co:complain(404,'not found','no such control panel is available in this version of parsav') end

for i, m in ipairs(mappings) do
	if lib.render.conf[m.render] then
		invoker = quote
			if path(1):cmp(lib.str.lit([m.url])) then
				var body = [lib.render.conf[m.render]] (co, path)
				var a: lib.str.acc a:init(body.ct+48)
				a:lpush(['<h1>' .. m.title .. '</h1>']):ppush(body)
				panel = a:finalize()
				body:free()
			else [invoker] end
		end
	end
end

local terra 
render_conf([co], [path])
	var menu: lib.str.acc menu:init(64):lpush('<hr>') defer menu:free()

	-- build menu
	do var p = co.who.rights.powers
		if p.config() then menu:lpush '<a href="/conf/srv">server settings</a>' end
		if p.rebrand() then menu:lpush '<a href="/conf/brand">instance branding</a>' end
		if p.censor() then menu:lpush '<a href="/conf/censor">badthink alerts</a>' end
		if p:affect_users() then menu:lpush '<a href="/conf/users">users</a>' end
	end

	-- select the appropriate panel
	var [panel] = pstr { ptr = ''; ct = 0 }
	if path.ct >= 2 then [invoker] end

	-- avoid the hr if we didn't add any elements
	var mptr = pstr { ptr = menu.buf, ct = menu.sz }
	if menu.sz <= 4 then mptr.ct = 0 end -- 🙄
	var pg = data.view.conf {
		menu = mptr;
		panel = panel;
	}

	var pgt = pg:tostr() defer pgt:free()
	co:stdpage([lib.srv.convo.page] {
		title = 'configure'; body = pgt;
		class = lib.str.plit 'conf';
	})

	if panel.ct ~= 0 then panel:free() end
end

return render_conf

Added render/conf/profile.t version [248ab207d4].









































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-- vim: ft=terra
local pstr = lib.mem.ptr(int8)
local pref = lib.mem.ref(int8)

local terra cs(s: rawstring)
	return pstr { ptr = s, ct = lib.str.sz(s) }
end

local terra 
render_conf_profile(co: &lib.srv.convo, path: lib.mem.ptr(pref)): pstr

	var c = data.view.conf_profile {
		handle = cs(co.who.handle);
		nym = cs(lib.coalesce(co.who.nym,''));
		bio = cs(lib.coalesce(co.who.bio,''));
	}
	return c:tostr()
end

return render_conf_profile

Modified render/login.t from [671b92715b] to [dd5c50c3e9].

31
32
33
34
35
36
37

38
39
40
41

42
43
44
45

46
47
48
49
50
51
52
			handle = user.handle;
			name = lib.coalesce(user.nym, user.handle);
		}
		if creds.pw() then
			ch.challenge = P'enter the password associated with your account'
			ch.label = P'password'
			ch.method = P'pw'

		elseif creds.otp() then
			ch.challenge = P'enter a valid one-time password for your account'
			ch.label = P'OTP code'
			ch.method = P'otp'

		elseif creds.challenge() then
			ch.challenge = P'sign the challenge token: <code>...</code>'
			ch.label = P'digest'
			ch.method = P'challenge'

		else
			co:complain(500,'login failure','unknown login method')
			return
		end

		doc.body = ch:tostr()
	else







>




>




>







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
			handle = user.handle;
			name = lib.coalesce(user.nym, user.handle);
		}
		if creds.pw() then
			ch.challenge = P'enter the password associated with your account'
			ch.label = P'password'
			ch.method = P'pw'
			ch.auto = P'current-password';
		elseif creds.otp() then
			ch.challenge = P'enter a valid one-time password for your account'
			ch.label = P'OTP code'
			ch.method = P'otp'
			ch.auto = P'one-time-code';
		elseif creds.challenge() then
			ch.challenge = P'sign the challenge token: <code>...</code>'
			ch.label = P'digest'
			ch.method = P'challenge'
			ch.auto = P'one-time-code';
		else
			co:complain(500,'login failure','unknown login method')
			return
		end

		doc.body = ch:tostr()
	else

Added render/nym.t version [89e574dd98].











































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- vim: ft=terra
local pstr = lib.mem.ptr(int8)

local terra 
render_nym(who: &lib.store.actor, scope: uint64)
	var n: lib.str.acc n:init(128)
	if who.nym ~= nil and who.nym[0] ~= 0 then
		n:compose('<span class="nym">',who.nym,'</span> [<span class="handle">',
			who.xid,'</span>]')
	else n:compose('<span class="handle">',who.xid,'</span>') end

	if who.epithet ~= nil then
		n:lpush(' <span class="epithet">'):push(who.epithet,0):lpush('</span>')
	end
	
	-- TODO: if scope == chat room then lookup titles in room member db

	return n:finalize()
end

return render_nym

Modified render/profile.t from [efe49adad0] to [03b39adc21].

28
29
30
31
32
33
34
35





36
37
38


39
40
41
42
43
44
45
..
47
48
49
50
51
52
53

54
55
56
57

	var strfbuf: int8[28*4]
	var stats = co.srv:actor_stats(actor.id)
		var sn_posts     = cs(lib.math.decstr_friendly(stats.posts, &strfbuf[ [strfbuf.type.N - 1] ]))
		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 profile = data.view.profile {
		nym = cs(lib.coalesce(actor.nym, actor.handle));
		bio = cs(lib.coalesce(actor.bio, "<em>tall, dark, and mysterious</em>"));


		xid = cs(actor.xid);
		avatar = lib.trn(actor.origin == 0, pstr{ptr=avistr.buf,ct=avistr.sz},
			cs(lib.coalesce(actor.avatar, '/s/default-avatar.webp')));

		nposts = sn_posts, nfollows = sn_follows;
		nfollowers = sn_followers, nmutuals = sn_mutuals;
		tweetday = cs(timestr);
................................................................................

		auxbtn = auxp;
	}

	var ret = profile:tostr()
	if actor.origin == 0 then avistr:free() end
	if not (co.aid ~= 0 and co.who.id == actor.id) then auxp:free() end

	return ret
end

return render_profile







<
>
>
>
>
>

<
<
>
>







 







>




28
29
30
31
32
33
34

35
36
37
38
39
40


41
42
43
44
45
46
47
48
49
..
51
52
53
54
55
56
57
58
59
60
61
62

	var strfbuf: int8[28*4]
	var stats = co.srv:actor_stats(actor.id)
		var sn_posts     = cs(lib.math.decstr_friendly(stats.posts, &strfbuf[ [strfbuf.type.N - 1] ]))
		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.html.sanitize(cs(actor.bio), false)
	end
	var fullname = lib.render.nym(actor,0) defer fullname:free()
	var profile = data.view.profile {


		nym = fullname;
		bio = bio;
		xid = cs(actor.xid);
		avatar = lib.trn(actor.origin == 0, pstr{ptr=avistr.buf,ct=avistr.sz},
			cs(lib.coalesce(actor.avatar, '/s/default-avatar.webp')));

		nposts = sn_posts, nfollows = sn_follows;
		nfollowers = sn_followers, nmutuals = sn_mutuals;
		tweetday = cs(timestr);
................................................................................

		auxbtn = auxp;
	}

	var ret = profile:tostr()
	if actor.origin == 0 then avistr:free() end
	if not (co.aid ~= 0 and co.who.id == actor.id) then auxp:free() end
	if actor.bio ~= nil then bio:free() end
	return ret
end

return render_profile

Modified render/tweet.t from [00c7b6fd89] to [ac0f8e680f].

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
	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 tpl = data.view.tweet {
		text = bhtml;
		subject = cs(lib.coalesce(p.subject,''));
		nym = cs(lib.coalesce(author.nym, author.handle));
		xid = cs(author.xid);
		when = cs(&timestr[0]);
		avatar = cs(lib.trn(author.origin == 0, avistr.buf,
			lib.coalesce(author.avatar, '/s/default-avatar.webp')));
		acctlink = cs(author.xid);
		permalink = permalink:finalize();
	}
	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







|



|
<












22
23
24
25
26
27
28
29
30
31
32
33

34
35
36
37
38
39
40
41
42
43
44
45
	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) defer fullname:free()
	var tpl = data.view.tweet {
		text = bhtml;
		subject = cs(lib.coalesce(p.subject,''));
		nym = fullname;

		when = cs(&timestr[0]);
		avatar = cs(lib.trn(author.origin == 0, avistr.buf,
			lib.coalesce(author.avatar, '/s/default-avatar.webp')));
		acctlink = cs(author.xid);
		permalink = permalink:finalize();
	}
	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

Modified route.t from [d6af263481] to [4fbd6ed0a5].

81
82
83
84
85
86
87
88
89
90

91
92
93
94
95
96
97
...
169
170
171
172
173
174
175




176
177
178
179
180
181
182
...
262
263
264
265
266
267
268



269
270
271
272
273
274

275
		var fakeactor: lib.store.actor
		if act.ptr == nil then
			-- the user is known to us but has not yet claimed an
			-- account on the server. create a template for the
			-- account that will be created once they log in
			fakeact = true
			fakeactor = lib.store.actor {
				id = 0, handle = usn, nym = usn;
				origin = 0, bio = nil;
				key = [lib.mem.ptr(uint8)] {ptr=nil, ct=0}

			}
			act.ct = 1
			act.ptr = &fakeactor
			act.ptr.rights = lib.store.rights_default()
		end
		if am == nil then
			-- pick an auth method
................................................................................
		lib.render.docpage(co,path(1))
	elseif path.ct == 1 then
		lib.render.docpage(co, rstring.null())
	else
		co:complain(404, 'no such documentation', 'invalid documentation URL')
	end
end





do local branches = quote end
	local filename, flen = symbol(&int8), symbol(intptr)
	local page = symbol(lib.http.page)
	local send = label()
	local storage = data.stmap
	for i,e in ipairs(config.embeds) do local id,mime = e[1],e[2]
................................................................................
		if path.ptr[0]:cmp(lib.str.lit('user')) then
			http.actor_profile_uid(co, path, meth)
		elseif path.ptr[0]:cmp(lib.str.lit('tl')) then
			http.timeline(co, path)
		elseif path.ptr[0]:cmp(lib.str.lit('doc')) then
			if meth ~= method.get and meth ~= method.head then goto wrongmeth end
			http.documentation(co, path)



		else goto notfound end
		return
	end

	::wrongmeth:: co:complain(405, 'method not allowed', 'that method is not meaningful for this endpoint') do return end
	::notfound:: co:complain(404, 'not found', 'no such resource available') do return end

end







|

|
>







 







>
>
>
>







 







>
>
>






>

81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
...
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
...
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
		var fakeactor: lib.store.actor
		if act.ptr == nil then
			-- the user is known to us but has not yet claimed an
			-- account on the server. create a template for the
			-- account that will be created once they log in
			fakeact = true
			fakeactor = lib.store.actor {
				id = 0, handle = usn, nym = nil;
				origin = 0, bio = nil;
				key = [lib.mem.ptr(uint8)] {ptr=nil, ct=0};
				epithet = nil;
			}
			act.ct = 1
			act.ptr = &fakeactor
			act.ptr.rights = lib.store.rights_default()
		end
		if am == nil then
			-- pick an auth method
................................................................................
		lib.render.docpage(co,path(1))
	elseif path.ct == 1 then
		lib.render.docpage(co, rstring.null())
	else
		co:complain(404, 'no such documentation', 'invalid documentation URL')
	end
end

terra http.configure(co: &lib.srv.convo, path: hpath)
	lib.render.conf(co,path)
end

do local branches = quote end
	local filename, flen = symbol(&int8), symbol(intptr)
	local page = symbol(lib.http.page)
	local send = label()
	local storage = data.stmap
	for i,e in ipairs(config.embeds) do local id,mime = e[1],e[2]
................................................................................
		if path.ptr[0]:cmp(lib.str.lit('user')) then
			http.actor_profile_uid(co, path, meth)
		elseif path.ptr[0]:cmp(lib.str.lit('tl')) then
			http.timeline(co, path)
		elseif path.ptr[0]:cmp(lib.str.lit('doc')) then
			if meth ~= method.get and meth ~= method.head then goto wrongmeth end
			http.documentation(co, path)
		elseif path.ptr[0]:cmp(lib.str.lit('conf')) then
			if co.aid == 0 then goto unauth end
			http.configure(co,path)
		else goto notfound end
		return
	end

	::wrongmeth:: co:complain(405, 'method not allowed', 'that method is not meaningful for this endpoint') do return end
	::notfound:: co:complain(404, 'not found', 'no such resource available') do return end
	::unauth:: co:complain(401, 'unauthorized', 'this content is not available at your clearance level') do return end
end

Modified smackdown.t from [896438e021] to [c51eb8a6b2].

52
53
54
55
56
57
58
59
60



61
62
63
64
65
66
67
...
112
113
114
115
116
117
118

119
120
121
122
123
124
125
local terra scanline_wordend(l: rawstring, max: intptr, n: rawstring, nc: intptr)
	var sl = scanline(l,max,n,nc)
	if sl == nil then return nil else sl = sl + nc end
	if sl >= l+max or isws(@sl) then return sl-nc end
	return nil
end

terra m.html(md: pstr)
	if md.ct == 0 then md.ct = lib.str.sz(md.ptr) end



	var styled: lib.str.acc styled:init(md.ct)

	do var i = 0 while i < md.ct do
		var wordstart = (i == 0 or isws(md.ptr[i-1]))
		var wordend = (i == md.ct - 1 or isws(md.ptr[i+1]))

		var here = md.ptr + i
................................................................................
				goto skip
			end
		end

		::fallback::styled:push(here,1) -- :/
		i = i + 1
	::skip::end end


	-- we make two passes: the first detects and transforms inline elements,
	-- the second carries out block-level organization

	var html: lib.str.acc html:init(styled.sz)
	var s = state {
		segt = segt.none;







|
|
>
>
>







 







>







52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
...
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
local terra scanline_wordend(l: rawstring, max: intptr, n: rawstring, nc: intptr)
	var sl = scanline(l,max,n,nc)
	if sl == nil then return nil else sl = sl + nc end
	if sl >= l+max or isws(@sl) then return sl-nc end
	return nil
end

terra m.html(input: pstr)
	if input.ct == 0 then input.ct = lib.str.sz(input.ptr) end

	var md = lib.html.sanitize(input,false)

	var styled: lib.str.acc styled:init(md.ct)

	do var i = 0 while i < md.ct do
		var wordstart = (i == 0 or isws(md.ptr[i-1]))
		var wordend = (i == md.ct - 1 or isws(md.ptr[i+1]))

		var here = md.ptr + i
................................................................................
				goto skip
			end
		end

		::fallback::styled:push(here,1) -- :/
		i = i + 1
	::skip::end end
	md:free()

	-- we make two passes: the first detects and transforms inline elements,
	-- the second carries out block-level organization

	var html: lib.str.acc html:init(styled.sz)
	var s = state {
		segt = segt.none;

Modified srv.t from [8808dbd7a5] to [7234d58b59].

11
12
13
14
15
16
17

18
19
20
21
22
23
24
...
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
...
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592

593


594
595
596










597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
	pol_reg: bool
}
local struct srv {
	sources: lib.mem.ptr(lib.store.source)
	webmgr: lib.net.mg_mgr
	webcon: &lib.net.mg_connection
	cfg: cfgcache

}

terra cfgcache:free() -- :/
	self.secret:free()
	self.instance:free()
end

................................................................................
		if self.sources(i).backend ~= nil and
		   self.sources(i).backend.actor_auth_pw ~= nil then
			var aid,uid,newhnd = self.sources(i):actor_auth_pw(ip,user,pw)
			if aid ~= 0 then
				if uid == 0 then
					lib.dbg('new user just logged in, creating account entry')
					var kbuf: uint8[lib.crypt.const.maxdersz]
					var newkp = lib.crypt.genkp()
					var privsz = lib.crypt.der(false,&newkp,&kbuf[0])
					var na = lib.store.actor {
						id = 0; nym = nil; handle = newhnd.ptr;
						origin = 0; bio = nil; avatar = nil;
						knownsince = lib.osclock.time(nil);
						rights = lib.store.rights_default();
						title = nil, key = [lib.mem.ptr(uint8)] {
							ptr = &kbuf[0], ct = privsz
						};
					}
					var newuid: uint64
					if self.sources(i).backend.actor_create ~= nil then
						newuid = self.sources(i):actor_create(&na)
					else newuid = self:actor_create(&na) end

					if self.sources(i).backend.actor_auth_register_uid ~= nil then
						self.sources(i):actor_auth_register_uid(aid,newuid)
................................................................................
--9twh8y94i5c1qqr7hxu20fyd
terra cfgcache.methods.load :: {&cfgcache} -> {}
terra cfgcache:init(o: &srv)
	self.overlord = o
	self:load()
end

srv.methods.start = terra(self: &srv, befile: rawstring)
	cfg(self, befile)
	var success = false
	if self.sources.ct == 0 then lib.bail('no data sources specified') end
	for i=0,self.sources.ct do var src = self.sources.ptr + i
		lib.report('opening data source ', src.id.ptr, '(', src.backend.id, ')')
		src.handle = src.backend.open(src)
		if src.handle ~= nil then success = true end
	end
	if not success then
		lib.bail('could not connect to any data sources!')
	end




	self.cfg:init(self)

	var dbbind = self:conf_get('bind')










	var envbind = lib.proc.getenv('parsav_bind')
	var bind: rawstring
	if envbind ~= nil then
		bind = envbind
	elseif dbbind.ptr ~= nil then
		bind = dbbind.ptr
	else bind = '[::]:10917' end

	lib.report('binding to ', bind)
	lib.net.mg_mgr_init(&self.webmgr)
	self.webcon = lib.net.mg_http_listen(&self.webmgr, bind, handle.http, self)

	if dbbind.ptr ~= nil then dbbind:free() end
end

srv.methods.poll = terra(self: &srv)
	lib.net.mg_mgr_poll(&self.webmgr,1000)
end

srv.methods.shutdown = terra(self: &srv)
	lib.net.mg_mgr_free(&self.webmgr)
	for i=0,self.sources.ct do var src = self.sources.ptr + i
		lib.report('closing data source ', src.id.ptr, '(', src.backend.id, ')')
		src:close()
	end
	self.sources:free()
end







>







 







<
<
|
<
<
<
<
<
<
<
<







 







|











>

>
>

<

>
>
>
>
>
>
>
>
>
>






|








|



|







11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
...
540
541
542
543
544
545
546


547








548
549
550
551
552
553
554
...
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588

589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
	pol_reg: bool
}
local struct srv {
	sources: lib.mem.ptr(lib.store.source)
	webmgr: lib.net.mg_mgr
	webcon: &lib.net.mg_connection
	cfg: cfgcache
	id: rawstring
}

terra cfgcache:free() -- :/
	self.secret:free()
	self.instance:free()
end

................................................................................
		if self.sources(i).backend ~= nil and
		   self.sources(i).backend.actor_auth_pw ~= nil then
			var aid,uid,newhnd = self.sources(i):actor_auth_pw(ip,user,pw)
			if aid ~= 0 then
				if uid == 0 then
					lib.dbg('new user just logged in, creating account entry')
					var kbuf: uint8[lib.crypt.const.maxdersz]


					var na = lib.store.actor.mk(&kbuf[0])








					var newuid: uint64
					if self.sources(i).backend.actor_create ~= nil then
						newuid = self.sources(i):actor_create(&na)
					else newuid = self:actor_create(&na) end

					if self.sources(i).backend.actor_auth_register_uid ~= nil then
						self.sources(i):actor_auth_register_uid(aid,newuid)
................................................................................
--9twh8y94i5c1qqr7hxu20fyd
terra cfgcache.methods.load :: {&cfgcache} -> {}
terra cfgcache:init(o: &srv)
	self.overlord = o
	self:load()
end

terra srv:setup(befile: rawstring)
	cfg(self, befile)
	var success = false
	if self.sources.ct == 0 then lib.bail('no data sources specified') end
	for i=0,self.sources.ct do var src = self.sources.ptr + i
		lib.report('opening data source ', src.id.ptr, '(', src.backend.id, ')')
		src.handle = src.backend.open(src)
		if src.handle ~= nil then success = true end
	end
	if not success then
		lib.bail('could not connect to any data sources!')
	end
end

terra srv:start(iname: rawstring)
	self:conprep(lib.store.prepmode.full)
	self.cfg:init(self)

	var dbbind = self:conf_get('bind')
	if iname == nil then iname = lib.proc.getenv('parsav_instance') end
	if iname == nil then
		self.id = self.cfg.instance.ptr;
		-- let this leak -- it'll be needed for the lifetime of the process anyway
	else self.id = iname end 

	if iname ~= nil then
		lib.report('parsav instance "',iname,'" starting')
	end

	var envbind = lib.proc.getenv('parsav_bind')
	var bind: rawstring
	if envbind ~= nil then
		bind = envbind
	elseif dbbind.ptr ~= nil then
		bind = dbbind.ptr
	else bind = '[::1]:10917' end

	lib.report('binding to ', bind)
	lib.net.mg_mgr_init(&self.webmgr)
	self.webcon = lib.net.mg_http_listen(&self.webmgr, bind, handle.http, self)

	if dbbind.ptr ~= nil then dbbind:free() end
end

terra srv:poll()
	lib.net.mg_mgr_poll(&self.webmgr,1000)
end

terra srv:shutdown()
	lib.net.mg_mgr_free(&self.webmgr)
	for i=0,self.sources.ct do var src = self.sources.ptr + i
		lib.report('closing data source ', src.id.ptr, '(', src.backend.id, ')')
		src:close()
	end
	self.sources:free()
end

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

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
...
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
...
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
...
287
288
289
290
291
292
293

















294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
...
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
...
355
356
357
358
359
360
361

362



363
364
365
366
367
368
369
...
457
458
459
460
461
462
463

464
465
466
467
468
469
470
...
476
477
478
479
480
481
482

483
484
485
486
487
488
489
490
491







































	color: tone(25%);
	font-size: 14pt;
	margin: 0;
	padding: 0;
}
a[href] {
	color: tone(10%);
	text-decoration-color: adjust-color($color, $lightness: 10%, $alpha: -0.5);
	&:hover {
		color: white;
		text-shadow: 0 0 15px tone(20%);
		text-decoration-color: adjust-color($color, $lightness: 10%, $alpha: -0.1);
	}
}
a[href^="//"],
a[href^="http://"],
a[href^="https://"] { // external link
	&:hover::after {
		color: black;
................................................................................
		background-color: white;
	}
	&::after {
		content: "↗";
		display: inline-block;
		color: black;
		margin-left: 4pt;
		background-color: adjust-color($color, $lightness: 10%);
		padding: 0 4px;
		text-shadow: none;
		padding-right: 5px;
		vertical-align: baseline;
		font-size: 80%;
	}
}
................................................................................

%content {
	width: 8in;
	margin: auto;
}

%glow {
	box-shadow: 0 0 20px adjust-color($color, $alpha: -0.8);
}

%button {
	@extend %sans;
	font-size: 14pt;
	padding: 0.1in 0.2in;
	border: 1px solid black;
	color: adjust-color($color, $lightness: 25%);
	text-shadow: 1px 1px black;
	text-decoration: none;
	text-align: center;

	background: linear-gradient(to bottom,
		adjust-color($color, $lightness: -45%),
		adjust-color($color, $lightness: -50%) 15%,
		adjust-color($color, $lightness: -50%) 75%,
		adjust-color($color, $lightness: -55%)

	);
	&:hover, &:focus {
		@extend %glow;
		outline: none;
		color: adjust-color($color, $lightness: -55%);
		text-shadow: none;
		background: linear-gradient(to bottom,
			adjust-color($color, $lightness: -25%),
			adjust-color($color, $lightness: -30%) 15%,
			adjust-color($color, $lightness: -30%) 75%,
			adjust-color($color, $lightness: -35%)
		);
	}
	&:active {
		color: black;
		padding-bottom: calc(0.1in - 2px);
		padding-top: calc(0.1in + 2px);
		background: linear-gradient(to top,
			adjust-color($color, $lightness: -25%),
			adjust-color($color, $lightness: -30%) 15%,
			adjust-color($color, $lightness: -30%) 75%,
			adjust-color($color, $lightness: -35%)
		);
	}
}

button { @extend %button;
	&:first-of-type {
		@extend %button;
		color: white;
		box-shadow: inset 0 1px  adjust-color($color, $lightness: -25%),
		            inset 0 -1px adjust-color($color, $lightness: -50%);
		background: linear-gradient(to bottom,
			adjust-color($color, $lightness: -35%),
			adjust-color($color, $lightness: -40%) 15%,
			adjust-color($color, $lightness: -40%) 75%,
			adjust-color($color, $lightness: -45%)
		);
		&:hover, &:focus {
			box-shadow: inset 0 1px  adjust-color($color, $lightness: -15%),
						inset 0 -1px adjust-color($color, $lightness: -40%);
		}
		&:active {
			box-shadow: inset 0 1px  adjust-color($color, $lightness: -50%),
						inset 0 -1px adjust-color($color, $lightness: -25%);
			background: linear-gradient(to top,
				adjust-color($color, $lightness: -30%),
				adjust-color($color, $lightness: -35%) 15%,
				adjust-color($color, $lightness: -35%) 75%,
				adjust-color($color, $lightness: -40%)
			);
		}
	}
	&:hover { font-weight: bold; }
}

$grad-ui-focus: linear-gradient(to bottom,
	adjust-color($color, $lightness: -50%),
	adjust-color($color, $lightness: -35%)
);

input[type='text'], input[type='password'], textarea {
	@extend %serif;
	padding: 0.08in 0.1in;
	border: 1px solid black;
	background: linear-gradient(to bottom,
		adjust-color($color, $lightness: -55%),
		adjust-color($color, $lightness: -40%)
	);
	font-size: 16pt;
	color: adjust-color($color, $lightness: 25%);

	box-shadow: inset 0 0 20px -3px adjust-color($color, $lightness: -55%);
	&:focus {
		color: white;
		border-image: linear-gradient(to bottom,
			adjust-color($color, $lightness: -10%),
			adjust-color($color, $lightness: -30%)
		) 1 / 1px;
		background: $grad-ui-focus;
		outline: none;
		@extend %glow;
	}
}

@mixin glass {
	@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
		backdrop-filter: blur(40px);
		-webkit-backdrop-filter: blur(40px);
		background-color: adjust-color($color, $lightness: -53%, $alpha: -0.7);
	}
	@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
		background-color: adjust-color($color, $lightness: -53%, $alpha: -0.1);
	}
}

h1 { margin-top: 0 }

header {
	position: fixed;
................................................................................
	position: relative;
	min-height: calc(100vh - 1.1in);
	margin-top: 0;
	margin-bottom: 0;
	padding: 0 0.4in;
	padding-top: 1.1in;
	padding-bottom: 0.1in;
	background-color: adjust-color($color, $lightness: -45%, $alpha: 0.4);
	border: {
		left: 1px solid black;
		right: 1px solid black;
	}
}

div.profile {
................................................................................
			grid-column: 1 / 2;
			grid-row: 1 / 3;
			border: 1px solid black;
		}
		> .id {
			grid-column: 2 / 3;
			grid-row: 1 / 2;
			color: adjust-color($color, $lightness: 25%, $alpha: -0.4);
			> .nym {
				font-weight: bold;
				color: adjust-color($color, $lightness: 25%);
			}
			> .xid {
				color: adjust-color($color, $lightness: 20%, $alpha: -0.1);
				font-size: 80%;
				vertical-align: text-top;
			}
		}
		> .bio {
			grid-column: 2 / 3;
			grid-row: 2 / 3;
................................................................................
			display: block;
			height: 0.3in;
			width: 1px;
			border-left: 1px solid rgba(0,0,0,0.6);
		}
	}
}


















%box {
	margin: auto;
	border: 1px solid adjust-color($color, $lightness: -55%);
	border-bottom: 3px solid black;
	box-shadow: 0 0 1px black;
	border-image: linear-gradient(to bottom,
		adjust-color($color, $lightness: -40%),
		adjust-color($color, $lightness: -52%) 10%,
		adjust-color($color, $lightness: -55%) 90%,
		adjust-color($color, $lightness: -60%)
	) 1 / 1px;
	background: linear-gradient(to bottom,
		adjust-color($color, $lightness: -58%),
		adjust-color($color, $lightness: -55%) 10%,
		adjust-color($color, $lightness: -50%) 80%,
		adjust-color($color, $lightness: -45%)
	);
	// outline: 1px solid black;
}

body.error .message {
	@extend %box;
	width: 4in;
................................................................................
	> .msg {
		text-align: center;
		padding: 0.3in;
	}
	> .msg:first-child { padding-top: 0; }
	> .user {
		width: min-content; margin: auto;
		background: adjust-color($color, $lightness: -20%, $alpha: -0.3);
		border: 1px solid black;
		color: adjust-color($color, $lightness: -50%);
		padding: 0.1in;
		> img { width: 1in; height: 1in; border: 1px solid black; }
		> .name { @extend %serif; text-align: center; font-size: 130%; font-weight: bold; margin-top: 0.08in; }
	}
	>form {
		display: grid;
		grid-template-columns: 1fr 1fr;
................................................................................
	@extend %box;
	display: grid;
	grid-template-columns: 1.1in 2fr min-content 1fr;
	grid-template-rows: 1fr min-content;
	grid-gap: 2px;
	padding: 0.1in;
	> img { grid-column: 1/2; grid-row: 1/3; width: 1in; height: 1in;}

	> textarea { grid-column: 2/5; grid-row: 1/2; height: 3in;}



	> input[name="acl"] { grid-column: 2/3; grid-row: 2/3; }
	> button { grid-column: 4/5; grid-row: 2/3; }
	a.help[href] { margin-right: 0.05in }
}

a.help[href] {
	display: block;
................................................................................
		background: linear-gradient(to right, tone(-55%), transparent);
	}
	>.content {
		grid-column: 2/4; grid-row: 1/2;
		padding: 0.2in;
		@extend %serif;
		font-size: 110%;

	}
	> a[href].permalink {
		display: block;
		grid-column: 3/4; grid-row: 2/3;
		font-size: 80%;
		text-align: right;
		padding: 0.1in;
................................................................................

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

body.doc main {
	@extend %serif;

	li { margin-top: 0.05in; }
	li:first-child { margin-top: 0; }
	h1, h2, h3, h4, h5, h6 {
		background: linear-gradient(to right, tone(-50%), transparent);
		margin-left: -0.4in;
		padding-left: 0.2in;
		text-shadow: 0 2px 0 black;
	}
}














































|



|







 







|







 







|







|



>

<
|
|
|
>




|


|
|
|
|







|
|
|
|








|
|

|
|
|
|


|
|


|
|

|
|
|
|







|
|






|
<
<
<

<
>
|


|
<
<
<










|


|







 







|







 







|


|


|







 







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



|



|
|
|
|


|
|
|
|







 







|

|







 







>
|
>
>
>







 







>







 







>









>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
...
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
...
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
...
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
...
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
...
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
...
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
...
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
541
542
543
544
545
546
547
548
	color: tone(25%);
	font-size: 14pt;
	margin: 0;
	padding: 0;
}
a[href] {
	color: tone(10%);
	text-decoration-color: tone(10%,-0.5);
	&:hover {
		color: white;
		text-shadow: 0 0 15px tone(20%);
		text-decoration-color: tone(10%,-0.1);
	}
}
a[href^="//"],
a[href^="http://"],
a[href^="https://"] { // external link
	&:hover::after {
		color: black;
................................................................................
		background-color: white;
	}
	&::after {
		content: "↗";
		display: inline-block;
		color: black;
		margin-left: 4pt;
		background-color: tone(10%);
		padding: 0 4px;
		text-shadow: none;
		padding-right: 5px;
		vertical-align: baseline;
		font-size: 80%;
	}
}
................................................................................

%content {
	width: 8in;
	margin: auto;
}

%glow {
	box-shadow: 0 0 20px tone(0%,-0.8);
}

%button {
	@extend %sans;
	font-size: 14pt;
	padding: 0.1in 0.2in;
	border: 1px solid black;
	color: tone(25%);
	text-shadow: 1px 1px black;
	text-decoration: none;
	text-align: center;
	cursor: default;
	background: linear-gradient(to bottom,

		tone(-47%),
		tone(-50%) 15%,
		tone(-50%) 75%,
		tone(-53%)
	);
	&:hover, &:focus {
		@extend %glow;
		outline: none;
		color: tone(-55%);
		text-shadow: none;
		background: linear-gradient(to bottom,
			tone(-27%),
			tone(-30%) 15%,
			tone(-30%) 75%,
			tone(-35%)
		);
	}
	&:active {
		color: black;
		padding-bottom: calc(0.1in - 2px);
		padding-top: calc(0.1in + 2px);
		background: linear-gradient(to top,
			tone(-25%),
			tone(-30%) 15%,
			tone(-30%) 75%,
			tone(-35%)
		);
	}
}

button { @extend %button;
	&:first-of-type {
		@extend %button;
		color: white;
		box-shadow: inset 0 1px  tone(-25%),
		            inset 0 -1px tone(-50%);
		background: linear-gradient(to bottom,
			tone(-35%),
			tone(-40%) 15%,
			tone(-40%) 75%,
			tone(-45%)
		);
		&:hover, &:focus {
			box-shadow: inset 0 1px  tone(-15%),
						inset 0 -1px tone(-40%);
		}
		&:active {
			box-shadow: inset 0 1px  tone(-50%),
						inset 0 -1px tone(-25%);
			background: linear-gradient(to top,
				tone(-30%),
				tone(-35%) 15%,
				tone(-35%) 75%,
				tone(-40%)
			);
		}
	}
	&:hover { font-weight: bold; }
}

$grad-ui-focus: linear-gradient(to bottom,
	tone(-50%),
	tone(-35%)
);

input[type='text'], input[type='password'], textarea {
	@extend %serif;
	padding: 0.08in 0.1in;
	border: 1px solid black;
	background: linear-gradient(to bottom, tone(-55%), tone(-40%));



	font-size: 16pt;

	color: tone(25%);
	box-shadow: inset 0 0 20px -3px tone(-55%);
	&:focus {
		color: white;
		border-image: linear-gradient(to bottom, tone(-10%), tone(-30%)) 1 / 1px;



		background: $grad-ui-focus;
		outline: none;
		@extend %glow;
	}
}

@mixin glass {
	@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
		backdrop-filter: blur(40px);
		-webkit-backdrop-filter: blur(40px);
		background-color: tone(-53%, -0.7);
	}
	@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
		background-color: tone(-53%, -0.1);
	}
}

h1 { margin-top: 0 }

header {
	position: fixed;
................................................................................
	position: relative;
	min-height: calc(100vh - 1.1in);
	margin-top: 0;
	margin-bottom: 0;
	padding: 0 0.4in;
	padding-top: 1.1in;
	padding-bottom: 0.1in;
	background-color: tone(-45%,-0.3);
	border: {
		left: 1px solid black;
		right: 1px solid black;
	}
}

div.profile {
................................................................................
			grid-column: 1 / 2;
			grid-row: 1 / 3;
			border: 1px solid black;
		}
		> .id {
			grid-column: 2 / 3;
			grid-row: 1 / 2;
			color: tone(25%,-0.4);
			> .nym {
				font-weight: bold;
				color: tone(25%);
			}
			> .xid {
				color: tone(20%,-0.1);
				font-size: 80%;
				vertical-align: text-top;
			}
		}
		> .bio {
			grid-column: 2 / 3;
			grid-row: 2 / 3;
................................................................................
			display: block;
			height: 0.3in;
			width: 1px;
			border-left: 1px solid rgba(0,0,0,0.6);
		}
	}
}

.epithet {
	display: inline-block;
	background: tone(20%);
	color: tone(-45%);
	text-shadow: 0 0 3px tone(-30%, -0.4);
	border-radius: 3px;
	padding: 6px;
	padding-top: 2px;
	padding-bottom: 4px;
	font-size: 80%;
	vertical-align: top;
	font-weight: 300;
	letter-spacing: 0.5px;
	margin: 0 5pt;
	// transform: scale(80%) translateX(-10pt); // cheating!
}

%box {
	margin: auto;
	border: 1px solid tone(-55%);
	border-bottom: 3px solid black;
	box-shadow: 0 0 1px black;
	border-image: linear-gradient(to bottom,
		tone(-40%),
		tone(-52%) 10%,
		tone(-55%) 90%,
		tone(-60%)
	) 1 / 1px;
	background: linear-gradient(to bottom,
		tone(-58%),
		tone(-55%) 10%,
		tone(-50%) 80%,
		tone(-45%)
	);
	// outline: 1px solid black;
}

body.error .message {
	@extend %box;
	width: 4in;
................................................................................
	> .msg {
		text-align: center;
		padding: 0.3in;
	}
	> .msg:first-child { padding-top: 0; }
	> .user {
		width: min-content; margin: auto;
		background: tone(-20%,-0.3);
		border: 1px solid black;
		color: tone(-50%);
		padding: 0.1in;
		> img { width: 1in; height: 1in; border: 1px solid black; }
		> .name { @extend %serif; text-align: center; font-size: 130%; font-weight: bold; margin-top: 0.08in; }
	}
	>form {
		display: grid;
		grid-template-columns: 1fr 1fr;
................................................................................
	@extend %box;
	display: grid;
	grid-template-columns: 1.1in 2fr min-content 1fr;
	grid-template-rows: 1fr min-content;
	grid-gap: 2px;
	padding: 0.1in;
	> img { grid-column: 1/2; grid-row: 1/3; width: 1in; height: 1in;}
	> textarea {
		grid-column: 2/5; grid-row: 1/2; height: 3in;
		resize: vertical;
		margin-bottom: 0.08in;
	}
	> input[name="acl"] { grid-column: 2/3; grid-row: 2/3; }
	> button { grid-column: 4/5; grid-row: 2/3; }
	a.help[href] { margin-right: 0.05in }
}

a.help[href] {
	display: block;
................................................................................
		background: linear-gradient(to right, tone(-55%), transparent);
	}
	>.content {
		grid-column: 2/4; grid-row: 1/2;
		padding: 0.2in;
		@extend %serif;
		font-size: 110%;
		text-align: justify;
	}
	> a[href].permalink {
		display: block;
		grid-column: 3/4; grid-row: 2/3;
		font-size: 80%;
		text-align: right;
		padding: 0.1in;
................................................................................

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

body.doc main {
	@extend %serif;
	text-align: justify;
	li { margin-top: 0.05in; }
	li:first-child { margin-top: 0; }
	h1, h2, h3, h4, h5, h6 {
		background: linear-gradient(to right, tone(-50%), transparent);
		margin-left: -0.4in;
		padding-left: 0.2in;
		text-shadow: 0 2px 0 black;
	}
}

body.conf main {
	display: grid;
	grid-template-columns: 2in 1fr;
	grid-template-rows: max-content 1fr;
	> .menu {
		margin-left: -0.25in;
		grid-column: 1/2; grid-row: 1/2;
		background: linear-gradient(to bottom, tone(-45%),tone(-55%));
		border: 1px solid black;
		padding: 0.1in;
		> a[href] {
			@extend %button;
			display: block;
			text-align: left;
		}
		> a[href] + a[href] {
			border-top: none;
		}
		hr {
			border: none;
		}
	}
	> .panel {
		grid-column: 2/3; grid-row: 1/3;
		padding-left: 0.15in;
		> h1 {
			padding-bottom: 0.1in;
			margin-bottom: 0.1in;
			margin-left: -0.15in;
			padding-left: 0.15in;
			padding-top: 0.12in;
			background: linear-gradient(to right, tone(-50%), tone(-50%,-0.7));
			border: 1px solid tone(-55%);
			border-left: none;
			text-shadow: 1px 1px 0 black;
		}
	}
}

Modified store.t from [4959208545] to [71684bc451].

4
5
6
7
8
9
10

11
12
13
14
15
16
17
..
20
21
22
23
24
25
26
27




28
29
30
31
32
33
34
35
36
37
38
39
40
41
..
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78














79
80
81
82
83
84
85
...
178
179
180
181
182
183
184



185
186
187
188
189
190
191
...
231
232
233
234
235
236
237





238
239
240
241
242
243
244
	scope = lib.enum {
		'public', 'private', 'local';
		'personal', 'direct', 'circle';
	};
	notiftype = lib.enum {
		'mention', 'like', 'rt', 'react'
	};

	relation = lib.enum {
		'follow', 'mute', 'block'
	};
	credset = lib.set {
		'pw', 'otp', 'challenge', 'trust'
	};
	privset = lib.set {
................................................................................
	powerset = lib.set {
		-- user powers -- default on
		'login', 'visible', 'post', 'shout',
		'propagate', 'upload', 'acct', 'edit';

		-- admin powers -- default off
		'purge', 'config', 'censor', 'suspend',
		'cred', 'elevate', 'demote', 'rebrand' -- modify site's brand identity




	}
}

terra m.powerset:affect_users()
	return self.purge() or self.censor() or self.suspend() or
	       self.elevate() or self.demote() or self.rebrand() or
		   self.cred()
end

local str = rawstring
local pstr = lib.mem.ptr(int8)

struct m.source

................................................................................

struct m.actor {
	id: uint64
	nym: str
	handle: str
	origin: uint64
	bio: str
	title: str
	avatar: str
	knownsince: m.timepoint
	rights: m.rights
	key: lib.mem.ptr(uint8)

-- ephemera
	xid: str
	source: &m.source
}















struct m.actor_stats {
	posts: intptr
	follows: intptr
	followers: intptr
	mutuals: intptr
}
................................................................................
	blacklist: bool
}

-- backends only handle content on the local server
struct m.backend { id: rawstring
	open: &m.source -> &opaque
	close: &m.source -> {}




	conf_get: {&m.source, rawstring} -> lib.mem.ptr(int8)
	conf_set: {&m.source, rawstring, rawstring} -> {}
	conf_reset: {&m.source, rawstring} -> {}

	actor_save: {&m.source, &m.actor} -> bool
	actor_create: {&m.source, &m.actor} -> uint64
................................................................................
		-- notifies the backend module of the UID that has been assigned for
		-- an authentication ID
			-- aid: uint64
			-- uid: uint64

	actor_conf_str: cnf(rawstring, lib.mem.ptr(int8))
	actor_conf_int: cnf(intptr, lib.stat(intptr))






	post_save: {&m.source, &m.post} -> {}
	post_create: {&m.source, &m.post} -> uint64
	post_enum_author_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post))
	convo_fetch_xid: {&m.source,rawstring} -> lib.mem.ptr(m.post)
	convo_fetch_uid: {&m.source,uint64} -> lib.mem.ptr(m.post)








>







 







|
>
>
>
>





|
<







 







|









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







 







>
>
>







 







>
>
>
>
>







4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
..
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
..
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
...
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
...
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
	scope = lib.enum {
		'public', 'private', 'local';
		'personal', 'direct', 'circle';
	};
	notiftype = lib.enum {
		'mention', 'like', 'rt', 'react'
	};

	relation = lib.enum {
		'follow', 'mute', 'block'
	};
	credset = lib.set {
		'pw', 'otp', 'challenge', 'trust'
	};
	privset = lib.set {
................................................................................
	powerset = lib.set {
		-- user powers -- default on
		'login', 'visible', 'post', 'shout',
		'propagate', 'upload', 'acct', 'edit';

		-- admin powers -- default off
		'purge', 'config', 'censor', 'suspend',
		'cred', 'elevate', 'demote', 'rebrand', -- modify site's brand identity
		'herald' -- grant serverwide epithets
	};
	prepmode = lib.enum {
		'full','conf','admin'
	}
}

terra m.powerset:affect_users()
	return self.purge() or self.censor() or self.suspend() or
	       self.elevate() or self.demote() or self.cred()

end

local str = rawstring
local pstr = lib.mem.ptr(int8)

struct m.source

................................................................................

struct m.actor {
	id: uint64
	nym: str
	handle: str
	origin: uint64
	bio: str
	epithet: str
	avatar: str
	knownsince: m.timepoint
	rights: m.rights
	key: lib.mem.ptr(uint8)

-- ephemera
	xid: str
	source: &m.source
}

terra m.actor.methods.mk(kbuf: &uint8)
	var newkp = lib.crypt.genkp()
	var privsz = lib.crypt.der(false,&newkp,kbuf)
	return m.actor {
		id = 0; nym = nil; handle = nil;
		origin = 0; bio = nil; avatar = nil;
		knownsince = lib.osclock.time(nil);
		rights = m.rights_default();
		epithet = nil, key = [lib.mem.ptr(uint8)] {
			ptr = &kbuf[0], ct = privsz
		};
	}
end

struct m.actor_stats {
	posts: intptr
	follows: intptr
	followers: intptr
	mutuals: intptr
}
................................................................................
	blacklist: bool
}

-- backends only handle content on the local server
struct m.backend { id: rawstring
	open: &m.source -> &opaque
	close: &m.source -> {}
	dbsetup: &m.source -> bool -- creates the schema needed to call conprep (called only once per database e.g. with `parsav db init`)
	conprep: {&m.source, m.prepmode.t} -> {} -- prepares queries and similar tasks that require the schema to already be in place
	obliterate_everything: &m.source -> bool -- wipes everything parsav-related out of the database

	conf_get: {&m.source, rawstring} -> lib.mem.ptr(int8)
	conf_set: {&m.source, rawstring, rawstring} -> {}
	conf_reset: {&m.source, rawstring} -> {}

	actor_save: {&m.source, &m.actor} -> bool
	actor_create: {&m.source, &m.actor} -> uint64
................................................................................
		-- notifies the backend module of the UID that has been assigned for
		-- an authentication ID
			-- aid: uint64
			-- uid: uint64

	actor_conf_str: cnf(rawstring, lib.mem.ptr(int8))
	actor_conf_int: cnf(intptr, lib.stat(intptr))

	auth_create_pw: {&m.source, uint64, bool, lib.mem.ptr(int8)} -> {}
		-- uid: uint64
		-- reset: bool (delete other passwords?)
		-- pw: pstring

	post_save: {&m.source, &m.post} -> {}
	post_create: {&m.source, &m.post} -> uint64
	post_enum_author_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post))
	convo_fetch_xid: {&m.source,rawstring} -> lib.mem.ptr(m.post)
	convo_fetch_uid: {&m.source,uint64} -> lib.mem.ptr(m.post)

Modified str.t from [5af1afba76] to [f2457b558f].

6
7
8
9
10
11
12


13
14
15
16
17
18
19

local m = {
	sz = terralib.externfunction('strlen', rawstring -> intptr);
	cmp = terralib.externfunction('strcmp', {rawstring, rawstring} -> int);
	ncmp = terralib.externfunction('strncmp', {rawstring, rawstring, intptr} -> int);
	cpy = terralib.externfunction('stpcpy',{rawstring, rawstring} -> rawstring);
	ncpy = terralib.externfunction('stpncpy',{rawstring, rawstring, intptr} -> rawstring);


	dup = terralib.externfunction('strdup',rawstring -> rawstring);
	ndup = terralib.externfunction('strndup',{rawstring, intptr} -> rawstring);
	fmt = terralib.externfunction('asprintf',
		terralib.types.funcpointer({&rawstring,rawstring},{int},true));
	bfmt = terralib.externfunction('sprintf',
		terralib.types.funcpointer({rawstring,rawstring},{int},true));
	span = terralib.externfunction('strspn',{rawstring, rawstring} -> rawstring);







>
>







6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

local m = {
	sz = terralib.externfunction('strlen', rawstring -> intptr);
	cmp = terralib.externfunction('strcmp', {rawstring, rawstring} -> int);
	ncmp = terralib.externfunction('strncmp', {rawstring, rawstring, intptr} -> int);
	cpy = terralib.externfunction('stpcpy',{rawstring, rawstring} -> rawstring);
	ncpy = terralib.externfunction('stpncpy',{rawstring, rawstring, intptr} -> rawstring);
	cat = terralib.externfunction('strcat',{rawstring, rawstring} -> rawstring);
	ncat = terralib.externfunction('strncat',{rawstring, rawstring, intptr} -> rawstring);
	dup = terralib.externfunction('strdup',rawstring -> rawstring);
	ndup = terralib.externfunction('strndup',{rawstring, intptr} -> rawstring);
	fmt = terralib.externfunction('asprintf',
		terralib.types.funcpointer({&rawstring,rawstring},{int},true));
	bfmt = terralib.externfunction('sprintf',
		terralib.types.funcpointer({rawstring,rawstring},{int},true));
	span = terralib.externfunction('strspn',{rawstring, rawstring} -> rawstring);

Modified tpl.t from [ba911a8ebc] to [682e534236].

1
2
3
4
5
6

7
8
9
10
11
12
13
..
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
..
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
..
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
-- vim: ft=terra
-- string template generator:
-- returns a function that fills out a template
-- with the strings given

local util = lib.util

local m = {}
function m.mk(tplspec)
	local str
	if type(tplspec) == 'string'
		then str = tplspec tplspec = {}
		else str = tplspec.body
	end
................................................................................
	str = str:gsub('%s+[\n$]','')
	str = str:gsub('\n','')
	str = str:gsub('</a><a ','</a> <a ') -- keep nav links from getting smooshed
	str = str:gsub(tplchar .. '%?([-%w]+)', function(file)
		if not docs[file] then docs[file] = data.doc[file] end
		return string.format('<a href="#help-%s" class="help">?</a>', file)
	end)
	for start, key, stop in string.gmatch(str,'()'..tplchar..'(%w+)()') do
		if string.sub(str,start-1,start-1) ~= '\\' then
			segs[#segs+1] = string.sub(str,last,start-1)
			fields[#segs] = key
			last = stop
		end
	end
	segs[#segs+1] = string.sub(str,last)

	for i, s in ipairs(segs) do
		segs[i] = string.gsub(s, '\\'..tplchar, tplchar_o)
................................................................................
	local runningtally = symbol(intptr)
	local tallyup = {quote
		var [runningtally] = 1 + constlen
	end}
	local rec = terralib.types.newstruct(string.format('template<%s>',tplspec.id or ''))
	local symself = symbol(&rec)
	do local kfac = {}

		for afterseg,key in pairs(fields) do
			if not kfac[key] then
				rec.entries[#rec.entries + 1] = {
					field = key;
					type = lib.mem.ptr(int8);
				}
			end
			kfac[key] = (kfac[key] or 0) + 1

		end
		for key, fac in pairs(kfac) do


			tallyup[#tallyup + 1] = quote
				[runningtally] = [runningtally] + ([symself].[key].ct)*fac
			end
		end
	end

	local copiers = {}
	local senders = {}
	local appenders = {}
................................................................................
	local cpypos = symbol(&opaque)
	local accumulator = symbol(&lib.str.acc)
	local destcon = symbol(&lib.net.mg_connection)
	for idx, seg in ipairs(segs) do
		copiers[#copiers+1] = quote [cpypos] = lib.mem.cpy([cpypos], [&opaque]([seg]), [#seg]) end
		senders[#senders+1] = quote lib.net.mg_send([destcon], [seg], [#seg]) end
		appenders[#appenders+1] = quote [accumulator]:push([seg], [#seg]) end
		if fields[idx] then
			--local fsz = `lib.str.sz(symself.[fields[idx]])
			local fval = `symself.[fields[idx]].ptr























			local fsz = `symself.[fields[idx]].ct
			copiers[#copiers+1] = quote

				[cpypos] = lib.mem.cpy([cpypos], [&opaque]([fval]), [fsz])
			end

			senders[#senders+1] = quote

				lib.net.mg_send([destcon], [fval], [fsz])
			end

			appenders[#appenders+1] = quote
				[accumulator]:push([fval], [fsz])
			end
		end
	end

	local tid = tplspec.id or '<anonymous>'
	rec.methods.tostr = terra([symself])
		lib.dbg(['compiling template ' .. tid])
		[tallyup]
		var [symtxt] = lib.mem.heapa(int8, [runningtally])
		var [cpypos] = [&opaque](symtxt.ptr)
		[copiers]
		@[&int8](cpypos) = 0

		return symtxt
	end
	rec.methods.append = terra([symself], [accumulator])
		lib.dbg(['appending template ' .. tid])
		[tallyup]
		accumulator:cue([runningtally])
		[appenders]






>







 







|


|







 







>
|
|

|



|
>


>
>

|







 







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

>
|
|
>

|












>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
..
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
..
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
..
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
-- vim: ft=terra
-- string template generator:
-- returns a function that fills out a template
-- with the strings given

local util = lib.util
local pstr = lib.mem.ptr(int8)
local m = {}
function m.mk(tplspec)
	local str
	if type(tplspec) == 'string'
		then str = tplspec tplspec = {}
		else str = tplspec.body
	end
................................................................................
	str = str:gsub('%s+[\n$]','')
	str = str:gsub('\n','')
	str = str:gsub('</a><a ','</a> <a ') -- keep nav links from getting smooshed
	str = str:gsub(tplchar .. '%?([-%w]+)', function(file)
		if not docs[file] then docs[file] = data.doc[file] end
		return string.format('<a href="#help-%s" class="help">?</a>', file)
	end)
	for start, mode, key, stop in string.gmatch(str,'()'..tplchar..'([:!]?)(%w+)()') do
		if string.sub(str,start-1,start-1) ~= '\\' then
			segs[#segs+1] = string.sub(str,last,start-1)
			fields[#segs] = { key = key, mode = (mode ~= '' and mode or nil) }
			last = stop
		end
	end
	segs[#segs+1] = string.sub(str,last)

	for i, s in ipairs(segs) do
		segs[i] = string.gsub(s, '\\'..tplchar, tplchar_o)
................................................................................
	local runningtally = symbol(intptr)
	local tallyup = {quote
		var [runningtally] = 1 + constlen
	end}
	local rec = terralib.types.newstruct(string.format('template<%s>',tplspec.id or ''))
	local symself = symbol(&rec)
	do local kfac = {}
		local sanmode = {}
		for afterseg,fld in ipairs(fields) do
			if not kfac[fld.key] then
				rec.entries[#rec.entries + 1] = {
					field = fld.key;
					type = lib.mem.ptr(int8);
				}
			end
			kfac[fld.key] = (kfac[fld.key] or 0) + 1
			sanmode[fld.key] = fld.mode == ':' and 6 or fld.mode == '!' and 5 or 1
		end
		for key, fac in pairs(kfac) do
			local sanfac = sanmode[key]
			
			tallyup[#tallyup + 1] = quote
				[runningtally] = [runningtally] + ([symself].[key].ct)*fac*sanfac
			end
		end
	end

	local copiers = {}
	local senders = {}
	local appenders = {}
................................................................................
	local cpypos = symbol(&opaque)
	local accumulator = symbol(&lib.str.acc)
	local destcon = symbol(&lib.net.mg_connection)
	for idx, seg in ipairs(segs) do
		copiers[#copiers+1] = quote [cpypos] = lib.mem.cpy([cpypos], [&opaque]([seg]), [#seg]) end
		senders[#senders+1] = quote lib.net.mg_send([destcon], [seg], [#seg]) end
		appenders[#appenders+1] = quote [accumulator]:push([seg], [#seg]) end
		if fields[idx] and fields[idx].mode then
			local f = fields[idx]
			local fp = `symself.[f.key]
			copiers[#copiers+1] = quote 
				if fp.ct > 0 then
					var san = lib.html.sanitize(fp, [f.mode == ':'])
					[cpypos] = lib.mem.cpy([cpypos], [&opaque](san.ptr), san.ct)
					--san:free()
				end
			end
			senders[#senders+1] = quote
				if fp.ct > 0 then
					var san = lib.html.sanitize(fp, [f.mode == ':'])
					lib.net.mg_send([destcon], san.ptr, san.ct)
					--san:free()
				end
			end
			appenders[#appenders+1] = quote
				if fp.ct > 0 then
					var san = lib.html.sanitize(fp, [f.mode == ':'])
					[accumulator]:ppush(san)
					--san:free()
				end
			end
		elseif fields[idx] then
			local f = fields[idx]
			local fp = `symself.[f.key]
			copiers[#copiers+1] = quote 
				if fp.ct > 0 then
					[cpypos] = lib.mem.cpy([cpypos], [&opaque](fp.ptr), fp.ct)
				end
			end
			senders[#senders+1] = quote
				if fp.ct > 0 then
					lib.net.mg_send([destcon], fp.ptr, fp.ct)
				end
			end
			appenders[#appenders+1] = quote
				if fp.ct > 0 then [accumulator]:ppush(fp) end
			end
		end
	end

	local tid = tplspec.id or '<anonymous>'
	rec.methods.tostr = terra([symself])
		lib.dbg(['compiling template ' .. tid])
		[tallyup]
		var [symtxt] = lib.mem.heapa(int8, [runningtally])
		var [cpypos] = [&opaque](symtxt.ptr)
		[copiers]
		@[&int8](cpypos) = 0
		symtxt.ct = [&int8](cpypos) - symtxt.ptr
		return symtxt
	end
	rec.methods.append = terra([symself], [accumulator])
		lib.dbg(['appending template ' .. tid])
		[tallyup]
		accumulator:cue([runningtally])
		[appenders]

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

1
2
3
4
5
6













<form class="compose" method="post">
	<img src="/avi/@handle">
	<textarea autofocus name="post" placeholder="it was a dark and stormy night…">@content</textarea>
	<input required type="text" name="acl" class="acl" value="@acl"> @?acl
	<button type="submit">commit</button>
</form>















|
|


>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<form class="compose" method="post">
	<img src="/avi/@handle">
	<textarea autofocus name="post" placeholder="it was a dark and stormy night…">@!content</textarea>
	<input required autocomplete="on" type="text" name="acl" class="acl" value="@acl" list="scopes" placeholder="access control"> @?acl
	<button type="submit">commit</button>
</form>

<datalist id="scopes">
	<option>all</option>
	<option>mentioned</option>
	<option>local</option>
	<option>mutual</option>
	<option>followers</option>
	<option>followed</option>
	<option>groupies</option>
	<option>staff</option>
	<option>admin</option>
	@circles
</datalist>

Added view/conf-profile.tpl version [746111dd26].













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

Added view/conf-sec.tpl version [7ba95a81c5].





















>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
<form method="post">
	<p>if you are concerned that your account may have been compromised, you can terminate all other login sessions by invalidating their session cookies. note that this will not have any effect on API tokens; these must be revoked separately!</p>
	<label>
		sessions valid from
		<div class="txtbox">@lastreset</div>
	</label>
	<button type="submit" name="act" value="invalidate">
		invalidate other sessions
	</button>
</form>

Added view/conf.tpl version [bd130ad9d3].































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

<div class="panel">
	@panel
</div>

Modified view/load.lua from [1503c625ad] to [f63ef60595].

4
5
6
7
8
9
10

11
12



13
14
15
16
17
18
19
-- create templates from when we return to terra
local path = ...
local sources = {
	'docskel';
	'tweet';
	'profile';
	'compose';

	'login-username';
	'login-challenge';



}

local ingest = function(filename)
	local hnd = io.open(path..'/'..filename)
	local txt = hnd:read('*a')
	io.close(hnd)
	txt = txt:gsub('([^\\])!%b[]', '%1')







>


>
>
>







4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- create templates from when we return to terra
local path = ...
local sources = {
	'docskel';
	'tweet';
	'profile';
	'compose';

	'login-username';
	'login-challenge';

	'conf';
	'conf-profile';
}

local ingest = function(filename)
	local hnd = io.open(path..'/'..filename)
	local txt = hnd:read('*a')
	io.close(hnd)
	txt = txt:gsub('([^\\])!%b[]', '%1')

Modified view/login-challenge.tpl from [c8511de2b7] to [84fccbb367].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div class="login">
	<div class="user">
		<img src="/avi/@handle">
		<div class="name">@name</div>
	</div>
	<div class="msg">@challenge</div>
	<form action="/login" method="post">
		<label for="response">@label</label>
		<input type="hidden" name="user" value="@handle">
		<input type="password" name="response" id="response" autofocus required>
		<button type="submit" name="authmethod" value="@method">authenticate</button>
		<a href="/login">cancel</a>
	</form>
</div>



|




|
|




1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div class="login">
	<div class="user">
		<img src="/avi/@handle">
		<div class="name">@!name</div>
	</div>
	<div class="msg">@challenge</div>
	<form action="/login" method="post">
		<label for="response">@label</label>
		<input type="hidden" name="user" value="@:handle">
		<input type="password" autocomplete="@auto" name="response" id="response" autofocus required>
		<button type="submit" name="authmethod" value="@method">authenticate</button>
		<a href="/login">cancel</a>
	</form>
</div>

Modified view/login-username.tpl from [4dc628d5ef] to [8c165f8ae9].

1
2
3
4
5
6
7
8
<div class="login">
	<div class="msg">@loginmsg</div>
	<form action="/login" method="post">
		<label for="user">local handle</label>
		<input type="text" name="user" id="user" autofocus required>
		<button type="submit">log on</button>
	</form>
</div>




|



1
2
3
4
5
6
7
8
<div class="login">
	<div class="msg">@loginmsg</div>
	<form action="/login" method="post">
		<label for="user">local handle</label>
		<input type="text" name="user" id="user" autocomplete="username" autofocus required>
		<button type="submit">log on</button>
	</form>
</div>

Modified view/profile.tpl from [6c61b2a3c1] to [d21ccabbe8].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<div class="profile">
	<div class="banner">
		<img class="avatar" src="@avatar">
		<div class="id"><span class="nym">@nym</span> [<span class="xid">@xid</span>]</div>
		<div class="bio">
			@bio
		</div>
	</div>
	<table class="stats">
		<tr><th>posts</th> <td>@nposts</td></tr>
		<tr><th>following</th> <td>@nfollows</td></tr>
		<tr><th>followers</th> <td>@nfollowers</td></tr>
		<tr><th>mutuals</th> <td>@nmutuals</td></tr>
		<tr><th>@timephrase</th> <td>@tweetday</td></tr>
	</table>
	<div class="menu">
		<a href="/@xid">posts</a>
		<a href="/@xid/media">media</a>
		<a href="/@xid/social">associates</a>
		<hr>
		@auxbtn
	</div>
</div>


|
|












|
|
|




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<div class="profile">
	<div class="banner">
		<img class="avatar" src="@:avatar">
		<div class="id">@nym</div>
		<div class="bio">
			@bio
		</div>
	</div>
	<table class="stats">
		<tr><th>posts</th> <td>@nposts</td></tr>
		<tr><th>following</th> <td>@nfollows</td></tr>
		<tr><th>followers</th> <td>@nfollowers</td></tr>
		<tr><th>mutuals</th> <td>@nmutuals</td></tr>
		<tr><th>@timephrase</th> <td>@tweetday</td></tr>
	</table>
	<div class="menu">
		<a href="/@:xid">posts</a>
		<a href="/@:xid/media">media</a>
		<a href="/@:xid/social">associates</a>
		<hr>
		@auxbtn
	</div>
</div>

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

1
2
3
4
5
6
7
8
9
10
11
<div class="post">
	<div class="avatar"><img src="@avatar"></div>
	<a class="username" href="/@acctlink">
		<span class="nym">@nym</span> [<span class="handle">@xid</span>]
	</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
<div class="post">
	<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>