parsav  Check-in [c774e2c5a9]

Overview
Comment:wrote mimelib, continued iterating on litepub support; tweets can now be imported into honk
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: c774e2c5a9739cd9fbe1c939815b9a592d7acdcd43e8e612735515034c98fbb7
User & Date: lexi on 2021-01-28 00:51:21
Other Links: manifest | tags
Context
2021-01-28
02:44
add in a bunch of missing pqclears, because i am a *retard*, and wipe out a fuckton of memory leaks check-in: a4e71fdfda user: lexi tags: trunk
00:51
wrote mimelib, continued iterating on litepub support; tweets can now be imported into honk check-in: c774e2c5a9 user: lexi tags: trunk
2021-01-25
14:38
iterating check-in: 050ce7d4fc user: lexi tags: trunk
Changes

Modified api/lp/actor.t from [b336ed6430] to [c96b63a8f1].

9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
..
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
		"preferredUsername": %$handle,
		"name": %$nym,
		"summary": %$desc,
		"alsoKnownAs": ["https://%+domain/@%+handle"],
		"publicKey": {
			"id": "%lpid#ident-rsa",
			"owner": "%lpid",
			"publicKeyPem": %rsa
		},
		"icon": {
			"type": "Image",
			"url": "https://%+domain%+avi"
		},
		"capabilities": { "acceptsChatMessages": false },
		"discoverable": true,
................................................................................
		"outbox": "https://%+domain/api/lp/outbox/user/%uid",
		"followers": "https://%+domain/api/lp/rel/%uid/followers",
		"following": "https://%+domain/api/lp/rel/%uid/following"
	}]];
}

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

local terra 
api_lp_actor(co: &lib.srv.convo, actor: &lib.store.actor)
	var lpid = co:stra(64)
	lpid:lpush'https://':ppush(co.srv.cfg.domain):lpush'/user/':shpush(actor.id)
	var uid = co:stra(32) uid:shpush(actor.id) -- dumb hack bc lazy FIXME












	var body = tpl {
		domain = co.srv.cfg.domain;
		uid = uid:finalize();
		lpid = lpid:finalize();
		handle = cs(actor.handle);
		nym = cs(actor.nym);
		desc = cs(actor.bio);
		avi = cs(actor.avatar);
		rsa = '';
		locked = 'false';
	}

	co:json(body:poolstr(&co.srv.pool))

end
return api_lp_actor







|







 







|







>
>
>
>
>
>
>
>
>
>
>








|




>


9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
..
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
		"preferredUsername": %$handle,
		"name": %$nym,
		"summary": %$desc,
		"alsoKnownAs": ["https://%+domain/@%+handle"],
		"publicKey": {
			"id": "%lpid#ident-rsa",
			"owner": "%lpid",
			"publicKeyPem": %$rsa
		},
		"icon": {
			"type": "Image",
			"url": "https://%+domain%+avi"
		},
		"capabilities": { "acceptsChatMessages": false },
		"discoverable": true,
................................................................................
		"outbox": "https://%+domain/api/lp/outbox/user/%uid",
		"followers": "https://%+domain/api/lp/rel/%uid/followers",
		"following": "https://%+domain/api/lp/rel/%uid/following"
	}]];
}

local pstr = lib.str.t
terra cs(s: rawstring) return pstr {s, lib.trn(s == nil,0,lib.str.sz(s))} end

local terra 
api_lp_actor(co: &lib.srv.convo, actor: &lib.store.actor)
	var lpid = co:stra(64)
	lpid:lpush'https://':ppush(co.srv.cfg.domain):lpush'/user/':shpush(actor.id)
	var uid = co:stra(32) uid:shpush(actor.id) -- dumb hack bc lazy FIXME

	var upk = lib.crypt.loadpriv(actor.key)
	var pem: lib.crypt.pemfile

	if not upk.ok then
		lib.warn("could not load user's keypair; this is a sign of a bug, a corrupt database, or a problem with mbedtls")
	else defer upk.val:free()
		if not lib.crypt.pem(true, &upk.val, &pem[0]) then
			pem[0] = 0;
			lib.warn('could not export actor certificate as PEM file; there is a bug, the database is corrupt, or there is a problem in mbedtls')
		end
	end
	var body = tpl {
		domain = co.srv.cfg.domain;
		uid = uid:finalize();
		lpid = lpid:finalize();
		handle = cs(actor.handle);
		nym = cs(actor.nym);
		desc = cs(actor.bio);
		avi = cs(actor.avatar);
		rsa = cs(&pem[0]);
		locked = 'false';
	}

	co:json(body:poolstr(&co.srv.pool))

end
return api_lp_actor

Modified api/lp/tweet.t from [1ada03554b] to [cbbfbd7e42].

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
local obj = lib.tpl.mk [[{
	"\@context": "https://@+domain/s/litepub.jsonld",
	"type": "Note",
	"id": "https://@+domain/post/@^pid",
	"content": @$html,
	"source": @$raw,
	"attributedTo": "https://@+domain/user/@^uid",

	"published": "@pubtime"




	@extra
}]]

local wrap = lib.tpl.mk [[{
	"\@context": "https://@+domain/s/litepub.jsonld",
	"type": "@kind",
	"actor": "https://@+domain/user/@^uid",
	"published": "@pubtime",
	"id": "https://@+domain/api/lp/act/@^aid",

	"object": @obj
}]]

local terra 
lp_tweet(co: &lib.srv.convo, p: &lib.store.post, act_wrap: bool)
	var opdate = lib.conv.datetime(&co.srv.pool, p.posted)












































	var tweet = (obj {
		domain = co.srv.cfg.domain, uid = p.author, pid = p.id;
		html = lib.smackdown.html(&co.srv.pool, p.body, false);
		raw = p.body, pubtime = opdate, extra = '';

	}):poolstr(&co.srv.pool)

	if act_wrap then
		return (wrap {
			domain = co.srv.cfg.domain, obj = tweet;
			kind = lib.trn(p.rtdby == 0, 'Create', 'Announce');
			uid = lib.trn(p.rtdby == 0, p.author, p.rtdby);
			aid = lib.trn(p.rtdby == 0, p.id, p.rtact);
			pubtime = lib.trn(p.rtdby == 0, opdate,
				lib.conv.datetime(&co.srv.pool,p.rtdat));
		}):poolstr(&co.srv.pool)
	else
		return tweet
	end
end

return lp_tweet







>
|
>
>
>
>









>




|
|

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



|
>









|







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
local obj = lib.tpl.mk [[{
	"\@context": "https://@+domain/s/litepub.jsonld",
	"type": "Note",
	"id": "https://@+domain/post/@^pid",
	"content": @$html,
	"source": @$raw,
	"attributedTo": "https://@+domain/user/@^uid",
	"actor": "https://@+domain/user/@^uid",
	"published": "@pubtime",
	"sensitive": false,
	"directMessage": false,
	"to": ["https://www.w3.org/ns/activitystreams#Public"],
	"summary": @$subj
	@extra
}]]

local wrap = lib.tpl.mk [[{
	"\@context": "https://@+domain/s/litepub.jsonld",
	"type": "@kind",
	"actor": "https://@+domain/user/@^uid",
	"published": "@pubtime",
	"id": "https://@+domain/api/lp/act/@^aid",
	"to": ["https://www.w3.org/ns/activitystreams#Public"],
	"object": @obj
}]]

local terra 
lp_tweet(co: &lib.srv.convo, p: &lib.store.post, act_wrap: bool): pstr
	var opdate = lib.munge.datetime(&co.srv.pool, p.posted)

	var extra: lib.str.acc extra:pool(&co.srv.pool,256)
	
	if p.parent ~= 0 then
		extra:lpush ',"inReplyTo":"'
		var par = co.srv:post_fetch(p.parent)
		if not par then
			lib.warn('database integrity violation: broken parent reference')
		else defer par:free()
			if par().localpost then -- gen uri for parent
				extra:lpush'https://':qpush(co.srv.cfg.domain):lpush'/post/':shpush(p.parent)
			else extra:push(par().uri,0) end
		end
		extra:lpush'"'
	end

	extra:lpush ',"conversation":"'
	if p.convoheaduri ~= nil then
		extra:qpush(p.convoheaduri)
	else
		var cid: uint64 = 0
		if p.parent ~= 0 then
			var top = co.srv:thread_top_find(p.parent)
			var tp = co.srv:post_fetch(top)
			if not tp then
				lib.warn('database integrity violation: missing thread parent')
				cid = p.id
			else
				if tp().convoheaduri ~= nil then
					extra:push(tp().convoheaduri,0)
				elseif tp().localpost == false then
					extra:push(tp().uri,0)
				else cid = top end
			end
		else
			cid = p.id
		end
		if cid ~= 0 then
			extra:lpush'https://':qpush(co.srv.cfg.domain)
			     :lpush'/post/':shpush(cid):lpush'/tree'
		end
	end
	extra:lpush'"'

	var tweet = (obj {
		domain = co.srv.cfg.domain, uid = p.author, pid = p.id;
		html = lib.smackdown.html(&co.srv.pool, p.body, false);
		raw = p.body, pubtime = opdate, extra = extra:finalize();
		subj = lib.trn(p.subject ~= nil, pstr(p.subject), pstr'');
	}):poolstr(&co.srv.pool)

	if act_wrap then
		return (wrap {
			domain = co.srv.cfg.domain, obj = tweet;
			kind = lib.trn(p.rtdby == 0, 'Create', 'Announce');
			uid = lib.trn(p.rtdby == 0, p.author, p.rtdby);
			aid = lib.trn(p.rtdby == 0, p.id, p.rtact);
			pubtime = lib.trn(p.rtdby == 0, opdate,
				lib.munge.datetime(&co.srv.pool,p.rtdat));
		}):poolstr(&co.srv.pool)
	else
		return tweet
	end
end

return lp_tweet

Modified api/webfinger.t from [c64d390bdc] to [06153fd078].

10
11
12
13
14
15
16

17
18
19
20
21
22
23
			"type": "text/html", "href": @$pfp }
	]
}]]

local terra 
webfinger(co: &lib.srv.convo)
	var res = co:pgetv('resource')

	if (not res) or not res:startswith 'acct:' then goto err end
	
	var acct = res + 5
	var svp = lib.str.find(acct, '@')
	if svp:ref() then
		acct.ct = (svp.ptr - acct.ptr)
		svp:advance(1)







>







10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
			"type": "text/html", "href": @$pfp }
	]
}]]

local terra 
webfinger(co: &lib.srv.convo)
	var res = co:pgetv('resource')
	lib.dbg('got webfinger request for resource ', {res.ptr,res.ct})
	if (not res) or not res:startswith 'acct:' then goto err end
	
	var acct = res + 5
	var svp = lib.str.find(acct, '@')
	if svp:ref() then
		acct.ct = (svp.ptr - acct.ptr)
		svp:advance(1)

Modified backend/pgsql.t from [18ea7fb9d0] to [d6c61590d0].

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
...
502
503
504
505
506
507
508













509
510
511
512
513
514
515
....
1022
1023
1024
1025
1026
1027
1028

1029
1030
1031
1032
1033
1034
1035
....
1044
1045
1046
1047
1048
1049
1050

1051
1052
1053
1054
1055
1056
1057
1058




1059
1060
1061
1062
1063

1064
1065
1066
1067
1068
1069
1070
....
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100

1101
1102
1103
1104
1105
1106
1107
....
1123
1124
1125
1126
1127
1128
1129






1130
1131
1132
1133
1134
1135
1136

1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
....
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
....
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
....
2091
2092
2093
2094
2095
2096
2097










