parsav  Check-in [d4ecea913f]

Overview
Comment:add lots more shit
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: d4ecea913fb950b3a9a46867432839060f4dfbaab8854363f7d7d63e4866e634
User & Date: lexi on 2020-12-31 00:15:53
Other Links: manifest | tags
Context
2020-12-31
02:18
start work on user mgmt check-in: db4c5fd644 user: lexi tags: trunk
00:15
add lots more shit check-in: d4ecea913f user: lexi tags: trunk
2020-12-30
02:44
enable profile editing check-in: ac4a630ad5 user: lexi tags: trunk
Changes

Modified acl.t from [3939510d0a] to [7cc6c4467d].

1




























-- vim: ft=terra





























>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
-- vim: ft=terra
local m = {
	agentkind = lib.enum {
		'user', 'circle'
	};
}

struct m.agent {
	kind: m.agentkind.t
	id: uint64
}

terra m.eval(expr: lib.str.t, agent: m.agent)

end

terra lib.store.post:save(ctupdate: bool)
-- this post handles the messy details of registering a post's
-- circles and actors, and increments the edit-count if ctupdate
-- is true, which is should be in almost all cases.
	if ctupdate then
		self.chgcount = self.chgcount + 1
		self.edited = lib.osclock.time(nil)
	end
	-- TODO extract mentions from body, circles from acl
	self.source:post_save(self)
end

return m

Modified backend/pgsql.t from [d54604496b] to [af6b4187ca].

167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
...
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
...
209
210
211
212
213
214
215
216



217
218
219
220
221
222
223
...
234
235
236
237
238
239
240















241
242
243
244
245
246
247
...
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
...
294
295
296
297
298
299
300

301
302
303
304
305
306
307
308
...
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
...
918
919
920
921
922
923
924
925

926
927
928
929
930
931
932
933
934
...
959
960
961
962
963
964
965











966
967
968
969
970
971
972
....
1106
1107
1108
1109
1110
1111
1112


























