parsav  Check-in [24ec409083]

Overview
Comment:add live updates, system to only update when necessary almost works
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 24ec4090837417ad087eff63c16c1336dffc58f941ab2786f4175be4b558a7ad
User & Date: lexi on 2021-01-01 04:33:10
Other Links: manifest | tags
Context
2021-01-01
16:24
move from webp to svg except where necessary check-in: aa17a03321 user: lexi tags: trunk
04:33
add live updates, system to only update when necessary almost works check-in: 24ec409083 user: lexi tags: trunk
2020-12-31
02:18
start work on user mgmt check-in: db4c5fd644 user: lexi tags: trunk
Changes

Modified acl.t from [7cc6c4467d] to [db020952e6].

10
11
12
13
14
15
16








17
18
19
20
21
22
23
24
25
26
27
28
29
	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







>
>
>
>
>
>
>
>

|
|
|




|




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
	id: uint64
}

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

end


terra lib.store.post:comp()
 -- TODO extract mentions from body, circles from acl
	self.mentions = [lib.mem.ptr(uint64)].null()
	self.circles = [lib.mem.ptr(uint64)].null()
	self.convoheaduri = nil
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
	self:comp()
	self.source:post_save(self)
end

return m

Modified backend/pgsql.t from [35848d4bf0] to [3e99c3d4ab].

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
...
287
288
289
290
291
292
293

294


295
296
297
298
299
300
301

302
303
304
305
306
307
308
309
...
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
...
357
358
359
360
361
362
363
364


365
366

367
368
369
370
371
372
373
...
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447






















































448
449
450
451
452
453
454
...
651
652
653
654
655
656
657

658
659
660
661
662
663
664
....
1025
1026
1027
1028
1029
1030
1031

1032


1033
1034
1035

1036
1037
1038
1039
1040
1041
1042
....
1114
1115
1116
1117
1118
1119
1120






















1121
1122
1123
1124
1125


1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
....
1208
1209
1210
1211
1212
1213
1214



























1215
1216
1217
1218
1219
1220
1221
....
1226
1227
1228
1229
1230
1231
1232





























1233
1234
1235
1236
			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
................................................................................
				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_destroy_prepare = {
		params = {uint64}, cmd = true, sql = [[
			update parsav_posts set
................................................................................
	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
			limit case when $3::bigint = 0 then null
			           else $3::bigint end
................................................................................
		]];
	};
	post_attach_ctl_ins = {
		params = {uint64, uint64}, cmd=true, sql = [[
			update parsav_posts set
				artifacts = artifacts || $2::bigint
			where id = $1::bigint and not
				artifacts @> array[$2::bigint]
		]];
	};
	post_attach_ctl_del = {
		params = {uint64, uint64}, cmd=true, sql = [[
			update parsav_posts set
				artifacts = array_remove(artifacts, $2::bigint)
			where id = $1::bigint and
				artifacts @> array[$2::bigint]
		]];
	};






















































}

local struct pqr {
	sz: intptr
	res: &lib.pq.PGresult
}
terra pqr:free() if self.sz > 0 then lib.pq.PQclear(self.res) end end
................................................................................
		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)
	var av: rawstring, avlen: intptr
................................................................................
			            [lib.mem.ptr(lib.store.actor)] { ptr = nil, ct = 0 }
	end];

	post_create = [terra(
		src: &lib.store.source,
		post: &lib.store.post
	): uint64

		var r = queries.post_create.exec(src,post.author,post.subject,post.acl,post.body) 


		if r.sz == 0 then return 0 end
		defer r:free()
		var id = r:int(uint64,0,0)

		return id
	end];

	post_destroy = [terra(
		src: &lib.store.source,
		post: uint64
	): {}
................................................................................
		lib.dbg('created new actor, establishing powers')
		privupdate(src,ac)

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























	auth_create_pw = [terra(
		src: &lib.store.source,
		uid: uint64,
		reset: bool,
		pw: lib.mem.ptr(int8)


	): {}
		var hash: uint8[lib.crypt.algsz.sha256]
		if lib.md.mbedtls_md(lib.md.mbedtls_md_info_from_type(lib.crypt.alg.sha256.id),
			[&uint8](pw.ptr), pw.ct, &hash[0]) ~= 0 then
			lib.bail('cannot hash password')
		end
		if reset then queries.auth_purge_type.exec(src, nil, uid, 'pw-%') end
		queries.auth_create_pw.exec(src, uid, [lib.mem.ptr(uint8)] {ptr = &hash[0], ct = [hash.type.N]})
	end];

	auth_purge_pw = [terra(src: &lib.store.source, uid: uint64, handle: rawstring): {}
		queries.auth_purge_type.exec(src, handle, uid, 'pw-%')
	end];

	auth_purge_otp = [terra(src: &lib.store.source, uid: uint64, handle: rawstring): {}
................................................................................
		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()
................................................................................

	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







|
|


|
>











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







 







>
|
>
>


|
|



>
|







 







|
>
>

|
>



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








|
>
>
>







 







|
>
>

|
>







 







|










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







 







>







 







>
|
>
>



>







 







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



<
>
>







|







 







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







 







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




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
...
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
...
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
...
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
...
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
...
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
....
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
....
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269

1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
....
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
....
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
			update parsav_actors set
				authtime = to_timestamp($2::bigint)
				where id = $1::bigint
		]];
	};

	auth_create_pw = {
		params = {uint64, binblob, pstring}, cmd = true, sql = [[
			insert into parsav_auth (uid, name, kind, cred, comment) values (
				$1::bigint,
				(select handle from parsav_actors where id = $1::bigint),
				'pw-sha256', $2::bytea,
				$3::text
			)
		]]
	};

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

	auth_enum_uid = {
		params = {uint64}, sql = [[
			select aid, kind, comment, netmask, blacklist from parsav_auth where uid = $1::bigint
		]];
	};

	auth_enum_handle = {
		params = {rawstring}, sql = [[
			select aid, kind, comment, netmask, blacklist from parsav_auth where name = $1::text
		]];
	};

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

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

	post_destroy_prepare = {
		params = {uint64}, cmd = true, sql = [[
			update parsav_posts set
................................................................................
	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,
				coalesce(c.value, -1)::smallint

			from parsav_posts as p
				inner join parsav_actors          as a on p.author = a.id
				left join  parsav_actor_conf_ints as c on c.uid    = a.id and c.key = 'ui-accent'
			where p.id = $1::bigint
		]];
	};

	post_enum_parent = {
		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,
				coalesce(c.value, -1)::smallint

			from parsav_posts as p
				inner join parsav_actors as a on a.id = p.author
				left join  parsav_actor_conf_ints as c on c.uid = a.id and c.key = 'ui-accent'
			where p.parent = $1::bigint
			order by p.posted, p.discovered asc
		]]
	};

	thread_latest_arrival_calc = {
		params = {uint64}, sql = [[
			with recursive posts(id) as (
				select id from parsav_posts where parent = $1::bigint
			union
				select p.id from parsav_posts as p
					inner join posts on posts.id = p.parent
			), 

			maxes as (
				select unnest(array[max(p.posted), max(p.discovered), max(p.edited)]) as m
					from posts
					inner join parsav_posts as p
						on p.id = posts.id
			)

			select extract(epoch from max(m))::bigint from maxes
		]];
	};

	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,
				coalesce((select value from parsav_actor_conf_ints as c where
					c.uid = $1::bigint and c.key = 'ui-accent'),-1)::smallint

			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,
				coalesce(c.value, -1)::smallint

			from parsav_posts as p
				inner join parsav_actors          as a on p.author = a.id
				left join  parsav_actor_conf_ints as c on c.uid    = a.id and c.key = 'ui-accent'
			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
			limit case when $3::bigint = 0 then null
			           else $3::bigint end
................................................................................
		]];
	};
	post_attach_ctl_ins = {
		params = {uint64, uint64}, cmd=true, sql = [[
			update parsav_posts set
				artifacts = artifacts || $2::bigint
			where id = $1::bigint and not
				artifacts @> array[$2::bigint] -- prevent duplication
		]];
	};
	post_attach_ctl_del = {
		params = {uint64, uint64}, cmd=true, sql = [[
			update parsav_posts set
				artifacts = array_remove(artifacts, $2::bigint)
			where id = $1::bigint and
				artifacts @> array[$2::bigint]
		]];
	};

	actor_conf_str_get = {
		params = {uint64, rawstring}, sql = [[
			select value from parsav_actor_conf_strs where
				uid = $1::bigint and
				key = $2::text
			limit 1
		]];
	};
	actor_conf_str_set = {
		params = {uint64, rawstring, rawstring}, cmd = true, sql = [[
			insert into parsav_actor_conf_strs (uid,key,value)
				values ($1::bigint, $2::text, $3::text)
			on conflict (uid,key) do update set value = $3::text
		]];
	};
	actor_conf_str_enum = {
		params = {uint64}, sql = [[
			select value from parsav_actor_conf_strs where uid = $1::bigint
		]];
	};
	actor_conf_str_reset = {
		params = {uint64, rawstring}, cmd = true, sql = [[
			delete from parsav_actor_conf_strs where
				uid = $1::bigint and ($2::text is null or key = $2::text)
		]]
	};

	actor_conf_int_get = {
		params = {uint64, rawstring}, sql = [[
			select value from parsav_actor_conf_ints where
				uid = $1::bigint and
				key = $2::text
			limit 1
		]];
	};
	actor_conf_int_set = {
		params = {uint64, rawstring, uint64}, cmd = true, sql = [[
			insert into parsav_actor_conf_ints (uid,key,value)
				values ($1::bigint, $2::text, $3::bigint)
			on conflict (uid,key) do update set value = $3::bigint
		]];
	};
	actor_conf_int_enum = {
		params = {uint64}, sql = [[
			select value from parsav_actor_conf_ints where uid = $1::bigint
		]];
	};
	actor_conf_int_reset = {
		params = {uint64, rawstring}, cmd = true, sql = [[
			delete from parsav_actor_conf_ints where
				uid = $1::bigint and ($2::text is null or key = $2::text)
		]]
	};
}