2098
2099
2100
2101
2102
2103
2104
			where id = $1::bigint
		]]
	};

	post_create = {
		params = {
			uint64, rawstring, rawstring, rawstring,
			uint64, uint64, rawstring
		}, sql = [[
			insert into parsav_posts (
				author, subject, acl, body,
				parent, posted, discovered,
				circles, mentions, convoheaduri
			) values (
				$1::bigint, case when $2::text = '' then null else $2::text end,
				$3::text, $4::text, 
				$5::bigint, $6::bigint, $6::bigint,
				array[]::bigint[], array[]::bigint[], $7::text
			) returning id
		]]; -- TODO array handling
	};

	post_destroy_prepare = {
		params = {uint64}, cmd = true, sql = [[
			update parsav_posts set
................................................................................
		params = {uint64}, sql = [[
			select (p.post).*
			from pg_temp.parsavpg_known_content as p
				where (p.post).parent = $1::bigint and (p.post).rtdby = 0
				order by (p.post).posted, (p.post).discovered asc
		]];
	};














	thread_latest_arrival_calc = {
		params = {uint64}, sql = [[
			with recursive posts(id) as (
				select id from parsav_posts where parent = $1::bigint
			union
				select p.id from parsav_posts as p
................................................................................
		if ct == 0 then
			lib.pq.PQclear(res)
			return pqr {0, nil}
		else
			return pqr {ct, res}
		end
	end

end

local terra row_to_artifact(res: &pqr, i: intptr): lib.mem.ptr(lib.store.artifact)
	var id = res:int(uint64,i,0)
	var idbuf: int8[lib.math.shorthand.maxlen]
	var idlen = lib.math.shorthand.gen(id, &idbuf[0])
	var desc = res:_string(i,2)
................................................................................
	m.ptr.rid = id
	return m
end

local terra row_to_post(r: &pqr, row: intptr): lib.mem.ptr(lib.store.post)
	var subj: rawstring, sblen: intptr
	var cvhu: rawstring, cvhlen: intptr

	if r:null(row,3)
		then subj = nil sblen = 0
		else subj = r:string(row,3) sblen = r:len(row,3)+1
	end
	if r:null(row,10)
		then cvhu = nil cvhlen = 0
		else cvhu = r:string(row,10) cvhlen = r:len(row,10)+1
	end




	var p = [ lib.str.encapsulate(lib.store.post, {
		subject = { `subj, `sblen };
		acl = {`r:string(row,4), `r:len(row,4)+1};
		body = {`r:string(row,5), `r:len(row,5)+1};
		convoheaduri = { `cvhu, `cvhlen }; --FIXME

	}) ]
	p.ptr.id = r:int(uint64,row,1)
	p.ptr.author = r:int(uint64,row,2)
	if r:null(row,6)
		then p.ptr.posted = 0
		else p.ptr.posted = r:int(uint64,row,6)
	end
................................................................................
		else p.ptr.edited = r:int(uint64,row,8)
	end
	p.ptr.parent = r:int(uint64,row,9)
	if r:null(row,11)
		then p.ptr.chgcount = 0
		else p.ptr.chgcount = r:int(uint32,row,11)
	end 
	p.ptr.accent = r:int(int16,row,12)
	p.ptr.rtdby = r:int(uint64,row,13)
	p.ptr.rtdat = r:int(uint64,row,14)
	p.ptr.rtact = r:int(uint64,row,15)
	p.ptr.likes = r:int(uint32,row,16)
	p.ptr.rts = r:int(uint32,row,17)
	p.ptr.isreply = r:bool(row,18)
	p.ptr.localpost = r:bool(row,0)

	return p
end
local terra row_to_actor(r: &pqr, row: intptr): lib.mem.ptr(lib.store.actor)
	var a: lib.mem.ptr(lib.store.actor)
	var av: rawstring, avlen: intptr
	var nym: rawstring, nymlen: intptr
	var bio: rawstring, biolen: intptr
	var epi: rawstring, epilen: intptr

	var origin: uint64 = 0
	var handle = r:_string(row, 2)
	if not r:null(row,3) then origin = r:int(uint64,row,3) end

	var avia = lib.str.acc {buf=nil}
	if origin == 0 then
		avia:compose('/avi/',handle)
................................................................................
		bio = r:string(row,4)
		biolen = r:len(row,4)+1
	end
	if r:null(row,9) then epilen = 0 epi = nil else
		epi = r:string(row,9)
		epilen = r:len(row,9)+1
	end






	a = [ lib.str.encapsulate(lib.store.actor, {
		nym = {`nym, `nymlen};
		bio = {`bio, `biolen};
		epithet = {`epi, `epilen};
		avatar = {`av,`avlen};
		handle = {`handle.ptr, `handle.ct + 1};
		xid = {`r:string(row, 11); `r:len(row,11) + 1};

	}) ]
	a.ptr.id = r:int(uint64, row, 0);
	a.ptr.rights = lib.store.rights_default();
	a.ptr.rights.rank = r:int(uint16, row, 6);
	a.ptr.rights.quota = r:int(uint32, row, 7);
	a.ptr.rights.invites = r:int(uint32, row, 12);
	a.ptr.knownsince = r:int(int64,row, 10);
	a.ptr.avatarid = r:int(uint64,row, 13);
	if r:null(row,8) then
		a.ptr.key.ct = 0 a.ptr.key.ptr = nil
	else
		a.ptr.key = r:bin(row,8)
	end
	a.ptr.origin = origin
	if avia.buf ~= nil then avia:free() end
	return a
end

local privmap = lib.store.powmap

................................................................................
				blacklist = res:bool(i, 3);
				pubkey = res:bin(i, 4);
			}
		end]
		if rsakeys.sz > 0 then defer rsakeys:free()
			for i=0, rsakeys.sz do var props = toprops(&rsakeys, i)
				lib.dbg('loading next RSA pubkey')
				var pub = lib.crypt.loadpub(props.pubkey.ptr, props.pubkey.ct)
				if pub.ok then defer pub.val:free()
					lib.dbg('checking pubkey against response')
					var vfy, secl = lib.crypt.verify(&pub.val, token.ptr, token.ct, sig.ptr, sig.ct)
					if vfy then
						lib.dbg('signature verified')
						if props.blacklist then lib.dbg('key blacklisted!') goto fail end
						var dupname = lib.str.dup(props.name.ptr)
................................................................................

	post_create = [terra(
		src: &lib.store.source,
		post: &lib.store.post
	): uint64
		var r = queries.post_create.exec(src,
			post.author,post.subject,post.acl,post.body,
			post.parent,post.posted,post.convoheaduri
		) 
		if r.sz == 0 then return 0 end
		defer r:free()
		var id = r:int(uint64,0,0)
		post.source = src
		return id
	end];
................................................................................
		if n.kind == lib.store.noticetype.react then
			var react = r:_string(0,5)
			lib.str.ncpy(n.reaction, react.ptr, lib.math.smallest(react.ct,[(`n.reaction).tree.type.N]))
		end

		return n
	end];











	thread_latest_arrival_calc = [terra(
		src: &lib.store.source,
		post: uint64
	): lib.store.timepoint
		var r = queries.thread_latest_arrival_calc.exec(src,post)
		if r.sz == 0 or r:null(0,0) then return 0 end







|




|




|







 







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







 







>







 







>








>
>
>
>





>







 







|
|
|
|
|
|
|










>







 







>
>
>
>
>
>







>








<
<
<
<
<







 







|







 







|







 







>
>
>
>
>
>
>
>
>
>







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
...
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
....
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
....
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
....
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
....
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172





1173
1174
1175
1176
1177
1178
1179
....
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
....
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
....
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
			where id = $1::bigint
		]]
	};

	post_create = {
		params = {
			uint64, rawstring, rawstring, rawstring,
			uint64, uint64, rawstring, rawstring
		}, sql = [[
			insert into parsav_posts (
				author, subject, acl, body,
				parent, posted, discovered,
				circles, mentions, convoheaduri, uri
			) values (
				$1::bigint, case when $2::text = '' then null else $2::text end,
				$3::text, $4::text, 
				$5::bigint, $6::bigint, $6::bigint,
				array[]::bigint[], array[]::bigint[], $7::text, $8::text
			) returning id
		]]; -- TODO array handling
	};

	post_destroy_prepare = {
		params = {uint64}, cmd = true, sql = [[
			update parsav_posts set
................................................................................
		params = {uint64}, sql = [[
			select (p.post).*
			from pg_temp.parsavpg_known_content as p
				where (p.post).parent = $1::bigint and (p.post).rtdby = 0
				order by (p.post).posted, (p.post).discovered asc
		]];
	};

	thread_top_find = {
		params = {uint64}, sql = [[
			with recursive tree(gen,id,par) as (
				select 0, id, parent from parsav_posts where id = $1::bigint
			union
				select tree.gen + 1, p.id, p.parent from tree
					inner join parsav_posts as p on p.id = tree.par
			)

			select id from tree order by gen desc limit 1
		]];
	};

	thread_latest_arrival_calc = {
		params = {uint64}, sql = [[
			with recursive posts(id) as (
				select id from parsav_posts where parent = $1::bigint
			union
				select p.id from parsav_posts as p
................................................................................
		if ct == 0 then
			lib.pq.PQclear(res)
			return pqr {0, nil}
		else
			return pqr {ct, res}
		end
	end
	q.exec.name = 'pgsql.' .. k .. '.exec'
end

local terra row_to_artifact(res: &pqr, i: intptr): lib.mem.ptr(lib.store.artifact)
	var id = res:int(uint64,i,0)
	var idbuf: int8[lib.math.shorthand.maxlen]
	var idlen = lib.math.shorthand.gen(id, &idbuf[0])
	var desc = res:_string(i,2)
................................................................................
	m.ptr.rid = id
	return m
end

local terra row_to_post(r: &pqr, row: intptr): lib.mem.ptr(lib.store.post)
	var subj: rawstring, sblen: intptr
	var cvhu: rawstring, cvhlen: intptr
	var uri:  rawstring, urilen: intptr
	if r:null(row,3)
		then subj = nil sblen = 0
		else subj = r:string(row,3) sblen = r:len(row,3)+1
	end
	if r:null(row,10)
		then cvhu = nil cvhlen = 0
		else cvhu = r:string(row,10) cvhlen = r:len(row,10)+1
	end
	if r:null(row,12)
		then uri = nil urilen = 0
		else uri = r:string(row,12) urilen = r:len(row,12)+1
	end
	var p = [ lib.str.encapsulate(lib.store.post, {
		subject = { `subj, `sblen };
		acl = {`r:string(row,4), `r:len(row,4)+1};
		body = {`r:string(row,5), `r:len(row,5)+1};
		convoheaduri = { `cvhu, `cvhlen }; --FIXME
		uri = { `uri, `urilen };
	}) ]
	p.ptr.id = r:int(uint64,row,1)
	p.ptr.author = r:int(uint64,row,2)
	if r:null(row,6)
		then p.ptr.posted = 0
		else p.ptr.posted = r:int(uint64,row,6)
	end
................................................................................
		else p.ptr.edited = r:int(uint64,row,8)
	end
	p.ptr.parent = r:int(uint64,row,9)
	if r:null(row,11)
		then p.ptr.chgcount = 0
		else p.ptr.chgcount = r:int(uint32,row,11)
	end 
	p.ptr.accent = r:int(int16,row,13)
	p.ptr.rtdby = r:int(uint64,row,14)
	p.ptr.rtdat = r:int(uint64,row,15)
	p.ptr.rtact = r:int(uint64,row,16)
	p.ptr.likes = r:int(uint32,row,17)
	p.ptr.rts   = r:int(uint32,row,18)
	p.ptr.isreply = r:bool(row,19)
	p.ptr.localpost = r:bool(row,0)

	return p
end
local terra row_to_actor(r: &pqr, row: intptr): lib.mem.ptr(lib.store.actor)
	var a: lib.mem.ptr(lib.store.actor)
	var av: rawstring, avlen: intptr
	var nym: rawstring, nymlen: intptr
	var bio: rawstring, biolen: intptr
	var epi: rawstring, epilen: intptr
	var key: &uint8, keylen: intptr
	var origin: uint64 = 0
	var handle = r:_string(row, 2)
	if not r:null(row,3) then origin = r:int(uint64,row,3) end

	var avia = lib.str.acc {buf=nil}
	if origin == 0 then
		avia:compose('/avi/',handle)
................................................................................
		bio = r:string(row,4)
		biolen = r:len(row,4)+1
	end
	if r:null(row,9) then epilen = 0 epi = nil else
		epi = r:string(row,9)
		epilen = r:len(row,9)+1
	end
	if r:null(row,8) then
		keylen = 0 key = nil
	else
		var k = r:bin(row,8)
		keylen = k.ct key = k.ptr
	end
	a = [ lib.str.encapsulate(lib.store.actor, {
		nym = {`nym, `nymlen};
		bio = {`bio, `biolen};
		epithet = {`epi, `epilen};
		avatar = {`av,`avlen};
		handle = {`handle.ptr, `handle.ct + 1};
		xid = {`r:string(row, 11); `r:len(row,11) + 1};
		key = {`key,`keylen};
	}) ]
	a.ptr.id = r:int(uint64, row, 0);
	a.ptr.rights = lib.store.rights_default();
	a.ptr.rights.rank = r:int(uint16, row, 6);
	a.ptr.rights.quota = r:int(uint32, row, 7);
	a.ptr.rights.invites = r:int(uint32, row, 12);
	a.ptr.knownsince = r:int(int64,row, 10);
	a.ptr.avatarid = r:int(uint64,row, 13);





	a.ptr.origin = origin
	if avia.buf ~= nil then avia:free() end
	return a
end

local privmap = lib.store.powmap

................................................................................
				blacklist = res:bool(i, 3);
				pubkey = res:bin(i, 4);
			}
		end]
		if rsakeys.sz > 0 then defer rsakeys:free()
			for i=0, rsakeys.sz do var props = toprops(&rsakeys, i)
				lib.dbg('loading next RSA pubkey')
				var pub = lib.crypt.loadpub(props.pubkey)
				if pub.ok then defer pub.val:free()
					lib.dbg('checking pubkey against response')
					var vfy, secl = lib.crypt.verify(&pub.val, token.ptr, token.ct, sig.ptr, sig.ct)
					if vfy then
						lib.dbg('signature verified')
						if props.blacklist then lib.dbg('key blacklisted!') goto fail end
						var dupname = lib.str.dup(props.name.ptr)
................................................................................

	post_create = [terra(
		src: &lib.store.source,
		post: &lib.store.post
	): uint64
		var r = queries.post_create.exec(src,
			post.author,post.subject,post.acl,post.body,
			post.parent,post.posted,post.convoheaduri,post.uri
		) 
		if r.sz == 0 then return 0 end
		defer r:free()
		var id = r:int(uint64,0,0)
		post.source = src
		return id
	end];
................................................................................
		if n.kind == lib.store.noticetype.react then
			var react = r:_string(0,5)
			lib.str.ncpy(n.reaction, react.ptr, lib.math.smallest(react.ct,[(`n.reaction).tree.type.N]))
		end

		return n
	end];

	thread_top_find = [terra(
		src: &lib.store.source,
		post: uint64
	): uint64
		var r = queries.thread_top_find.exec(src,post)
		if r.sz == 0 then return 0 end
		defer r:free()
		return r:int(uint64,0,0)
	end];

	thread_latest_arrival_calc = [terra(
		src: &lib.store.source,
		post: uint64
	): lib.store.timepoint
		var r = queries.thread_latest_arrival_calc.exec(src,post)
		if r.sz == 0 or r:null(0,0) then return 0 end

Modified backend/schema/pgsql-views.sql from [b916bb0a63] to [a4ebd0c2e8].

106
107
108
109
110
111
112

113
114
115
116
117
118
119
...
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
	body		text,
	posted		bigint,
	discovered	bigint,
	edited		bigint,
	parent		bigint,
	convoheaduri text,
	chgcount	integer,

-- ephemeral
	accent		smallint,
	rtdby		bigint, -- note that these must be 0 if the record
	rtdat		bigint, -- in question does not represent an RT!
	rtid		bigint, -- (this one too)
	n_likes		integer,
	n_rts		integer,
................................................................................
pg_temp.parsavpg_translate_post(parsav_posts,bigint,bigint,bigint)
returns pg_temp.parsavpg_intern_post as $$
	select a.origin is null,
		($1).id,     ($1).author,
		($1).subject,($1).acl,         ($1).body,
		($1).posted, ($1).discovered,  ($1).edited,
		($1).parent, ($1).convoheaduri,($1).chgcount,
		coalesce(c.value, -1)::smallint,
		$2 as rtdby, $3 as rtdat, $4 as rtid,
		re.likes, re.rts,
		($1).parent in (select id from parsav_posts)
	from parsav_actors as a 
		left join parsav_actor_conf_ints as c
		          on c.key = 'ui-accent' and
		             c.uid = a.id







>







 







|







106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
...
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
	body		text,
	posted		bigint,
	discovered	bigint,
	edited		bigint,
	parent		bigint,
	convoheaduri text,
	chgcount	integer,
	uri         text,
-- ephemeral
	accent		smallint,
	rtdby		bigint, -- note that these must be 0 if the record
	rtdat		bigint, -- in question does not represent an RT!
	rtid		bigint, -- (this one too)
	n_likes		integer,
	n_rts		integer,
................................................................................
pg_temp.parsavpg_translate_post(parsav_posts,bigint,bigint,bigint)
returns pg_temp.parsavpg_intern_post as $$
	select a.origin is null,
		($1).id,     ($1).author,
		($1).subject,($1).acl,         ($1).body,
		($1).posted, ($1).discovered,  ($1).edited,
		($1).parent, ($1).convoheaduri,($1).chgcount,
		($1).uri, coalesce(c.value, -1)::smallint,
		$2 as rtdby, $3 as rtdat, $4 as rtid,
		re.likes, re.rts,
		($1).parent in (select id from parsav_posts)
	from parsav_actors as a 
		left join parsav_actor_conf_ints as c
		          on c.key = 'ui-accent' and
		             c.uid = a.id

Modified backend/schema/pgsql.sql from [4c18dab250] to [abc8356ef1].

59
60
61
62
63
64
65

66
67
68
69
70
71
72
);
create index on parsav_rights (actor);
comment on table parsav_rights is
'a backward-compatible list of every non-default privilege or deprivilege granted to a local user';

create table parsav_posts (
	id         <def:uniq>,

	author     bigint references parsav_actors(id) on delete cascade,
	subject    text,
	acl        text not null default 'all', -- just store the script raw 🤷
	body       text,
	posted     bigint not null,
	discovered bigint not null,
	chgcount   integer not null default 0,







>







59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
);
create index on parsav_rights (actor);
comment on table parsav_rights is
'a backward-compatible list of every non-default privilege or deprivilege granted to a local user';

create table parsav_posts (
	id         <def:uniq>,
	uri        text, -- null if local
	author     bigint references parsav_actors(id) on delete cascade,
	subject    text,
	acl        text not null default 'all', -- just store the script raw 🤷
	body       text,
	posted     bigint not null,
	discovered bigint not null,
	chgcount   integer not null default 0,

Added convo.t version [ff68c1adfe].













































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
-- vim: ft=terra
local srv = ...
local pstring = lib.str.t

local struct convo {
	srv: &srv
	con: &lib.net.mg_connection
	msg: &lib.net.mg_http_message
	aid: uint64 -- 0 if logged out
	aid_issue: lib.store.timepoint
	who: &lib.store.actor -- who we're logged in as, if aid ~= 0
	peer: lib.store.inet
	reqtype: lib.http.mime.t -- negotiated content type
	method: lib.http.method.t
	live_last: lib.store.timepoint
	uploads: lib.mem.vec(lib.http.upload)
	body: pstring
-- cache
	ui_hue: uint16
	navbar: pstring
	actorcache: lib.mem.cache(lib.mem.ptr(lib.store.actor),32) -- naive cache to avoid unnecessary queries
-- private
	varbuf: pstring
	vbofs: &int8
}

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

local usrdefs = {
	str = {
		['acl-follow'    ] = {cfgfld = 'usrdef_pol_follow', fallback = 'local'};
		['acl-follow-req'] = {cfgfld = 'usrdef_pol_follow_req', fallback = 'all'};
	};
}

terra convo:matchmime(mime: lib.http.mime.t): bool
	return self.reqtype == [lib.http.mime.none]
		or self.reqtype == mime
end

terra convo:usercfg_str(uid: uint64, setting: pstring): pstring
	var set = self.srv:actor_conf_str_get(&self.srv.pool, uid, setting)
	if not set then
		[(function()
			local q = quote return pstring.null() end
			for key, dfl in pairs(usrdefs.str) do
				local rv
				if dfl.cfgfld then
					rv = quote
						var cf = self.srv.cfg.[dfl.cfgfld]
					in terralib.select(not cf, pstring([dfl.fallback]), cf) end
				elseif dfl.lit then rv = dfl.lit end
				q = quote
					if setting:cmp([key]) then return [rv] else [q] end
				end
			end
			return q
		end)()]
	else return set end
end

terra convo:uid2actor_live(uid: uint64)
	var actor = self.srv:actor_fetch_uid(uid)
	if actor:ref() then
		if self.aid ~= 0 and self.who.id ~= uid then
			actor(0).relationship = self.srv:actor_rel_calc(self.who.id, uid)
		else -- defensive branch
			actor(0).relationship = lib.store.relationship {
				agent = 0, patient = uid;
				rel   = [lib.store.relation.null],
				recip = [lib.store.relation.null],
			}
		end
	end
	return actor
end

terra convo:uid2actor(uid: uint64)
	var actor: &lib.store.actor = nil
	for j = 0, self.actorcache.top do
		if uid == self.actorcache(j).ptr.id then
			actor = self.actorcache(j).ptr
			break
		end
	end
	if actor == nil then
		actor = self.actorcache:insert(self:uid2actor_live(uid)).ptr
	end
	return actor
end

terra convo:rawpage(code: uint16, pg: convo.page, hdrs: lib.mem.ptr(lib.http.header))
	var doc = data.view.docskel {
		instance = self.srv.cfg.instance;
		title = pg.title;
		body = pg.body;
		class = pg.class;
		navlinks = self.navbar;
		attr = '';
	}
	var attrbuf: int8[32]
	if self.aid ~= 0 and self.ui_hue ~= 323 then
		var hdecbuf: int8[21]
		var hdec = lib.math.decstr(self.ui_hue, &hdecbuf[20])
		lib.str.cpy(&attrbuf[0], ' style="--hue:')
		lib.str.cpy(&attrbuf[14], hdec)
		var len = &hdecbuf[20] - hdec 
		lib.str.cpy(&attrbuf[14] + len, '"')
		doc.attr = &attrbuf[0]
	end

	if self.method == [lib.http.method.head]
		then doc:head(self.con,code,hdrs)
		else doc:send(self.con,code,hdrs)
	end
end

terra convo:statpage(code: uint16, pg: convo.page)
	var hdrs = array(
		lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' },
		lib.http.header { key = 'Cache-Control', value = 'no-store' }
	)
	self:rawpage(code,pg, [lib.mem.ptr(lib.http.header)] {
		ptr = &hdrs[0];
		ct = [hdrs.type.N] - lib.trn(pg.cache,1,0);
	})
end

terra convo:livepage(pg: convo.page, lastup: lib.store.timepoint)
	var nbuf: int8[21]
	var hdrs = array(
		lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' },
		lib.http.header { key = 'Cache-Control', value = 'no-store' },
		lib.http.header {
			key = 'X-Live-Newest-Artifact';
			value = lib.math.decstr(lastup, &nbuf[20]);
		},
		lib.http.header { key = 'Content-Length', value = '0' }
	)
	if self.live_last ~= 0 and self.live_last == lastup then
		lib.net.mg_printf(self.con, 'HTTP/1.1 %s', lib.http.codestr(200))
		for i = 0, [hdrs.type.N] do
			lib.net.mg_printf(self.con, '%s: %s\r\n', hdrs[i].key, hdrs[i].value)
		end
		lib.net.mg_printf(self.con, '\r\n')
	else
		self:rawpage(200, pg, [lib.mem.ptr(lib.http.header)] {
			ptr = &hdrs[0], ct = 3
		})
	end
end

terra convo:stdpage(pg: convo.page) self:statpage(200, pg) end

terra convo:bytestream_trusted(lockdown: bool, mime: pstring, data: lib.mem.ptr(uint8))
	var lockhdr = "Content-Security-Policy: sandbox; default-src 'none'; form-action 'none'; navigate-to 'none';\r\n"
	if not lockdown then lockhdr = "" end
	lib.net.mg_printf(self.con, "HTTP/1.1 200 OK\r\nContent-Type: %.*s\r\nContent-Length: %llu\r\n%sX-Content-Options: nosniff\r\n\r\n", mime.ct, mime.ptr, data.ct + 2, lockhdr)
	lib.net.mg_send(self.con, data.ptr, data.ct)
	lib.net.mg_send(self.con, '\r\n', 2)
end

terra convo:json(data: pstring)
	self:bytestream_trusted(false, 'application/activity+json; charset=utf-8', data:blob())
end

terra convo:bytestream(mime: pstring, data: lib.mem.ptr(uint8))
	var ty = lib.mime.lookup(mime)
	if ty == nil then
		lib.dbg("mime type ", {mime.ptr,mime.ct}, ' not in database!')
		mime = 'application/x-octet-stream'
	else
		if not ty.safe then
			lib.dbg("mime type ", {mime.ptr,mime.ct}, ' not safe!')
			if ty.binary then
				mime = 'application/x-octet-stream'
			else
				mime = 'text/plain'
			end
		end
	end
	self:bytestream_trusted(true, mime, data)
end

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

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

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

terra convo:reroute(dest: rawstring) self:reroute_cookie(dest,nil) end

terra convo:installkey(dest: rawstring, aid: uint64)
	var sesskey: int8[lib.session.maxlen + #lib.session.cookiename + #"=; Path=/" + 1]
	do var p = &sesskey[0]
		p = lib.str.ncpy(p, [lib.session.cookiename .. '='], [#lib.session.cookiename + 1])
		p = p + lib.session.cookie_gen(self.srv.cfg.secret, aid, lib.osclock.time(nil), p)
		lib.dbg('sending cookie ',{&sesskey[0],15})
		p = lib.str.ncpy(p, '; Path=/', 9)
	end
	self:reroute_cookie(dest, &sesskey[0])
end
 
terra convo:stra(sz: intptr) -- convenience function
	var s: lib.str.acc
	s:pool(&self.srv.pool,sz)
	return s
end

convo.methods.qstr = macro(function(self, ...) -- convenience string builder
	local exp = {...}
	return `lib.str.acc{}:pcompose(&self.srv.pool, [exp]):finalize()
end)

terra convo:complain(code: uint16, title: rawstring, msg: rawstring)
	if msg == nil then msg = "i'm sorry, dave. i can't let you do that" end

	if self:matchmime(lib.http.mime.html) then
		var body = [convo.page] {
			title = self:qstr('error :: ', title);
			body = self:qstr('<div class="message"><img class="icon" src="/s/warn.svg"><h1>',title,'</h1><p>',msg,'</p></div>');
			class = 'error';
			cache = false;
		}

		self:statpage(code, body)
	else
		var pg = lib.http.page { respcode = code, body = pstring.null() }
		var ctt = lib.http.mime.none
		if self:matchmime(lib.http.mime.json) then ctt = lib.http.mime.json
			pg.body = ([lib.tpl.mk'{"_parsav_error":@$ekind, "_parsav_error_desc":@$edesc}']
				{ekind = title, edesc = msg}):poolstr(&self.srv.pool)
		elseif self:matchmime(lib.http.mime.text) then ctt = lib.http.mime.text
			pg.body = self:qstr('error: ',title,'\n',msg)
		elseif self:matchmime(lib.http.mime.mkdown) then ctt = lib.http.mime.mkdown
			pg.body = self:qstr('# error :: ',title,'\n\n',msg)
		elseif self:matchmime(lib.http.mime.ansi) then ctt = lib.http.mime.ansi
			pg.body = self:qstr('\27[1;31merror :: ',title,'\27[m\n',msg)
		end
		var cthdr = lib.http.header { 'Content-Type', 'text/plain' }
		if ctt == lib.http.mime.none then
			pg.headers.ct = 0
		else
			pg.headers = lib.typeof(pg.headers) { &cthdr, 1 }
			switch ctt do
				escape
					for key,ty in ipairs(lib.mime.types) do
						if key ~= 'none' and lib.http.mime[key] ~= nil then
							emit quote case [ctt.type](lib.http.mime.[key]) then cthdr.value = [ty.id[1]] end end
						end
					end
				end
			end
		end
		pg:send(self.con)
	end
end

terra convo:fail(code: uint16)
	switch code do
		escape
			local stderrors = {
				{400, 'bad request', "the action you have attempted on this resource is not meaningful"};
				{401, 'unauthorized', "this resource is not available at your clearance level"};
				{403, 'forbidden', "we can neither confirm nor deny the existence of this resource"};
				{404, 'resource not found', "that resource is not extant on or known to this server"};
				{405, 'method not allowed', "the method you have attempted on this resource is not meaningful"};
				{406, 'not acceptable', "none of the suggested content types are a viable representation of this resource"};
				{500, 'internal server error', "parsav did a fucksy wucksy"};
			}

			for i,v in ipairs(stderrors) do
				emit quote case uint16([v[1]]) then
					self:complain([v])
				end end
			end
		end
		else self:complain(500,'unknown error','an unrecognized error was thrown. this is a bug')
	end
end

terra convo:confirm(title: pstring, msg: pstring, cancel: pstring)
	var conf = data.view.confirm {
		title = title;
		query = msg;
		cancel = cancel;
	}
	var ti: lib.str.acc ti:pcompose(&self.srv.pool,'confirm :: ', title)
	var body = conf:poolstr(&self.srv.pool) -- defer body:free()
	var cf = [convo.page] {
		title = ti:finalize();
		class = 'query';
		body = body; cache = false;
	}
	self:stdpage(cf)
	--cf.title:free()
end

convo.methods.assertpow = macro(function(self, pow)
	return quote
		var ok = true
		if self.aid == 0 or self.who.rights.powers.[pow:asvalue()]() == false then
			ok = false
			self:complain(403,'insufficient privileges',['you lack the <strong>'..pow:asvalue()..'</strong> power and cannot perform this action'])
		end
	in ok end
end)

local pstr2mg, mg2pstr
do -- aaaaaaaaaaaaaaaaaaaaaaaa
	mgstr = lib.util.find(lib.net.mg_http_message.entries, function(v)
		if v.field == 'body' or v[1] == 'body' then return v.type end
	end)
	terra pstr2mg(p: pstring): mgstr
		return mgstr { ptr = p.ptr, len = p.ct }
	end
	terra mg2pstr(m: mgstr): pstring
		return pstring { ptr = m.ptr, ct = m.len }
	end
end

-- CALL ONLY ONCE PER VAR
terra convo:postv_next(name: pstring, start: &pstring)
	if self.varbuf.ptr == nil then
		self.varbuf = self.srv.pool:alloc(int8, self.msg.body.len + self.msg.query.len)
		self.vbofs = self.varbuf.ptr
	end
	var conv = pstr2mg(@start)
	var o = lib.net.mg_http_get_var(
		&conv,
		name.ptr, self.vbofs,
		self.varbuf.ct - (self.vbofs - self.varbuf.ptr)
	)
	if o > 0 then
		start:advance(name.ct + o + 2)
		var r = self.vbofs
		self.vbofs = self.vbofs + o + 1
		@(self.vbofs - 1) = 0
		var norm = lib.str.normalize([lib.mem.ptr(int8)]{ptr = r, ct = o})
		return norm.ptr, norm.ct
	else return nil, 0 end
end
terra convo:postv(name: pstring)
	var start = mg2pstr(self.msg.body)
	return self:postv_next(name, &start)
end
terra convo:ppostv(name: pstring)
	var s,l = self:postv(name)
	return pstring { ptr = s, ct = l }
end
do
	local struct postiter { co: &convo where: pstring name: pstring }
	terra convo:eachpostv(name: pstring)
		return postiter { co = self, where = mg2pstr(self.msg.body), name = name } 
	end
	postiter.metamethods.__for = function(self, body)
		return quote
			while true do
				var str, len = self.co:postv_next(self.name, &self.where)
				if str == nil then break end
				[ body(`pstring {str, len}) ]
			end
		end
	end
end

terra convo:getv(name: rawstring)
	if self.varbuf.ptr == nil then
		self.varbuf = self.srv.pool:alloc(int8, self.msg.query.len + self.msg.body.len)
		self.vbofs = self.varbuf.ptr
	end
	var o = lib.net.mg_http_get_var(&self.msg.query, name, self.vbofs, self.varbuf.ct - (self.vbofs - self.varbuf.ptr))
	if o > 0 then
		var r = self.vbofs
		self.vbofs = self.vbofs + o + 1
		@(self.vbofs - 1) = 0
		var norm = lib.str.normalize([lib.mem.ptr(int8)]{ptr = r, ct = o})
		return norm.ptr, norm.ct
	else return nil, 0 end
end
terra convo:pgetv(name: rawstring)
	var s,l = self:getv(name)
	return pstring { ptr = s, ct = l }
end

return convo

Modified crypt.t from [a12c25b6dd] to [530b761d29].

14
15
16
17
18
19
20

21
22
23
24
25
26
27
28
..
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
...
112
113
114
115
116
117
118

119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
const.maxdersz = const.maxpemsz -- FIXME this is a safe value but obvs not the correct one

local ctx = lib.pk.mbedtls_pk_context
terra ctx:free() lib.pk.mbedtls_pk_free(self) end

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;
................................................................................
		v = v % (to - from) + from -- only works with unsigned!!
	in v end
end)

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

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

local binblob = lib.mem.ptr(uint8)
terra m.der(pub: bool, key: &ctx, buf: &uint8): binblob
	var ofs: ptrdiff
	if pub then
................................................................................
	lib.pk.mbedtls_pk_setup(&pk, lib.pk.mbedtls_pk_info_from_type(lib.pk.MBEDTLS_PK_RSA))
	var rsa = [&lib.rsa.mbedtls_rsa_context](pk.pk_ctx)
	lib.rsa.mbedtls_rsa_gen_key(rsa, callbacks.randomize, nil, const.keybits, 65537)

	return pk
end


terra m.loadpriv(buf: &uint8, len: intptr): lib.stat(ctx)
	lib.dbg('parsing saved private key')

	var pk: ctx
	lib.pk.mbedtls_pk_init(&pk)
	var rt = lib.pk.mbedtls_pk_parse_key(&pk, buf, len + 1, nil, 0)
	if rt == 0 then
		return [lib.stat(ctx)] { ok = true, val = pk }
	else
		lib.pk.mbedtls_pk_free(&pk)
		return [lib.stat(ctx)] { ok = false }
	end
end

terra m.loadpub(buf: &uint8, len: intptr): lib.stat(ctx)
	lib.dbg('parsing saved key')

	var pk: ctx
	lib.pk.mbedtls_pk_init(&pk)
	var rt = lib.pk.mbedtls_pk_parse_public_key(&pk, buf, len)
	if rt == 0 then
		return [lib.stat(ctx)] { ok = true, val = pk }
	else
		lib.pk.mbedtls_pk_free(&pk)
		return [lib.stat(ctx)] { ok = false, error = rt }
	end
end







>
|







 







|

|

|







 







>
|




|




|



|




|







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
..
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
...
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
const.maxdersz = const.maxpemsz -- FIXME this is a safe value but obvs not the correct one

local ctx = lib.pk.mbedtls_pk_context
terra ctx:free() lib.pk.mbedtls_pk_free(self) end

local struct hashalg { id: uint8 bytes: intptr }
local m = {
	pemfile = int8[const.maxpemsz];
	derfile = uint8[const.maxdersz];
	const = const;
	algsz = {
		sha1 =   160/8;
		sha256 = 256/8;
		sha512 = 512/8;
		sha384 = 384/8;
		sha224 = 224/8;
................................................................................
		v = v % (to - from) + from -- only works with unsigned!!
	in v end
end)

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

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

local binblob = lib.mem.ptr(uint8)
terra m.der(pub: bool, key: &ctx, buf: &uint8): binblob
	var ofs: ptrdiff
	if pub then
................................................................................
	lib.pk.mbedtls_pk_setup(&pk, lib.pk.mbedtls_pk_info_from_type(lib.pk.MBEDTLS_PK_RSA))
	var rsa = [&lib.rsa.mbedtls_rsa_context](pk.pk_ctx)
	lib.rsa.mbedtls_rsa_gen_key(rsa, callbacks.randomize, nil, const.keybits, 65537)

	return pk
end

local binblob = lib.mem.ptr(uint8)
terra m.loadpriv(buf: binblob): lib.stat(ctx)
	lib.dbg('parsing saved private key')

	var pk: ctx
	lib.pk.mbedtls_pk_init(&pk)
	var rt = lib.pk.mbedtls_pk_parse_key(&pk, buf.ptr, buf.ct, nil, 0)
	if rt == 0 then
		return [lib.stat(ctx)] { ok = true, val = pk }
	else
		lib.pk.mbedtls_pk_free(&pk)
		return [lib.stat(ctx)] { ok = false, error = rt }
	end
end

terra m.loadpub(buf: binblob): lib.stat(ctx)
	lib.dbg('parsing saved key')

	var pk: ctx
	lib.pk.mbedtls_pk_init(&pk)
	var rt = lib.pk.mbedtls_pk_parse_public_key(&pk, buf.ptr, buf.ct)
	if rt == 0 then
		return [lib.stat(ctx)] { ok = true, val = pk }
	else
		lib.pk.mbedtls_pk_free(&pk)
		return [lib.stat(ctx)] { ok = false, error = rt }
	end
end

Modified mgtool.t from [63607cdb2b] to [aa223ca2fb].

421
422
423
424
425
426
427

428
429
430
431
432
433
434
...
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
...
524
525
526
527
528
529
530


















531
532
533
534
535
536
537
					[ lib.emit(false, 1, 'usage: ', `argv[0], ' actor ', umode.type.helptxt.flags, ' <xid> <cmd> [<args>…]', umode.type.helptxt.opts, cmdhelp {
						{ 'actor <xid> rank <value>', 'set an actor\'s rank to <value> (remote actors cannot exercise rank-related powers, but benefit from rank immunities)' };
						{ 'actor <xid> degrade', 'alias for `actor <xid> rank 0`' };
						{ 'actor <xid> bestow <epithet>', 'bestow an epithet upon an actor' };
						{ 'actor <xid> instantiate', 'instantiate a remote actor, retrieving their profile and posts even if no one follows them' };
						{ 'actor <xid> proscribe', 'globally ban an actor from interacting with your server' };
						{ 'actor <xid> rehabilitate', 'lift a proscription on an actor' };

						{ 'actor <xid> purge-all <confirm-str>', 'remove all traces of a user from the database (except local user credentials -- use \27[1mauth all purge\27[m to prevent a user from accessing the instance)' };
					}) ]
					return 1
				end
				if umode.arglist.ct >= 2 then
					var degrade = lib.str.cmp(umode.arglist(1),'degrade') == 0
					var xid = umode.arglist(0)
................................................................................
							lib.warn('completely purging actor ', usr.ptr.xid, ' and all related content from database')
							dlg:actor_purge_uid(usr.ptr.id)
							lib.report('actor purged')
						else goto cmderr end
					else goto cmderr end
				else goto cmderr end
			elseif lib.str.cmp(mode.arglist(0),'user') == 0 then

				var umode: pbasic umode:parse(mode.arglist.ct, &mode.arglist(0))
				if umode.help then
					[ lib.emit(false, 1, 'usage: ', `argv[0], ' user ', umode.type.helptxt.flags, ' <handle> <cmd> [<args>…]', umode.type.helptxt.opts, cmdhelp {
						{ 'user <handle> create', 'add a new user' };
						{ 'user <handle> auth <type> new', '(where applicable, managed auth only) create a new authentication token of the given type for a user' };
						{ 'user <handle> auth <type> reset', '(where applicable, managed auth only) delete all of a user\'s authentication tokens of the given type and issue a new one' };
						{ 'user <handle> auth (<type>|all) purge', 'delete all credentials that would allow this user to log in (where possible)' };
						{ 'user <handle> (grant|revoke) (<priv>|all)', 'grant or revoke a specific power to or from a user' };
						{ 'user <handle> emasculate', 'strip all administrative powers and rank from a user' };
						{ 'user <handle> forgive', 'restore all default powers to a user' };
						{ 'user <handle> suspend [<timespec>]', '(e.g. \27[1muser jokester suspend 5d 6h 7m 3s\27[m to suspend "jokester" for five days, six hours, seven minutes, and three seconds) suspend a user'};

					}) ]
					return 1
				end
				var handle = umode.arglist(0)
				var usr = dlg:actor_fetch_xid(pstr {ptr=handle, ct=lib.str.sz(handle)})
				if umode.arglist.ct == 2 and lib.str.cmp(umode.arglist(1),'create')==0 then
					if usr:ref() then lib.bail('that user already exists') end
................................................................................
									end
								end
							end
						end

						usr.ptr.rights.powers = newprivs
						dlg:actor_save_privs(usr.ptr)


















					elseif lib.str.cmp(umode.arglist(1),'auth') == 0 and umode.arglist.ct == 4 then
						var reset = lib.str.cmp(umode.arglist(3),'reset') == 0
						if reset or lib.str.cmp(umode.arglist(3),'new') == 0 then
							-- FIXME enable resetting pws for users who have
							-- not logged in yet
							if not usr then lib.bail('unknown handle') end
							if lib.str.cmp(umode.arglist(2),'pw') == 0 then







>







 







>











>







 







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







421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
...
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
...
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
					[ lib.emit(false, 1, 'usage: ', `argv[0], ' actor ', umode.type.helptxt.flags, ' <xid> <cmd> [<args>…]', umode.type.helptxt.opts, cmdhelp {
						{ 'actor <xid> rank <value>', 'set an actor\'s rank to <value> (remote actors cannot exercise rank-related powers, but benefit from rank immunities)' };
						{ 'actor <xid> degrade', 'alias for `actor <xid> rank 0`' };
						{ 'actor <xid> bestow <epithet>', 'bestow an epithet upon an actor' };
						{ 'actor <xid> instantiate', 'instantiate a remote actor, retrieving their profile and posts even if no one follows them' };
						{ 'actor <xid> proscribe', 'globally ban an actor from interacting with your server' };
						{ 'actor <xid> rehabilitate', 'lift a proscription on an actor' };
						{ 'actor <xid> xkey [pem|der]', 'extract an actor\'s public key in either PEM or DER form' };
						{ 'actor <xid> purge-all <confirm-str>', 'remove all traces of a user from the database (except local user credentials -- use \27[1mauth all purge\27[m to prevent a user from accessing the instance)' };
					}) ]
					return 1
				end
				if umode.arglist.ct >= 2 then
					var degrade = lib.str.cmp(umode.arglist(1),'degrade') == 0
					var xid = umode.arglist(0)
................................................................................
							lib.warn('completely purging actor ', usr.ptr.xid, ' and all related content from database')
							dlg:actor_purge_uid(usr.ptr.id)
							lib.report('actor purged')
						else goto cmderr end
					else goto cmderr end
				else goto cmderr end
			elseif lib.str.cmp(mode.arglist(0),'user') == 0 then
				if mode.arglist.ct < 3 then goto cmderr end
				var umode: pbasic umode:parse(mode.arglist.ct, &mode.arglist(0))
				if umode.help then
					[ lib.emit(false, 1, 'usage: ', `argv[0], ' user ', umode.type.helptxt.flags, ' <handle> <cmd> [<args>…]', umode.type.helptxt.opts, cmdhelp {
						{ 'user <handle> create', 'add a new user' };
						{ 'user <handle> auth <type> new', '(where applicable, managed auth only) create a new authentication token of the given type for a user' };
						{ 'user <handle> auth <type> reset', '(where applicable, managed auth only) delete all of a user\'s authentication tokens of the given type and issue a new one' };
						{ 'user <handle> auth (<type>|all) purge', 'delete all credentials that would allow this user to log in (where possible)' };
						{ 'user <handle> (grant|revoke) (<priv>|all)', 'grant or revoke a specific power to or from a user' };
						{ 'user <handle> emasculate', 'strip all administrative powers and rank from a user' };
						{ 'user <handle> forgive', 'restore all default powers to a user' };
						{ 'user <handle> suspend [<timespec>]', '(e.g. \27[1muser jokester suspend 5d 6h 7m 3s\27[m to suspend "jokester" for five days, six hours, seven minutes, and three seconds) suspend a user'};
						{ 'user <handle> xkey [pem|der]', 'extract an user\'s *private* key in either PEM or DER form' };
					}) ]
					return 1
				end
				var handle = umode.arglist(0)
				var usr = dlg:actor_fetch_xid(pstr {ptr=handle, ct=lib.str.sz(handle)})
				if umode.arglist.ct == 2 and lib.str.cmp(umode.arglist(1),'create')==0 then
					if usr:ref() then lib.bail('that user already exists') end
................................................................................
									end
								end
							end
						end

						usr.ptr.rights.powers = newprivs
						dlg:actor_save_privs(usr.ptr)
					elseif lib.str.cmp(umode.arglist(1),'xkey') == 0 and umode.arglist.ct == 3 then
						if not usr then lib.bail('unknown handle') end
						if lib.str.cmp(umode.arglist(2),'pem') == 0 then
							var pk = lib.crypt.loadpriv(usr().key)
							if not pk.ok then
								lib.bail('could not parse key! this is probably a bug')
							end
							var pem: lib.crypt.pemfile
							if not lib.crypt.pem(false, &pk.val, &pem[0]) then
								lib.bail('could not convert key to PEM! this is probably a bug')
							end
							lib.io.send(1, pem, lib.str.sz(&pem[0]))
							pk.val:free()
						elseif lib.str.cmp(umode.arglist(2),'der') == 0 then
							-- TODO avoid dumping binary to tty
							lib.warn('dumping user\'s \x1b[1mprivate\x1b[m key!')
							lib.io.send(1, [&int8](usr().key.ptr), usr().key.ct)
						else lib.bail('invalid key format') end
					elseif lib.str.cmp(umode.arglist(1),'auth') == 0 and umode.arglist.ct == 4 then
						var reset = lib.str.cmp(umode.arglist(3),'reset') == 0
						if reset or lib.str.cmp(umode.arglist(3),'new') == 0 then
							-- FIXME enable resetting pws for users who have
							-- not logged in yet
							if not usr then lib.bail('unknown handle') end
							if lib.str.cmp(umode.arglist(2),'pw') == 0 then

Modified mime.t from [b6a24abaaf] to [8a0a5cf230].


1
2
3
4

5
6
7
8
9
10
11


12






13
14
15






16



















































































local knowntypes = {
	['text/csrc'] = {
		ext = 'c', lang = 'c';
	};

	['text/html'] = {
		ext = 'html', lang = 'html';
		unsafe = true;
	};
	['text/x-lua'] = {
		ext = 'lua', lang = 'lua';
	};


	['text/markdown'] = {






		formatter = 'smackdown';
		ext = 'md', doc = true;
	};






}


















































































>

<
|
<
>
|
|
|
|
<
<

>
>
|
>
>
>
>
>
>

<

>
>
>
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
-- vim: ft=terra
local knowntypes = {

	html = {

		ext = 'html', kind = 'markup', unsafe = true, id = {
			'text/html'; 
			'application/xhtml+xml';
			'application/vnd.wap.xhtml+xml';
		};


	};
	flash = { ext = 'swf', kind = 'vm_prog', id = 'application/x-shockwave-flash', unsafe = true, binary = true };
	java = { ext = 'java', kind = 'vm_prog', id = 'application/java', unsafe = true, binary = true };
	css = { ext = 'css', kind = 'lang', id = 'text/css'};
	text = { ext = 'txt', kind = 'text', id = 'text/plain' };
	c = { ext = 'c', kind = 'prog_lang', id = 'text/csrc' };
	xml = { ext = 'xml', kind = 'markup', unsafe = true, id = 'text/xml' };
	lua = { ext = 'lua', kind = 'prog_lang', id = 'text/x-lua' };
	ansi = { ext = 'ans', kind = 'text', id = 'text/x-ansi', doc = true, binary = true};
	mkdown = { ext = 'md', kind = 'text', doc = true; id = 'text/markdown';
		formatter = 'smackdown';

	};
	json = {
		ext = 'json', kind = 'lang', id = {
			'application/json';
			'application/activity+json';
			'application/ld+json';
			'application/jrd+json';
		};
	};
	svg = { ext = 'svg', kind = 'image', id = 'image/svg+xml' };
	webp = { ext = 'webp', kind = 'image', id = 'image/webp', binary = true };
	png = { ext = 'png', kind = 'image', id = 'image/png', binary = true };
	jpeg = { ext = 'jpg', kind = 'image', id = 'image/jpeg', binary = true };

	-- wildcard
	none = { id = '*/*' };
}

local idcache = {}


local pstr = lib.str.t
local filekind = lib.enum [[none image text lang prog_lang markup vm_prog]]
local struct mime {
	key: pstr
	canonical: pstr
	safe: bool
	binary: bool
	ext: pstr
	kind: filekind.t
	output: lib.http.mime.t
}

local typestore = {}
for typecode, ty in pairs(knowntypes) do
	ty.key = typecode
	if type(ty.id) == 'string' then ty.id = {ty.id} end
	for i, mime in ipairs(ty.id) do
		idcache[mime] = ty
	end

	local op = lib.http.mime[typecode]
	if op == nil then op = lib.http.mime.none end
	print(typecode,op)

	ty.offset = #typestore
	typestore[#typestore + 1] = `mime {
		key = typecode;
		canonical = [ty.id[1]];
		safe = [not ty.unsafe];
		ext = [ty.ext or `pstr{nil,0}];
		kind = [ty.kind and filekind[ty.kind] or filekind.none];
		binary = [ty.binary or false];
		output = [op];
	}

end

local typedex = global(`array([typestore]))
local struct mimemapping {
	string: pstr
	type: &mime
}

local typemap_l = {}
for mime, ty in pairs(idcache) do
	typemap_l[#typemap_l + 1] = `mimemapping {
		string = mime;
		type = &typedex[ [ty.offset] ];
	}

end
local typemap = global(`array([typemap_l]));


return {
	type = mime;
	types = knowntypes;
	tbl = idcache;
	typedex = typedex;
	lookup = terra(m: pstr): &mime
		for i=0, [#typemap_l] do
			if m:cmp(typemap[i].string) then
				lib.io.fmt('returning type %s %u\n', typemap[i].type.key, typemap[i].type.output)
				return typemap[i].type
			end
		end
		return nil
	end;
}

Name change from conv.t to munge.t.

Modified parsav.t from [76fc393228] to [3c7c1240d3].

254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
...
440
441
442
443
444
445
446

447
448
449
450
451
452
453
454
455
	if #tbl >= 2^32 then ty = uint64 -- hey, can't be too safe
	elseif #tbl >= 2^16 then ty = uint32
	elseif #tbl >= 2^8 then ty = uint16 end
	local o = { t = ty, members = tbl }
	local strings = {}
	for i, name in ipairs(tbl) do
		o[name] = `[ty]([i - 1])
		strings[i] = `[lib.mem.ref(int8)]{ptr=[name], ct=[#name]}
	end
	o._str = terra(val: ty)
		var l = array([strings])
		return l[val]
	end
	return o
end
................................................................................
lib.pq = lib.loadlib('libpq','libpq-fe.h')
lib.jc = lib.loadlib('json-c','json.h')

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


	'smackdown'; -- md-alike parser
	'conv'; -- miscellaneous conversion/munging functions
}

local be = {}
for _, b in pairs(config.backends) do
	be[#be+1] = terralib.loadfile(string.format('backend/%s.t',b))()
end
lib.store.backends = global(`array([be]))







|







 







>

|







254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
...
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
	if #tbl >= 2^32 then ty = uint64 -- hey, can't be too safe
	elseif #tbl >= 2^16 then ty = uint32
	elseif #tbl >= 2^8 then ty = uint16 end
	local o = { t = ty, members = tbl }
	local strings = {}
	for i, name in ipairs(tbl) do
		o[name] = `[ty]([i - 1])
		strings[i] = `[lib.str.t]{ptr=[name], ct=[#name]}
	end
	o._str = terra(val: ty)
		var l = array([strings])
		return l[val]
	end
	return o
end
................................................................................
lib.pq = lib.loadlib('libpq','libpq-fe.h')
lib.jc = lib.loadlib('json-c','json.h')

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

	'mime'; -- mimetype database & whitelist
	'smackdown'; -- md-alike parser
	'munge'; -- miscellaneous conversion/munging functions
}

local be = {}
for _, b in pairs(config.backends) do
	be[#be+1] = terralib.loadfile(string.format('backend/%s.t',b))()
end
lib.store.backends = global(`array([be]))

Modified route.t from [56f7ddd740] to [1bb0eb41f3].

282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
...
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
...
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
...
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414

415



416
417
418
419
420

421
422
423
424
425
426
427
...
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
...
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
	elseif path.ct == 1 then
		lib.render.docpage(co, rstring.null())
	else
		co:complain(404, 'no such documentation', 'invalid documentation URL')
	end
end

terra http.tweet_page(co: &lib.srv.convo, path: hpath, meth: method.t)
	var pid, ok = lib.math.shorthand.parse(path(1).ptr, path(1).ct)
	if not ok then
		co:complain(400, 'bad post ID', 'that post ID is not valid')
		return
	end
	var post = co.srv:post_fetch(pid)
	var rt: lib.store.notice
................................................................................
	end
	defer post:free() -- NOP on null

	if path.ct == 3 then
		var lnk: lib.str.acc lnk:compose('/post/', path(1))
		var lnkp = lnk:finalize() defer lnkp:free()
		if post:ref() and path(2):cmp(lib.str.lit 'snitch') then
			if meth_get(meth) then
				var ui = data.view.report {
					badtweet = lib.render.tweet(co, post.ptr, nil);
					clnk = lnkp;
				}

				co:stdpage([lib.srv.convo.page] {
					title = 'post :: report';
................................................................................
			end
			return
		elseif post:ref() and post(0).author ~= co.who.id then
			co:complain(403, 'forbidden', 'you cannot alter other people\'s posts')
			return
		elseif post:ref() and path(2):cmp(lib.str.lit 'edit') then
			if not co:assertpow('edit') then return end
			if meth_get(meth) then
				lib.render.compose(co, post.ptr, nil)
				return
			elseif meth == method.post then
				var newbody = co:postv('post')._0
				var newacl = co:postv('acl')._0
				var newsubj = co:postv('subject')._0
				if newbody ~= nil then post(0).body = newbody end
				if newacl  ~= nil then post(0).acl = newacl end
				if newsubj ~= nil then post(0).subject = newsubj end
				post(0):save(true)
				co:reroute(lnkp.ptr)
			end
			return
		elseif path(2):cmp(lib.str.lit 'del') then
			if meth_get(meth) then
				var conf: data.view.confirm
				if post:ref() then
					conf = data.view.confirm {
						title =  'delete post';
						query =  'are you sure you want to delete this post?';
						cancel = lnkp
					}
				else
					conf = data.view.confirm {
						title =  'cancel retweet';
						query =  'are you sure you want to undo this retweet?';
						cancel = '/';
					}
				end
				var body = conf:poolstr(&co.srv.pool) --defer body:free()
				co:stdpage([lib.srv.convo.page] {
					title =  'post :: delete';
					class =  'query';
					body = body; cache = false;
				})
				return
			elseif meth == method.post then
				var act = co:ppostv('act')
				if act:cmp('confirm') then
					if post:ref() then
						post().source:post_destroy(post().id)
					elseif rt.kind ~= 0 then
						co.srv:post_act_cancel(pid)
					end
................................................................................
					co:reroute('/') -- TODO maybe return to parent or conversation if possible
					return
				else goto badop end
			end
		else goto badurl end
	end

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

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

	if not post then goto badurl end


	lib.render.tweet_page(co, path, post.ptr)



	do return end

	::noauth:: do co:fail(401) return end
	::badurl:: do co:fail(404) return end
	::badop :: do co:fail(405) return end

end

local terra 
credsec_for_uid(co: &lib.srv.convo, uid: uint64)
	var act = co:ppostv('act')
	if not act then return true end
	lib.dbg('handling credential action')
................................................................................
			var fr = co.srv.pool:frame()
			var hmac = lib.crypt.hmacp(&co.srv.pool, lib.crypt.alg.sha256, co.srv.cfg.secret:blob(), nonce)
			if not lib.math.truncate64(hmac.ptr, hmac.ct) == noncevld then
				co:complain(403,'nice try','what exactly are you trying to accomplish here, buddy')
				return false
			end

			var pkres = lib.crypt.loadpub(rsapub.ptr,rsapub.ct+1) -- needs NUL
			if not pkres.ok then
				co:complain(400,'invalid key','the key you have supplied is not a valid PEM or DER file')
				return false
			end
			var pk = pkres.val
			defer pk:free()

................................................................................
			else co:reroute_cookie('/','auth=; Path=/')
		end
	else -- hierarchical routes
		var path = lib.http.hier(&co.srv.pool, uri) --defer path:free()
		if path.ct > 1 and path(0):cmp('user') then
			http.actor_profile_uid(co, path)
		elseif path.ct > 1 and path(0):cmp('post') then
			http.tweet_page(co, path, meth)
		elseif path(0):cmp('tl') then
			http.timeline(co, path)
		elseif path(0):cmp('.well-known') then
			if path(1):cmp('webfinger') then
				if not co:matchmime(lib.http.mime.json) then goto nacc end
				lib.api.webfinger(co)
			end







|







 







|







 







|


|











|



|
|




|
|





|
|



|







 







|


|


|


|


|



|













>
|
>
>
>





>







 







|







 







|







282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
...
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
...
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
...
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
...
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
...
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
	elseif path.ct == 1 then
		lib.render.docpage(co, rstring.null())
	else
		co:complain(404, 'no such documentation', 'invalid documentation URL')
	end
end

terra http.tweet_page(co: &lib.srv.convo, path: hpath)
	var pid, ok = lib.math.shorthand.parse(path(1).ptr, path(1).ct)
	if not ok then
		co:complain(400, 'bad post ID', 'that post ID is not valid')
		return
	end
	var post = co.srv:post_fetch(pid)
	var rt: lib.store.notice
................................................................................
	end
	defer post:free() -- NOP on null

	if path.ct == 3 then
		var lnk: lib.str.acc lnk:compose('/post/', path(1))
		var lnkp = lnk:finalize() defer lnkp:free()
		if post:ref() and path(2):cmp(lib.str.lit 'snitch') then
			if meth_get(co.method) then
				var ui = data.view.report {
					badtweet = lib.render.tweet(co, post.ptr, nil);
					clnk = lnkp;
				}

				co:stdpage([lib.srv.convo.page] {
					title = 'post :: report';
................................................................................
			end
			return
		elseif post:ref() and post(0).author ~= co.who.id then
			co:complain(403, 'forbidden', 'you cannot alter other people\'s posts')
			return
		elseif post:ref() and path(2):cmp(lib.str.lit 'edit') then
			if not co:assertpow('edit') then return end
			if meth_get(co.method) then
				lib.render.compose(co, post.ptr, nil)
				return
			elseif co.method == method.post then
				var newbody = co:postv('post')._0
				var newacl = co:postv('acl')._0
				var newsubj = co:postv('subject')._0
				if newbody ~= nil then post(0).body = newbody end
				if newacl  ~= nil then post(0).acl = newacl end
				if newsubj ~= nil then post(0).subject = newsubj end
				post(0):save(true)
				co:reroute(lnkp.ptr)
			end
			return
		elseif path(2):cmp(lib.str.lit 'del') then
			if meth_get(co.method) then
				var conf: data.view.confirm
				if post:ref() then
					conf = data.view.confirm {
						title = 'delete post';
						query = 'are you sure you want to delete this post?';
						cancel = lnkp
					}
				else
					conf = data.view.confirm {
						title = 'cancel retweet';
						query = 'are you sure you want to undo this retweet?';
						cancel = '/';
					}
				end
				var body = conf:poolstr(&co.srv.pool) --defer body:free()
				co:stdpage([lib.srv.convo.page] {
					title = 'post :: delete';
					class = 'query';
					body = body; cache = false;
				})
				return
			elseif co.method == method.post then
				var act = co:ppostv('act')
				if act:cmp('confirm') then
					if post:ref() then
						post().source:post_destroy(post().id)
					elseif rt.kind ~= 0 then
						co.srv:post_act_cancel(pid)
					end
................................................................................
					co:reroute('/') -- TODO maybe return to parent or conversation if possible
					return
				else goto badop end
			end
		else goto badurl end
	end

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

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

	if not post then goto badurl end

	if co:matchmime(lib.http.mime.html) then
		lib.render.tweet_page(co, path, post.ptr)
	elseif co:matchmime(lib.http.mime.json) then
		co:json(lib.api.lp.tweet(co, post.ptr, false))
	else goto notacc end
	do return end

	::noauth:: do co:fail(401) return end
	::badurl:: do co:fail(404) return end
	::badop :: do co:fail(405) return end
	::notacc:: do co:fail(406) return end
end

local terra 
credsec_for_uid(co: &lib.srv.convo, uid: uint64)
	var act = co:ppostv('act')
	if not act then return true end
	lib.dbg('handling credential action')
................................................................................
			var fr = co.srv.pool:frame()
			var hmac = lib.crypt.hmacp(&co.srv.pool, lib.crypt.alg.sha256, co.srv.cfg.secret:blob(), nonce)
			if not lib.math.truncate64(hmac.ptr, hmac.ct) == noncevld then
				co:complain(403,'nice try','what exactly are you trying to accomplish here, buddy')
				return false
			end

			var pkres = lib.crypt.loadpub(binblob{rsapub.ptr,rsapub.ct+1}) -- needs NUL
			if not pkres.ok then
				co:complain(400,'invalid key','the key you have supplied is not a valid PEM or DER file')
				return false
			end
			var pk = pkres.val
			defer pk:free()

................................................................................
			else co:reroute_cookie('/','auth=; Path=/')
		end
	else -- hierarchical routes
		var path = lib.http.hier(&co.srv.pool, uri) --defer path:free()
		if path.ct > 1 and path(0):cmp('user') then
			http.actor_profile_uid(co, path)
		elseif path.ct > 1 and path(0):cmp('post') then
			http.tweet_page(co, path)
		elseif path(0):cmp('tl') then
			http.timeline(co, path)
		elseif path(0):cmp('.well-known') then
			if path(1):cmp('webfinger') then
				if not co:matchmime(lib.http.mime.json) then goto nacc end
				lib.api.webfinger(co)
			end

Modified srv.t from [68c9cc33d4] to [dafa2dc374].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
149
150
151
152
153
154
155


156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
...
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
...
634
635
636
637
638
639
640
641

642
643
644
645
646
647
648
649
650
651
652
653
654
...
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
...
878
879
880
881
882
883
884


885
886
887
888
889
890
891
-- vim: ft=terra
local util = lib.util
local secmode = lib.enum { 'public', 'private', 'lockdown', 'isolate' }
local pstring = lib.mem.ptr(int8)
local mimetypes = {
	{'html', 'text/html'};
	{'json', 'application/json'};
	{'json', 'application/activity+json'};
	{'json', 'application/ld+json'};
	{'mkdown', 'text/markdown'};
	{'text', 'text/plain'};
	{'ansi', 'text/x-ansi'};
}

local struct srv
local struct cfgcache {
	secret: pstring
	pol_sec: secmode.t
	pol_reg: bool
	pol_autoherald: bool
................................................................................

terra lib.store.post:publish(s: &srv)
	self:comp()
	self.posted = lib.osclock.time(nil)
	self.discovered = self.posted
	self.chgcount = 0
	self.edited = 0


	self.id = s:post_create(self)
	return self.id
end

local struct convo {
	srv: &srv
	con: &lib.net.mg_connection
	msg: &lib.net.mg_http_message
	aid: uint64 -- 0 if logged out
	aid_issue: lib.store.timepoint
	who: &lib.store.actor -- who we're logged in as, if aid ~= 0
	peer: lib.store.inet
	reqtype: lib.http.mime.t -- negotiated content type
	method: lib.http.method.t
	live_last: lib.store.timepoint
	uploads: lib.mem.vec(lib.http.upload)
	body: lib.str.t
-- cache
	ui_hue: uint16
	navbar: lib.mem.ptr(int8)
	actorcache: lib.mem.cache(lib.mem.ptr(lib.store.actor),32) -- naive cache to avoid unnecessary queries
-- private
	varbuf: lib.mem.ptr(int8)
	vbofs: &int8
}

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

local usrdefs = {
	str = {
		['acl-follow'    ] = {cfgfld = 'usrdef_pol_follow', fallback = 'local'};
		['acl-follow-req'] = {cfgfld = 'usrdef_pol_follow_req', fallback = 'all'};
	};
}

terra convo:matchmime(mime: lib.http.mime.t): bool
	return self.reqtype == [lib.http.mime.none]
		or self.reqtype == mime
end

terra convo:usercfg_str(uid: uint64, setting: pstring): pstring
	var set = self.srv:actor_conf_str_get(&self.srv.pool, uid, setting)
	if not set then
		[(function()
			local q = quote return pstring.null() end
			for key, dfl in pairs(usrdefs.str) do
				local rv
				if dfl.cfgfld then
					rv = quote
						var cf = self.srv.cfg.[dfl.cfgfld]
					in terralib.select(not cf, pstring([dfl.fallback]), cf) end
				elseif dfl.lit then rv = dfl.lit end
				q = quote
					if setting:cmp([key]) then return [rv] else [q] end
				end
			end
			return q
		end)()]
	else return set end
end

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

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

terra convo:uid2actor_live(uid: uint64)
	var actor = self.srv:actor_fetch_uid(uid)
	if actor:ref() then
		if self.aid ~= 0 and self.who.id ~= uid then
			actor(0).relationship = self.srv:actor_rel_calc(self.who.id, uid)
		else -- defensive branch
			actor(0).relationship = lib.store.relationship {
				agent = 0, patient = uid;
				rel   = [lib.store.relation.null],
				recip = [lib.store.relation.null],
			}
		end
	end
	return actor
end

terra convo:uid2actor(uid: uint64)
	var actor: &lib.store.actor = nil
	for j = 0, self.actorcache.top do
		if uid == self.actorcache(j).ptr.id then
			actor = self.actorcache(j).ptr
			break
		end
	end
	if actor == nil then
		actor = self.actorcache:insert(self:uid2actor_live(uid)).ptr
	end
	return actor
end

terra convo:rawpage(code: uint16, pg: convo.page, hdrs: lib.mem.ptr(lib.http.header))
	var doc = data.view.docskel {
		instance = self.srv.cfg.instance;
		title = pg.title;
		body = pg.body;
		class = pg.class;
		navlinks = self.navbar;
		attr = '';
	}
	var attrbuf: int8[32]
	if self.aid ~= 0 and self.ui_hue ~= 323 then
		var hdecbuf: int8[21]
		var hdec = lib.math.decstr(self.ui_hue, &hdecbuf[20])
		lib.str.cpy(&attrbuf[0], ' style="--hue:')
		lib.str.cpy(&attrbuf[14], hdec)
		var len = &hdecbuf[20] - hdec 
		lib.str.cpy(&attrbuf[14] + len, '"')
		doc.attr = &attrbuf[0]
	end

	if self.method == [lib.http.method.head]
		then doc:head(self.con,code,hdrs)
		else doc:send(self.con,code,hdrs)
	end
end

terra convo:statpage(code: uint16, pg: convo.page)
	var hdrs = array(
		lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' },
		lib.http.header { key = 'Cache-Control', value = 'no-store' }
	)
	self:rawpage(code,pg, [lib.mem.ptr(lib.http.header)] {
		ptr = &hdrs[0];
		ct = [hdrs.type.N] - lib.trn(pg.cache,1,0);
	})
end

terra convo:livepage(pg: convo.page, lastup: lib.store.timepoint)
	var nbuf: int8[21]
	var hdrs = array(
		lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' },
		lib.http.header { key = 'Cache-Control', value = 'no-store' },
		lib.http.header {
			key = 'X-Live-Newest-Artifact';
			value = lib.math.decstr(lastup, &nbuf[20]);
		},
		lib.http.header { key = 'Content-Length', value = '0' }
	)
	if self.live_last ~= 0 and self.live_last == lastup then
		lib.net.mg_printf(self.con, 'HTTP/1.1 %s', lib.http.codestr(200))
		for i = 0, [hdrs.type.N] do
			lib.net.mg_printf(self.con, '%s: %s\r\n', hdrs[i].key, hdrs[i].value)
		end
		lib.net.mg_printf(self.con, '\r\n')
	else
		self:rawpage(200, pg, [lib.mem.ptr(lib.http.header)] {
			ptr = &hdrs[0], ct = 3
		})
	end
end

terra convo:stdpage(pg: convo.page) self:statpage(200, pg) end

terra convo:bytestream_trusted(lockdown: bool, mime: pstring, data: lib.mem.ptr(uint8))
	var lockhdr = "Content-Security-Policy: sandbox; default-src 'none'; form-action 'none'; navigate-to 'none';\r\n"
	if not lockdown then lockhdr = "" end
	lib.net.mg_printf(self.con, "HTTP/1.1 200 OK\r\nContent-Type: %.*s\r\nContent-Length: %llu\r\n%sX-Content-Options: nosniff\r\n\r\n", mime.ct, mime.ptr, data.ct + 2, lockhdr)
	lib.net.mg_send(self.con, data.ptr, data.ct)
	lib.net.mg_send(self.con, '\r\n', 2)
end

terra convo:json(data: pstring)
	self:bytestream_trusted(false, 'application/activity+json; charset=utf-8', data:blob())
end

terra convo:bytestream(mime: pstring, data: lib.mem.ptr(uint8))
	-- TODO this is not a satisfactory solution; it's a bandaid on a gaping
	-- chest wound. ultimately we need to compile a whitelist of safe mime
	-- types as part of mimelib, but that is no small task. for now, this
	-- will keep the patient from immediately bleeding out
	if mime:cmp('text/html') or
		mime:cmp('text/xml') or
		mime:cmp('application/xhtml+xml') or
		mime:cmp('application/vnd.wap.xhtml+xml')
	then -- danger will robinson
		mime = 'text/plain'
	elseif mime:cmp('application/x-shockwave-flash') then
		mime = 'application/octet-stream'
	end
	self:bytestream_trusted(true, mime, data)
end

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

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

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

terra convo:reroute(dest: rawstring) self:reroute_cookie(dest,nil) end

terra convo:installkey(dest: rawstring, aid: uint64)
	var sesskey: int8[lib.session.maxlen + #lib.session.cookiename + #"=; Path=/" + 1]
	do var p = &sesskey[0]
		p = lib.str.ncpy(p, [lib.session.cookiename .. '='], [#lib.session.cookiename + 1])
		p = p + lib.session.cookie_gen(self.srv.cfg.secret, aid, lib.osclock.time(nil), p)
		lib.dbg('sending cookie ',{&sesskey[0],15})
		p = lib.str.ncpy(p, '; Path=/', 9)
	end
	self:reroute_cookie(dest, &sesskey[0])
end
 
terra convo:stra(sz: intptr) -- convenience function
	var s: lib.str.acc
	s:pool(&self.srv.pool,sz)
	return s
end

convo.methods.qstr = macro(function(self, ...) -- convenience string builder
	local exp = {...}
	return `lib.str.acc{}:pcompose(&self.srv.pool, [exp]):finalize()
end)

terra convo:complain(code: uint16, title: rawstring, msg: rawstring)
	if msg == nil then msg = "i'm sorry, dave. i can't let you do that" end

	if self:matchmime(lib.http.mime.html) then
		var body = [convo.page] {
			title = self:qstr('error :: ', title);
			body = self:qstr('<div class="message"><img class="icon" src="/s/warn.svg"><h1>',title,'</h1><p>',msg,'</p></div>');
			class = 'error';
			cache = false;
		}

		self:statpage(code, body)
	else
		var pg = lib.http.page { respcode = code, body = pstring.null() }
		var ctt = lib.http.mime.none
		if self:matchmime(lib.http.mime.json) then ctt = lib.http.mime.json
			pg.body = ([lib.tpl.mk'{"_parsav_error":@$ekind, "_parsav_error_desc":@$edesc}']
				{ekind = title, edesc = msg}):poolstr(&self.srv.pool)
		elseif self:matchmime(lib.http.mime.text) then ctt = lib.http.mime.text
			pg.body = self:qstr('error: ',title,'\n',msg)
		elseif self:matchmime(lib.http.mime.mkdown) then ctt = lib.http.mime.mkdown
			pg.body = self:qstr('# error :: ',title,'\n\n',msg)
		elseif self:matchmime(lib.http.mime.ansi) then ctt = lib.http.mime.ansi
			pg.body = self:qstr('\27[1;31merror :: ',title,'\27[m\n',msg)
		end
		var cthdr = lib.http.header { 'Content-Type', 'text/plain' }
		if ctt == lib.http.mime.none then
			pg.headers.ct = 0
		else
			pg.headers = lib.typeof(pg.headers) { &cthdr, 1 }
			switch ctt do
				case [ctt.type](lib.http.mime.json) then
					cthdr.value = 'application/json'
				end
				escape
					for i,v in ipairs(mimetypes) do local key,mime = v[1],v[2]
						if key ~= 'json' then
							emit quote case [ctt.type](lib.http.mime.[key]) then cthdr.value = [mime] end end
						end
					end
				end
			end
		end
		pg:send(self.con)
	end
end

terra convo:fail(code: uint16)
	switch code do
		escape
			local stderrors = {
				{400, 'bad request', "the action you have attempted on this resource is not meaningful"};
				{401, 'unauthorized', "this resource is not available at your clearance level"};
				{403, 'forbidden', "we can neither confirm nor deny the existence of this resource"};
				{404, 'resource not found', "that resource is not extant on or known to this server"};
				{405, 'method not allowed', "the method you have attempted on this resource is not meaningful"};
				{406, 'not acceptable', "none of the suggested content types are a viable representation of this resource"};
				{500, 'internal server error', "parsav did a fucksy wucksy"};
			}

			for i,v in ipairs(stderrors) do
				emit quote case uint16([v[1]]) then
					self:complain([v])
				end end
			end
		end
		else self:complain(500,'unknown error','an unrecognized error was thrown. this is a bug')
	end
end

terra convo:confirm(title: pstring, msg: pstring, cancel: pstring)
	var conf = data.view.confirm {
		title = title;
		query = msg;
		cancel = cancel;
	}
	var ti: lib.str.acc ti:pcompose(&self.srv.pool,'confirm :: ', title)
	var body = conf:poolstr(&self.srv.pool) -- defer body:free()
	var cf = [convo.page] {
		title = ti:finalize();
		class = 'query';
		body = body; cache = false;
	}
	self:stdpage(cf)
	--cf.title:free()
end

convo.methods.assertpow = macro(function(self, pow)
	return quote
		var ok = true
		if self.aid == 0 or self.who.rights.powers.[pow:asvalue()]() == false then
			ok = false
			self:complain(403,'insufficient privileges',['you lack the <strong>'..pow:asvalue()..'</strong> power and cannot perform this action'])
		end
	in ok end
end)

local pstr2mg, mg2pstr
do -- aaaaaaaaaaaaaaaaaaaaaaaa
	mgstr = lib.util.find(lib.net.mg_http_message.entries, function(v)
		if v.field == 'body' or v[1] == 'body' then return v.type end
	end)
	terra pstr2mg(p: pstring): mgstr
		return mgstr { ptr = p.ptr, len = p.ct }
	end
	terra mg2pstr(m: mgstr): pstring
		return pstring { ptr = m.ptr, ct = m.len }
	end
end

-- CALL ONLY ONCE PER VAR
terra convo:postv_next(name: pstring, start: &pstring)
	if self.varbuf.ptr == nil then
		self.varbuf = self.srv.pool:alloc(int8, self.msg.body.len + self.msg.query.len)
		self.vbofs = self.varbuf.ptr
	end
	var conv = pstr2mg(@start)
	var o = lib.net.mg_http_get_var(
		&conv,
		name.ptr, self.vbofs,
		self.varbuf.ct - (self.vbofs - self.varbuf.ptr)
	)
	if o > 0 then
		start:advance(name.ct + o + 2)
		var r = self.vbofs
		self.vbofs = self.vbofs + o + 1
		@(self.vbofs - 1) = 0
		var norm = lib.str.normalize([lib.mem.ptr(int8)]{ptr = r, ct = o})
		return norm.ptr, norm.ct
	else return nil, 0 end
end
terra convo:postv(name: pstring)
	var start = mg2pstr(self.msg.body)
	return self:postv_next(name, &start)
end
terra convo:ppostv(name: pstring)
	var s,l = self:postv(name)
	return pstring { ptr = s, ct = l }
end
do
	local struct postiter { co: &convo where: pstring name: pstring }
	terra convo:eachpostv(name: pstring)
		return postiter { co = self, where = mg2pstr(self.msg.body), name = name } 
	end
	postiter.metamethods.__for = function(self, body)
		return quote
			while true do
				var str, len = self.co:postv_next(self.name, &self.where)
				if str == nil then break end
				[ body(`pstring {str, len}) ]
			end
		end
	end
end

terra convo:getv(name: rawstring)
	if self.varbuf.ptr == nil then
		self.varbuf = self.srv.pool:alloc(int8, self.msg.query.len + self.msg.body.len)
		self.vbofs = self.varbuf.ptr
	end
	var o = lib.net.mg_http_get_var(&self.msg.query, name, self.vbofs, self.varbuf.ct - (self.vbofs - self.varbuf.ptr))
	if o > 0 then
		var r = self.vbofs
		self.vbofs = self.vbofs + o + 1
		@(self.vbofs - 1) = 0
		var norm = lib.str.normalize([lib.mem.ptr(int8)]{ptr = r, ct = o})
		return norm.ptr, norm.ct
	else return nil, 0 end
end
terra convo:pgetv(name: rawstring)
	var s,l = self:getv(name)
	return pstring { ptr = s, ct = l }
end

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

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

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

local handle = {
	http = terra(con: &lib.net.mg_connection, event_kind: int, event: &opaque, userdata: &opaque)
		var server = [&srv](userdata)
		var mgpeer = getpeer(con)
		-- var pbuf: int8[128]

		-- the peer property is currently broken and there is precious
................................................................................
				  co.body.ptr = msg.body.ptr co.body.ct = msg.body.len

				-- first, check for an accept header. if it's there, we need to
				-- iterate over the values and pick the highest-priority one
				do var acc = lib.http.findheader(msg, 'Accept')
					-- TODO handle q-value
					if acc ~= nil and acc.ptr ~= nil then
						var [mimevar] = [lib.mem.ref(int8)] { ptr = acc.ptr }

						var i = 0 while i < acc.ct do
							if acc.ptr[i] == @',' or acc.ptr[i] == @';' then
								mimevar.ct = (acc.ptr+i) - mimevar.ptr
								var t = [mimeneg]
								if t ~= lib.http.mime.none then
									co.reqtype = t
									goto foundtype
								end

								if acc.ptr[i] == @';' then -- fast-forward over q
									for j=i+1,acc.ct do i=j
										if acc.ptr[j] == @',' then break end
									end
................................................................................

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

				-- we need to check if there's any cookies sent with the request,
				-- and if so, whether they contain any credentials. this will be
				-- used to set the auth parameters in the http conversation
				var cookies_p = lib.http.findheader(msg, 'Cookie')
				if cookies_p ~= nil and cookies_p.ptr ~= nil then
................................................................................
							end
							bsr:free()
							upmap:free()
						end
					end
				end



				route.dispatch_http(&co, uri)

				::fail::
				if co.uploads.run > 0 then
					for i=0,co.uploads.sz do
						co.uploads(i).filename:free()
						co.uploads(i).field:free()




<
<
<
<
<
<
<
<
<







 







>
>




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







 







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



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







 







|
>



|
|
|







 







|
|
|


|







 







>
>







1
2
3
4









5
6
7
8
9
10
11
...
140
141
142
143
144
145
146
147
148
149
150
151
152


153



























































154
155
156
157
158
159
160
...
161
162
163
164
165
166
167





















































































































































































































































































































































168
169
170













171
172
173
174
175
176
177
...
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
...
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
...
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
-- 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: pstring
	pol_sec: secmode.t
	pol_reg: bool
	pol_autoherald: bool
................................................................................

terra lib.store.post:publish(s: &srv)
	self:comp()
	self.posted = lib.osclock.time(nil)
	self.discovered = self.posted
	self.chgcount = 0
	self.edited = 0
	self.uri = nil -- only for foreign posts
	self.convoheaduri = nil -- ditto
	self.id = s:post_create(self)
	return self.id
end



local convo = terralib.loadfile 'convo.t'(srv)



























































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

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






















































































































































































































































































































































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














local handle = {
	http = terra(con: &lib.net.mg_connection, event_kind: int, event: &opaque, userdata: &opaque)
		var server = [&srv](userdata)
		var mgpeer = getpeer(con)
		-- var pbuf: int8[128]

		-- the peer property is currently broken and there is precious
................................................................................
				  co.body.ptr = msg.body.ptr co.body.ct = msg.body.len

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

								if acc.ptr[i] == @';' then -- fast-forward over q
									for j=i+1,acc.ct do i=j
										if acc.ptr[j] == @',' then break end
									end
................................................................................

								mimevar.ptr = acc.ptr + i + 1
							end
							i=i+1
						end
						if co.reqtype == lib.http.mime.none then
							mimevar.ct = acc.ct - (mimevar.ptr - acc.ptr)
							var mk = lib.mime.lookup(mimevar)
							if mk ~= nil and mk.output ~= lib.http.mime.none then
								co.reqtype = mk.output
							end
						end
					end
				::foundtype::end

				-- we need to check if there's any cookies sent with the request,
				-- and if so, whether they contain any credentials. this will be
				-- used to set the auth parameters in the http conversation
				var cookies_p = lib.http.findheader(msg, 'Cookie')
				if cookies_p ~= nil and cookies_p.ptr ~= nil then
................................................................................
							end
							bsr:free()
							upmap:free()
						end
					end
				end

				var mtt = lib.http.mime._str(co.reqtype)
				lib.dbg('routing with negotiated type of ', {mtt.ptr,mtt.ct})
				route.dispatch_http(&co, uri)

				::fail::
				if co.uploads.run > 0 then
					for i=0,co.uploads.sz do
						co.uploads(i).filename:free()
						co.uploads(i).field:free()

Modified store.t from [33ecd773b1] to [6e43eba049].

213
214
215
216
217
218
219

220
221
222
223
224
225
226
...
489
490
491
492
493
494
495





496
497
498
499
500
501
502
	elseif self.mode == 3 then
		return 0,0,self.to_idx,self.from_idx
	else lib.bail('invalid mode on timeline range!') end
end

struct m.post {
	id: uint64

	author: uint64
	subject: str
	body: str
	acl: str
	posted: m.timepoint
	discovered: m.timepoint
	edited: m.timepoint
................................................................................
	circle_destroy: {&m.source, uint64, uint64} -> {}
	circle_members_fetch_cid:  {&m.source, &lib.mem.pool, uint64} -> lib.mem.ptr(uint64)
	circle_members_fetch_name: {&m.source, &lib.mem.pool, uint64, pstring} -> lib.mem.ptr(uint64)
	circle_members_add_uid: {&m.source, uint64, uint64} -> {}
	circle_members_del_uid: {&m.source, uint64, uint64} -> {}
	circle_memberships_uid: {&m.source, &lib.mem.pool, uint64, uint64} -> lib.mem.ptr(m.circle)






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

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







>







 







>
>
>
>
>







213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
...
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
	elseif self.mode == 3 then
		return 0,0,self.to_idx,self.from_idx
	else lib.bail('invalid mode on timeline range!') end
end

struct m.post {
	id: uint64
	uri: str
	author: uint64
	subject: str
	body: str
	acl: str
	posted: m.timepoint
	discovered: m.timepoint
	edited: m.timepoint
................................................................................
	circle_destroy: {&m.source, uint64, uint64} -> {}
	circle_members_fetch_cid:  {&m.source, &lib.mem.pool, uint64} -> lib.mem.ptr(uint64)
	circle_members_fetch_name: {&m.source, &lib.mem.pool, uint64, pstring} -> lib.mem.ptr(uint64)
	circle_members_add_uid: {&m.source, uint64, uint64} -> {}
	circle_members_del_uid: {&m.source, uint64, uint64} -> {}
	circle_memberships_uid: {&m.source, &lib.mem.pool, uint64, uint64} -> lib.mem.ptr(m.circle)

	thread_top_find: {&m.source, uint64} -> uint64
		-- NOTE: this won't work if conversations are broken across multiple data sources!
		-- if this is a thing that's likely to happen, the overlord-side wrapper for this
		-- function (srv.t) should implement a more sophisticated algorithm over all the
		-- data sources, instead of just stopping when one parent is found
	thread_latest_arrival_calc: {&m.source, uint64} -> m.timepoint

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

Modified str.t from [072253bf19] to [bff3416286].

567
568
569
570
571
572
573
574
575
576
577
578
579
580


581
582
583
584
585
586







587
588
589





590
		var add, cont = disemvowel_codepoint(cur)
		if add:ref() then acc:ppush(add) end
		cur = cont
	end
	return acc:finalize()
end

terra m.qesc(pool: &lib.mem.pool, str: m.t, wrap: bool): m.t
 -- escape double-quotes
	var a: m.acc a:pool(pool, str.ct + str.ct/2)
	if wrap then a:lpush '"' end
	for i=0, str.ct do
		if     str(i) == @'"'  then a:lpush '\\"'
		elseif str(i) == @'\\' then a:lpush '\\\\'


		elseif str(i) < 0x20 then -- for json
			var hex = lib.math.hexbyte(str(i))
			a:lpush('\\u00'):push(&hex[0], 2)
		else   a:push(str.ptr + i,1) end
	end
	if wrap then a:lpush '"' end







	return a:finalize()
end






return m







|

<
|

|
|
>
>


|
|

|
>
>
>
>
>
>
>



>
>
>
>
>

567
568
569
570
571
572
573
574
575

576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
		var add, cont = disemvowel_codepoint(cur)
		if add:ref() then acc:ppush(add) end
		cur = cont
	end
	return acc:finalize()
end

terra m.acc:qesc(str: m.t, wrap: bool)
 -- escape double-quotes

	if wrap then self:lpush '"' end
	for i=0, str.ct do
		if     str(i) == @'"'  then self:lpush '\\"'
		elseif str(i) == @'\\' then self:lpush '\\\\'
		elseif str(i) == @'\n' then self:lpush '\\n'
		elseif str(i) == @'\t' then self:lpush '\\t'
		elseif str(i) < 0x20 then -- for json
			var hex = lib.math.hexbyte(str(i))
			self:lpush('\\u00'):push(&hex[0], 2)
		else   self:push(str.ptr + i,1) end
	end
	if wrap then self:lpush '"' end
	return self
end

terra m.qesc(pool: &lib.mem.pool, str: m.t, wrap: bool): m.t
 -- convenience function
	var a: m.acc a:pool(pool, 2 + str.ct + str.ct/2)
	a:qesc(str,wrap)
	return a:finalize()
end

terra m.acc:qpush(str: m.t)
 -- convenience adaptor
	return self:qesc(str, false)
end

return m

Modified tpl.t from [b153c48352] to [f930d7a874].

34
35
36
37
38
39
40

41
42


43

44
45

46
47
48
49
50
51
52
53
54
55
...
114
115
116
117
118
119
120
121



122
123
124
125
126
127
128
	str = str:gsub('%s+[\n$]','')
	str = str:gsub('\n','')
	str = str:gsub('</a><a ','</a> <a ') -- keep nav links from getting smooshed
	str = str:gsub(tplchar .. '%?([-%w]+)', function(file)
		if not docs[file] then docs[file] = data.doc[file] end
		return string.format('<a href="#help-%s" class="help">?</a>', file)
	end)

	for start, mode, key, stop in string.gmatch(str,'()'..tplchar..'([+:!$#%^]?)([-a-zA-Z0-9_]+):?()') do
		if string.sub(str,start-1,start-1) ~= '\\' then


			segs[#segs+1] = string.sub(str,last,start-1)

			fields[#segs] = { key = key:gsub('-','_'), mode = (mode ~= '' and mode or nil) }
			last = stop

		end
	end
	segs[#segs+1] = string.sub(str,last)

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

	for n,d in pairs(docs) do
................................................................................
		senders[#senders+1] = quote lib.net.mg_send([destcon], [seg], [#seg]) end
		appenders[#appenders+1] = quote [accumulator]:push([seg], [#seg]) end
		if fields[idx] and fields[idx].mode then
			local f = fields[idx]
			local fp = `symself.[f.key]
			local sanexp
			local nulexp
			if f.mode == '$' then sanexp = `lib.str.qesc(pool, fp, true)



			elseif f.mode == '+' then sanexp = `lib.str.qesc(pool, fp, false)
			elseif f.mode == '#' then
				sanexp = quote
					var ibuf: int8[21]
					var ptr = lib.math.decstr(fp, &ibuf[20])
				in pstr {ptr=ptr, ct=&ibuf[20] - ptr} end
			elseif f.mode == '^' then







>


>
>
|
>


>


|







 







|
>
>
>







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
...
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
	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)
	local detritus = ""
	for start, mode, key, stop in string.gmatch(str,'()'..tplchar..'([+:!$#%^]?)([-a-zA-Z0-9_]+):?()') do
		if string.sub(str,start-1,start-1) ~= '\\' then
			local suffix = ""
			if mode == '$' then suffix = '"' end
			segs[#segs+1] = detritus .. string.sub(str,last,start-1) .. suffix
			detritus = ''
			fields[#segs] = { key = key:gsub('-','_'), mode = (mode ~= '' and mode or nil) }
			last = stop
			if mode == '$' then detritus = '"' end
		end
	end
	segs[#segs+1] = detritus .. 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
................................................................................
		senders[#senders+1] = quote lib.net.mg_send([destcon], [seg], [#seg]) end
		appenders[#appenders+1] = quote [accumulator]:push([seg], [#seg]) end
		if fields[idx] and fields[idx].mode then
			local f = fields[idx]
			local fp = `symself.[f.key]
			local sanexp
			local nulexp
			if f.mode == '$' then sanexp = `lib.str.qesc(pool, fp, false)
				-- we use the detritus mechanism rather than the quote-wrap mechanism bc, apart
				-- from being faster, 0-length strings cannot be sanitized into -- >0-length
				-- strings due to how nullity is indicated (to wit, if fp == 0, ptr can be wild)
			elseif f.mode == '+' then sanexp = `lib.str.qesc(pool, fp, false)
			elseif f.mode == '#' then
				sanexp = quote
					var ibuf: int8[21]
					var ptr = lib.math.decstr(fp, &ibuf[20])
				in pstr {ptr=ptr, ct=&ibuf[20] - ptr} end
			elseif f.mode == '^' then