1113
1114
1115
1116
1117

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

	actor_auth_how = {
		params = {rawstring, lib.store.inet}, sql = [[
		with mts as (select a.kind from parsav_auth as a
			left join parsav_actors as u on u.id = a.uid
			where (a.uid is null or u.handle = $1::text or (
................................................................................
				(select count(*) from mts where kind like 'otp-%') > 0,
				(select count(*) from mts where kind like 'challenge-%') > 0,
				(select count(*) from mts where kind = 'trust') > 0
		]]; -- cheat
	};

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

			       au.restrict,
................................................................................
						array['admin' ] <@ au.restrict as can_admin

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

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



		]];
	};

	actor_powers_fetch = {
		params = {uint64}, sql = [[
			select key, allow from parsav_rights where actor = $1::bigint
		]]
................................................................................
	actor_power_delete = {
		params = {uint64,lib.mem.ptr(int8)}, cmd = true, sql = [[
			delete from parsav_rights where
				actor = $1::bigint and
				key = $2::text
		]]
	};
















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
















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















	post_enum_author_uid = {
		params = {uint64,uint64,uint64,uint64, uint64}, sql = [[
			select a.origin is null,
				p.id, p.author, p.subject, p.acl, p.body,
				extract(epoch from p.posted    )::bigint,
				extract(epoch from p.discovered)::bigint,

				p.parent, p.convoheaduri
			from parsav_posts as p
				inner join parsav_actors as a on p.author = a.id
			where p.author = $5::bigint and
				($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and
				($2::bigint = 0 or to_timestamp($2::bigint) < p.posted)
			order by (p.posted, p.discovered) desc
			limit case when $3::bigint = 0 then null
................................................................................

	timeline_instance_fetch = {
		params = {uint64, uint64, uint64, uint64}, sql = [[
			select true,
				p.id, p.author, p.subject, p.acl, p.body,
				extract(epoch from p.posted    )::bigint,
				extract(epoch from p.discovered)::bigint,

				p.parent, null::text
			from parsav_posts as p
				inner join parsav_actors as a on p.author = a.id
			where
				($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and
				($2::bigint = 0 or to_timestamp($2::bigint) < p.posted) and
				(a.origin is null)
			order by (p.posted, p.discovered) desc
................................................................................
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,9)
		then cvhu = nil cvhlen = 0
		else cvhu = r:string(row,9) cvhlen = r:len(row,9)+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)
	p.ptr.posted = r:int(uint64,row,6)
	p.ptr.discovered = r:int(uint64,row,7)

	if r:null(row,8)
		then p.ptr.parent = 0
		else p.ptr.parent = r:int(uint64,row,8)




	end 
	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)
................................................................................
		s.mutuals = r:int(uint64, 0, 3)
		return s
	end];

	actor_session_fetch = [terra(
		src: &lib.store.source,
		aid: uint64,
		ip : lib.store.inet

	): { lib.stat(lib.store.auth), lib.mem.ptr(lib.store.actor) }
		var r = queries.actor_session_fetch.exec(src, aid, ip)
		if r.sz == 0 then goto fail end
		do defer r:free()

			if r:null(0,0) then goto fail end

			var a = row_to_actor(&r, 0)
			a.ptr.source = src
................................................................................
	): uint64
		var r = queries.post_create.exec(src,post.author,post.subject,post.acl,post.body) 
		if r.sz == 0 then return 0 end
		defer r:free()
		var id = r:int(uint64,0,0)
		return id
	end];












	timeline_instance_fetch = [terra(src: &lib.store.source, rg: lib.store.range)
		var r = pqr { sz = 0 }
		var A,B,C,D = rg:matrix() -- :/
		r = queries.timeline_instance_fetch.exec(src,A,B,C,D)
		
		var ret: lib.mem.ptr(lib.mem.ptr(lib.store.post)) ret:init(r.sz)
................................................................................
		detach: bool
	): {}
		if detach
			then queries.post_attach_ctl_del.exec(src,post,artifact)
			else queries.post_attach_ctl_ins.exec(src,post,artifact)
		end
	end];



























	actor_auth_register_uid = nil; -- TODO better support non-view based auth
}

return b







|







 







|







 







|
>
>
>







 







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







 







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







 







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







>
|







 







>
|







 







|

|











>
|

|
>
>
>
>







 







|
>

|







 







>
>
>
>
>
>
>
>
>
>
>







 







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





167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
...
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
...
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
...
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
...
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
...
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
...
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
...
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
....
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
....
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209

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

	actor_auth_how = {
		params = {rawstring, lib.store.inet}, sql = [[
		with mts as (select a.kind from parsav_auth as a
			left join parsav_actors as u on u.id = a.uid
			where (a.uid is null or u.handle = $1::text or (
................................................................................
				(select count(*) from mts where kind like 'otp-%') > 0,
				(select count(*) from mts where kind like 'challenge-%') > 0,
				(select count(*) from mts where kind = 'trust') > 0
		]]; -- cheat
	};

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

			       au.restrict,
................................................................................
						array['admin' ] <@ au.restrict as can_admin

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

			where au.aid = $1::bigint and au.blacklist = false and
				(au.netmask is null or au.netmask >> $2::inet) and
				($3::bigint = 0 or --slightly abusing the epoch time fmt here, but
					((a.authtime   is null or a.authtime   <= to_timestamp($3::bigint)) and
					 (au.valperiod is null or au.valperiod <= to_timestamp($3::bigint))))
		]];
	};

	actor_powers_fetch = {
		params = {uint64}, sql = [[
			select key, allow from parsav_rights where actor = $1::bigint
		]]
................................................................................
	actor_power_delete = {
		params = {uint64,lib.mem.ptr(int8)}, cmd = true, sql = [[
			delete from parsav_rights where
				actor = $1::bigint and
				key = $2::text
		]]
	};

	auth_sigtime_user_fetch = {
		params = {uint64}, sql = [[
			select extract(epoch from authtime)::bigint
			from parsav_actors where id = $1::bigint
		]];
	};

	auth_sigtime_user_alter = {
		params = {uint64,int64}, cmd = true, sql = [[
			update parsav_actors set
				authtime = to_timestamp($2::bigint)
				where id = $1::bigint
		]];
	};

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

	post_save = {
		params = {
			uint64, uint32, int64;
			rawstring, rawstring, rawstring;
		}, cmd = true, sql = [[
			update parsav_posts set
				subject = $4::text,
				acl = $5::text,
				body = $6::text,
				chgcount = $2::integer,
				edited = to_timestamp($3::bigint)
			where id = $1::bigint
		]]
	};

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

	post_fetch = {
		params = {uint64}, sql = [[
			select a.origin is null,
				p.id, p.author, p.subject, p.acl, p.body,
				extract(epoch from p.posted    )::bigint,
				extract(epoch from p.discovered)::bigint,
				extract(epoch from p.edited    )::bigint,
				p.parent, p.convoheaduri, p.chgcount
			from parsav_posts as p
				inner join parsav_actors as a on p.author = a.id
			where p.id = $1::bigint
		]];
	};

	post_enum_author_uid = {
		params = {uint64,uint64,uint64,uint64, uint64}, sql = [[
			select a.origin is null,
				p.id, p.author, p.subject, p.acl, p.body,
				extract(epoch from p.posted    )::bigint,
				extract(epoch from p.discovered)::bigint,
				extract(epoch from p.edited    )::bigint,
				p.parent, p.convoheaduri, p.chgcount
			from parsav_posts as p
				inner join parsav_actors as a on p.author = a.id
			where p.author = $5::bigint and
				($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and
				($2::bigint = 0 or to_timestamp($2::bigint) < p.posted)
			order by (p.posted, p.discovered) desc
			limit case when $3::bigint = 0 then null
................................................................................

	timeline_instance_fetch = {
		params = {uint64, uint64, uint64, uint64}, sql = [[
			select true,
				p.id, p.author, p.subject, p.acl, p.body,
				extract(epoch from p.posted    )::bigint,
				extract(epoch from p.discovered)::bigint,
				extract(epoch from p.edited    )::bigint,
				p.parent, null::text, p.chgcount
			from parsav_posts as p
				inner join parsav_actors as a on p.author = a.id
			where
				($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and
				($2::bigint = 0 or to_timestamp($2::bigint) < p.posted) and
				(a.origin is null)
			order by (p.posted, p.discovered) desc
................................................................................
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)
	p.ptr.posted = r:int(uint64,row,6)
	p.ptr.discovered = r:int(uint64,row,7)
	p.ptr.edited = r:int(uint64,row,8)
	if r:null(row,9)
		then p.ptr.parent = 0
		else p.ptr.parent = r:int(uint64,row,9)
	end 
	if r:null(row,11)
		then p.ptr.chgcount = 0
		else p.ptr.chgcount = r:int(uint32,row,11)
	end 
	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)
................................................................................
		s.mutuals = r:int(uint64, 0, 3)
		return s
	end];

	actor_session_fetch = [terra(
		src: &lib.store.source,
		aid: uint64,
		ip : lib.store.inet,
		issuetime: lib.store.timepoint
	): { lib.stat(lib.store.auth), lib.mem.ptr(lib.store.actor) }
		var r = queries.actor_session_fetch.exec(src, aid, ip, issuetime)
		if r.sz == 0 then goto fail end
		do defer r:free()

			if r:null(0,0) then goto fail end

			var a = row_to_actor(&r, 0)
			a.ptr.source = src
................................................................................
	): uint64
		var r = queries.post_create.exec(src,post.author,post.subject,post.acl,post.body) 
		if r.sz == 0 then return 0 end
		defer r:free()
		var id = r:int(uint64,0,0)
		return id
	end];

	post_fetch = [terra(
		src: &lib.store.source,
		post: uint64
	): lib.mem.ptr(lib.store.post)
		var r = queries.post_fetch.exec(src, post)
		if r.sz == 0 then return [lib.mem.ptr(lib.store.post)].null() end
		var p = row_to_post(&r, 0)
		p.ptr.source = src
		return p
	end];

	timeline_instance_fetch = [terra(src: &lib.store.source, rg: lib.store.range)
		var r = pqr { sz = 0 }
		var A,B,C,D = rg:matrix() -- :/
		r = queries.timeline_instance_fetch.exec(src,A,B,C,D)
		
		var ret: lib.mem.ptr(lib.mem.ptr(lib.store.post)) ret:init(r.sz)
................................................................................
		detach: bool
	): {}
		if detach
			then queries.post_attach_ctl_del.exec(src,post,artifact)
			else queries.post_attach_ctl_ins.exec(src,post,artifact)
		end
	end];
	
	post_save = [terra(
		src: &lib.store.source,
		post: &lib.store.post
	): {}
		queries.post_save.exec(src,
			post.id, post.chgcount, post.edited,
			post.subject, post.acl, post.body)
	end];

	auth_sigtime_user_fetch = [terra(
		src: &lib.store.source,
		uid: uint64
	): lib.store.timepoint
		var r = queries.auth_sigtime_user_fetch.exec(src, uid)
		if r.sz > 0 then defer r:free()
			var t = r:int(int64,0,0)
			return t
		else return 0 end
	end];

	auth_sigtime_user_alter = [terra(
		src: &lib.store.source,
		uid: uint64,
		time: lib.store.timepoint
	): {} queries.auth_sigtime_user_alter.exec(src, uid, time) end];

	actor_auth_register_uid = nil; -- TODO better support non-view based auth
}

return b

Modified backend/schema/pgsql-auth.sql from [1170b3857b] to [8bbbf24f23].

42
43
44
45
46
47
48




49
50
51
		-- if the credential matches, access will be denied, even if
		-- non-blacklisted credentials match. most useful with
		-- uid = null, kind = trust, cidr = (untrusted IP range)

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





	unique(name,kind,cred)
);







>
>
>
>



42
43
44
45
46
47
48
49
50
51
52
53
54
55
		-- if the credential matches, access will be denied, even if
		-- non-blacklisted credentials match. most useful with
		-- uid = null, kind = trust, cidr = (untrusted IP range)

	valperiod timestamp default now(),
		-- cookies bearing timestamps earlier than this point in time
		-- will be considered invalid and will not grant access
	
	comment text,
		-- a field the user can use to identify the specific credential,
		-- in order to aid credential management

	unique(name,kind,cred)
);

Modified backend/schema/pgsql.sql from [cb277a93b1] to [135f2b367a].

24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
..
56
57
58
59
60
61
62


63
64
65
66
67
68
69

create table parsav_actors (
	id        bigint primary key default (1+random()*(2^63-1))::bigint,
	nym       text,
	handle    text not null, -- nym [@handle@origin] 
	origin    bigint references parsav_servers(id)
		on delete cascade, -- null origin = local actor
	knownsince timestamp,
	bio       text,
	avatarid  bigint, -- artifact id, null if remote
	avataruri text, -- null if local
	rank      smallint not null default 0,
	quota     integer not null default 1000,
	key       bytea, -- private if localactor; public if remote
	epithet   text,
................................................................................
	author     bigint references parsav_actors(id)
		on delete cascade,
	subject    text,
	acl        text not null default 'all', -- just store the script raw 🤷
	body       text,
	posted     timestamp not null,
	discovered timestamp not null,


	parent     bigint not null default 0, -- if post: part of conversation; if chatroom: top-level post
	circles    bigint[], -- TODO at edit or creation, iterate through each circle
	mentions   bigint[], -- a user has, check if it can see her post, and if so add
	artifacts  bigint[],

	convoheaduri text
	-- only used for tracking foreign conversations and tying them to post heads;







|







 







>
>







24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
..
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

create table parsav_actors (
	id        bigint primary key default (1+random()*(2^63-1))::bigint,
	nym       text,
	handle    text not null, -- nym [@handle@origin] 
	origin    bigint references parsav_servers(id)
		on delete cascade, -- null origin = local actor
	knownsince timestamp not null default now(),
	bio       text,
	avatarid  bigint, -- artifact id, null if remote
	avataruri text, -- null if local
	rank      smallint not null default 0,
	quota     integer not null default 1000,
	key       bytea, -- private if localactor; public if remote
	epithet   text,
................................................................................
	author     bigint references parsav_actors(id)
		on delete cascade,
	subject    text,
	acl        text not null default 'all', -- just store the script raw 🤷
	body       text,
	posted     timestamp not null,
	discovered timestamp not null,
	chgcount   integer not null default 0,
	edited     timestamp,
	parent     bigint not null default 0, -- if post: part of conversation; if chatroom: top-level post
	circles    bigint[], -- TODO at edit or creation, iterate through each circle
	mentions   bigint[], -- a user has, check if it can see her post, and if so add
	artifacts  bigint[],

	convoheaduri text
	-- only used for tracking foreign conversations and tying them to post heads;

Modified cmdparse.t from [49c267b075] to [dec7841af5].

21
22
23
24
25
26
27

28
29




30
31
32
33
34
35
36
..
48
49
50
51
52
53
54

55
56

57
58
59
60
61
62
63
		for o,desc in pairs(tbl) do
			local consume = desc.consume or 0
			local incr = desc.inc or 0
			options.entries[#options.entries + 1] = {
				field = o, type = (consume > 0) and &rawstring or
				                  (incr    > 0) and uint       or bool
			}

			helpstr = helpstr .. string.format('    -%s --%s: %s\n',
				desc[1], sanitize(o), desc[2])




		end
		for o,desc in pairs(tbl) do
			local flag = desc[1]
			local consume = desc.consume or 0
			local incr = desc.inc or 0
			init[#init + 1] = quote [self].[o] = [
				(consume > 0 and `nil) or 
................................................................................
					end
				end
			elseif incr > 0 then
				ch = quote [self].[o] = [self].[o] + incr end
			else ch = quote
				[self].[o] = true
			end end

			shortcases[#shortcases + 1] = quote
				case [int8]([string.byte(flag)]) then [ch] end

			end
			longcases[#longcases + 1] = quote
				if lib.str.cmp([arg]+2, [sanitize(o)]) == 0 then [ch] goto [skip] end
			end
		end
		terra options:free() self.arglist:free() end
		options.methods.parse = terra([self], [argc], [argv])







>
|
|
>
>
>
>







 







>
|
|
>







21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
..
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
		for o,desc in pairs(tbl) do
			local consume = desc.consume or 0
			local incr = desc.inc or 0
			options.entries[#options.entries + 1] = {
				field = o, type = (consume > 0) and &rawstring or
				                  (incr    > 0) and uint       or bool
			}
			if desc[1] then
				helpstr = helpstr .. string.format('    -%s --%s: %s\n',
					desc[1], sanitize(o), desc[2])
			else
				helpstr = helpstr .. string.format('       --%s: %s\n',
					sanitize(o), desc[2])
			end
		end
		for o,desc in pairs(tbl) do
			local flag = desc[1]
			local consume = desc.consume or 0
			local incr = desc.inc or 0
			init[#init + 1] = quote [self].[o] = [
				(consume > 0 and `nil) or 
................................................................................
					end
				end
			elseif incr > 0 then
				ch = quote [self].[o] = [self].[o] + incr end
			else ch = quote
				[self].[o] = true
			end end
			if flag ~= nil then
				shortcases[#shortcases + 1] = quote
					case [int8]([string.byte(flag)]) then [ch] end
				end
			end
			longcases[#longcases + 1] = quote
				if lib.str.cmp([arg]+2, [sanitize(o)]) == 0 then [ch] goto [skip] end
			end
		end
		terra options:free() self.arglist:free() end
		options.methods.parse = terra([self], [argc], [argv])

Modified config.lua from [931922a3e1] to [3d0e5432a4].

44
45
46
47
48
49
50





51
52
53
54

55
56
57
58
59
60
61
		when = os.date();
	};
	feat = {};
	debug = u.tobool(default('parsav_enable_debug',true)); 
	backends = defaultlist('parsav_backends', 'pgsql');
	braingeniousmode = false;
	embeds = {





		{'style.css', 'text/css'};
		{'default-avatar.webp', 'image/webp'};
		{'padlock.webp', 'image/webp'};
		{'warn.webp', 'image/webp'};

	};
}
if os.getenv('parsav_let_me_be_an_idiot') == "i know what i'm doing" then
	conf.braingeniousmode = true -- SOUND GENERAL QUARTERS
end
if u.ping '.fslckout' or u.ping '_FOSSIL_' then
	if u.ping '_FOSSIL_' then default_os = 'windows' end







>
>
>
>
>




>







44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
		when = os.date();
	};
	feat = {};
	debug = u.tobool(default('parsav_enable_debug',true)); 
	backends = defaultlist('parsav_backends', 'pgsql');
	braingeniousmode = false;
	embeds = {
		-- TODO with gzip compression, svg is dramatically superior to webp
		-- we should have a build-time option to serve svg so instances
		-- proxied behind nginx can serve svgz, or possibly just straight-up
		-- add support for content-encoding headers and pre-compress the
		-- damn things before compiling
		{'style.css', 'text/css'};
		{'default-avatar.webp', 'image/webp'};
		{'padlock.webp', 'image/webp'};
		{'warn.webp', 'image/webp'};
		{'query.webp', 'image/webp'};
	};
}
if os.getenv('parsav_let_me_be_an_idiot') == "i know what i'm doing" then
	conf.braingeniousmode = true -- SOUND GENERAL QUARTERS
end
if u.ping '.fslckout' or u.ping '_FOSSIL_' then
	if u.ping '_FOSSIL_' then default_os = 'windows' end

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

1
2
3
4


5
6
7
8
9
10
11
...
182
183
184
185
186
187
188
189





















































190
-- vim: ft=terra
local m = {
	shorthand = {maxlen = 14}
}



-- swap in place -- faster on little endian
m.netswap_ip = macro(function(ty, src, dest)
	if ty:astype().type ~= 'integer' then error('bad type') end
	local bytes = ty:astype().bytes
	src = `[&uint8](src)
	dest = `[&uint8](dest)
................................................................................
		else dgtct = dgtct + 1 end
	end else
		buf = buf - 1
		@buf = 0x30
	end
	return buf
end






















































return m




>
>







 








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

1
2
3
4
5
6
7
8
9
10
11
12
13
...
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
-- vim: ft=terra
local m = {
	shorthand = {maxlen = 14}
}

local pstring = lib.mem.ptr(int8)

-- swap in place -- faster on little endian
m.netswap_ip = macro(function(ty, src, dest)
	if ty:astype().type ~= 'integer' then error('bad type') end
	local bytes = ty:astype().bytes
	src = `[&uint8](src)
	dest = `[&uint8](dest)
................................................................................
		else dgtct = dgtct + 1 end
	end else
		buf = buf - 1
		@buf = 0x30
	end
	return buf
end

terra m.ndigits(n: intptr, base: intptr): intptr
	var c = base
	var i = 1
	while true do
		if n < c then return i end
		c = c * base
		i = i + 1
	end
end

terra m.fsz_parse(f: pstring): {intptr, bool}
-- take a string representing a file size and return {nbytes, true}
-- or {0, false} if the parse fails
	if f.ct == 0 then f.ct = lib.str.sz(f.ptr) end
	var sz: intptr = 0
	for i = 0, f.ct do
		if f(i) == @',' then goto skip end
		if f(i) >= 0x30 and f(i) <= 0x39 then
			sz = sz * 10
			sz = sz + f(i) - 0x30
		else
			if i+1 == f.ct or f(i) == 0 then return sz, true end
			if i+2 == f.ct or f(i+1) == 0 then
				if f(i) == @'b' then return sz/8, true end -- bits
			else
				var s: intptr = 0
				if i+3 == f.ct or f(i+2) == 0 then 
					s = i + 1
				elseif (i+4 == f.ct or f(i+3) == 0) and f(i+1) == @'i' then
				-- grudgingly tolerate ~mebibits~ and its ilk, without
				-- affecting the result in any way
					s = i + 2
				else return 0, false end

				if f(s) == @'b' then sz = sz/8 -- bits
				elseif f(s) ~= @'B' then return 0, false end -- wth
			end
			var c = f(i)
			if c >= @'A' and c <= @'Z' then c = c - 0x20 end
			switch c do -- normal char literal syntax doesn't work here, leads to llvm error (!!)
				case [uint8]([string.byte('k')]) then return sz * [1024ULL ^ 1], true end
				case [uint8]([string.byte('m')]) then return sz * [1024ULL ^ 2], true end
				case [uint8]([string.byte('g')]) then return sz * [1024ULL ^ 3], true end
				case [uint8]([string.byte('t')]) then return sz * [1024ULL ^ 4], true end
				case [uint8]([string.byte('e')]) then return sz * [1024ULL ^ 5], true end
				case [uint8]([string.byte('y')]) then return sz * [1024ULL ^ 6], true end
				else return sz, true
			end
		end
	::skip::end
	return sz, true
end

return m

Modified mgtool.t from [03b0c72484] to [faf7450a82].

297
298
299
300
301
302
303


304
305
306
307
308
309
310
...
329
330
331
332
333
334
335
336
337
338

339
340
341
342
343
344





345
346
347
348
349
350
351
...
364
365
366
367
368
369
370
371

372
373
374
375
376
377
378

				if dbmode.arglist.ct == 1 then
					lib.bail('you are attempting to completely obliterate all data! make sure you have selected your target correctly. if you really want to do this, pass the confirmation string ', &cfmstr[0])
				elseif dbmode.arglist.ct == 2 then
					if lib.str.cmp(dbmode.arglist(1), cfmstr) == 0 then
						lib.warn('completely obliterating all data!')
						dlg:obliterate_everything()


					else
						lib.bail('you passed an incorrect confirmation string; pass ', &cfmstr[0], ' if you really want to destroy everything')
					end
				else goto cmderr end
			else goto cmderr end
		elseif lib.str.cmp(mode.arglist(0),'be') == 0 then
			srv:setup(cnf) 
................................................................................
			if cfmode.arglist.ct < 1 then goto cmderr end

			if cfmode.arglist.ct == 1 then
				if lib.str.cmp(cfmode.arglist(0),'chsec') == 0 then
					var sec: int8[65] gensec(&sec[0])
					dlg:conf_set('server-secret', &sec[0])
					lib.report('server secret reset')
					-- FIXME notify server to reload its config
				elseif lib.str.cmp(cfmode.arglist(0),'refresh') == 0 then
					-- TODO notify server to reload config

				else goto cmderr end
			elseif cfmode.arglist.ct == 3 and
				lib.str.cmp(cfmode.arglist(0),'set') == 0 then
				dlg:conf_set(cfmode.arglist(1),cfmode.arglist(2))
				lib.report('parameter set')
			else goto cmderr end





		else
			srv:setup(cnf) 
			srv:conprep(lib.store.prepmode.full)
			if lib.str.cmp(mode.arglist(0),'mkroot') == 0 then
				var cfmode: pbasic cfmode:parse(mode.arglist.ct, &mode.arglist(0))
				if cfmode.help then
					[ lib.emit(false, 1, 'usage: ', `argv[0], ' mkroot ', cfmode.type.helptxt.flags, ' <handle>', cfmode.type.helptxt.opts) ]
................................................................................
					var root = lib.store.actor.mk(&kbuf[0])
					root.handle = cfmode.arglist(0)
					var epithets = array(
						'root', 'god', 'regional jehovah', 'titan king',
						'king of olympus', 'cyberpharaoh', 'electric ellimist',
						"rampaging c'tan", 'deathless tweetlord', 'postmaster',
						'faerie queene', 'lord of the posts', 'ruthless cybercrat',
						'general secretary', 'commissar', 'kwisatz haderach'

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







>
>







 







<

<
>






>
>
>
>
>







 







|
>







297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
...
331
332
333
334
335
336
337

338

339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
...
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385

				if dbmode.arglist.ct == 1 then
					lib.bail('you are attempting to completely obliterate all data! make sure you have selected your target correctly. if you really want to do this, pass the confirmation string ', &cfmstr[0])
				elseif dbmode.arglist.ct == 2 then
					if lib.str.cmp(dbmode.arglist(1), cfmstr) == 0 then
						lib.warn('completely obliterating all data!')
						dlg:obliterate_everything()
					elseif lib.str.cmp(dbmode.arglist(1), 'print-confirmation-string') == 0 then
						lib.io.send(1, cfmstr, lib.str.sz(cfmstr))
					else
						lib.bail('you passed an incorrect confirmation string; pass ', &cfmstr[0], ' if you really want to destroy everything')
					end
				else goto cmderr end
			else goto cmderr end
		elseif lib.str.cmp(mode.arglist(0),'be') == 0 then
			srv:setup(cnf) 
................................................................................
			if cfmode.arglist.ct < 1 then goto cmderr end

			if cfmode.arglist.ct == 1 then
				if lib.str.cmp(cfmode.arglist(0),'chsec') == 0 then
					var sec: int8[65] gensec(&sec[0])
					dlg:conf_set('server-secret', &sec[0])
					lib.report('server secret reset')

				elseif lib.str.cmp(cfmode.arglist(0),'refresh') == 0 then

					cfmode.no_notify = false -- duh
				else goto cmderr end
			elseif cfmode.arglist.ct == 3 and
				lib.str.cmp(cfmode.arglist(0),'set') == 0 then
				dlg:conf_set(cfmode.arglist(1),cfmode.arglist(2))
				lib.report('parameter set')
			else goto cmderr end

			-- successful commands fall through
			if not cfmode.no_notify then
				dlg:ipc_send(lib.ipc.cmd.cfgrefresh,0)
			end
		else
			srv:setup(cnf) 
			srv:conprep(lib.store.prepmode.full)
			if lib.str.cmp(mode.arglist(0),'mkroot') == 0 then
				var cfmode: pbasic cfmode:parse(mode.arglist.ct, &mode.arglist(0))
				if cfmode.help then
					[ lib.emit(false, 1, 'usage: ', `argv[0], ' mkroot ', cfmode.type.helptxt.flags, ' <handle>', cfmode.type.helptxt.opts) ]
................................................................................
					var root = lib.store.actor.mk(&kbuf[0])
					root.handle = cfmode.arglist(0)
					var epithets = array(
						'root', 'god', 'regional jehovah', 'titan king',
						'king of olympus', 'cyberpharaoh', 'electric ellimist',
						"rampaging c'tan", 'deathless tweetlord', 'postmaster',
						'faerie queene', 'lord of the posts', 'ruthless cybercrat',
						'general secretary', 'commissar', 'kwisatz haderach',
						'dedicated hyperturing'
						-- feel free to add more
					)
					root.epithet = epithets[lib.crypt.random(intptr,0,[epithets.type.N])]
					root.rights.powers:fill() -- grant omnipotence
					root.rights.rank = 1
					var ruid = dlg:actor_create(&root)
					dlg:conf_set('master',root.handle)

Modified parsav.t from [dc97a3f269] to [022b1bf037].

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
...
273
274
275
276
277
278
279













280
281
282
283
284
285
286
...
292
293
294
295
296
297
298





















299
300
301
302
303
304
305
...
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
...
387
388
389
390
391
392
393
394
395
396

397
398
399
400
401
402

403
404
405
406
407
408
409
			var [val]
			[exp]
		in val end
		return q
	end);
	proc = {
		fork = terralib.externfunction('fork', {} -> int);
		daemonize = terralib.externfunction('daemon', {int,int} -> {});
		exit = terralib.externfunction('exit', int -> {});
		getenv = terralib.externfunction('getenv', rawstring -> rawstring);
		exec = terralib.externfunction('execv', {rawstring,&rawstring} -> int);
		execp = terralib.externfunction('execvp', {rawstring,&rawstring} -> int);
	};
	io = {
		send = terralib.externfunction('write', {int, rawstring, intptr} -> ptrdiff);
		recv = terralib.externfunction('read',  {int, rawstring, intptr} -> ptrdiff);
		close = terralib.externfunction('close', {int} -> int);
		say = macro(function(msg) return `lib.io.send(2, msg, [#(msg:asvalue())]) end);
		fmt = terralib.externfunction('printf',
			terralib.types.funcpointer({rawstring},{int},true));

	};
	str = { sz = terralib.externfunction('strlen', rawstring -> intptr) };
	copy = function(tbl)
		local new = {}
		for k,v in pairs(tbl) do new[k] = v end
		setmetatable(new, getmetatable(tbl))
		return new
................................................................................
					then lib.io.say([' - ' .. v .. ': true\n'])
					else lib.io.say([' - ' .. v .. ': false\n'])
				end
			end
		end
		return q
	end)













	set.metamethods.__add = macro(function(self,other)
		local new = symbol(set)
		local q = quote var [new] new:clear() end
		for i = 0, bytes - 1 do
			q = quote [q]
				new._store[i] = self._store[i] or other._store[i]
			end
................................................................................
		local q = quote var [new] new:clear() end
		for i = 0, bytes - 1 do
			q = quote [q]
				new._store[i] = self._store[i] and other._store[i]
			end
		end
		return quote [q] in new end





















	end)
	set.metamethods.__not = macro(function(self)
		local new = symbol(set)
		local q = quote var [new] new:clear() end
		for i = 0, bytes - 1 do
			q = quote [q]
				new._store[i] = not self._store[i] 
................................................................................
lib.md = lib.loadlib('mbedtls','mbedtls/md.h')
lib.b64 = lib.loadlib('mbedtls','mbedtls/base64.h')
lib.net = lib.loadlib('mongoose','mongoose.h')
lib.pq = lib.loadlib('libpq','libpq-fe.h')

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

	'smackdown'; -- md-alike parser
}

local be = {}
for _, b in pairs(config.backends) do
	be[#be+1] = terralib.loadfile('backend/' .. b .. '.t')()
................................................................................

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

	'render:compose';
	'render:tweet';

	'render:userpage';
	'render:timeline';

	'render:docpage';

	'render:conf:profile';

	'render:conf';
	'route';
}

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







<












>







 







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







 







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







 







|







 







<


>
|





>







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
...
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
...
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
...
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
...
421
422
423
424
425
426
427

428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
			var [val]
			[exp]
		in val end
		return q
	end);
	proc = {
		fork = terralib.externfunction('fork', {} -> int);

		exit = terralib.externfunction('exit', int -> {});
		getenv = terralib.externfunction('getenv', rawstring -> rawstring);
		exec = terralib.externfunction('execv', {rawstring,&rawstring} -> int);
		execp = terralib.externfunction('execvp', {rawstring,&rawstring} -> int);
	};
	io = {
		send = terralib.externfunction('write', {int, rawstring, intptr} -> ptrdiff);
		recv = terralib.externfunction('read',  {int, rawstring, intptr} -> ptrdiff);
		close = terralib.externfunction('close', {int} -> int);
		say = macro(function(msg) return `lib.io.send(2, msg, [#(msg:asvalue())]) end);
		fmt = terralib.externfunction('printf',
			terralib.types.funcpointer({rawstring},{int},true));
		ttyp = terralib.externfunction('isatty', int -> int);
	};
	str = { sz = terralib.externfunction('strlen', rawstring -> intptr) };
	copy = function(tbl)
		local new = {}
		for k,v in pairs(tbl) do new[k] = v end
		setmetatable(new, getmetatable(tbl))
		return new
................................................................................
					then lib.io.say([' - ' .. v .. ': true\n'])
					else lib.io.say([' - ' .. v .. ': false\n'])
				end
			end
		end
		return q
	end)
	terra set:setbit(i: intptr, val: bool)
		if val then
			self._store[i/8] = self._store[i/8] or (1 << (i % 8))
		else
			self._store[i/8] = self._store[i/8] and not (1 << (i % 8))
		end
	end
	set.bits = {}
	set.idvmap = {}
	for i,v in ipairs(tbl) do
		set.idvmap[v] = i
		set.bits[v] = quote var b: set b:clear() b:setbit(i, true) in b end
	end
	set.metamethods.__add = macro(function(self,other)
		local new = symbol(set)
		local q = quote var [new] new:clear() end
		for i = 0, bytes - 1 do
			q = quote [q]
				new._store[i] = self._store[i] or other._store[i]
			end
................................................................................
		local q = quote var [new] new:clear() end
		for i = 0, bytes - 1 do
			q = quote [q]
				new._store[i] = self._store[i] and other._store[i]
			end
		end
		return quote [q] in new end
	end)
	set.metamethods.__eq = macro(function(self,other)
		local rt = symbol(bool)
		local fb if #tbl % 8 == 0 then fb = bytes - 1 else fb = bytes - 2 end
		local q = quote rt = true end
		for i = 0, fb do
			q = quote
				if self._store[i] ~= other._store[i] then rt = false else [q] end
			end
		end
		-- we need to mask out any extraneous bits the values might have, as we
		-- don't want the kind of noise introduced by :fill() to affect comparison
		if #tbl % 8 ~= 0 then
			local last = #tbl-1
			local msk = (2 ^ (#tbl % 8)) - 1
			q = quote
				if (self._store [last] and [uint8](msk)) ~=
				   (other._store[last] and [uint8](msk)) then rt = false else [q] end
			end
		end
		return quote var [rt]; [q] in rt end
	end)
	set.metamethods.__not = macro(function(self)
		local new = symbol(set)
		local q = quote var [new] new:clear() end
		for i = 0, bytes - 1 do
			q = quote [q]
				new._store[i] = not self._store[i] 
................................................................................
lib.md = lib.loadlib('mbedtls','mbedtls/md.h')
lib.b64 = lib.loadlib('mbedtls','mbedtls/base64.h')
lib.net = lib.loadlib('mongoose','mongoose.h')
lib.pq = lib.loadlib('libpq','libpq-fe.h')

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

	'smackdown'; -- md-alike parser
}

local be = {}
for _, b in pairs(config.backends) do
	be[#be+1] = terralib.loadfile('backend/' .. b .. '.t')()
................................................................................

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

	'render:compose';
	'render:tweet';
	'render:tweet-page';
	'render:user-page';
	'render:timeline';

	'render:docpage';

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

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

Modified render/compose.t from [cb3a66bab9] to [13509724e6].

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
-- vim: ft=terra
local terra 
render_compose(co: &lib.srv.convo, edit: &lib.store.post)
	var target, tgtlen = co:getv('to')
	var form: data.view.compose
	if edit == nil then
		form = data.view.compose {
			content = lib.coalesce(target, '');
			acl = lib.trn(target == nil, 'all', 'mentioned'); -- TODO default acl setting?
			handle = co.who.handle;
			circles = ''; -- TODO: list user's circles, rooms, and saved aclexps
		}






	end


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

	var doc = data.view.docskel {
		instance = co.srv.cfg.instance;

		title = lib.str.plit 'compose';
		body = cotxt;
		class = lib.str.plit 'compose';
		navlinks = co.navbar;
	}

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

return render_compose


|


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

>
>


<
<
>



|


|
<
<
<



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
-- vim: ft=terra
local terra 
render_compose(co: &lib.srv.convo, edit: &lib.store.post, acc: &lib.str.acc)
	var target, tgtlen = co:getv('to')
	var form: data.view.compose

	form = data.view.compose {


		handle = co.who.handle;
		circles = ''; -- TODO: list user's circles, rooms, and saved aclexps
	}
	if edit == nil then
		form.content = lib.coalesce(target, '')
		form.acl = lib.trn(target == nil, 'all', 'mentioned') -- TODO default acl setting?
	else
		form.content = lib.coalesce(edit.body, '')
		form.acl = edit.acl
	end
	if acc ~= nil then form:append(acc) return end 

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



	var doc = [lib.srv.convo.page] {
		title = lib.str.plit 'compose';
		body = cotxt;
		class = lib.str.plit 'compose';
		cache = true;
	}

	co:stdpage(doc)



end

return render_compose

Modified render/conf/profile.t from [248ab207d4] to [7f970c2f4a].

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

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

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

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

return render_conf_profile







<









4
5
6
7
8
9
10

11
12
13
14
15
16
17
18
19

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

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

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

return render_conf_profile

Added render/conf/sec.t version [2ed8642241].



















































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
-- vim: ft=terra
local pstr = lib.mem.ptr(int8)
local pref = lib.mem.ref(int8)
local terra 
render_conf_sec(co: &lib.srv.convo, path: lib.mem.ptr(pref)): pstr
	var time: lib.store.timepoint = co.who.source:auth_sigtime_user_fetch(co.who.id)
	var tstr: int8[26]
	lib.osclock.ctime_r(&time, &tstr[0])
	var body = data.view.conf_sec {
		lastreset = pstr {
			ptr = &tstr[0], ct = lib.str.sz(&tstr[0])
		}
	}
	
	if co.srv.cfg.credmgd then
		var a: lib.str.acc a:init(768)
		body:append(&a)
		var credmgr = data.view.conf_sec_credmg {
			credlist = '<option>your password</option>'
		}
		credmgr:append(&a)
		return a:finalize()
	else return body:tostr() end
end
return render_conf_sec

Modified render/docpage.t from [3eda6110a6] to [148acf7303].

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

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







|







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

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

Modified render/profile.t from [19457b4b7c] to [ae13f6f2b7].

3
4
5
6
7
8
9

10
11
12
13
14





15
16
17
18
19
20
21
22
23
24
25
26
local terra cs(s: rawstring)
	return pstr { ptr = s, ct = lib.str.sz(s) }
end

local terra 
render_profile(co: &lib.srv.convo, actor: &lib.store.actor)
	var aux: lib.str.acc

	if co.aid ~= 0 and co.who.id == actor.id then
		aux:compose('<a href="/conf/profile?go=/',actor.xid,'">alter</a>')
	elseif co.aid ~= 0 then
		aux:compose('<a href="/', actor.xid, '/follow">follow</a><a href="/',
			actor.xid, '/chat">chat</a>')





		if co.who.rights.powers:affect_users() then
			aux:lpush('<a href="/'):push(actor.xid,0):lpush('/ctl">control</a>')
		end
	else
		aux:compose('<a href="/', actor.xid, '/follow">remote follow</a>')
	end
	var auxp = aux:finalize()
	var avistr: lib.str.acc if actor.origin == 0 then
		avistr:compose('/avi/',actor.handle)
	end
	var timestr: int8[26] lib.osclock.ctime_r(&actor.knownsince, &timestr[0])








>

|

<
|
>
>
>
>
>

|


|







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
local terra cs(s: rawstring)
	return pstr { ptr = s, ct = lib.str.sz(s) }
end

local terra 
render_profile(co: &lib.srv.convo, actor: &lib.store.actor)
	var aux: lib.str.acc
	var followed = true -- FIXME
	if co.aid ~= 0 and co.who.id == actor.id then
		aux:compose('<a class="button" href="/conf/profile?go=/',actor.xid,'">alter</a>')
	elseif co.aid ~= 0 then

		if not followed then
			aux:compose('<button method="post" name="act" value="follow">follow</a>')
		elseif not followed then
			aux:compose('<button method="post" name="act" value="unfollow">unfollow</a>')
		end
		aux:lpush('<a href="/'):push(actor.xid,0):lpush('/chat">chat</a>')
		if co.who.rights.powers:affect_users() then
			aux:lpush('<a class="button" href="/'):push(actor.xid,0):lpush('/ctl">control</a>')
		end
	else
		aux:compose('<a class="button" href="/', actor.xid, '/follow">remote follow</a>')
	end
	var auxp = aux:finalize()
	var avistr: lib.str.acc if actor.origin == 0 then
		avistr:compose('/avi/',actor.handle)
	end
	var timestr: int8[26] lib.osclock.ctime_r(&actor.knownsince, &timestr[0])

Added render/tweet-page.t version [ad592d16bd].





































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
-- vim: ft=terra
local pstr = lib.mem.ptr(int8)
local pref = lib.mem.ref(int8)
local terra cs(s: rawstring)
	return pstr { ptr = s, ct = lib.str.sz(s) }
end

local terra 
render_tweet_page(
	co: &lib.srv.convo,
	path: lib.mem.ptr(pref),
	p: &lib.store.post
): {}
	var pg: lib.str.acc pg:init(256)
	lib.render.tweet(co, p, &pg)
	pg:lpush('<form class="action-bar" method="post">')

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

		if co.who.rights.powers.post() then
			lib.render.compose(co, nil, &pg)
		end
	end

	var ppg = pg:finalize() defer ppg:free()
	co:stdpage([lib.srv.convo.page] {
		title = lib.str.plit 'post'; cache = false;
		class = lib.str.plit 'post'; body = ppg;
	})

	-- TODO display conversation
	-- perhaps display descendant nodes here, and have a link to the top of the whole tree?
end

return render_tweet_page

Name change from render/userpage.t to render/user-page.t.

Modified route.t from [ae96c17fe6] to [2469fad253].

10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
..
32
33
34
35
36
37
38
39
40
41


42


43
44
45
46
47
48
49
..
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
...
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
...
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
...
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
...
222
223
224
225
226
227
228

229
230
231
232
233
234
235
236
...
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
	var handle = [lib.mem.ptr(int8)] { ptr = &uri.ptr[2], ct = 0 }
	for i=2,uri.ct do
		if uri.ptr[i] == @'/' or uri.ptr[i] == 0 then handle.ct = i - 2 break end
	end
	if handle.ct == 0 then
		handle.ct = uri.ct - 2
		uri:advance(uri.ct)
	else
		if handle.ct + 2 < uri.ct then
			uri:advance(handle.ct + 2)
			--uri.ptr = uri.ptr + (handle.ct + 2)
			--uri.ct = uri.ct - (handle.ct + 2)
		end
	end

	lib.dbg('looking up user by xid "', {handle.ptr,handle.ct} ,'", path: ', {uri.ptr,uri.ct})

	var path = lib.http.hier(uri) defer path:free()
	for i=0,path.ct do
		lib.dbg('got path component ', {path.ptr[i].ptr, path.ptr[i].ct})
	end
................................................................................
	var actor = co.srv:actor_fetch_xid(handle)
	if actor.ptr == nil then
		co:complain(404,'no such user','no such user known to this server')
		return
	end
	defer actor:free()

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



terra http.actor_profile_uid(co: &lib.srv.convo, path: lib.mem.ptr(lib.mem.ref(int8)), meth: method.t)


	if path.ct < 2 then
		co:complain(404,'bad url','invalid user url')
		return
	end

	var uid, ok = lib.math.shorthand.parse(path.ptr[1].ptr, path.ptr[1].ct)
	if not ok then
................................................................................
	var actor = co.srv:actor_fetch_uid(uid)
	if actor.ptr == nil then
		co:complain(404, 'no such user', 'no user by that ID is known to this instance')
		return
	end
	defer actor:free()

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

terra http.login_form(co: &lib.srv.convo, meth: method.t)
	if meth == method.get then
		-- request a username
		lib.render.login(co, nil, nil, lib.str.plit(nil))
	elseif meth == method.post then
................................................................................
			if lib.str.ncmp('pw', am, lib.math.biggest(2,aml)) == 0 and chrs ~= nil then
				aid = co.srv:actor_auth_pw(co.peer,
					[lib.mem.ptr(int8)]{ptr=usn,ct=usnl},
					[lib.mem.ptr(int8)]{ptr=chrs,ct=chrsl})
			elseif lib.str.ncmp('otp', am, lib.math.biggest(2,aml)) == 0 and chrs ~= nil then
				lib.dbg('using otp auth')
				-- ··· --
			else
				lib.dbg('invalid auth method')
			end

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

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

	if meth == method.get then
		lib.render.compose(co, nil)
	elseif meth == method.post then
		var text, textlen = co:postv("post")
		var acl, acllen = co:postv("acl")
		var subj, subjlen = co:postv("subject")
		if text == nil or acl == nil then
			co:complain(405, 'invalid post', 'every post must have at least body text and an ACL')
			return
................................................................................
		lib.render.docpage(co,path(1))
	elseif path.ct == 1 then
		lib.render.docpage(co, rstring.null())
	else
		co:complain(404, 'no such documentation', 'invalid documentation URL')
	end
end




















































terra http.configure(co: &lib.srv.convo, path: hpath, meth: method.t)
	var msg = pstring.null()
	if meth == method.post and path.ct >= 1 then
		var user_refresh = false var fail = false
		if path(1):cmp(lib.str.lit 'profile') then

			co.who.bio = co:postv('bio')._0
			co.who.nym = co:postv('nym')._0
			if co.who.bio ~= nil and @co.who.bio == 0 then co.who.bio = nil end
			if co.who.nym ~= nil and @co.who.nym == 0 then co.who.nym = nil end
			co.who.source:actor_save(co.who)
			msg = lib.str.plit 'profile changes saved'
			--user_refresh = true -- not really necessary here, actually
		elseif path(1):cmp(lib.str.lit 'srv') then
		elseif path(1):cmp(lib.str.lit 'users') then










		end

		if user_refresh then -- refresh the user info for the renderer
			var usr = co.srv:actor_fetch_uid(co.who.id)
			lib.mem.heapf(co.who)
			co.who = usr.ptr
		end
................................................................................
					ct  = storage[([i-1])].ct;
				}
				goto [send]
			end
		end
	end
	terra http.static_content(co: &lib.srv.convo, [filename], [flen])

		var hdrs = array(lib.http.header{'Content-Type',nil})
		var [page] = lib.http.page {
			respcode = 200;
			headers = [lib.mem.ptr(lib.http.header)] {
				ptr = &hdrs[0], ct = 1
			}
		}
		[branches]
................................................................................
		if co.aid == 0
			then goto notfound
			else co:reroute_cookie('/','auth=; Path=/')
		end
		return
	else -- hierarchical routes
		var path = lib.http.hier(uri) defer path:free()
		if path.ptr[0]:cmp(lib.str.lit('user')) then
			http.actor_profile_uid(co, path, meth)


		elseif path.ptr[0]:cmp(lib.str.lit('tl')) then
			http.timeline(co, path)
		elseif path.ptr[0]:cmp(lib.str.lit('doc')) then
			if meth ~= method.get and meth ~= method.head then goto wrongmeth end
			http.documentation(co, path)
		elseif path.ptr[0]:cmp(lib.str.lit('conf')) then
			if co.aid == 0 then goto unauth end
			http.configure(co,path,meth)
		else goto notfound end
		return
	end

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







<
|
<
<
<
<
<







 







|


>
>
|
>
>







 







|







 







<
|
<





|
<
<
<
<
<
<
<







 







|







 








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





>









<
>
>
>
>
>
>
>
>
>







 







>
|







 







|

>
>
|

|


|










10
11
12
13
14
15
16

17





18
19
20
21
22
23
24
..
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
..
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
...
101
102
103
104
105
106
107

108

109
110
111
112
113
114







115
116
117
118
119
120
121
...
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
...
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
...
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
...
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
	var handle = [lib.mem.ptr(int8)] { ptr = &uri.ptr[2], ct = 0 }
	for i=2,uri.ct do
		if uri.ptr[i] == @'/' or uri.ptr[i] == 0 then handle.ct = i - 2 break end
	end
	if handle.ct == 0 then
		handle.ct = uri.ct - 2
		uri:advance(uri.ct)

	elseif handle.ct + 2 < uri.ct then uri:advance(handle.ct + 2) end






	lib.dbg('looking up user by xid "', {handle.ptr,handle.ct} ,'", path: ', {uri.ptr,uri.ct})

	var path = lib.http.hier(uri) defer path:free()
	for i=0,path.ct do
		lib.dbg('got path component ', {path.ptr[i].ptr, path.ptr[i].ct})
	end
................................................................................
	var actor = co.srv:actor_fetch_xid(handle)
	if actor.ptr == nil then
		co:complain(404,'no such user','no such user known to this server')
		return
	end
	defer actor:free()

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

terra http.actor_profile_uid (
	co: &lib.srv.convo,
	path: lib.mem.ptr(lib.mem.ref(int8)),
	meth: method.t
)
	if path.ct < 2 then
		co:complain(404,'bad url','invalid user url')
		return
	end

	var uid, ok = lib.math.shorthand.parse(path.ptr[1].ptr, path.ptr[1].ct)
	if not ok then
................................................................................
	var actor = co.srv:actor_fetch_uid(uid)
	if actor.ptr == nil then
		co:complain(404, 'no such user', 'no user by that ID is known to this instance')
		return
	end
	defer actor:free()

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

terra http.login_form(co: &lib.srv.convo, meth: method.t)
	if meth == method.get then
		-- request a username
		lib.render.login(co, nil, nil, lib.str.plit(nil))
	elseif meth == method.post then
................................................................................
			if lib.str.ncmp('pw', am, lib.math.biggest(2,aml)) == 0 and chrs ~= nil then
				aid = co.srv:actor_auth_pw(co.peer,
					[lib.mem.ptr(int8)]{ptr=usn,ct=usnl},
					[lib.mem.ptr(int8)]{ptr=chrs,ct=chrsl})
			elseif lib.str.ncmp('otp', am, lib.math.biggest(2,aml)) == 0 and chrs ~= nil then
				lib.dbg('using otp auth')
				-- ··· --

			else lib.dbg('invalid auth method') end


			-- error out
			if aid == 0 then
				lib.render.login(co, nil, nil, lib.str.plit 'authentication failure')
			else
				co:installkey('/',aid)







			end
		end
		if act.ptr ~= nil and fakeact == false then act:free() end
	else
		::wrongmeth:: co:complain(405, 'method not allowed', 'that method is not meaningful for this endpoint') do return end
	end
	return
................................................................................

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

	if meth == method.get then
		lib.render.compose(co, nil, nil)
	elseif meth == method.post then
		var text, textlen = co:postv("post")
		var acl, acllen = co:postv("acl")
		var subj, subjlen = co:postv("subject")
		if text == nil or acl == nil then
			co:complain(405, 'invalid post', 'every post must have at least body text and an ACL')
			return
................................................................................
		lib.render.docpage(co,path(1))
	elseif path.ct == 1 then
		lib.render.docpage(co, rstring.null())
	else
		co:complain(404, 'no such documentation', 'invalid documentation URL')
	end
end

terra http.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)
	if not post then
		co:complain(404, 'post not found', 'no such post is known to this server')
		return
	end
	defer post:free()

	if path.ct == 3 then
		if path(2):cmp(lib.str.lit 'edit') then
			if post(0).author ~= co.who.id then
				co:complain(403, 'forbidden', 'you cannot edit other people\'s posts')
				return
			end

			if meth == method.get 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)

				var lnk: lib.str.acc lnk:compose('/post/', path(1))
				co:reroute(lnk.buf)
				lnk:free()
			end
			return
		else goto badurl end
	end

	if meth == method.post then
		co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful')
		return
	end

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

	::badurl:: co:complain(404, 'invalid URL', 'this URL does not reference extant content or functionality')
end

terra http.configure(co: &lib.srv.convo, path: hpath, meth: method.t)
	var msg = pstring.null()
	if meth == method.post and path.ct >= 1 then
		var user_refresh = false var fail = false
		if path(1):cmp(lib.str.lit 'profile') then
			lib.dbg('updating profile')
			co.who.bio = co:postv('bio')._0
			co.who.nym = co:postv('nym')._0
			if co.who.bio ~= nil and @co.who.bio == 0 then co.who.bio = nil end
			if co.who.nym ~= nil and @co.who.nym == 0 then co.who.nym = nil end
			co.who.source:actor_save(co.who)
			msg = lib.str.plit 'profile changes saved'
			--user_refresh = true -- not really necessary here, actually
		elseif path(1):cmp(lib.str.lit 'srv') then
		elseif path(1):cmp(lib.str.lit 'users') then

		elseif path(1):cmp(lib.str.lit 'sec') then
			var act = co:ppostv('act')
			if act:cmp(lib.str.plit 'invalidate') then
				lib.dbg('setting user\'s cookie validation time to now')
				co.who.source:auth_sigtime_user_alter(co.who.id, lib.osclock.time(nil))
				-- the current session has been invalidated as well, so we need to immediately install a new authentication cookie with the same aid so the user doesn't need to log back in all over again
				co:installkey('/conf/sec',co.aid)
				return
			end
		end

		if user_refresh then -- refresh the user info for the renderer
			var usr = co.srv:actor_fetch_uid(co.who.id)
			lib.mem.heapf(co.who)
			co.who = usr.ptr
		end
................................................................................
					ct  = storage[([i-1])].ct;
				}
				goto [send]
			end
		end
	end
	terra http.static_content(co: &lib.srv.convo, [filename], [flen])
		var hdrs = array(
		lib.http.header{'Content-Type',nil})
		var [page] = lib.http.page {
			respcode = 200;
			headers = [lib.mem.ptr(lib.http.header)] {
				ptr = &hdrs[0], ct = 1
			}
		}
		[branches]
................................................................................
		if co.aid == 0
			then goto notfound
			else co:reroute_cookie('/','auth=; Path=/')
		end
		return
	else -- hierarchical routes
		var path = lib.http.hier(uri) defer path:free()
		if path.ct > 1 and path(0):cmp(lib.str.lit('user')) then
			http.actor_profile_uid(co, path, meth)
		elseif path.ct > 1 and path(0):cmp(lib.str.lit('post')) then
			http.tweet_page(co, path, meth)
		elseif path(0):cmp(lib.str.lit('tl')) then
			http.timeline(co, path)
		elseif path(0):cmp(lib.str.lit('doc')) then
			if meth ~= method.get and meth ~= method.head then goto wrongmeth end
			http.documentation(co, path)
		elseif path(0):cmp(lib.str.lit('conf')) then
			if co.aid == 0 then goto unauth end
			http.configure(co,path,meth)
		else goto notfound end
		return
	end

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

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

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
		[lib.mem.ptr(uint8)] {ptr = [&uint8](secret.ptr), ct = secret.ct},
		[lib.mem.ptr( int8)] {ptr = out, ct = len},
	&hash[0])
	ptr = ptr + lib.math.shorthand.gen(lib.math.truncate64(hash, [hash.type.N]), ptr)
	return ptr - out
end

terra m.cookie_interpret(secret: lib.mem.ptr(int8), c: lib.mem.ptr(int8), now: uint64): uint64 -- returns either 0 or a valid authid
	var authid_sz = lib.str.cspan(c.ptr, lib.str.lit '.', c.ct)
	if authid_sz == 0 then return 0 end
	if authid_sz + 1 > c.ct then return 0 end
	var time_sz = lib.str.cspan(c.ptr+authid_sz+1, lib.str.lit '.', c.ct - (authid_sz+1))
	if time_sz == 0 then return 0 end
	if (authid_sz + time_sz + 2) > c.ct then return 0 end
	var hash_sz = c.ct - (authid_sz + time_sz + 2)

	var knownhash: uint8[lib.crypt.algsz.sha256]
	lib.crypt.hmac(lib.crypt.alg.sha256,
		[lib.mem.ptr(uint8)] {ptr = [&uint8](secret.ptr), ct = secret.ct},
		[lib.mem.ptr( int8)] {ptr = c.ptr, ct = c.ct - hash_sz},
	&knownhash[0])

	var authid, authok = lib.math.shorthand.parse(c.ptr, authid_sz)
	var time, timeok = lib.math.shorthand.parse(c.ptr + authid_sz + 1, time_sz)
	var hash, hashok = lib.math.shorthand.parse(c.ptr + c.ct - hash_sz, hash_sz)
	if not (timeok and authok and hashok) then return 0 end
	if lib.math.truncate64(knownhash, [knownhash.type.N]) ~= hash then return 0 end
	if now - time > m.maxage then return 0 end

	return authid
end

return m







|

|
|

|
|











|
|
|

|



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
		[lib.mem.ptr(uint8)] {ptr = [&uint8](secret.ptr), ct = secret.ct},
		[lib.mem.ptr( int8)] {ptr = out, ct = len},
	&hash[0])
	ptr = ptr + lib.math.shorthand.gen(lib.math.truncate64(hash, [hash.type.N]), ptr)
	return ptr - out
end

terra m.cookie_interpret(secret: lib.mem.ptr(int8), c: lib.mem.ptr(int8), now: uint64) -- returns either 0,0 or a valid {authid, timepoint}
	var authid_sz = lib.str.cspan(c.ptr, lib.str.lit '.', c.ct)
	if authid_sz == 0 then return 0,0 end
	if authid_sz + 1 > c.ct then return 0,0 end
	var time_sz = lib.str.cspan(c.ptr+authid_sz+1, lib.str.lit '.', c.ct - (authid_sz+1))
	if time_sz == 0 then return 0,0 end
	if (authid_sz + time_sz + 2) > c.ct then return 0,0 end
	var hash_sz = c.ct - (authid_sz + time_sz + 2)

	var knownhash: uint8[lib.crypt.algsz.sha256]
	lib.crypt.hmac(lib.crypt.alg.sha256,
		[lib.mem.ptr(uint8)] {ptr = [&uint8](secret.ptr), ct = secret.ct},
		[lib.mem.ptr( int8)] {ptr = c.ptr, ct = c.ct - hash_sz},
	&knownhash[0])

	var authid, authok = lib.math.shorthand.parse(c.ptr, authid_sz)
	var time, timeok = lib.math.shorthand.parse(c.ptr + authid_sz + 1, time_sz)
	var hash, hashok = lib.math.shorthand.parse(c.ptr + c.ct - hash_sz, hash_sz)
	if not (timeok and authok and hashok) then return 0,0 end
	if lib.math.truncate64(knownhash, [knownhash.type.N]) ~= hash then return 0,0 end
	if now - time > m.maxage then return 0,0 end

	return authid, time
end

return m

Modified srv.t from [4721bcd333] to [9e0d1b7489].

1
2
3
4
5
6
7
8
9
10
11




12
13
14
15
16
17
18
...
108
109
110
111
112
113
114

115
116
117
118
119
120
121
...
154
155
156
157
158
159
160











161
162
163
164
165
166
167
...
232
233
234
235
236
237
238




239
240
241
242
243
244
245
...
247
248
249
250
251
252
253




254
255
256
257
258
259
260
...
303
304
305
306
307
308
309
310
311

312
313
314
315
316
317
318
...
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
...
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655




















656
657
658
659
660
661
662
-- vim: ft=terra
local util = lib.util
local secmode = lib.enum { 'public', 'private', 'lockdown', 'isolate' }
local pstring = lib.mem.ptr(int8)
local struct srv
local struct cfgcache {
	secret: lib.mem.ptr(int8)
	instance: lib.mem.ptr(int8)
	overlord: &srv
	pol_sec: secmode.t
	pol_reg: bool




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

local struct convo {
	srv: &srv
	con: &lib.net.mg_connection
	msg: &lib.net.mg_http_message
	aid: uint64 -- 0 if logged out

	who: &lib.store.actor -- who we're logged in as, if aid ~= 0
	peer: lib.store.inet
	reqtype: lib.http.mime.t -- negotiated content type
-- cache
	navbar: lib.mem.ptr(int8)
	actorcache: lib.mem.cache(lib.mem.ptr(lib.store.actor),32) -- naive cache to avoid unnecessary queries
-- private
................................................................................

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

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











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

................................................................................
		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:getv(name: rawstring)
	if self.varbuf.ptr == nil then
		self.varbuf = lib.mem.heapa(int8, self.msg.query.len + self.msg.body.len)
		self.vbofs = self.varbuf.ptr
	end
	var o = lib.net.mg_http_get_var(&self.msg.query, name, self.vbofs, self.varbuf.ct - (self.vbofs - self.varbuf.ptr))
................................................................................
		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





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

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

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

				} co.varbuf.ptr = nil
				  co.navbar.ptr = nil
				  co.actorcache.top = 0
				  co.actorcache.cur = 0

				-- first, check for an accept header. if it's there, we need to
				-- iterate over the values and pick the highest-priority one
................................................................................
					end
					if val.ptr == nil then goto nocookie end
					val.ct = (cookies + i) - val.ptr
					if lib.str.ncmp(key.ptr, lib.session.cookiename, lib.math.biggest([#lib.session.cookiename], key.ct)) ~= 0 then
						goto nocookie
					end
					::foundcookie:: do
						var aid = lib.session.cookie_interpret(server.cfg.secret,
							[lib.mem.ptr(int8)]{ptr=val.ptr,ct=val.ct},
							lib.osclock.time(nil))
						if aid ~= 0 then co.aid = aid end
					end ::nocookie::;
				end

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

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

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

	self.pol_reg = false
	var sreg = self.overlord:conf_get('policy-self-register')
	if sreg.ptr ~= nil then
		if lib.str.cmp(sreg.ptr, 'on') == 0
			then self.pol_reg = true
			else self.pol_reg = false
		end
	end
	sreg:free()




















	
	self.pol_sec = secmode.lockdown
	var smode = self.overlord:conf_get('policy-security')
	if smode.ptr ~= nil then
		if lib.str.cmp(smode.ptr, 'public') == 0 then
			self.pol_sec = secmode.public
		elseif lib.str.cmp(smode.ptr, 'private') == 0 then







<
<


>
>
>
>







 







>







 







>
>
>
>
>
>
>
>
>
>
>







 







>
>
>
>







 







>
>
>
>







 







|

>







 







|


|




|
|







 







|

|





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







1
2
3
4
5
6
7


8
9
10
11
12
13
14
15
16
17
18
19
20
...
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
...
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
...
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
...
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
...
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
...
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
...
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
-- vim: ft=terra
local util = lib.util
local secmode = lib.enum { 'public', 'private', 'lockdown', 'isolate' }
local pstring = lib.mem.ptr(int8)
local struct srv
local struct cfgcache {
	secret: lib.mem.ptr(int8)


	pol_sec: secmode.t
	pol_reg: bool
	credmgd: bool
	maxupsz: intptr
	instance: lib.mem.ptr(int8)
	overlord: &srv
}
local struct srv {
	sources: lib.mem.ptr(lib.store.source)
	webmgr: lib.net.mg_mgr
	webcon: &lib.net.mg_connection
	cfg: cfgcache
	id: rawstring
................................................................................
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
-- cache
	navbar: lib.mem.ptr(int8)
	actorcache: lib.mem.cache(lib.mem.ptr(lib.store.actor),32) -- naive cache to avoid unnecessary queries
-- private
................................................................................

	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:complain(code: uint16, title: rawstring, msg: rawstring)
	var hdrs = array(
		lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' },
		lib.http.header { key = 'Cache-Control', value = 'no-store' }
	)

................................................................................
		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:ppostv(name: rawstring)
	var s,l = self:postv(name)
	return pstring { ptr = s, ct = l }
end

terra convo:getv(name: rawstring)
	if self.varbuf.ptr == nil then
		self.varbuf = lib.mem.heapa(int8, self.msg.query.len + self.msg.body.len)
		self.vbofs = self.varbuf.ptr
	end
	var o = lib.net.mg_http_get_var(&self.msg.query, name, self.vbofs, self.varbuf.ct - (self.vbofs - self.varbuf.ptr))
................................................................................
		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 urimatch = macro(function(uri, ptn)
	return `lib.net.mg_globmatch(ptn, [#ptn], uri.ptr, uri.ct+1)
end)

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

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

				-- first, check for an accept header. if it's there, we need to
				-- iterate over the values and pick the highest-priority one
................................................................................
					end
					if val.ptr == nil then goto nocookie end
					val.ct = (cookies + i) - val.ptr
					if lib.str.ncmp(key.ptr, lib.session.cookiename, lib.math.biggest([#lib.session.cookiename], key.ct)) ~= 0 then
						goto nocookie
					end
					::foundcookie:: do
						var aid, tp = lib.session.cookie_interpret(server.cfg.secret,
							[lib.mem.ptr(int8)]{ptr=val.ptr,ct=val.ct},
							lib.osclock.time(nil))
						if aid ~= 0 then co.aid = aid co.aid_issue = tp end
					end ::nocookie::;
				end

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

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

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

	do self.pol_reg = false
	var sreg = self.overlord:conf_get('policy-self-register')
	if sreg:ref() then
		if lib.str.cmp(sreg.ptr, 'on') == 0
			then self.pol_reg = true
			else self.pol_reg = false
		end
	end
	sreg:free() end

	do self.credmgd = false
	var sreg = self.overlord:conf_get('credential-store')
	if sreg:ref() then
		if lib.str.cmp(sreg.ptr, 'managed') == 0
			then self.credmgd = true
			else self.credmgd = false
		end
	end
	sreg:free() end

	do self.maxupsz = [1024 * 100] -- 100 kilobyte default
	var sreg = self.overlord:conf_get('maximum-artifact-size')
	if sreg:ref() then
		var sz, ok = lib.math.fsz_parse(sreg)
		if ok then self.maxupsz = sz else
			lib.warn('invalid configuration value for maximum-artifact-size; keeping default 100K upload limit')
		end
	end
	sreg:free() end
	
	self.pol_sec = secmode.lockdown
	var smode = self.overlord:conf_get('policy-security')
	if smode.ptr ~= nil then
		if lib.str.cmp(smode.ptr, 'public') == 0 then
			self.pol_sec = secmode.public
		elseif lib.str.cmp(smode.ptr, 'private') == 0 then

Added static/query.svg version [eb8b842615].



















































































































































































































































































































































































































































































































































































































































































































































































































































































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

<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:xlink="http://www.w3.org/1999/xlink"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   width="1in"
   height="1in"
   viewBox="0 0 25.4 25.400001"
   version="1.1"
   id="svg8"
   inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
   sodipodi:docname="query.svg"
   inkscape:export-filename="/home/lexi/dev/parsav/static/warn.png"
   inkscape:export-xdpi="200"
   inkscape:export-ydpi="200">
  <defs
     id="defs2">
    <linearGradient
       id="linearGradient1362"
       inkscape:collect="always">
      <stop
         id="stop1358"
         offset="0"
         style="stop-color:#b64bc7;stop-opacity:1" />
      <stop
         id="stop1360"
         offset="1"
         style="stop-color:#7932a4;stop-opacity:1" />
    </linearGradient>
    <linearGradient
       inkscape:collect="always"
       id="linearGradient973">
      <stop
         style="stop-color:#9b3bd7;stop-opacity:1;"
         offset="0"
         id="stop969" />
      <stop
         style="stop-color:#5d267e;stop-opacity:1"
         offset="1"
         id="stop971" />
    </linearGradient>
    <linearGradient
       inkscape:collect="always"
       id="linearGradient2322">
      <stop
         style="stop-color:#ca0050;stop-opacity:1;"
         offset="0"
         id="stop2318" />
      <stop
         style="stop-color:#7b0031;stop-opacity:1"
         offset="1"
         id="stop2320" />
    </linearGradient>
    <linearGradient
       inkscape:collect="always"
       id="linearGradient2302">
      <stop
         style="stop-color:#ffffff;stop-opacity:1;"
         offset="0"
         id="stop2298" />
      <stop
         style="stop-color:#ffffff;stop-opacity:0;"
         offset="1"
         id="stop2300" />
    </linearGradient>
    <linearGradient
       inkscape:collect="always"
       id="linearGradient851">
      <stop
         style="stop-color:#ffffff;stop-opacity:0"
         offset="0"
         id="stop847" />
      <stop
         style="stop-color:#ffa9c6;stop-opacity:1"
         offset="1"
         id="stop849" />
    </linearGradient>
    <radialGradient
       inkscape:collect="always"
       xlink:href="#linearGradient851"
       id="radialGradient853"
       cx="12.699999"
       cy="285.82184"
       fx="12.699999"
       fy="285.82184"
       r="1.7905753"
       gradientTransform="matrix(2.2137788,-2.5697531e-5,6.7771541e-5,5.8383507,-15.43436,-1382.906)"
       gradientUnits="userSpaceOnUse" />
    <radialGradient
       inkscape:collect="always"
       xlink:href="#linearGradient2302"
       id="radialGradient2304"
       cx="7.2493401"
       cy="278.65524"
       fx="7.2493401"
       fy="278.65524"
       r="10.573204"
       gradientTransform="matrix(1.1874875,-1.9679213e-8,1.9699479e-8,1.1887104,-2.3813503,-53.649456)"
       gradientUnits="userSpaceOnUse" />
    <linearGradient
       inkscape:collect="always"
       xlink:href="#linearGradient2322"
       id="linearGradient2324"
       x1="20.298828"
       y1="97.196846"
       x2="20.298828"
       y2="12.911131"
       gradientUnits="userSpaceOnUse"
       gradientTransform="translate(1.3858268e-6)" />
    <radialGradient
       inkscape:collect="always"
       xlink:href="#linearGradient2302"
       id="radialGradient2304-8"
       cx="12.58085"
       cy="285.25314"
       fx="12.58085"
       fy="285.25314"
       r="10.573204"
       gradientTransform="matrix(4.4881418,-7.4378129e-8,7.4454725e-8,4.4927638,-17.038663,-1237.2864)"
       gradientUnits="userSpaceOnUse" />
    <radialGradient
       inkscape:collect="always"
       xlink:href="#linearGradient2302"
       id="radialGradient960"
       gradientUnits="userSpaceOnUse"
       gradientTransform="matrix(0.77290068,-0.9015271,0.4066395,0.34862174,-111.85097,215.11933)"
       cx="23.48217"
       cy="269.32919"
       fx="23.48217"
       fy="269.32919"
       r="10.573204" />
    <linearGradient
       inkscape:collect="always"
       xlink:href="#linearGradient973"
       id="linearGradient975"
       x1="5.8321538"
       y1="275.4801"
       x2="21.037828"
       y2="292.65216"
       gradientUnits="userSpaceOnUse"
       gradientTransform="matrix(3.7795276,0,0,3.7795276,0,-1026.5196)" />
    <linearGradient
       inkscape:collect="always"
       xlink:href="#linearGradient973"
       id="linearGradient1014"
       gradientUnits="userSpaceOnUse"
       x1="5.8321538"
       y1="275.4801"
       x2="21.037828"
       y2="292.65216"
       gradientTransform="translate(-42.333335)" />
    <radialGradient
       inkscape:collect="always"
       xlink:href="#linearGradient2302"
       id="radialGradient1016"
       gradientUnits="userSpaceOnUse"
       gradientTransform="matrix(1.1874875,-1.9679213e-8,1.9699479e-8,1.1887104,-44.714686,-53.649456)"
       cx="7.2493401"
       cy="278.65524"
       fx="7.2493401"
       fy="278.65524"
       r="10.573204" />
    <radialGradient
       inkscape:collect="always"
       xlink:href="#linearGradient2302"
       id="radialGradient1018"
       gradientUnits="userSpaceOnUse"
       gradientTransform="matrix(0.77290068,-0.9015271,0.4066395,0.34862174,-154.18433,215.11933)"
       cx="23.48217"
       cy="269.32919"
       fx="23.48217"
       fy="269.32919"
       r="10.573204" />
    <filter
       inkscape:collect="always"
       style="color-interpolation-filters:sRGB"
       id="filter1222"
       x="-0.12898994"
       width="1.2579799"
       y="-0.05840071"
       height="1.1168014">
      <feGaussianBlur
         inkscape:collect="always"
         stdDeviation="0.33911411"
         id="feGaussianBlur1224" />
    </filter>
    <filter
       inkscape:collect="always"
       style="color-interpolation-filters:sRGB"
       id="filter1352"
       x="-0.15389959"
       width="1.3077992"
       y="-0.077627083"
       height="1.1552542">
      <feGaussianBlur
         inkscape:collect="always"
         stdDeviation="0.50074934"
         id="feGaussianBlur1354" />
    </filter>
    <radialGradient
       inkscape:collect="always"
       xlink:href="#linearGradient1362"
       id="radialGradient1356"
       cx="6.5823746"
       cy="278.22546"
       fx="6.5823746"
       fy="278.22546"
       r="9.8748493"
       gradientTransform="matrix(2.0818304,0,0,2.0818304,-8.516817,-303.27685)"
       gradientUnits="userSpaceOnUse" />
    <meshgradient
       y="277.31741"
       x="5.7174305"
       gradientUnits="userSpaceOnUse"
       id="meshgradient5802"
       inkscape:collect="always">
      <meshrow
         id="meshrow5804">
        <meshpatch
           id="meshpatch5806">
          <stop
             path="c 3.85637,-3.85637  10.1088,-3.85637  13.9651,0"
             style="stop-color:#eccaff;stop-opacity:1"
             id="stop5808" />
          <stop
             path="c 3.85637,3.85637  3.85637,10.1088  0,13.9651"
             style="stop-color:#800080;stop-opacity:1"
             id="stop5810" />
          <stop
             path="c -3.85637,3.85637  -10.1088,3.85637  -13.9651,0"
             style="stop-color:#ef9aff;stop-opacity:1"
             id="stop5812" />
          <stop
             path="c -3.85637,-3.85637  -3.85637,-10.1088  0,-13.9651"
             style="stop-color:#800080;stop-opacity:1"
             id="stop5814" />
        </meshpatch>
      </meshrow>
    </meshgradient>
  </defs>
  <sodipodi:namedview
     id="base"
     pagecolor="#313131"
     bordercolor="#666666"
     borderopacity="1.0"
     inkscape:pageopacity="0"
     inkscape:pageshadow="2"
     inkscape:zoom="3.959798"
     inkscape:cx="81.587655"
     inkscape:cy="1.9529273"
     inkscape:document-units="mm"
     inkscape:current-layer="layer1"
     showgrid="false"
     units="in"
     inkscape:window-width="1920"
     inkscape:window-height="1042"
     inkscape:window-x="0"
     inkscape:window-y="38"
     inkscape:window-maximized="0" />
  <metadata
     id="metadata5">
    <rdf:RDF>
      <cc:Work
         rdf:about="">
        <dc:format>image/svg+xml</dc:format>
        <dc:type
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
        <dc:title></dc:title>
      </cc:Work>
    </rdf:RDF>
  </metadata>
  <g
     inkscape:label="Layer 1"
     inkscape:groupmode="layer"
     id="layer1"
     transform="translate(0,-271.59998)">
    <circle
       style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:url(#radialGradient2304);stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
       id="path910"
       cx="12.7"
       cy="284.29999"
       r="9.8746281" />
    <path
       id="rect829"
       style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.26499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
       d="m 11.543949,290.50847 -7e-6,0.52521 0.893337,0.89333 0.525444,2.4e-4 0.893329,-0.89333 -2.34e-4,-0.52545 -0.893338,-0.89333 h -0.525203 z"
       inkscape:connector-curvature="0"
       sodipodi:nodetypes="ccccccccc" />
    <g
       aria-label="?"
       style="font-style:normal;font-weight:normal;font-size:10.58333302px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.12330705;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
       id="text877"
       transform="matrix(2.1597213,0,0,2.1385442,0.73403254,-313.27713)">
      <path
         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Averia Serif Libre';-inkscape-font-specification:'Averia Serif Libre';vector-effect:none;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.12330705;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
         d="m 7.6924933,277.43408 c 0.1165795,0.95131 -0.6562552,1.66432 -1.3319477,2.19936 -0.6322781,0.5146 -0.3926261,1.03197 -0.696752,1.4212 -0.7264746,0.20404 -0.708357,-0.94298 -0.5084803,-1.3771 0.3220556,-0.77195 1.5189026,-1.18082 1.6106521,-2.0731 0.1697248,-0.85219 -1.0638516,-1.33257 -1.6164509,-0.77546 -0.4346018,0.40863 -1.0937506,0.72692 -1.0597346,-0.0803 0.061275,-0.68615 0.5563323,-0.90025 1.144623,-0.96295 0.9913021,-0.15129 2.2140894,0.34092 2.4316458,1.40994 l 0.018295,0.11853 z"
         id="path884"
         inkscape:connector-curvature="0"
         sodipodi:nodetypes="ccccccccccc" />
    </g>
    <path
       style="opacity:1;vector-effect:none;fill:url(#meshgradient5802);fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
       d="M 12.7,274.42513 A 9.874628,9.874628 0 0 0 2.8251504,284.29998 9.874628,9.874628 0 0 0 12.7,294.17483 9.874628,9.874628 0 0 0 22.574849,284.29998 9.874628,9.874628 0 0 0 12.7,274.42513 Z m 0.160197,2.02055 c 1.946164,0.016 4.019091,1.07178 4.43022,3.07216 l 0.03979,0.25373 0.01757,0.25632 c 0.251779,2.03442 -1.41752,3.55938 -2.876827,4.70359 -1.365545,1.10049 -0.847991,2.2067 -1.504818,3.03909 -1.568983,0.43635 -1.529802,-2.01666 -1.098124,-2.94504 0.69555,-1.65085 3.280704,-2.52514 3.478857,-4.43332 0.366559,-1.82245 -2.297799,-2.8497 -3.491259,-1.6583 -0.938619,0.87387 -2.3622147,1.55471 -2.2887497,-0.17156 0.132337,-1.46737 1.2016567,-1.92522 2.4722007,-2.05931 0.267617,-0.0404 0.543115,-0.0596 0.821139,-0.0574 z m -0.422714,13.16922 h 0.525033 l 0.893485,0.89349 v 0.52555 l -0.893485,0.89348 -0.525033,-5.2e-4 -0.893485,-0.89348 v -0.52503 z"
       id="circle967"
       inkscape:connector-curvature="0" />
    <circle
       r="9.8746281"
       cy="284.29999"
       cx="12.7"
       id="circle958"
       style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:url(#radialGradient960);stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
    <g
       aria-label="?"
       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:31.11434174px;line-height:1.25;font-family:'Averia Serif Libre';-inkscape-font-specification:'Averia Serif Libre';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:0.26458335"
       id="text979" />
    <circle
       r="9.8746281"
       cy="284.29999"
       cx="-29.633333"
       id="circle998"
       style="opacity:1;vector-effect:none;fill:url(#linearGradient1014);fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
    <g
       transform="translate(-42.333335,-0.90874193)"
       id="g1006">
      <path
         id="path1000"
         style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.26499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
         d="m 11.543949,291.41721 -7e-6,0.52521 0.893337,0.89333 0.525444,2.4e-4 0.893329,-0.89333 -2.34e-4,-0.52545 -0.893338,-0.89333 h -0.525203 z"
         inkscape:connector-curvature="0"
         sodipodi:nodetypes="ccccccccc" />
      <g
         aria-label="?"
         style="font-style:normal;font-weight:normal;font-size:10.58333302px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.12330705;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
         id="g1004"
         transform="matrix(2.1597213,0,0,2.1385442,0.73403254,-312.36839)">
        <path
           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Averia Serif Libre';-inkscape-font-specification:'Averia Serif Libre';vector-effect:none;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.12330705;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
           d="m 7.6924933,277.43408 c 0.1165795,0.95131 -0.6562552,1.66432 -1.3319477,2.19936 -0.6322781,0.5146 -0.3926261,1.03197 -0.696752,1.4212 -0.7264746,0.20404 -0.708357,-0.94298 -0.5084803,-1.3771 0.3220556,-0.77195 1.5189026,-1.18082 1.6106521,-2.0731 0.1697248,-0.85219 -1.0638516,-1.33257 -1.6164509,-0.77546 -0.4346018,0.40863 -1.0937506,0.72692 -1.0597346,-0.0803 0.061275,-0.68615 0.5563323,-0.90025 1.144623,-0.96295 0.9913021,-0.15129 2.2140894,0.34092 2.4316458,1.40994 l 0.018295,0.11853 z"
           id="path1002"
           inkscape:connector-curvature="0"
           sodipodi:nodetypes="ccccccccccc" />
      </g>
    </g>
    <circle
       r="9.8746281"
       cy="284.29999"
       cx="-29.633333"
       id="circle1008"
       style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:url(#radialGradient1016);stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
    <circle
       style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:url(#radialGradient1018);stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
       id="circle1010"
       cx="-29.633333"
       cy="284.29999"
       r="9.8746281" />
    <g
       id="g1012"
       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:31.11434174px;line-height:1.25;font-family:'Averia Serif Libre';-inkscape-font-specification:'Averia Serif Libre';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:0.26458335"
       aria-label="?"
       transform="translate(-42.333335)" />
    <g
       transform="matrix(2.1597213,0,0,2.1385442,0.73403254,-313.27713)"
       id="g1024"
       style="font-style:normal;font-weight:normal;font-size:10.58333302px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.12330705;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
       aria-label="?" />
    <g
       transform="translate(-3.3333335e-8,-0.90874193)"
       id="g1039"
       style="opacity:0.64600004;filter:url(#filter1352)">
      <path
         id="path1033"
         style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.26499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
         d="m 11.543949,291.41721 -7e-6,0.52521 0.893337,0.89333 0.525444,2.4e-4 0.893329,-0.89333 -2.34e-4,-0.52545 -0.893338,-0.89333 h -0.525203 z"
         inkscape:connector-curvature="0"
         sodipodi:nodetypes="ccccccccc" />
      <g
         aria-label="?"
         style="font-style:normal;font-weight:normal;font-size:10.58333302px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.12330705;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
         id="g1037"
         transform="matrix(2.1597213,0,0,2.1385442,0.73403254,-312.36839)">
        <path
           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Averia Serif Libre';-inkscape-font-specification:'Averia Serif Libre';vector-effect:none;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.12330705;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
           d="m 7.6924933,277.43408 c 0.1165795,0.95131 -0.6562552,1.66432 -1.3319477,2.19936 -0.6322781,0.5146 -0.3926261,1.03197 -0.696752,1.4212 -0.7264746,0.20404 -0.708357,-0.94298 -0.5084803,-1.3771 0.3220556,-0.77195 1.5189026,-1.18082 1.6106521,-2.0731 0.1697248,-0.85219 -1.0638516,-1.33257 -1.6164509,-0.77546 -0.4346018,0.40863 -1.0937506,0.72692 -1.0597346,-0.0803 0.061275,-0.68615 0.5563323,-0.90025 1.144623,-0.96295 0.9913021,-0.15129 2.2140894,0.34092 2.4316458,1.40994 l 0.018295,0.11853 z"
           id="path1035"
           inkscape:connector-curvature="0"
           sodipodi:nodetypes="ccccccccccc" />
      </g>
    </g>
    <path
       d="m 12.150391,277.24219 a 0.748792,0.748792 0 0 1 -0.0332,0.004 c -0.568389,0.06 -1.027224,0.19774 -1.306641,0.39649 -0.277209,0.19717 -0.442118,0.42727 -0.494141,0.97851 -7.25e-4,0.16027 0.0015,0.16112 0.01172,0.19922 0.194129,-0.0613 0.650675,-0.29317 1.017578,-0.63476 l -0.01953,0.0195 c 0.869948,-0.86845 2.158273,-0.89534 3.158203,-0.45899 0.990823,0.43239 1.8253,1.49756 1.589844,2.76368 -0.148683,1.24943 -1.014306,2.08321 -1.78711,2.75976 -0.782208,0.68479 -1.493175,1.28446 -1.730468,1.84766 a 0.748792,0.748792 0 0 1 -0.0098,0.0254 c -0.113045,0.24312 -0.238287,1.0468 -0.144531,1.52344 0.02878,0.14631 0.0631,0.17601 0.09961,0.24218 0.06761,-0.20148 0.132148,-0.34938 0.222656,-0.73047 0.140154,-0.5901 0.460972,-1.37295 1.275391,-2.02929 a 0.748792,0.748792 0 0 1 0.0078,-0.008 c 0.71169,-0.55801 1.441989,-1.19038 1.9375,-1.86914 0.495512,-0.67875 0.758472,-1.35793 0.660157,-2.15234 a 0.748792,0.748792 0 0 1 -0.0039,-0.041 l -0.01562,-0.2246 -0.0293,-0.19141 c -0.185302,-0.89101 -0.77691,-1.53674 -1.607422,-1.96484 -0.83249,-0.42913 -1.892004,-0.59212 -2.798828,-0.45508 z m 0.548828,13.16797 -0.359375,0.36132 0.359375,0.35938 0.361328,-0.35938 z"
       id="path1212"
       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.58333302px;line-height:1.25;font-family:'Averia Serif Libre';-inkscape-font-specification:'Averia Serif Libre';letter-spacing:0px;word-spacing:0px;vector-effect:none;fill:#fa93ff;fill-opacity:1;stroke:none;stroke-width:0.26500002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter1222)"
       inkscape:original="M 12.039062 276.50195 C 10.768518 276.63604 9.6987432 277.09514 9.5664062 278.5625 C 9.4929412 280.28878 10.91685 279.60825 11.855469 278.73438 C 13.048929 277.54298 15.712261 278.57013 15.345703 280.39258 C 15.14755 282.30076 12.562739 283.17532 11.867188 284.82617 C 11.43551 285.75456 11.397815 288.20783 12.966797 287.77148 C 13.623624 286.93909 13.105159 285.83097 14.470703 284.73047 C 15.930011 283.58626 17.599435 282.06176 17.347656 280.02734 L 17.330078 279.77148 L 17.291016 279.51758 C 16.821155 277.23144 14.179998 276.17841 12.039062 276.50195 z M 12.4375 289.61523 L 11.544922 290.50781 L 11.542969 291.0332 L 12.4375 291.92773 L 12.962891 291.92773 L 13.855469 291.0332 L 13.855469 290.50781 L 12.962891 289.61523 L 12.4375 289.61523 z "
       inkscape:radius="-0.74871713"
       sodipodi:type="inkscape:offset" />
    <path
       sodipodi:type="inkscape:offset"
       inkscape:radius="-0.16717836"
       inkscape:original="M -14.34375 277.00195 C -14.35155 277.00395 -14.359347 277.00486 -14.367188 277.00586 C -14.957202 277.06816 -15.456819 277.209 -15.789062 277.44531 C -16.11815 277.67939 -16.326757 277.98861 -16.384766 278.59375 C -16.396596 278.94282 -16.323264 279.09764 -16.308594 279.11523 C -16.293644 279.13313 -16.320444 279.13779 -16.197266 279.12109 C -15.950904 279.08739 -15.37302 278.75785 -14.949219 278.36328 L -14.960938 278.375 C -14.179326 277.59474 -12.995641 277.56495 -12.070312 277.96875 C -11.152622 278.36922 -10.401691 279.31598 -10.617188 280.46484 C -10.74706 281.62372 -11.562681 282.41436 -12.332031 283.08789 C -13.106385 283.7658 -13.851986 284.37545 -14.125 285.02344 C -14.1275 285.02844 -14.130113 285.03396 -14.132812 285.03906 C -14.279093 285.35366 -14.401281 286.17218 -14.294922 286.71289 C -14.241742 286.98325 -14.139292 287.17048 -14.054688 287.24414 C -14.009657 287.28334 -13.897079 287.25733 -13.828125 287.26562 C -13.685143 287.00234 -13.611598 286.71247 -13.498047 286.23438 C -13.363381 285.66737 -13.078496 284.95798 -12.306641 284.33594 C -12.304641 284.33494 -12.302781 284.33303 -12.300781 284.33203 C -11.583283 283.76946 -10.83545 283.12505 -10.316406 282.41406 C -9.7973609 281.70308 -9.5060504 280.95629 -9.6132812 280.08984 C -9.6147813 280.08084 -9.6160875 280.0716 -9.6171875 280.0625 L -9.6328125 279.82812 L -9.6640625 279.61914 C -9.8643945 278.64441 -10.518063 277.93724 -11.400391 277.48242 C -12.28272 277.02761 -13.384016 276.85692 -14.34375 277.00195 z "
       xlink:href="#path1199"
       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.58333302px;line-height:1.25;font-family:'Averia Serif Libre';-inkscape-font-specification:'Averia Serif Libre';letter-spacing:0px;word-spacing:0px;vector-effect:none;fill:#ffe2fb;fill-opacity:1;stroke:none;stroke-width:0.26500002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
       id="path5733"
       inkscape:href="#path1199"
       d="m -13.605469,277.11914 c -0.24345,-0.003 -0.482418,0.014 -0.71289,0.0488 -0.01058,0.002 -0.02313,0.003 -0.02734,0.004 a 0.16719508,0.16719508 0 0 1 -0.0039,0 c -0.57365,0.0606 -1.048495,0.20154 -1.341797,0.41015 -0.297394,0.21154 -0.469187,0.45403 -0.525391,1.01758 -3.1e-5,9.2e-4 3e-5,0.001 0,0.002 -0.0052,0.16054 0.0082,0.27296 0.02344,0.3418 0.07849,-0.0163 0.262351,-0.0861 0.46875,-0.20898 0.218361,-0.13004 0.460956,-0.30687 0.662109,-0.49415 a 0.16719508,0.16719508 0 0 1 0.05273,-0.0312 c 0.0011,-0.001 0.0028,-9.3e-4 0.0039,-0.002 0.838825,-0.77648 2.051057,-0.80558 3.001953,-0.39062 0.975575,0.42573 1.78232,1.4453 1.550781,2.67968 -0.141936,1.22067 -0.998063,2.04142 -1.769531,2.7168 -0.776996,0.68022 -1.502751,1.2928 -1.748047,1.875 a 0.16719508,0.16719508 0 0 1 -0.0039,0.01 c -7.97e-4,0.002 -0.0037,0.007 -0.0059,0.0117 -0.05727,0.12317 -0.127965,0.40183 -0.162109,0.70117 -0.03414,0.29933 -0.03604,0.62635 0.01172,0.86914 0.0462,0.2349 0.143766,0.38461 0.177734,0.41992 0.0013,-2e-5 0.0045,4e-5 0.0059,0 0.109999,-0.22507 0.183634,-0.46863 0.28711,-0.9043 0.139671,-0.58808 0.443488,-1.34184 1.248047,-1.99023 a 0.16719508,0.16719508 0 0 1 0.0078,-0.004 c 0.0021,-0.002 0.0037,-0.004 0.0059,-0.006 a 0.16719508,0.16719508 0 0 1 0.002,-0.002 c 0.71045,-0.55752 1.444993,-1.19161 1.945312,-1.87695 0.5037376,-0.69002 0.7731349,-1.38688 0.6718751,-2.20508 -0.00154,-0.01 -0.00277,-0.0199 -0.00391,-0.0293 a 0.16719508,0.16719508 0 0 1 0,-0.008 l -0.015625,-0.22852 -0.029297,-0.19336 -0.00195,-0.004 c -0.1901449,-0.91782 -0.7983519,-1.58039 -1.6464839,-2.01758 -0.635335,-0.32749 -1.398557,-0.50322 -2.128907,-0.51172 z"
       transform="translate(26.458334)" />
    <path
       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.58333302px;line-height:1.25;font-family:'Averia Serif Libre';-inkscape-font-specification:'Averia Serif Libre';letter-spacing:0px;word-spacing:0px;vector-effect:none;fill:#ffe2fb;fill-opacity:1;stroke:none;stroke-width:0.26500002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
       d="m -14.343099,277.00195 c -0.0078,0.002 -0.0156,0.003 -0.02344,0.004 -0.590014,0.0623 -1.089633,0.20314 -1.421876,0.43945 -0.329088,0.23408 -0.537694,0.5433 -0.595703,1.14844 -0.01183,0.34907 0.0615,0.50389 0.07617,0.52148 0.01495,0.0179 -0.01185,0.0227 0.111328,0.006 0.246362,-0.0337 0.824247,-0.36324 1.248048,-0.75781 l -0.01172,0.0117 c 0.781612,-0.78026 1.965296,-0.81005 2.890625,-0.40625 0.91769,0.40047 1.668621,1.34723 1.453124,2.49609 -0.129872,1.15888 -0.945493,1.94952 -1.714843,2.62305 -0.774354,0.67791 -1.519955,1.28756 -1.792969,1.93555 -0.0025,0.005 -0.0051,0.0105 -0.0078,0.0156 -0.14628,0.3146 -0.268469,1.13312 -0.16211,1.67383 0.05318,0.27036 0.155631,0.45759 0.240235,0.53125 0.04503,0.0392 0.157608,0.0132 0.226562,0.0215 0.142982,-0.26329 0.216528,-0.55315 0.330079,-1.03124 0.134666,-0.56701 0.419551,-1.2764 1.191406,-1.89844 0.002,-10e-4 0.0039,-0.003 0.0059,-0.004 0.717498,-0.56257 1.46533,-1.20698 1.984374,-1.91797 0.5190453,-0.71098 0.8103562,-1.45777 0.7031253,-2.32422 -0.0015,-0.009 -0.0028,-0.0182 -0.0039,-0.0273 l -0.01563,-0.23438 -0.03125,-0.20898 c -0.200332,-0.97473 -0.8539993,-1.6819 -1.7363273,-2.13672 -0.882329,-0.45481 -1.983626,-0.6255 -2.94336,-0.48047 z"
       id="path1199"
       inkscape:connector-curvature="0"
       sodipodi:nodetypes="ccccccccccscccccccccccccccc" />
    <path
       style="opacity:1;fill:#ffe7f0;fill-opacity:1;stroke:none;stroke-width:0.26499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
       d="m 12.204228,290.65853 -3e-6,0.22524 0.383115,0.38311 0.225341,1.1e-4 0.383112,-0.38311 -1.01e-4,-0.22535 -0.383115,-0.38311 h -0.225238 z"
       id="path1232"
       inkscape:connector-curvature="0" />
  </g>
</svg>

Modified static/style.scss from [2a06f65525] to [9b25bded91].

9
10
11
12
13
14
15








16
17
18
19
20
21
22
23

24
25
26
27
28
29
30
..
51
52
53
54
55
56
57

58
59
60
61
62
63
64
...
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
...
141
142
143
144
145
146
147

148
149
150
151
152
153
154
...
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
...
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
...
321
322
323
324
325
326
327
328

329

330
331
332
333
334
335
336
...
469
470
471
472
473
474
475




476
477
478
479
480
481
482
...
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
...
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
...
591
592
593
594
595
596
597












	@extend %sans;
	background-color: tone(-55%);
	color: tone(25%);
	font-size: 14pt;
	margin: 0;
	padding: 0;
}








a[href] {
	color: tone(10%);
	text-decoration-color: tone(10%,-0.5);
	&:hover {
		color: white;
		text-shadow: 0 0 15px tone(20%);
		text-decoration-color: tone(10%,-0.1);
	}

}
a[href^="//"],
a[href^="http://"],
a[href^="https://"] { // external link
	&:hover::after {
		color: black;
		background-color: white;
................................................................................
%glow {
	box-shadow: 0 0 20px tone(0%,-0.8);
}

%button {
	@extend %sans;
	font-size: 14pt;

	padding: 0.1in 0.2in;
	border: 1px solid black;
	color: tone(25%);
	text-shadow: 1px 1px black;
	text-decoration: none;
	text-align: center;
	cursor: default;
................................................................................
}

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

input[type='text'], input[type='password'], textarea {
	@extend %serif;
	padding: 0.08in 0.1in;
	box-sizing: border-box;
	border: 1px solid black;
	background: linear-gradient(to bottom, tone(-55%), tone(-40%));
	font-size: 16pt;
	color: tone(25%);
................................................................................
		color: white;
		border-image: linear-gradient(to bottom, tone(-10%), tone(-30%)) 1 / 1px;
		background: $grad-ui-focus;
		outline: none;
		@extend %glow;
	}
}


@mixin glass {
	@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
		backdrop-filter: blur(40px);
		-webkit-backdrop-filter: blur(40px);
		background-color: tone(-53%, -0.7);
	}
................................................................................
			grid-row: 2 / 3;
		}
	}
	> .stats {
		grid-column: 3 / 4;
		grid-row: 1 / 3;
	}
	> .menu {
		grid-column: 1 / 3; grid-row: 2 / 3;
		padding-top: 0.075in;
		flex-wrap: wrap;
		display: flex;
		justify-content: center;
		align-items: center;
		> a[href] {
			@extend %button;
			display: block;
			margin: 0.025in 0.05in;
		}
		> hr {
			all: unset;
			display: block;
			height: 0.3in;
................................................................................
}

.epithet {
	display: inline-block;
	background: tone(20%);
	color: tone(-45%);
	text-shadow: 0 0 3px tone(-30%, -0.4);
	border-radius: 3px;
	padding: 6px;
	padding-top: 2px;
	padding-bottom: 4px;
	font-size: 80%;
	vertical-align: top;
	font-weight: 300;
	letter-spacing: 0.5px;
................................................................................
		tone(-55%) 10%,
		tone(-50%) 80%,
		tone(-45%)
	);
	// outline: 1px solid black;
}

body.error .message {

	@extend %box;

	width: 4in;
	margin:auto;
	padding: 0.5in;
	text-align: center;
}

div.login {
................................................................................
		padding: 0.1in;
		padding-left: 0.15in;
		>.nym { font-weight: bold; }
		color: tone(0%,-0.4);
		> span.nym { color: tone(10%) }
		> span.handle { color: tone(-5%) }
		background: linear-gradient(to right, tone(-55%), transparent);




	}
	>.content {
		grid-column: 2/4; grid-row: 1/2;
		padding: 0.2in;
		@extend %serif;
		font-size: 110%;
		text-align: justify;
................................................................................
	}
}

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

}






form {



	.elem {
		margin: 0.1in 0;
		label { display:block; font-weight: bold; padding: 0.03in 0; }
		.txtbox {
			@extend %serif;
			box-sizing: border-box;
			padding: 0.08in 0.1in;
			border: 1px solid black;
			background: tone(-55%);
		}
		textarea { resize: vertical; min-height: 2in; }
		input, textarea, .txtbox {
			display: block;
			width: 100%;
		}

















		button { float: right; width: 50%; }








































	}
}

@keyframes flashup {
	0% { opacity: 0; transform: scale(0.8); }
	10% { opacity: 1; transform: scale(1.1); }
	80% { opacity: 1; transform: scale(1); }
................................................................................
	border-radius: 3px;
	box-shadow: 0 0 50px tone(-55%);
	color: white;
	animation: ease forwards flashup;
	//cubic-bezier(0.4, 0.63, 0.6, 0.31)
	animation-duration: 3s;
}



















>
>
>
>
>
>
>
>








>







 







>







 







|







 







>







 







|







<







 







|







 







|
>

>







 







>
>
>
>







 







|







 







>
>
>
>
>

>
>
>










<




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







 







>
>
>
>
>
>
>
>
>
>
>
>
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
..
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
...
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
...
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
...
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287

288
289
290
291
292
293
294
...
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
...
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
...
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
...
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
...
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
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
...
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
	@extend %sans;
	background-color: tone(-55%);
	color: tone(25%);
	font-size: 14pt;
	margin: 0;
	padding: 0;
}
::selection {
	color: tone(-60%);
	background-color: tone(-10%);
}
::placeholder {
	color: tone(0,-0.3);
	font-style: italic;
}
a[href] {
	color: tone(10%);
	text-decoration-color: tone(10%,-0.5);
	&:hover {
		color: white;
		text-shadow: 0 0 15px tone(20%);
		text-decoration-color: tone(10%,-0.1);
	}
	&.button { @extend %button; }
}
a[href^="//"],
a[href^="http://"],
a[href^="https://"] { // external link
	&:hover::after {
		color: black;
		background-color: white;
................................................................................
%glow {
	box-shadow: 0 0 20px tone(0%,-0.8);
}

%button {
	@extend %sans;
	font-size: 14pt;
	box-sizing: border-box;
	padding: 0.1in 0.2in;
	border: 1px solid black;
	color: tone(25%);
	text-shadow: 1px 1px black;
	text-decoration: none;
	text-align: center;
	cursor: default;
................................................................................
}

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

input[type='text'], input[type='password'], textarea, select {
	@extend %serif;
	padding: 0.08in 0.1in;
	box-sizing: border-box;
	border: 1px solid black;
	background: linear-gradient(to bottom, tone(-55%), tone(-40%));
	font-size: 16pt;
	color: tone(25%);
................................................................................
		color: white;
		border-image: linear-gradient(to bottom, tone(-10%), tone(-30%)) 1 / 1px;
		background: $grad-ui-focus;
		outline: none;
		@extend %glow;
	}
}
select { width: 100%; }

@mixin glass {
	@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
		backdrop-filter: blur(40px);
		-webkit-backdrop-filter: blur(40px);
		background-color: tone(-53%, -0.7);
	}
................................................................................
			grid-row: 2 / 3;
		}
	}
	> .stats {
		grid-column: 3 / 4;
		grid-row: 1 / 3;
	}
	> form.actions {
		grid-column: 1 / 3; grid-row: 2 / 3;
		padding-top: 0.075in;
		flex-wrap: wrap;
		display: flex;
		justify-content: center;
		align-items: center;
		> a[href] {

			display: block;
			margin: 0.025in 0.05in;
		}
		> hr {
			all: unset;
			display: block;
			height: 0.3in;
................................................................................
}

.epithet {
	display: inline-block;
	background: tone(20%);
	color: tone(-45%);
	text-shadow: 0 0 3px tone(-30%, -0.4);
	border-radius: 2px;
	padding: 6px;
	padding-top: 2px;
	padding-bottom: 4px;
	font-size: 80%;
	vertical-align: top;
	font-weight: 300;
	letter-spacing: 0.5px;
................................................................................
		tone(-55%) 10%,
		tone(-50%) 80%,
		tone(-45%)
	);
	// outline: 1px solid black;
}

//body.error .message {
.message {
	@extend %box;
	display: block;
	width: 4in;
	margin:auto;
	padding: 0.5in;
	text-align: center;
}

div.login {
................................................................................
		padding: 0.1in;
		padding-left: 0.15in;
		>.nym { font-weight: bold; }
		color: tone(0%,-0.4);
		> span.nym { color: tone(10%) }
		> span.handle { color: tone(-5%) }
		background: linear-gradient(to right, tone(-55%), transparent);
		&:hover {
			> span.nym { color: white; }
			> span.handle { color: tone(15%) }
		}
	}
	>.content {
		grid-column: 2/4; grid-row: 1/2;
		padding: 0.2in;
		@extend %serif;
		font-size: 110%;
		text-align: justify;
................................................................................
	}
}

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

}

hr {
	border: none;
	border-top: 1px solid tone(-30%);
	border-bottom: 1px solid tone(-55%);
}
form {
	margin: 0.15in 0;
	> p:first-child { margin-top: 0; }
	> p:last-child { margin-bottom: 0; }
	.elem {
		margin: 0.1in 0;
		label { display:block; font-weight: bold; padding: 0.03in 0; }
		.txtbox {
			@extend %serif;
			box-sizing: border-box;
			padding: 0.08in 0.1in;
			border: 1px solid black;
			background: tone(-55%);
		}

		input, textarea, .txtbox {
			display: block;
			width: 100%;
		}
		textarea { resize: vertical; min-height: 2in; }
	}
	.elem + %button { margin-left: 50%; width: 50%; }
}

menu.choice {
	display: flex;
	&.horizontal {
		flex-flow: row-reverse wrap;
		justify-content: space-evenly;
	}
	&.vertical {
		flex-flow: column;
		margin-left: 50%;
	}
	&.vertical-float {
		flex-flow: column;
		float: right;
		width: 40%;
		margin-left: 0.1in;
	}
	> %button { display: block; margin: 2px; flex-grow: 1 }
}

.check-panel {
	display: flex;
	flex-flow: row wrap;
	> label {
		display: block;
		box-sizing: border-box;
		width: calc(50% - 0.2in);
		padding: 0.1in 0.1in;
		margin: 0.1in 0.1in;
		background: tone(-45%);
		border: 1px solid black;
		text-shadow: 1px 1px black;
		flex-grow: 1;
		&:focus-within {
			border: 1px inset tone(-10%);
			background: tone(-50%);
		}
	}
	input[type="checkbox"] {
		-webkit-appearance: none;
		padding: 0.5em;
		background: tone(-35%);
		border: 1px outset tone(-50%);
		vertical-align: bottom;
		box-shadow: 0 1px tone(-50%);
		&:checked {
			border: 1px inset tone(-35%);
			background: tone(-60%);
			box-shadow: 0 1px tone(-40%);
		}
		&:focus {
			border-color: tone(10%);
			outline: none;
		}
	}
}

@keyframes flashup {
	0% { opacity: 0; transform: scale(0.8); }
	10% { opacity: 1; transform: scale(1.1); }
	80% { opacity: 1; transform: scale(1); }
................................................................................
	border-radius: 3px;
	box-shadow: 0 0 50px tone(-55%);
	color: white;
	animation: ease forwards flashup;
	//cubic-bezier(0.4, 0.63, 0.6, 0.31)
	animation-duration: 3s;
}

form.action-bar {
	display: flex;
	> * {
		flex-grow: 1;
		flex-basis: 0;
		margin-left: 0.1in;
	}
	> *:first-child {
		margin-left: 0;
	}
}

Modified store.t from [f53ab94a55] to [004846cca6].

1
2
3
4
5
6
7
8
9
10
11
12




13


14
15
16
17
18
19
20
...
142
143
144
145
146
147
148


149
150
151
152
153
154
155


156
157
158
159
160
161
162
...
227
228
229
230
231
232
233







234
235
236
237
238
239
240
...
256
257
258
259
260
261
262

263
264
265
266
267
268
269
...
283
284
285
286
287
288
289
290
291
292
293
294

295
296
297
298
299
300
301
...
304
305
306
307
308
309
310









311
312
313

314
315
316
317
318
319
320
-- vim: ft=terra
local m = {
	timepoint = int64;
	scope = lib.enum {
		'public', 'private', 'local';
		'personal', 'direct', 'circle';
	};
	notiftype = lib.enum {
		'mention', 'like', 'rt', 'react'
	};

	relation = lib.enum {




		'follow', 'mute', 'block'


	};
	credset = lib.set {
		'pw', 'otp', 'challenge', 'trust'
	};
	privset = lib.set {
		'post', 'edit', 'acct', 'upload', 'censor', 'admin', 'invite'
	};
................................................................................
	id: uint64
	author: uint64
	subject: str
	body: str
	acl: str
	posted: m.timepoint
	discovered: m.timepoint


	mentions: lib.mem.ptr(uint64)
	circles: lib.mem.ptr(uint64) --only meaningful if scope is set to circle
	convoheaduri: str
	parent: uint64
-- ephemera
	localpost: bool
	source: &m.source


}

local cnf = terralib.memoize(function(ty,rty)
	rty = rty or ty
	return struct {
		enum: {&opaque, uint64, rawstring} -> intptr
		get: {&opaque, uint64, rawstring} -> rty
................................................................................
	aid: uint64
	uid: uint64
	aname: str
	netmask: m.inet
	privs: m.privset
	blacklist: bool
}








-- backends only handle content on the local server
struct m.backend { id: rawstring
	open: &m.source -> &opaque
	close: &m.source -> {}
	dbsetup: &m.source -> bool -- creates the schema needed to call conprep (called only once per database e.g. with `parsav db init`)
	conprep: {&m.source, m.prepmode.t} -> {} -- prepares queries and similar tasks that require the schema to already be in place
................................................................................
	actor_save_privs: {&m.source, &m.actor} -> {}
	actor_fetch_xid: {&m.source, lib.mem.ptr(int8)} -> lib.mem.ptr(m.actor)
	actor_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.actor)
	actor_notif_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.notif)
	actor_enum: {&m.source} -> lib.mem.ptr(&m.actor)
	actor_enum_local: {&m.source} -> lib.mem.ptr(&m.actor)
	actor_stats: {&m.source, uint64} -> m.actor_stats


	actor_auth_how: {&m.source, m.inet, rawstring} -> {m.credset, bool}
		-- returns a set of auth method categories that are available for a
		-- given user from a certain origin
			-- origin: inet
			-- username: rawstring
	actor_auth_otp: {&m.source, m.inet, rawstring, rawstring}
................................................................................
			-> {uint64, uint64, pstr}
		-- handles API authentication
			-- origin: inet
			-- handle: rawstring
			-- key:    rawstring (X-API-Key)
	actor_auth_record_fetch: {&m.source, uint64} -> lib.mem.ptr(m.auth)
	actor_powers_fetch: {&m.source, uint64} -> m.powerset
	actor_session_fetch: {&m.source, uint64, m.inet} -> {lib.stat(m.auth), lib.mem.ptr(m.actor)}
		-- retrieves an auth record + actor combo suitable by AID suitable
		-- for determining session validity & caps
			-- aid:    uint64
			-- origin: inet

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

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










	post_save: {&m.source, &m.post} -> {}
	post_create: {&m.source, &m.post} -> uint64

	post_enum_author_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post))
	post_attach_ctl: {&m.source, uint64, uint64, bool} -> {}
		-- attaches or detaches an existing database artifact
			-- post id: uint64
			-- artifact id: uint64
			-- detach: bool
	artifact_instantiate: {&m.source, lib.mem.ptr(uint8), lib.mem.ptr(int8)} -> uint64


|








|
>
>
>
>
|
>
>







 







>
>







>
>







 







>
>
>
>
>
>
>







 







>







 







|




>







 







>
>
>
>
>
>
>
>
>



>







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
...
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
...
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
...
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
...
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
...
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
-- vim: ft=terra
local m = {
	timepoint = lib.osclock.time_t;
	scope = lib.enum {
		'public', 'private', 'local';
		'personal', 'direct', 'circle';
	};
	notiftype = lib.enum {
		'mention', 'like', 'rt', 'react'
	};

	relation = lib.set {
		'silence', -- messages will not be accepted
		'collapse', -- posts will be collapsed by default
		'disemvowel', -- posts will be ritually humiliated, but shown
		'avoid', -- posts will be kept out of the timeline but will show on users' posts and in conversations
		'follow',
		'mute', -- posts will be completely hidden at all times
		'block', -- no interactions will be permitted, but posts will remain visible
	};
	credset = lib.set {
		'pw', 'otp', 'challenge', 'trust'
	};
	privset = lib.set {
		'post', 'edit', 'acct', 'upload', 'censor', 'admin', 'invite'
	};
................................................................................
	id: uint64
	author: uint64
	subject: str
	body: str
	acl: str
	posted: m.timepoint
	discovered: m.timepoint
	edited: m.timepoint
	chgcount: uint
	mentions: lib.mem.ptr(uint64)
	circles: lib.mem.ptr(uint64) --only meaningful if scope is set to circle
	convoheaduri: str
	parent: uint64
-- ephemera
	localpost: bool
	source: &m.source

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

local cnf = terralib.memoize(function(ty,rty)
	rty = rty or ty
	return struct {
		enum: {&opaque, uint64, rawstring} -> intptr
		get: {&opaque, uint64, rawstring} -> rty
................................................................................
	aid: uint64
	uid: uint64
	aname: str
	netmask: m.inet
	privs: m.privset
	blacklist: bool
}

struct m.relationship {
	agent: uint64
	patient: uint64
	rel: m.relation -- agent → patient
	recip: m.relation -- patient → agent
}

-- backends only handle content on the local server
struct m.backend { id: rawstring
	open: &m.source -> &opaque
	close: &m.source -> {}
	dbsetup: &m.source -> bool -- creates the schema needed to call conprep (called only once per database e.g. with `parsav db init`)
	conprep: {&m.source, m.prepmode.t} -> {} -- prepares queries and similar tasks that require the schema to already be in place
................................................................................
	actor_save_privs: {&m.source, &m.actor} -> {}
	actor_fetch_xid: {&m.source, lib.mem.ptr(int8)} -> lib.mem.ptr(m.actor)
	actor_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.actor)
	actor_notif_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.notif)
	actor_enum: {&m.source} -> lib.mem.ptr(&m.actor)
	actor_enum_local: {&m.source} -> lib.mem.ptr(&m.actor)
	actor_stats: {&m.source, uint64} -> m.actor_stats
	actor_rel: {&m.source, uint64, uint64} -> m.relationship

	actor_auth_how: {&m.source, m.inet, rawstring} -> {m.credset, bool}
		-- returns a set of auth method categories that are available for a
		-- given user from a certain origin
			-- origin: inet
			-- username: rawstring
	actor_auth_otp: {&m.source, m.inet, rawstring, rawstring}
................................................................................
			-> {uint64, uint64, pstr}
		-- handles API authentication
			-- origin: inet
			-- handle: rawstring
			-- key:    rawstring (X-API-Key)
	actor_auth_record_fetch: {&m.source, uint64} -> lib.mem.ptr(m.auth)
	actor_powers_fetch: {&m.source, uint64} -> m.powerset
	actor_session_fetch: {&m.source, uint64, m.inet, m.timepoint} -> {lib.stat(m.auth), lib.mem.ptr(m.actor)}
		-- retrieves an auth record + actor combo suitable by AID suitable
		-- for determining session validity & caps
			-- aid:    uint64
			-- origin: inet
			-- cookie issue time: m.timepoint
	actor_auth_register_uid: {&m.source, uint64, uint64} -> {}
		-- notifies the backend module of the UID that has been assigned for
		-- an authentication ID
			-- aid: uint64
			-- uid: uint64

	actor_conf_str: cnf(rawstring, lib.mem.ptr(int8))
................................................................................
	auth_create_pw: {&m.source, uint64, bool, lib.mem.ptr(int8)} -> {}
		-- uid: uint64
		-- reset: bool (delete other passwords?)
		-- pw: pstring
	auth_purge_pw: {&m.source, uint64, rawstring} -> {}
	auth_purge_otp: {&m.source, uint64, rawstring} -> {}
	auth_purge_trust: {&m.source, uint64, rawstring} -> {}
	auth_sigtime_user_fetch: {&m.source, uint64} -> m.timepoint
		-- authentication tokens and accounts have a property that controls
		-- whether auth cookies dated to a certain point are valid. cookies
		-- that are generated before the timepoint are considered invalid.
		-- this is used primarily to lock out untrusted sessions.
			-- uid: uint64
	auth_sigtime_user_alter: {&m.source, uint64, m.timepoint} -> {}
			-- uid: uint64
			-- timestamp: timepoint

	post_save: {&m.source, &m.post} -> {}
	post_create: {&m.source, &m.post} -> uint64
	post_fetch: {&m.source, uint64} -> lib.mem.ptr(m.post)
	post_enum_author_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post))
	post_attach_ctl: {&m.source, uint64, uint64, bool} -> {}
		-- attaches or detaches an existing database artifact
			-- post id: uint64
			-- artifact id: uint64
			-- detach: bool
	artifact_instantiate: {&m.source, lib.mem.ptr(uint8), lib.mem.ptr(int8)} -> uint64

Modified str.t from [f2457b558f] to [265a0fa659].

1
2
3
4
5
6
7

8
9
10
11
12
13
14
..
92
93
94
95
96
97
98












99
100
101
102
103
104
105
-- vim: ft=terra
-- string.t: string classes
local util = lib.util
local pstr = lib.mem.ptr(int8)
local pref = lib.mem.ref(int8)

local m = {

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

struct m.acc {
	buf: rawstring
	sz: intptr
	run: intptr
	space: intptr
}













local terra biggest(a: intptr, b: intptr)
	if a > b then return a else return b end
end

terra m.acc:init(run: intptr)
	--lib.dbg('initializing string accumulator')







>







 







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







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
..
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
-- vim: ft=terra
-- string.t: string classes
local util = lib.util
local pstr = lib.mem.ptr(int8)
local pref = lib.mem.ref(int8)

local m = {
	t = pstr, ref = pref;
	sz = terralib.externfunction('strlen', rawstring -> intptr);
	cmp = terralib.externfunction('strcmp', {rawstring, rawstring} -> int);
	ncmp = terralib.externfunction('strncmp', {rawstring, rawstring, intptr} -> int);
	cpy = terralib.externfunction('stpcpy',{rawstring, rawstring} -> rawstring);
	ncpy = terralib.externfunction('stpncpy',{rawstring, rawstring, intptr} -> rawstring);
	cat = terralib.externfunction('strcat',{rawstring, rawstring} -> rawstring);
	ncat = terralib.externfunction('strncat',{rawstring, rawstring, intptr} -> rawstring);
................................................................................

struct m.acc {
	buf: rawstring
	sz: intptr
	run: intptr
	space: intptr
}

terra m.cdowncase(c: int8)
	if c >= @'A' and c <= @'Z' then
		return c + (@'a' - @'A')
	else return c end
end

terra m.cupcase(c: int8)
	if c >= @'a' and c <= @'z' then
		return c - (@'a' - @'A')
	else return c end
end

local terra biggest(a: intptr, b: intptr)
	if a > b then return a else return b end
end

terra m.acc:init(run: intptr)
	--lib.dbg('initializing string accumulator')

Modified view/conf-profile.tpl from [f1f77d7014] to [d384fd3b9f].

1
2
3
4
5
6
<form method="post">
	<div class="elem"><label>handle</label> <div class="txtbox">@!handle</div>
	<div class="elem"><label for="nym">display name</label> <input type="text" name="nym" id="nym" placeholder="j. random poster" value="@:nym"></div>
	<div class="elem"><label for="bio">bio</label><textarea name="bio" id="bio" placeholder="tall, dark, and mysterious">@!bio</textarea></div>
	<button>commit</button>
</form>

|




1
2
3
4
5
6
<form method="post">
	<div class="elem"><label>handle</label> <div class="txtbox">@!handle</div></div>
	<div class="elem"><label for="nym">display name</label> <input type="text" name="nym" id="nym" placeholder="j. random poster" value="@:nym"></div>
	<div class="elem"><label for="bio">bio</label><textarea name="bio" id="bio" placeholder="tall, dark, and mysterious">@!bio</textarea></div>
	<button>commit</button>
</form>

Added view/conf-sec-credmg.tpl version [43efff9618].







































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
<hr>
<form method="post">
	<p>your account can currently be accessed with the credentials listed below. if you fear a credential has been compromised, you can revoke or reset it.</p>
	<select size="6" name="cred">
		@credlist
	</select>
	<menu class="horizontal choice">
		<button name="act" value="reset">reset</button>
		<button name="act" value="revoke">revoke</button>
	</menu>
</form>
<hr>
<form method="post">
	<p>you can associate extra credentials with your account. you can also limit how much of your authority these credentials can be used to exercise &mdash; for instance, it might be useful to create API keys that can read your timeline, but not post as you or access any administrative powers you may have. if you don't select a capability set, the credential will be able to wield the full scope of your powers.</p>
	<div class="check-panel">
		<label><input type="checkbox" name="allow-post"> post</label>
		<label><input type="checkbox" name="allow-edit"> edit</label>
		<label><input type="checkbox" name="allow-acct"> manage account</label>
		<label><input type="checkbox" name="allow-upload"> upload artifacts</label>
		<label><input type="checkbox" name="allow-censor"> moderation</label>
		<label><input type="checkbox" name="allow-admin"> other admin powers</label>
		<label><input type="checkbox" name="allow-invite"> invite</label>
	</div>
	<p>you can also specify an IP address range in CIDR format to associate with this credential. if you do so, this credential will only be usable when connecting from an IP address in that range. otherwise, it will be valid when connecting from anywhere on the internet.</p>
	<div class="elem">
		<label for="netmask">netmask</label>
		<input type="text" name="netmask" id="netmask" placeholder="10.0.0.0/8">
	</div>
	<menu class="vertical choice">
		<button name="kind" value="pw">new password</button>
		<button name="kind" value="otp">new OTP key</button>
		<button name="kind" value="api">new API token</button>
		<button name="kind" value="challenge">new challenge key</button>
	</div>
</form>

Modified view/conf-sec.tpl from [7ba95a81c5] to [de1cf7e8f0].

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


|
|

|




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

Modified view/conf.tpl from [bd130ad9d3] to [09b447ff32].

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

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









|




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

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

Added view/confirm.tpl version [9198c794e9].



















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

Modified view/load.lua from [f63ef60595] to [212041720e].

1
2
3
4
5
6
7

8
9
10
11
12
13
14
15
16


17
18
19
20
21
22
23
-- because lua can't scan directories, we need a
-- file that indexes the templates manually, and
-- copies them into a data structure we can then
-- create templates from when we return to terra
local path = ...
local sources = {
	'docskel';

	'tweet';
	'profile';
	'compose';

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

	'conf';
	'conf-profile';


}

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







>









>
>







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
-- because lua can't scan directories, we need a
-- file that indexes the templates manually, and
-- copies them into a data structure we can then
-- create templates from when we return to terra
local path = ...
local sources = {
	'docskel';
	'confirm';
	'tweet';
	'profile';
	'compose';

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

	'conf';
	'conf-profile';
	'conf-sec';
	'conf-sec-credmg';
}

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

Modified view/profile.tpl from [d21ccabbe8] to [cfeb837b05].

9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
	<table class="stats">
		<tr><th>posts</th> <td>@nposts</td></tr>
		<tr><th>following</th> <td>@nfollows</td></tr>
		<tr><th>followers</th> <td>@nfollowers</td></tr>
		<tr><th>mutuals</th> <td>@nmutuals</td></tr>
		<tr><th>@timephrase</th> <td>@tweetday</td></tr>
	</table>
	<div class="menu">
		<a href="/@:xid">posts</a>
		<a href="/@:xid/media">media</a>
		<a href="/@:xid/social">associates</a>
		<hr>
		@auxbtn
	</div>
</div>







|
|
|
|


|

9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
	<table class="stats">
		<tr><th>posts</th> <td>@nposts</td></tr>
		<tr><th>following</th> <td>@nfollows</td></tr>
		<tr><th>followers</th> <td>@nfollowers</td></tr>
		<tr><th>mutuals</th> <td>@nmutuals</td></tr>
		<tr><th>@timephrase</th> <td>@tweetday</td></tr>
	</table>
	<form class="actions">
		<a class="button" href="/@:xid">posts</a>
		<a class="button" href="/@:xid/media">media</a>
		<a class="button" href="/@:xid/social">associates</a>
		<hr>
		@auxbtn
	</form>
</div>