local struct pqr {
	sz: intptr
	res: &lib.pq.PGresult
}
terra pqr:free() if self.sz > 0 then lib.pq.PQclear(self.res) end end
................................................................................
		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.accent = r:int(int16,row,12)
	p.ptr.localpost = r:bool(row,0)

	return p
end
local terra row_to_actor(r: &pqr, row: intptr): lib.mem.ptr(lib.store.actor)
	var a: lib.mem.ptr(lib.store.actor)
	var av: rawstring, avlen: intptr
................................................................................
			            [lib.mem.ptr(lib.store.actor)] { ptr = nil, ct = 0 }
	end];

	post_create = [terra(
		src: &lib.store.source,
		post: &lib.store.post
	): uint64
		var r = queries.post_create.exec(src,
			post.author,post.subject,post.acl,post.body,
			post.parent,post.posted,post.convoheaduri
		) 
		if r.sz == 0 then return 0 end
		defer r:free()
		var id = r:int(uint64,0,0)
		post.source = src
		return id
	end];

	post_destroy = [terra(
		src: &lib.store.source,
		post: uint64
	): {}
................................................................................
		lib.dbg('created new actor, establishing powers')
		privupdate(src,ac)

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

	auth_enum_uid = [terra(
		src: &lib.store.source,
		uid: uint64
	): lib.mem.ptr(lib.mem.ptr(lib.store.auth))
		var r = queries.auth_enum_uid.exec(src,uid)
		if r.sz == 0 then return [lib.mem.ptr(lib.mem.ptr(lib.store.auth))].null() end
		var ret = lib.mem.heapa([lib.mem.ptr(lib.store.auth)], r.sz)
		for i=0, r.sz do
			var kind = r:_string(i, 1)
			var comment = r:_string(i, 2)
			var a = [ lib.str.encapsulate(lib.store.auth, {
				kind = {`kind.ptr, `kind.ct};
				comment = {`comment.ptr, `comment.ct};
			}) ]
			a.ptr.aid = r:int(uint64, i, 0)
			a.ptr.netmask = r:cidr(i, 3)
			a.ptr.blacklist = r:bool(i, 4)
			ret.ptr[i] = a
		end
		return ret
	end];

	auth_attach_pw = [terra(
		src: &lib.store.source,
		uid: uint64,
		reset: bool,

		pw: pstring,
		comment: pstring
	): {}
		var hash: uint8[lib.crypt.algsz.sha256]
		if lib.md.mbedtls_md(lib.md.mbedtls_md_info_from_type(lib.crypt.alg.sha256.id),
			[&uint8](pw.ptr), pw.ct, &hash[0]) ~= 0 then
			lib.bail('cannot hash password')
		end
		if reset then queries.auth_purge_type.exec(src, nil, uid, 'pw-%') end
		queries.auth_create_pw.exec(src, uid, binblob {ptr = &hash[0], ct = [hash.type.N]}, comment)
	end];

	auth_purge_pw = [terra(src: &lib.store.source, uid: uint64, handle: rawstring): {}
		queries.auth_purge_type.exec(src, handle, uid, 'pw-%')
	end];

	auth_purge_otp = [terra(src: &lib.store.source, uid: uint64, handle: rawstring): {}
................................................................................
		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];

	post_enum_parent = [terra(
		src: &lib.store.source,
		post: uint64
	): lib.mem.ptr(lib.mem.ptr(lib.store.post))
		var r = queries.post_enum_parent.exec(src,post)
		if r.sz == 0 then
			return [lib.mem.ptr(lib.mem.ptr(lib.store.post))].null()
		end
		defer r:free()
		var lst = lib.mem.heapa([lib.mem.ptr(lib.store.post)], r.sz)

		for i=0, r.sz do lst.ptr[i] = row_to_post(&r, i) end

		return lst
	end];

	thread_latest_arrival_calc = [terra(
		src: &lib.store.source,
		post: uint64
	): lib.store.timepoint
		var r = queries.thread_latest_arrival_calc.exec(src,post)
		if r.sz == 0 or r:null(0,0) then return 0 end
		var tp: lib.store.timepoint = r:int(int64,0,0)
		r:free()
		return tp
	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()
................................................................................

	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_conf_str_enum = nil;
	actor_conf_str_get = [terra(src: &lib.store.source, uid: uint64, key: rawstring): pstring
			var r = queries.actor_conf_str_get.exec(src, uid, key)
			if r.sz > 0 then
				var ret = r:String(0,0)
				r:free()
				return ret
			else return pstring.null() end
		end];
	actor_conf_str_set = [terra(src: &lib.store.source, uid: uint64, key: rawstring, value: rawstring): {}
			queries.actor_conf_str_set.exec(src,uid,key,value) end];
	actor_conf_str_reset = [terra(src: &lib.store.source, uid: uint64, key: rawstring): {}
			queries.actor_conf_str_reset.exec(src,uid,key) end];

	actor_conf_int_enum = nil;
	actor_conf_int_get = [terra(src: &lib.store.source, uid: uint64, key: rawstring)
			var r = queries.actor_conf_int_get.exec(src, uid, key)
			if r.sz > 0 then
				var ret = r:int(uint64,0,0)
				r:free()
				return ret, true
			end
			return 0, false
		end];
	actor_conf_int_set = [terra(src: &lib.store.source, uid: uint64, key: rawstring, value: uint64): {}
			queries.actor_conf_int_set.exec(src,uid,key,value) end];
	actor_conf_int_reset = [terra(src: &lib.store.source, uid: uint64, key: rawstring): {}
			queries.actor_conf_int_reset.exec(src,uid,key) end];

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

return b

Modified backend/schema/pgsql-drop.sql from [17a37aa5f6] to [fa02548662].

1
2
3
4
5


6
7
8
9
10
11
12
-- destroy absolutely everything

drop table if exists parsav_config cascade;
drop table if exists parsav_servers cascade;
drop table if exists parsav_actors cascade;


drop table if exists parsav_rights cascade;
drop table if exists parsav_posts cascade;
drop table if exists parsav_conversations cascade;
drop table if exists parsav_rels cascade;
drop table if exists parsav_acts cascade;
drop table if exists parsav_log cascade;
drop table if exists parsav_artifacts cascade;





>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- destroy absolutely everything

drop table if exists parsav_config cascade;
drop table if exists parsav_servers cascade;
drop table if exists parsav_actors cascade;
drop table if exists parsav_actor_conf_strs cascade;
drop table if exists parsav_actor_conf_ints cascade;
drop table if exists parsav_rights cascade;
drop table if exists parsav_posts cascade;
drop table if exists parsav_conversations cascade;
drop table if exists parsav_rels cascade;
drop table if exists parsav_acts cascade;
drop table if exists parsav_log cascade;
drop table if exists parsav_artifacts cascade;

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

167
168
169
170
171
172
173









174
175
176
177
	nature smallint not null, -- silence, suspend, disemvowel, censor, noreply, etc
	victim bigint not null, -- can be user, room, or post
	expire timestamp, -- auto-expires if set
	review timestamp,  -- brings up for review at given time if set
	reason text, -- visible to victim if set
	context text -- admin-only note
);










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







>
>
>
>
>
>
>
>
>




167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
	nature smallint not null, -- silence, suspend, disemvowel, censor, noreply, etc
	victim bigint not null, -- can be user, room, or post
	expire timestamp, -- auto-expires if set
	review timestamp,  -- brings up for review at given time if set
	reason text, -- visible to victim if set
	context text -- admin-only note
);

create table parsav_actor_conf_strs (
	uid bigint not null references parsav_actors(id) on delete cascade,
	key text not null, value text not null, unique (uid,key)
);
create table parsav_actor_conf_ints (
	uid bigint not null references parsav_actors(id) on delete cascade,
	key text not null, value bigint not null, unique (uid,key)
);

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

Modified config.lua from [3d0e5432a4] to [cd48dd2db6].

50
51
52
53
54
55
56

57
58
59
60
61

62
63
64
65
66
67
68
	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
	conf.build.branch = u.exec { 'fossil', 'branch', 'current' }







>





>







50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
	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'};
		{'live.js', 'text/javascript'}; -- rrrrrrrr
		{'default-avatar.webp', 'image/webp'};
		{'padlock.webp', 'image/webp'};
		{'warn.webp', 'image/webp'};
		{'query.webp', 'image/webp'};
	};
	default_ui_accent = tonumber(default('parsav_ui_default_accent',323));
}
if os.getenv('parsav_let_me_be_an_idiot') == "i know what i'm doing" then
	conf.braingeniousmode = true -- SOUND GENERAL QUARTERS
end
if u.ping '.fslckout' or u.ping '_FOSSIL_' then
	if u.ping '_FOSSIL_' then default_os = 'windows' end
	conf.build.branch = u.exec { 'fossil', 'branch', 'current' }

Modified math.t from [bc01716315] to [1d1d177061].

184
185
186
187
188
189
190


















191
192
193
194
195
196
197
		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







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







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
		else dgtct = dgtct + 1 end
	end else
		buf = buf - 1
		@buf = 0x30
	end
	return buf
end

terra m.decparse(s: pstring): {intptr, bool}
	if not s then return 0, false end
	var val:intptr = 0
	var c = s.ptr
	while @c ~= 0 do
		if @c >= 0x30 and @c <= 0x39 then
			val = val * 10
			val = val + (@c - 0x30)
		else
			return 0, false
		end

		c = c + 1
		if s.ct ~= 0 and (c - s.ptr > s.ct) then lib.dbg('reached end') return val, true end
	end
	return val, true
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

