parsav  Check-in [bbfea467bf]

Overview
Comment:permissions work now
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: bbfea467bf467820010635f449f683a1400fbb42537efd99a1816b71a36e4146
User & Date: lexi on 2020-12-27 02:31:30
Other Links: manifest | tags
Context
2020-12-27
04:08
look ma, im tweetin check-in: 8f954221a1 user: lexi tags: trunk
02:31
permissions work now check-in: bbfea467bf user: lexi tags: trunk
2020-12-25
23:37
iteration and important api adjustments check-in: f9559a83fc user: lexi tags: trunk
Changes

Modified backend/pgsql.t from [1740166796] to [36b1398523].

58
59
60
61
62
63
64
65























66
67
68
69
70
71
72
73
74
75
...
161
162
163
164
165
166
167






168
169
170
171
172
173
174
...
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
...
421
422
423
424
425
426
427









428
429
430
431
432
433
434
435
436
437
438
439


440
441
442
443
444
445
446
447
448
...
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
...
652
653
654
655
656
657
658
659







660



























661
			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 = {pstring,rawstring,pstring,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
................................................................................
			left join parsav_actors a     on au.uid = a.id
			left join parsav_servers s    on a.origin = s.id

			where au.aid = $1::bigint and au.blacklist = false and
				(au.netmask is null or au.netmask >> $2::inet)
		]];
	};







	post_create = {
		params = {uint64, rawstring, rawstring, rawstring}, sql = [[
			insert into parsav_posts (
				author, subject, acl, body,
				posted, discovered,
				circles, mentions
................................................................................
terra pqr:len(row: intptr, col: intptr)
	return lib.pq.PQgetlength(self.res, row, col)
end
terra pqr:cols() return lib.pq.PQnfields(self.res) end
terra pqr:string(row: intptr, col: intptr) -- not to be exported!!
	if self:null(row,col) then return nil end
	var v = lib.pq.PQgetvalue(self.res, row, col)
--	var r: lib.mem.ptr(int8)
--	r.ct = lib.str.sz(v)
--	r.ptr = v
	return v
end







terra pqr:bin(row: intptr, col: intptr) -- not to be exported!! DO NOT FREE

	return [lib.mem.ptr(uint8)] {
		ptr = [&uint8](lib.pq.PQgetvalue(self.res, row, col));
		ct = lib.pq.PQgetlength(self.res, row, col);
	}
end
terra pqr:String(row: intptr, col: intptr) -- suitable to be exported
	if self:null(row,col) then return [lib.mem.ptr(int8)] {ptr=nil,ct=0} end

	var s = [lib.mem.ptr(int8)] { ptr = lib.str.dup(self:string(row,col)) }
	s.ct = lib.pq.PQgetlength(self.res, row, col)

	return s
end
terra pqr:bool(row: intptr, col: intptr)
	var v = lib.pq.PQgetvalue(self.res, row, col)
	if @v == 0x01 then return true else return false end
end
terra pqr:cidr(row: intptr, col: intptr)
................................................................................
	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 = {}

................................................................................
	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)
................................................................................
		end
		
		var ret: lib.mem.ptr(lib.mem.ptr(lib.store.post)) ret:init(r.sz)
		for i=0,r.sz do ret.ptr[i] = row_to_post(&r, i) end -- MUST FREE ALL

		return ret
	end];
}



































return b








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


|







 







>
>
>
>
>
>







 







<
<
<


>
>
>
>
>
>
>

>






|
>
|
|
>







 







>
>
>
>
>
>
>
>
>












>
>

|







 







|








|







 







|
>
>
>
>
>
>
>

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

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
...
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
...
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
...
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
...
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
...
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
			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_create = {
		params = {
			rawstring, rawstring, uint64, lib.store.timepoint,
			rawstring, rawstring, lib.mem.ptr(uint8),
			rawstring, uint16, uint32
		};
		sql = [[
			insert into parsav_actors (
				nym,handle,
				origin,knownsince,
				bio,avataruri,key,
				title,rank,quota
			) values ($1::text, $2::text,
				case when $3::bigint = 0 then null
				     else $3::bigint end,
				to_timestamp($4::bigint),
				$5::bigint, $6::bigint, $7::bytea,
				$8::text, $9::smallint, $10::integer
			) returning id
		]];
	};


	actor_auth_pw = {
		params = {pstring,rawstring,pstring,lib.store.inet}, sql = [[
			select a.aid, a.uid, a.name 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
................................................................................
			left join parsav_actors a     on au.uid = a.id
			left join parsav_servers s    on a.origin = s.id

			where au.aid = $1::bigint and au.blacklist = false and
				(au.netmask is null or au.netmask >> $2::inet)
		]];
	};

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

	post_create = {
		params = {uint64, rawstring, rawstring, rawstring}, sql = [[
			insert into parsav_posts (
				author, subject, acl, body,
				posted, discovered,
				circles, mentions
................................................................................
terra pqr:len(row: intptr, col: intptr)
	return lib.pq.PQgetlength(self.res, row, col)
end
terra pqr:cols() return lib.pq.PQnfields(self.res) end
terra pqr:string(row: intptr, col: intptr) -- not to be exported!!
	if self:null(row,col) then return nil end
	var v = lib.pq.PQgetvalue(self.res, row, col)



	return v
end
terra pqr:_string(row: intptr, col: intptr) -- not to be exported!!
	if self:null(row,col) then return pstring.null() end
	return pstring {
		ptr = lib.pq.PQgetvalue (self.res, row, col);
		ct  = lib.pq.PQgetlength(self.res, row, col);
	}
end
terra pqr:bin(row: intptr, col: intptr) -- not to be exported!! DO NOT FREE
	if self:null(row,col) then return [lib.mem.ptr(uint8)].null() end
	return [lib.mem.ptr(uint8)] {
		ptr = [&uint8](lib.pq.PQgetvalue(self.res, row, col));
		ct = lib.pq.PQgetlength(self.res, row, col);
	}
end
terra pqr:String(row: intptr, col: intptr) -- suitable to be exported
	if self:null(row,col) then return pstring.null() end
	var s = pstring {
		ptr = lib.str.dup(self:string(row,col));
		ct = lib.pq.PQgetlength(self.res, row, col);
	}
	return s
end
terra pqr:bool(row: intptr, col: intptr)
	var v = lib.pq.PQgetvalue(self.res, row, col)
	if @v == 0x01 then return true else return false end
end
terra pqr:cidr(row: intptr, col: intptr)
................................................................................
	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 privmap = {}
do local struct pt { name:pstring, priv:lib.store.powerset }
for k,v in pairs(lib.store.powerset.members) do
	privmap[#privmap + 1] = quote
		var ps: lib.store.powerset ps:clear()
		(ps.[v] << true)
	in pt {name = lib.str.plit(v), priv = ps} end
end 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)
				var uid = r:int(uint64, 0,1)
				var name = r:String(0,2)
				r:free()
				return aid, uid, name
			end
		end
	end
	
	local out = symbol(uint8[64])
	local vdrs = {}

................................................................................
	end];
	 
	actor_auth_pw = [terra(
			src: &lib.store.source,
			ip: lib.store.inet,
			username: lib.mem.ptr(int8),
			cred: lib.mem.ptr(int8)
		): {uint64, uint64, pstring}

		[ 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, 0, pstring.null()
	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)
................................................................................
		end
		
		var ret: lib.mem.ptr(lib.mem.ptr(lib.store.post)) ret:init(r.sz)
		for i=0,r.sz do ret.ptr[i] = row_to_post(&r, i) end -- MUST FREE ALL

		return ret
	end];

	actor_powers_fetch = [terra(
		src: &lib.store.source,
		uid: uint64
	): lib.store.powerset
		var powers = lib.store.rights_default().powers
		var map = array([privmap])
		var r = queries.actor_powers_fetch.exec(src, uid)

		for i=0, r.sz do
			for j=0, [map.type.N] do
				var pn = r:_string(i,0)
				if map[j].name:cmp(pn) then
					if r:bool(i,1)
						then powers = powers + map[j].priv
						else powers = powers - map[j].priv
					end
				end
			end
		end

		return powers
	end];

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

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

return b

Modified common.lua from [3d397d42e6] to [e762fc8997].

92
93
94
95
96
97
98





99
100
101
102
103
104
105
		for _, v in pairs(b) do a[#a+1] = v end
	end;
	has = function(haystack,needle,eq)
		eq = eq or function(a,b) return a == b end
		for k,v in pairs(haystack) do
			if eq(needle,v) then return k end
		end





	end;
	ingest = function(f)
		local h = io.open(f, 'r')
		if h == nil then return nil end
		local txt = f:read('*a') f:close()
		return chomp(txt)
	end;







>
>
>
>
>







92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
		for _, v in pairs(b) do a[#a+1] = v end
	end;
	has = function(haystack,needle,eq)
		eq = eq or function(a,b) return a == b end
		for k,v in pairs(haystack) do
			if eq(needle,v) then return k end
		end
	end;
	keys = function(ary)
		local kt = {}
		for k,v in pairs(ary) do kt[#kt+1] = k end
		return kt
	end;
	ingest = function(f)
		local h = io.open(f, 'r')
		if h == nil then return nil end
		local txt = f:read('*a') f:close()
		return chomp(txt)
	end;

Modified config.lua from [dc89401662] to [29834379ec].

27
28
29
30
31
32
33




34
35
36
37
38
39
40
..
44
45
46
47
48
49
50



51
52
53
54
55
56
57
	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();
	};
................................................................................
	embeds = {
		{'style.css', 'text/css'};
		{'default-avatar.webp', 'image/webp'};
		{'padlock.webp', 'image/webp'};
		{'warn.webp', 'image/webp'};
	};
}



if u.ping '.fslckout' or u.ping '_FOSSIL_' then
	if u.ping '_FOSSIL_' then default_os = 'windows' end
	conf.build.branch = u.exec { 'fossil', 'branch', 'current' }
	conf.build.checkout = (u.exec { 'fossil', 'sql',
		[[select value from localdb.vvar where name = 'checkout-hash']]
	}):gsub("^'(.*)'$", '%1')
end







>
>
>
>







 







>
>
>







27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
..
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
	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)); 
	doc       = {
		online  = u.tobool(default('parsav_online_documentation',true)); 
		offline = u.tobool(default('parsav_offline_documentation',true)); 
	};
	outform   = default('parsav_emit_type', 'o');
	endian    = default('parsav_arch_endian', 'little');
	build     = {
		id = u.rndstr(6);
		release = u.ingest('release');
		when = os.date();
	};
................................................................................
	embeds = {
		{'style.css', 'text/css'};
		{'default-avatar.webp', 'image/webp'};
		{'padlock.webp', 'image/webp'};
		{'warn.webp', 'image/webp'};
	};
}
if os.getenv('parsav_let_me_be_an_idiot') == "i know what i'm doing" then
	conf.braingeniousmode = true -- SOUND GENERAL QUARTERS
end
if u.ping '.fslckout' or u.ping '_FOSSIL_' then
	if u.ping '_FOSSIL_' then default_os = 'windows' end
	conf.build.branch = u.exec { 'fossil', 'branch', 'current' }
	conf.build.checkout = (u.exec { 'fossil', 'sql',
		[[select value from localdb.vvar where name = 'checkout-hash']]
	}):gsub("^'(.*)'$", '%1')
end

Modified crypt.t from [48369b50e0] to [bf3957f4f4].

7
8
9
10
11
12
13

14
15
16
17
18
19

20
21
22
23
24
25
26
..
61
62
63
64
65
66
67








68
69
70
71
72
73
74
	rawcode = terra(code: int)
		if code < 0 then code = -code end
		return code and 0xFF80
	end;
	toobig = -lib.pk.MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE;
}
const.maxpemsz = math.floor((const.keybits / 8)*6.4) + 128 -- idk why this formula works but it basically seems to


local ctx = lib.pk.mbedtls_pk_context

local struct hashalg { id: uint8 bytes: intptr }
local m = {
	pemfile = uint8[const.maxpemsz];

	algsz = {
		sha1 =   160/8;
		sha256 = 256/8;
		sha512 = 512/8;
		sha384 = 384/8;
		sha224 = 224/8;
	}
................................................................................
terra m.pem(pub: bool, key: &ctx, buf: &uint8): bool
	if pub then
		return lib.pk.mbedtls_pk_write_pubkey_pem(key, buf, const.maxpemsz) == 0
	else
		return lib.pk.mbedtls_pk_write_key_pem(key, buf, const.maxpemsz) == 0
	end
end









m.destroy = lib.dispatch {
	[ctx] = function(v) return `lib.pk.mbedtls_pk_free(&v) end;

	[false] = function(ptr) return `ptr:free() end;
}








>






>







 







>
>
>
>
>
>
>
>







7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
..
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
	rawcode = terra(code: int)
		if code < 0 then code = -code end
		return code and 0xFF80
	end;
	toobig = -lib.pk.MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE;
}
const.maxpemsz = math.floor((const.keybits / 8)*6.4) + 128 -- idk why this formula works but it basically seems to
const.maxdersz = const.maxpemsz -- FIXME this is a safe value but obvs not the correct one

local ctx = lib.pk.mbedtls_pk_context

local struct hashalg { id: uint8 bytes: intptr }
local m = {
	pemfile = uint8[const.maxpemsz];
	const = const;
	algsz = {
		sha1 =   160/8;
		sha256 = 256/8;
		sha512 = 512/8;
		sha384 = 384/8;
		sha224 = 224/8;
	}
................................................................................
terra m.pem(pub: bool, key: &ctx, buf: &uint8): bool
	if pub then
		return lib.pk.mbedtls_pk_write_pubkey_pem(key, buf, const.maxpemsz) == 0
	else
		return lib.pk.mbedtls_pk_write_key_pem(key, buf, const.maxpemsz) == 0
	end
end

terra m.der(pub: bool, key: &ctx, buf: &uint8): intptr
	if pub then
		return lib.pk.mbedtls_pk_write_pubkey_der(key, buf, const.maxdersz)
	else
		return lib.pk.mbedtls_pk_write_key_der(key, buf, const.maxdersz)
	end
end

m.destroy = lib.dispatch {
	[ctx] = function(v) return `lib.pk.mbedtls_pk_free(&v) end;

	[false] = function(ptr) return `ptr:free() end;
}

Added doc/acl.md version [ea4e893c12].









































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
# access control

to help limit who can see your post (and how far it is propagated), `parsav` uses **ACL expressions**. 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 `~`, a tilde character, so `~all` matches nobody, and `~followed` matches users you do not follow.

* **all**: matches any and all users
* **local**: matches users who belong to this instance
* **mutuals**: matches users you follow who also follow you
* **followed**: matches users you follow
* **followers**: matches users who follow you
* **groupies**: matches users who follow you, but whom you do not follow
* **mentioned**: matches users who are mentioned in the post
* **staff**: matches instance staff (equivalent to `~%0`)
* **admin**: matches the individual named as the instance administrator, if any
* **@**`handle`: matches the user `handle`
* **+**`circle`: matches users you have categorized under `circle`
* **#**`room`: matches users who are members of `room`
* **%**`rank`: matches users of `rank` or higher (e.g. `%3` matches users of rank 3, 2, and 1). as a special case, `%0` matches ordinary users
* **#**`room`**%**`rank`: matches users who hold `rank` in `room`
* **<**`title`**>**: matches peers of the net who have been created `title` by the sovereign
* **#**`room`**<**`title`**>**: matches peers of the chat who have been created `title` by `room` staff

to evaluate an ACL expression, `parsav` 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.

**policy** is whether a term grants or denies access. the default term policy is **allow**, but you can control the policy with the keywords `allow` and `deny`. 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; `allow @bob` and `deny trent` do exactly what you'd expect — the former allows bob and only bob in; the latter denies access only to trent, but grants access to the rest of the world.

expressions must contain at least one term to be valid. if they consist only of policy keywords, they will be rejected.

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 `local` means only local users can view it. however, much more complex expressions are possible.

* `deny groupies allow +illuminati`: permits access to the illuminati, but excluding those members who are groupies
* `+illuminati deny groupies`: allows access to everyone but groupies (unless they're in the illuminati)
* `@eve @alice@nowhere.tld deny @bob @trent@witches.live`: grants access to eve and alice, but locks out bob and trent
* `<grand duke> #4th-intl<comrade>`: restricts the post to the eyes of the Fourth International's secret cabal of anointed comrades and the grand dukes of the Empire
* `deny ~%3`: blocks a post from being seen by anyone with a staff rank level below 3

**limitations:** 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.

Added doc/invocation.md version [2a91b27d48].



>
1
# daemon invocation

Added doc/load.lua version [68f6a9a498].















































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
local path = ...
local sources = {
-- user section
	acl = {title = 'access control lists'};
-- admin section
	--overview = {title = 'server overview', priv = 'config'};
	invocation = {title = 'daemon invocation', priv = 'config'};
	--backends = {title = 'storage backends', priv = 'config'};
		--pgsql = {title = 'pgsql', priv = 'config', parent = 'backends'};
}

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

local doc = {}
for n,meta in pairs(sources) do doc[n] = {
	name = n;
	text = ingest(n .. '.md');
	meta = meta;
} end
return doc

Modified http.t from [654249752e] to [7092b409fb].

1
2
3
4
5
6
7
8
9
10
-- 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';


|







1
2
3
4
5
6
7
8
9
10
-- vim: ft=terra
local m = {}
local util = lib.util

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

Modified mem.t from [15de92d877] to [a177326f1c].

52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
...
104
105
106
107
108
109
110




111
112
113
114
115
116
117
		{'ct', intptr};
	}
	t.ptr_basetype = ty
	local recurse = false
	--if ty:isstruct() then
		--if ty.methods.free then recurse = true end
	--end
	t.metamethods.__not = macro(function(self)
		return `self.ptr
	end)
	if dyn then
		t.methods = {
			free = terra(self: &t): bool
				[recurse and quote
					self.ptr:free()
				end or {}]
				if self.ct > 0 then
................................................................................
		end
	end
	terra t:advance(n: intptr)
		self.ptr = self.ptr + n
		self.ct = self.ct - n
		return self.ptr
	end




	if not ty:isstruct() then
		terra t:cmp_raw(other: &ty)
			for i=0, self.ct do
				if self.ptr[i] ~= other[i] then return false end
			end
			return true
		end







<
<
<







 







>
>
>
>







52
53
54
55
56
57
58



59
60
61
62
63
64
65
...
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
		{'ct', intptr};
	}
	t.ptr_basetype = ty
	local recurse = false
	--if ty:isstruct() then
		--if ty.methods.free then recurse = true end
	--end



	if dyn then
		t.methods = {
			free = terra(self: &t): bool
				[recurse and quote
					self.ptr:free()
				end or {}]
				if self.ct > 0 then
................................................................................
		end
	end
	terra t:advance(n: intptr)
		self.ptr = self.ptr + n
		self.ct = self.ct - n
		return self.ptr
	end
	terra t.methods.null(): t return t { ptr = nil, ct = 0 } end -- maybe should be a macro?
	terra t:ref() return self.ptr ~= nil end
	t.metamethods.__not = macro(function(self) return `not self:ref() end)
	t.metamethods.__apply = macro(function(self,idx) return `self.ptr[idx] end)
	if not ty:isstruct() then
		terra t:cmp_raw(other: &ty)
			for i=0, self.ct do
				if self.ptr[i] ~= other[i] then return false end
			end
			return true
		end

Modified parsav.md from [eb5d145ae6] to [ae23203c4b].

5
6
7
8
9
10
11

12
13
14
15
16



17
18
19
20
21
22
23
24
25
..
28
29
30
31
32
33
34


35
36
37
38
39
40
41
..
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79






80
81
82
83
84
85
86
..
95
96
97
98
99
100
101
102
103
104
105
## backends
parsav is designed to be storage-agnostic, and can draw data from multiple backends at a time. backends can be enabled or disabled at compile time to avoid unnecessary dependencies.

* postgresql

## dependencies


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

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

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

................................................................................
		netmask cidr,
		blacklist bool
    )

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

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

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

below is a full list of authentication types we intend to support. a checked box indicates the scheme has been implemented.

* ☑ pw-sha{512,384,256,224}: an ordinary password, hashed with the appropriate algorithm
* ☐ pw-{sha1,md5,clear} (insecure, must be manually enabled at compile time with the config variable `parsav_let_me_be_a_dumbass="i know what i'm doing"`)
* ☐ pw-pbkdf2-hmac-sha{…}: a password hashed with the Password-Based Key Derivation Function 2 instead of plain SHA2






* ☐ api-digest-sha{…}: a value that can be hashed with the current epoch to derive a temporary access key without logging in. these are used for API calls, sent in the header `X-API-Key`.
* ☐ otp-time-sha1: a TOTP PSK: the first two bytes represent the step, the third byte the OTP length, and the remaining ten bytes the secret key
* ☐ tls-cert-fp: a fingerprint of a client certificate
* ☐ tls-cert-ca: a value of the form `fp/key=value` where a client certificate with the property `key=value` (e.g. `uid=cyberlord19`) signed by a certificate authority matching the given fingerprint `fp` can authenticate the user
* ☐ challenge-rsa-sha256: an RSA public key. the user is presented with a challenge and must sign it with the corresponding private key using SHA256.
* ☐ challenge-ecc-sha256: a Curve25519 public key. the user is presented with a challenge and must sign it with the corresponding private key using SHA256.
* ☐ challenge-ecc448-sha256: a Curve448 public key. the user is presented with a challenge and must sign it with the corresponding private key using SHA256.
................................................................................
parsav needs more storage backends, as it currently supports only postgres. some possibilities, in order of priority, are:

* plain text/filesystem storage
* lmdb
* sqlite3
* generic odbc
* lua
* ldap?? possibly just for users
* cdb (for static content, maybe?)
* mariadb/mysql
* the various nosql horrors, e.g. redis, mongo, and so on







>
|
|
|
|
|
>
>
>

|







 







>
>







 







|



|




>
>
>
>
>
>







 







|



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
..
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
..
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
...
107
108
109
110
111
112
113
114
115
116
117
## backends
parsav is designed to be storage-agnostic, and can draw data from multiple backends at a time. backends can be enabled or disabled at compile time to avoid unnecessary dependencies.

* postgresql

## dependencies

* runtime
  * mongoose
  * json-c
  * mbedtls
  * **postgresql backend:**
    * postgresql-libs 
* compile-time
  * cmark (commonmark implementation), for transformation of the help files, whose source is in commonmark. online documentation transforms these into html and embeds them in the binary; cmark is also used to to produce the troff source which is used to build the offline documentation. disable with `parsav_online_documentation=no parsav_offline_documentation=no`
  * troff implementation (tested with groff but as far as i know we don't need any groff-specific extensions) to produce PDFs and manpages from the cmark-generated intermediate forms. disable with `parsav_offline_documentation=no`

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

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

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

## configuring

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

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

................................................................................
		netmask cidr,
		blacklist bool
    )

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

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

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

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

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

* plain text/filesystem storage
* lmdb
* sqlite3
* generic odbc
* lua
* ldap for auth (and maybe actors?)
* cdb (for static content, maybe?)
* mariadb/mysql
* the various nosql horrors, e.g. redis, mongo, and so on

Modified parsav.t from [579976ae23] to [8c471232dd].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
..
45
46
47
48
49
50
51
52

53
54
55
56
57
58
59
..
73
74
75
76
77
78
79
80

81
82
83
84
85
86
87
...
174
175
176
177
178
179
180



181
182
183
184
185
186
187
188
...
260
261
262
263
264
265
266






























267
268
269
270
271
272
273
...
310
311
312
313
314
315
316

317
318
319
320
321
322
323
...
337
338
339
340
341
342
343

344
345
346
347
348
349
350
-- vim: ft=terra

local util = dofile('common.lua')
local buildopts, buildargs = util.parseargs{...}
config = dofile('config.lua')

lib = {
	init = {};
	load = function(lst)
		for _, l in pairs(lst) do
			local path = {}
			for m in l:gmatch('([^:]+)') do path[#path+1]=m end
			local tgt = lib
			for i=1,#path-1 do
				if tgt[path[i]] == nil then tgt[path[i]] = {} end
................................................................................
				local str = tostring(v:asvalue())
				code[#code+1] = `lib.io.send(2, str, [#str])
			else
				code[#code+1] = quote var n = v in
					lib.io.send(2, n, lib.str.sz(n)) end
			end
		end
		if nl then code[#code+1] = `lib.io.send(fd, '\n', 1) end

		return code
	end;
	emitv = function(nl,fd,...)
		local vec = {}
		local defs = {}
		for i,v in ipairs{...} do
			local str, ct
................................................................................
				else--if v.tree:is 'constant' then
					str = tostring(v:asvalue())
				end
				ct = ct or #str
			end
			vec[#vec + 1] = `[lib.uio.iovec]{iov_base = [&opaque](str), iov_len = ct}
		end
		if nl then vec[#vec + 1] = `[lib.uio.iovec]{iov_base = [&opaque]('\n'), iov_len = 1} end

		return quote
			[defs]
			var strs = array( [vec] )
		in lib.uio.writev(fd, strs, [#vec]) end
	end;
	trn = macro(function(cond, i, e)
		return quote
................................................................................
		var dfs = arrayof(int8, 0x30 + diff/10, 0x30 + diff%10, 0x20, 0)
		[ lib.emit(false, 2, ' \27[36m+', `&dfs[0]) ]
	end
end

local defrep = function(level,n,code)
	return macro(function(...)



		local q = lib.emit(true, 2, noise_header(code,n), ...)
		return quote if noise >= level then timehdr(); [q] end end
	end);
end
lib.dbg = defrep(3,'debug', '32')
lib.report = defrep(2,'info', '35')
lib.warn = defrep(1,'warn', '33')
lib.bail = macro(function(...)
................................................................................
		local q = quote var [new] new:clear() end
		for i = 0, bytes - 1 do
			q = quote [q]
				new._store[i] = self._store[i] or other._store[i]
			end
		end
		return quote [q] in new end






























	end)
	bit.metamethods.__cast = function(from,to,e)
		local q = quote var s = e
			in (s._set._store[s._v/8] and (1 << s._v % 8)) end
		if to == bit then error('casting to bit is not meaningful')
		elseif to == bool then return `([q] ~= 0)
		elseif to:isintegral() then return q
................................................................................

lib.cmdparse = terralib.loadfile('cmdparse.t')()

do local collate = function(path,f, ...)
	return loadfile(path..'/'..f..'.lua')(path, ...)
end
data = {

	view = collate('view','load');
	static = {};
	stmap = global(lib.mem.ref(int8)[#config.embeds]); -- array of pointers to static content
} end
for i,e in ipairs(config.embeds) do local v = e[1]
	local fh = io.open('static/' .. v,'r')
	if fh == nil then error('static file ' .. v .. ' missing') end
................................................................................
	'render:nav';
	'render:login';
	'render:profile';
	'render:userpage';
	'render:compose';
	'render:tweet';
	'render:timeline';

	'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







|







 







|
>







 







|
>







 







>
>
>
|







 







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







 







>







 







>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
..
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
..
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
...
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
...
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
...
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
...
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
-- vim: ft=terra

local util = dofile('common.lua')
local buildopts, buildargs = util.parseargs{...}
config = dofile('config.lua')

lib = {
	init = {}, util = util;
	load = function(lst)
		for _, l in pairs(lst) do
			local path = {}
			for m in l:gmatch('([^:]+)') do path[#path+1]=m end
			local tgt = lib
			for i=1,#path-1 do
				if tgt[path[i]] == nil then tgt[path[i]] = {} end
................................................................................
				local str = tostring(v:asvalue())
				code[#code+1] = `lib.io.send(2, str, [#str])
			else
				code[#code+1] = quote var n = v in
					lib.io.send(2, n, lib.str.sz(n)) end
			end
		end
		if nl == true then code[#code+1] = `lib.io.send(fd, '\n', 1)
		elseif nl then code[#code+1] = `lib.io.send(fd, nl, [#nl]) end
		return code
	end;
	emitv = function(nl,fd,...)
		local vec = {}
		local defs = {}
		for i,v in ipairs{...} do
			local str, ct
................................................................................
				else--if v.tree:is 'constant' then
					str = tostring(v:asvalue())
				end
				ct = ct or #str
			end
			vec[#vec + 1] = `[lib.uio.iovec]{iov_base = [&opaque](str), iov_len = ct}
		end
		if nl == true then vec[#vec + 1] = `[lib.uio.iovec]{iov_base = [&opaque]('\n'), iov_len = 1}
		elseif nl then vec[#vec + 1] = `[lib.uio.iovec]{iov_base = [&opaque](nl), iov_len = [#nl]} end
		return quote
			[defs]
			var strs = array( [vec] )
		in lib.uio.writev(fd, strs, [#vec]) end
	end;
	trn = macro(function(cond, i, e)
		return quote
................................................................................
		var dfs = arrayof(int8, 0x30 + diff/10, 0x30 + diff%10, 0x20, 0)
		[ lib.emit(false, 2, ' \27[36m+', `&dfs[0]) ]
	end
end

local defrep = function(level,n,code)
	return macro(function(...)
		local fn = (...).filename
		local ln = tostring((...).linenumber)
		local dbgtag = string.format('\27[35m · \27[34m%s:\27[1m%s\27[m\n', fn,ln)
		local q = lib.emit(level < 3 and true or dbgtag, 2, noise_header(code,n), ...)
		return quote if noise >= level then timehdr(); [q] end end
	end);
end
lib.dbg = defrep(3,'debug', '32')
lib.report = defrep(2,'info', '35')
lib.warn = defrep(1,'warn', '33')
lib.bail = macro(function(...)
................................................................................
		local q = quote var [new] new:clear() end
		for i = 0, bytes - 1 do
			q = quote [q]
				new._store[i] = self._store[i] or other._store[i]
			end
		end
		return quote [q] in new end
	end)
	set.metamethods.__and = macro(function(self,other)
		local new = symbol(set)
		local q = quote var [new] new:clear() end
		for i = 0, bytes - 1 do
			q = quote [q]
				new._store[i] = self._store[i] and other._store[i]
			end
		end
		return quote [q] in new end
	end)
	set.metamethods.__not = macro(function(self)
		local new = symbol(set)
		local q = quote var [new] new:clear() end
		for i = 0, bytes - 1 do
			q = quote [q]
				new._store[i] = not self._store[i] 
			end
		end
		return quote [q] in new end
	end)
	set.metamethods.__sub = macro(function(self,other)
		local new = symbol(set)
		local q = quote var [new] new:clear() end
		for i = 0, bytes - 1 do
			q = quote [q]
				new._store[i] = self._store[i] and (not other._store[i])
			end
		end
		return quote [q] in new end
	end)
	bit.metamethods.__cast = function(from,to,e)
		local q = quote var s = e
			in (s._set._store[s._v/8] and (1 << s._v % 8)) end
		if to == bit then error('casting to bit is not meaningful')
		elseif to == bool then return `([q] ~= 0)
		elseif to:isintegral() then return q
................................................................................

lib.cmdparse = terralib.loadfile('cmdparse.t')()

do local collate = function(path,f, ...)
	return loadfile(path..'/'..f..'.lua')(path, ...)
end
data = {
	doc = collate('doc','load');
	view = collate('view','load');
	static = {};
	stmap = global(lib.mem.ref(int8)[#config.embeds]); -- array of pointers to static content
} end
for i,e in ipairs(config.embeds) do local v = e[1]
	local fh = io.open('static/' .. v,'r')
	if fh == nil then error('static file ' .. v .. ' missing') end
................................................................................
	'render:nav';
	'render:login';
	'render:profile';
	'render:userpage';
	'render:compose';
	'render:tweet';
	'render:timeline';
	'render:docpage';
	'route';
}

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

Added render/docpage.t version [7e1168b387].















































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
-- vim: ft=terra
local page = lib.srv.convo.page
local pstr = lib.mem.ptr(int8)
local pref = lib.mem.ref(int8)
local P = lib.str.plit
local R = lib.str.lit

local topics = lib.util.keys(data.doc)
local topicidxt = {}
table.sort(topics) -- because deterministic builds are good
local branches = {}
for i,k in pairs(topics) do
	topicidxt[k] = i
	local par = data.doc[k].meta.parent
	if par then
		branches[par] = branches[par] or {}
		local br = branches[par]
		br[#br+1] = k
	end
end

local struct pgpair {
	content:page name:pref title:pref
	priv:lib.store.powerset parent:intptr
}
local pages = symbol(pgpair[#topics])
local allpages = {}


for i,v in ipairs(topics) do
	local t = data.doc[v]
	local par = 0
	if t.meta.parent then par = topicidxt[t.meta.parent] end
	local restrict = symbol(lib.store.powerset)
	local setbits = quote restrict:clear() end
	if t.meta.priv then
		if type(t.meta.priv) ~= 'table' then t.meta.priv = {t.meta.priv} end
		for _,v in pairs(t.meta.priv) do
			setbits = quote [setbits]; (restrict.[v] << true) end
		end
	end
	allpages[i] = quote var [restrict]; [setbits] in pgpair {
		name = R(v);
		parent = par;
		priv = restrict;
		title = R(t.meta.title);
		content = page {
			title = ['documentation :: ' .. t.meta.title];
			body = [ t.text ];
			class = P'doc article';
		};
	} end
end

local terra 
showpage(co: &lib.srv.convo, id: pref)
	var [pages] = array([allpages])
	for i=0,[pages.type.N] do
		if pages[i].name:cmp(id) then
			co:stdpage(pages[i].content)
			return
		end
	end -- else
	co:complain(404,'not found', 'no help article with that identifier is available')
end

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

local terra 
render_docpage(co: &lib.srv.convo, pg: pref)
	var nullprivs: lib.store.powerset nullprivs:clear()
	if not pg then -- display index
		var list: lib.str.acc list:compose('<ul>')
		var [pages] = array([allpages])
		for i=0,[pages.type.N] do
			if pages[i].parent == 0 and (pages[i].priv:sz() == 0 or
				(co.aid ~= 0 and (co.who.rights.powers
					and pages[i].priv):sz() > 0)) then
				list:lpush('<li><a href="/doc/'):rpush(pages[i].name):lpush('">')
					:rpush(pages[i].title):lpush('</a>')
				if co.aid ~= 0 then
					pushbranches(&list, i, co.who.rights.powers)
				else
					pushbranches(&list, i, nullprivs)
				end
				list:lpush('</li>')
			end
		end
		list:lpush('</ul>')

		var bp = list:finalize()
		co:stdpage(page {
			title = 'documentation';
			body = bp;
			class = P'doc listing';
		})
		bp:free()
	else showpage(co, pg) end
end

return render_docpage

Modified render/nav.t from [450b73f673] to [27572ae99b].

3
4
5
6
7
8
9
10
11
12
13
14
15
16
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="/help">help</a> <a href="/logout">log out</a>')
	else
		t:lpush('<a href="/help">help</a> <a href="/login">log in</a>')
	end
	return t:finalize()
end
return render_nav







|

|




3
4
5
6
7
8
9
10
11
12
13
14
15
16
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="/doc">docs</a> <a href="/logout">log out</a>')
	else
		t:lpush('<a href="/doc">docs</a> <a href="/login">log in</a>')
	end
	return t:finalize()
end
return render_nav

Modified render/profile.t from [30c7a0b6c6] to [d063b74f92].

15
16
17
18
19
20
21

22
23
24
25
26
27
28
			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:finalize()
	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]







>







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
			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:finalize()
	else
		aux:compose('<a href="/', actor.xid, '/follow">remote follow</a>')
		auxp = aux:finalize()
	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]

Modified render/userpage.t from [7b79f9904c] to [9774faf032].

4
5
6
7
8
9
10
11
12
13
14

15
16
17
18
19
20
21
22
23
24
25
26
27
	var ti: lib.str.acc 
	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;
		title = ti:finalize();

		body = pftxt;
		class = lib.str.plit 'profile';
		navlinks = co.navbar;
	}

	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]})
	doc.title:free()
end

return render_userpage







<
<
<
|
>
|

<
|

<
<
<
<
|



4
5
6
7
8
9
10



11
12
13
14

15
16




17
18
19
20
	var ti: lib.str.acc 
	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 tiptr = ti:finalize()
	co:stdpage([lib.srv.convo.page] {
		title = tiptr; body = pftxt;
		class = lib.str.plit 'profile';

	})





	tiptr:free()
end

return render_userpage

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

158
159
160
161
162
163
164
165









166
167
168
169
170
171
172
...
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
...
251
252
253
254
255
256
257



258
259
260
261
262
263
264
		var redirto: lib.str.acc redirto:compose('/post/',{idbuf,idlen}) defer redirto:free()
		co:reroute(redirto.buf)
	end
end

terra http.timeline(co: &lib.srv.convo, mode: hpath)
	lib.render.timeline(co,lib.trn(mode.ptr == nil, rstring{ptr=nil}, mode.ptr[1]))
	return









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
................................................................................
	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)
	lib.dbg('got nav ', {co.navbar.ptr,co.navbar.ct}, "||", co.navbar.ptr)
	-- 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
		if (co.srv.cfg.pol_sec == lib.srv.secmode.private or
................................................................................
		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)
		elseif path.ptr[0]:cmp(lib.str.lit('tl')) then
			http.timeline(co, path)



		else goto notfound end
		return
	end

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







|
>
>
>
>
>
>
>
>
>







 







<







 







>
>
>







158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
...
212
213
214
215
216
217
218

219
220
221
222
223
224
225
...
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
		var redirto: lib.str.acc redirto:compose('/post/',{idbuf,idlen}) defer redirto:free()
		co:reroute(redirto.buf)
	end
end

terra http.timeline(co: &lib.srv.convo, mode: hpath)
	lib.render.timeline(co,lib.trn(mode.ptr == nil, rstring{ptr=nil}, mode.ptr[1]))
end

terra http.documentation(co: &lib.srv.convo, path: hpath)
	if path.ct == 2 then
		lib.render.docpage(co,path(1))
	elseif path.ct == 1 then
		lib.render.docpage(co, rstring.null())
	else
		co:complain(404, 'no such documentation', 'invalid documentation URL')
	end
end

do local branches = quote end
	local filename, flen = symbol(&int8), symbol(intptr)
	local page = symbol(lib.http.page)
	local send = label()
	local storage = data.stmap
................................................................................
	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
		if (co.srv.cfg.pol_sec == lib.srv.secmode.private or
................................................................................
		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)
		elseif path.ptr[0]:cmp(lib.str.lit('tl')) then
			http.timeline(co, path)
		elseif path.ptr[0]:cmp(lib.str.lit('doc')) then
			if meth ~= method.get and meth ~= method.head then goto wrongmeth end
			http.documentation(co, path)
		else goto notfound end
		return
	end

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

Modified schema.sql from [6bf306c986] to [097969b0cb].

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

drop table if exists parsav_rights cascade;
create table parsav_rights (
	key text,
	actor bigint references parsav_actors(id)
		on delete cascade,
	allow boolean,


	primary key (key,actor)
);

insert into parsav_actors (handle,rank,quota) values (:'admin',1,0);
insert into parsav_rights (actor,key,allow)
	select (select id from parsav_actors where handle=:'admin'), a.column1, a.column2 from (values
		('ban',true),
		('config',true),
		('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)







|
>







|



>
>
>







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

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

	primary key (key,actor)
);

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

drop table if exists parsav_posts cascade;
create table parsav_posts (
	id         bigint primary key default (1+random()*(2^63-1))::bigint,
	author     bigint references parsav_actors(id)

Modified srv.t from [d14bb4b6ce] to [8c264568c9].

1
2
3

4
5
6
7
8
9
10
..
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
...
163
164
165
166
167
168
169






















170
171
172
173
174
175
176
...
344
345
346
347
348
349
350
351



352
353
354
355
356
357
358
...
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496






































497
498
499
500
501
502
503
-- 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
................................................................................
			elseif rt.stat_basetype then tk = stat
			elseif rt.ptr_basetype then tk = ptr end
			break
		end
	end
	
	local r = symbol(rt)

	if tk == primary then
		return quote
			var [r]
			for i=0,self.sources.ct do var src = self.sources.ptr + i
				if src.handle ~= nil and src.backend.[meth] ~= nil then
					r = src:[meth]([expr])
					goto success
				end
			end
			lib.bail(['no active backends provide critical capability ' .. meth .. '!'])
			::success::;
		in r end
	else local ok, empty
		if tk == ptr then
			ok = `r.ptr ~= nil
			empty = `[rt]{ptr=nil,ct=0}
		elseif tk == stat then
			ok = `r.ok == true
................................................................................
	body:send(self.con, code, [lib.mem.ptr(lib.http.header)] {
		ptr = &hdrs[0], ct = [hdrs.type.N]
	})

	body.title:free()
	body.body:free()
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
................................................................................
							lib.osclock.time(nil))
						if aid ~= 0 then co.aid = aid end
					end ::nocookie::;
				end

				if co.aid ~= 0 then
					var sess, usr = co.srv:actor_session_fetch(co.aid, peer)
					if sess.ok == false then co.aid = 0 else co.who = usr.ptr end



				end

				var uridec = lib.mem.heapa(int8, msg.uri.len) defer uridec:free()
				var urideclen = lib.net.mg_url_decode(msg.uri.ptr, msg.uri.len, uridec.ptr, uridec.ct, 1)

				var uri = uridec
				if urideclen == -1 then
................................................................................
	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

srv.methods.start = terra(self: &srv, befile: rawstring)

|

>







 







>






|



|







 







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







 







|
>
>
>







 







|








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







1
2
3
4
5
6
7
8
9
10
11
..
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
...
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
...
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
...
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
-- vim: ft=terra
local util = lib.util
local secmode = lib.enum { 'public', 'private', 'lockdown', 'isolate' }
local pstring = lib.mem.ptr(int8)
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
................................................................................
			elseif rt.stat_basetype then tk = stat
			elseif rt.ptr_basetype then tk = ptr end
			break
		end
	end
	
	local r = symbol(rt)
	local succ = label()
	if tk == primary then
		return quote
			var [r]
			for i=0,self.sources.ct do var src = self.sources.ptr + i
				if src.handle ~= nil and src.backend.[meth] ~= nil then
					r = src:[meth]([expr])
					goto [succ]
				end
			end
			lib.bail(['no active backends provide critical capability ' .. meth .. '!'])
			::[succ]::;
		in r end
	else local ok, empty
		if tk == ptr then
			ok = `r.ptr ~= nil
			empty = `[rt]{ptr=nil,ct=0}
		elseif tk == stat then
			ok = `r.ok == true
................................................................................
	body:send(self.con, code, [lib.mem.ptr(lib.http.header)] {
		ptr = &hdrs[0], ct = [hdrs.type.N]
	})

	body.title:free()
	body.body:free()
end

struct convo.page {
	title: pstring
	body: pstring
	class: pstring
}

terra convo:stdpage(pg: convo.page)
	var doc = data.view.docskel {
		instance = self.srv.cfg.instance;
		title = pg.title;
		body = pg.body;
		class = pg.class;
		navlinks = self.navbar;
	}

	var hdrs = array(
		lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' }
	)

	doc:send(self.con,200,[lib.mem.ptr(lib.http.header)] {ct = [hdrs.type.N], ptr = &hdrs[0]})
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
................................................................................
							lib.osclock.time(nil))
						if aid ~= 0 then co.aid = aid end
					end ::nocookie::;
				end

				if co.aid ~= 0 then
					var sess, usr = co.srv:actor_session_fetch(co.aid, peer)
					if sess.ok == false then co.aid = 0 else
						co.who = usr.ptr
						co.who.rights.powers = server:actor_powers_fetch(co.who.id)
					end
				end

				var uridec = lib.mem.heapa(int8, msg.uri.len) defer uridec:free()
				var urideclen = lib.net.mg_url_decode(msg.uri.ptr, msg.uri.len, uridec.ptr, uridec.ct, 1)

				var uri = uridec
				if urideclen == -1 then
................................................................................
	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(i):actor_auth_how(ip, usn)
		if iok then
			cs = cs + set
			ok = iok
		end
	end
	return cs, ok
end

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

					if self.sources(i).backend.actor_auth_register_uid ~= nil then
						self.sources(i):actor_auth_register_uid(aid,newuid)
					end
				end
				return aid
			end
		end
	end

	return 0
end

--9twh8y94i5c1qqr7hxu20fyd
terra cfgcache.methods.load :: {&cfgcache} -> {}
terra cfgcache:init(o: &srv)
	self.overlord = o
	self:load()
end

srv.methods.start = terra(self: &srv, befile: rawstring)

Modified static/style.scss from [1dafd96ed6] to [9520b92c84].

212
213
214
215
216
217
218

219
220
221
222
223
224
225
...
463
464
465
466
467
468
469






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

................................................................................
		background: linear-gradient(to left, tone(-55%,-0.5), transparent);
	}
}

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













>







 







>
>
>
>
>
>
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
...
464
465
466
467
468
469
470
471
472
473
474
475
476
	display: block;
	position: relative;
	min-height: calc(100vh - 1.1in);
	margin-top: 0;
	margin-bottom: 0;
	padding: 0 0.4in;
	padding-top: 1.1in;
	padding-bottom: 0.1in;
	background-color: adjust-color($color, $lightness: -45%, $alpha: 0.4);
	border: {
		left: 1px solid black;
		right: 1px solid black;
	}
}

................................................................................
		background: linear-gradient(to left, tone(-55%,-0.5), transparent);
	}
}

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

body.doc main {
	@extend %serif;
	li { margin-top: 0.05in; }
	li:first-child { margin-top: 0; }
}

Modified store.t from [4782518865] to [5b8778f17f].

30
31
32
33
34
35
36
37

38
39
40
41
42
43
44
..
61
62
63
64
65
66
67

68
69
70
71
72
73
74
...
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

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

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

	avatar: str
	knownsince: m.timepoint
	rights: m.rights
	key: lib.mem.ptr(uint8)

-- ephemera
	xid: str
................................................................................
	open: &m.source -> &opaque
	close: &m.source -> {}

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

	actor_save: {&m.source, m.actor} -> bool
	actor_create: {&m.source, m.actor} -> 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
			-- fingerprint: rawstring
	actor_auth_api:    {&m.source, m.inet, rawstring, rawstring} -> uint64

		-- handles API authentication
			-- origin: inet
			-- handle: rawstring
			-- key:    rawstring (X-API-Key)
	actor_auth_record_fetch: {&m.source, uint64} -> lib.mem.ptr(m.auth)

	actor_session_fetch: {&m.source, uint64, m.inet} -> {lib.stat(m.auth), lib.mem.ptr(m.actor)}
		-- retrieves an auth record + actor combo suitable by AID suitable
		-- for determining session validity & caps
			-- aid:    uint64
			-- origin: inet






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

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







|
>







 







>







 







|
|












|
>
|
>




|
>




>





>





>
>
>
>
>







30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
..
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
...
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

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

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

struct m.source

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

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

-- ephemera
	xid: str
................................................................................
	open: &m.source -> &opaque
	close: &m.source -> {}

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

	actor_save: {&m.source, &m.actor} -> bool
	actor_create: {&m.source, &m.actor} -> uint64
	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, uint64, pstr}
	actor_auth_pw: {&m.source, m.inet, lib.mem.ptr(int8), lib.mem.ptr(int8) }
			-> {uint64, uint64, pstr}
		-- handles password-based logins against hashed passwords
			-- origin: inet
			-- handle: rawstring
			-- token:  rawstring
	actor_auth_tls:    {&m.source, m.inet, rawstring}
			-> {uint64, uint64, pstr}
		-- handles implicit authentication performed as part of an TLS connection
			-- origin: inet
			-- fingerprint: rawstring
	actor_auth_api:    {&m.source, m.inet, rawstring, rawstring} -> uint64
			-> {uint64, uint64, pstr}
		-- handles API authentication
			-- origin: inet
			-- handle: rawstring
			-- key:    rawstring (X-API-Key)
	actor_auth_record_fetch: {&m.source, uint64} -> lib.mem.ptr(m.auth)
	actor_powers_fetch: {&m.source, uint64} -> m.powerset
	actor_session_fetch: {&m.source, uint64, m.inet} -> {lib.stat(m.auth), lib.mem.ptr(m.actor)}
		-- retrieves an auth record + actor combo suitable by AID suitable
		-- for determining session validity & caps
			-- aid:    uint64
			-- origin: inet
	actor_auth_register_uid: {&m.source, uint64, uint64} -> {}
		-- notifies the backend module of the UID that has been assigned for
		-- an authentication ID
			-- aid: uint64
			-- uid: uint64

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

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

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

1
2
3
4
5
6
7
8
9
10
...
174
175
176
177
178
179
180


181
182
183
184
185
186
187
-- vim: ft=terra
-- string.t: string classes
local util = dofile('common.lua')
local pstr = lib.mem.ptr(int8)
local pref = lib.mem.ref(int8)

local m = {
	sz = terralib.externfunction('strlen', rawstring -> intptr);
	cmp = terralib.externfunction('strcmp', {rawstring, rawstring} -> int);
	ncmp = terralib.externfunction('strncmp', {rawstring, rawstring, intptr} -> int);
................................................................................
		return `[lib.mem.ptr(int8)] {ptr = nil, ct = 0}
	end
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 = {}
	for i,v in ipairs{...} do


|







 







>
>







1
2
3
4
5
6
7
8
9
10
...
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
-- vim: ft=terra
-- string.t: string classes
local util = lib.util
local pstr = lib.mem.ptr(int8)
local pref = lib.mem.ref(int8)

local m = {
	sz = terralib.externfunction('strlen', rawstring -> intptr);
	cmp = terralib.externfunction('strcmp', {rawstring, rawstring} -> int);
	ncmp = terralib.externfunction('strncmp', {rawstring, rawstring, intptr} -> int);
................................................................................
		return `[lib.mem.ptr(int8)] {ptr = nil, ct = 0}
	end
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.rpush = terra(self: &m.acc, str: lib.mem.ref(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 = {}
	for i,v in ipairs{...} do

Modified tpl.t from [3e776df34f] to [ba911a8ebc].

1
2
3
4
5
6
7
8
9
10
11
12
13
..
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
-- vim: ft=terra
-- string template generator:
-- returns a function that fills out a template
-- with the strings given

local util = dofile 'common.lua'
local m = {}
function m.mk(tplspec)
	local str
	if type(tplspec) == 'string'
		then str = tplspec tplspec = {}
		else str = tplspec.body
	end
................................................................................
	tplchar_o = string.gsub(tplchar_o,'%%','%%%%')
	tplchar = string.gsub(tplchar, '.', function(c)
		if magic[c] then return '%' .. c end
	end)
	local last = 1
	local fields = {}
	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
	segs[#segs+1] = string.sub(str,last)

	for i, s in ipairs(segs) do
		segs[i] = string.gsub(s, '\\'..tplchar, tplchar_o)
		constlen = constlen + string.len(segs[i])
	end










	local runningtally = symbol(intptr)
	local tallyup = {quote
		var [runningtally] = 1 + constlen
	end}
	local rec = terralib.types.newstruct(string.format('template<%s>',tplspec.id or ''))
	local symself = symbol(&rec)





|







 







>







>
>
>
>








>




>
>
>
>
>
>
>
>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
..
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
-- vim: ft=terra
-- string template generator:
-- returns a function that fills out a template
-- with the strings given

local util = lib.util
local m = {}
function m.mk(tplspec)
	local str
	if type(tplspec) == 'string'
		then str = tplspec tplspec = {}
		else str = tplspec.body
	end
................................................................................
	tplchar_o = string.gsub(tplchar_o,'%%','%%%%')
	tplchar = string.gsub(tplchar, '.', function(c)
		if magic[c] then return '%' .. c end
	end)
	local last = 1
	local fields = {}
	local segs = {}
	local docs = {}
	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
	str = str:gsub(tplchar .. '%?([-%w]+)', function(file)
		if not docs[file] then docs[file] = data.doc[file] end
		return string.format('<a href="#help-%s" class="help">?</a>', file)
	end)
	for start, key, stop in string.gmatch(str,'()'..tplchar..'(%w+)()') do
		if string.sub(str,start-1,start-1) ~= '\\' then
			segs[#segs+1] = string.sub(str,last,start-1)
			fields[#segs] = key
			last = stop
		end
	end
	segs[#segs+1] = string.sub(str,last)

	for i, s in ipairs(segs) do
		segs[i] = string.gsub(s, '\\'..tplchar, tplchar_o)
		constlen = constlen + string.len(segs[i])
	end

	for n,d in pairs(docs) do
		local html = string.format(
			'<div id="help-%s" class="modal"> <a href="#0">close</a> <div>%s</div></div>', n, d.text
		)
		segs[#segs] = segs[#segs] .. html
		constlen = constlen + #html
	end
	

	local runningtally = symbol(intptr)
	local tallyup = {quote
		var [runningtally] = 1 + constlen
	end}
	local rec = terralib.types.newstruct(string.format('template<%s>',tplspec.id or ''))
	local symself = symbol(&rec)

Modified view/compose.tpl from [09c6180294] to [bb642e2999].

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>



|
<


<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
1
2
3
4

5
6


































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

	<button type="submit">commit</button>
</form>