| Comment: | vastly improve the setup process |
|---|---|
| Downloads: | Tarball | ZIP archive | SQL archive |
| Timelines: | family | ancestors | descendants | both | trunk |
| Files: | files | file ages | folders |
| SHA3-256: |
d228cd7fcb7c10b982f8c2129c506440 |
| User & Date: | lexi on 2020-12-28 23:42:22 |
| Other Links: | manifest | tags |
|
2020-12-29
| ||
| 00:57 | add privilege control verbs check-in: a64461061f user: lexi tags: trunk | |
|
2020-12-28
| ||
| 23:42 | vastly improve the setup process check-in: d228cd7fcb user: lexi tags: trunk | |
|
2020-12-27
| ||
| 04:08 | look ma, im tweetin check-in: 8f954221a1 user: lexi tags: trunk | |
Modified backend/pgsql.t from [2c2a215381] to [0fdc39456b].
6 6 params = {rawstring}, sql = [[ 7 7 select value from parsav_config 8 8 where key = $1::text limit 1 9 9 ]]; 10 10 }; 11 11 12 12 conf_set = { 13 - params = {rawstring,rawstring}, sql = [[ 13 + params = {rawstring,rawstring}, cmd=true, sql = [[ 14 14 insert into parsav_config (key, value) 15 15 values ($1::text, $2::text) 16 16 on conflict (key) do update set value = $2::text 17 17 ]]; 18 18 }; 19 19 20 20 conf_reset = { 21 - params = {rawstring}, sql = [[ 21 + params = {rawstring}, cmd=true, sql = [[ 22 22 delete from parsav_config where 23 23 key = $1::text 24 24 ]]; 25 25 }; 26 26 27 27 actor_fetch_uid = { 28 28 params = {uint64}, sql = [[ 29 29 select a.id, a.nym, a.handle, a.origin, a.bio, 30 - a.avataruri, a.rank, a.quota, a.key, 30 + a.avataruri, a.rank, a.quota, a.key, a.epithet, 31 31 extract(epoch from a.knownsince)::bigint, 32 32 coalesce(a.handle || '@' || s.domain, 33 33 '@' || a.handle) as xid 34 34 35 35 from parsav_actors as a 36 36 left join parsav_servers as s 37 37 on a.origin = s.id ................................................................................ 38 38 where a.id = $1::bigint 39 39 ]]; 40 40 }; 41 41 42 42 actor_fetch_xid = { 43 43 params = {pstring}, sql = [[ 44 44 select a.id, a.nym, a.handle, a.origin, a.bio, 45 - a.avataruri, a.rank, a.quota, a.key, 45 + a.avataruri, a.rank, a.quota, a.key, a.epithet, 46 46 extract(epoch from a.knownsince)::bigint, 47 47 coalesce(a.handle || '@' || s.domain, 48 48 '@' || a.handle) as xid, 49 49 50 50 coalesce(s.domain, 51 51 (select value from parsav_config 52 52 where key='domain' limit 1)) as domain ................................................................................ 70 70 rawstring, uint16, uint32 71 71 }; 72 72 sql = [[ 73 73 insert into parsav_actors ( 74 74 nym,handle, 75 75 origin,knownsince, 76 76 bio,avataruri,key, 77 - title,rank,quota 77 + epithet,rank,quota 78 78 ) values ($1::text, $2::text, 79 79 case when $3::bigint = 0 then null 80 80 else $3::bigint end, 81 81 to_timestamp($4::bigint), 82 82 $5::bigint, $6::bigint, $7::bytea, 83 83 $8::text, $9::smallint, $10::integer 84 84 ) returning id ................................................................................ 98 98 order by blacklist desc limit 1 99 99 ]]; 100 100 }; 101 101 102 102 actor_enum_local = { 103 103 params = {}, sql = [[ 104 104 select id, nym, handle, origin, bio, 105 - null::text, rank, quota, key, 105 + null::text, rank, quota, key, epithet, 106 106 extract(epoch from knownsince)::bigint, 107 107 handle ||'@'|| 108 108 (select value from parsav_config 109 109 where key='domain' limit 1) as xid 110 110 from parsav_actors where origin is null 111 111 ]]; 112 112 }; 113 113 114 114 actor_enum = { 115 115 params = {}, sql = [[ 116 116 select a.id, a.nym, a.handle, a.origin, a.bio, 117 - a.avataruri, a.rank, a.quota, a.key, 117 + a.avataruri, a.rank, a.quota, a.key, a.epithet, 118 118 extract(epoch from a.knownsince)::bigint, 119 119 coalesce(a.handle || '@' || s.domain, 120 120 '@' || a.handle) as xid 121 121 from parsav_actors a 122 122 left join parsav_servers s on s.id = a.origin 123 123 ]]; 124 124 }; ................................................................................ 163 163 (select count(*) from mts where kind = 'trust') > 0 164 164 ]]; -- cheat 165 165 }; 166 166 167 167 actor_session_fetch = { 168 168 params = {uint64, lib.store.inet}, sql = [[ 169 169 select a.id, a.nym, a.handle, a.origin, a.bio, 170 - a.avataruri, a.rank, a.quota, a.key, 170 + a.avataruri, a.rank, a.quota, a.key, a.epithet, 171 171 extract(epoch from a.knownsince)::bigint, 172 172 coalesce(a.handle || '@' || s.domain, 173 173 '@' || a.handle) as xid, 174 174 175 175 au.restrict, 176 176 array['post' ] <@ au.restrict as can_post, 177 177 array['edit' ] <@ au.restrict as can_edit, ................................................................................ 190 190 }; 191 191 192 192 actor_powers_fetch = { 193 193 params = {uint64}, sql = [[ 194 194 select key, allow from parsav_rights where actor = $1::bigint 195 195 ]] 196 196 }; 197 + 198 + actor_power_insert = { 199 + params = {uint64,lib.mem.ptr(int8),uint16}, cmd = true, sql = [[ 200 + insert into parsav_rights (actor, key, allow) values ( 201 + $1::bigint, $2::text, ($3::smallint)::integer::bool 202 + ) 203 + ]] 204 + }; 205 + 206 + auth_create_pw = { 207 + params = {uint64, lib.mem.ptr(uint8)}, cmd = true, sql = [[ 208 + insert into parsav_auth (uid, name, kind, cred) values ( 209 + $1::bigint, 210 + (select handle from parsav_actors where id = $1::bigint), 211 + 'pw-sha256', $2::bytea 212 + ) 213 + ]] 214 + }; 197 215 198 216 post_create = { 199 217 params = {uint64, rawstring, rawstring, rawstring}, sql = [[ 200 218 insert into parsav_posts ( 201 219 author, subject, acl, body, 202 220 posted, discovered, 203 221 circles, mentions ................................................................................ 339 357 return buf 340 358 end 341 359 end; 342 360 } 343 361 344 362 local con = symbol(&lib.pq.PGconn) 345 363 local prep = {} 346 -local sqlsquash = function(s) return s:gsub('%s+',' '):gsub('^%s*(.-)%s*$','%1') end 364 +local function sqlsquash(s) return s 365 + :gsub('%%include (.-)%%',function(f) 366 + return sqlsquash(lib.util.ingest('backend/schema/' .. f)) 367 + end) -- include dependencies 368 + :gsub('%-%-.-\n','') -- remove disruptive line comments 369 + :gsub('%-%-.-$','') -- remove unnecessary terminal comments 370 + :gsub('%s+',' ') -- remove whitespace 371 + :gsub('^%s*(.-)%s*$','%1') -- chomp 372 +end 373 + 347 374 for k,q in pairs(queries) do 348 375 local qt = sqlsquash(q.sql) 349 376 local stmt = 'parsavpg_' .. k 350 - prep[#prep + 1] = quote 377 + terra q.prep([con]) 351 378 var res = lib.pq.PQprepare([con], stmt, qt, [#q.params], nil) 352 379 defer lib.pq.PQclear(res) 353 380 if res == nil or lib.pq.PQresultStatus(res) ~= lib.pq.PGRES_COMMAND_OK then 354 381 if res == nil then 355 382 lib.bail('grievous error occurred preparing ',k,' statement') 356 383 end 357 384 lib.bail('could not prepare PGSQL statement ',k,': ',lib.pq.PQresultErrorMessage(res)) 358 385 end 359 386 lib.dbg('prepared PGSQL statement ',k) 360 387 end 388 + prep[#prep + 1] = quote q.prep([con]) end 361 389 362 390 local args, casts, counters, fixers, ft, yield = {}, {}, {}, {}, {}, {} 363 391 local dumpers = {} 364 392 for i, ty in ipairs(q.params) do 365 393 args[i] = symbol(ty) 366 394 ft[i] = `1 367 395 if ty == rawstring then ................................................................................ 389 417 dumpers[#dumpers+1] = `lib.io.fmt([tostring(i)..'. got int %llu\n'], [args[i]]) 390 418 fixers[#fixers + 1] = quote 391 419 [args[i]] = lib.math.netswap(ty, [args[i]]) 392 420 end 393 421 end 394 422 end 395 423 424 + local okconst = lib.pq.PGRES_TUPLES_OK 425 + if q.cmd then okconst = lib.pq.PGRES_COMMAND_OK end 396 426 terra q.exec(src: &lib.store.source, [args]) 397 427 var params = arrayof([&int8], [casts]) 398 428 var params_sz = arrayof(int, [counters]) 399 429 var params_ft = arrayof(int, [ft]) 400 430 [fixers] 401 431 --[dumpers] 402 432 var res = lib.pq.PQexecPrepared([&lib.pq.PGconn](src.handle), stmt, 403 433 [#args], params, params_sz, params_ft, 1) 404 434 if res == nil then 405 435 lib.bail(['grievous error occurred executing '..k..' against database']) 406 - elseif lib.pq.PQresultStatus(res) ~= lib.pq.PGRES_TUPLES_OK then 436 + elseif lib.pq.PQresultStatus(res) ~= okconst then 407 437 lib.bail(['PGSQL database procedure '..k..' failed\n'], 408 438 lib.pq.PQresultErrorMessage(res)) 409 439 end 410 440 411 441 var ct = lib.pq.PQntuples(res) 412 442 if ct == 0 then 413 443 lib.pq.PQclear(res) ................................................................................ 448 478 return p 449 479 end 450 480 local terra row_to_actor(r: &pqr, row: intptr): lib.mem.ptr(lib.store.actor) 451 481 var a: lib.mem.ptr(lib.store.actor) 452 482 var av: rawstring, avlen: intptr 453 483 var nym: rawstring, nymlen: intptr 454 484 var bio: rawstring, biolen: intptr 485 + var epi: rawstring, epilen: intptr 455 486 if r:null(row,5) then avlen = 0 av = nil else 456 487 av = r:string(row,5) 457 488 avlen = r:len(row,5)+1 458 489 end 459 490 if r:null(row,1) then nymlen = 0 nym = nil else 460 491 nym = r:string(row,1) 461 492 nymlen = r:len(row,1)+1 462 493 end 463 494 if r:null(row,4) then biolen = 0 bio = nil else 464 495 bio = r:string(row,4) 465 496 biolen = r:len(row,4)+1 497 + end 498 + if r:null(row,9) then epilen = 0 epi = nil else 499 + epi = r:string(row,9) 500 + epilen = r:len(row,9)+1 466 501 end 467 502 a = [ lib.str.encapsulate(lib.store.actor, { 468 503 nym = {`nym, `nymlen}; 469 504 bio = {`bio, `biolen}; 505 + epithet = {`epi, `epilen}; 470 506 avatar = {`av,`avlen}; 471 507 handle = {`r:string(row, 2); `r:len(row,2) + 1}; 472 - xid = {`r:string(row, 10); `r:len(row,10) + 1}; 508 + xid = {`r:string(row, 11); `r:len(row,11) + 1}; 473 509 }) ] 474 510 a.ptr.id = r:int(uint64, row, 0); 475 511 a.ptr.rights = lib.store.rights_default(); 476 512 a.ptr.rights.rank = r:int(uint16, row, 6); 477 513 a.ptr.rights.quota = r:int(uint32, row, 7); 478 - a.ptr.knownsince = r:int(int64,row, 9); 514 + a.ptr.knownsince = r:int(int64,row, 10); 479 515 if r:null(row,8) then 480 516 a.ptr.key.ct = 0 a.ptr.key.ptr = nil 481 517 else 482 518 a.ptr.key = r:bin(row,8) 483 519 end 484 520 if r:null(row,3) then a.ptr.origin = 0 485 521 else a.ptr.origin = r:int(uint64,row,3) end ................................................................................ 530 566 lib.dbg(['searching for hashed password credentials in format SHA' .. tostring(hash)]) 531 567 var [out] 532 568 [vdrs] 533 569 lib.dbg(['could not find password hash']) 534 570 end 535 571 end 536 572 573 +local schema = sqlsquash(lib.util.ingest('backend/schema/pgsql.sql')) 574 +local obliterator = sqlsquash(lib.util.ingest('backend/schema/pgsql-drop.sql')) 575 + 537 576 local b = `lib.store.backend { 538 577 id = "pgsql"; 539 578 open = [terra(src: &lib.store.source): &opaque 540 579 lib.report('connecting to postgres database: ', src.string.ptr) 541 580 var [con] = lib.pq.PQconnectdb(src.string.ptr) 542 581 if lib.pq.PQstatus(con) ~= lib.pq.CONNECTION_OK then 543 582 lib.warn('postgres backend connection failed') ................................................................................ 556 595 defer lib.pq.PQclear(res) 557 596 if lib.pq.PQresultStatus(res) ~= lib.pq.PGRES_TUPLES_OK then 558 597 lib.warn('failed to secure postgres connection') 559 598 lib.pq.PQfinish(con) 560 599 return nil 561 600 end 562 601 563 - [prep] 564 602 return con 565 603 end]; 604 + 566 605 close = [terra(src: &lib.store.source) lib.pq.PQfinish([&lib.pq.PGconn](src.handle)) end]; 606 + 607 + conprep = [terra(src: &lib.store.source, mode: lib.store.prepmode.t) 608 + var [con] = [&lib.pq.PGconn](src.handle) 609 + if mode == lib.store.prepmode.full then [prep] 610 + elseif mode == lib.store.prepmode.conf or 611 + mode == lib.store.prepmode.admin then 612 + queries.conf_get.prep(con) 613 + queries.conf_set.prep(con) 614 + queries.conf_reset.prep(con) 615 + if mode == lib.store.prepmode.admin then 616 + end 617 + else lib.bail('unsupported connection preparation mode') end 618 + end]; 619 + 620 + dbsetup = [terra(src: &lib.store.source) 621 + var res = lib.pq.PQexec([&lib.pq.PGconn](src.handle), schema) 622 + if lib.pq.PQresultStatus(res) == lib.pq.PGRES_COMMAND_OK then 623 + lib.report('successfully instantiated schema in database') 624 + return true 625 + else 626 + lib.warn('backend pgsql - failed to initialize database: \n', lib.pq.PQresultErrorMessage(res)) 627 + return false 628 + end 629 + end]; 630 + 631 + obliterate_everything = [terra(src: &lib.store.source) 632 + var res = lib.pq.PQexec([&lib.pq.PGconn](src.handle), obliterator) 633 + if lib.pq.PQresultStatus(res) == lib.pq.PGRES_COMMAND_OK then 634 + lib.report('successfully wiped out everything parsav-related in database') 635 + return true 636 + else 637 + lib.warn('backend pgsql - failed to obliterate database: \n', lib.pq.PQresultErrorMessage(res)) 638 + return false 639 + end 640 + end]; 567 641 568 642 conf_get = [terra(src: &lib.store.source, key: rawstring) 569 643 var r = queries.conf_get.exec(src, key) 570 644 if r.sz == 0 then return [lib.mem.ptr(int8)] { ptr = nil, ct = 0 } else 571 645 defer r:free() 572 646 return r:String(0,0) 573 647 end ................................................................................ 678 752 679 753 var a = row_to_actor(&r, 0) 680 754 a.ptr.source = src 681 755 682 756 var au = [lib.stat(lib.store.auth)] { ok = true } 683 757 au.val.aid = aid 684 758 au.val.uid = a.ptr.id 685 - if not r:null(0,12) then -- restricted? 759 + if not r:null(0,13) then -- restricted? 686 760 au.val.privs:clear() 687 - (au.val.privs.post << r:bool(0,13)) 688 - (au.val.privs.edit << r:bool(0,14)) 689 - (au.val.privs.acct << r:bool(0,15)) 690 - (au.val.privs.upload << r:bool(0,16)) 691 - (au.val.privs.censor << r:bool(0,17)) 692 - (au.val.privs.admin << r:bool(0,18)) 761 + (au.val.privs.post << r:bool(0,14)) 762 + (au.val.privs.edit << r:bool(0,15)) 763 + (au.val.privs.acct << r:bool(0,16)) 764 + (au.val.privs.upload << r:bool(0,17)) 765 + (au.val.privs.censor << r:bool(0,18)) 766 + (au.val.privs.admin << r:bool(0,19)) 693 767 else au.val.privs:fill() end 694 768 695 769 return au, a 696 770 end 697 771 698 772 ::fail:: return [lib.stat (lib.store.auth) ] { ok = false }, 699 773 [lib.mem.ptr(lib.store.actor)] { ptr = nil, ct = 0 } ................................................................................ 759 833 return powers 760 834 end]; 761 835 762 836 actor_create = [terra( 763 837 src: &lib.store.source, 764 838 ac: &lib.store.actor 765 839 ): uint64 766 - var r = queries.actor_create.exec(src,ac.nym, ac.handle, ac.origin, ac.knownsince, ac.bio, ac.avatar, ac.key, ac.title, ac.rights.rank, ac.rights.quota) 840 + var r = queries.actor_create.exec(src,ac.nym, ac.handle, ac.origin, ac.knownsince, ac.bio, ac.avatar, ac.key, ac.epithet, ac.rights.rank, ac.rights.quota) 767 841 if r.sz == 0 then lib.bail('failed to create actor!') end 768 - return r:int(uint64,0,0) 842 + var uid = r:int(uint64,0,0) 843 + 844 + -- check against default rights, insert records for wherever powers differ 845 + lib.dbg('created new actor, establishing powers') 846 + var pdef = lib.store.rights_default().powers 847 + var map = array([privmap]) 848 + for i=0, [map.type.N] do 849 + var d = pdef and map[i].priv 850 + var u = ac.rights.powers and map[i].priv 851 + if d:sz() > 0 and u:sz() == 0 then 852 + lib.dbg('blocking power ', {map[i].name.ptr, map[i].name.ct}) 853 + queries.actor_power_insert.exec(src, uid, map[i].name, 0) 854 + elseif d:sz() == 0 and u:sz() > 0 then 855 + lib.dbg('granting power ', {map[i].name.ptr, map[i].name.ct}) 856 + queries.actor_power_insert.exec(src, uid, map[i].name, 1) 857 + end 858 + end 859 + 860 + lib.dbg('powers established') 861 + return uid 862 + end]; 863 + 864 + auth_create_pw = [terra( 865 + src: &lib.store.source, 866 + uid: uint64, 867 + reset: bool, 868 + pw: lib.mem.ptr(int8) 869 + ): {} 870 + -- TODO impl reset support 871 + var hash: uint8[lib.crypt.algsz.sha256] 872 + if lib.md.mbedtls_md(lib.md.mbedtls_md_info_from_type(lib.crypt.alg.sha256.id), 873 + [&uint8](pw.ptr), pw.ct, &hash[0]) ~= 0 then 874 + lib.bail('cannot hash password') 875 + end 876 + queries.auth_create_pw.exec(src, uid, [lib.mem.ptr(uint8)] {ptr = &hash[0], ct = [hash.type.N]}) 769 877 end]; 770 878 771 879 actor_auth_register_uid = nil; -- not necessary for view-based auth 880 + 772 881 } 773 882 774 883 return b
Added backend/schema/pgsql-auth.sql version [1170b3857b].
1 +-- in managed-auth configurations, parsav_auth is a table which is directly 2 +-- controlled by the parsav daemon and utilities themselves. in unmanaged 3 +-- configuration, you will need to create your own view with the same fields 4 +-- as this table 5 +create table parsav_auth ( 6 + aid bigint primary key default (1+random()*(2^63-1))::bigint, 7 + -- the AID is the value that links a session to its credentials, 8 + -- so the aid needs to be stable over time. if you don't have a 9 + -- convenient field to rely on in your own datasets, the best 10 + -- approach is to use digest(str,'sha256') from the pgcrypto 11 + -- extension to create a value that depends on the values of 12 + -- kind, cred, and a unique user ID from your own dataset (NOT 13 + -- uid, as the UID associated with a session will change when 14 + -- a user logs in for the first time). 15 + 16 + uid bigint, 17 + -- the UID links a credential set to an actor in the parsav 18 + -- database. if it is equal to 0 (but not null) a new actor 19 + -- will be created and associated with the authentication 20 + -- records bearing its name when that user first logs in 21 + 22 + name text, 23 + -- this is the handle of the actor that will be created when 24 + -- a user first logs in with this as the username and one of 25 + -- its associated credentials. the field is otherwise unused. 26 + 27 + kind text not null, -- see parsav.md 28 + cred bytea, 29 + restrict text[], 30 + -- per-credential restrictions can be levelled, for instance 31 + -- to prevent a certain API key from being used to post tweets 32 + -- as that user, while allowing it to be used to collect data. 33 + -- if restrict is null, no restrictions will be applied. 34 + -- otherwise, it should be an array of privileges that will be 35 + -- permitted when authenticated via this credential. 36 + 37 + netmask cidr, 38 + -- if not null, the credential will only be valid when logging 39 + -- in from an IP address contained by this netmask. 40 + 41 + blacklist bool not null default false, 42 + -- if the credential matches, access will be denied, even if 43 + -- non-blacklisted credentials match. most useful with 44 + -- uid = null, kind = trust, cidr = (untrusted IP range) 45 + 46 + valperiod timestamp default now(), 47 + -- cookies bearing timestamps earlier than this point in time 48 + -- will be considered invalid and will not grant access 49 + 50 + unique(name,kind,cred) 51 +);
Added backend/schema/pgsql-drop.sql version [e1fb43be2e].
1 +-- destroy absolutely everything 2 + 3 +drop table if exists parsav_config cascade; 4 +drop table if exists parsav_servers cascade; 5 +drop table if exists parsav_actors cascade; 6 +drop table if exists parsav_rights cascade; 7 +drop table if exists parsav_posts cascade; 8 +drop table if exists parsav_conversations cascade; 9 +drop table if exists parsav_rels cascade; 10 +drop table if exists parsav_acts cascade; 11 +drop table if exists parsav_log cascade; 12 +drop table if exists parsav_attach cascade; 13 +drop table if exists parsav_circles cascade; 14 +drop table if exists parsav_rooms cascade; 15 +drop table if exists parsav_room_members cascade; 16 +drop table if exists parsav_invites cascade; 17 +drop table if exists parsav_interventions cascade; 18 +drop table if exists parsav_auth cascade;
Modified backend/schema/pgsql.sql from [097969b0cb] to [0ef43163b5].
1 -\prompt 'domain name: ' domain 2 -\prompt 'instance name: ' inst 3 -\prompt 'bind to socket: ' bind 4 -\qecho 'how locked down should this server be? public = anyone can see public timeline and tweets, private = anyone can see tweets with a link but login required for everything else, lockdown = login required for all activities, isolate = like lockdown but with federation protocols completely disabled' 5 -\prompt 'security mode: ' secmode 6 -\qecho 'should user self-registration be allowed? yes or no' 7 -\prompt 'registration: ' regpol 8 -\qecho 'by default, parsav tracks rights on its own. you can override this later by replacing the rights table with a view, but you''ll then need to set appropriate rules on the view to allow administrators to modify rights from the web UI, or set the rights-readonly flag in the config table to true. for now, enter the name of an actor who will be granted full rights when she logs in and identified as the server owner.' 9 -\prompt 'master actor: ' admin 10 -\qecho 'you will need to create an authentication view named parsav_auth mapping your user database to something parsav can understand; see auth.sql for an example.' 11 - 12 -begin; 13 - 14 -drop table if exists parsav_config; 15 -create table if not exists parsav_config ( 1 +create table parsav_config ( 16 2 key text primary key, 17 3 value text 18 4 ); 19 5 20 -insert into parsav_config (key,value) values 21 - ('bind',:'bind'), 22 - ('domain',:'domain'), 23 - ('instance-name',:'inst'), 24 - ('policy-security',:'secmode'), 25 - ('policy-self-register',:'regpol'), 26 - ('master',:'admin'), 27 - ('server-secret', encode( 28 - digest(int8send((2^63 * (random()*2 - 1))::bigint), 29 - 'sha512'), 'base64')); 6 +insert into parsav_config (key,value) values ('schema-version','1'), 7 + ('credential-store','managed'); 8 +-- ('bind',:'bind'), 9 +-- ('domain',:'domain'), 10 +-- ('instance-name',:'inst'), 11 +-- ('policy-security',:'secmode'), 12 +-- ('policy-self-register',:'regpol'), 13 +-- ('master',:'admin'), 30 14 31 15 -- note that valid ids should always > 0, as 0 is reserved for null 32 16 -- on the client side, vastly simplifying code 33 -drop table if exists parsav_servers cascade; 34 17 create table parsav_servers ( 35 18 id bigint primary key default (1+random()*(2^63-1))::bigint, 36 19 domain text not null, 37 20 key bytea, 38 21 knownsince timestamp, 39 22 parsav boolean -- whether to use parsav protocol extensions 40 23 ); 41 24 42 -drop table if exists parsav_actors cascade; 43 25 create table parsav_actors ( 44 26 id bigint primary key default (1+random()*(2^63-1))::bigint, 45 27 nym text, 46 28 handle text not null, -- nym [@handle@origin] 47 29 origin bigint references parsav_servers(id) 48 30 on delete cascade, -- null origin = local actor 49 31 knownsince timestamp, 50 32 bio text, 51 33 avataruri text, -- null if local 52 34 rank smallint not null default 0, 53 35 quota integer not null default 1000, 54 36 key bytea, -- private if localactor; public if remote 55 - title text, 37 + epithet text, 38 + authtime timestamp not null default now(), -- cookies earlier than this timepoint will not be accepted 56 39 57 40 unique (handle,origin) 58 41 ); 59 42 60 -drop table if exists parsav_rights cascade; 61 43 create table parsav_rights ( 62 44 key text, 63 45 actor bigint references parsav_actors(id) 64 46 on delete cascade, 65 47 allow boolean not null, 66 48 scope bigint, -- for future expansion 67 49 68 50 primary key (key,actor) 69 51 ); 70 52 71 -insert into parsav_actors (handle,rank,quota) values (:'admin',1,0); 72 -insert into parsav_rights (actor,key,allow) 73 - select (select id from parsav_actors where handle=:'admin'), a.column1, a.column2 from (values 74 - ('purge',true), 75 - ('config',true), 76 - ('censor',true), 77 - ('suspend',true), 78 - ('cred',true), 79 - ('elevate',true), 80 - ('demote',true), 81 - ('rebrand',true) 82 - ) as a; 83 - 84 -drop table if exists parsav_posts cascade; 85 53 create table parsav_posts ( 86 54 id bigint primary key default (1+random()*(2^63-1))::bigint, 87 55 author bigint references parsav_actors(id) 88 56 on delete cascade, 89 57 subject text, 90 58 acl text not null default 'all', -- just store the script raw 🤷 91 59 body text, ................................................................................ 97 65 98 66 convoheaduri text 99 67 -- only used for tracking foreign conversations and tying them to post heads; 100 68 -- local conversations are tracked directly and mapped to URIs based on the 101 69 -- head's ID. null if native tweet or not the first tweet in convo 102 70 ); 103 71 104 -drop table if exists parsav_conversations cascade; 105 - 106 -drop table if exists parsav_rels cascade; 107 72 create table parsav_rels ( 108 73 relator bigint references parsav_actors(id) 109 74 on delete cascade, -- e.g. follower 110 75 relatee bigint references parsav_actors(id) 111 76 on delete cascade, -- e.g. followed 112 77 kind smallint, -- e.g. follow, block, mute 113 78 114 79 primary key (relator, relatee, kind) 115 80 ); 116 81 117 -drop table if exists parsav_acts cascade; 118 82 create table parsav_acts ( 119 83 id bigint primary key default (1+random()*(2^63-1))::bigint, 120 84 kind text not null, -- like, react, so on 121 85 time timestamp not null default now(), 122 86 actor bigint references parsav_actors(id) 123 87 on delete cascade, 124 88 subject bigint -- may be post or act, depending on kind 125 89 ); 126 90 127 -drop table if exists parsav_log cascade; 128 91 create table parsav_log ( 129 92 -- accesses are tracked for security & sending delete acts 130 93 id bigint primary key default (1+random()*(2^63-1))::bigint, 131 94 time timestamp not null default now(), 132 95 actor bigint references parsav_actors(id) 133 96 on delete cascade, 134 97 post bigint not null 135 98 ); 136 99 137 -drop table if exists parsav_attach cascade; 138 100 create table parsav_attach ( 139 101 id bigint primary key default (1+random()*(2^63-1))::bigint, 140 102 birth timestamp not null default now(), 141 103 content bytea not null, 142 104 mime text, -- null if unknown, will be reported as x-octet-stream 143 105 description text, 144 106 parent bigint -- post id, or userid for avatars 145 107 ); 146 108 147 -drop table if exists parsav_circles cascade; 148 109 create table parsav_circles ( 149 110 id bigint primary key default (1+random()*(2^63-1))::bigint, 150 111 owner bigint not null references parsav_actors(id), 151 112 name text not null, 152 113 members bigint[] not null default array[]::bigint[], 153 114 154 115 unique (owner,name) 155 116 ); 156 117 157 -drop table if exists parsav_rooms cascade; 158 118 create table parsav_rooms ( 159 119 id bigint primary key default (1+random()*(2^63-1))::bigint, 160 120 origin bigint references parsav_servers(id), 161 121 name text not null, 162 122 description text not null, 163 123 policy smallint not null 164 124 ); 165 125 166 -drop table if exists parsav_room_members cascade; 167 126 create table parsav_room_members ( 168 127 room bigint references parsav_rooms(id), 169 128 member bigint references parsav_actors(id), 170 129 rank smallint not null default 0, 171 130 admin boolean not null default false, -- non-admins with rank can only moderate + invite 172 131 title text, -- admin-granted title like reddit flair 173 132 vouchedby bigint references parsav_actors(id) 174 133 ); 175 134 176 -drop table if exists parsav_invites cascade; 177 135 create table parsav_invites ( 178 136 id bigint primary key default (1+random()*(2^63-1))::bigint, 179 137 -- when a user is created from an invite, the invite is deleted and the invite 180 138 -- ID becomes the user ID. privileges granted on the invite ID during the invite 181 139 -- process are thus inherited by the user 182 140 issuer bigint references parsav_actors(id), 183 141 handle text, -- admin can lock invite to specific handle 184 142 rank smallint not null default 0, 185 143 quota integer not null default 1000 186 144 ); 187 145 188 -drop table if exists parsav_interventions cascade; 189 146 create table parsav_interventions ( 190 147 id bigint primary key default (1+random()*(2^63-1))::bigint, 191 148 issuer bigint references parsav_actors(id) not null, 192 149 scope bigint, -- can be null or room for local actions 193 150 nature smallint not null, -- silence, suspend, disemvowel, etc 194 151 victim bigint not null, -- could potentially target group as well 195 152 expire timestamp -- auto-expires if set 196 153 ); 197 154 198 -end; 155 +-- create a temporary managed auth table; we can delete this later 156 +-- if it ends up being replaced with a view 157 +%include pgsql-auth.sql%
Modified cmdparse.t from [50677a3c0c] to [bfedd61eec].
1 1 -- vim: ft=terra 2 -return function(tbl) 2 +return function(tbl,opts) 3 + opts = opts or {} 3 4 local options = terralib.types.newstruct('options') do 4 5 local flags = '' for _,d in pairs(tbl) do flags = flags .. d[1] end 5 - local helpstr = 'usage: parsav [-' .. flags .. '] [<arg>...]\n' 6 + local flagstr = '[-' .. flags .. ']' 7 + local helpstr = '\n' 6 8 options.entries = { 7 9 {field = 'arglist', type = lib.mem.ptr(rawstring)} 8 10 } 9 11 local shortcases, longcases, init, verifiers = {}, {}, {}, {} 10 12 local self = symbol(&options) 11 13 local arg = symbol(rawstring) 12 14 local idx = symbol(uint) 13 15 local argv = symbol(&rawstring) 14 16 local argc = symbol(int) 15 17 local optstack = symbol(intptr) 18 + local subcmd = symbol(intptr) 16 19 local skip = label() 17 20 local sanitize = function(s) return s:gsub('_','-') end 18 21 for o,desc in pairs(tbl) do 19 - local consume = desc[3] or 0 22 + local consume = desc.consume or 0 23 + local incr = desc.inc or 0 20 24 options.entries[#options.entries + 1] = { 21 - field = o, type = (consume > 0) and &rawstring or bool 25 + field = o, type = (consume > 0) and &rawstring or 26 + (incr > 0) and uint or bool 22 27 } 23 28 helpstr = helpstr .. string.format(' -%s --%s: %s\n', 24 29 desc[1], sanitize(o), desc[2]) 25 30 end 26 31 for o,desc in pairs(tbl) do 27 32 local flag = desc[1] 28 - local consume = desc[3] or 0 29 - init[#init + 1] = quote [self].[o] = [(consume > 0 and `nil) or false] end 33 + local consume = desc.consume or 0 34 + local incr = desc.inc or 0 35 + init[#init + 1] = quote [self].[o] = [ 36 + (consume > 0 and `nil) or 37 + (incr > 0 and `0 ) or false 38 + ] end 30 39 local ch if consume > 0 then 31 40 ch = quote 32 41 [self].[o] = argv+(idx+1+optstack) 33 42 optstack = optstack + consume 34 43 end 35 44 verifiers[#verifiers+1] = quote 36 45 var terminus = argv + argc 37 46 if [self].[o] ~= nil and [self].[o] >= terminus then 38 47 lib.bail(['missing argument for command line option ' .. sanitize(o)]) 39 48 end 40 49 end 50 + elseif incr > 0 then 51 + ch = quote [self].[o] = [self].[o] + incr end 41 52 else ch = quote 42 53 [self].[o] = true 43 54 end end 44 55 shortcases[#shortcases + 1] = quote 45 56 case [int8]([string.byte(flag)]) then [ch] end 46 57 end 47 58 longcases[#longcases + 1] = quote ................................................................................ 49 60 end 50 61 end 51 62 terra options:free() self.arglist:free() end 52 63 options.methods.parse = terra([self], [argc], [argv]) 53 64 [init] 54 65 var parseopts = true 55 66 var [optstack] = 0 67 + var [subcmd] = [ opts.subcmd or 0 ] 56 68 self.arglist = lib.mem.heapa(rawstring, argc) 57 69 var finalargc = 0 58 70 for [idx]=1,argc do 59 71 var [arg] = argv[idx] 60 72 if optstack > 0 then optstack = optstack - 1 goto [skip] end 61 73 if arg[0] == @'-' and parseopts then 62 74 if arg[1] == @'-' then -- long option ................................................................................ 68 80 switch arg[j] do [shortcases] end 69 81 j = j + 1 70 82 end 71 83 end 72 84 else 73 85 self.arglist.ptr[finalargc] = arg 74 86 finalargc = finalargc + 1 87 + if subcmd > 0 then 88 + subcmd = subcmd - 1 89 + if subcmd == 0 then parseopts = false end 90 + end 75 91 end 76 92 ::[skip]:: 77 93 end 78 94 [verifiers] 79 95 if finalargc == 0 then self.arglist:free() 80 96 else self.arglist:resize(finalargc) end 81 97 end 82 - options.helptxt = helpstr 98 + options.helptxt = { opts = helpstr, flags = flagstr } 83 99 end 84 100 return options 85 101 end
Modified common.lua from [e762fc8997] to [c72e0a0971].
101 101 local kt = {} 102 102 for k,v in pairs(ary) do kt[#kt+1] = k end 103 103 return kt 104 104 end; 105 105 ingest = function(f) 106 106 local h = io.open(f, 'r') 107 107 if h == nil then return nil end 108 - local txt = f:read('*a') f:close() 108 + local txt = h:read('*a') h:close() 109 109 return chomp(txt) 110 110 end; 111 111 parseargs = function(a) 112 112 local raw = false 113 113 local opts, args = {}, {} 114 114 for i,v in ipairs(a) do 115 115 if v == '--' then
Modified config.lua from [29834379ec] to [0c09bec0e6].
33 33 tgthf = u.tobool(default('parsav_arch_armhf',true)); 34 34 doc = { 35 35 online = u.tobool(default('parsav_online_documentation',true)); 36 36 offline = u.tobool(default('parsav_offline_documentation',true)); 37 37 }; 38 38 outform = default('parsav_emit_type', 'o'); 39 39 endian = default('parsav_arch_endian', 'little'); 40 + prefix = default('parsav_install_prefix', './'); 40 41 build = { 41 42 id = u.rndstr(6); 42 43 release = u.ingest('release'); 43 44 when = os.date(); 44 45 }; 45 46 feat = {}; 47 + debug = u.tobool(default('parsav_enable_debug',true)); 46 48 backends = defaultlist('parsav_backends', 'pgsql'); 47 49 braingeniousmode = false; 48 50 embeds = { 49 51 {'style.css', 'text/css'}; 50 52 {'default-avatar.webp', 'image/webp'}; 51 53 {'padlock.webp', 'image/webp'}; 52 54 {'warn.webp', 'image/webp'}; ................................................................................ 58 60 if u.ping '.fslckout' or u.ping '_FOSSIL_' then 59 61 if u.ping '_FOSSIL_' then default_os = 'windows' end 60 62 conf.build.branch = u.exec { 'fossil', 'branch', 'current' } 61 63 conf.build.checkout = (u.exec { 'fossil', 'sql', 62 64 [[select value from localdb.vvar where name = 'checkout-hash']] 63 65 }):gsub("^'(.*)'$", '%1') 64 66 end 65 -conf.os = default('parsav_host_os', default_os); 66 -conf.tgtos = default('parsav_target_os', default_os); 67 +conf.os = default('parsav_host_os', default_os) 68 +conf.tgtos = default('parsav_target_os', default_os) 67 69 conf.posix = posixes[conf.os] 68 -conf.exe = u.tobool(default('parsav_link',not conf.tgttrip)); -- turn off for partial builds 70 +conf.exe = u.tobool(default('parsav_link',not conf.tgttrip)) -- turn off for partial builds 71 +conf.prefix_conf = default('parsav_install_prefix_cfg', conf.prefix) 72 +conf.prefix_static = default('parsav_install_prefix_static', nil) 69 73 conf.build.origin = coalesce( 70 74 os.getenv('parsav_builder'), 71 75 string.format('%s@%s', coalesce ( 72 76 os.getenv('USER'), 73 77 u.exec{'whoami'} 74 78 ), u.exec{'hostname'}) -- whoami and hostname are present on both windows & unix 75 79 )
Modified crypt.t from [bf3957f4f4] to [9b6529621c].
34 34 sha384 = `hashalg {id = lib.md.MBEDTLS_MD_SHA384; bytes = m.algsz.sha384}; 35 35 sha224 = `hashalg {id = lib.md.MBEDTLS_MD_SHA224; bytes = m.algsz.sha224}; 36 36 -- md5 = {id = lib.md.MBEDTLS_MD_MD5};-- !!! 37 37 }; 38 38 local callbacks = {} 39 39 if config.feat.randomizer == 'kern' then 40 40 local rnd = terralib.externfunction('getrandom', {&opaque, intptr, uint} -> ptrdiff); 41 - terra callbacks.randomize(ctx: &opaque, dest: &uint8, sz: intptr): int 41 + terra m.spray(dest: &uint8, sz: intptr): int 42 42 return rnd(dest, sz, 0) 43 43 end 44 44 elseif config.feat.randomizer == 'devfs' then 45 - terra callbacks.randomize(ctx: &opaque, dest: &uint8, sz: intptr): int 45 + terra m.spray(dest: &uint8, sz: intptr): int 46 46 var gen = lib.io.open("/dev/urandom",0) 47 47 lib.io.read(gen, dest, sz) 48 48 lib.io.close(gen) 49 49 return sz 50 50 end 51 51 elseif config.feat.randomizer == 'libc' then 52 52 local rnd = terralib.externfunction('rand', {} -> int); 53 53 local srnd = terralib.externfunction('srand', uint -> int); 54 54 local time = terralib.includec 'time.h' 55 55 lib.init[#lib.init + 1] = quote srnd(time.time(nil)) end 56 56 print '(warn) using libc soft-rand function for cryptographic purposes, this is very bad!' 57 - terra callbacks.randomize(ctx: &opaque, dest: &uint8, sz: intptr): int 57 + terra m.spray(dest: &uint8, sz: intptr): int 58 58 for i=0,sz do dest[i] = [uint8](rnd()) end 59 59 return sz 60 60 end 61 61 end 62 + 63 +m.random = macro(function(typ, from, to) 64 + local ty = typ:astype() 65 + return quote 66 + var v: ty 67 + m.spray([&uint8](&v), sizeof(ty)) 68 + v = v % (to - from) + from -- only works with unsigned!! 69 + in v end 70 +end) 71 + 72 +terra callbacks.randomize(ctx: &opaque, dest: &uint8, sz: intptr) 73 + return m.spray(dest,sz) end 62 74 63 75 terra m.pem(pub: bool, key: &ctx, buf: &uint8): bool 64 76 if pub then 65 77 return lib.pk.mbedtls_pk_write_pubkey_pem(key, buf, const.maxpemsz) == 0 66 78 else 67 79 return lib.pk.mbedtls_pk_write_key_pem(key, buf, const.maxpemsz) == 0 68 80 end
Added html.t version [ee4d50abb4].
1 +-- vim: ft=terra 2 +local m={} 3 +local pstr = lib.mem.ptr(int8) 4 + 5 +terra m.sanitize(txt: pstr, quo: bool) 6 + var a: lib.str.acc a:init(txt.ct*1.3) 7 + for i=0,txt.ct do 8 + if txt(i) == @'<' then a:lpush('<') 9 + elseif txt(i) == @'>' then a:lpush('>') 10 + elseif txt(i) == @'&' then a:lpush('&') 11 + elseif quo and txt(i) == @'"' then a:lpush('"') 12 + else a:push(&txt(i),1) end 13 + end 14 + return a:finalize() 15 +end 16 + 17 +return m
Modified makefile from [3210eb684d] to [8946539e56].
1 1 dl = git 2 2 dbg-flags = $(if $(dbg),-g) 3 3 4 4 images = $(addsuffix .webp, $(basename $(wildcard static/*.svg))) 5 5 styles = $(addsuffix .css, $(basename $(wildcard static/*.scss))) 6 6 7 -parsav: parsav.t config.lua pkgdata.lua $(images) $(styles) 7 +parsav parsavd: parsav.t config.lua pkgdata.lua $(images) $(styles) 8 8 terra $(dbg-flags) $< 9 -parsav.o: parsav.t config.lua pkgdata.lua $(images) $(styles) 9 +parsav.o parsavd.o: parsav.t config.lua pkgdata.lua $(images) $(styles) 10 10 env parsav_link=no terra $(dbg-flags) $< 11 -parsav.ll: parsav.t config.lua pkgdata.lua $(images) $(styles) 11 +parsav.ll parsavd.ll: parsav.t config.lua pkgdata.lua $(images) $(styles) 12 12 env parsav_emit_type=ll parsav_link=no terra $(dbg-flags) $< 13 -parsav.s: parsav.ll 13 +parsav.s parsavd.ss: parsav.ll 14 14 llc --march=$(target) $< 15 15 16 16 static/%.webp: static/%.png 17 17 cwebp -q 90 $< -o $@ 18 18 static/%.png: static/%.svg 19 19 inkscape -f $< -C -d 180 -e $@ 20 20 static/%.css: static/%.scss
Added mgtool.t version [293667feb7].
1 +-- vim: ft=terra 2 +-- provides the functionality of the `parsav` utility that controls `parsavd` 3 +local pstr = lib.mem.ptr(int8) 4 +local ctloptions = lib.cmdparse({ 5 + version = {'V', 'display information about the binary build and exit'}; 6 + verbose = {'v', 'increase logging verbosity', inc=1}; 7 + quiet = {'q', 'do not print to standard out'}; 8 + help = {'h', 'display this list'}; 9 + backend_file = {'B', 'init from specified backend file', consume=1}; 10 + backend = {'b', 'operate on only the selected backend'}; 11 + instance = {'i', 'specify the instance to control by name', consume=1}; 12 + all = {'A', 'affect all running instances'}; 13 +}, { subcmd = 1 }) 14 + 15 +local pbasic = lib.cmdparse { 16 + help = {'h', 'display this list'} 17 +} 18 +local subcmds = { 19 +} 20 + 21 +local ctlcmds = { 22 + { 'start', 'start a new instance of the server' }; 23 + { 'stop', 'stop a running instance' }; 24 + { 'attach', 'capture log output from a running instance' }; 25 + { 'db init <domain>', 'initialize backend databases (or a single specified database) with the necessary schema and structures for the given FQDN' }; 26 + { 'db vacuum', 'delete old remote content from the database' }; 27 + { 'db extract (<artifact>|<post>/<attachment number>)', 'extracts an attachment artifact from the database and prints it to standard out' }; 28 + { 'db excise <artifact>', 'extracts an attachment artifact from the database and prints it to standard out' }; 29 + { 'db obliterate', 'completely purge all parsav-related content and structure from the database, destroying all user content (requires confirmation)' }; 30 + { 'db insert', 'reads a file from standard in and inserts it into the attachment database, printing the resulting ID' }; 31 + { 'mkroot <handle>', 'establish a new root user with the given handle' }; 32 + { 'user <handle> auth <type> new', '(where applicable, managed auth only) create a new authentication token of the given type for a user' }; 33 + { 'user <handle> auth <type> reset', '(where applicable, managed auth only) delete all of a user\'s authentication tokens of the given type and issue a new one' }; 34 + { 'user <handle> auth purge-credentials [<type>]', 'delete all credentials that would allow this user to log in (where possible)' }; 35 + { 'user <handle> (grant|revoke) (<priv>|all)', 'grant or revoke a specific power to or from a user' }; 36 + { 'user <handle> emasculate', 'strip all administrative powers from a user' }; 37 + { 'user <handle> suspend [<timespec>]', '(e.g. \27[1muser jokester suspend 5d 6h 7m 3s\27[m to suspend "jokester" for five days, six hours, seven minutes, and three seconds) suspend a user'}; 38 + { 'actor <xid> purge-all', 'remove all traces of a user from the database (except local user credentials -- use \27[1mauth purge-credentials\27[m to prevent a user from accessing the instance)' }; 39 + { 'actor <xid> create', 'instantiate a new actor' }; 40 + { 'actor <xid> bestow <epithet>', 'bestow an epithet upon an actor' }; 41 + { 'conf set <setting> <value>', 'add or a change a server configuration parameter to the database' }; 42 + { 'conf get <setting>', 'report the value of a server setting' }; 43 + { 'conf reset <setting>', 'reset a server setting to its default value' }; 44 + { 'conf refresh', 'instruct an instance to refresh its configuration cache' }; 45 + { 'conf chsec', 'reset the server secret, invalidating all authentication cookies' }; 46 + { 'serv dl', 'initiate an update cycle over foreign actors' }; 47 + { 'tl', 'print the current local timeline to standard out' }; 48 + { 'be pgsql setup-auth (managed|unmanaged)', '(PGSQL backends) select the authentication strategy to use' }; 49 +} 50 + 51 +local ctlcmdhelp = 'commands:\n' 52 +for _, v in ipairs(ctlcmds) do 53 + ctlcmdhelp = ctlcmdhelp .. string.format ( 54 + ' \27[1m%s\27[m: %s\n', v[1]:gsub('(<%w+>)','\27[36m%1\27[;1m'), v[2] 55 + ) 56 +end 57 + 58 +local struct idelegate { 59 + all: bool 60 + src: &lib.store.source 61 + srv: &lib.srv.overlord 62 +} 63 +idelegate.metamethods.__methodmissing = macro(function(meth, self, ...) 64 + local expr = {...} 65 + local rt 66 + for _,f in pairs(lib.store.backend.entries) do 67 + local fn = f.field or f[1] 68 + local ft = f.type or f[2] 69 + if fn == meth then rt = ft.type.returntype break end 70 + end 71 + 72 + return quote 73 + var r: rt 74 + if self.all 75 + then r=self.srv:[meth]([expr]) 76 + elseif self.src ~= nil then r=self.src:[meth]([expr]) 77 + else lib.bail('no data source specified') 78 + end 79 + in r end 80 +end) 81 + 82 +local terra gensec(sdest: rawstring) 83 + var dest = [&uint8](sdest) 84 + lib.crypt.spray(dest,64) 85 + for i=0,64 do dest[i] = dest[i] % (0x7e - 0x20) + 0x20 end 86 + dest[64] = 0 87 +end 88 + 89 +local terra entry_mgtool(argc: int, argv: &rawstring): int 90 + if argc < 1 then lib.bail('bad invocation!') end 91 + 92 + lib.noise_init(2) 93 + [lib.init] 94 + 95 + var srv: lib.srv.overlord 96 + var dlg = idelegate { srv = &srv, src = nil } 97 + 98 + var mode: ctloptions 99 + mode:parse(argc,argv) defer mode:free() 100 + if mode.version then version() return 0 end 101 + if mode.help then 102 + [ lib.emit(false, 1, 'usage: ', `argv[0], ' ', ctloptions.helptxt.flags, ' <cmd> [<args>…]', ctloptions.helptxt.opts, ctlcmdhelp) ] 103 + return 0 104 + end 105 + var cnf: rawstring 106 + if mode.backend_file ~= nil 107 + then cnf = @mode.backend_file 108 + else cnf = lib.proc.getenv('parsav_backend_file') 109 + end 110 + if cnf == nil then cnf = "backend.conf" end 111 + if mode.all then dlg.all = true else 112 + -- iterate through and pick the right backend 113 + end 114 + 115 + if mode.arglist.ct == 0 then lib.bail('no command') return 1 end 116 + if lib.str.cmp(mode.arglist(0),'attach') == 0 then 117 + elseif lib.str.cmp(mode.arglist(0),'start') == 0 then 118 + elseif lib.str.cmp(mode.arglist(0),'stop') == 0 then 119 + else 120 + if lib.str.cmp(mode.arglist(0),'db') == 0 then 121 + var dbmode: pbasic dbmode:parse(mode.arglist.ct, &mode.arglist(0)) 122 + if dbmode.help then 123 + [ lib.emit(false, 1, 'usage: ', `argv[0], ' db ', dbmode.type.helptxt.flags, ' <cmd> [<args>…]', dbmode.type.helptxt.opts) ] 124 + return 1 125 + end 126 + if dbmode.arglist.ct < 1 then goto cmderr end 127 + 128 + srv:setup(cnf) 129 + if lib.str.cmp(dbmode.arglist(0),'init') == 0 and dbmode.arglist.ct == 2 then 130 + lib.report('initializing new database structure for domain ', dbmode.arglist(1)) 131 + dlg:dbsetup() 132 + srv:conprep(lib.store.prepmode.conf) 133 + dlg:conf_set('instance-name', dbmode.arglist(1)) 134 + do var sec: int8[65] gensec(&sec[0]) 135 + dlg:conf_set('server-secret', &sec[0]) 136 + end 137 + lib.report('database setup complete; use mkroot to create an administrative user') 138 + elseif lib.str.cmp(dbmode.arglist(0),'obliterate') == 0 then 139 + var confirmstrs = array( 140 + 'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'eta', 'nu', 'kappa' 141 + ) 142 + var cfmstr: int8[64] cfmstr[0] = 0 143 + var tdx = lib.osclock.time(nil) / 60 144 + for i=0,3 do 145 + if i ~= 0 then lib.str.cat(&cfmstr[0], '-') end 146 + lib.str.cat(&cfmstr[0], confirmstrs[(tdx + 49*i) % [confirmstrs.type.N]]) 147 + end 148 + 149 + if dbmode.arglist.ct == 1 then 150 + lib.bail('you are attempting to completely obliterate all data! make sure you have selected your target correctly. if you really want to do this, pass the confirmation string ', &cfmstr[0]) 151 + elseif dbmode.arglist.ct == 2 then 152 + if lib.str.cmp(dbmode.arglist(1), cfmstr) == 0 then 153 + lib.warn('completely obliterating all data!') 154 + dlg:obliterate_everything() 155 + else 156 + lib.bail('you passed an incorrect confirmation string; pass ', &cfmstr[0], ' if you really want to destroy everything') 157 + end 158 + else goto cmderr end 159 + else goto cmderr end 160 + elseif lib.str.cmp(mode.arglist(0),'be') == 0 then 161 + srv:setup(cnf) 162 + elseif lib.str.cmp(mode.arglist(0),'conf') == 0 then 163 + srv:setup(cnf) 164 + srv:conprep(lib.store.prepmode.conf) 165 + var cfmode: lib.cmdparse { 166 + help = {'h','display this list'}; 167 + no_notify = {'n', "don't instruct the server to refresh its configuration cache after making changes; useful for \"transactional\" configuration changes."}; 168 + } 169 + cfmode:parse(mode.arglist.ct, &mode.arglist(0)) 170 + if cfmode.help then 171 + [ lib.emit(false, 1, 'usage: ', `argv[0], ' conf ', cfmode.type.helptxt.flags, ' <cmd> [<args>…]', cfmode.type.helptxt.opts) ] 172 + return 1 173 + end 174 + if cfmode.arglist.ct < 1 then goto cmderr end 175 + 176 + if cfmode.arglist.ct == 1 then 177 + if lib.str.cmp(cfmode.arglist(0),'chsec') == 0 then 178 + var sec: int8[65] gensec(&sec[0]) 179 + dlg:conf_set('server-secret', &sec[0]) 180 + lib.report('server secret reset') 181 + -- FIXME notify server to reload its config 182 + elseif lib.str.cmp(cfmode.arglist(0),'refresh') == 0 then 183 + -- TODO notify server to reload config 184 + else goto cmderr end 185 + elseif cfmode.arglist.ct == 3 and 186 + lib.str.cmp(cfmode.arglist(0),'set') == 0 then 187 + dlg:conf_set(cfmode.arglist(1),cfmode.arglist(2)) 188 + lib.report('parameter set') 189 + else goto cmderr end 190 + else 191 + srv:setup(cnf) 192 + srv:conprep(lib.store.prepmode.full) 193 + if lib.str.cmp(mode.arglist(0),'mkroot') == 0 then 194 + var cfmode: pbasic cfmode:parse(mode.arglist.ct, &mode.arglist(0)) 195 + if cfmode.help then 196 + [ lib.emit(false, 1, 'usage: ', `argv[0], ' mkroot ', cfmode.type.helptxt.flags, ' <handle>', cfmode.type.helptxt.opts) ] 197 + return 1 198 + end 199 + if cfmode.arglist.ct == 1 then 200 + var am = dlg:conf_get('credential-store') 201 + var mg: bool 202 + if (not am) or am:cmp(lib.str.plit 'managed') then 203 + mg = true 204 + elseif am:cmp(lib.str.plit 'unmanaged') then 205 + lib.warn('credential store is unmanaged; you will need to create credentials for the new root user manually!') 206 + mg = false 207 + else lib.bail('unknown credential store mode "',{am.ptr,am.ct},'"; should be either "managed" or "unmanaged"') end 208 + var kbuf: uint8[lib.crypt.const.maxdersz] 209 + var root = lib.store.actor.mk(&kbuf[0]) 210 + root.handle = cfmode.arglist(0) 211 + var epithets = array( 212 + 'root', 'god', 'regional jehovah', 'titan king', 213 + 'king of olympus', 'cyberpharaoh', 'electric ellimist', 214 + "rampaging c'tan", 'deathless tweetlord', 'postmaster', 215 + 'faerie queene', 'lord of the posts', 'ruthless cybercrat', 216 + 'general secretary', 'commissar', 'kwisatz haderach' 217 + -- feel free to add more 218 + ) 219 + root.epithet = epithets[lib.crypt.random(intptr,0,[epithets.type.N])] 220 + root.rights.powers:fill() -- grant omnipotence 221 + root.rights.rank = 1 222 + var ruid = dlg:actor_create(&root) 223 + dlg:conf_set('master',root.handle) 224 + lib.report('created new administrator') 225 + if mg then 226 + lib.dbg('generating temporary password') 227 + var tmppw: uint8[33] 228 + lib.crypt.spray(&tmppw[0],32) tmppw[32] = 0 229 + for i=0,32 do 230 + tmppw[i] = tmppw[i] % (10 + 26*2) 231 + if tmppw[i] >= 36 then 232 + tmppw[i] = tmppw[i] + (0x61 - 36) 233 + elseif tmppw[i] >= 10 then 234 + tmppw[i] = tmppw[i] + (0x41 - 10) 235 + else tmppw[i] = tmppw[i] + 0x30 end 236 + end 237 + lib.dbg('assigning temporary password') 238 + dlg:auth_create_pw(ruid, false, pstr { 239 + ptr = [rawstring](&tmppw[0]), ct = 32 240 + }) 241 + lib.report('temporary root pw: ', {[rawstring](&tmppw[0]), 32}) 242 + end 243 + else goto cmderr end 244 + elseif lib.str.cmp(mode.arglist(0),'user') == 0 then 245 + elseif lib.str.cmp(mode.arglist(0),'actor') == 0 then 246 + elseif lib.str.cmp(mode.arglist(0),'tl') == 0 then 247 + elseif lib.str.cmp(mode.arglist(0),'serv') == 0 then 248 + else goto cmderr end 249 + end 250 + end 251 + 252 + do return 0 end 253 + ::cmderr:: lib.bail('invalid command') return 2 254 +end 255 + 256 +return entry_mgtool
Modified parsav.md from [ae23203c4b] to [4b27db126a].
39 39 40 40 if you use nixos and wish to build the pdf documentation, you're going to have to do a bit of extra work (but you're used to that, aren't you). for some incomprehensible reason, the groff package on nix is split up, seemingly randomly, with many crucial output devices relegated to the "perl" output of the package, which is not installed by default (and `nix-env -iA nixos.groff.perl` doesn't work either; i don't know why either). you'll have to instantiate and install the outputs directly by path, e.g. `nix-env -i /nix/store/*groff*/` to get everything you need into your profile. alas, the battle is not over: you also need to change the environment variables `GROFF_FONT_PATH` and `GROFF_TMAC_PATH` to point at the `font` and `tmac` subdirs of `~/.nix-profile/share/groff/$groff_version/`. once this is done, invoking `groff -Tpdf` will work as expected. 41 41 42 42 ## configuring 43 43 44 44 the `parsav` configuration is comprised of two components: the backends list and the config store. the backends list is a simple text file that tells `parsav` which data sources to draw from. the config store is a key-value store which contains the rest of the server's configuration, and is loaded from the backends. the configuration store can be spread across the backends; backends will be checked for configuration keys according to the order in which they are listed. changes to the configuration store affect parsav in real time; you only need to restart the server if you make a change to the backend list. 45 45 46 -eventually, we'll add a command-line tool `parsav-cfg` to enable easy modification of the configuration store from the command line; for now, you'll need to modify the database by hand or use the online administration menu. the schema.sql file contains commands to prompt for various important values like the name of your administrative user. 46 +you can directly modify the store from the command line with the `parsav conf` command; see `parsav conf -h` for more information. 47 47 48 48 by default, parsav looks for a file called `backend.conf` in the current directory when it is launched. you can override this default with the `parsav_backend_file` environment or with the `-b`/`--backend-file` flag. `backend.conf` lists one backend per line, in the form `id type confstring`. for instance, if you had two postgresql databases, you might write a backend file like 49 49 50 50 master pgsql host=localhost dbname=parsav 51 51 tweets pgsql host=420.69.dread.cloud dbname=content 52 52 53 53 the form the configuration string takes depends on the specific backend. 54 54 55 +once you've set up a backend and confirmed parsav can connect succesfully to it, you can initialize the database with the command `parsav db init <domain>`, where `<domain>` is the name of the domain name you will be hosting `parsav` from. this will install all necessary structures and functions in the target and create all necessary files. it will not, however, create any users. you can create an initial administrative user with the `parsav mkroot <handle>` command, where `<handle>` is the handle you want to use on the server. this will also assign a temporary password for the user if possible. you should now be able to log in and administer the server. 56 + 57 +by default, parsav binds to [::1]:10917. if you want to change this (to run it on a different port, or make it directly accessible to other servers on the network), you can use the command `parsav conf set bind <address>`, where `address` is a binding specification like `0.0.0.0:80`. it is recommended, however, that `parsavd` be kept accessible only from localhost, and that connections be forwarded to it from nginx, haproxy, or a similar reverse proxy. (this can also be changed with the online configuration UI) 58 + 55 59 ### postgresql backend 56 60 57 -currently, postgres needs to be configured manually before parsav can make use of it to store data. the first step is to create a database for parsav's use. once you've done that, you need to create the database schema with the command `$ psql (-h $host) -d $database -f schema.sql`. you'll be prompted for some crucial settings to install in the configuration store, such as the name of the relation you want to use for authentication (we'll call it `parsav_auth` from here on out). 61 +a database will need to be created for `parsav`'s use before `parsav db init` will work. this can be accomplished with a command like `$ createdb parsav`. you'll also of course need to set up some way for `parsavd` to authenticate itself to `postgres`. peer auth is the most secure option, and this is what you should use if postgres and `parsavd` are running on the same box. specify the database name to the backend the usual way, with a clause like `dbname=parsav` in your connection string. 58 62 59 -parsav separates the storage of user credentials from the storage of other user data, in order to facilitate centralized user accounting. you don't need to take advantage of this feature, and if you don't want to, you can just create a `parsav_auth` table and have done. however, `parsav_auth` can also be a view, collecting a list of authorized users and their various credentials from whatever source you please. 63 +the postgresql backend has some extra features that enable it to be integrated with existing authentication databases you may have. when you initialize the database, a table `parsav_auth` will be created to hold the credentials of the instance users and the authentication mode will be set to "managed", which will enable parsav's built-in credential administration tools. if you would prefer to use your own source of credentials, you'll need to set parsav to "unmanaged" mode with the command `parsav be pgsql setup-auth unmanaged`. 64 + 65 +this command will reconfigure `parsav` and remove the `parsav_auth` table, making room for you to create a view with the same name. if you want to go back to managed mode at any time, just run `parsav be psql setup-auth managed`; just be aware that this will delete your auth view! 60 66 61 67 `parsav_auth` has the following schema: 62 68 63 69 create table parsav_auth ( 64 70 aid bigint primary key, 65 71 uid bigint, 66 72 newname text, ................................................................................ 69 75 restrict text[], 70 76 netmask cidr, 71 77 blacklist bool 72 78 ) 73 79 74 80 `aid` is a unique value identifying the authentication method. it must be deterministic -- values based on time of creation or a hash of `uid`+`kind`+`cred` are ideal. `uid` is the identifier of the user the row specifies credentials for. `kind` is a string indicating the credential type, and `cred` is the content of that credential.for the meaning of these fields and use of this structure, see **authentication** below. 75 81 76 -## authentication 77 82 in the most basic case, an authentication record would be something like `{uid = 123, kind = "pw-sha512", cred = "\x12bf90…a10e"::bytea}`. but `parsav` is not restricted to username-password authentication, and in addition to various hashing styles, it also will support more esoteric forms of authentcation. any individual user can have as many auth rows as she likes. there is also a `restrict` field, which is normally null, but can be specified in order to restrict a particular credential to certain operations, such as posting tweets or updating a bio. `blacklist` indicates that any attempt to authenticate that matches this row will be denied, regardless of whether it matches other rows. if `netmask` is present, this authentication will only succeed if it comes from the specified IP mask. 78 83 79 -`uid` can also be `0` (not null, which matches any user!), indicating that there is not yet a record in `parsav_actors` for this account. if this is the case, `name` must contain the handle of the account to be created when someone attempts to log in with this credential. whether `name` is used in the authentication process depends on whether the authentication method accepts a username. all rows with the same `uid` *must* have the same `name`. 84 +`uid` can also be `0` (emphatically *not* null, which causes the rule to match any user!), indicating that there is not yet a record in `parsav_actors` for this account. if this is the case, `name` must contain the handle of the account to be created when someone attempts to log in with this credential. whether `name` is used in the authentication process depends on whether the authentication method accepts a username. all rows with the same `uid` *must* have the same `name`. 85 + 86 +## invoking 87 +the build process generates two binaries, `parsav` and `parsavd`. `parsav` is a driver tool that can be used to set up and start a `parsav` instance, as well as administer it from the command line. it accesses databases directly and uses the same backend configuration file as parsav, but can also send IPC messages directly to running `parsavd` instances. 88 + 89 +as a convenience, the `parsav start` command can be used to start and daemonize a `parsav` instance. additionally, the `-l` option to `parsav start` can be used to redirect `parsavd`'s logging output to a file; without `-l`, logging output will be discarded and can be viewed only by connecting to the running instance with `parsav attach`. `parsav start` passes its arguments on to `parsavd`; you can use this to pass options by separating `parsav`'s arguments from `parsavd`'s with `--`. if you launch an instance with `parsav start -- -i chungus`, you can then stop that instance with `parsav -i chungus stop`. `parsav stop` can be used on its own if only one `parsavd` instance is running; otherwise, `parsav -a stop` will cleanly terminate all running instances. 90 + 91 +you generally should not invoke `parsavd` directly except for debugging purposes, or in the context of an init daemon (particularly systemd). if you launch `parsavd` directly it will not fork to the background. 80 92 93 +## authentication 81 94 below is a full list of authentication types we intend/hope to one day support. contributors should consider this a to-do list. a checked box indicates the scheme has been implemented. 82 95 83 96 * ☑ pw-sha{512,384,256,224}: an ordinary password, hashed with the appropriate algorithm 84 97 * ☐ pw-{sha1,md5,clear} (insecure, must be manually enabled at compile time with the config variable `parsav_let_me_be_a_dumbass="i know what i'm doing"`) 85 98 * ☐ pw-pbkdf2-hmac-sha{…}: a password hashed with the Password-Based Key Derivation Function 2 instead of plain SHA2 86 99 * ☐ pw-extern-ldap: try to authenticate by binding against an LDAP server 87 100 * ☐ pw-extern-cyrus: try to authenticate against saslauthd ................................................................................ 92 105 * ☐ api-digest-sha{…}: a value that can be hashed with the current epoch to derive a temporary access key without logging in. these are used for API calls, sent in the header `X-API-Key`. 93 106 * ☐ otp-time-sha1: a TOTP PSK: the first two bytes represent the step, the third byte the OTP length, and the remaining ten bytes the secret key 94 107 * ☐ tls-cert-fp: a fingerprint of a client certificate 95 108 * ☐ tls-cert-ca: a value of the form `fp/key=value` where a client certificate with the property `key=value` (e.g. `uid=cyberlord19`) signed by a certificate authority matching the given fingerprint `fp` can authenticate the user 96 109 * ☐ challenge-rsa-sha256: an RSA public key. the user is presented with a challenge and must sign it with the corresponding private key using SHA256. 97 110 * ☐ challenge-ecc-sha256: a Curve25519 public key. the user is presented with a challenge and must sign it with the corresponding private key using SHA256. 98 111 * ☐ challenge-ecc448-sha256: a Curve448 public key. the user is presented with a challenge and must sign it with the corresponding private key using SHA256. 99 -* ☑ trust: authentication always succeeds. only use in combination with netmask!!! 112 +* ☑ trust: authentication always succeeds (or fails, if blacklisted). only use in combination with netmask!!! 113 + 114 +## legal 115 + 116 +parsav is released under the terms of the EUPL v1.2. copies of this license are included in the repository. by contributing any intellectual property to this project, you reassign ownership and all attendant rights over that intellectual property to the current maintainer. this is to ensure that the project can be relicensed without difficulty in the unlikely event that it is necessary. 100 117 101 -## license 118 +## code of conduct 102 119 103 -parsav is released under the terms of the EUPL v1.2. copies of this license are included in the repository. dependencies are produced 120 +when hacking on `parsav`, it is absolutely mandatory to wear a wizard hat and burgundy silk summoning cloak. this code of conduct is enforced capriciously by the Fair Folk, and violations are punishable by dancing hex. 104 121 105 122 ## future direction 106 123 107 124 parsav needs more storage backends, as it currently supports only postgres. some possibilities, in order of priority, are: 108 125 109 126 * plain text/filesystem storage 110 127 * lmdb
Modified parsav.t from [1c10e6f8f8] to [d1470e4b10].
11 11 local path = {} 12 12 for m in l:gmatch('([^:]+)') do path[#path+1]=m end 13 13 local tgt = lib 14 14 for i=1,#path-1 do 15 15 if tgt[path[i]] == nil then tgt[path[i]] = {} end 16 16 tgt = tgt[path[i]] 17 17 end 18 - tgt[path[#path]] = terralib.loadfile(l:gsub(':','/') .. '.t')() 18 + tgt[path[#path]:gsub('-','_')] = terralib.loadfile(l:gsub(':','/') .. '.t')() 19 19 end 20 20 end; 21 21 loadlib = function(name,hdr) 22 22 local p = config.pkg[name] 23 23 -- for _,v in pairs(p.dylibs) do 24 24 -- terralib.linklibrary(p.libdir .. '/' .. v) 25 25 -- end ................................................................................ 175 175 else -- print time since last msg 176 176 var dfs = arrayof(int8, 0x30 + diff/10, 0x30 + diff%10, 0x20, 0) 177 177 [ lib.emit(false, 2, ' \27[36m+', `&dfs[0]) ] 178 178 end 179 179 end 180 180 181 181 local defrep = function(level,n,code) 182 + if level >= 3 and config.debug == false then 183 + return macro(function(...) return {} end) 184 + end 182 185 return macro(function(...) 183 186 local fn = (...).filename 184 187 local ln = tostring((...).linenumber) 185 188 local dbgtag = string.format('\27[35m · \27[34m%s:\27[1m%s\27[m\n', fn,ln) 186 189 local q = lib.emit(level < 3 and true or dbgtag, 2, noise_header(code,n), ...) 187 - return quote if noise >= level then timehdr(); [q] end end 190 + return quote 191 + --lib.io.fmt(['attempting to emit at ' .. fn..':'..ln.. '\n']) 192 + if noise >= level then timehdr(); [q] end end 188 193 end); 189 194 end 190 195 lib.dbg = defrep(3,'debug', '32') 191 196 lib.report = defrep(2,'info', '35') 192 197 lib.warn = defrep(1,'warn', '33') 193 198 lib.bail = macro(function(...) 194 199 local q = lib.emit(true, 2, noise_header('31','fatal'), ...) ................................................................................ 328 333 lib.md = lib.loadlib('mbedtls','mbedtls/md.h') 329 334 lib.b64 = lib.loadlib('mbedtls','mbedtls/base64.h') 330 335 lib.net = lib.loadlib('mongoose','mongoose.h') 331 336 lib.pq = lib.loadlib('libpq','libpq-fe.h') 332 337 333 338 lib.load { 334 339 'mem', 'math', 'str', 'file', 'crypt'; 335 - 'http', 'session', 'tpl', 'store'; 340 + 'http', 'html', 'session', 'tpl', 'store'; 336 341 337 342 'smackdown'; -- md-alike parser 338 343 } 339 344 340 345 local be = {} 341 346 for _, b in pairs(config.backends) do 342 347 be[#be+1] = terralib.loadfile('backend/' .. b .. '.t')() ................................................................................ 367 372 local t = lib.tpl.mk { body = v, id = 'view/'..k } 368 373 data.view[k] = t 369 374 end 370 375 371 376 lib.load { 372 377 'srv'; 373 378 'render:nav'; 379 + 'render:nym'; 374 380 'render:login'; 375 381 'render:profile'; 382 + 376 383 'render:compose'; 377 384 'render:tweet'; 378 385 'render:userpage'; 379 386 'render:timeline'; 387 + 380 388 'render:docpage'; 389 + 390 + 'render:conf:profile'; 391 + 'render:conf'; 381 392 'route'; 382 393 } 383 394 384 395 do 385 396 local p = string.format('parsav: %s\nbuilt on %s\n', config.build.str, config.build.when) 386 397 terra version() lib.io.send(1, p, [#p]) end 387 398 end 388 399 389 -terra noise_init() 400 +terra lib.noise_init(default_level: uint) 390 401 starttime = lib.osclock.time(nil) 391 402 lastnoisetime = 0 392 403 var n = lib.proc.getenv('parsav_noise') 393 404 if n ~= nil then 394 405 if n[0] >= 0x30 and n[0] <= 0x39 and n[1] == 0 then 395 406 noise = n[0] - 0x30 396 407 return 397 408 end 398 409 end 399 - noise = 1 410 + noise = default_level 400 411 end 412 +lib.load{'mgtool'} 401 413 402 414 local options = lib.cmdparse { 403 415 version = {'V', 'display information about the binary build and exit'}; 416 + verbose = {'v', 'increase logging verbosity', inc=1}; 404 417 quiet = {'q', 'do not print to standard out'}; 405 418 help = {'h', 'display this list'}; 406 - backend_file = {'b', 'init from specified backend file', 1}; 407 - static_dir = {'S', 'directory with overrides for static content', 1}; 408 - builtin_data = {'B', 'do not load static content overrides at runtime under any circumstances'}; 419 + backend_file = {'B', 'init from specified backend file', consume=1}; 420 + static_dir = {'S', 'directory with overrides for static content', consume=1}; 421 + builtin_data = {'D', 'do not load static content overrides at runtime under any circumstances'}; 422 + instance = {'i', 'set an instance name to make it easier to control multiple daemons', consume = 1}; 409 423 } 410 424 411 425 412 426 local static_setup = quote end 413 427 local mapin = quote end 414 428 local odir = symbol(rawstring) 415 429 local pathbuf = symbol(lib.str.acc) ................................................................................ 437 451 [static_setup] 438 452 if mode.builtin_data then return end 439 453 440 454 var [odir] = lib.proc.getenv('parsav_override_dir') 441 455 if mode.static_dir ~= nil then 442 456 odir=@mode.static_dir 443 457 end 444 - if odir == nil then return end 458 + if odir == nil then [ 459 + config.prefix_static and quote 460 + odir = [config.prefix_static] 461 + end or quote return end 462 + ] end 445 463 446 464 var [pathbuf] defer pathbuf:free() 447 465 pathbuf:compose(odir,'/') 448 466 [mapin] 449 467 end 450 468 451 -terra entry(argc: int, argv: &rawstring): int 469 +local terra entry_daemon(argc: int, argv: &rawstring): int 452 470 if argc < 1 then lib.bail('bad invocation!') end 453 471 454 - noise_init() 472 + lib.noise_init(1) 455 473 [lib.init] 456 474 457 475 -- shut mongoose the fuck up 458 476 lib.net.mg_log_set_callback([terra(msg: &opaque, sz: int, u: &opaque) end], nil) 459 477 var srv: lib.srv.overlord 460 478 461 479 do var mode: options 462 480 mode:parse(argc,argv) defer mode:free() 463 481 static_init(&mode) 464 482 if mode.version then version() return 0 end 465 483 if mode.help then 466 - lib.io.send(1, [options.helptxt], [#options.helptxt]) 484 + [ lib.emit(true, 1, 'usage: ',`argv[0],' ', options.helptxt.flags, ' [<args>…]', options.helptxt.opts) ] 467 485 return 0 468 486 end 469 487 var cnf: rawstring 470 488 if mode.backend_file ~= nil 471 489 then cnf = @mode.backend_file 472 490 else cnf = lib.proc.getenv('parsav_backend_file') 473 491 end 474 - if cnf == nil then cnf = "backend.conf" end 492 + if cnf == nil then cnf = [config.prefix_conf .. "backend.conf"] end 475 493 476 - srv:start(cnf) 494 + srv:setup(cnf) 495 + srv:start(lib.trn(mode.instance ~= nil, @mode.instance, nil)) 477 496 end 478 497 479 498 lib.report('listening for requests') 480 499 while true do 481 500 srv:poll() 482 501 end 483 502 srv:shutdown() 484 503 485 504 return 0 486 505 end 506 + 487 507 488 508 local bflag = function(long,short) 489 509 if short and util.has(buildopts, short) then return true end 490 510 if long and util.has(buildopts, long) then return true end 491 511 return false 492 512 end 493 513 494 514 if bflag('dump-config','C') then 495 515 print(util.dump(config)) 496 516 os.exit(0) 497 517 end 498 518 499 519 local holler = print 500 -local out = config.exe and 'parsav' or ('parsav.' .. config.outform) 520 +local suffix = config.exe and '' or ('.'..config.outform) 521 +local out = 'parsavd' .. suffix 501 522 local linkargs = {} 523 +local target = config.tgttrip and terralib.newtarget { 524 + Triple = config.tgttrip; 525 + CPU = config.tgtcpu; 526 + FloatABIHard = config.tgthf; 527 +} or nil 502 528 503 529 if bflag('quiet','q') then holler = function() end end 504 530 if bflag('asan','s') then linkargs[#linkargs+1] = '-fsanitize=address' end 505 531 if bflag('lsan','S') then linkargs[#linkargs+1] = '-fsanitize=leak' end 506 532 533 +for _,p in pairs(config.pkg) do util.append(linkargs, p.linkargs) end 534 +local linkargs_d = linkargs -- controller is not multithreaded 507 535 if config.posix then 508 - linkargs[#linkargs+1] = '-pthread' 536 + linkargs_d[#linkargs_d+1] = '-pthread' 509 537 end 510 -for _,p in pairs(config.pkg) do util.append(linkargs, p.linkargs) end 511 538 holler('linking with args',util.dump(linkargs)) 512 -terralib.saveobj(out, { 513 - main = entry 514 - }, 515 - linkargs, 516 - config.tgttrip and terralib.newtarget { 517 - Triple = config.tgttrip; 518 - CPU = config.tgtcpu; 519 - FloatABIHard = config.tgthf; 520 - } or nil) 539 + 540 +terralib.saveobj('parsavd'..suffix, { main = entry_daemon }, linkargs_d, target) 541 +terralib.saveobj('parsav' ..suffix, { main = lib.mgtool }, linkargs, target)
Modified render/compose.t from [0685338a7e] to [cb3a66bab9].
4 4 var target, tgtlen = co:getv('to') 5 5 var form: data.view.compose 6 6 if edit == nil then 7 7 form = data.view.compose { 8 8 content = lib.coalesce(target, ''); 9 9 acl = lib.trn(target == nil, 'all', 'mentioned'); -- TODO default acl setting? 10 10 handle = co.who.handle; 11 + circles = ''; -- TODO: list user's circles, rooms, and saved aclexps 11 12 } 12 13 end 13 14 var cotxt = form:tostr() defer cotxt:free() 14 15 15 16 var doc = data.view.docskel { 16 17 instance = co.srv.cfg.instance; 17 18 title = lib.str.plit 'compose';
Added render/conf.t version [6e08f785f6].
1 +-- vim: ft=terra 2 +local pstr = lib.mem.ptr(int8) 3 +local pref = lib.mem.ref(int8) 4 + 5 +local mappings = { 6 + {url = 'profile', title = 'account profile', render = 'profile'}; 7 + {url = 'avi', title = 'avatar', render = 'avatar'}; 8 + {url = 'sec', title = 'security', render = 'sec'}; 9 + {url = 'rel', title = 'relationships', render = 'rel'}; 10 + {url = 'qnt', title = 'quarantine', render = 'quarantine'}; 11 + {url = 'acl', title = 'access control shortcuts', render = 'acl'}; 12 + {url = 'rooms', title = 'chatrooms', render = 'rooms'}; 13 + {url = 'circles', title = 'circles', render = 'circles'}; 14 + 15 + {url = 'srv', title = 'server settings', render = 'srv'}; 16 + {url = 'brand', title = 'instance branding', render = 'rebrand'}; 17 + {url = 'censor', title = 'censorship & badthink suppression', render = 'rebrand'}; 18 + {url = 'users', title = 'user accounting', render = 'users'}; 19 + 20 +} 21 + 22 +local path = symbol(lib.mem.ptr(pref)) 23 +local co = symbol(&lib.srv.convo) 24 +local panel = symbol(pstr) 25 +local invoker = quote co:complain(404,'not found','no such control panel is available in this version of parsav') end 26 + 27 +for i, m in ipairs(mappings) do 28 + if lib.render.conf[m.render] then 29 + invoker = quote 30 + if path(1):cmp(lib.str.lit([m.url])) then 31 + var body = [lib.render.conf[m.render]] (co, path) 32 + var a: lib.str.acc a:init(body.ct+48) 33 + a:lpush(['<h1>' .. m.title .. '</h1>']):ppush(body) 34 + panel = a:finalize() 35 + body:free() 36 + else [invoker] end 37 + end 38 + end 39 +end 40 + 41 +local terra 42 +render_conf([co], [path]) 43 + var menu: lib.str.acc menu:init(64):lpush('<hr>') defer menu:free() 44 + 45 + -- build menu 46 + do var p = co.who.rights.powers 47 + if p.config() then menu:lpush '<a href="/conf/srv">server settings</a>' end 48 + if p.rebrand() then menu:lpush '<a href="/conf/brand">instance branding</a>' end 49 + if p.censor() then menu:lpush '<a href="/conf/censor">badthink alerts</a>' end 50 + if p:affect_users() then menu:lpush '<a href="/conf/users">users</a>' end 51 + end 52 + 53 + -- select the appropriate panel 54 + var [panel] = pstr { ptr = ''; ct = 0 } 55 + if path.ct >= 2 then [invoker] end 56 + 57 + -- avoid the hr if we didn't add any elements 58 + var mptr = pstr { ptr = menu.buf, ct = menu.sz } 59 + if menu.sz <= 4 then mptr.ct = 0 end -- 🙄 60 + var pg = data.view.conf { 61 + menu = mptr; 62 + panel = panel; 63 + } 64 + 65 + var pgt = pg:tostr() defer pgt:free() 66 + co:stdpage([lib.srv.convo.page] { 67 + title = 'configure'; body = pgt; 68 + class = lib.str.plit 'conf'; 69 + }) 70 + 71 + if panel.ct ~= 0 then panel:free() end 72 +end 73 + 74 +return render_conf
Added render/conf/profile.t version [248ab207d4].
1 +-- vim: ft=terra 2 +local pstr = lib.mem.ptr(int8) 3 +local pref = lib.mem.ref(int8) 4 + 5 +local terra cs(s: rawstring) 6 + return pstr { ptr = s, ct = lib.str.sz(s) } 7 +end 8 + 9 +local terra 10 +render_conf_profile(co: &lib.srv.convo, path: lib.mem.ptr(pref)): pstr 11 + 12 + var c = data.view.conf_profile { 13 + handle = cs(co.who.handle); 14 + nym = cs(lib.coalesce(co.who.nym,'')); 15 + bio = cs(lib.coalesce(co.who.bio,'')); 16 + } 17 + return c:tostr() 18 +end 19 + 20 +return render_conf_profile
Modified render/login.t from [671b92715b] to [dd5c50c3e9].
31 31 handle = user.handle; 32 32 name = lib.coalesce(user.nym, user.handle); 33 33 } 34 34 if creds.pw() then 35 35 ch.challenge = P'enter the password associated with your account' 36 36 ch.label = P'password' 37 37 ch.method = P'pw' 38 + ch.auto = P'current-password'; 38 39 elseif creds.otp() then 39 40 ch.challenge = P'enter a valid one-time password for your account' 40 41 ch.label = P'OTP code' 41 42 ch.method = P'otp' 43 + ch.auto = P'one-time-code'; 42 44 elseif creds.challenge() then 43 45 ch.challenge = P'sign the challenge token: <code>...</code>' 44 46 ch.label = P'digest' 45 47 ch.method = P'challenge' 48 + ch.auto = P'one-time-code'; 46 49 else 47 50 co:complain(500,'login failure','unknown login method') 48 51 return 49 52 end 50 53 51 54 doc.body = ch:tostr() 52 55 else
Added render/nym.t version [89e574dd98].
1 +-- vim: ft=terra 2 +local pstr = lib.mem.ptr(int8) 3 + 4 +local terra 5 +render_nym(who: &lib.store.actor, scope: uint64) 6 + var n: lib.str.acc n:init(128) 7 + if who.nym ~= nil and who.nym[0] ~= 0 then 8 + n:compose('<span class="nym">',who.nym,'</span> [<span class="handle">', 9 + who.xid,'</span>]') 10 + else n:compose('<span class="handle">',who.xid,'</span>') end 11 + 12 + if who.epithet ~= nil then 13 + n:lpush(' <span class="epithet">'):push(who.epithet,0):lpush('</span>') 14 + end 15 + 16 + -- TODO: if scope == chat room then lookup titles in room member db 17 + 18 + return n:finalize() 19 +end 20 + 21 +return render_nym
Modified render/profile.t from [efe49adad0] to [03b39adc21].
28 28 29 29 var strfbuf: int8[28*4] 30 30 var stats = co.srv:actor_stats(actor.id) 31 31 var sn_posts = cs(lib.math.decstr_friendly(stats.posts, &strfbuf[ [strfbuf.type.N - 1] ])) 32 32 var sn_follows = cs(lib.math.decstr_friendly(stats.follows, sn_posts.ptr - 1)) 33 33 var sn_followers = cs(lib.math.decstr_friendly(stats.followers, sn_follows.ptr - 1)) 34 34 var sn_mutuals = cs(lib.math.decstr_friendly(stats.mutuals, sn_followers.ptr - 1)) 35 - 35 + var bio = lib.str.plit "<em>tall, dark, and mysterious</em>" 36 + if actor.bio ~= nil then 37 + bio = lib.html.sanitize(cs(actor.bio), false) 38 + end 39 + var fullname = lib.render.nym(actor,0) defer fullname:free() 36 40 var profile = data.view.profile { 37 - nym = cs(lib.coalesce(actor.nym, actor.handle)); 38 - bio = cs(lib.coalesce(actor.bio, "<em>tall, dark, and mysterious</em>")); 41 + nym = fullname; 42 + bio = bio; 39 43 xid = cs(actor.xid); 40 44 avatar = lib.trn(actor.origin == 0, pstr{ptr=avistr.buf,ct=avistr.sz}, 41 45 cs(lib.coalesce(actor.avatar, '/s/default-avatar.webp'))); 42 46 43 47 nposts = sn_posts, nfollows = sn_follows; 44 48 nfollowers = sn_followers, nmutuals = sn_mutuals; 45 49 tweetday = cs(timestr); ................................................................................ 47 51 48 52 auxbtn = auxp; 49 53 } 50 54 51 55 var ret = profile:tostr() 52 56 if actor.origin == 0 then avistr:free() end 53 57 if not (co.aid ~= 0 and co.who.id == actor.id) then auxp:free() end 58 + if actor.bio ~= nil then bio:free() end 54 59 return ret 55 60 end 56 61 57 62 return render_profile
Modified render/tweet.t from [00c7b6fd89] to [ac0f8e680f].
22 22 var timestr: int8[26] lib.osclock.ctime_r(&p.posted, ×tr[0]) 23 23 24 24 var bhtml = lib.smackdown.html([lib.mem.ptr(int8)] {ptr=p.body,ct=0}) defer bhtml:free() 25 25 26 26 var idbuf: int8[lib.math.shorthand.maxlen] 27 27 var idlen = lib.math.shorthand.gen(p.id, idbuf) 28 28 var permalink: lib.str.acc permalink:compose('/post/',{idbuf,idlen}) 29 - 29 + var fullname = lib.render.nym(author,0) defer fullname:free() 30 30 var tpl = data.view.tweet { 31 31 text = bhtml; 32 32 subject = cs(lib.coalesce(p.subject,'')); 33 - nym = cs(lib.coalesce(author.nym, author.handle)); 34 - xid = cs(author.xid); 33 + nym = fullname; 35 34 when = cs(×tr[0]); 36 35 avatar = cs(lib.trn(author.origin == 0, avistr.buf, 37 36 lib.coalesce(author.avatar, '/s/default-avatar.webp'))); 38 37 acctlink = cs(author.xid); 39 38 permalink = permalink:finalize(); 40 39 } 41 40 defer tpl.permalink:free() 42 41 if acc ~= nil then tpl:append(acc) return [lib.mem.ptr(int8)]{ptr=nil,ct=0} end 43 42 var txt = tpl:tostr() 44 43 return txt 45 44 end 46 45 return render_tweet
Modified route.t from [d6af263481] to [4fbd6ed0a5].
81 81 var fakeactor: lib.store.actor 82 82 if act.ptr == nil then 83 83 -- the user is known to us but has not yet claimed an 84 84 -- account on the server. create a template for the 85 85 -- account that will be created once they log in 86 86 fakeact = true 87 87 fakeactor = lib.store.actor { 88 - id = 0, handle = usn, nym = usn; 88 + id = 0, handle = usn, nym = nil; 89 89 origin = 0, bio = nil; 90 - key = [lib.mem.ptr(uint8)] {ptr=nil, ct=0} 90 + key = [lib.mem.ptr(uint8)] {ptr=nil, ct=0}; 91 + epithet = nil; 91 92 } 92 93 act.ct = 1 93 94 act.ptr = &fakeactor 94 95 act.ptr.rights = lib.store.rights_default() 95 96 end 96 97 if am == nil then 97 98 -- pick an auth method ................................................................................ 169 170 lib.render.docpage(co,path(1)) 170 171 elseif path.ct == 1 then 171 172 lib.render.docpage(co, rstring.null()) 172 173 else 173 174 co:complain(404, 'no such documentation', 'invalid documentation URL') 174 175 end 175 176 end 177 + 178 +terra http.configure(co: &lib.srv.convo, path: hpath) 179 + lib.render.conf(co,path) 180 +end 176 181 177 182 do local branches = quote end 178 183 local filename, flen = symbol(&int8), symbol(intptr) 179 184 local page = symbol(lib.http.page) 180 185 local send = label() 181 186 local storage = data.stmap 182 187 for i,e in ipairs(config.embeds) do local id,mime = e[1],e[2] ................................................................................ 262 267 if path.ptr[0]:cmp(lib.str.lit('user')) then 263 268 http.actor_profile_uid(co, path, meth) 264 269 elseif path.ptr[0]:cmp(lib.str.lit('tl')) then 265 270 http.timeline(co, path) 266 271 elseif path.ptr[0]:cmp(lib.str.lit('doc')) then 267 272 if meth ~= method.get and meth ~= method.head then goto wrongmeth end 268 273 http.documentation(co, path) 274 + elseif path.ptr[0]:cmp(lib.str.lit('conf')) then 275 + if co.aid == 0 then goto unauth end 276 + http.configure(co,path) 269 277 else goto notfound end 270 278 return 271 279 end 272 280 273 281 ::wrongmeth:: co:complain(405, 'method not allowed', 'that method is not meaningful for this endpoint') do return end 274 282 ::notfound:: co:complain(404, 'not found', 'no such resource available') do return end 283 + ::unauth:: co:complain(401, 'unauthorized', 'this content is not available at your clearance level') do return end 275 284 end
Modified smackdown.t from [896438e021] to [c51eb8a6b2].
52 52 local terra scanline_wordend(l: rawstring, max: intptr, n: rawstring, nc: intptr) 53 53 var sl = scanline(l,max,n,nc) 54 54 if sl == nil then return nil else sl = sl + nc end 55 55 if sl >= l+max or isws(@sl) then return sl-nc end 56 56 return nil 57 57 end 58 58 59 -terra m.html(md: pstr) 60 - if md.ct == 0 then md.ct = lib.str.sz(md.ptr) end 59 +terra m.html(input: pstr) 60 + if input.ct == 0 then input.ct = lib.str.sz(input.ptr) end 61 + 62 + var md = lib.html.sanitize(input,false) 63 + 61 64 var styled: lib.str.acc styled:init(md.ct) 62 65 63 66 do var i = 0 while i < md.ct do 64 67 var wordstart = (i == 0 or isws(md.ptr[i-1])) 65 68 var wordend = (i == md.ct - 1 or isws(md.ptr[i+1])) 66 69 67 70 var here = md.ptr + i ................................................................................ 112 115 goto skip 113 116 end 114 117 end 115 118 116 119 ::fallback::styled:push(here,1) -- :/ 117 120 i = i + 1 118 121 ::skip::end end 122 + md:free() 119 123 120 124 -- we make two passes: the first detects and transforms inline elements, 121 125 -- the second carries out block-level organization 122 126 123 127 var html: lib.str.acc html:init(styled.sz) 124 128 var s = state { 125 129 segt = segt.none;
Modified srv.t from [8808dbd7a5] to [7234d58b59].
11 11 pol_reg: bool 12 12 } 13 13 local struct srv { 14 14 sources: lib.mem.ptr(lib.store.source) 15 15 webmgr: lib.net.mg_mgr 16 16 webcon: &lib.net.mg_connection 17 17 cfg: cfgcache 18 + id: rawstring 18 19 } 19 20 20 21 terra cfgcache:free() -- :/ 21 22 self.secret:free() 22 23 self.instance:free() 23 24 end 24 25 ................................................................................ 539 540 if self.sources(i).backend ~= nil and 540 541 self.sources(i).backend.actor_auth_pw ~= nil then 541 542 var aid,uid,newhnd = self.sources(i):actor_auth_pw(ip,user,pw) 542 543 if aid ~= 0 then 543 544 if uid == 0 then 544 545 lib.dbg('new user just logged in, creating account entry') 545 546 var kbuf: uint8[lib.crypt.const.maxdersz] 546 - var newkp = lib.crypt.genkp() 547 - var privsz = lib.crypt.der(false,&newkp,&kbuf[0]) 548 - var na = lib.store.actor { 549 - id = 0; nym = nil; handle = newhnd.ptr; 550 - origin = 0; bio = nil; avatar = nil; 551 - knownsince = lib.osclock.time(nil); 552 - rights = lib.store.rights_default(); 553 - title = nil, key = [lib.mem.ptr(uint8)] { 554 - ptr = &kbuf[0], ct = privsz 555 - }; 556 - } 547 + var na = lib.store.actor.mk(&kbuf[0]) 557 548 var newuid: uint64 558 549 if self.sources(i).backend.actor_create ~= nil then 559 550 newuid = self.sources(i):actor_create(&na) 560 551 else newuid = self:actor_create(&na) end 561 552 562 553 if self.sources(i).backend.actor_auth_register_uid ~= nil then 563 554 self.sources(i):actor_auth_register_uid(aid,newuid) ................................................................................ 574 565 --9twh8y94i5c1qqr7hxu20fyd 575 566 terra cfgcache.methods.load :: {&cfgcache} -> {} 576 567 terra cfgcache:init(o: &srv) 577 568 self.overlord = o 578 569 self:load() 579 570 end 580 571 581 -srv.methods.start = terra(self: &srv, befile: rawstring) 572 +terra srv:setup(befile: rawstring) 582 573 cfg(self, befile) 583 574 var success = false 584 575 if self.sources.ct == 0 then lib.bail('no data sources specified') end 585 576 for i=0,self.sources.ct do var src = self.sources.ptr + i 586 577 lib.report('opening data source ', src.id.ptr, '(', src.backend.id, ')') 587 578 src.handle = src.backend.open(src) 588 579 if src.handle ~= nil then success = true end 589 580 end 590 581 if not success then 591 582 lib.bail('could not connect to any data sources!') 592 583 end 584 +end 593 585 586 +terra srv:start(iname: rawstring) 587 + self:conprep(lib.store.prepmode.full) 594 588 self.cfg:init(self) 595 - 596 589 var dbbind = self:conf_get('bind') 590 + if iname == nil then iname = lib.proc.getenv('parsav_instance') end 591 + if iname == nil then 592 + self.id = self.cfg.instance.ptr; 593 + -- let this leak -- it'll be needed for the lifetime of the process anyway 594 + else self.id = iname end 595 + 596 + if iname ~= nil then 597 + lib.report('parsav instance "',iname,'" starting') 598 + end 599 + 597 600 var envbind = lib.proc.getenv('parsav_bind') 598 601 var bind: rawstring 599 602 if envbind ~= nil then 600 603 bind = envbind 601 604 elseif dbbind.ptr ~= nil then 602 605 bind = dbbind.ptr 603 - else bind = '[::]:10917' end 606 + else bind = '[::1]:10917' end 604 607 605 608 lib.report('binding to ', bind) 606 609 lib.net.mg_mgr_init(&self.webmgr) 607 610 self.webcon = lib.net.mg_http_listen(&self.webmgr, bind, handle.http, self) 608 611 609 612 if dbbind.ptr ~= nil then dbbind:free() end 610 613 end 611 614 612 -srv.methods.poll = terra(self: &srv) 615 +terra srv:poll() 613 616 lib.net.mg_mgr_poll(&self.webmgr,1000) 614 617 end 615 618 616 -srv.methods.shutdown = terra(self: &srv) 619 +terra srv:shutdown() 617 620 lib.net.mg_mgr_free(&self.webmgr) 618 621 for i=0,self.sources.ct do var src = self.sources.ptr + i 619 622 lib.report('closing data source ', src.id.ptr, '(', src.backend.id, ')') 620 623 src:close() 621 624 end 622 625 self.sources:free() 623 626 end
Modified static/style.scss from [b0a8082b0c] to [1f479ddac7].
11 11 color: tone(25%); 12 12 font-size: 14pt; 13 13 margin: 0; 14 14 padding: 0; 15 15 } 16 16 a[href] { 17 17 color: tone(10%); 18 - text-decoration-color: adjust-color($color, $lightness: 10%, $alpha: -0.5); 18 + text-decoration-color: tone(10%,-0.5); 19 19 &:hover { 20 20 color: white; 21 21 text-shadow: 0 0 15px tone(20%); 22 - text-decoration-color: adjust-color($color, $lightness: 10%, $alpha: -0.1); 22 + text-decoration-color: tone(10%,-0.1); 23 23 } 24 24 } 25 25 a[href^="//"], 26 26 a[href^="http://"], 27 27 a[href^="https://"] { // external link 28 28 &:hover::after { 29 29 color: black; ................................................................................ 30 30 background-color: white; 31 31 } 32 32 &::after { 33 33 content: "↗"; 34 34 display: inline-block; 35 35 color: black; 36 36 margin-left: 4pt; 37 - background-color: adjust-color($color, $lightness: 10%); 37 + background-color: tone(10%); 38 38 padding: 0 4px; 39 39 text-shadow: none; 40 40 padding-right: 5px; 41 41 vertical-align: baseline; 42 42 font-size: 80%; 43 43 } 44 44 } ................................................................................ 45 45 46 46 %content { 47 47 width: 8in; 48 48 margin: auto; 49 49 } 50 50 51 51 %glow { 52 - box-shadow: 0 0 20px adjust-color($color, $alpha: -0.8); 52 + box-shadow: 0 0 20px tone(0%,-0.8); 53 53 } 54 54 55 55 %button { 56 56 @extend %sans; 57 57 font-size: 14pt; 58 58 padding: 0.1in 0.2in; 59 59 border: 1px solid black; 60 - color: adjust-color($color, $lightness: 25%); 60 + color: tone(25%); 61 61 text-shadow: 1px 1px black; 62 62 text-decoration: none; 63 63 text-align: center; 64 + cursor: default; 64 65 background: linear-gradient(to bottom, 65 - adjust-color($color, $lightness: -45%), 66 - adjust-color($color, $lightness: -50%) 15%, 67 - adjust-color($color, $lightness: -50%) 75%, 68 - adjust-color($color, $lightness: -55%) 66 + tone(-47%), 67 + tone(-50%) 15%, 68 + tone(-50%) 75%, 69 + tone(-53%) 69 70 ); 70 71 &:hover, &:focus { 71 72 @extend %glow; 72 73 outline: none; 73 - color: adjust-color($color, $lightness: -55%); 74 + color: tone(-55%); 74 75 text-shadow: none; 75 76 background: linear-gradient(to bottom, 76 - adjust-color($color, $lightness: -25%), 77 - adjust-color($color, $lightness: -30%) 15%, 78 - adjust-color($color, $lightness: -30%) 75%, 79 - adjust-color($color, $lightness: -35%) 77 + tone(-27%), 78 + tone(-30%) 15%, 79 + tone(-30%) 75%, 80 + tone(-35%) 80 81 ); 81 82 } 82 83 &:active { 83 84 color: black; 84 85 padding-bottom: calc(0.1in - 2px); 85 86 padding-top: calc(0.1in + 2px); 86 87 background: linear-gradient(to top, 87 - adjust-color($color, $lightness: -25%), 88 - adjust-color($color, $lightness: -30%) 15%, 89 - adjust-color($color, $lightness: -30%) 75%, 90 - adjust-color($color, $lightness: -35%) 88 + tone(-25%), 89 + tone(-30%) 15%, 90 + tone(-30%) 75%, 91 + tone(-35%) 91 92 ); 92 93 } 93 94 } 94 95 95 96 button { @extend %button; 96 97 &:first-of-type { 97 98 @extend %button; 98 99 color: white; 99 - box-shadow: inset 0 1px adjust-color($color, $lightness: -25%), 100 - inset 0 -1px adjust-color($color, $lightness: -50%); 100 + box-shadow: inset 0 1px tone(-25%), 101 + inset 0 -1px tone(-50%); 101 102 background: linear-gradient(to bottom, 102 - adjust-color($color, $lightness: -35%), 103 - adjust-color($color, $lightness: -40%) 15%, 104 - adjust-color($color, $lightness: -40%) 75%, 105 - adjust-color($color, $lightness: -45%) 103 + tone(-35%), 104 + tone(-40%) 15%, 105 + tone(-40%) 75%, 106 + tone(-45%) 106 107 ); 107 108 &:hover, &:focus { 108 - box-shadow: inset 0 1px adjust-color($color, $lightness: -15%), 109 - inset 0 -1px adjust-color($color, $lightness: -40%); 109 + box-shadow: inset 0 1px tone(-15%), 110 + inset 0 -1px tone(-40%); 110 111 } 111 112 &:active { 112 - box-shadow: inset 0 1px adjust-color($color, $lightness: -50%), 113 - inset 0 -1px adjust-color($color, $lightness: -25%); 113 + box-shadow: inset 0 1px tone(-50%), 114 + inset 0 -1px tone(-25%); 114 115 background: linear-gradient(to top, 115 - adjust-color($color, $lightness: -30%), 116 - adjust-color($color, $lightness: -35%) 15%, 117 - adjust-color($color, $lightness: -35%) 75%, 118 - adjust-color($color, $lightness: -40%) 116 + tone(-30%), 117 + tone(-35%) 15%, 118 + tone(-35%) 75%, 119 + tone(-40%) 119 120 ); 120 121 } 121 122 } 122 123 &:hover { font-weight: bold; } 123 124 } 124 125 125 126 $grad-ui-focus: linear-gradient(to bottom, 126 - adjust-color($color, $lightness: -50%), 127 - adjust-color($color, $lightness: -35%) 127 + tone(-50%), 128 + tone(-35%) 128 129 ); 129 130 130 131 input[type='text'], input[type='password'], textarea { 131 132 @extend %serif; 132 133 padding: 0.08in 0.1in; 133 134 border: 1px solid black; 134 - background: linear-gradient(to bottom, 135 - adjust-color($color, $lightness: -55%), 136 - adjust-color($color, $lightness: -40%) 137 - ); 135 + background: linear-gradient(to bottom, tone(-55%), tone(-40%)); 138 136 font-size: 16pt; 139 - color: adjust-color($color, $lightness: 25%); 140 - box-shadow: inset 0 0 20px -3px adjust-color($color, $lightness: -55%); 137 + color: tone(25%); 138 + box-shadow: inset 0 0 20px -3px tone(-55%); 141 139 &:focus { 142 140 color: white; 143 - border-image: linear-gradient(to bottom, 144 - adjust-color($color, $lightness: -10%), 145 - adjust-color($color, $lightness: -30%) 146 - ) 1 / 1px; 141 + border-image: linear-gradient(to bottom, tone(-10%), tone(-30%)) 1 / 1px; 147 142 background: $grad-ui-focus; 148 143 outline: none; 149 144 @extend %glow; 150 145 } 151 146 } 152 147 153 148 @mixin glass { 154 149 @supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) { 155 150 backdrop-filter: blur(40px); 156 151 -webkit-backdrop-filter: blur(40px); 157 - background-color: adjust-color($color, $lightness: -53%, $alpha: -0.7); 152 + background-color: tone(-53%, -0.7); 158 153 } 159 154 @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { 160 - background-color: adjust-color($color, $lightness: -53%, $alpha: -0.1); 155 + background-color: tone(-53%, -0.1); 161 156 } 162 157 } 163 158 164 159 h1 { margin-top: 0 } 165 160 166 161 header { 167 162 position: fixed; ................................................................................ 215 210 position: relative; 216 211 min-height: calc(100vh - 1.1in); 217 212 margin-top: 0; 218 213 margin-bottom: 0; 219 214 padding: 0 0.4in; 220 215 padding-top: 1.1in; 221 216 padding-bottom: 0.1in; 222 - background-color: adjust-color($color, $lightness: -45%, $alpha: 0.4); 217 + background-color: tone(-45%,-0.3); 223 218 border: { 224 219 left: 1px solid black; 225 220 right: 1px solid black; 226 221 } 227 222 } 228 223 229 224 div.profile { ................................................................................ 246 241 grid-column: 1 / 2; 247 242 grid-row: 1 / 3; 248 243 border: 1px solid black; 249 244 } 250 245 > .id { 251 246 grid-column: 2 / 3; 252 247 grid-row: 1 / 2; 253 - color: adjust-color($color, $lightness: 25%, $alpha: -0.4); 248 + color: tone(25%,-0.4); 254 249 > .nym { 255 250 font-weight: bold; 256 - color: adjust-color($color, $lightness: 25%); 251 + color: tone(25%); 257 252 } 258 253 > .xid { 259 - color: adjust-color($color, $lightness: 20%, $alpha: -0.1); 254 + color: tone(20%,-0.1); 260 255 font-size: 80%; 261 256 vertical-align: text-top; 262 257 } 263 258 } 264 259 > .bio { 265 260 grid-column: 2 / 3; 266 261 grid-row: 2 / 3; ................................................................................ 287 282 display: block; 288 283 height: 0.3in; 289 284 width: 1px; 290 285 border-left: 1px solid rgba(0,0,0,0.6); 291 286 } 292 287 } 293 288 } 289 + 290 +.epithet { 291 + display: inline-block; 292 + background: tone(20%); 293 + color: tone(-45%); 294 + text-shadow: 0 0 3px tone(-30%, -0.4); 295 + border-radius: 3px; 296 + padding: 6px; 297 + padding-top: 2px; 298 + padding-bottom: 4px; 299 + font-size: 80%; 300 + vertical-align: top; 301 + font-weight: 300; 302 + letter-spacing: 0.5px; 303 + margin: 0 5pt; 304 + // transform: scale(80%) translateX(-10pt); // cheating! 305 +} 294 306 295 307 %box { 296 308 margin: auto; 297 - border: 1px solid adjust-color($color, $lightness: -55%); 309 + border: 1px solid tone(-55%); 298 310 border-bottom: 3px solid black; 299 311 box-shadow: 0 0 1px black; 300 312 border-image: linear-gradient(to bottom, 301 - adjust-color($color, $lightness: -40%), 302 - adjust-color($color, $lightness: -52%) 10%, 303 - adjust-color($color, $lightness: -55%) 90%, 304 - adjust-color($color, $lightness: -60%) 313 + tone(-40%), 314 + tone(-52%) 10%, 315 + tone(-55%) 90%, 316 + tone(-60%) 305 317 ) 1 / 1px; 306 318 background: linear-gradient(to bottom, 307 - adjust-color($color, $lightness: -58%), 308 - adjust-color($color, $lightness: -55%) 10%, 309 - adjust-color($color, $lightness: -50%) 80%, 310 - adjust-color($color, $lightness: -45%) 319 + tone(-58%), 320 + tone(-55%) 10%, 321 + tone(-50%) 80%, 322 + tone(-45%) 311 323 ); 312 324 // outline: 1px solid black; 313 325 } 314 326 315 327 body.error .message { 316 328 @extend %box; 317 329 width: 4in; ................................................................................ 327 339 > .msg { 328 340 text-align: center; 329 341 padding: 0.3in; 330 342 } 331 343 > .msg:first-child { padding-top: 0; } 332 344 > .user { 333 345 width: min-content; margin: auto; 334 - background: adjust-color($color, $lightness: -20%, $alpha: -0.3); 346 + background: tone(-20%,-0.3); 335 347 border: 1px solid black; 336 - color: adjust-color($color, $lightness: -50%); 348 + color: tone(-50%); 337 349 padding: 0.1in; 338 350 > img { width: 1in; height: 1in; border: 1px solid black; } 339 351 > .name { @extend %serif; text-align: center; font-size: 130%; font-weight: bold; margin-top: 0.08in; } 340 352 } 341 353 >form { 342 354 display: grid; 343 355 grid-template-columns: 1fr 1fr; ................................................................................ 355 367 @extend %box; 356 368 display: grid; 357 369 grid-template-columns: 1.1in 2fr min-content 1fr; 358 370 grid-template-rows: 1fr min-content; 359 371 grid-gap: 2px; 360 372 padding: 0.1in; 361 373 > img { grid-column: 1/2; grid-row: 1/3; width: 1in; height: 1in;} 362 - > textarea { grid-column: 2/5; grid-row: 1/2; height: 3in;} 374 + > textarea { 375 + grid-column: 2/5; grid-row: 1/2; height: 3in; 376 + resize: vertical; 377 + margin-bottom: 0.08in; 378 + } 363 379 > input[name="acl"] { grid-column: 2/3; grid-row: 2/3; } 364 380 > button { grid-column: 4/5; grid-row: 2/3; } 365 381 a.help[href] { margin-right: 0.05in } 366 382 } 367 383 368 384 a.help[href] { 369 385 display: block; ................................................................................ 457 473 background: linear-gradient(to right, tone(-55%), transparent); 458 474 } 459 475 >.content { 460 476 grid-column: 2/4; grid-row: 1/2; 461 477 padding: 0.2in; 462 478 @extend %serif; 463 479 font-size: 110%; 480 + text-align: justify; 464 481 } 465 482 > a[href].permalink { 466 483 display: block; 467 484 grid-column: 3/4; grid-row: 2/3; 468 485 font-size: 80%; 469 486 text-align: right; 470 487 padding: 0.1in; ................................................................................ 476 493 477 494 a[href].rawlink { 478 495 @extend %teletype; 479 496 } 480 497 481 498 body.doc main { 482 499 @extend %serif; 500 + text-align: justify; 483 501 li { margin-top: 0.05in; } 484 502 li:first-child { margin-top: 0; } 485 503 h1, h2, h3, h4, h5, h6 { 486 504 background: linear-gradient(to right, tone(-50%), transparent); 487 505 margin-left: -0.4in; 488 506 padding-left: 0.2in; 489 507 text-shadow: 0 2px 0 black; 490 508 } 491 509 } 510 + 511 +body.conf main { 512 + display: grid; 513 + grid-template-columns: 2in 1fr; 514 + grid-template-rows: max-content 1fr; 515 + > .menu { 516 + margin-left: -0.25in; 517 + grid-column: 1/2; grid-row: 1/2; 518 + background: linear-gradient(to bottom, tone(-45%),tone(-55%)); 519 + border: 1px solid black; 520 + padding: 0.1in; 521 + > a[href] { 522 + @extend %button; 523 + display: block; 524 + text-align: left; 525 + } 526 + > a[href] + a[href] { 527 + border-top: none; 528 + } 529 + hr { 530 + border: none; 531 + } 532 + } 533 + > .panel { 534 + grid-column: 2/3; grid-row: 1/3; 535 + padding-left: 0.15in; 536 + > h1 { 537 + padding-bottom: 0.1in; 538 + margin-bottom: 0.1in; 539 + margin-left: -0.15in; 540 + padding-left: 0.15in; 541 + padding-top: 0.12in; 542 + background: linear-gradient(to right, tone(-50%), tone(-50%,-0.7)); 543 + border: 1px solid tone(-55%); 544 + border-left: none; 545 + text-shadow: 1px 1px 0 black; 546 + } 547 + } 548 +}
Modified store.t from [4959208545] to [71684bc451].
4 4 scope = lib.enum { 5 5 'public', 'private', 'local'; 6 6 'personal', 'direct', 'circle'; 7 7 }; 8 8 notiftype = lib.enum { 9 9 'mention', 'like', 'rt', 'react' 10 10 }; 11 + 11 12 relation = lib.enum { 12 13 'follow', 'mute', 'block' 13 14 }; 14 15 credset = lib.set { 15 16 'pw', 'otp', 'challenge', 'trust' 16 17 }; 17 18 privset = lib.set { ................................................................................ 20 21 powerset = lib.set { 21 22 -- user powers -- default on 22 23 'login', 'visible', 'post', 'shout', 23 24 'propagate', 'upload', 'acct', 'edit'; 24 25 25 26 -- admin powers -- default off 26 27 'purge', 'config', 'censor', 'suspend', 27 - 'cred', 'elevate', 'demote', 'rebrand' -- modify site's brand identity 28 + 'cred', 'elevate', 'demote', 'rebrand', -- modify site's brand identity 29 + 'herald' -- grant serverwide epithets 30 + }; 31 + prepmode = lib.enum { 32 + 'full','conf','admin' 28 33 } 29 34 } 30 35 31 36 terra m.powerset:affect_users() 32 37 return self.purge() or self.censor() or self.suspend() or 33 - self.elevate() or self.demote() or self.rebrand() or 34 - self.cred() 38 + self.elevate() or self.demote() or self.cred() 35 39 end 36 40 37 41 local str = rawstring 38 42 local pstr = lib.mem.ptr(int8) 39 43 40 44 struct m.source 41 45 ................................................................................ 62 66 63 67 struct m.actor { 64 68 id: uint64 65 69 nym: str 66 70 handle: str 67 71 origin: uint64 68 72 bio: str 69 - title: str 73 + epithet: str 70 74 avatar: str 71 75 knownsince: m.timepoint 72 76 rights: m.rights 73 77 key: lib.mem.ptr(uint8) 74 78 75 79 -- ephemera 76 80 xid: str 77 81 source: &m.source 78 82 } 83 + 84 +terra m.actor.methods.mk(kbuf: &uint8) 85 + var newkp = lib.crypt.genkp() 86 + var privsz = lib.crypt.der(false,&newkp,kbuf) 87 + return m.actor { 88 + id = 0; nym = nil; handle = nil; 89 + origin = 0; bio = nil; avatar = nil; 90 + knownsince = lib.osclock.time(nil); 91 + rights = m.rights_default(); 92 + epithet = nil, key = [lib.mem.ptr(uint8)] { 93 + ptr = &kbuf[0], ct = privsz 94 + }; 95 + } 96 +end 79 97 80 98 struct m.actor_stats { 81 99 posts: intptr 82 100 follows: intptr 83 101 followers: intptr 84 102 mutuals: intptr 85 103 } ................................................................................ 178 196 blacklist: bool 179 197 } 180 198 181 199 -- backends only handle content on the local server 182 200 struct m.backend { id: rawstring 183 201 open: &m.source -> &opaque 184 202 close: &m.source -> {} 203 + dbsetup: &m.source -> bool -- creates the schema needed to call conprep (called only once per database e.g. with `parsav db init`) 204 + conprep: {&m.source, m.prepmode.t} -> {} -- prepares queries and similar tasks that require the schema to already be in place 205 + obliterate_everything: &m.source -> bool -- wipes everything parsav-related out of the database 185 206 186 207 conf_get: {&m.source, rawstring} -> lib.mem.ptr(int8) 187 208 conf_set: {&m.source, rawstring, rawstring} -> {} 188 209 conf_reset: {&m.source, rawstring} -> {} 189 210 190 211 actor_save: {&m.source, &m.actor} -> bool 191 212 actor_create: {&m.source, &m.actor} -> uint64 ................................................................................ 231 252 -- notifies the backend module of the UID that has been assigned for 232 253 -- an authentication ID 233 254 -- aid: uint64 234 255 -- uid: uint64 235 256 236 257 actor_conf_str: cnf(rawstring, lib.mem.ptr(int8)) 237 258 actor_conf_int: cnf(intptr, lib.stat(intptr)) 259 + 260 + auth_create_pw: {&m.source, uint64, bool, lib.mem.ptr(int8)} -> {} 261 + -- uid: uint64 262 + -- reset: bool (delete other passwords?) 263 + -- pw: pstring 238 264 239 265 post_save: {&m.source, &m.post} -> {} 240 266 post_create: {&m.source, &m.post} -> uint64 241 267 post_enum_author_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post)) 242 268 convo_fetch_xid: {&m.source,rawstring} -> lib.mem.ptr(m.post) 243 269 convo_fetch_uid: {&m.source,uint64} -> lib.mem.ptr(m.post) 244 270
Modified str.t from [5af1afba76] to [f2457b558f].
6 6 7 7 local m = { 8 8 sz = terralib.externfunction('strlen', rawstring -> intptr); 9 9 cmp = terralib.externfunction('strcmp', {rawstring, rawstring} -> int); 10 10 ncmp = terralib.externfunction('strncmp', {rawstring, rawstring, intptr} -> int); 11 11 cpy = terralib.externfunction('stpcpy',{rawstring, rawstring} -> rawstring); 12 12 ncpy = terralib.externfunction('stpncpy',{rawstring, rawstring, intptr} -> rawstring); 13 + cat = terralib.externfunction('strcat',{rawstring, rawstring} -> rawstring); 14 + ncat = terralib.externfunction('strncat',{rawstring, rawstring, intptr} -> rawstring); 13 15 dup = terralib.externfunction('strdup',rawstring -> rawstring); 14 16 ndup = terralib.externfunction('strndup',{rawstring, intptr} -> rawstring); 15 17 fmt = terralib.externfunction('asprintf', 16 18 terralib.types.funcpointer({&rawstring,rawstring},{int},true)); 17 19 bfmt = terralib.externfunction('sprintf', 18 20 terralib.types.funcpointer({rawstring,rawstring},{int},true)); 19 21 span = terralib.externfunction('strspn',{rawstring, rawstring} -> rawstring);
Modified tpl.t from [ba911a8ebc] to [682e534236].
1 1 -- vim: ft=terra 2 2 -- string template generator: 3 3 -- returns a function that fills out a template 4 4 -- with the strings given 5 5 6 6 local util = lib.util 7 +local pstr = lib.mem.ptr(int8) 7 8 local m = {} 8 9 function m.mk(tplspec) 9 10 local str 10 11 if type(tplspec) == 'string' 11 12 then str = tplspec tplspec = {} 12 13 else str = tplspec.body 13 14 end ................................................................................ 33 34 str = str:gsub('%s+[\n$]','') 34 35 str = str:gsub('\n','') 35 36 str = str:gsub('</a><a ','</a> <a ') -- keep nav links from getting smooshed 36 37 str = str:gsub(tplchar .. '%?([-%w]+)', function(file) 37 38 if not docs[file] then docs[file] = data.doc[file] end 38 39 return string.format('<a href="#help-%s" class="help">?</a>', file) 39 40 end) 40 - for start, key, stop in string.gmatch(str,'()'..tplchar..'(%w+)()') do 41 + for start, mode, key, stop in string.gmatch(str,'()'..tplchar..'([:!]?)(%w+)()') do 41 42 if string.sub(str,start-1,start-1) ~= '\\' then 42 43 segs[#segs+1] = string.sub(str,last,start-1) 43 - fields[#segs] = key 44 + fields[#segs] = { key = key, mode = (mode ~= '' and mode or nil) } 44 45 last = stop 45 46 end 46 47 end 47 48 segs[#segs+1] = string.sub(str,last) 48 49 49 50 for i, s in ipairs(segs) do 50 51 segs[i] = string.gsub(s, '\\'..tplchar, tplchar_o) ................................................................................ 63 64 local runningtally = symbol(intptr) 64 65 local tallyup = {quote 65 66 var [runningtally] = 1 + constlen 66 67 end} 67 68 local rec = terralib.types.newstruct(string.format('template<%s>',tplspec.id or '')) 68 69 local symself = symbol(&rec) 69 70 do local kfac = {} 70 - for afterseg,key in pairs(fields) do 71 - if not kfac[key] then 71 + local sanmode = {} 72 + for afterseg,fld in ipairs(fields) do 73 + if not kfac[fld.key] then 72 74 rec.entries[#rec.entries + 1] = { 73 - field = key; 75 + field = fld.key; 74 76 type = lib.mem.ptr(int8); 75 77 } 76 78 end 77 - kfac[key] = (kfac[key] or 0) + 1 79 + kfac[fld.key] = (kfac[fld.key] or 0) + 1 80 + sanmode[fld.key] = fld.mode == ':' and 6 or fld.mode == '!' and 5 or 1 78 81 end 79 82 for key, fac in pairs(kfac) do 83 + local sanfac = sanmode[key] 84 + 80 85 tallyup[#tallyup + 1] = quote 81 - [runningtally] = [runningtally] + ([symself].[key].ct)*fac 86 + [runningtally] = [runningtally] + ([symself].[key].ct)*fac*sanfac 82 87 end 83 88 end 84 89 end 85 90 86 91 local copiers = {} 87 92 local senders = {} 88 93 local appenders = {} ................................................................................ 90 95 local cpypos = symbol(&opaque) 91 96 local accumulator = symbol(&lib.str.acc) 92 97 local destcon = symbol(&lib.net.mg_connection) 93 98 for idx, seg in ipairs(segs) do 94 99 copiers[#copiers+1] = quote [cpypos] = lib.mem.cpy([cpypos], [&opaque]([seg]), [#seg]) end 95 100 senders[#senders+1] = quote lib.net.mg_send([destcon], [seg], [#seg]) end 96 101 appenders[#appenders+1] = quote [accumulator]:push([seg], [#seg]) end 97 - if fields[idx] then 98 - --local fsz = `lib.str.sz(symself.[fields[idx]]) 99 - local fval = `symself.[fields[idx]].ptr 100 - local fsz = `symself.[fields[idx]].ct 101 - copiers[#copiers+1] = quote 102 - [cpypos] = lib.mem.cpy([cpypos], [&opaque]([fval]), [fsz]) 102 + if fields[idx] and fields[idx].mode then 103 + local f = fields[idx] 104 + local fp = `symself.[f.key] 105 + copiers[#copiers+1] = quote 106 + if fp.ct > 0 then 107 + var san = lib.html.sanitize(fp, [f.mode == ':']) 108 + [cpypos] = lib.mem.cpy([cpypos], [&opaque](san.ptr), san.ct) 109 + --san:free() 110 + end 111 + end 112 + senders[#senders+1] = quote 113 + if fp.ct > 0 then 114 + var san = lib.html.sanitize(fp, [f.mode == ':']) 115 + lib.net.mg_send([destcon], san.ptr, san.ct) 116 + --san:free() 117 + end 118 + end 119 + appenders[#appenders+1] = quote 120 + if fp.ct > 0 then 121 + var san = lib.html.sanitize(fp, [f.mode == ':']) 122 + [accumulator]:ppush(san) 123 + --san:free() 124 + end 125 + end 126 + elseif fields[idx] then 127 + local f = fields[idx] 128 + local fp = `symself.[f.key] 129 + copiers[#copiers+1] = quote 130 + if fp.ct > 0 then 131 + [cpypos] = lib.mem.cpy([cpypos], [&opaque](fp.ptr), fp.ct) 132 + end 103 133 end 104 134 senders[#senders+1] = quote 105 - lib.net.mg_send([destcon], [fval], [fsz]) 135 + if fp.ct > 0 then 136 + lib.net.mg_send([destcon], fp.ptr, fp.ct) 137 + end 106 138 end 107 139 appenders[#appenders+1] = quote 108 - [accumulator]:push([fval], [fsz]) 140 + if fp.ct > 0 then [accumulator]:ppush(fp) end 109 141 end 110 142 end 111 143 end 112 144 113 145 local tid = tplspec.id or '<anonymous>' 114 146 rec.methods.tostr = terra([symself]) 115 147 lib.dbg(['compiling template ' .. tid]) 116 148 [tallyup] 117 149 var [symtxt] = lib.mem.heapa(int8, [runningtally]) 118 150 var [cpypos] = [&opaque](symtxt.ptr) 119 151 [copiers] 120 152 @[&int8](cpypos) = 0 153 + symtxt.ct = [&int8](cpypos) - symtxt.ptr 121 154 return symtxt 122 155 end 123 156 rec.methods.append = terra([symself], [accumulator]) 124 157 lib.dbg(['appending template ' .. tid]) 125 158 [tallyup] 126 159 accumulator:cue([runningtally]) 127 160 [appenders]
Modified view/compose.tpl from [bb642e2999] to [5ccb8d92d6].
1 1 <form class="compose" method="post"> 2 2 <img src="/avi/@handle"> 3 - <textarea autofocus name="post" placeholder="it was a dark and stormy night…">@content</textarea> 4 - <input required type="text" name="acl" class="acl" value="@acl"> @?acl 3 + <textarea autofocus name="post" placeholder="it was a dark and stormy night…">@!content</textarea> 4 + <input required autocomplete="on" type="text" name="acl" class="acl" value="@acl" list="scopes" placeholder="access control"> @?acl 5 5 <button type="submit">commit</button> 6 6 </form> 7 + 8 +<datalist id="scopes"> 9 + <option>all</option> 10 + <option>mentioned</option> 11 + <option>local</option> 12 + <option>mutual</option> 13 + <option>followers</option> 14 + <option>followed</option> 15 + <option>groupies</option> 16 + <option>staff</option> 17 + <option>admin</option> 18 + @circles 19 +</datalist>
Added view/conf-profile.tpl version [746111dd26].
1 +<form method="post"> 2 + <label>handle <div class="txtbox">@!handle</div></label> 3 + <label>display name <input type="text" name="nym" value="@:nym"></label> 4 + <label>bio <textarea name="bio">@!bio</textarea></label> 5 + <input type="submit" value="commit"> 6 +</form>
Added view/conf-sec.tpl version [7ba95a81c5].
1 +<form method="post"> 2 + <p>if you are concerned that your account may have been compromised, you can terminate all other login sessions by invalidating their session cookies. note that this will not have any effect on API tokens; these must be revoked separately!</p> 3 + <label> 4 + sessions valid from 5 + <div class="txtbox">@lastreset</div> 6 + </label> 7 + <button type="submit" name="act" value="invalidate"> 8 + invalidate other sessions 9 + </button> 10 +</form>
Added view/conf.tpl version [bd130ad9d3].
1 +<div class="menu"> 2 + <a href="/conf/profile">profile</a> 3 + <a href="/conf/avi">avatar</a> 4 + <a href="/conf/sec">security</a> 5 + <a href="/conf/rel">relationships</a> 6 + <a href="/conf/qnt">quarantine</a> 7 + <a href="/conf/acl">ACL shortcuts</a> 8 + <a href="/conf/rooms">chatrooms</a> 9 + <a href="/conf/circles">circles</a> 10 + @menu 11 +</div> 12 + 13 +<div class="panel"> 14 + @panel 15 +</div>
Modified view/load.lua from [1503c625ad] to [f63ef60595].
4 4 -- create templates from when we return to terra 5 5 local path = ... 6 6 local sources = { 7 7 'docskel'; 8 8 'tweet'; 9 9 'profile'; 10 10 'compose'; 11 + 11 12 'login-username'; 12 13 'login-challenge'; 14 + 15 + 'conf'; 16 + 'conf-profile'; 13 17 } 14 18 15 19 local ingest = function(filename) 16 20 local hnd = io.open(path..'/'..filename) 17 21 local txt = hnd:read('*a') 18 22 io.close(hnd) 19 23 txt = txt:gsub('([^\\])!%b[]', '%1')
Modified view/login-challenge.tpl from [c8511de2b7] to [84fccbb367].
1 1 <div class="login"> 2 2 <div class="user"> 3 3 <img src="/avi/@handle"> 4 - <div class="name">@name</div> 4 + <div class="name">@!name</div> 5 5 </div> 6 6 <div class="msg">@challenge</div> 7 7 <form action="/login" method="post"> 8 8 <label for="response">@label</label> 9 - <input type="hidden" name="user" value="@handle"> 10 - <input type="password" name="response" id="response" autofocus required> 9 + <input type="hidden" name="user" value="@:handle"> 10 + <input type="password" autocomplete="@auto" name="response" id="response" autofocus required> 11 11 <button type="submit" name="authmethod" value="@method">authenticate</button> 12 12 <a href="/login">cancel</a> 13 13 </form> 14 14 </div>
Modified view/login-username.tpl from [4dc628d5ef] to [8c165f8ae9].
1 1 <div class="login"> 2 2 <div class="msg">@loginmsg</div> 3 3 <form action="/login" method="post"> 4 4 <label for="user">local handle</label> 5 - <input type="text" name="user" id="user" autofocus required> 5 + <input type="text" name="user" id="user" autocomplete="username" autofocus required> 6 6 <button type="submit">log on</button> 7 7 </form> 8 8 </div>
Modified view/profile.tpl from [6c61b2a3c1] to [d21ccabbe8].
1 1 <div class="profile"> 2 2 <div class="banner"> 3 - <img class="avatar" src="@avatar"> 4 - <div class="id"><span class="nym">@nym</span> [<span class="xid">@xid</span>]</div> 3 + <img class="avatar" src="@:avatar"> 4 + <div class="id">@nym</div> 5 5 <div class="bio"> 6 6 @bio 7 7 </div> 8 8 </div> 9 9 <table class="stats"> 10 10 <tr><th>posts</th> <td>@nposts</td></tr> 11 11 <tr><th>following</th> <td>@nfollows</td></tr> 12 12 <tr><th>followers</th> <td>@nfollowers</td></tr> 13 13 <tr><th>mutuals</th> <td>@nmutuals</td></tr> 14 14 <tr><th>@timephrase</th> <td>@tweetday</td></tr> 15 15 </table> 16 16 <div class="menu"> 17 - <a href="/@xid">posts</a> 18 - <a href="/@xid/media">media</a> 19 - <a href="/@xid/social">associates</a> 17 + <a href="/@:xid">posts</a> 18 + <a href="/@:xid/media">media</a> 19 + <a href="/@:xid/social">associates</a> 20 20 <hr> 21 21 @auxbtn 22 22 </div> 23 23 </div>
Modified view/tweet.tpl from [43ed0b36e9] to [806c88c01c].
1 1 <div class="post"> 2 - <div class="avatar"><img src="@avatar"></div> 3 - <a class="username" href="/@acctlink"> 4 - <span class="nym">@nym</span> [<span class="handle">@xid</span>] 5 - </a> 2 + <div class="avatar"><img src="@:avatar"></div> 3 + <a class="username" href="/@:acctlink">@nym</a> 6 4 <div class="content"> 7 - <div class="subject">@subject</div> 5 + <div class="subject">@!subject</div> 8 6 <div class="text">@text</div> 9 7 </div> 10 8 <a class="permalink" href="@permalink">@when</a> 11 9 </div>