Modified mgtool.t from [faf7450a82] to [39281cf1cf].

59
60
61
62
63
64
65

66
67
68
69
70
71
72
...
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
...
334
335
336
337
338
339
340






341
342
343
344
345
346
347

	sid: uint64
	iname: rawstring
}
idelegate.metamethods.__methodmissing = macro(function(meth, self, ...)
	local expr = {...}
	local rt

	for _,f in pairs(lib.store.backend.entries) do
		local fn = f.field or f[1]
		local ft = f.type or f[2]
		if fn == meth then rt = ft.type.returntype break end
	end

	return quote
................................................................................
		if tmppw[i] >= 36 then
			tmppw[i] = tmppw[i] + (0x61 - 36)
		elseif tmppw[i] >= 10 then
			tmppw[i] = tmppw[i] + (0x41 - 10)
		else tmppw[i] = tmppw[i] + 0x30 end
	end
	lib.dbg('assigning temporary password')
	dlg:auth_create_pw(uid, reset, pstr {
		ptr = [rawstring](tmppw), ct = 32
	})
end

local terra ipc_report(acks: lib.mem.ptr(lib.ipc.ack), rep: rawstring)
	var decbuf: int8[21]
	for i=0,acks.ct do
		var num = lib.math.decstr(acks(i).clid, &decbuf[20])
		if acks(i).success 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







>







 







|
|
|







 







>
>
>
>
>
>







59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
...
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
...
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354

	sid: uint64
	iname: rawstring
}
idelegate.metamethods.__methodmissing = macro(function(meth, self, ...)
	local expr = {...}
	local rt

	for _,f in pairs(lib.store.backend.entries) do
		local fn = f.field or f[1]
		local ft = f.type or f[2]
		if fn == meth then rt = ft.type.returntype break end
	end

	return quote
................................................................................
		if tmppw[i] >= 36 then
			tmppw[i] = tmppw[i] + (0x61 - 36)
		elseif tmppw[i] >= 10 then
			tmppw[i] = tmppw[i] + (0x41 - 10)
		else tmppw[i] = tmppw[i] + 0x30 end
	end
	lib.dbg('assigning temporary password')
	dlg:auth_attach_pw(uid, reset,
		pstr { ptr = [rawstring](tmppw), ct = 32 },
		lib.str.plit 'temporary password');
end

local terra ipc_report(acks: lib.mem.ptr(lib.ipc.ack), rep: rawstring)
	var decbuf: int8[21]
	for i=0,acks.ct do
		var num = lib.math.decstr(acks(i).clid, &decbuf[20])
		if acks(i).success 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 == 2 and
				lib.str.cmp(cfmode.arglist(0),'reset') == 0 or
				lib.str.cmp(cfmode.arglist(0),'clear') == 0 or
				lib.str.cmp(cfmode.arglist(0),'unset') == 0 then
				dlg:conf_reset(cfmode.arglist(1))
				lib.report('parameter cleared')
			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

Modified parsav.md from [409d9b6b60] to [bfa0a26bd5].

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

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

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

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

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

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

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

## building

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








|












|







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

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

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

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

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

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

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

## building

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

Modified parsav.t from [90c24eca6f] to [4b4b2876e4].

602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
	print(util.dump(config))
	os.exit(0)
end

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

if bflag('quiet','q') then holler = function() end end







|







602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
	print(util.dump(config))
	os.exit(0)
end

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

if bflag('quiet','q') then holler = function() end end

Modified render/conf.t from [1b75d5dd6d] to [fa178f73b5].

1
2
3
4
5
6
7

8
9
10
11
12
13
14
-- vim: ft=terra
local pstr = lib.mem.ptr(int8)
local pref = lib.mem.ref(int8)

local mappings = {
	{url = 'profile', title = 'account profile', render = 'profile'};
	{url = 'avi', title = 'avatar', render = 'avatar'};

	{url = 'sec', title = 'security', render = 'sec'};
	{url = 'rel', title = 'relationships', render = 'rel'};
	{url = 'qnt', title = 'quarantine', render = 'quarantine'};
	{url = 'acl', title = 'access control shortcuts', render = 'acl'};
	{url = 'rooms', title = 'chatrooms', render = 'rooms'};
	{url = 'circles', title = 'circles', render = 'circles'};








>







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

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

Modified render/conf/profile.t from [7f970c2f4a] to [864a63a85e].

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







>




>





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

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 hue: int8[21]
	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,''));
		hue = lib.math.decstr(co.ui_hue, &hue[20]);
	}
	return c:tostr()
end

return render_conf_profile

Modified render/timeline.t from [2afba48373] to [ab5808172b].

23
24
25
26
27
28
29


30
31


32
33
34

35
36
37
38
39
40
41
42
43
44
45
			to_idx = 64;
		})
	elseif mode == modes.fediglobal then
	elseif mode == modes.circle then
	end

	var acc: lib.str.acc acc:init(1024)


	for i = 0, posts.sz do
		lib.render.tweet(co, posts(i).ptr, &acc)


		posts(i):free()
	end
	posts:free()


	var doc = [lib.srv.convo.page] {
		title = lib.str.plit'timeline';
		body = acc:finalize();
		class = lib.str.plit'timeline';
		cache = false;
	}
	co:stdpage(doc)
	doc.body:free()
end
return render_timeline







>
>


>
>



>







|



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
			to_idx = 64;
		})
	elseif mode == modes.fediglobal then
	elseif mode == modes.circle then
	end

	var acc: lib.str.acc acc:init(1024)
	acc:lpush('<div id="tl" data-live="10">')
	var newest: lib.store.timepoint = 0
	for i = 0, posts.sz do
		lib.render.tweet(co, posts(i).ptr, &acc)
		var t = lib.math.biggest(lib.math.biggest(posts(i).ptr.posted, posts(i).ptr.discovered),posts(i).ptr.edited)
		if t > newest then newest = t end
		posts(i):free()
	end
	posts:free()
	acc:lpush('</div>')

	var doc = [lib.srv.convo.page] {
		title = lib.str.plit'timeline';
		body = acc:finalize();
		class = lib.str.plit'timeline';
		cache = false;
	}
	co:livepage(doc,newest)
	doc.body:free()
end
return render_timeline

Modified render/tweet-page.t from [ad592d16bd] to [005ba03599].

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






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







>
>


<


>







 







>
>
>
>
>
|
|
<



|


|






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
..
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
-- 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_replies(
	co: &lib.srv.convo,
	acc: &lib.str.acc,
	id: uint64
): {}
	var replies = co.srv:post_enum_parent(id)
	if replies.ct == 0 then return end
	acc:lpush('<div class="thread">')
	for i=0, replies.ct do
		var post = replies(i).ptr
		lib.render.tweet(co, post, acc)
		render_tweet_replies(co, acc, post.id)
	end
	acc:lpush('</div>')
end

local terra 
render_tweet_page(
	co: &lib.srv.convo,
	path: lib.mem.ptr(pref),
	p: &lib.store.post
): {}
	var livetime = co.srv:thread_latest_arrival_calc(p.id)

	var pg: lib.str.acc pg:init(256)
	lib.render.tweet(co, p, &pg)


	if co.aid ~= 0 then
		pg:lpush('<form class="action-bar" method="post">')
		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
................................................................................
		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>')

	end
	pg:lpush('<div id="convo" data-live="10">')
	render_tweet_replies(co, &pg, p.id)
	pg:lpush('</div>')

	if co.aid ~= 0 and co.who.rights.powers.post() then
		lib.render.compose(co, nil, &pg)

	end

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

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

return render_tweet_page

Modified render/tweet.t from [ac0f8e680f] to [77ab77b2db].

32
33
34
35
36
37
38

39












40
41
42
43
44
45
		subject = cs(lib.coalesce(p.subject,''));
		nym = fullname;
		when = cs(&timestr[0]);
		avatar = cs(lib.trn(author.origin == 0, avistr.buf,
			lib.coalesce(author.avatar, '/s/default-avatar.webp')));
		acctlink = cs(author.xid);
		permalink = permalink:finalize();

	}












	defer tpl.permalink:free()
	if acc ~= nil then tpl:append(acc) return [lib.mem.ptr(int8)]{ptr=nil,ct=0} end
	var txt = tpl:tostr()
	return txt
end
return render_tweet







>

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






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

	var attrbuf: int8[32]
	if p.accent ~= -1 and p.accent ~= co.ui_hue then
		var hdecbuf: int8[21]
		var hdec = lib.math.decstr(p.accent, &hdecbuf[20])
		lib.str.cpy(&attrbuf[0], ' style="--hue:')
		lib.str.cpy(&attrbuf[14], hdec)
		var len = &hdecbuf[20] - hdec 
		lib.str.cpy(&attrbuf[14] + len, '"')
		tpl.attr = &attrbuf[0]
	end

	defer tpl.permalink:free()
	if acc ~= nil then tpl:append(acc) return [lib.mem.ptr(int8)]{ptr=nil,ct=0} end
	var txt = tpl:tostr()
	return txt
end
return render_tweet

Modified render/user-page.t from [8e478d7a95] to [08cdf2fd9f].

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
	var stoptime = lib.osclock.time(nil)
	var posts = co.srv:post_enum_author_uid(actor.id, lib.store.range {
		mode = 1; -- T->I
		from_time = stoptime;
		to_idx = 64;
	})



	for i = 0, posts.sz do
		lib.render.tweet(co, posts(i).ptr, &acc)


		posts(i):free()
	end
	posts:free()


	var bdf = acc:finalize()
	co:stdpage([lib.srv.convo.page] {
		title = tiptr; body = bdf;
		class = lib.str.plit 'profile';
		cache = false;
	})

	tiptr:free()
	bdf:free()
