parsav  Check-in [5b3a03ad34]

Overview
Comment:big ol iteration
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 5b3a03ad34669be0b5e5ecd853db0a2c2fc923886345f08e93cece08a84c357f
User & Date: lexi on 2020-12-25 03:59:32
Other Links: manifest | tags
Context
2020-12-25
23:37
iteration and important api adjustments check-in: f9559a83fc user: lexi tags: trunk
03:59
big ol iteration check-in: 5b3a03ad34 user: lexi tags: trunk
2020-12-22
23:01
milestone check-in: 419d1a1ebe user: lexi tags: trunk
Changes

Modified backend/pgsql.t from [17bd63f8c3] to [0ea1a47601].

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
..
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
..
93
94
95
96
97
98
99
100
101

102
103
104
105
106
107
108
...
171
172
173
174
175
176
177

178
179
180
181
182
183
184
...
216
217
218
219
220
221
222

223
224
225
226
227
228

229
230
231
232
233
234

235
236
237

238
239
240

241
242
243
244
245
246
247
248
249
250
251
252

253
254
255
256
257
258
259
...
268
269
270
271
272
273
274
275
276
277
278

279
280
281
282
283
284
285

286
287
288
289
290
291
292
293

294
295
296
297
298
299
300
301
302
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
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
...
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
...
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
				key = $1::text 
		]];
	};

	actor_fetch_uid = {
		params = {uint64}, sql = [[
			select
				id, nym, handle, origin,
				bio, rank, quota, key


			from parsav_actors
				where id = $1::bigint
		]];
	};

	actor_fetch_xid = {
		params = {lib.mem.ptr(int8)}, sql = [[
			select a.id, a.nym, a.handle, a.origin,
			       a.bio, a.rank, a.quota, a.key, 

				   coalesce(a.handle || '@' || s.domain,
				            '@' || a.handle) as xid,

				coalesce(s.domain,
				        (select value from parsav_config
							where key='domain' limit 1)) as domain

................................................................................
			where $1::text = (a.handle || '@' || domain) or
			      $1::text = ('@' || a.handle || '@' || domain) or
				  (a.origin is null and
					  $1::text = a.handle or
					  $1::text = ('@' || a.handle))
		]];
	};














	actor_enum_local = {
		params = {}, sql = [[
			select id, nym, handle, origin,
			       bio, rank, quota, key,

				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.rank, a.quota, a.key,

				   coalesce(a.handle || '@' || s.domain,
				            '@' || a.handle) as xid
			from parsav_actors a
			left join parsav_servers s on s.id = a.origin
		]];
	};
























	actor_auth_how = {
		params = {rawstring, lib.store.inet}, sql = [[
		with mts as (select a.kind from parsav_auth as a
			left join parsav_actors as u on u.id = a.uid
			where (a.uid is null or u.handle = $1::text or (
					a.uid = 0 and a.name = $1::text
................................................................................
				(select count(*) from mts where kind like 'challenge-%') > 0,
				(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.rank, a.quota, a.key,

				   coalesce(a.handle || '@' || s.domain,
				            '@' || a.handle) as xid,

			       au.restrict,
						array['post'  ] <@ au.restrict as can_post,
						array['edit'  ] <@ au.restrict as can_edit,
						array['acct'  ] <@ au.restrict as can_acct,
................................................................................
	for j=0,sz do i.v6[j] = v[4 + j] end -- 😬
	return i
end
pqr.methods.int = macro(function(self, ty, row, col)
	return quote
		var i: ty:astype()
		var v = lib.pq.PQgetvalue(self.res, row, col)

		lib.math.netswap_ip(ty, v, &i)
	in i end
end)

local pqt = {
	[lib.store.inet] = function(cidr)
		local tycode = cidr and 0x01 or 0x00
................................................................................
			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 = {}, {}, {}, {}, {}, {}

	for i, ty in ipairs(q.params) do
		args[i] = symbol(ty)
		ft[i] = `1
		if ty == rawstring then
			counters[i] = `lib.trn([args[i]] == nil, 0, lib.str.sz([args[i]]))
			casts[i] = `[&int8]([args[i]])

		elseif ty == lib.store.inet then -- assume not CIDR
			counters[i] = `lib.trn([args[i]].pv == 4,4,16)+4
			casts[i] = quote
				var ipbuf: int8[20]
				;[pqt[lib.store.inet](false)]([args[i]], [&uint8](&ipbuf))
			in &ipbuf[0] end

		elseif ty.ptr_basetype == int8 or ty.ptr_basetype == uint8 then
			counters[i] = `[args[i]].ct
			casts[i] = `[&int8]([args[i]].ptr)

		elseif ty:isintegral() then
			counters[i] = ty.bytes
			casts[i] = `[&int8](&[args[i]])

			fixers[#fixers + 1] = quote
				--lib.io.fmt('uid=%llu(%llx)\n',[args[i]],[args[i]])
				[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]

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

local terra row_to_actor(r: &pqr, row: intptr): lib.mem.ptr(lib.store.actor)
	var a: lib.mem.ptr(lib.store.actor)

	if r:cols() >= 8 then 
		a = [ lib.str.encapsulate(lib.store.actor, {
			nym = {`r:string(row,1), `r:len(row,1)+1};
			bio = {`r:string(row,4), `r:len(row,4)+1};

			handle = {`r:string(row, 2); `r:len(row,2) + 1};
			xid = {`r:string(row, 8); `r:len(row,8) + 1};
		}) ]
	else
		a = [ lib.str.encapsulate(lib.store.actor, {
			nym = {`r:string(row,1), `r:len(row,1)+1};
			bio = {`r:string(row,4), `r:len(row,4)+1};

			handle = {`r:string(row, 2); `r:len(row,2) + 1};
		}) ]
		a.ptr.xid = nil
	end
	a.ptr.id = r:int(uint64, row, 0);
	a.ptr.rights = lib.store.rights_default();
	a.ptr.rights.rank = r:int(uint16, row, 5);
	a.ptr.rights.quota = r:int(uint32, row, 6);

	if r:null(row,7) then
		a.ptr.key.ct = 0 a.ptr.key.ptr = nil
	else
		a.ptr.key = r:bin(row,7)
	end
	if r:null(row,3) then a.ptr.origin = 0
	else a.ptr.origin = r:int(uint64,row,3) end
	return a
end

local checksha = function(hnd, query, hash, origin, username, pw)
	local inet_buf = symbol(uint8[4 + 16])
	local validate = function(kind, cred, credlen)
		return quote 

			var osz: intptr if origin.pv == 4 then osz = 4 else osz = 16 end 
			var formats = arrayof([int], 1,1,1,1)
			var params = arrayof([&int8], username, kind,
				[&int8](&cred), [&int8](&inet_buf))
			var lens = arrayof(int, lib.str.sz(username), [#kind], credlen, osz + 4)
			var res = lib.pq.PQexecParams([&lib.pq.PGconn](hnd), query, 4, nil,
				params, lens, formats, 1)
			if res == nil then
				lib.bail('grievous failure checking pwhash')
			elseif lib.pq.PQresultStatus(res) ~= lib.pq.PGRES_TUPLES_OK then
				lib.warn('pwhash query failed: ', lib.pq.PQresultErrorMessage(res), '\n', query)
			else
				var r = pqr {
					sz = lib.pq.PQntuples(res);
					res = res;
				}
				if r.sz > 0 then -- found a record! stop here
					var aid = r:int(uint64, 0,0)
					r:free()
					return aid
				end
			end
		end
	end
	
	local out = symbol(uint8[64])
	local vdrs = {}

		local alg = lib.md['MBEDTLS_MD_SHA' .. tostring(hash)]
		vdrs[#vdrs+1] = quote
			if lib.md.mbedtls_md(lib.md.mbedtls_md_info_from_type(alg),
				[&uint8](pw), lib.str.sz(pw), out) ~= 0 then
				lib.bail('hashing failure!')
			end
			[ validate(string.format('pw-sha%u', hash), out, hash / 8) ]
		end

	return quote
		lib.dbg(['searching for hashed password credentials in format SHA' .. tostring(hash)])
		var [inet_buf]
		[pqt[lib.store.inet](false)](origin, inet_buf)
		var [out]
		[vdrs]
		lib.dbg(['could not find password hash'])
	end
end

local b = `lib.store.backend {
................................................................................
		end
	end];

	actor_auth_how = [terra(
			src: &lib.store.source,
			ip: lib.store.inet,
			username: rawstring
		)
		var cs: lib.store.credset cs:clear();
		var r = queries.actor_auth_how.exec(src, username, ip) 
		if r.sz == 0 then return cs end -- just in case
		defer r:free()
		(cs.pw << r:bool(0,0))
		(cs.otp << r:bool(0,1))
		(cs.challenge << r:bool(0,2))
		(cs.trust << r:bool(0,3))
		return cs
	end];
	 
	actor_auth_pw = [terra(
			src: &lib.store.source,
			ip: lib.store.inet,
			username: rawstring,
			cred: rawstring
		)
		var q = [[select a.aid from parsav_auth as a
			left join parsav_actors as u on u.id = a.uid
			where (a.uid is null or u.handle = $1::text or (
					a.uid = 0 and a.name = $1::text
				)) and
				(a.kind = 'trust' or (a.kind = $2::text and a.cred = $3::bytea)) and
				(a.netmask is null or a.netmask >> $4::inet)
			order by blacklist desc limit 1]]

		[ checksha(`src.handle, q, 256, ip, username, cred) ] -- most common
		[ checksha(`src.handle, q, 512, ip, username, cred) ] -- most secure
		[ checksha(`src.handle, q, 384, ip, username, cred) ] -- weird
		[ checksha(`src.handle, q, 224, ip, username, cred) ] -- weirdest

		-- TODO: check pbkdf2-hmac
		-- TODO: check OTP
		return 0
	end];












	actor_session_fetch = [terra(
		src: &lib.store.source,
		aid: uint64,
		ip : lib.store.inet
	): { lib.stat(lib.store.auth), lib.mem.ptr(lib.store.actor) }
		var r = queries.actor_session_fetch.exec(src, aid, ip)
................................................................................

			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,10) then -- restricted?
				au.val.privs:clear()
				(au.val.privs.post   << r:bool(0,11)) 
				(au.val.privs.edit   << r:bool(0,12))
				(au.val.privs.acct   << r:bool(0,13))
				(au.val.privs.upload << r:bool(0,14))
				(au.val.privs.censor << r:bool(0,15))
				(au.val.privs.admin  << r:bool(0,16))
			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 }
	end];
}

return b







|
|
>
>







|
|
>







 







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



|
|
>









|
|
>






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







 







|
|
>







 







>







 







>






>






>



>



>












>







 







|



>

|





>






|
|
>
|


|






|
<


>
|
|
|
|
|
<
<
<
<
<
<
<
<
<
<
<
|
|
|
|
<










|


|




<
<







 







|


|





|





|
|
|
<
<
<
<
<
<
<
<

|
|
|
|





>
>
>
>
>
>
>
>
>
>
>







 







|

|
|
|
|
|
|











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
..
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
...
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
...
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
...
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
...
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
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
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
...
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
...
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
				key = $1::text 
		]];
	};

	actor_fetch_uid = {
		params = {uint64}, sql = [[
			select
				id, nym, handle, origin, bio,
				avataruri, rank, quota, key,
				extract(epoch from knownsince)::bigint

			from parsav_actors
				where id = $1::bigint
		]];
	};

	actor_fetch_xid = {
		params = {lib.mem.ptr(int8)}, sql = [[
			select a.id, a.nym, a.handle, a.origin, a.bio,
			       a.avataruri, a.rank, a.quota, a.key, 
			       extract(epoch from 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

................................................................................
			where $1::text = (a.handle || '@' || domain) or
			      $1::text = ('@' || a.handle || '@' || domain) or
				  (a.origin is null and
					  $1::text = a.handle or
					  $1::text = ('@' || a.handle))
		]];
	};

	actor_auth_pw = {
		params = {lib.mem.ptr(int8),rawstring,lib.mem.ptr(int8),lib.store.inet}, sql = [[
			select a.aid from parsav_auth as a
				left join parsav_actors as u on u.id = a.uid
			where (a.uid is null or u.handle = $1::text or (
					a.uid = 0 and a.name = $1::text
				)) and
				(a.kind = 'trust' or (a.kind = $2::text and a.cred = $3::bytea)) and
				(a.netmask is null or a.netmask >> $4::inet)
			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 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
		]];
	};

	actor_stats = {
		params = {uint64}, sql = ([[
			with tweets as (
				select from parsav_posts where author = $1::bigint
			),
			follows as (
				select relatee as user from parsav_rels
					where relator = $1::bigint and kind = <follow>
			),
			followers as (
				select relator as user from parsav_rels
					where relatee = $1::bigint and kind = <follow>
			),
			mutuals as (select * from follows intersect select * from followers)

			select count(tweets.*)::bigint,
			       count(follows.*)::bigint,
				   count(followers.*)::bigint,
				   count(mutuals.*)::bigint
			from tweets, follows, followers, mutuals
		]]):gsub('<(%w+)>',function(r) return tostring(lib.store.relation[r]) end)
	};

	actor_auth_how = {
		params = {rawstring, lib.store.inet}, sql = [[
		with mts as (select a.kind from parsav_auth as a
			left join parsav_actors as u on u.id = a.uid
			where (a.uid is null or u.handle = $1::text or (
					a.uid = 0 and a.name = $1::text
................................................................................
				(select count(*) from mts where kind like 'challenge-%') > 0,
				(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 knownsince)::bigint,
				   coalesce(a.handle || '@' || s.domain,
				            '@' || a.handle) as xid,

			       au.restrict,
						array['post'  ] <@ au.restrict as can_post,
						array['edit'  ] <@ au.restrict as can_edit,
						array['acct'  ] <@ au.restrict as can_acct,
................................................................................
	for j=0,sz do i.v6[j] = v[4 + j] end -- 😬
	return i
end
pqr.methods.int = macro(function(self, ty, row, col)
	return quote
		var i: ty:astype()
		var v = lib.pq.PQgetvalue(self.res, row, col)
		--i = @[&uint64](v)
		lib.math.netswap_ip(ty, v, &i)
	in i end
end)

local pqt = {
	[lib.store.inet] = function(cidr)
		local tycode = cidr and 0x01 or 0x00
................................................................................
			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
			counters[i] = `lib.trn([args[i]] == nil, 0, lib.str.sz([args[i]]))
			casts[i] = `[&int8]([args[i]])
			dumpers[#dumpers+1] = `lib.io.fmt([tostring(i)..'. got rawstr %s\n'], [args[i]])
		elseif ty == lib.store.inet then -- assume not CIDR
			counters[i] = `lib.trn([args[i]].pv == 4,4,16)+4
			casts[i] = quote
				var ipbuf: int8[20]
				;[pqt[lib.store.inet](false)]([args[i]], [&uint8](&ipbuf))
			in &ipbuf[0] end
			dumpers[#dumpers+1] = `lib.io.fmt([tostring(i)..'. got inet\n'])
		elseif ty.ptr_basetype == int8 or ty.ptr_basetype == uint8 then
			counters[i] = `[args[i]].ct
			casts[i] = `[&int8]([args[i]].ptr)
			dumpers[#dumpers+1] = `lib.io.fmt([tostring(i)..'. got ptr %llu %.*s\n'], [args[i]].ct, [args[i]].ct, [args[i]].ptr)
		elseif ty:isintegral() then
			counters[i] = ty.bytes
			casts[i] = `[&int8](&[args[i]])
			dumpers[#dumpers+1] = `lib.io.fmt([tostring(i)..'. got int %llu\n'], [args[i]])
			fixers[#fixers + 1] = quote
				--lib.io.fmt('uid=%llu(%llx)\n',[args[i]],[args[i]])
				[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
	end
end

local terra row_to_actor(r: &pqr, row: intptr): lib.mem.ptr(lib.store.actor)
	var a: lib.mem.ptr(lib.store.actor)

	if r:cols() >= 9 then 
		a = [ lib.str.encapsulate(lib.store.actor, {
			nym = {`r:string(row,1), `r:len(row,1)+1};
			bio = {`r:string(row,4), `r:len(row,4)+1};
			avatar = {`r:string(row,5), `r:len(row,5)+1};
			handle = {`r:string(row, 2); `r:len(row,2) + 1};
			xid = {`r:string(row, 10); `r:len(row,10) + 1};
		}) ]
	else
		a = [ lib.str.encapsulate(lib.store.actor, {
			nym = {`r:string(row,1), `r:len(row,1)+1};
			bio = {`r:string(row,4), `r:len(row,4)+1};
			avatar = {`r:string(row,5), `r:len(row,5)+1};
			handle = {`r:string(row, 2); `r:len(row,2) + 1};
		}) ]
		a.ptr.xid = nil
	end
	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
	return a
end

local checksha = function(src, hash, origin, username, pw)

	local validate = function(kind, cred, credlen)
		return quote 
			var r = queries.actor_auth_pw.exec(
				[&lib.store.source](src),
				username,
				kind,
				[lib.mem.ptr(int8)] {ptr=[&int8](cred), ct=credlen},
				origin)











			if r.sz > 0 then -- found a record! stop here
				var aid = r:int(uint64, 0,0)
				r:free()
				return aid

			end
		end
	end
	
	local out = symbol(uint8[64])
	local vdrs = {}

		local alg = lib.md['MBEDTLS_MD_SHA' .. tostring(hash)]
		vdrs[#vdrs+1] = quote
			if lib.md.mbedtls_md(lib.md.mbedtls_md_info_from_type(alg),
				[&uint8](pw.ptr), pw.ct, out) ~= 0 then
				lib.bail('hashing failure!')
			end
			[ validate(string.format('pw-sha%u', hash), `&out[0], hash / 8) ]
		end

	return quote
		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 {
................................................................................
		end
	end];

	actor_auth_how = [terra(
			src: &lib.store.source,
			ip: lib.store.inet,
			username: rawstring
		): {lib.store.credset, bool}
		var cs: lib.store.credset cs:clear();
		var r = queries.actor_auth_how.exec(src, username, ip) 
		if r.sz == 0 then return cs, false end -- just in case
		defer r:free()
		(cs.pw << r:bool(0,0))
		(cs.otp << r:bool(0,1))
		(cs.challenge << r:bool(0,2))
		(cs.trust << r:bool(0,3))
		return cs, true
	end];
	 
	actor_auth_pw = [terra(
			src: &lib.store.source,
			ip: lib.store.inet,
			username: lib.mem.ptr(int8),
			cred: lib.mem.ptr(int8)
		): uint64









		[ checksha(`src, 256, ip, username, cred) ] -- most common
		[ checksha(`src, 512, ip, username, cred) ] -- most secure
		[ checksha(`src, 384, ip, username, cred) ] -- weird
		[ checksha(`src, 224, ip, username, cred) ] -- weirdest

		-- TODO: check pbkdf2-hmac
		-- TODO: check OTP
		return 0
	end];

	actor_stats = [terra(src: &lib.store.source, uid: uint64)
		var r = queries.actor_stats.exec(src, uid)
		if r.sz == 0 then lib.bail('error fetching actor stats!') end
		var s: lib.store.actor_stats
		s.posts = r:int(uint64, 0, 0)
		s.follows = r:int(uint64, 0, 1)
		s.followers = r:int(uint64, 0, 2)
		s.mutuals = r:int(uint64, 0, 3)
		return s
	end];

	actor_session_fetch = [terra(
		src: &lib.store.source,
		aid: uint64,
		ip : lib.store.inet
	): { lib.stat(lib.store.auth), lib.mem.ptr(lib.store.actor) }
		var r = queries.actor_session_fetch.exec(src, aid, ip)
................................................................................

			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 }
	end];
}

return b

Modified config.lua from [7cb566b503] to [dc89401662].

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
	dist      = default('parsav_dist', coalesce(
		os.getenv('NIX_PATH')  and 'nixos',
		os.getenv('NIX_STORE') and 'nixos',
	''));
	tgttrip   = default('parsav_arch_triple'); -- target triple, used in xcomp
	tgtcpu    = default('parsav_arch_cpu'); -- target cpu, used in xcomp
	tgthf     = u.tobool(default('parsav_arch_armhf',true)); 

	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'};



	};
}
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']]







>











>
>
>







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
	dist      = default('parsav_dist', coalesce(
		os.getenv('NIX_PATH')  and 'nixos',
		os.getenv('NIX_STORE') and 'nixos',
	''));
	tgttrip   = default('parsav_arch_triple'); -- target triple, used in xcomp
	tgtcpu    = default('parsav_arch_cpu'); -- target cpu, used in xcomp
	tgthf     = u.tobool(default('parsav_arch_armhf',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']]

Modified http.t from [e5e590a634] to [654249752e].

1
2
3
4
5








6
7
8
9
10
11
12
-- vim: ft=terra
local m = {}
local util = dofile('common.lua')

m.method = lib.enum { 'get', 'post', 'head', 'options', 'put', 'delete' }









m.findheader = terralib.externfunction('mg_http_get_header', {&lib.net.mg_http_message, rawstring} -> &lib.mem.ref(int8)) -- unfortunately necessary to access this function, as its return type conflicts with a function name

struct m.header {
	key: rawstring
	value: rawstring
}





>
>
>
>
>
>
>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-- vim: ft=terra
local m = {}
local util = dofile('common.lua')

m.method = lib.enum { 'get', 'post', 'head', 'options', 'put', 'delete' }
m.mime = lib.enum {
	'html'; -- default
	'json';
	'mkdown';
	'text';
	'ansi';
	'none';
}

m.findheader = terralib.externfunction('mg_http_get_header', {&lib.net.mg_http_message, rawstring} -> &lib.mem.ref(int8)) -- unfortunately necessary to access this function, as its return type conflicts with a function name

struct m.header {
	key: rawstring
	value: rawstring
}

Modified makefile from [2d2acfe121] to [3210eb684d].

1
2
3



4
5
6
7











8
9
10
11
12
13
14
..
39
40
41
42
43
44
45

46
47
48
49
50
51
52
53
54
55
56
57
58
dl = git
dbg-flags = $(if $(dbg),-g)




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












clean:
	rm parsav parsav.o

install: parsav
	mkdir $(prefix)/bin
	cp $< $(prefix)/bin/
................................................................................
	cd lib/json-c && cmake .
lib/json-c/libjson-c.a: lib/json-c/Makefile
	$(MAKE) -C lib/json-c
lib/mbedtls/library/%.a: lib/mbedtls 
	$(MAKE) -C lib/mbedtls/library $*.a

ifeq ($(dl), git)

lib/mongoose: lib
	cd lib && git clone https://github.com/cesanta/mongoose.git
lib/mbedtls: lib
	cd lib && git clone https://github.com/ARMmbed/mbedtls.git
lib/json-c: lib
	cd lib && git clone https://github.com/json-c/json-c.git
else
lib/%: lib/%.tar.gz
	cd lib && tar zxf $*.tar.gz
	mv lib/$$(tar tf $< | head -n1) $@

ifeq ($(dl), wget)
    dlfile = wget "$(1)" -O "$(2)"



>
>
>
|

|

>
>
>
>
>
>
>
>
>
>
>







 







>

|

|

|







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
..
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
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
	sassc -t compressed $< $@

clean:
	rm parsav parsav.o

install: parsav
	mkdir $(prefix)/bin
	cp $< $(prefix)/bin/
................................................................................
	cd lib/json-c && cmake .
lib/json-c/libjson-c.a: lib/json-c/Makefile
	$(MAKE) -C lib/json-c
lib/mbedtls/library/%.a: lib/mbedtls 
	$(MAKE) -C lib/mbedtls/library $*.a

ifeq ($(dl), git)
clone = git clone --depth 1 # save time
lib/mongoose: lib
	cd lib && $(clone) https://github.com/cesanta/mongoose.git
lib/mbedtls: lib
	cd lib && $(clone) https://github.com/ARMmbed/mbedtls.git
lib/json-c: lib
	cd lib && $(clone) https://github.com/json-c/json-c.git
else
lib/%: lib/%.tar.gz
	cd lib && tar zxf $*.tar.gz
	mv lib/$$(tar tf $< | head -n1) $@

ifeq ($(dl), wget)
    dlfile = wget "$(1)" -O "$(2)"

Modified math.t from [f661c3d77e] to [573a13128c].

145
146
147
148
149
150
151
152





































153
		buf = buf + 1
	end
end

terra m.b32str(a: lib.mem.ptr(uint64))
	
end






































return m








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

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
		buf = buf + 1
	end
end

terra m.b32str(a: lib.mem.ptr(uint64))
	
end

terra m.decstr(val: intptr, buf: &int8): rawstring
-- works backwards to avoid copies. log10(2^64) ≈ 19.2 and we
-- need a byte for NUL so buf MUST point to THE END OF a buffer
-- at least 21 bytes long
	@buf = 0
	if val > 0 then while val > 0 do
		buf = buf - 1
		var dgt = val % 10
		val = val / 10
		@buf = 0x30 + dgt
	end else
		buf = buf - 1
		@buf = 0x30
	end
	return buf
end

terra m.decstr_friendly(val: intptr, buf: &int8): rawstring
-- as above except needs size-28 buffers, on account of all the commas
	@buf = 0
	var dgtct: uint8 = 0
	if val > 0 then while val > 0 do
		buf = buf - 1
		var dgt = val % 10
		val = val / 10
		@buf = 0x30 + dgt
		if dgtct == 2 and val > 0 then
			buf = buf - 1 @buf = @',' 
			dgtct = 0
		else dgtct = dgtct + 1 end
	end else
		buf = buf - 1
		@buf = 0x30
	end
	return buf
end

return m

Modified parsav.md from [93a3706cc3] to [eb5d145ae6].

11
12
13
14
15
16
17












18
19
20
21
22
23
24
25
26
27

* mongoose
* json-c
* mbedtls
* **postgresql backend:**
  * postgresql-libs 













## building

first, either install any missing dependencies as shared libraries, or build them as static libraries with the command `make dep.$LIBRARY`. as a shortcut, `make dep` will build all dependencies as static libraries. note that if the build system finds a static version of a librari in the `lib/` folder, it will use that instead of any system library.

postgresql-libs must be installed systemwide, as `parsav` does not currently provide for statically compiling and linking it

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








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


|







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

* mongoose
* json-c
* mbedtls
* **postgresql backend:**
  * postgresql-libs 

additional build-time dependencies are necessary if you are building directly from trunk, rather than from a release tarball that includes certain build artifacts which need to be embedded in the binary:

* inkscape, for rendering out UI graphics
* cwebp (libwebp package), for transforming inkscape PNGs to webp
* sassc, for compiling the SCSS stylesheet into its final CSS

all builds require terra, which, unfortunately, requires installing an older version of llvm, v9 at the latest (which i develop parsav under). with any luck, your distro will be clever enough to package terra and its dependencies properly (it's trivial on nix, tho you'll need to tweak the terra expression to select a more recent llvm package); Arch Linux is one of those distros which is not so clever, and whose (AUR) terra package is totally broken. due to these unfortunate circumstances, terra is distributed not just in source form, but also in the the form of LLVM IR. distributions will also be made in the form of tarballed object code and assembly listings for various common platforms, currently including x86-32/64, arm7hf, aarch64, riscv, mips32/64, and ppc64/64le.

i've noticed that terra (at least with llvm9) seems to get a bit cantankerous and trigger llvm to fail with bizarre errors when you try to cross-compile parsav from x86-64 to any other platform, even x86-32. i don't know if this problem exists on other architectures or in what form, but as a workaround, the current cross-compile process consists of generating LLVM IR (ostensible for x86-64, though this is in reality an architecture-independent language), and then compiling that down to an object file with llc. this is an enormous hassle; hopefully the terra people will fix this eventually.

also note that, while parsav has a flag to build with ASAN, ASAN has proven unusable for most purposes as it routinely reports false positive buffer-heap-overflows. if you figure out how to defuckulate this, i will be overjoyed.

## building

first, either install any missing dependencies as shared libraries, or build them as static libraries with the command `make dep.$LIBRARY`. as a shortcut, `make dep` will build all dependencies as static libraries. note that if the build system finds a static version of a library in the `lib/` folder, it will use that instead of any system library. note that these commands require GNU make (it may be installed as `gmake` on your system), although this is a fairly soft dependency -- if you really need to build it on BSD make, you can probably translate it with a minute or so of work; you'll just have to do some of the various gmake functions' work manually. this may be worthwhile if you're packaging for a BSD.

postgresql-libs must be installed systemwide, as `parsav` does not currently provide for statically compiling and linking it

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

Modified parsav.t from [41c6f93980] to [11ad9b3025].

197
198
199
200
201
202
203

204
205





206
207
208
209
210
211
212
...
214
215
216
217
218
219
220
221
222







223
224
225
226
227
228
229
...
306
307
308
309
310
311
312


313
314

315
316
317
318
319
320
321
...
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
end)
lib.enum = function(tbl)
	local ty = uint8
	if #tbl >= 2^32 then ty = uint64 -- hey, can't be too safe
	elseif #tbl >= 2^16 then ty = uint32
	elseif #tbl >= 2^8 then ty = uint16 end
	local o = { t = ty }

	for i, name in ipairs(tbl) do
		o[name] = i





	end
	return o
end
lib.set = function(tbl)
	local bytes = math.ceil(#tbl / 8)
	local o = {}
	for i, name in ipairs(tbl) do o[name] = i end
................................................................................
	local struct bit { _v: intptr _set: &set}
	terra set:clear() for i=0,bytes do self._store[i] = 0 end end
	terra set:fill() for i=0,bytes do self._store[i] = 0xFF end end
	set.members = tbl
	set.name = string.format('set<%s>', table.concat(tbl, '|'))
	set.metamethods.__entrymissing = macro(function(val, obj)
		if o[val] == nil then error('value ' .. val .. ' not in set') end
		return `bit { _v=[o[val] - 1], _set = &obj }
	end)







	set.methods.dump = macro(function(self)
		local q = quote lib.io.say('dumping set:\n') end
		for i,v in ipairs(tbl) do
			q = quote
				[q]
				if [bool](self.[v])
					then lib.io.say([' - ' .. v .. ': true\n'])
................................................................................
for k,v in pairs(data.view) do
	local t = lib.tpl.mk { body = v, id = 'view/'..k }
	data.view[k] = t
end

lib.load {
	'srv';


	'render:profile';
	'render:userpage';

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

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







>

|
>
>
>
>
>







 







|

>
>
>
>
>
>
>







 







>
>


>







 







|
|







197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
...
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
...
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
...
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
end)
lib.enum = function(tbl)
	local ty = uint8
	if #tbl >= 2^32 then ty = uint64 -- hey, can't be too safe
	elseif #tbl >= 2^16 then ty = uint32
	elseif #tbl >= 2^8 then ty = uint16 end
	local o = { t = ty }
	local strings = {}
	for i, name in ipairs(tbl) do
		o[name] = i - 1
		strings[i] = `[lib.mem.ref(int8)]{ptr=[name], ct=[#name]}
	end
	o._str = terra(val: ty)
		var l = array([strings])
		return l[val]
	end
	return o
end
lib.set = function(tbl)
	local bytes = math.ceil(#tbl / 8)
	local o = {}
	for i, name in ipairs(tbl) do o[name] = i end
................................................................................
	local struct bit { _v: intptr _set: &set}
	terra set:clear() for i=0,bytes do self._store[i] = 0 end end
	terra set:fill() for i=0,bytes do self._store[i] = 0xFF end end
	set.members = tbl
	set.name = string.format('set<%s>', table.concat(tbl, '|'))
	set.metamethods.__entrymissing = macro(function(val, obj)
		if o[val] == nil then error('value ' .. val .. ' not in set') end
		return `bit { _v=[o[val] - 1], _set = &(obj) }
	end)
	terra set:sz()
		var ct: intptr = 0
		for i = 0, [#tbl] do
			if (self._store[i/8] and (1 << i % 8)) ~= 0 then ct = ct + 1 end
		end
		return ct
	end
	set.methods.dump = macro(function(self)
		local q = quote lib.io.say('dumping set:\n') end
		for i,v in ipairs(tbl) do
			q = quote
				[q]
				if [bool](self.[v])
					then lib.io.say([' - ' .. v .. ': true\n'])
................................................................................
for k,v in pairs(data.view) do
	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:userpage';
	'render:compose';
	'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
................................................................................

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 = {'-O4'}

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'

Added render/compose.t version [7a7e8f43ac].



























































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
-- vim: ft=terra
local terra 
render_compose(co: &lib.srv.convo, edit: &lib.store.post)
	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.ptr;
		title = 'compose';
		body = cotxt.ptr;
		class = 'compose';
		navlinks = co.navbar.ptr;
	}

	var hdrs = array(
		lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' }
	)
	doc:send(co.con,200,[lib.mem.ptr(lib.http.header)] {ct = 1, ptr = &hdrs[0]})
end

return render_compose

Added render/login.t version [0d69ec17a3].





























































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
-- vim: ft=terra
local terra 
login_form(co: &lib.srv.convo, user: &lib.store.actor, creds: &lib.store.credset, msg: &int8)
	var doc = data.view.docskel {
		instance = co.srv.cfg.instance.ptr;
		title = 'instance logon';
		class = 'login';
		navlinks = co.navbar.ptr;
	}

	if user == nil then
		var form = data.view.login_username {
			loginmsg = msg;
		}
		if form.loginmsg == nil then
			form.loginmsg = 'identify yourself for access to this instance.'
		end
		var formtxt = form:tostr()
		doc.body = formtxt.ptr
	elseif creds:sz() == 0 then
		co:complain(403,'access denied','your host is not eligible to authenticate as this user')
		return
	elseif creds:sz() == 1 then
		if creds.trust() then
			-- TODO log in immediately
			return
		end

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

		doc.body = ch:tostr().ptr
	else
		-- pick a method
	end

	var hdrs = array(
		lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' }
	)
	doc:send(co.con,200,[lib.mem.ptr(lib.http.header)] {ct = 1, ptr = &hdrs[0]})
	lib.mem.heapf(doc.body)
end

return login_form

Added render/nav.t version [2d4aa38bec].

































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

Modified render/profile.t from [a405db9158] to [ecfc7ba460].

1
2
3


























4
5
6
7
8


9
10
11

12

13

14

15



16
17
18
-- vim: ft=terra
local terra 
render_profile(actor: &lib.store.actor)


























	var profile = data.view.profile {
		nym = lib.coalesce(actor.nym, actor.handle);
		bio = lib.coalesce(actor.bio, "tall, dark, and mysterious");
		xid = actor.xid;
		avatar = "/no-avatars-yet.png";



		nposts = '0', nfollows = '0';
		nfollowers = '0', nmutuals = '0';

		tweetday = 'novembuary 67th';

	}



	return profile:tostr()



end

return render_profile


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


|

<
>
>

|
<
>
|
>
|
>
|
>
|
>
>
>



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
-- vim: ft=terra
local terra 
render_profile(co: &lib.srv.convo, actor: &lib.store.actor)
	var aux: lib.str.acc
	var auxp: rawstring
	if co.aid ~= 0 and co.who.id == actor.id then
		auxp = '<a href="/conf/profile">alter</a>'
	elseif co.aid ~= 0 then
		aux:compose('<a href="/', actor.xid, '/follow">follow</a><a href="/',
			actor.xid, '/chat">chat</a>')
		if co.who.rights.powers:affect_users() then
			aux:push('<a href="/',11):push(actor.xid,0):push('/ctl">control</a>',17)
		end
		auxp = aux.buf
	else
		aux:compose('<a href="/', actor.xid, '/follow">remote follow</a>')
	end
	var avistr: lib.str.acc if actor.origin == 0 then
		avistr:compose('/avi/',actor.handle)
	end
	var timestr: int8[26] lib.osclock.ctime_r(&actor.knownsince, &timestr[0])

	var strfbuf: int8[28*4]
	var stats = co.srv:actor_stats(actor.id)
		var sn_posts = lib.math.decstr_friendly(stats.posts, &strfbuf[ [strfbuf.type.N - 1] ])
		var sn_follows = lib.math.decstr_friendly(stats.follows, sn_posts - 1)
		var sn_followers = lib.math.decstr_friendly(stats.followers, sn_follows - 1)
		var sn_mutuals = lib.math.decstr_friendly(stats.mutuals, sn_followers - 1)
	
	var profile = data.view.profile {
		nym = lib.coalesce(actor.nym, actor.handle);
		bio = lib.coalesce(actor.bio, "<em>tall, dark, and mysterious</em>");
		xid = actor.xid;

		avatar = lib.trn(actor.origin == 0, avistr.buf,
			lib.coalesce(actor.avatar, '/s/default-avatar.webp'));

		nposts = sn_posts, nfollows = sn_follows;

		nfollowers = sn_followers, nmutuals = sn_mutuals;
		tweetday = timestr;
		timephrase = lib.trn(actor.origin == 0, 'joined', 'known since');

		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 aux:free() end
	return ret
end

return render_profile

Modified render/userpage.t from [052285d84c] to [cdf1e65d22].

3
4
5
6
7
8
9
10
11
12
13
14
15
16

17
18
19
20
21
22
23
24
25
render_userpage(co: &lib.srv.convo, actor: &lib.store.actor)
	var ti: lib.str.acc defer ti:free()
	if co.aid ~= 0 and co.who.id == actor.id then
		ti:compose('my profile')
	else
		ti:compose('profile :: ', actor.handle)
	end
	var pftxt = lib.render.profile(actor) defer pftxt:free()

	var doc = data.view.docskel {
		instance = co.srv.cfg.instance.ptr;
		title = ti.buf;
		body = pftxt.ptr;
		class = 'profile';

	}

	var hdrs = array(
		lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' }
	)
	doc:send(co.con,200,[lib.mem.ptr(lib.http.header)] {ct = 1, ptr = &hdrs[0]})
end

return render_userpage







|






>









3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
render_userpage(co: &lib.srv.convo, actor: &lib.store.actor)
	var ti: lib.str.acc defer ti:free()
	if co.aid ~= 0 and co.who.id == actor.id then
		ti:compose('my profile')
	else
		ti:compose('profile :: ', actor.handle)
	end
	var pftxt = lib.render.profile(co,actor) defer pftxt:free()

	var doc = data.view.docskel {
		instance = co.srv.cfg.instance.ptr;
		title = ti.buf;
		body = pftxt.ptr;
		class = 'profile';
		navlinks = co.navbar.ptr;
	}

	var hdrs = array(
		lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' }
	)
	doc:send(co.con,200,[lib.mem.ptr(lib.http.header)] {ct = 1, ptr = &hdrs[0]})
end

return render_userpage

Modified route.t from [70d0da6b8e] to [d7d680b0a3].

53
54
55
56
57
58
59




















































































60
61
62
63
64
65
66
..
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
		co:complain(404, 'no such user', 'no user by that ID is known to this instance')
		return
	end
	defer actor:free()

	lib.render.userpage(co, actor.ptr)
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]
................................................................................
		}
		[branches]
		do return false end
		::[send]:: page:send(co.con) return true
	end
end

http.static_content:printpretty()






-- entry points
terra r.dispatch_http(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t)




	if uri.ptr[0] ~= @'/' then
		co:complain(404, 'what the hell', 'how did you do that')











	elseif uri.ptr[1] == @'@' then
		http.actor_profile_xid(co, uri, meth)

	elseif uri.ptr[1] == @'s' and uri.ptr[2] == @'/' and uri.ct > 3 then
		if meth ~= method.get then goto wrongmeth end
		if not http.static_content(co, uri.ptr + 3, uri.ct - 3) then goto notfound end
	else




















		var path = lib.http.hier(uri) defer path:free()
		if path.ptr[0]:cmp(lib.str.lit('user')) then
			http.actor_profile_uid(co, path, meth)
		else goto notfound end

	end

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







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







 







<
>
>
>
>
>



>
>
>
>


>
>
>
>
>
>
>
>
>
>
>


>



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




>


|


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
...
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
		co:complain(404, 'no such user', 'no user by that ID is known to this instance')
		return
	end
	defer actor:free()

	lib.render.userpage(co, actor.ptr)
end

terra http.login_form(co: &lib.srv.convo, meth: method.t)
	if meth == method.get then
		-- request a username
		lib.render.login(co, nil, nil, nil)
	elseif meth == method.post then
		var usn, usnl = co:postv('user')
		lib.dbg('got name ',{usn,usnl})
		lib.io.fmt('name len %llu\n',usnl)
		var am, aml = co:postv('authmethod')
		var chrs, chrsl = co:postv('response')
		var cs, authok = co.srv:actor_auth_how(co.peer, usn)
		var act = co.srv:actor_fetch_xid([lib.mem.ptr(int8)] {
			ptr = usn, ct = usnl
		})
		if authok == false then
			lib.render.login(co, nil, nil, 'access denied')
			return
		end
		var fakeact = false
		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.login(co, act.ptr, &cs, nil)
		else var aid: uint64 = 0
			lib.dbg('authentication attempt beginning')
			-- attempt login with provided method
			if lib.str.ncmp('pw', am, lib.math.biggest(2,aml)) == 0 and chrs ~= nil then
				aid = co.srv:actor_auth_pw(co.peer,
					[lib.mem.ptr(int8)]{ptr=usn,ct=usnl},
					[lib.mem.ptr(int8)]{ptr=chrs,ct=chrsl})
			elseif lib.str.ncmp('otp', am, lib.math.biggest(2,aml)) == 0 and chrs ~= nil then
				lib.dbg('using otp auth')
				-- ··· --
			else
				lib.dbg('invalid auth method')
			end

			lib.io.fmt('login got aid = %llu\n', aid)
			-- error out
			if aid == 0 then
				lib.render.login(co, nil, nil, 'authentication failure')
			else
				var sesskey: int8[lib.session.maxlen + #lib.session.cookiename + #"=; Path=/" + 1]
				do var p = &sesskey[0]
					p = lib.str.ncpy(p, [lib.session.cookiename .. '='], [#lib.session.cookiename + 1])
					p = p + lib.session.cookie_gen(co.srv.cfg.secret, aid, lib.osclock.time(nil), p)
					lib.dbg('sending cookie',&sesskey[0])
					p = lib.str.ncpy(p, '; Path=/', 9)
				end
				co:reroute_cookie('/', &sesskey[0])
			end
		end
		if act.ptr ~= nil and fakeact == false then act:free() end
	else
		::wrongmeth:: co:complain(405, 'method not allowed', 'that method is not meaningful for this endpoint') do return end
	end
	return
end

terra http.post_compose(co: &lib.srv.convo, meth: method.t)
	if meth == method.get then
		lib.render.compose(co, nil)
	elseif meth == method.post then
		if co.who.rights.powers.post() == false then
			co:complain(401,'insufficient privileges','you lack the <strong>post</strong> power and cannot perform this action') return
		end

	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]
................................................................................
		}
		[branches]
		do return false end
		::[send]:: page:send(co.con) return true
	end
end



terra http.local_avatar(co: &lib.srv.convo, handle: lib.mem.ptr(int8))
	-- TODO retrieve user avatars
	co:reroute('/s/default-avatar.webp')
end

-- entry points
terra r.dispatch_http(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t)
	lib.dbg('handling URI of form ', {uri.ptr,uri.ct})
	co.navbar = lib.render.nav(co)
	-- some routes are non-hierarchical, and can be resolved with a simple strcmp
	-- we run through those first before giving up and parsing the URI
	if uri.ptr[0] ~= @'/' then
		co:complain(404, 'what the hell', 'how did you do that')
		return
	elseif uri.ct == 1 then -- root
		lib.io.fmt('root directory, aid is %llu\n', co.aid)
		if (co.srv.cfg.pol_sec == lib.srv.secmode.private or
		   co.srv.cfg.pol_sec == lib.srv.secmode.lockdown) and co.aid == 0 then
		   http.login_form(co, meth)
		else
			-- FIXME display home screen
			goto notfound
		end
		return
	elseif uri.ptr[1] == @'@' then
		http.actor_profile_xid(co, uri, meth)
		return
	elseif uri.ptr[1] == @'s' and uri.ptr[2] == @'/' and uri.ct > 3 then
		if meth ~= method.get then goto wrongmeth end
		if not http.static_content(co, uri.ptr + 3, uri.ct - 3) then goto notfound end
		return
	elseif lib.str.ncmp('/avi/', uri.ptr, 5) == 0 then
		http.local_avatar(co, [lib.mem.ptr(int8)] {ptr = uri.ptr + 5, ct = uri.ct - 5})
		return
	elseif lib.str.ncmp('/compose', uri.ptr, lib.math.biggest(uri.ct,8)) == 0 then
		if co.aid == 0 then co:reroute('/login') return end
		http.post_compose(co,meth)
		return
	elseif lib.str.ncmp('/login', uri.ptr, lib.math.biggest(uri.ct,6)) == 0 then
		if co.aid == 0
			then http.login_form(co, meth)
			else co:reroute('/')
		end
		return
	elseif lib.str.ncmp('/logout', uri.ptr, lib.math.biggest(uri.ct,7)) == 0 then
		if co.aid == 0
			then goto notfound
			else co:reroute_cookie('/','auth=; Path=/')
		end
		return
	else -- hierarchical routes
		var path = lib.http.hier(uri) defer path:free()
		if path.ptr[0]:cmp(lib.str.lit('user')) then
			http.actor_profile_uid(co, path, meth)
		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

Modified schema.sql from [636689e0dd] to [a3359b8b76].

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
..
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
\prompt 'domain name: ' domain
\prompt 'instance name: ' inst
\prompt 'bind to socket: ' bind




\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.'
\prompt 'admin 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'),


	('administrator',:'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

);

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
	bio text,


	rank smallint not null default 0,
	quota integer not null default 1000,
	key bytea, -- private if localactor; public if remote

	
	unique (handle,origin)
);

drop table if exists parsav_rights cascade;
create table parsav_rights (
	key text,
................................................................................
		('censor',true),
		('suspend',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,

	body text,
	posted timestamp not null,
	discovered timestamp not null,
	scope smallint not null,
	convo bigint, parent bigint,
	circles bigint[], mentions bigint[]



);

drop table if exists parsav_conversations cascade;
create table parsav_conversations (
	id bigint primary key default (1+random()*(2^63-1))::bigint,
	uri text not null,
	discovered timestamp not null,
	head bigint references parsav_posts(id)
);

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. follower
	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,
	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,
	actor bigint references parsav_actors(id)
		on delete cascade,
	post bigint not null
);




























































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
60
..
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
\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,
	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

	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,
................................................................................
		('censor',true),
		('suspend',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,
	posted     timestamp not null,
	discovered timestamp not null,
	scope      smallint not null,

	convo      bigint,
	parent     bigint,
	circles    bigint[],
	mentions   bigint[]
);

drop table if exists parsav_conversations cascade;
create table parsav_conversations (
	id         bigint primary key default (1+random()*(2^63-1))::bigint,
	uri        text      not null,
	discovered timestamp not null,
	head       bigint references parsav_posts(id)
);

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[],

	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
);

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

Modified session.t from [58f0eab21d] to [e8a79576f0].

3
4
5
6
7
8
9

10
11
12
13
14
15
16
-- are tracked by storing an encrypted cookie which contains an authid,
-- a login epoch time, and a truncated hmac code authenticating both, all
-- encoded using Shorthand. we need functions to generate and parse these

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

}

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







>







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

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

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

Modified srv.t from [ed3d5ec62e] to [afa0417e30].

1
2
3
4
5
6
7
8


9
10
11
12
13
14
15
..
68
69
70
71
72
73
74







75
76
77
78
79
80
81
..
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
...
128
129
130
131
132
133
134




135
136
137
138
139
140
141



142







































143
144
145
146
147
148
149
...
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
...
204
205
206
207
208
209
210






211
212
213
214
215


216
217
218
219
220
221
222
...
292
293
294
295
296
297
298















299
300
301

302
303

304

305

306
307
308
309
310
311
312
313
...
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
...
362
363
364
365
366
367
368

























369
370
371
372
373
374

375
-- vim: ft=terra
local util = dofile 'common.lua'

local struct srv
local struct cfgcache {
	secret: lib.mem.ptr(int8)
	instance: lib.mem.ptr(int8)
	overlord: &srv


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

local struct convo {
	srv: &srv
	con: &lib.net.mg_connection
	msg: &lib.net.mg_http_message
	aid: uint64 -- 0 if logged out
	who: &lib.store.actor -- who we're logged in as, if aid ~= 0







}

-- this is unfortunately necessary to work around a terra bug
-- it can't seem to handle forward-declarations of structs in C

local getpeer
do local struct strucheader {
................................................................................
		peer: lib.net.mg_addr
	}
	terra getpeer(con: &lib.net.mg_connection)
		return [&strucheader](con).peer
	end
end























terra convo:complain(code: uint16, title: rawstring, msg: rawstring)
	var hdrs = array(lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' })

	var ti: lib.str.acc ti:compose('error :: ', title) defer ti:free()

	var body = data.view.docskel {
		instance = self.srv.cfg.instance.ptr;
		title = ti.buf;
		body = msg;
		class = 'error';

	}

	if body.body == nil then
		body.body = "i'm sorry, dave. i can't let you do that"
	end

	body:send(self.con, code, [lib.mem.ptr(lib.http.header)] {
		ptr = &hdrs[0], ct = [hdrs.type.N]
	})
end




























local urimatch = macro(function(uri, ptn)
	return `lib.net.mg_globmatch(ptn, [#ptn], uri.ptr, uri.ct+1)
end)

local route = {} -- these are defined in route.t, as they need access to renderers
terra route.dispatch_http ::  {&convo, lib.mem.ptr(int8), lib.http.method.t} -> {}






















local handle = {
	http = terra(con: &lib.net.mg_connection, event: int, p: &opaque, ext: &opaque)
		var server = [&srv](ext)
		var mgpeer = getpeer(con)
		var peer = lib.store.inet { port = mgpeer.port; }
		if mgpeer.is_ip6 then peer.pv = 6 else peer.pv = 4 end
................................................................................
		end
		-- the peer property is currently broken and there is precious
		-- little i can do about this -- it always reports a peer v4 IP
		-- of 0.0.0.0, altho the port seems to come through correctly.
		-- for now i'm leaving it as is, but note that netmask restrictions
		-- WILL NOT WORK until upstream gets its shit together. FIXME





		switch event do
			case lib.net.MG_EV_HTTP_MSG then
				lib.dbg('routing HTTP request')
				var msg = [&lib.net.mg_http_message](p)
				var co = convo {
					con = con, srv = server, msg = msg;
					aid = 0, who = nil;



				}








































				-- we need to check if there's any cookies sent with the request,
				-- and if so, whether they contain any credentials. this will be
				-- used to set the auth parameters in the http conversation
				var cookies_p = lib.http.findheader(msg, 'Cookie')
				if cookies_p ~= nil then
					var cookies = cookies_p.ptr
................................................................................
								key.ct = (cookies + i) - key.ptr
								val.ptr = cookies + i + 1
							end
							i = i + 1
						else
							if cookies[i] == @';' then
								val.ct = (cookies + i) - val.ptr
								if lib.str.ncmp(key.ptr, 'auth', key.ct) == 0 then
									goto foundcookie
								end

								i = i + 1
								i = lib.str.ffw(cookies + i, cookies_p.ct - i) - cookies
								key.ptr = cookies + i
								val.ptr = nil
							else i = i + 1 end
						end
					end
					if val.ptr == nil then goto nocookie end
					val.ct = (cookies + i) - val.ptr
					if lib.str.ncmp(key.ptr, 'auth', key.ct) ~= 0 then
						goto nocookie
					end
					::foundcookie:: do
						var aid = lib.session.cookie_interpret(server.cfg.secret,
							[lib.mem.ptr(int8)]{ptr=val.ptr,ct=val.ct},
							lib.osclock.time(nil))
						if aid ~= 0 then co.aid = aid end
................................................................................
					end
					uri.ct = msg.uri.len
				else uri.ct = urideclen end
				lib.dbg('routing URI ', {uri.ptr, uri.ct})
				
				if lib.str.ncmp('GET', msg.method.ptr, msg.method.len) == 0 then
					route.dispatch_http(&co, uri, [lib.http.method.get])






				else
					co:complain(400,'unknown method','you have submitted an invalid http request')
				end

				if co.aid ~= 0 then lib.mem.heapf(co.who) end


			end
		end
	end;
}

local terra cfg(s: &srv, befile: rawstring)
	lib.report('configuring backends from ', befile)
................................................................................
	if c.sz > 0 then
		s.sources = c:crush()
	else
		s.sources.ptr = nil
		s.sources.ct = 0
	end
end
















terra srv:actor_auth_how(ip: lib.store.inet, usn: rawstring)
	var cs: lib.store.credset cs:clear()

	for i=0,self.sources.ct do
		var set: lib.store.credset = self.sources.ptr[i]:actor_auth_how(ip, usn)

		cs = cs + set

	end

	return cs
end

terra cfgcache.methods.load :: {&cfgcache} -> {}
terra cfgcache:init(o: &srv)
	self.overlord = o
	self:load()
end
................................................................................
		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)

	var buf: int8[lib.session.maxlen]
	var len = lib.session.cookie_gen(self.cfg.secret, 9139084444658983115ULL, lib.osclock.time(nil), &buf[0])
	buf[len] = 0
	
	var authid = lib.session.cookie_interpret(self.cfg.secret, [lib.mem.ptr(int8)] {ptr=buf, ct=len}, lib.osclock.time(nil))
	lib.io.fmt('generated cookie %s -- got authid %llu\n', buf, authid)

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

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

................................................................................
	end
	self.sources:free()
end

terra cfgcache:load()
	self.instance = self.overlord:conf_get('instance-name')
	self.secret = self.overlord:conf_get('server-secret')

























end

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

}


|





>
>







 







>
>
>
>
>
>
>







 







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




>



|

>










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







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







 







>
>
>
>






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







 







|












|







 







>
>
>
>
>
>





>
>







 







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



>

|
>
|
>
|
>
|







 







<
<
<
<
<
<
<







 







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






>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
..
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
..
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
...
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
...
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
...
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
...
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
...
490
491
492
493
494
495
496







497
498
499
500
501
502
503
...
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
-- vim: ft=terra
local util = dofile 'common.lua'
local secmode = lib.enum { 'public', 'private', 'lockdown', 'isolate' }
local struct srv
local struct cfgcache {
	secret: lib.mem.ptr(int8)
	instance: lib.mem.ptr(int8)
	overlord: &srv
	pol_sec: secmode.t
	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
}
................................................................................

local struct convo {
	srv: &srv
	con: &lib.net.mg_connection
	msg: &lib.net.mg_http_message
	aid: uint64 -- 0 if logged out
	who: &lib.store.actor -- who we're logged in as, if aid ~= 0
	peer: lib.store.inet
	reqtype: lib.http.mime.t -- negotiated content type
-- cache
	navbar: lib.mem.ptr(int8)
-- private
	varbuf: lib.mem.ptr(int8)
	vbofs: &int8
}

-- this is unfortunately necessary to work around a terra bug
-- it can't seem to handle forward-declarations of structs in C

local getpeer
do local struct strucheader {
................................................................................
		peer: lib.net.mg_addr
	}
	terra getpeer(con: &lib.net.mg_connection)
		return [&strucheader](con).peer
	end
end

terra convo:reroute_cookie(dest: rawstring, cookie: rawstring)
	var hdrs = array(
		lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' },
		lib.http.header { key = 'Location',     value = dest },
		lib.http.header { key = 'Set-Cookie',   value = cookie }
	)

	var body = data.view.docskel {
		instance = self.srv.cfg.instance.ptr;
		title = 'rerouting';
		body = 'you are being redirected';
		class = 'error';
		navlinks = '';
	}

	body:send(self.con, 303, [lib.mem.ptr(lib.http.header)] {
		ptr = &hdrs[0], ct = [hdrs.type.N] - lib.trn(cookie == nil,1,0)
	})
end

terra convo:reroute(dest: rawstring) self:reroute_cookie(dest,nil) end
 
terra convo:complain(code: uint16, title: rawstring, msg: rawstring)
	var hdrs = array(lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' })

	var ti: lib.str.acc ti:compose('error :: ', title) defer ti:free()
	var bo: lib.str.acc bo:compose('<div class="message"><img class="icon" src="/s/warn.webp"><h1>error</h1><p>',msg,'</p></div>') defer bo:free()
	var body = data.view.docskel {
		instance = self.srv.cfg.instance.ptr;
		title = ti.buf;
		body = bo.buf;
		class = 'error';
		navlinks = lib.coalesce(self.navbar.ptr, '');
	}

	if body.body == nil then
		body.body = "i'm sorry, dave. i can't let you do that"
	end

	body:send(self.con, code, [lib.mem.ptr(lib.http.header)] {
		ptr = &hdrs[0], ct = [hdrs.type.N]
	})
end

-- CALL ONLY ONCE PER VAR
terra convo:postv(name: rawstring)
	if self.varbuf.ptr == nil then
		self.varbuf = lib.mem.heapa(int8, self.msg.body.len + self.msg.query.len)
		self.vbofs = self.varbuf.ptr
	end
	var o = lib.net.mg_http_get_var(&self.msg.body, name, self.vbofs, self.varbuf.ct - (self.vbofs - self.varbuf.ptr))
	if o > 0 then
		var r = self.vbofs
		self.vbofs = self.vbofs + o
		return r, o
	else return nil, 0 end
end

terra convo:getv(name: rawstring)
	if self.varbuf.ptr == nil then
		self.varbuf = lib.mem.heapa(int8, self.msg.query.len + self.msg.body.len)
		self.vbofs = self.varbuf.ptr
	end
	var o = lib.net.mg_http_get_var(&self.msg.query, name, self.vbofs, self.varbuf.ct - (self.vbofs - self.varbuf.ptr))
	if o > 0 then
		var r = self.vbofs
		self.vbofs = self.vbofs + o
		return r, o
	else return nil, 0 end
end

local urimatch = macro(function(uri, ptn)
	return `lib.net.mg_globmatch(ptn, [#ptn], uri.ptr, uri.ct+1)
end)

local route = {} -- these are defined in route.t, as they need access to renderers
terra route.dispatch_http ::  {&convo, lib.mem.ptr(int8), lib.http.method.t} -> {}

local mimetypes = {
	{'html', 'text/html'};
	{'json', 'application/json'};
	{'mkdown', 'text/markdown'};
	{'text', 'text/plain'};
	{'ansi', 'text/x-ansi'};
}

local mimevar = symbol(lib.mem.ref(int8))
local mimeneg = `lib.http.mime.none

for i, t in ipairs(mimetypes) do
	local name, mime = t[1], t[2]
	mimeneg = quote
		var ret: lib.http.mime.t
		if lib.str.ncmp(mimevar.ptr, mime, lib.math.biggest(mimevar.ct, [#mime])) == 0 then
			ret = [lib.http.mime[name]]
		else ret = [mimeneg] end
	in ret end
end

local handle = {
	http = terra(con: &lib.net.mg_connection, event: int, p: &opaque, ext: &opaque)
		var server = [&srv](ext)
		var mgpeer = getpeer(con)
		var peer = lib.store.inet { port = mgpeer.port; }
		if mgpeer.is_ip6 then peer.pv = 6 else peer.pv = 4 end
................................................................................
		end
		-- the peer property is currently broken and there is precious
		-- little i can do about this -- it always reports a peer v4 IP
		-- of 0.0.0.0, altho the port seems to come through correctly.
		-- for now i'm leaving it as is, but note that netmask restrictions
		-- WILL NOT WORK until upstream gets its shit together. FIXME

		-- needs to check for an X-Forwarded-For header from nginx and
		-- use that instead of the peer iff peer is ::1/127.1 FIXME
		-- maybe also haproxy support?

		switch event do
			case lib.net.MG_EV_HTTP_MSG then
				lib.dbg('routing HTTP request')
				var msg = [&lib.net.mg_http_message](p)
				var co = convo {
					con = con, srv = server, msg = msg;
					aid = 0, who = nil, peer = peer;
					reqtype = lib.http.mime.none;
				} co.varbuf.ptr = nil
				  co.navbar.ptr = nil

				-- first, check for an accept header. if it's there, we need to
				-- iterate over the values and pick the highest-priority one
				do var acc = lib.http.findheader(msg, 'Accept')
					-- TODO handle q-value
					if acc.ptr ~= nil then
						var [mimevar] = [lib.mem.ref(int8)] { ptr = acc.ptr }
						var i = 0 while i < acc.ct do
							if acc.ptr[i] == @',' or acc.ptr[i] == @';' then
								mimevar.ct = (acc.ptr+i) - mimevar.ptr
								var t = [mimeneg]
								if t ~= lib.http.mime.none then
									co.reqtype = t
									goto foundtype
								end

								if acc.ptr[i] == @';' then -- fast-forward over q
									for j=i+1,acc.ct do i=j
										if acc.ptr[j] == @',' then break end
									end
								end
								
								while i < acc.ct and -- fast-forward over ws
									acc.ptr[i+1] == @' ' or
									acc.ptr[i+1] == @'\t'
								do i=i+1 end

								mimevar.ptr = acc.ptr + i + 1
							end
							i=i+1
						end
						if co.reqtype == lib.http.mime.none then
							mimevar.ct = acc.ct - (mimevar.ptr - acc.ptr)
							co.reqtype = [mimeneg]
							if co.reqtype == lib.http.mime.none then
								co.reqtype = lib.http.mime.html
							end
						end
					else co.reqtype = lib.http.mime.html end
				::foundtype::end

				-- we need to check if there's any cookies sent with the request,
				-- and if so, whether they contain any credentials. this will be
				-- used to set the auth parameters in the http conversation
				var cookies_p = lib.http.findheader(msg, 'Cookie')
				if cookies_p ~= nil then
					var cookies = cookies_p.ptr
................................................................................
								key.ct = (cookies + i) - key.ptr
								val.ptr = cookies + i + 1
							end
							i = i + 1
						else
							if cookies[i] == @';' then
								val.ct = (cookies + i) - val.ptr
								if lib.str.ncmp(key.ptr, lib.session.cookiename, lib.math.biggest([#lib.session.cookiename],key.ct)) == 0 then
									goto foundcookie
								end

								i = i + 1
								i = lib.str.ffw(cookies + i, cookies_p.ct - i) - cookies
								key.ptr = cookies + i
								val.ptr = nil
							else i = i + 1 end
						end
					end
					if val.ptr == nil then goto nocookie end
					val.ct = (cookies + i) - val.ptr
					if lib.str.ncmp(key.ptr, lib.session.cookiename, lib.math.biggest([#lib.session.cookiename], key.ct)) ~= 0 then
						goto nocookie
					end
					::foundcookie:: do
						var aid = lib.session.cookie_interpret(server.cfg.secret,
							[lib.mem.ptr(int8)]{ptr=val.ptr,ct=val.ct},
							lib.osclock.time(nil))
						if aid ~= 0 then co.aid = aid end
................................................................................
					end
					uri.ct = msg.uri.len
				else uri.ct = urideclen end
				lib.dbg('routing URI ', {uri.ptr, uri.ct})
				
				if lib.str.ncmp('GET', msg.method.ptr, msg.method.len) == 0 then
					route.dispatch_http(&co, uri, [lib.http.method.get])
				elseif lib.str.ncmp('POST', msg.method.ptr, msg.method.len) == 0 then
					route.dispatch_http(&co, uri, [lib.http.method.post])
				elseif lib.str.ncmp('HEAD', msg.method.ptr, msg.method.len) == 0 then
					route.dispatch_http(&co, uri, [lib.http.method.head])
				elseif lib.str.ncmp('OPTIONS', msg.method.ptr, msg.method.len) == 0 then
					route.dispatch_http(&co, uri, [lib.http.method.options])
				else
					co:complain(400,'unknown method','you have submitted an invalid http request')
				end

				if co.aid ~= 0 then lib.mem.heapf(co.who) end
				if co.varbuf.ptr ~= nil then co.varbuf:free() end
				if co.navbar.ptr ~= nil then co.navbar:free() end
			end
		end
	end;
}

local terra cfg(s: &srv, befile: rawstring)
	lib.report('configuring backends from ', befile)
................................................................................
	if c.sz > 0 then
		s.sources = c:crush()
	else
		s.sources.ptr = nil
		s.sources.ct = 0
	end
end

terra srv:actor_stats(uid: uint64)
	var stats = lib.store.actor_stats {
		posts = 0, mutuals = 0;
		follows = 0, followers = 0;
	}
	for i=0,self.sources.ct do
		var s = self.sources.ptr[i]:actor_stats(uid)
		stats.posts     = stats.posts     + s.posts
		stats.mutuals   = stats.mutuals   + s.mutuals
		stats.followers = stats.followers + s.followers
		stats.follows   = stats.follows   + s.follows
	end
	return stats
end

terra srv:actor_auth_how(ip: lib.store.inet, usn: rawstring)
	var cs: lib.store.credset cs:clear()
	var ok = false
	for i=0,self.sources.ct do
		var set, iok = self.sources.ptr[i]:actor_auth_how(ip, usn)
		if iok then
			cs = cs + set
			ok = iok
		end
	end
	return cs, ok
end

terra cfgcache.methods.load :: {&cfgcache} -> {}
terra cfgcache:init(o: &srv)
	self.overlord = o
	self:load()
end
................................................................................
		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

................................................................................
	end
	self.sources:free()
end

terra cfgcache:load()
	self.instance = self.overlord:conf_get('instance-name')
	self.secret = self.overlord:conf_get('server-secret')

	self.pol_reg = false
	var sreg = self.overlord:conf_get('policy-self-register')
	if sreg.ptr ~= nil then
		if lib.str.cmp(sreg.ptr, 'on') == 0
			then self.pol_reg = true
			else self.pol_reg = false
		end
	end
	sreg:free()
	
	self.pol_sec = secmode.lockdown
	var smode = self.overlord:conf_get('policy-security')
	if smode.ptr ~= nil then
		if lib.str.cmp(smode.ptr, 'public') == 0 then
			self.pol_sec = secmode.public
		elseif lib.str.cmp(smode.ptr, 'private') == 0 then
			self.pol_sec = secmode.private
		elseif lib.str.cmp(smode.ptr, 'lockdown') == 0 then
			self.pol_sec = secmode.lockdown
		elseif lib.str.cmp(smode.ptr, 'isolate') == 0 then
			self.pol_sec = secmode.isolate
		end
	end
	smode:free()
end

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

Added static/default-avatar.svg version [2764158102].

































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->

<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:xlink="http://www.w3.org/1999/xlink"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   width="25.4mm"
   height="25.4mm"
   viewBox="0 0 25.4 25.400001"
   version="1.1"
   id="svg8"
   sodipodi:docname="default-avatar.svg"
   inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
   inkscape:export-filename="/home/lexi/dev/parsav/static/default-avatar.png"
   inkscape:export-xdpi="128"
   inkscape:export-ydpi="128">
  <defs
     id="defs2">
    <linearGradient
       id="linearGradient5138"
       inkscape:collect="always">
      <stop
         id="stop5134"
         offset="0"
         style="stop-color:#ffffff;stop-opacity:1" />
      <stop
         id="stop5136"
         offset="1"
         style="stop-color:#000000;stop-opacity:0.98260868" />
    </linearGradient>
    <linearGradient
       inkscape:collect="always"
       id="linearGradient5126">
      <stop
         style="stop-color:#ff648d;stop-opacity:0.68235296"
         offset="0"
         id="stop5122" />
      <stop
         style="stop-color:#ff628a;stop-opacity:0.52549022"
         offset="1"
         id="stop5124" />
    </linearGradient>
    <linearGradient
       inkscape:collect="always"
       id="linearGradient5075">
      <stop
         style="stop-color:#100004;stop-opacity:0"
         offset="0"
         id="stop5071" />
      <stop
         style="stop-color:#100004;stop-opacity:0.59130436"
         offset="1"
         id="stop5073" />
    </linearGradient>
    <radialGradient
       inkscape:collect="always"
       xlink:href="#linearGradient5075"
       id="radialGradient5077"
       cx="12.7"
       cy="284.29998"
       fx="12.7"
       fy="284.29998"
       r="12.7"
       gradientUnits="userSpaceOnUse" />
    <radialGradient
       inkscape:collect="always"
       xlink:href="#linearGradient5126"
       id="radialGradient5128"
       cx="47.583008"
       cy="72.722656"
       fx="47.583008"
       fy="72.722656"
       r="29.078285"
       gradientTransform="matrix(1,0,0,0.74354777,0,18.649887)"
       gradientUnits="userSpaceOnUse" />
    <linearGradient
       inkscape:collect="always"
       xlink:href="#linearGradient5138"
       id="linearGradient5132"
       x1="47.611866"
       y1="62.544083"
       x2="47.611866"
       y2="83.615517"
       gradientUnits="userSpaceOnUse"
       gradientTransform="matrix(0.26458333,0,0,0.26458333,0,271.59998)" />
    <linearGradient
       inkscape:collect="always"
       xlink:href="#linearGradient5138"
       id="linearGradient5146"
       gradientUnits="userSpaceOnUse"
       x1="47.611866"
       y1="62.544083"
       x2="47.611866"
       y2="83.615517" />
    <linearGradient
       inkscape:collect="always"
       xlink:href="#linearGradient5138"
       id="linearGradient5152"
       gradientUnits="userSpaceOnUse"
       x1="47.611866"
       y1="62.544083"
       x2="47.611866"
       y2="83.615517" />
    <mask
       maskUnits="userSpaceOnUse"
       id="mask5148">
      <path
         id="path5150"
         d="m 39.580078,61.101544 c -4.433597,0.63549 -9.840689,2.053607 -13.849609,5.18555 -8.081221,6.313436 -7.197266,18.056655 -7.197266,18.056655 h 58.099611 c 0,0 0.883952,-11.743219 -7.197268,-18.056655 -3.931143,-3.071206 -9.20211,-4.489247 -13.585935,-5.142576 -2.90603,2.777877 -5.844385,4.437505 -8.111331,4.437505 -2.278666,0 -5.237507,-1.676296 -8.158202,-4.480479 z"
         style="fill:url(#linearGradient5152);fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
         inkscape:connector-curvature="0" />
    </mask>
    <radialGradient
       inkscape:collect="always"
       xlink:href="#linearGradient5126"
       id="radialGradient843"
       gradientUnits="userSpaceOnUse"
       gradientTransform="matrix(1,0,0,0.74354777,0,18.649887)"
       cx="47.583008"
       cy="72.722656"
       fx="47.583008"
       fy="72.722656"
       r="29.078285" />
  </defs>
  <sodipodi:namedview
     id="base"
     pagecolor="#1a1a1a"
     bordercolor="#666666"
     borderopacity="1.0"
     inkscape:pageopacity="0"
     inkscape:pageshadow="2"
     inkscape:zoom="5.6"
     inkscape:cx="41.777193"
     inkscape:cy="59.103277"
     inkscape:document-units="mm"
     inkscape:current-layer="layer1"
     showgrid="false"
     units="mm"
     inkscape:window-width="1920"
     inkscape:window-height="1042"
     inkscape:window-x="0"
     inkscape:window-y="38"
     inkscape:window-maximized="0" />
  <metadata
     id="metadata5">
    <rdf:RDF>
      <cc:Work
         rdf:about="">
        <dc:format>image/svg+xml</dc:format>
        <dc:type
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
        <dc:title />
      </cc:Work>
    </rdf:RDF>
  </metadata>
  <g
     inkscape:label="Layer 1"
     inkscape:groupmode="layer"
     id="layer1"
     transform="translate(0,-271.59998)">
    <rect
       style="fill:url(#radialGradient5077);fill-opacity:1;stroke:none;stroke-width:1.05821478;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
       id="rect4518"
       width="25.4"
       height="25.4"
       x="-2.220446e-16"
       y="271.59998" />
    <g
       id="g841"
       transform="translate(0.11032924)">
      <path
         mask="url(#mask5148)"
         transform="matrix(0.26458333,0,0,0.26458333,0,271.59998)"
         id="path5086"
         d="m 39.580078,61.101562 c -4.433597,0.635491 -9.840689,2.053587 -13.849609,5.185547 -8.081221,6.313437 -7.197266,18.056641 -7.197266,18.056641 h 58.099609 c 0,0 0.883954,-11.743204 -7.197265,-18.056641 -3.931145,-3.071199 -9.202111,-4.489236 -13.585938,-5.142578 -2.906029,2.777894 -5.844384,4.4375 -8.111328,4.4375 -2.278667,0 -5.237509,-1.67631 -8.158203,-4.480469 z"
         style="fill:url(#radialGradient843);fill-opacity:1;stroke:none;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
         inkscape:connector-curvature="0" />
      <path
         sodipodi:nodetypes="saszs"
         inkscape:connector-curvature="0"
         d="m 17.503541,281.31806 c 0.203041,-3.87199 -2.372092,-5.70473 -4.872977,-5.70473 -2.500884,0 -5.0760174,1.83274 -4.8729766,5.70473 0.2030395,3.87195 3.2371566,7.26639 4.8729766,7.26639 1.63582,0 4.669938,-3.39444 4.872977,-7.26639 z"
         style="fill:#ffb6c8;fill-opacity:0.86086958;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
         id="path5081" />
    </g>
  </g>
</svg>

Added static/padlock.svg version [a621b7bd06].









































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->

<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:xlink="http://www.w3.org/1999/xlink"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   width="0.2in"
   height="0.2in"
   viewBox="0 0 5.0800002 5.0800002"
   version="1.1"
   id="svg8"
   inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
   sodipodi:docname="padlock.svg"
   inkscape:export-filename="/home/lexi/dev/parsav/static/padlock.png"
   inkscape:export-xdpi="240"
   inkscape:export-ydpi="240">
  <defs
     id="defs2">
    <linearGradient
       inkscape:collect="always"
       id="linearGradient884">
      <stop
         style="stop-color:#d2a7b2;stop-opacity:1"
         offset="0"
         id="stop880" />
      <stop
         style="stop-color:#bd7c8d;stop-opacity:1"
         offset="1"
         id="stop882" />
    </linearGradient>
    <linearGradient
       inkscape:collect="always"
       id="linearGradient863">
      <stop
         style="stop-color:#894657;stop-opacity:1"
         offset="0"
         id="stop859" />
      <stop
         id="stop867"
         offset="0.35023043"
         style="stop-color:#733a49;stop-opacity:1" />
      <stop
         style="stop-color:#884556;stop-opacity:1"
         offset="1"
         id="stop861" />
    </linearGradient>
    <linearGradient
       inkscape:collect="always"
       xlink:href="#linearGradient863"
       id="linearGradient865"
       x1="4.1353216"
       y1="294.38614"
       x2="4.1353216"
       y2="296.71402"
       gradientUnits="userSpaceOnUse"
       gradientTransform="translate(1.0345579e-4,-0.15310986)" />
    <radialGradient
       inkscape:collect="always"
       xlink:href="#linearGradient884"
       id="radialGradient886"
       cx="2.0557182"
       cy="292.57834"
       fx="2.0557182"
       fy="292.57834"
       r="1.4498034"
       gradientTransform="matrix(1,0,0,0.99890953,0,0.47312247)"
       gradientUnits="userSpaceOnUse" />
    <radialGradient
       inkscape:collect="always"
       xlink:href="#linearGradient884"
       id="radialGradient921"
       gradientUnits="userSpaceOnUse"
       gradientTransform="matrix(1,0,0,0.99890953,5.2916667,0.47312243)"
       cx="2.0557182"
       cy="292.57834"
       fx="2.0557182"
       fy="292.57834"
       r="1.4498034" />
    <radialGradient
       inkscape:collect="always"
       xlink:href="#linearGradient884"
       id="radialGradient925"
       gradientUnits="userSpaceOnUse"
       gradientTransform="matrix(1,0,0,0.99890953,4.6302084,-0.40803457)"
       cx="2.0557182"
       cy="292.57834"
       fx="2.0557182"
       fy="292.57834"
       r="1.4498034" />
    <radialGradient
       inkscape:collect="always"
       xlink:href="#linearGradient884"
       id="radialGradient938"
       gradientUnits="userSpaceOnUse"
       gradientTransform="matrix(0.92374945,0,0,0.92274213,0.19377986,22.690922)"
       cx="2.0557182"
       cy="292.57834"
       fx="2.0557182"
       fy="292.57834"
       r="1.4498034" />
    <radialGradient
       inkscape:collect="always"
       xlink:href="#linearGradient884"
       id="radialGradient943"
       gradientUnits="userSpaceOnUse"
       gradientTransform="matrix(1,0,0,0.99890953,-5.2916667,0.47312247)"
       cx="2.0557182"
       cy="292.57834"
       fx="2.0557182"
       fy="292.57834"
       r="1.4498034" />
  </defs>
  <sodipodi:namedview
     id="base"
     pagecolor="#4b002d"
     bordercolor="#666666"
     borderopacity="1.0"
     inkscape:pageopacity="0"
     inkscape:pageshadow="2"
     inkscape:zoom="11.2"
     inkscape:cx="25.660286"
     inkscape:cy="1.1491243"
     inkscape:document-units="mm"
     inkscape:current-layer="layer1"
     showgrid="false"
     inkscape:window-width="1920"
     inkscape:window-height="1042"
     inkscape:window-x="0"
     inkscape:window-y="38"
     inkscape:window-maximized="0"
     units="in" />
  <metadata
     id="metadata5">
    <rdf:RDF>
      <cc:Work
         rdf:about="">
        <dc:format>image/svg+xml</dc:format>
        <dc:type
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
        <dc:title></dc:title>
      </cc:Work>
    </rdf:RDF>
  </metadata>
  <g
     inkscape:label="Layer 1"
     inkscape:groupmode="layer"
     id="layer1"
     transform="translate(0,-291.91998)">
    <path
       style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
       d="m 2.5400001,292.32047 c -1.3794528,0 -1.3389364,1.46451 -1.3389364,1.46451 v 0.44804 H 0.94474863 v 2.32802 H 4.1352514 v -2.32802 H 3.8794531 v -0.44804 c 0,0 0.04,-1.46451 -1.339453,-1.46451 z m 0,0.5209 c 0.8656493,0 0.8402588,0.9493 0.8402588,0.9493 v 0.44235 H 1.6997413 v -0.44235 c 0,0 -0.02539,-0.9493 0.8402588,-0.9493 z"
       id="path945"
       inkscape:connector-curvature="0" />
    <path
       id="path936"
       style="fill:url(#radialGradient938);fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
       d="m 3.8792355,293.78476 c 0,0 0.040321,-1.46443 -1.3391321,-1.46443 -1.3794528,0 -1.339132,1.46443 -1.339132,1.46443 v 0.75607 l 0.4987852,0.006 v -0.75607 c 0,0 -0.025302,-0.94927 0.8403468,-0.94927 0.8656493,0 0.8403469,0.94927 0.8403469,0.94927 v 0.75607 l 0.4987852,-0.006 z"
       inkscape:connector-curvature="0"
       sodipodi:nodetypes="ccccccccccc" />
    <rect
       style="fill:url(#linearGradient865);fill-opacity:1;stroke:none;stroke-width:0.69999999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
       id="rect857"
       width="3.1906431"
       height="2.3278909"
       x="0.94478196"
       y="294.23303" />
    <path
       style="fill:#281419;fill-opacity:1;stroke:#000000;stroke-width:0.09348611;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
       d="m 2.5400001,294.87847 c -0.1706687,1e-5 -0.3090221,0.13836 -0.3090249,0.30903 1.516e-4,0.1325 0.084759,0.25016 0.2103229,0.29248 v 0 l -0.00938,0.74317 h 0.108082 0.1085944 l -0.010409,-0.74317 v 0 c 0.1257675,-0.0422 0.2106132,-0.15984 0.2108396,-0.29248 -2.8e-6,-0.17067 -0.1383562,-0.30902 -0.309025,-0.30903 z"
       id="path910"
       inkscape:connector-curvature="0"
       sodipodi:nodetypes="ccccccccccc" />
  </g>
</svg>

Modified static/style.scss from [a7ffc6f8bf] to [7905a3444d].











































































































































































































































































































































































































































































































































































































































































































































































































































































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

@function tone($pct) { @return adjust-color($color, $lightness: $pct) }

body {
	@extend %sans;
	background-color: adjust-color($color, $lightness: -55%);
	color: adjust-color($color, $lightness: 25%);
	font-size: 14pt;
	margin: 0;
	padding: 0;
}
a[href] {
	color: adjust-color($color, $lightness: 10%);
	&:hover {
		color: white;
		text-shadow: 0 0 15px adjust-color($color, $lightness: 20%);
	}
}
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);
	}
}

header {
	position: fixed;
	height: min-content;
	width: 100vw;
	margin: 0;
	padding: 0;
	border-bottom: 1px solid black;
	z-index: 1;
	@include glass;
	background: linear-gradient(to bottom, transparent, rgba(0,0,0,0.3));
	> div {
		position: relative;
		max-width: 10in;
		margin: auto;

		display: grid;
		grid-template-columns: 1fr max-content;
		grid-template-rows: 1fr;
		h1 {
			all: unset;
			display: block;
			font-size: 1.4em;
			padding: 0.25in 0;
			text-shadow: 2px 2px 1px black;
			grid-column: 1/2; grid-row: 1/2;
		}
		nav {
			all: unset;
			display: flex;
			justify-content: flex-end;
			align-items: center;
			grid-column: 2/3; grid-row: 1/2;
			> a[href] {
				display: block;
				padding: 0.25in 0.15in;
				//padding: calc((25% - 1em)/2) 0.15in;
				&, &::after { transition: 0.3s; }
				text-shadow: 1px 1px 1px black;
				&:hover{
					transform: scale(120%);
				}
			}
		}
	}
}

main {
	@extend %content;
	display: block;
	position: relative;
	min-height: calc(100vh - 1.1in);
	margin-top: 0;
	margin-bottom: 0;
	padding: 0 0.4in;
	padding-top: 1.1in;
	background-color: adjust-color($color, $lightness: -45%, $alpha: 0.4);
	border: {
		left: 1px solid black;
		right: 1px solid black;
	}
}

div.profile {
	@extend %box;
	padding: 0.1in;
	position: relative;
	display: grid;
	grid-template-columns: 2fr 1fr;
	grid-template-rows: 1fr 1fr;
	width: 100%;
	> .banner {
		grid-column: 1 / 3;
		grid-row: 1 / 2;
		display: grid;
		grid-template-columns: 1.1in 1fr;
		grid-template-rows: 0.3in 1fr;
		> .avatar {
			display: block;
			width: 1in; height: 1in;
			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;
		}
	}
	> .stats {
		grid-column: 3 / 4;
		grid-row: 1 / 3;
	}
	> .menu {
		grid-column: 1 / 3;
		grid-row: 2 / 3;
		display: flex;
		justify-content: center;
		align-items: center;
		> a[href] {
			@extend %button;
			display: block;
			margin: 0 0.05in;
		}
		> hr {
			all: unset;
			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;
	margin:auto;
	padding: 0.5in;
	text-align: center;
}

div.login {
	@extend %box;
	width: 4in;
	padding: 0.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;
		grid-template-rows: 1.2em 1fr 1fr;
		grid-gap: 5px;
		> label, input, button { display: block; }
		> label { grid-column: 1 / 3; grid-row: 1/2; font-weight: bold }
		> input { grid-column: 1 / 3; grid-row: 2/3; }
		> button { grid-column: 2 / 3; grid-row: 3/4; }
		> a { @extend %button; grid-column: 1 / 2; grid-row: 3/4; }
	}
}

form.compose {
	@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;
	text-align: center;
	padding: 0.09in 0.2in;
	background: tone(-40%);
	border: 1px solid black;
	font-weight: bold;
	text-decoration: none;
	cursor: help;
}

input.acl {
	@extend %teletype;
	background: url(/s/padlock.webp) no-repeat;
	background-size: 20pt;
	background-position: 0.05in 50%;
	&:focus {
		background: url(/s/padlock.webp) no-repeat, $grad-ui-focus;
		background-size: 20pt;
		background-position: 0.05in 50%;
	};
	padding-left: 0.40in;
}

div.modal {
	@extend %box;
	position: fixed;
	display: none;
	left: 0; right: 0; bottom: 0; top: 0;
	max-width: 7in;
	margin: 1in auto;
	padding: 0.2in 0.3in;
	&:target { display: block; }
	box-shadow: 0 0 4in 5in rgba(0,0,0,0.5);
	z-index: 2;
	> div {
		height: 100%;
		overflow-y: scroll;
		>p:first-of-type { margin-top: 0; }
	}
	>a[href="#0"] { // close link
		@extend %button;
		cursor: default;
		display: block;
		position: absolute;
		top: -0.3in;
		right: 0.1in;
		margin: 0.1in;
		padding: 0.1in;
		&:hover { font-weight: bold; }
	}
}

code {
	@extend %teletype;
	background: adjust-color($color, $lightness: -50%);
	border: 1px solid adjust-color($color, $lightness: -20%);
	padding: 2px 6px;
	text-shadow: 2px 2px black;
}

Added static/warn.svg version [a819eb65cc].











































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->

<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:xlink="http://www.w3.org/1999/xlink"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   width="1in"
   height="1in"
   viewBox="0 0 25.4 25.400001"
   version="1.1"
   id="svg8"
   inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
   sodipodi:docname="warn.svg"
   inkscape:export-filename="/home/lexi/dev/parsav/static/warn.png"
   inkscape:export-xdpi="200"
   inkscape:export-ydpi="200">
  <defs
     id="defs2">
    <linearGradient
       inkscape:collect="always"
       id="linearGradient2322">
      <stop
         style="stop-color:#ca0050;stop-opacity:1;"
         offset="0"
         id="stop2318" />
      <stop
         style="stop-color:#7b0031;stop-opacity:1"
         offset="1"
         id="stop2320" />
    </linearGradient>
    <linearGradient
       inkscape:collect="always"
       id="linearGradient2302">
      <stop
         style="stop-color:#ffffff;stop-opacity:1;"
         offset="0"
         id="stop2298" />
      <stop
         style="stop-color:#ffffff;stop-opacity:0;"
         offset="1"
         id="stop2300" />
    </linearGradient>
    <linearGradient
       inkscape:collect="always"
       id="linearGradient1228">
      <stop
         style="stop-color:#8c0037;stop-opacity:1"
         offset="0"
         id="stop1224" />
      <stop
         style="stop-color:#ff1a75;stop-opacity:1"
         offset="1"
         id="stop1226" />
    </linearGradient>
    <linearGradient
       inkscape:collect="always"
       id="linearGradient851">
      <stop
         style="stop-color:#ffffff;stop-opacity:0"
         offset="0"
         id="stop847" />
      <stop
         style="stop-color:#ffa9c6;stop-opacity:1"
         offset="1"
         id="stop849" />
    </linearGradient>
    <radialGradient
       inkscape:collect="always"
       xlink:href="#linearGradient851"
       id="radialGradient853"
       cx="12.699999"
       cy="285.82184"
       fx="12.699999"
       fy="285.82184"
       r="1.7905753"
       gradientTransform="matrix(2.2137788,-2.5697531e-5,6.7771541e-5,5.8383507,-15.43436,-1382.906)"
       gradientUnits="userSpaceOnUse" />
    <linearGradient
       inkscape:collect="always"
       xlink:href="#linearGradient1228"
       id="linearGradient1230"
       x1="87.388672"
       y1="90.857147"
       x2="67.185623"
       y2="-29.734592"
       gradientUnits="userSpaceOnUse" />
    <filter
       inkscape:collect="always"
       style="color-interpolation-filters:sRGB"
       id="filter2294"
       x="-0.38068429"
       width="1.7613686"
       y="-0.089708175"
       height="1.1794164">
      <feGaussianBlur
         inkscape:collect="always"
         stdDeviation="0.5243555"
         id="feGaussianBlur2296" />
    </filter>
    <radialGradient
       inkscape:collect="always"
       xlink:href="#linearGradient2302"
       id="radialGradient2304"
       cx="12.58085"
       cy="285.25314"
       fx="12.58085"
       fy="285.25314"
       r="10.573204"
       gradientTransform="matrix(1.1874875,-1.9679213e-8,1.9699479e-8,1.1887104,-2.3811363,-53.650199)"
       gradientUnits="userSpaceOnUse" />
    <linearGradient
       inkscape:collect="always"
       xlink:href="#linearGradient2322"
       id="linearGradient2324"
       x1="20.298828"
       y1="97.196846"
       x2="20.298828"
       y2="12.911131"
       gradientUnits="userSpaceOnUse"
       gradientTransform="translate(1.3858268e-6)" />
  </defs>
  <sodipodi:namedview
     id="base"
     pagecolor="#313131"
     bordercolor="#666666"
     borderopacity="1.0"
     inkscape:pageopacity="0"
     inkscape:pageshadow="2"
     inkscape:zoom="1.4"
     inkscape:cx="-71.210763"
     inkscape:cy="60.089308"
     inkscape:document-units="mm"
     inkscape:current-layer="layer1"
     showgrid="false"
     units="in"
     inkscape:window-width="1920"
     inkscape:window-height="1042"
     inkscape:window-x="0"
     inkscape:window-y="38"
     inkscape:window-maximized="0" />
  <metadata
     id="metadata5">
    <rdf:RDF>
      <cc:Work
         rdf:about="">
        <dc:format>image/svg+xml</dc:format>
        <dc:type
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
        <dc:title></dc:title>
      </cc:Work>
    </rdf:RDF>
  </metadata>
  <g
     inkscape:label="Layer 1"
     inkscape:groupmode="layer"
     id="layer1"
     transform="translate(0,-271.59998)">
    <path
       id="rect829"
       style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.26499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
       d="m 11.543949,291.41721 -7e-6,0.52521 0.893337,0.89333 0.525444,2.4e-4 0.893329,-0.89333 -2.34e-4,-0.52545 -0.893338,-0.89333 h -0.525203 z m 1.995206,-2.42709 0.813727,-9.44222 -0.826441,-0.74021 h -1.652884 l -0.826441,0.74021 0.813726,9.44222 0.419578,0.5079 h 0.839157 z"
       inkscape:connector-curvature="0" />
    <path
       style="fill:url(#linearGradient1230);fill-opacity:1;stroke:none;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
       d="M 48.013672 8.5 A 46.78571 46.78571 0 0 0 44.353516 8.6621094 L 8.6132812 80.511719 A 46.78571 46.78571 0 0 0 14.115234 87.5 L 81.867188 87.5 A 46.78571 46.78571 0 0 0 87.388672 80.460938 L 51.685547 8.6855469 A 46.78571 46.78571 0 0 0 48.013672 8.5 z M 44.876953 27.242188 L 51.123047 27.242188 L 54.248047 30.039062 L 51.171875 65.726562 L 49.585938 67.646484 L 46.414062 67.646484 L 44.828125 65.726562 L 41.751953 30.039062 L 44.876953 27.242188 z M 47.007812 71.523438 L 48.992188 71.523438 L 52.369141 74.900391 L 52.369141 76.884766 L 48.992188 80.261719 L 47.007812 80.261719 L 43.630859 76.884766 L 43.630859 74.900391 L 47.007812 71.523438 z "
       id="path817"
       transform="matrix(0.26458333,0,0,0.26458333,0,271.59998)" />
    <path
       inkscape:connector-curvature="0"
       id="path838"
       d="m -29.39374,273.80193 a 12.378719,12.378719 0 0 0 -0.968416,0.0424 l -9.456272,19.01021 a 12.378719,12.378719 0 0 0 1.455725,1.84899 h 17.926038 a 12.378719,12.378719 0 0 0 1.460894,-1.86242 l -9.446452,-18.99057 a 12.378719,12.378719 0 0 0 -0.971517,-0.0486 z"
       style="fill:#ca0050;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
    <path
       id="path1146"
       d="m 12.703617,273.84894 c -0.323241,0.002 -0.646295,0.016 -0.968416,0.0429 l -9.4562703,19.01021 c 0.425424,0.66116 0.9128677,1.28028 1.4557249,1.84898 H 21.660694 c 0.545337,-0.57272 1.034537,-1.19637 1.460892,-1.86242 l -9.446452,-18.99058 c -0.32307,-0.0291 -0.64716,-0.0455 -0.971517,-0.0491 z"
       style="fill:none;fill-opacity:1;stroke:url(#radialGradient2304);stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
       inkscape:connector-curvature="0"
       sodipodi:nodetypes="ccccccccc" />
    <path
       id="path1232"
       style="opacity:1;fill:#ffe7f0;fill-opacity:1;stroke:none;stroke-width:0.26499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
       d="m 12.134766,279.49414 -0.375,0.33594 0.647507,8.88867 0.07813,0.12618 h 0.429595 l 0.07813,-0.12618 0.647507,-8.88867 -0.375,-0.33594 z m 0.06945,12.07312 -3e-6,0.22524 0.383115,0.38311 0.225341,1.1e-4 0.383112,-0.38311 -1.01e-4,-0.22535 -0.383115,-0.38311 h -0.225238 z"
       inkscape:connector-curvature="0"
       sodipodi:nodetypes="cccccccccccccccccc" />
    <path
       inkscape:connector-curvature="0"
       d="m 11.543949,291.41721 -7e-6,0.52521 0.893337,0.89333 0.525444,2.4e-4 0.893329,-0.89333 -2.34e-4,-0.52545 -0.893338,-0.89333 h -0.525203 z m 1.995206,-2.42709 0.813727,-9.44222 -0.826441,-0.74021 h -1.652884 l -0.826441,0.74021 0.813726,9.44222 0.419578,0.5079 h 0.839157 z"
       style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.26499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter2294)"
       id="path1856" />
  </g>
</svg>

Modified store.t from [213b3d2729] to [5ad659a834].

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
...
165
166
167
168
169
170
171

172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
...
219
220
221
222
223
224
225
226
227
228
229
		'follow', 'mute', 'block'
	};
	credset = lib.set {
		'pw', 'otp', 'challenge', 'trust'
	};
	privset = lib.set {
		'post', 'edit', 'acct', 'upload', 'censor', 'admin'
	}




}












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

struct m.source

struct m.rights {
	rank: uint16 -- lower = more powerful except 0 = regular user
	-- creating staff automatically assigns rank immediately below you
	quota: uint32 -- # of allowed tweets per day; 0 = no limit
	
	-- user powers -- default on
	login: bool
	visible: bool
	post: bool
	shout: bool
	propagate: bool
	upload: bool
	acct: bool
	edit: bool

	-- admin powers -- default off
	ban: bool
	config: bool
	censor: bool
	suspend: bool
	rebrand: bool -- modify site's brand identity
}

terra m.rights_default()
	return m.rights {









		rank = 0, quota = 1000;
		
		login = true, visible = true, post = true;
		shout = true, propagate = true, upload = true;

		ban = false, config = false, censor = false;
		suspend = false, rebrand = false;
	}
end

struct m.actor {
	id: uint64
	nym: str
	handle: str
	origin: uint64
	bio: str


	rights: m.rights
	key: lib.mem.ptr(uint8)


	xid: str

	source: &m.source
}








struct m.range {
	time: bool
	union {
		from_time: m.timepoint
		from_idx: uint64
	}
................................................................................
	actor_save: {&m.source, m.actor} -> bool
	actor_create: {&m.source, m.actor} -> bool
	actor_fetch_xid: {&m.source, lib.mem.ptr(int8)} -> lib.mem.ptr(m.actor)
	actor_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.actor)
	actor_notif_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.notif)
	actor_enum: {&m.source} -> lib.mem.ptr(&m.actor)
	actor_enum_local: {&m.source} -> lib.mem.ptr(&m.actor)


	actor_auth_how: {&m.source, m.inet, rawstring} -> m.credset
		-- returns a set of auth method categories that are available for a
		-- given user from a certain origin
			-- origin: inet
			-- handle: rawstring
	actor_auth_otp: {&m.source, m.inet, rawstring, rawstring} -> uint64
	actor_auth_pw: {&m.source, m.inet, rawstring, rawstring} -> uint64
		-- handles password-based logins against hashed passwords
			-- origin: inet
			-- handle: rawstring
			-- token:  rawstring
	actor_auth_tls:    {&m.source, m.inet, rawstring} -> uint64
		-- handles implicit authentication performed as part of an TLS connection
			-- origin: inet
................................................................................
terra m.source:free()
	self.id:free()
	self.string:free()
end
m.source.metamethods.__methodmissing = macro(function(meth, obj, ...)
	local q = {...}
	-- syntax sugar to forward unrecognized calls onto the backend
	return `obj.backend.[meth](&obj, [q])
end)

return m







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










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

<
>
>
>
>
>
>
>
>
>
|
<
<
<
<
<
<
<








>
>



>

<


>
>
>
>
>
>
>







 







>

|



|

|







 







|



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
...
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
...
230
231
232
233
234
235
236
237
238
239
240
		'follow', 'mute', 'block'
	};
	credset = lib.set {
		'pw', 'otp', 'challenge', 'trust'
	};
	privset = lib.set {
		'post', 'edit', 'acct', 'upload', 'censor', 'admin'
	};
	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 --lib.mem.ptr(int8)

struct m.source

struct m.rights {
	rank: uint16 -- lower = more powerful except 0 = regular user
	-- creating staff automatically assigns rank immediately below you
	quota: uint32 -- # of allowed tweets per day; 0 = no limit
	
	powers: m.powerset








}








terra m.rights_default()

	var pow: m.powerset pow:fill()
	(pow.purge << false)
	(pow.config << false)
	(pow.censor << false)
	(pow.suspend << false)
	(pow.elevate << false)
	(pow.demote << false)
	(pow.cred << false)
	(pow.rebrand << false)
	return m.rights { rank = 0, quota = 1000, powers = pow; }







end

struct m.actor {
	id: uint64
	nym: str
	handle: str
	origin: uint64
	bio: str
	avatar: str
	knownsince: int64
	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
}

struct m.range {
	time: bool
	union {
		from_time: m.timepoint
		from_idx: uint64
	}
................................................................................
	actor_save: {&m.source, m.actor} -> bool
	actor_create: {&m.source, m.actor} -> bool
	actor_fetch_xid: {&m.source, lib.mem.ptr(int8)} -> lib.mem.ptr(m.actor)
	actor_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.actor)
	actor_notif_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.notif)
	actor_enum: {&m.source} -> lib.mem.ptr(&m.actor)
	actor_enum_local: {&m.source} -> lib.mem.ptr(&m.actor)
	actor_stats: {&m.source, uint64} -> m.actor_stats

	actor_auth_how: {&m.source, m.inet, rawstring} -> {m.credset, bool}
		-- returns a set of auth method categories that are available for a
		-- given user from a certain origin
			-- origin: inet
			-- username: rawstring
	actor_auth_otp: {&m.source, m.inet, rawstring, rawstring} -> uint64
	actor_auth_pw: {&m.source, m.inet, lib.mem.ptr(int8), lib.mem.ptr(int8) } -> uint64
		-- handles password-based logins against hashed passwords
			-- origin: inet
			-- handle: rawstring
			-- token:  rawstring
	actor_auth_tls:    {&m.source, m.inet, rawstring} -> uint64
		-- handles implicit authentication performed as part of an TLS connection
			-- origin: inet
................................................................................
terra m.source:free()
	self.id:free()
	self.string:free()
end
m.source.metamethods.__methodmissing = macro(function(meth, obj, ...)
	local q = {...}
	-- syntax sugar to forward unrecognized calls onto the backend
	return quote var r = obj.backend.[meth](&obj, [q]) in r end
end)

return m

Modified str.t from [c91733fef5] to [c8d105a016].

13
14
15
16
17
18
19


20
21
22
23
24



















25
26
27
28
29
30
31
..
65
66
67
68
69
70
71









72
73
74
75
76
77
78
..
87
88
89
90
91
92
93


94
95
96
97
98
99
100
	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);
}



(lib.mem.ptr(int8)).metamethods.__cast = function(from,to,e)
	if from == &int8 then
		return `[lib.mem.ptr(int8)]{ptr = e, ct = m.sz(e)}
	elseif to == &int8 then
		return e.ptr



















	end
end

struct m.acc {
	buf: rawstring
	sz: intptr
	run: intptr
................................................................................
	var pt: lib.mem.ptr(int8)
	pt.ptr = self.buf
	pt.ct = self.sz
	self.buf = nil
	self.sz = 0
	return pt
end;










terra m.acc:push(str: rawstring, len: intptr)
	--var llen = len
	if str == nil then return self end
	--if str[len - 1] == 0xA then llen = llen - 1 end -- don't display newlines in debug output
	-- lib.dbg('pushing "',{str,llen},'" onto accumulator')
	if self.buf == nil then self:init(self.run) end
................................................................................
	return self
end;

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



m.acc.methods.ppush = terra(self: &m.acc, str: lib.mem.ptr(int8))
	self:push(str.ptr, str.ct)            return self end;
m.acc.methods.merge = terra(self: &m.acc, str: lib.mem.ptr(int8))
	self:push(str.ptr, str.ct) str:free() return self end;
m.acc.methods.compose = macro(function(self, ...)
	local minlen = 0
	local pstrs = {}







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







 







>
>
>
>
>
>
>
>
>







 







>
>







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
..
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
...
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
	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);
}

do local strptr = (lib.mem.ptr(int8))
	local byteptr = (lib.mem.ptr(uint8))
	strptr.metamethods.__cast = function(from,to,e)
		if from == &int8 then
			return `strptr {ptr = e, ct = m.sz(e)}
		elseif to == &int8 then
			return e.ptr
		end
	end

	terra strptr:cmp(other: strptr)
		var sz = lib.math.biggest(self.ct, other.ct)
		for i = 0, sz do
			if self.ptr[i] == 0 and other.ptr[i] == 0 then return true end
			if self.ptr[i] ~= other.ptr[i] then return false end
		end
		return true
	end

	terra byteptr:cmp(other: byteptr)
		var sz = lib.math.biggest(self.ct, other.ct)
		for i = 0, sz do
			if self.ptr[i] == 0 and other.ptr[i] == 0 then return true end
			if self.ptr[i] ~= other.ptr[i] then return false end
		end
		return true
	end
end

struct m.acc {
	buf: rawstring
	sz: intptr
	run: intptr
................................................................................
	var pt: lib.mem.ptr(int8)
	pt.ptr = self.buf
	pt.ct = self.sz
	self.buf = nil
	self.sz = 0
	return pt
end;

terra m.acc:cue(sz: intptr)
	if sz <= self.run then return end
	self.run = sz
	if self.space - self.sz < self.run then
		self.space = self.sz + self.run
		self.buf = [rawstring](lib.mem.heapr_raw(self.buf, self.space))
	end
end

terra m.acc:push(str: rawstring, len: intptr)
	--var llen = len
	if str == nil then return self end
	--if str[len - 1] == 0xA then llen = llen - 1 end -- don't display newlines in debug output
	-- lib.dbg('pushing "',{str,llen},'" onto accumulator')
	if self.buf == nil then self:init(self.run) end
................................................................................
	return self
end;

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

m.acc.methods.lpush = macro(function(self,str)
	return `self:push([str:asvalue()], [#(str:asvalue())]) end)
m.acc.methods.ppush = terra(self: &m.acc, str: lib.mem.ptr(int8))
	self:push(str.ptr, str.ct)            return self end;
m.acc.methods.merge = terra(self: &m.acc, str: lib.mem.ptr(int8))
	self:push(str.ptr, str.ct) str:free() return self end;
m.acc.methods.compose = macro(function(self, ...)
	local minlen = 0
	local pstrs = {}

Modified tpl.t from [ad44dd6129] to [3cd51c8b03].

27
28
29
30
31
32
33

34
35
36
37
38
39
40
..
65
66
67
68
69
70
71

72
73

74
75
76
77

78
79
80
81
82
83
84
..
94
95
96
97
98
99
100







101
102
103
104
105
106
107
	local segs = {}
	local constlen = 0
	-- strip out all irrelevant whitespace to tidy things up
	-- TODO: find way to exclude <pre> tags?
	str = str:gsub('[\n^]%s+','')
	str = str:gsub('%s+[\n$]','')
	str = str:gsub('\n','')

	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
................................................................................
				[runningtally] = [runningtally] + lib.str.sz([symself].[key])*fac
			end
		end
	end

	local copiers = {}
	local senders = {}

	local symtxt = symbol(lib.mem.ptr(int8))
	local cpypos = symbol(&opaque)

	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

		if fields[idx] then
			copiers[#copiers+1] = quote
				[cpypos] = lib.mem.cpy([cpypos],
					[&opaque](symself.[fields[idx]]),
					lib.str.sz(symself.[fields[idx]]))
			end
			senders[#senders+1] = quote
................................................................................
		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.send = terra([symself], [destcon], code: uint16, hd: lib.mem.ptr(lib.http.header))
		lib.dbg(['transmitting template ' .. tid])
		[tallyup]
		lib.net.mg_printf([destcon], 'HTTP/1.1 %s', lib.http.codestr(code))
		for i = 0, hd.ct do
			lib.net.mg_printf([destcon], '%s: %s\r\n', hd.ptr[i].key, hd.ptr[i].value)







>







 







>


>




>







 







>
>
>
>
>
>
>







27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
..
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
..
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
	local segs = {}
	local constlen = 0
	-- strip out all irrelevant whitespace to tidy things up
	-- TODO: find way to exclude <pre> tags?
	str = str:gsub('[\n^]%s+','')
	str = str:gsub('%s+[\n$]','')
	str = str:gsub('\n','')
	str = str:gsub('</a><a ','</a> <a ') -- keep nav links from getting smooshed
	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
................................................................................
				[runningtally] = [runningtally] + lib.str.sz([symself].[key])*fac
			end
		end
	end

	local copiers = {}
	local senders = {}
	local appenders = {}
	local symtxt = symbol(lib.mem.ptr(int8))
	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
			copiers[#copiers+1] = quote
				[cpypos] = lib.mem.cpy([cpypos],
					[&opaque](symself.[fields[idx]]),
					lib.str.sz(symself.[fields[idx]]))
			end
			senders[#senders+1] = quote
................................................................................
		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]
		return accumulator
	end
	rec.methods.send = terra([symself], [destcon], code: uint16, hd: lib.mem.ptr(lib.http.header))
		lib.dbg(['transmitting template ' .. tid])
		[tallyup]
		lib.net.mg_printf([destcon], 'HTTP/1.1 %s', lib.http.codestr(code))
		for i = 0, hd.ct do
			lib.net.mg_printf([destcon], '%s: %s\r\n', hd.ptr[i].key, hd.ptr[i].value)

Added view/compose.tpl version [09c6180294].



















































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
<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">
		<a href="#aclhelp" class="help">?</a>
	<button type="submit">commit</button>
</form>

<div id="aclhelp" class="modal"> <a href="#0">close</a> <div>
	<p>to control who can see your post (and how far it is propagated), <code>parsav</code> uses <strong>ACL expressions</strong>. this is roughly equivalent to scoping in pleroma or GNU social terms. an ACL expression consists of one or more space-separated terms, each of which match a certain set of users. a term can be negated by prefixing it with <code>~</code>, a tilde character, so <code>~all</code> matches nobody, and <code>~followed</code> matches users you do not follow.</p>
	<ul>
		<li><strong>all</strong>: matches any and all users</li>
		<li><strong>local</strong>: matches users who belong to this instance</li>
		<li><strong>mutuals</strong>: matches users you follow who also follow you</li>
		<li><strong>followed</strong>: matches users you follow</li>
		<li><strong>followers</strong>: matches users who follow you</li>
		<li><strong>groupies</strong>: matches users who follow you, but whom you do not follow</li>
		<li><strong>mentioned</strong>: matches users who are mentioned in the post</li>
		<li><strong>staff</strong>: matches instance staff (equivalent to <code>~%0</code>)</li>
		<li><strong>admin</strong>: matches the individual named as the instance administrator, if any</li>
		<li><strong>\@</strong><em>handle</em>: matches the user <em>handle</em></li>
		<li><strong>+</strong><em>circle</em>: matches users you have categorized under <em>circle</em></li>
		<li><strong>#</strong><em>room</em>: matches users who are members of <em>room</em></li>
		<li><strong>%</strong><em>rank</em>: matches users of <em>rank</em> or higher (e.g. <code>%3</code> matches users of rank 3, 2, and 1). as a special case, <code>%0</code> matches ordinary users</li>
		<li><strong>#</strong><em>room</em><strong>%</strong><em>rank</em>: matches users who hold <em>rank</em> in <em>room</em></li>
		<li><strong>&lt;</strong><em>title</em><strong>&gt;</strong>: matches peers of the net who have been created <em>title</em> by the sovereign</li>
		<li><strong>#</strong><em>room</em><strong>&lt;</strong><em>title</em><strong>&gt;</strong>: matches peers of the chat who have been created <em>title</em> by <em>room</em> staff</li>
	</ul>
	<p>to evaluate an ACL expression, <code>parsav</code> reads each term from start to finish. for each term, it considers whether it describes the user who is attempting to access the content. if the term matches, its policy is applied and the expression completes. if the term doesn't match, the server proceeds on to the next term and the process repeats until it finds a matching term or runs out of terms, applying the fallback policy.</p>
	<p><strong>policy</strong> is whether a term grants or denies access. the default term policy is <strong>allow</strong>, but you can control the policy with the keywords <code>allow</code> and <code>deny</code>. if a term finishes evaluating without any match being found, a fallback policy is applied; this fallback is the opposite of whatever the current policy is. this sounds confusing but makes ACL expressions much more intuitive; <code>allow \@bob</code> and <code>deny trent</code> do exactly what you'd expect &em; the former allows bob and only bob in; the latter denies access only to trent, but grants access to the rest of the world.</p>
	<p>expressions must contain at least one term to be valid. if they consist only of policy keywords, they will be rejected.</p>
	<p>in effect, this all means that an ACL expression can be treated as a simple list of who is allowed to view your post. for instance, an expression of <code>local</code> means only local users can view it. however, much more complex expressions are possible.</p>
	<ul>
		<li><code>deny groupies allow +illuminati</code>: permits access to the illuminati, but excluding those members who are groupies</li>
		<li><code>+illuminati deny groupies</code>: allows access to everyone but groupies (unless they're in the illuminati)</li>
		<li><code>\@eve \@alice\@nowhere.tld deny \@bob \@trent\@witches.live</code>: grants access to eve and alice, but locks out bob and trent</li>
		<li><code>&lt;grand duke&gt; #4th-intl&lt;comrade&gt;</code>: restricts the post to the eyes of the Fourth International's secret cabal of anointed comrades and the grand dukes of the Empire</li>
		<li><code>deny ~%3</code>: blocks a post from being seen by anyone with a staff rank level below 3</li>
	</ul>
	<p><strong>limitations:</strong> to inhibit potential denial-of-service attacks, ACL expressions can be a maximum of 128 characters, can contain at most 16 words, and cannot trigger queries against other servers. all information needed to evaluate an ACL expression must be known locally. this is particularly relevant with respect to rooms.</p>
</div></div>

Modified view/docskel.tpl from [004398018e] to [cb4a31dcc6].

1
2
3
4
5
6
7

8






9

10
11
<!doctype html>
<html>
	<head>
		<title>@instance :: @title</title>
		<link rel="stylesheet" href="/s/style.css">
	</head>
	<body class="@class">

		<h1>@title</h1>






		@body

	</body>
</html>







>
|
>
>
>
>
>
>
|
>


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

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

3
4
5
6
7
8
9



10
11
12
13
14
15
16
..
18
19
20
21
22
23
24
25
26
-- copies them into a data structure we can then
-- create templates from when we return to terra
local path = ...
local sources = {
	'docskel';
	'tweet';
	'profile';



}

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


local views = {}
for _,n in pairs(sources) do views[n] = ingest(n .. '.tpl') end
return views







>
>
>







 







|

3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
..
21
22
23
24
25
26
27
28
29
-- copies them into a data structure we can then
-- 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')
................................................................................
	txt = txt:gsub('\\(!%b[])', '%1')
	txt = txt:gsub('\\(!!)', '%1')
	return txt
end


local views = {}
for _,n in pairs(sources) do views[n:gsub('-','_')] = ingest(n .. '.tpl') end
return views

Added view/login-challenge.tpl version [c8511de2b7].





























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

Added view/login-username.tpl version [4dc628d5ef].

















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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

21
22
<div class="profile">
	<div class="banner">
		<img class="avatar" src="@avatar">
		<div class="id">@nym [@xid]</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>account created</th> <td>@tweetday</td></tr>
	</table>
	<div class="menu">
		<a href="/\@@xid">posts</a>
		<a href="/\@@xid/media">media</a>
		<a href="/\@@xid/follows">follows</a>
		<a href="/\@@xid/chat">chat</a>

	</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"><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>