end

return render_userpage







>
>


>
>



>


|



|






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
	var stoptime = lib.osclock.time(nil)
	var posts = co.srv:post_enum_author_uid(actor.id, lib.store.range {
		mode = 1; -- T->I
		from_time = stoptime;
		to_idx = 64;
	})

	acc:lpush('<div id="feed" data-live="10">')
	var newest: lib.store.timepoint = 0
	for i = 0, posts.sz do
		lib.render.tweet(co, posts(i).ptr, &acc)
		var t = lib.math.biggest(lib.math.biggest(posts(i).ptr.posted, posts(i).ptr.discovered),posts(i).ptr.edited)
		if t > newest then newest = t end
		posts(i):free()
	end
	posts:free()
	acc:lpush('</div>')

	var bdf = acc:finalize()
	co:livepage([lib.srv.convo.page] {
		title = tiptr; body = bdf;
		class = lib.str.plit 'profile';
		cache = false;
	}, newest)

	tiptr:free()
	bdf:free()
end

return render_userpage

Modified route.t from [a9eb70a00e] to [f59188addc].

2
3
4
5
6
7
8

9
10
11
12
13
14
15
..
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
...
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
...
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
...
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
...
220
221
222
223
224
225
226
227













228
229
230
231
232
233
234
...
240
241
242
243
244
245
246























247
248
249
250
251
252
253
...
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
local r = lib.srv.route
local method = lib.http.method
local pstring = lib.mem.ptr(int8)
local rstring = lib.mem.ref(int8)
local hpath = lib.mem.ptr(rstring)
local http = {}


terra http.actor_profile_xid(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t)
	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
................................................................................
	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
		var usn, usnl = co:postv('user')
		var am, aml = co:postv('authmethod')
		var chrs, chrsl = co:postv('response')
		var cs, authok = co.srv:actor_auth_how(co.peer, usn)
................................................................................
end

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')
................................................................................
		end
		if subj == nil then subj = '' end

		var p = lib.store.post {
			author = co.who.id, acl = acl;
			body = text, subject = subj;
		}
		var newid = co.srv:post_create(&p)

		var idbuf: int8[lib.math.shorthand.maxlen]
		var idlen = lib.math.shorthand.gen(newid, idbuf)
		var redirto: lib.str.acc redirto:compose('/post/',{idbuf,idlen}) defer redirto:free()
		co:reroute(redirto.buf)
	end
end
................................................................................
	if path.ct == 3 then
		var lnk: lib.str.acc lnk:compose('/post/', path(1))
		var lnkp = lnk:finalize() defer lnkp:free()
		if post(0).author ~= co.who.id then
			co:complain(403, 'forbidden', 'you cannot alter other people\'s posts')
			return
		elseif path(2):cmp(lib.str.lit 'edit') then
			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)
				co:reroute(lnkp.ptr)
			end
			return
		elseif path(2):cmp(lib.str.lit 'del') then
			if meth == method.get then
				var conf = data.view.confirm {
					title = lib.str.plit 'delete post';
					query = lib.str.plit 'are you sure you want to delete this post?';
					cancel = lnkp
				}
				var body = conf:tostr() defer body:free()
				co:stdpage([lib.srv.convo.page] {
................................................................................
					co:reroute('/') -- TODO maybe return to parent or conversation if possible
					return
				else goto badop end
			end
		else goto badurl end
	end

	if meth == method.post then goto badop end














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

	::badurl:: do co:complain(404, 'invalid URL', 'this URL does not reference extant content or functionality') return end
	::badop :: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
end
................................................................................
		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
			if not co.who.rights.powers.config() then goto nopriv end
		elseif path(1):cmp(lib.str.lit 'brand') then
			if not co.who.rights.powers.rebrand() then goto nopriv end
		elseif path(1):cmp(lib.str.lit 'users') then
................................................................................

-- entry points
terra r.dispatch_http(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t)
	lib.dbg('handling URI of form ', {uri.ptr,uri.ct})
	co.navbar = lib.render.nav(co)
	-- some routes are non-hierarchical, and can be resolved with a simple strcmp
	-- we run through those first before giving up and parsing the URI
	if uri.ptr[0] ~= @'/' then
		co:complain(404, 'what the hell', 'how did you do that')
		return
	elseif uri.ct == 1 then -- root
		if (co.srv.cfg.pol_sec == lib.srv.secmode.private or
		   co.srv.cfg.pol_sec == lib.srv.secmode.lockdown) and co.aid == 0 then
		   http.login_form(co, meth)
		else
			-- FIXME display home screen
			http.timeline(co, hpath {ptr=nil})
			goto notfound
		end
		return
	elseif uri.ptr[1] == @'@' then
		http.actor_profile_xid(co, uri, meth)
		return
	elseif uri.ptr[1] == @'s' and uri.ptr[2] == @'/' and uri.ct > 3 then
		if meth ~= method.get then goto wrongmeth end

		if not http.static_content(co, uri.ptr + 3, uri.ct - 3) then goto notfound end
		return
	elseif lib.str.ncmp('/avi/', uri.ptr, 5) == 0 then
		http.local_avatar(co, [lib.mem.ptr(int8)] {ptr = uri.ptr + 5, ct = uri.ct - 5})
		return
	elseif lib.str.ncmp('/compose', uri.ptr, lib.math.biggest(uri.ct,8)) == 0 then
		if co.aid == 0 then co:reroute('/login') return end
		http.post_compose(co,meth)
		return
	elseif lib.str.ncmp('/login', uri.ptr, lib.math.biggest(uri.ct,6)) == 0 then
		if co.aid == 0
			then http.login_form(co, meth)
			else co:reroute('/')
		end
		return
	elseif lib.str.ncmp('/logout', uri.ptr, lib.math.biggest(uri.ct,7)) == 0 then
		if co.aid == 0
			then goto notfound
			else co:reroute_cookie('/','auth=; Path=/')
		end
		return
	else -- hierarchical routes
		var path = lib.http.hier(uri) defer path:free()
		if path.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







>







 







|







 







|







 







|







 







|







 







|







 







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







 







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







 







|

<




<
<
|
<
<
<


<

<
>

<


<



<





<





<









|





<

>





2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
..
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
...
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
...
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
...
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
...
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
...
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
...
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
local r = lib.srv.route
local method = lib.http.method
local pstring = lib.mem.ptr(int8)
local rstring = lib.mem.ref(int8)
local hpath = lib.mem.ptr(rstring)
local http = {}

terra meth_get(meth: method.t) return (meth == method.get) or (meth == method.head) end
terra http.actor_profile_xid(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t)
	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
................................................................................
	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_get(meth) then
		-- request a username
		lib.render.login(co, nil, nil, lib.str.plit(nil))
	elseif meth == method.post then
		var usn, usnl = co:postv('user')
		var am, aml = co:postv('authmethod')
		var chrs, chrsl = co:postv('response')
		var cs, authok = co.srv:actor_auth_how(co.peer, usn)
................................................................................
end

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_get(meth) 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')
................................................................................
		end
		if subj == nil then subj = '' end

		var p = lib.store.post {
			author = co.who.id, acl = acl;
			body = text, subject = subj;
		}
		var newid = p:publish(co.srv)

		var idbuf: int8[lib.math.shorthand.maxlen]
		var idlen = lib.math.shorthand.gen(newid, idbuf)
		var redirto: lib.str.acc redirto:compose('/post/',{idbuf,idlen}) defer redirto:free()
		co:reroute(redirto.buf)
	end
end
................................................................................
	if path.ct == 3 then
		var lnk: lib.str.acc lnk:compose('/post/', path(1))
		var lnkp = lnk:finalize() defer lnkp:free()
		if post(0).author ~= co.who.id then
			co:complain(403, 'forbidden', 'you cannot alter other people\'s posts')
			return
		elseif path(2):cmp(lib.str.lit 'edit') then
			if meth_get(meth) then
				lib.render.compose(co, post.ptr, nil)
				return
			elseif meth == method.post then
				var newbody = co:postv('post')._0
				var newacl = co:postv('acl')._0
				var newsubj = co:postv('subject')._0
				if newbody ~= nil then post(0).body = newbody end
................................................................................
				if newacl  ~= nil then post(0).acl = newacl end
				if newsubj ~= nil then post(0).subject = newsubj end
				post(0):save(true)
				co:reroute(lnkp.ptr)
			end
			return
		elseif path(2):cmp(lib.str.lit 'del') then
			if meth_get(meth) then
				var conf = data.view.confirm {
					title = lib.str.plit 'delete post';
					query = lib.str.plit 'are you sure you want to delete this post?';
					cancel = lnkp
				}
				var body = conf:tostr() defer body:free()
				co:stdpage([lib.srv.convo.page] {
................................................................................
					co:reroute('/') -- TODO maybe return to parent or conversation if possible
					return
				else goto badop end
			end
		else goto badurl end
	end

	if meth == method.post then
		var replytext = co:ppostv('post')
		var acl = co:ppostv('acl')
		var subj = co:ppostv('subject')
		if not acl then acl = lib.str.plit 'all' end
		if not replytext then goto badop end
		
		var reply = lib.store.post {
			author = co.who.id, parent = pid;
			subject = subj.ptr, acl = acl.ptr, body = replytext.ptr;
		}

		reply:publish(co.srv)
	end

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

	::badurl:: do co:complain(404, 'invalid URL', 'this URL does not reference extant content or functionality') return end
	::badop :: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
end
................................................................................
		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)

			var act = co:ppostv('act')
			var resethue = false
			if act:ref() then
				resethue = act:cmp(lib.str.plit 'reset-hue')
			end

			if not resethue then
				var shue = co:ppostv('hue')
				var nhue, okhue = lib.math.decparse(shue)
				if okhue and nhue ~= co.ui_hue then
					if nhue == co.srv.cfg.ui_hue
						then resethue = true
						else co.srv:actor_conf_int_set(co.who.id, 'ui-accent', nhue)
					end
					co.ui_hue = nhue
				end
			end
			if resethue then
				co.srv:actor_conf_int_reset(co.who.id, 'ui-accent')
				co.ui_hue = co.srv.cfg.ui_hue
			end

			msg = lib.str.plit 'profile changes saved'
			--user_refresh = true -- not really necessary here, actually
		elseif path(1):cmp(lib.str.lit 'srv') then
			if not co.who.rights.powers.config() then goto nopriv end
		elseif path(1):cmp(lib.str.lit 'brand') then
			if not co.who.rights.powers.rebrand() then goto nopriv end
		elseif path(1):cmp(lib.str.lit 'users') then
................................................................................

-- entry points
terra r.dispatch_http(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t)
	lib.dbg('handling URI of form ', {uri.ptr,uri.ct})
	co.navbar = lib.render.nav(co)
	-- some routes are non-hierarchical, and can be resolved with a simple strcmp
	-- we run through those first before giving up and parsing the URI
	if uri.ptr == nil or uri.ptr[0] ~= @'/' then
		co:complain(404, 'what the hell', 'how did you do that')

	elseif uri.ct == 1 then -- root
		if (co.srv.cfg.pol_sec == lib.srv.secmode.private or
		   co.srv.cfg.pol_sec == lib.srv.secmode.lockdown) and co.aid == 0 then
		   http.login_form(co, meth)


		else http.timeline(co, hpath {ptr=nil}) end



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

	elseif uri.ptr[1] == @'s' and uri.ptr[2] == @'/' and uri.ct > 3 then

		if not meth_get(meth) then goto wrongmeth end
		if not http.static_content(co, uri.ptr + 3, uri.ct - 3) then goto notfound end

	elseif lib.str.ncmp('/avi/', uri.ptr, 5) == 0 then
		http.local_avatar(co, [lib.mem.ptr(int8)] {ptr = uri.ptr + 5, ct = uri.ct - 5})

	elseif lib.str.ncmp('/compose', uri.ptr, lib.math.biggest(uri.ct,8)) == 0 then
		if co.aid == 0 then co:reroute('/login') return end
		http.post_compose(co,meth)

	elseif lib.str.ncmp('/login', uri.ptr, lib.math.biggest(uri.ct,6)) == 0 then
		if co.aid == 0
			then http.login_form(co, meth)
			else co:reroute('/')
		end

	elseif lib.str.ncmp('/logout', uri.ptr, lib.math.biggest(uri.ct,7)) == 0 then
		if co.aid == 0
			then goto notfound
			else co:reroute_cookie('/','auth=; Path=/')
		end

	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 not meth_get(meth) 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

	end
	do 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 srv.t from [9e0d1b7489] to [675eda18a7].

7
8
9
10
11
12
13

14
15
16
17
18
19
20
...
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
...
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
...
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
...
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
...
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
...
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
...
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
...
423
424
425
426
427
428
429


430
431







432
433
434
435
436
437
438
...
442
443
444
445
446
447
448

449
450

451
452

453
454

455
456
457
458
459
460
461
...
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
706
707
708
709
710
711

712






713

714
715
716
717
718
719
720
721
	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
................................................................................
					if [ok] then break
						else r = empty end
				end
			end
		in r end
	end
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
	varbuf: lib.mem.ptr(int8)
	vbofs: &int8
}








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

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
































































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

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

	}

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

................................................................................
		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 ti: lib.str.acc ti:compose('error :: ', title)
	var bo: lib.str.acc bo:compose('<div class="message"><img class="icon" src="/s/warn.webp"><h1>',title,'</h1><p>',msg,'</p></div>')
	var body = data.view.docskel {
		instance = self.srv.cfg.instance;
		title = ti:finalize();
		body = bo:finalize();
		class = lib.str.plit 'error';
		navlinks = lib.coalesce(self.navbar, [lib.mem.ptr(int8)]{ptr='',ct=0});
	}

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

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

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

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

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

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

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

	doc:send(self.con,200,[lib.mem.ptr(lib.http.header)] {ct = [hdrs.type.N] - lib.trn(pg.cache,1,0), ptr = &hdrs[0]})
end

-- CALL ONLY ONCE PER VAR
terra convo:postv(name: rawstring)
	if self.varbuf.ptr == nil then
		self.varbuf = lib.mem.heapa(int8, self.msg.body.len + self.msg.query.len)
		self.vbofs = self.varbuf.ptr
	end
	var o = lib.net.mg_http_get_var(&self.msg.body, name, self.vbofs, self.varbuf.ct - (self.vbofs - self.varbuf.ptr))
................................................................................
		if lib.str.ncmp(mimevar.ptr, mime, lib.math.biggest(mimevar.ct, [#mime])) == 0 then
			ret = [lib.http.mime[name]]
		else ret = [mimeneg] end
	in ret end
end

local handle = {
	http = terra(con: &lib.net.mg_connection, event: int, p: &opaque, ext: &opaque)
		var server = [&srv](ext)
		var mgpeer = getpeer(con)
		var peer = lib.store.inet { port = mgpeer.port; }
		if mgpeer.is_ip6 then peer.pv = 6 else peer.pv = 4 end
		if peer.pv == 6 then
			for i = 0, 16 do peer.v6[i] = mgpeer.ip6[i] end
		else -- v4
			@[&uint32](&peer.v4) = mgpeer.ip
................................................................................
		-- for now i'm leaving it as is, but note that netmask restrictions
		-- WILL NOT WORK until upstream gets its shit together. FIXME

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

		switch event do
			case lib.net.MG_EV_HTTP_MSG then
				lib.dbg('routing HTTP request')
				var msg = [&lib.net.mg_http_message](p)
				var co = convo {
					con = con, srv = server, msg = msg;
					aid = 0, 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
				do var acc = lib.http.findheader(msg, 'Accept')
					-- TODO handle q-value
					if acc.ptr ~= nil then
						var [mimevar] = [lib.mem.ref(int8)] { ptr = acc.ptr }
						var i = 0 while i < acc.ct do
							if acc.ptr[i] == @',' or acc.ptr[i] == @';' then
								mimevar.ct = (acc.ptr+i) - mimevar.ptr
								var t = [mimeneg]
								if t ~= lib.http.mime.none then
									co.reqtype = t
................................................................................
					else co.reqtype = lib.http.mime.html end
				::foundtype::end

				-- we need to check if there's any cookies sent with the request,
				-- and if so, whether they contain any credentials. this will be
				-- used to set the auth parameters in the http conversation
				var cookies_p = lib.http.findheader(msg, 'Cookie')
				if cookies_p ~= nil then
					var cookies = cookies_p.ptr
					var key = [lib.mem.ref(int8)] {ptr = cookies, ct = 0}
					var val = [lib.mem.ref(int8)] {ptr = nil, ct = 0}
					var i = 0 while i < cookies_p.ct    and
					                cookies[i] ~= 0     and
					                cookies[i] ~= @'\r' and
									cookies[i] ~= @'\n' do -- cover all the bases
................................................................................
				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)

				var uri = uridec
				if urideclen == -1 then
					for i = 0,msg.uri.len do
................................................................................
						end
					end
					uri.ct = msg.uri.len
				else uri.ct = urideclen end
				lib.dbg('routing URI ', {uri.ptr, uri.ct})
				
				if lib.str.ncmp('GET', msg.method.ptr, msg.method.len) == 0 then

					route.dispatch_http(&co, uri, [lib.http.method.get])
				elseif lib.str.ncmp('POST', msg.method.ptr, msg.method.len) == 0 then

					route.dispatch_http(&co, uri, [lib.http.method.post])
				elseif lib.str.ncmp('HEAD', msg.method.ptr, msg.method.len) == 0 then

					route.dispatch_http(&co, uri, [lib.http.method.head])
				elseif lib.str.ncmp('OPTIONS', msg.method.ptr, msg.method.len) == 0 then

					route.dispatch_http(&co, uri, [lib.http.method.options])
				else
					co:complain(400,'unknown method','you have submitted an invalid http request')
				end

				if co.aid ~= 0 then lib.mem.heapf(co.who) end
				if co.varbuf.ptr ~= nil then co.varbuf:free() end
................................................................................
	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
			self.pol_sec = secmode.private
		elseif lib.str.cmp(smode.ptr, 'lockdown') == 0 then
			self.pol_sec = secmode.lockdown
		elseif lib.str.cmp(smode.ptr, 'isolate') == 0 then
			self.pol_sec = secmode.isolate
		end

	end






	smode:free()

end

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







>







 







>
>
>
>
>
>
>
>
>
>










>
>

>






>
>
>
>
>
>
>







 







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













>







 







|
<
<
<



|
<



|


|
<
<
<
<
<
<







 







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







 







|
|







 







|


|




|




>





|







 







|







 







>
>


>
>
>
>
>
>
>







 







>


>


>


>







 







<
|
>








<
|
>








<
|
>













>

>
>
>
>
>
>
|
>








7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
...
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
...
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
...
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
...
282
283
284
285
286
287
288
























289
290
291
292
293
294
295
...
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
...
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
...
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
...
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
...
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
...
735
736
737
738
739
740
741

742
743
744
745
746
747
748
749
750
751

752
753
754
755
756
757
758
759
760
761

762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
	secret: lib.mem.ptr(int8)
	pol_sec: secmode.t
	pol_reg: bool
	credmgd: bool
	maxupsz: intptr
	instance: lib.mem.ptr(int8)
	overlord: &srv
	ui_hue: uint16
}
local struct srv {
	sources: lib.mem.ptr(lib.store.source)
	webmgr: lib.net.mg_mgr
	webcon: &lib.net.mg_connection
	cfg: cfgcache
	id: rawstring
................................................................................
					if [ok] then break
						else r = empty end
				end
			end
		in r end
	end
end)

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

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

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

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

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

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

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

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

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

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

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

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

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

................................................................................
		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)
	if msg == nil then msg = "i'm sorry, dave. i can't let you do that" end




	var ti: lib.str.acc ti:compose('error :: ', title)
	var bo: lib.str.acc bo:compose('<div class="message"><img class="icon" src="/s/warn.webp"><h1>',title,'</h1><p>',msg,'</p></div>')
	var body = [convo.page] {

		title = ti:finalize();
		body = bo:finalize();
		class = lib.str.plit 'error';
		cache = false;
	}

	self:statpage(code, body)







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

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

























-- CALL ONLY ONCE PER VAR
terra convo:postv(name: rawstring)
	if self.varbuf.ptr == nil then
		self.varbuf = lib.mem.heapa(int8, self.msg.body.len + self.msg.query.len)
		self.vbofs = self.varbuf.ptr
	end
	var o = lib.net.mg_http_get_var(&self.msg.body, name, self.vbofs, self.varbuf.ct - (self.vbofs - self.varbuf.ptr))
................................................................................
		if lib.str.ncmp(mimevar.ptr, mime, lib.math.biggest(mimevar.ct, [#mime])) == 0 then
			ret = [lib.http.mime[name]]
		else ret = [mimeneg] end
	in ret end
end

local handle = {
	http = terra(con: &lib.net.mg_connection, event_kind: int, event: &opaque, userdata: &opaque)
		var server = [&srv](userdata)
		var mgpeer = getpeer(con)
		var peer = lib.store.inet { port = mgpeer.port; }
		if mgpeer.is_ip6 then peer.pv = 6 else peer.pv = 4 end
		if peer.pv == 6 then
			for i = 0, 16 do peer.v6[i] = mgpeer.ip6[i] end
		else -- v4
			@[&uint32](&peer.v4) = mgpeer.ip
................................................................................
		-- for now i'm leaving it as is, but note that netmask restrictions
		-- WILL NOT WORK until upstream gets its shit together. FIXME

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

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

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

				-- we need to check if there's any cookies sent with the request,
				-- and if so, whether they contain any credentials. this will be
				-- used to set the auth parameters in the http conversation
				var cookies_p = lib.http.findheader(msg, 'Cookie')
				if cookies_p ~= nil and cookies_p.ptr ~= nil then
					var cookies = cookies_p.ptr
					var key = [lib.mem.ref(int8)] {ptr = cookies, ct = 0}
					var val = [lib.mem.ref(int8)] {ptr = nil, ct = 0}
					var i = 0 while i < cookies_p.ct    and
					                cookies[i] ~= 0     and
					                cookies[i] ~= @'\r' and
									cookies[i] ~= @'\n' do -- cover all the bases
................................................................................
				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)
						var userhue, hueok = server:actor_conf_int_get(co.who.id, 'ui-accent')
						if hueok then co.ui_hue = userhue end
					end
				end

				var livelast_p = lib.http.findheader(msg, 'X-Live-Last-Arrival')
				if livelast_p ~= nil and livelast_p.ptr ~= nil then
					var ll, ok = lib.math.decparse(pstring{ptr = livelast_p.ptr, ct = livelast_p.ct - 1})
					if ok then co.live_last = ll end
				end


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

				var uri = uridec
				if urideclen == -1 then
					for i = 0,msg.uri.len do
................................................................................
						end
					end
					uri.ct = msg.uri.len
				else uri.ct = urideclen end
				lib.dbg('routing URI ', {uri.ptr, uri.ct})
				
				if lib.str.ncmp('GET', msg.method.ptr, msg.method.len) == 0 then
					co.method = [lib.http.method.get]
					route.dispatch_http(&co, uri, [lib.http.method.get])
				elseif lib.str.ncmp('POST', msg.method.ptr, msg.method.len) == 0 then
					co.method = [lib.http.method.get]
					route.dispatch_http(&co, uri, [lib.http.method.post])
				elseif lib.str.ncmp('HEAD', msg.method.ptr, msg.method.len) == 0 then
					co.method = [lib.http.method.head]
					route.dispatch_http(&co, uri, [lib.http.method.head])
				elseif lib.str.ncmp('OPTIONS', msg.method.ptr, msg.method.len) == 0 then
					co.method = [lib.http.method.options]
					route.dispatch_http(&co, uri, [lib.http.method.options])
				else
					co:complain(400,'unknown method','you have submitted an invalid http request')
				end

				if co.aid ~= 0 then lib.mem.heapf(co.who) end
				if co.varbuf.ptr ~= nil then co.varbuf:free() end
................................................................................
	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

		sreg:free()
	end 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

		sreg:free()
	end 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

		sreg:free() end
	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
			self.pol_sec = secmode.private
		elseif lib.str.cmp(smode.ptr, 'lockdown') == 0 then
			self.pol_sec = secmode.lockdown
		elseif lib.str.cmp(smode.ptr, 'isolate') == 0 then
			self.pol_sec = secmode.isolate
		end
		smode:free()
	end

	self.ui_hue = config.default_ui_accent
	var shue = self.overlord:conf_get('ui-accent')
	if shue.ptr ~= nil then
		var hue,ok = lib.math.decparse(shue)
		if ok then self.ui_hue = hue end
		shue:free()
	end
end

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

Added static/live.js version [6fb4c9ec70].







































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
/* first things first, we need to scan over the document and see
 * if there are any UI elements unfortunate enough to need
 * interactivity beyond what native HTML+CSS can provide. if so,
 * we attach the appropriate listeners to them. */
window.addEventListener('load', function() {
	/* update hue-picker background when slider is adjusted */
	document.querySelectorAll('.color-picker').forEach(function(box) {
		let slider = box.querySelector('[data-color-pick]');
		box.style.setProperty('--hue', slider.value);
		slider.addEventListener('input', function(e) {
			box.style.setProperty('--hue', e.target.value);
		});
	});

	/* the main purpose of this script -- by marking itself with the
	 * data-live property, an html element registers itself for live
	 * updates from the server. this is pretty straightforward: we
	 * retrieve this url from the server as a get request, create a
	 * tree from its html, find the element in question, ferret out
	 * any deltas, and apply them. */
	document.querySelectorAll('*[data-live]').forEach(function(container) {
		let interv = parseFloat(container.attributes.getNamedItem('data-live').nodeValue) * 1000;
		container._liveLastArrival = '0'; /* TODO include header for this */

		window.setInterval(function() {
			var req = new Request(window.location, {
				method: 'GET',
				headers: {
					'X-Live-Last-Arrival': container._liveLastArrival
				}
			})
			
			fetch(req).then(function(resp) {
				if (!resp.ok) return;
				let newest = resp.headers.get('X-Live-Newest-Artifact');
				if (newest <= container._liveLastArrival) {
					resp.body.cancel();
					return;
				}
				container._liveLastArrival = newest

				resp.text().then(function(htmlbody) {
					var parser = new DOMParser();
					var newdoc = parser.parseFromString(htmlbody,'text/html')
					// console.log(newdoc.getElementById(container.id).innerHTML)
					container.innerHTML = newdoc.getElementById(container.id).innerHTML
				})
			})
		}, interv)
	});
});

Modified static/style.scss from [ada3763759] to [0e6b10a9e2].

1
2
3
4
5
6


7


8
9
10
11
12
13
14
..
20
21
22
23
24
25
26
27
28
29
30

31
32
33
34
35
36
37
..
69
70
71
72
73
74
75


76
77
78
79
80
81
82
...
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
...
341
342
343
344
345
346
347

348
349
350
351
352
353
354
...
458
459
460
461
462
463
464





465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
...
494
495
496
497
498
499
500

501
502
503
504
505
506
507
...
523
524
525
526
527
528
529



















530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
...
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
...
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







$color: hsl(323,100%,65%);
%sans { font-family: "Alegreya Sans", "Open Sans", sans-serif; }
%serif { font-family: Alegreya, GaramondNo8, "Garamond Premier Pro", "Adobe Garamond", Garamond, Junicode, serif; }
%teletype { font-family: "Inconsolata LGC", Inconsolata, monospace; font-size: 12pt !important; }

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





body {
	@extend %sans;
	background-color: tone(-55%);
	color: tone(25%);
	font-size: 14pt;
	margin: 0;
	padding: 0;
................................................................................
::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 {
................................................................................
	border: 1px solid black;
	color: tone(25%);
	text-shadow: 1px 1px black;
	text-decoration: none;
	text-align: center;
	cursor: default;
	user-select: none;


	background: linear-gradient(to bottom,
		tone(-47%),
		tone(-50%) 15%,
		tone(-50%) 75%,
		tone(-53%)
	);
	&:hover, &:focus {
................................................................................
			> a[href] {
				display: block;
				padding: 0.25in 0.10in;
				//padding: calc((25% - 1em)/2) 0.15in;
				&, &::after { transition: 0.3s; }
				text-shadow: 1px 1px 1px black;
				&:hover{
					transform: scale(120%);
				}
			}
		}
	}
}

main {
................................................................................
	@extend %box;
	display: block;
	width: 4in;
	margin:auto;
	padding: 0.5in;
	text-align: center;
	menu:first-of-type { margin-top: 0.3in; }

}

div.login {
	@extend %box;
	width: 4in;
	padding: 0.4in;
	> .msg {
................................................................................
	font-size: 1.5ex !important;
	letter-spacing: 1.3px;
	padding-bottom: 3px;
	border-radius: 2px;
	vertical-align: baseline;
	box-shadow: 1px 1px 1px black;
}






div.post {
	@extend %box;
	display: grid;
	grid-template-columns: 1in 1fr max-content;
	grid-template-rows: min-content max-content;
	margin-bottom: 0.1in;
	>.avatar {
		grid-column: 1/2; grid-row: 1/2;
		img { display: block; width: 1in; margin:0; }
		background: linear-gradient(to bottom, tone(-53%), tone(-57%));
	}
	>a[href].username {
		display: block;
		grid-column: 1/3;
		grid-row: 2/3;
		text-align: left;
................................................................................
	}
	>.content {
		grid-column: 2/4; grid-row: 1/2;
		padding: 0.2in;
		@extend %serif;
		font-size: 110%;
		text-align: justify;

	}
	> a[href].permalink {
		display: block;
		grid-column: 3/4; grid-row: 2/3;
		font-size: 80%;
		text-align: right;
		padding: 0.1in;
................................................................................
	h1, h2, h3, h4, h5, h6 {
		background: linear-gradient(to right, tone(-50%), transparent);
		margin-left: -0.4in;
		padding-left: 0.2in;
		text-shadow: 0 2px 0 black;
	}
}




















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

.check-panel {
	display: flex;
................................................................................
	100% { opacity: 0; transform: scale(0.9) translateY(-0.12in); display: none; }
}
.flashmsg {
	display: block;
	position: fixed;
	top: 1.3in;
	max-width: 3in;
	padding: 0.5in 0.2in;
	left: 0; right: 0;
	text-align: center;
	text-shadow: 0 0 15px tone(10%);
	margin: auto;
	background: linear-gradient(to bottom, tone(-49%), tone(-43%,-0.1));
	border: 1px solid tone(0%);
	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;
	}
}







|




|
>
>
|
>
>







 







|



>







 







>
>







 







|







 







>







 







>
>
>
>
>









|







 







>







 







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






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







 







|







 







|











|













>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
..
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
..
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
...
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
...
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
...
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
...
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
...
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569

















570
571
572
573
574
575
576
...
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
...
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
706
707
708
709
710
711
712
713
714
715
716
717
718
719
$default-color: hsl(323,100%,65%);
%sans { font-family: "Alegreya Sans", "Open Sans", sans-serif; }
%serif { font-family: Alegreya, GaramondNo8, "Garamond Premier Pro", "Adobe Garamond", Garamond, Junicode, serif; }
%teletype { font-family: "Inconsolata LGC", Inconsolata, monospace; font-size: 12pt !important; }

// @function tone($pct, $alpha: 0) { @return adjust-color($color, $lightness: $pct, $alpha: $alpha) }
@function tone($pct, $alpha: 0) {
 @return hsla(var(--hue), 100%, 65% + $pct, 1 + $alpha)
}

:root { --hue: 323; }
body {
	@extend %sans;
	background-color: tone(-55%);
	color: tone(25%);
	font-size: 14pt;
	margin: 0;
	padding: 0;
................................................................................
::placeholder {
	color: tone(0,-0.3);
	font-style: italic;
}
a[href] {
	color: tone(10%);
	text-decoration-color: tone(10%,-0.5);
	&:hover, &:focus {
		color: white;
		text-shadow: 0 0 15px tone(20%);
		text-decoration-color: tone(10%,-0.1);
		outline: none;
	}
	&.button { @extend %button; }
}
a[href^="//"],
a[href^="http://"],
a[href^="https://"] { // external link
	&:hover::after {
................................................................................
	border: 1px solid black;
	color: tone(25%);
	text-shadow: 1px 1px black;
	text-decoration: none;
	text-align: center;
	cursor: default;
	user-select: none;
	-webkit-user-drag: none;
	-webkit-app-region: no-drag;
	background: linear-gradient(to bottom,
		tone(-47%),
		tone(-50%) 15%,
		tone(-50%) 75%,
		tone(-53%)
	);
	&:hover, &:focus {
................................................................................
			> a[href] {
				display: block;
				padding: 0.25in 0.10in;
				//padding: calc((25% - 1em)/2) 0.15in;
				&, &::after { transition: 0.3s; }
				text-shadow: 1px 1px 1px black;
				&:hover{
					transform: scale(1.2);
				}
			}
		}
	}
}

main {
................................................................................
	@extend %box;
	display: block;
	width: 4in;
	margin:auto;
	padding: 0.5in;
	text-align: center;
	menu:first-of-type { margin-top: 0.3in; }
	img.icon { width: 1.875in; height: 1.875in; }
}

div.login {
	@extend %box;
	width: 4in;
	padding: 0.4in;
	> .msg {
................................................................................
	font-size: 1.5ex !important;
	letter-spacing: 1.3px;
	padding-bottom: 3px;
	border-radius: 2px;
	vertical-align: baseline;
	box-shadow: 1px 1px 1px black;
}

div.thread {
	margin-left: 0.3in;
	& + div.post { margin-top: 0.3in; }
}

div.post {
	@extend %box;
	display: grid;
	grid-template-columns: 1in 1fr max-content;
	grid-template-rows: min-content max-content;
	margin-bottom: 0.1in;
	>.avatar {
		grid-column: 1/2; grid-row: 1/2;
		img { display: block; width: 1in; height: 1in; margin:0; }
		background: linear-gradient(to bottom, tone(-53%), tone(-57%));
	}
	>a[href].username {
		display: block;
		grid-column: 1/3;
		grid-row: 2/3;
		text-align: left;
................................................................................
	}
	>.content {
		grid-column: 2/4; grid-row: 1/2;
		padding: 0.2in;
		@extend %serif;
		font-size: 110%;
		text-align: justify;
		color: tone(25%);
	}
	> a[href].permalink {
		display: block;
		grid-column: 3/4; grid-row: 2/3;
		font-size: 80%;
		text-align: right;
		padding: 0.1in;
................................................................................
	h1, h2, h3, h4, h5, h6 {
		background: linear-gradient(to right, tone(-50%), transparent);
		margin-left: -0.4in;
		padding-left: 0.2in;
		text-shadow: 0 2px 0 black;
	}
}

%navmenu, body.profile main > menu {
	margin-left: -0.25in;
	grid-column: 1/2; grid-row: 1/2;
	background: linear-gradient(to bottom, tone(-45%),tone(-55%));
	border: 1px solid black;
	padding: 0.1in;
	> a[href] {
		@extend %button;
		display: block;
		text-align: left;
	}
	> a[href] + a[href] {
		border-top: none;
	}
	hr {
		border: none;
	}
}

menu { all: unset; display: block; }
body.conf main {
	display: grid;
	grid-template-columns: 2in 1fr;
	grid-template-rows: max-content 1fr;
	> menu { @extend %navmenu; }

















	> .panel {
		grid-column: 2/3; grid-row: 1/3;
		padding-left: 0.15in;
		> h1 {
			padding-bottom: 0.1in;
			margin-bottom: 0.1in;
			margin-left: -0.15in;
................................................................................
	&.vertical-float {
		flex-flow: column;
		float: right;
		width: 40%;
		margin-left: 0.1in;
	}
	> %button {
		flex-basis: min-content;
		flex-grow: 1;
		display: block; margin: 2px;
	}
}

.check-panel {
	display: flex;
................................................................................
	100% { opacity: 0; transform: scale(0.9) translateY(-0.12in); display: none; }
}
.flashmsg {
	display: block;
	position: fixed;
	top: 1.3in;
	max-width: 3in;
	padding: 0.4in 0.2in;
	left: 0; right: 0;
	text-align: center;
	text-shadow: 0 0 15px tone(10%);
	margin: auto;
	background: linear-gradient(to bottom, tone(-49%), tone(-43%,-0.1));
	border: 1px solid tone(0%);
	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: 2.5s;
}

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

.color-picker {
	/* implemented using javascript, alas */
	@extend %box;
	label { text-shadow: 1px 1px black; }
	padding: 0.1in;
}

Modified store.t from [d79d41c9fe] to [e7b33e5534].

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
...
232
233
234
235
236
237
238

239

240
241
242
243
244
245
246
...
313
314
315
316
317
318
319
320
321
322
323
324
325
326

327
328
329
330
331
332
333
...
338
339
340
341
342
343
344

345
346
347
348
349



350
351
352
353
354
355
356
...
385
386
387
388
389
390
391
392
393
394
395
396
397



398
399
400
401
402
403
404
	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




		set: {&opaque, uint64, rawstring, ty} -> {}
		reset: {&opaque, uint64, rawstring} -> {}



	}
end)


struct m.notif {
	kind: m.notiftype.t
	when: uint64
	union {
		post: uint64
		reaction: int8[8]
	}
}

struct m.inet {
	pv: uint8 -- 0 = null, 4 = ipv4, 6 = ipv6
	union {
		v4: uint8[4]
................................................................................
	context: str
}

struct m.auth {
-- a credential record
	aid: uint64
	uid: uint64

	aname: str

	netmask: m.inet
	privs: m.privset
	blacklist: bool
}

struct m.relationship {
	agent: uint64
................................................................................
			-- 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))
	actor_conf_int: cnf(intptr, lib.stat(intptr))

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

	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.
................................................................................
			-- timestamp: timepoint

	post_save: {&m.source, &m.post} -> {}
	post_create: {&m.source, &m.post} -> uint64
	post_destroy: {&m.source, 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
		-- instantiate an artifact in the database, either installing a new
		-- artifact or returning the id of an existing artifact with the same hash
			-- artifact: bytea
			-- mime:     pstring
	artifact_quicksearch: {&m.source, lib.mem.ptr(uint8)} -> {uint64,bool}
		-- checks whether a hash is already in the database without uploading
................................................................................
			-- proto: kompromat (null for all records, or a prototype describing the records to return)
	nkvd_sanction_issue:  {&m.source, &m.sanction} -> uint64
	nkvd_sanction_vacate: {&m.source, uint64} -> {}
	nkvd_sanction_enum_target: {&m.source, uint64} -> {}
	nkvd_sanction_enum_issuer: {&m.source, uint64} -> {}
	nkvd_sanction_review: {&m.source, m.timepoint} -> {}

	convo_fetch_xid: {&m.source,rawstring} -> lib.mem.ptr(m.post)
	convo_fetch_cid: {&m.source,uint64} -> lib.mem.ptr(m.post)

	timeline_actor_fetch_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post))
	timeline_instance_fetch: {&m.source, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post))
}




struct m.source {
	backend: &m.backend
	id: lib.mem.ptr(int8)
	handle: &opaque
	string: lib.mem.ptr(int8)
}







>
>





|

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






|







 







>

>







 







|
|
<
|



>







 







>





>
>
>







 







<
<
<



>
>
>







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
...
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
...
326
327
328
329
330
331
332
333
334

335
336
337
338
339
340
341
342
343
344
345
346
...
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
...
402
403
404
405
406
407
408



409
410
411
412
413
414
415
416
417
418
419
420
421
	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
	accent: int16
	depth: uint16 -- used in conversations to indicate tree depth
	source: &m.source

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

m.user_conf_funcs = function(be,n,ty,rty,rty2)
	rty = rty or ty
	local gt
	if not rty2 -- what the fuck?
		then gt = {&m.source, uint64, rawstring} -> rty;
		else gt = {&m.source, uint64, rawstring} -> {rty, rty2};
	end
	for k, t in pairs {
		enum = {&m.source, uint64, rawstring} -> lib.mem.ptr(rty);
		get = gt;
		set = {&m.source, uint64, rawstring, ty} -> {};
		reset = {&m.source, uint64, rawstring} -> {};
	} do
		be.entries[#be.entries+1] = {
			field = 'actor_conf_'..n..'_'..k, type = t
		}
	end
end

struct m.notif {
	kind: m.notiftype.t
	when: uint64
	union {
		post: uint64
		reaction: int8[16]
	}
}

struct m.inet {
	pv: uint8 -- 0 = null, 4 = ipv4, 6 = ipv6
	union {
		v4: uint8[4]
................................................................................
	context: str
}

struct m.auth {
-- a credential record
	aid: uint64
	uid: uint64
	kind: str
	aname: str
	comment: str
	netmask: m.inet
	privs: m.privset
	blacklist: bool
}

struct m.relationship {
	agent: uint64
................................................................................
			-- 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

	auth_enum_uid:    {&m.source, uint64}    -> lib.mem.ptr(lib.mem.ptr(m.auth))
	auth_enum_handle: {&m.source, rawstring} -> lib.mem.ptr(lib.mem.ptr(m.auth))

	auth_attach_pw: {&m.source, uint64, bool, pstr, pstr} -> {}
		-- uid: uint64
		-- reset: bool (delete other passwords?)
		-- pw: pstring
		-- comment: 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.
................................................................................
			-- timestamp: timepoint

	post_save: {&m.source, &m.post} -> {}
	post_create: {&m.source, &m.post} -> uint64
	post_destroy: {&m.source, 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_enum_parent: {&m.source, uint64} -> 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

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

	artifact_instantiate: {&m.source, lib.mem.ptr(uint8), lib.mem.ptr(int8)} -> uint64
		-- instantiate an artifact in the database, either installing a new
		-- artifact or returning the id of an existing artifact with the same hash
			-- artifact: bytea
			-- mime:     pstring
	artifact_quicksearch: {&m.source, lib.mem.ptr(uint8)} -> {uint64,bool}
		-- checks whether a hash is already in the database without uploading
................................................................................
			-- proto: kompromat (null for all records, or a prototype describing the records to return)
	nkvd_sanction_issue:  {&m.source, &m.sanction} -> uint64
	nkvd_sanction_vacate: {&m.source, uint64} -> {}
	nkvd_sanction_enum_target: {&m.source, uint64} -> {}
	nkvd_sanction_enum_issuer: {&m.source, uint64} -> {}
	nkvd_sanction_review: {&m.source, m.timepoint} -> {}




	timeline_actor_fetch_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post))
	timeline_instance_fetch: {&m.source, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post))
}

m.user_conf_funcs(m.backend, 'str', rawstring, lib.mem.ptr(int8))
m.user_conf_funcs(m.backend, 'int', intptr, intptr, bool)

struct m.source {
	backend: &m.backend
	id: lib.mem.ptr(int8)
	handle: &opaque
	string: lib.mem.ptr(int8)
}

Modified tpl.t from [682e534236] to [9f68e11c52].

156
157
158
159
160
161
162
163
164
165
166
167
168
169
170






171
172





173
174
175
176
177
178
	rec.methods.append = terra([symself], [accumulator])
		lib.dbg(['appending template ' .. tid])
		[tallyup]
		accumulator:cue([runningtally])
		[appenders]
		return accumulator
	end
	rec.methods.send = terra([symself], [destcon], code: uint16, hd: lib.mem.ptr(lib.http.header))
		lib.dbg(['transmitting template ' .. tid])
		[tallyup]
		lib.net.mg_printf([destcon], 'HTTP/1.1 %s', lib.http.codestr(code))
		for i = 0, hd.ct do
			lib.net.mg_printf([destcon], '%s: %s\r\n', hd.ptr[i].key, hd.ptr[i].value)
		end
		lib.net.mg_printf([destcon],'Content-Length: %llu\r\n\r\n', [runningtally] + 1)






		[senders]
		lib.net.mg_send([destcon],'\r\n',2)





	end

	return rec
end

return m







|
|






>
>
>
>
>
>


>
>
>
>
>






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
	rec.methods.append = terra([symself], [accumulator])
		lib.dbg(['appending template ' .. tid])
		[tallyup]
		accumulator:cue([runningtally])
		[appenders]
		return accumulator
	end
	rec.methods.head = terra([symself], [destcon], code: uint16, hd: lib.mem.ptr(lib.http.header))
		lib.dbg(['transmitting template headers ' .. tid])
		[tallyup]
		lib.net.mg_printf([destcon], 'HTTP/1.1 %s', lib.http.codestr(code))
		for i = 0, hd.ct do
			lib.net.mg_printf([destcon], '%s: %s\r\n', hd.ptr[i].key, hd.ptr[i].value)
		end
		lib.net.mg_printf([destcon],'Content-Length: %llu\r\n\r\n', [runningtally] + 1)
	end
	rec.methods.send = terra([symself], [destcon], code: uint16, hd: lib.mem.ptr(lib.http.header))
		lib.dbg(['transmitting template ' .. tid])

		symself:head(destcon,code,hd)

		[senders]
		lib.net.mg_send([destcon],'\r\n',2)
	end
	rec.methods.sz = terra([symself])
		lib.dbg(['tallying template ' .. tid])
		[tallyup]
		return [runningtally] + 1
	end

	return rec
end

return m

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

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>




>
>
|
>
>

1
2
3
4
5
6
7
8
9
10
<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>
	<div class="elem color-picker"><label for="hue">accent</label><input type="range" min="0" max="360" value="@hue" name="hue" id="hue" data-color-pick></div>
	<menu class="choice vertical">
		<button>commit all</button>
		<button name="act" value="reset-hue">use server colors</button>
	</menu> 
</form>

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

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>



>












1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<menu>
	<a href="/conf/profile">profile</a>
	<a href="/conf/avi">avatar</a>
	<a href="/conf/ui">interface</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>

Modified view/docskel.tpl from [cb4a31dcc6] to [3229efd171].

1
2
3
4
5

6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!doctype html>
<html>
	<head>
		<title>@instance :: @title</title>
		<link rel="stylesheet" href="/s/style.css">

	</head>
	<body class="@class">
		<header><div>
			<h1>@title</h1>
			<nav>
				<a href="/instance">instance</a>
				@navlinks
			</nav>
		</div></header>
		<main>
			@body
		</main>
	</body>
</html>




|
>

|












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

Added view/media.tpl version [5a68c18a8e].

























>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
<menu>
	<a href="/user/@:xid/media">new uploads</a>
	@folders
</menu>

<div name="gallery">
	@images
</div>

<div name="files">
	@files
</div>

Modified view/profile.tpl from [cfeb837b05] to [694fa2eec6].

11
12
13
14
15
16
17

18
19
20
21
22
23
		<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>







>






11
12
13
14
15
16
17
18
19
20
21
22
23
24
		<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/arc">archive</a>
		<a class="button" href="/@:xid/media">media</a>
		<a class="button" href="/@:xid/social">associates</a>
		<hr>
		@auxbtn
	</form>
</div>

Added view/tweet-mini.tpl version [4c5364412a].









>
>
>
>
1
2
3
4
<div class="post-mini">
	<span class="permalink">[<a href="@permalink">@when</a>]</span>
	<span class="username">‹<a href="/@:acctlink">@nym</a>›</span> <span class="content">@text</span>
</div>

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

1
2
3
4
5
6
7
8
9
<div class="post">
	<div class="avatar"><img src="@:avatar"></div>
	<a class="username" href="/@:acctlink">@nym</a>
	<div class="content">
		<div class="subject">@!subject</div>
		<div class="text">@text</div>
	</div>
	<a class="permalink" href="@permalink">@when</a>
</div>
|








1
2
3
4
5
6
7
8
9
<div class="post"@attr>
	<div class="avatar"><img src="@:avatar"></div>
	<a class="username" href="/@:acctlink">@nym</a>
	<div class="content">
		<div class="subject">@!subject</div>
		<div class="text">@text</div>
	</div>
	<a class="permalink" href="@permalink">@when</a>
</div>