| Comment: | improve sql, js, docs, other tweaks |
|---|---|
| Downloads: | Tarball | ZIP archive | SQL archive |
| Timelines: | family | ancestors | descendants | both | trunk |
| Files: | files | file ages | folders |
| SHA3-256: |
7bd78f9b1c391894b305c8b3412d9c97 |
| User & Date: | lexi on 2021-01-05 22:10:42 |
| Other Links: | manifest | tags |
|
2021-01-05
| ||
| 22:13 | clean up debug code check-in: 7239d174b1 user: lexi tags: trunk | |
| 22:10 | improve sql, js, docs, other tweaks check-in: 7bd78f9b1c user: lexi tags: trunk | |
|
2021-01-04
| ||
| 20:33 | more jabbascript improvements check-in: b6c2a79945 user: lexi tags: trunk | |
Modified backend/pgsql.t from [2e62d4947d] to [7305c1c258].
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 - select a.id, a.nym, a.handle, a.origin, a.bio, 30 - a.avataruri, a.rank, a.quota, a.key, a.epithet, 31 - extract(epoch from a.knownsince)::bigint, 32 - coalesce(a.handle || '@' || s.domain, 33 - '@' || a.handle) as xid, 34 - a.invites 35 - 36 - from parsav_actors as a 37 - left join parsav_servers as s 38 - on a.origin = s.id 39 - where a.id = $1::bigint 29 + select (pg_temp.parsavpg_translate_actor(a)).* 30 + from parsav_actors as a 31 + where a.id = $1::bigint 40 32 ]]; 41 33 }; 42 34 43 35 actor_fetch_xid = { 44 36 params = {pstring}, sql = [[ 45 - select a.id, a.nym, a.handle, a.origin, a.bio, 46 - a.avataruri, a.rank, a.quota, a.key, a.epithet, 47 - extract(epoch from a.knownsince)::bigint, 48 - coalesce(a.handle || '@' || s.domain, 49 - '@' || a.handle) as xid, 50 - a.invites, 51 - 52 - coalesce(s.domain, 53 - (select value from parsav_config 54 - where key='domain' limit 1)) as domain 55 - 56 - from parsav_actors as a 57 - left join parsav_servers as s 58 - on a.origin = s.id 59 - 60 - where $1::text = (a.handle || '@' || domain) or 61 - $1::text = ('@' || a.handle || '@' || domain) or 37 + with txd as ( 38 + select (pg_temp.parsavpg_translate_actor(a)).* from parsav_actors as a 39 + ) 40 + select * from txd as a where $1::text = xid or 62 41 (a.origin is null and 63 42 $1::text = a.handle or 64 - $1::text = ('@' || a.handle)) 43 + $1::text = (a.handle ||'@'|| 44 + (select value from parsav_config where key='domain'))) 65 45 ]]; 66 46 }; 67 47 68 48 actor_purge_uid = { 69 49 params = {uint64}, cmd = true, sql = [[ 70 50 with d as ( -- cheating 71 51 delete from parsav_sanctions where victim = $1::bigint ................................................................................ 102 82 }; 103 83 104 84 actor_create = { 105 85 params = { 106 86 rawstring, rawstring, uint64, lib.store.timepoint, 107 87 rawstring, rawstring, lib.mem.ptr(uint8), 108 88 rawstring, uint16, uint32, uint32 109 - }; 110 - sql = [[ 89 + }, sql = [[ 111 90 insert into parsav_actors ( 112 91 nym,handle, 113 92 origin,knownsince, 114 93 bio,avataruri,key, 115 94 epithet,rank,quota, 116 - invites 95 + invites,authtime 117 96 ) values ($1::text, $2::text, 118 97 case when $3::bigint = 0 then null 119 98 else $3::bigint end, 120 - to_timestamp($4::bigint), 99 + $4::bigint, 121 100 $5::bigint, $6::bigint, $7::bytea, 122 101 $8::text, $9::smallint, $10::integer, 123 - $11::integer 102 + $11::integer,$4::bigint 124 103 ) returning id 125 104 ]]; 126 105 }; 127 106 128 107 actor_auth_pw = { 129 108 params = {pstring,rawstring,pstring,lib.store.inet}, sql = [[ 130 109 select a.aid, a.uid, a.name from parsav_auth as a ................................................................................ 138 117 ]]; 139 118 }; 140 119 141 120 actor_enum_local = { 142 121 params = {}, sql = [[ 143 122 select id, nym, handle, origin, bio, 144 123 null::text, rank, quota, key, epithet, 145 - extract(epoch from knownsince)::bigint, 124 + knownsince::bigint, 146 125 '@' || handle, 147 126 invites 148 127 from parsav_actors where origin is null 149 128 order by nullif(rank,0) nulls last, handle 150 129 ]]; 151 130 }; 152 131 153 132 actor_enum = { 154 133 params = {}, sql = [[ 155 - select a.id, a.nym, a.handle, a.origin, a.bio, 156 - a.avataruri, a.rank, a.quota, a.key, a.epithet, 157 - extract(epoch from a.knownsince)::bigint, 158 - coalesce(a.handle || '@' || s.domain, 159 - '@' || a.handle) as xid, 160 - invites 161 - from parsav_actors a 162 - left join parsav_servers s on s.id = a.origin 134 + select (pg_temp.parsavpg_translate_actor(a)).* 135 + from parsav_actors as a 136 + 163 137 order by nullif(a.rank,0) nulls last, a.handle, a.origin 164 138 ]]; 165 139 }; 166 140 167 141 actor_stats = { 168 - params = {uint64}, sql = ([[ 142 + params = {uint64}, sql = [[ 169 143 with tweets as ( 170 144 select from parsav_posts where author = $1::bigint 171 145 ), 172 146 follows as ( 173 147 select relatee as user from parsav_rels 174 - where relator = $1::bigint and kind = <follow> 148 + where relator = $1::bigint and kind = <rel:follow> 175 149 ), 176 150 followers as ( 177 151 select relator as user from parsav_rels 178 - where relatee = $1::bigint and kind = <follow> 152 + where relatee = $1::bigint and kind = <rel:follow> 179 153 ), 180 154 mutuals as ( 181 155 select * from follows intersect select * from followers 182 156 ) 183 157 184 158 values ( 185 159 (select count(tweets.*)::bigint from tweets), 186 160 (select count(follows.*)::bigint from follows), 187 161 (select count(followers.*)::bigint from followers), 188 162 (select count(mutuals.*)::bigint from mutuals) 189 163 ) 190 - ]]):gsub('<(%w+)>',function(r) return tostring(lib.store.relation.idvmap[r]) end) 164 + ]] 191 165 }; 192 166 193 167 actor_auth_how = { 194 168 params = {rawstring, lib.store.inet}, sql = [[ 195 169 with mts as (select a.kind from parsav_auth as a 196 170 left join parsav_actors as u on u.id = a.uid 197 171 where (a.uid is null or u.handle = $1::text or ( ................................................................................ 206 180 (select count(*) from mts where kind like 'challenge-%') > 0, 207 181 (select count(*) from mts where kind = 'trust') > 0 208 182 ]]; -- cheat 209 183 }; 210 184 211 185 actor_session_fetch = { 212 186 params = {uint64, lib.store.inet, int64}, sql = [[ 213 - select a.id, a.nym, a.handle, a.origin, a.bio, 214 - a.avataruri, a.rank, a.quota, a.key, a.epithet, 215 - extract(epoch from a.knownsince)::bigint, 216 - coalesce(a.handle || '@' || s.domain, 217 - '@' || a.handle) as xid, 187 + select (pg_temp.parsavpg_translate_actor(a)).*, 218 188 219 189 au.restrict, 220 190 array['post' ] <@ au.restrict, 221 191 array['edit' ] <@ au.restrict, 222 192 array['account' ] <@ au.restrict, 223 193 array['upload' ] <@ au.restrict, 224 194 array['moderate'] <@ au.restrict, 225 195 array['admin' ] <@ au.restrict 226 196 227 197 from parsav_auth au 228 198 left join parsav_actors a on au.uid = a.id 229 - left join parsav_servers s on a.origin = s.id 230 199 231 200 where au.aid = $1::bigint and au.blacklist = false and 232 201 (au.netmask is null or au.netmask >> $2::inet) and 233 202 ($3::bigint = 0 or --slightly abusing the epoch time fmt here, but 234 - ((a.authtime is null or a.authtime <= to_timestamp($3::bigint)) and 235 - (au.valperiod is null or au.valperiod <= to_timestamp($3::bigint)))) 203 + ((a.authtime is null or a.authtime <= $3::bigint) and 204 + (au.valperiod is null or au.valperiod <= $3::bigint))) 236 205 ]]; 237 206 }; 238 207 239 208 actor_powers_fetch = { 240 209 params = {uint64}, sql = [[ 241 210 select key, allow from parsav_rights where actor = $1::bigint 242 211 ]] ................................................................................ 253 222 actor_power_delete = { 254 223 params = {uint64,lib.mem.ptr(int8)}, cmd = true, sql = [[ 255 224 delete from parsav_rights where 256 225 actor = $1::bigint and 257 226 key = $2::text 258 227 ]] 259 228 }; 229 + 230 + actor_rel_create = { 231 + params = {uint16,uint64, uint64}, cmd = true, sql = [[ 232 + insert into parsav_rels (kind,relator,relatee) 233 + values($1::smallint, $2::bigint, $3::bigint) 234 + on conflict do nothing 235 + ]]; 236 + }; 237 + 238 + actor_rel_destroy = { 239 + params = {uint16,uint64, uint64}, cmd = true, sql = [[ 240 + delete from parsav_rels where 241 + kind = $1::smallint and 242 + relator = $2::bigint and 243 + relatee = $3::bigint 244 + ]]; 245 + }; 246 + 247 + actor_rel_enum = { 248 + params = {uint64, uint64}, sql = [[ 249 + select kind from parsav_rels where 250 + relator = $1::bigint and 251 + relatee = $2::bigint 252 + ]]; 253 + }; 254 + 255 + actor_notice_enum = { 256 + params = {uint64}, sql = [[ 257 + select (notice).* from pg_temp.parsavpg_notices 258 + where rcpt = $1::bigint 259 + ]]; 260 + }; 260 261 261 262 auth_sigtime_user_fetch = { 262 263 params = {uint64}, sql = [[ 263 - select extract(epoch from authtime)::bigint 264 + select authtime::bigint 264 265 from parsav_actors where id = $1::bigint 265 266 ]]; 266 267 }; 267 268 268 269 auth_sigtime_user_alter = { 269 270 params = {uint64,int64}, cmd = true, sql = [[ 270 271 update parsav_actors set 271 - authtime = to_timestamp($2::bigint) 272 + authtime = $2::bigint 272 273 where id = $1::bigint 273 274 ]]; 274 275 }; 275 276 276 277 auth_create_pw = { 277 - params = {uint64, binblob, pstring}, cmd = true, sql = [[ 278 - insert into parsav_auth (uid, name, kind, cred, comment) values ( 278 + params = {uint64, binblob, int64, pstring}, cmd = true, sql = [[ 279 + insert into parsav_auth (uid, name, kind, cred, valperiod, comment) values ( 279 280 $1::bigint, 280 281 (select handle from parsav_actors where id = $1::bigint), 281 282 'pw-sha256', $2::bytea, 282 - $3::text 283 + $3::bigint, $4::text 283 284 ) 284 285 ]] 285 286 }; 286 287 287 288 auth_purge_type = { 288 289 params = {rawstring, uint64, rawstring}, cmd = true, sql = [[ 289 290 delete from parsav_auth where ................................................................................ 310 311 rawstring, rawstring, rawstring; 311 312 }, cmd = true, sql = [[ 312 313 update parsav_posts set 313 314 subject = $4::text, 314 315 acl = $5::text, 315 316 body = $6::text, 316 317 chgcount = $2::integer, 317 - edited = to_timestamp($3::bigint) 318 + edited = $3::bigint 318 319 where id = $1::bigint 319 320 ]] 320 321 }; 321 322 322 323 post_create = { 323 324 params = { 324 325 uint64, rawstring, rawstring, rawstring, ................................................................................ 327 328 insert into parsav_posts ( 328 329 author, subject, acl, body, 329 330 parent, posted, discovered, 330 331 circles, mentions, convoheaduri 331 332 ) values ( 332 333 $1::bigint, case when $2::text = '' then null else $2::text end, 333 334 $3::text, $4::text, 334 - $5::bigint, to_timestamp($6::bigint), now(), 335 + $5::bigint, $6::bigint, $6::bigint, 335 336 array[]::bigint[], array[]::bigint[], $7::text 336 337 ) returning id 337 338 ]]; -- TODO array handling 338 339 }; 339 340 340 341 post_destroy_prepare = { 341 342 params = {uint64}, cmd = true, sql = [[ ................................................................................ 349 350 params = {uint64}, cmd = true, sql = [[ 350 351 delete from parsav_posts where id = $1::bigint 351 352 ]] 352 353 }; 353 354 354 355 post_fetch = { 355 356 params = {uint64}, sql = [[ 356 - with counts as ( 357 - select a.kind, p.id as subject, count(*) as ct from parsav_acts as a 358 - inner join parsav_posts as p on p.id = a.subject 359 - group by a.kind, p.id 360 - ) 361 - 362 - select a.origin is null, 363 - p.id, p.author, p.subject, p.acl, p.body, 364 - extract(epoch from p.posted )::bigint, 365 - extract(epoch from p.discovered)::bigint, 366 - extract(epoch from p.edited )::bigint, 367 - p.parent, p.convoheaduri, p.chgcount, 368 - coalesce(c.value, -1)::smallint, 0::bigint, 0::bigint, 369 - coalesce((select ct from counts where kind = 'like' and counts.subject = p.id),0)::integer, 370 - coalesce((select ct from counts where kind = 'rt' and counts.subject = p.id),0)::integer 371 - 372 - from parsav_posts as p 373 - inner join parsav_actors as a on p.author = a.id 374 - left join parsav_actor_conf_ints as c on c.uid = a.id and c.key = 'ui-accent' 375 - where p.id = $1::bigint 376 - ]]; 357 + select (p.post).* 358 + from pg_temp.parsavpg_known_content as p 359 + where (p.post).id = $1::bigint and (p.post).rtdby = 0 360 + ]] 377 361 }; 378 362 379 363 post_enum_parent = { 380 364 params = {uint64}, sql = [[ 381 - with counts as ( 382 - select a.kind, p.id as subject, count(*) as ct from parsav_acts as a 383 - inner join parsav_posts as p on p.id = a.subject 384 - group by a.kind, p.id 385 - ) 386 - 387 - select a.origin is null, 388 - p.id, p.author, p.subject, p.acl, p.body, 389 - extract(epoch from p.posted )::bigint, 390 - extract(epoch from p.discovered)::bigint, 391 - extract(epoch from p.edited )::bigint, 392 - p.parent, p.convoheaduri, p.chgcount, 393 - coalesce(c.value, -1)::smallint, 0::bigint, 0::bigint, 394 - coalesce((select ct from counts where kind = 'like' and counts.subject = p.id),0)::integer, 395 - coalesce((select ct from counts where kind = 'rt' and counts.subject = p.id),0)::integer 396 - 397 - from parsav_posts as p 398 - inner join parsav_actors as a on a.id = p.author 399 - left join parsav_actor_conf_ints as c on c.uid = a.id and c.key = 'ui-accent' 400 - where p.parent = $1::bigint 401 - order by p.posted, p.discovered asc 402 - ]] 365 + select (p.post).* 366 + from pg_temp.parsavpg_known_content as p 367 + where (p.post).parent = $1::bigint and (p.post).rtdby = 0 368 + order by (p.post).posted, (p.post).discovered asc 369 + ]]; 403 370 }; 404 371 405 372 thread_latest_arrival_calc = { 406 373 params = {uint64}, sql = [[ 407 374 with recursive posts(id) as ( 408 375 select id from parsav_posts where parent = $1::bigint 409 376 union ................................................................................ 414 381 maxes as ( 415 382 select unnest(array[max(p.posted), max(p.discovered), max(p.edited)]) as m 416 383 from posts 417 384 inner join parsav_posts as p 418 385 on p.id = posts.id 419 386 ) 420 387 421 - select extract(epoch from max(m))::bigint from maxes 388 + select max(m)::bigint from maxes 422 389 ]]; 423 390 }; 424 391 425 392 post_react_simple = { 426 - params = {uint64, uint64, pstring}, sql = [[ 427 - insert into parsav_acts (kind,actor,subject) values ( 428 - $3::text, $1::bigint, $2::bigint 393 + params = {uint64, uint64, pstring, int64}, sql = [[ 394 + insert into parsav_acts (kind,actor,subject,time) values ( 395 + $3::text, $1::bigint, $2::bigint, $4::bigint 429 396 ) returning id 430 397 ]]; 431 398 }; 432 399 433 400 post_react_cancel = { 434 401 params = {uint64, uint64, pstring}, cmd = true, sql = [[ 435 402 delete from parsav_acts where ................................................................................ 446 413 ($2::bigint = 0 or subject = $2::bigint) and 447 414 ($3::text is null or kind = $3::text ) 448 415 ]] 449 416 }; 450 417 451 418 post_enum_author_uid = { 452 419 params = {uint64,uint64,uint64,uint64, uint64}, sql = [[ 453 - with ownposts as ( 454 - select *, 0::bigint as rtid from parsav_posts as p 455 - where p.author = $5::bigint and 456 - ($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and 457 - ($2::bigint = 0 or to_timestamp($2::bigint) < p.posted) 458 - ), 420 + select (c.post).* 421 + from pg_temp.parsavpg_known_content as c 459 422 460 - retweets as ( 461 - select p.*, a.id as rtid from parsav_acts as a 462 - inner join parsav_posts as p on a.subject = p.id 463 - where a.actor = $5::bigint and 464 - a.kind = 'rt' and 465 - ($1::bigint = 0 or a.time <= to_timestamp($1::bigint)) and 466 - ($2::bigint = 0 or to_timestamp($2::bigint) < a.time) 467 - ), 423 + where c.promoter = $5::bigint and 424 + ($1::bigint = 0 or c.tltime <= $1::bigint) and 425 + ($2::bigint = 0 or $2::bigint < c.tltime) 426 + order by c.tltime desc 468 427 469 - allposts as (select *, 0::bigint as retweeter from ownposts 470 - union select *, $5::bigint as retweeter from retweets), 471 - 472 - counts as ( 473 - select a.kind, p.id as subject, count(*) as ct from parsav_acts as a 474 - inner join parsav_posts as p on p.id = a.subject 475 - group by a.kind, p.id 476 - ) 477 - 478 - select a.origin is null, 479 - p.id, p.author, p.subject, p.acl, p.body, 480 - extract(epoch from p.posted )::bigint, 481 - extract(epoch from p.discovered)::bigint, 482 - extract(epoch from p.edited )::bigint, 483 - p.parent, p.convoheaduri, p.chgcount, 484 - coalesce(c.value,-1)::smallint, 485 - p.retweeter, p.rtid, 486 - coalesce((select ct from counts where kind = 'like' and counts.subject = p.id),0)::integer, 487 - coalesce((select ct from counts where kind = 'rt' and counts.subject = p.id),0)::integer 488 - from allposts as p 489 - inner join parsav_actors as a on p.author = a.id 490 - left join parsav_actor_conf_ints as c 491 - on c.key = 'ui-accent' and 492 - c.uid = a.id 493 - order by (p.posted, p.discovered) desc 494 428 limit case when $3::bigint = 0 then null 495 429 else $3::bigint end 496 430 offset $4::bigint 497 - ]] 431 + ]]; 498 432 }; 499 433 500 434 -- maybe there's some way to unify these two, idk, im tired 501 435 502 436 timeline_instance_fetch = { 503 437 params = {uint64, uint64, uint64, uint64}, sql = [[ 504 - with posts as ( 505 - select true, 506 - p.id, p.author, p.subject, p.acl, p.body, 507 - extract(epoch from p.posted )::bigint, 508 - extract(epoch from p.discovered)::bigint, 509 - extract(epoch from p.edited )::bigint, 510 - p.parent, null::text, p.chgcount, 511 - coalesce(c.value, -1)::smallint, 0::bigint, 0::bigint 512 - 513 - from parsav_posts as p 514 - inner join parsav_actors as a on p.author = a.id 515 - left join parsav_actor_conf_ints as c on c.uid = a.id and c.key = 'ui-accent' 516 - where 517 - ($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and 518 - ($2::bigint = 0 or to_timestamp($2::bigint) < p.posted) and 519 - (a.origin is null) 520 - order by (p.posted, p.discovered) desc 521 - limit case when $3::bigint = 0 then null 522 - else $3::bigint end 523 - offset $4::bigint 524 - ), counts as ( 525 - select a.kind, p.id as subject, count(*) as ct from parsav_acts as a 526 - inner join parsav_posts as p on p.id = a.subject 527 - group by a.kind, p.id 438 + select (c.post).* 439 + from pg_temp.parsavpg_known_content as c 440 + 441 + where (c.post).localpost = true and 442 + ($1::bigint = 0 or c.tltime <= $1::bigint) and 443 + ($2::bigint = 0 or $2::bigint < c.tltime) 444 + order by c.tltime desc 445 + 446 + limit case when $3::bigint = 0 then null 447 + else $3::bigint end 448 + offset $4::bigint 449 + ]]; 450 + }; 451 + 452 + timeline_actor_fetch = { 453 + params = {uint64, uint64, uint64, uint64, uint64}, sql = [[ 454 + with followed as ( 455 + select relatee from parsav_rels where 456 + kind = <rel:follow> and 457 + relator = $1::bigint 458 + ), avoided as ( 459 + select relatee as avoidee from parsav_rels where 460 + kind = <rel:avoid> or kind = <rel:mute> and 461 + relator = $1::bigint 462 + union select relator as avoidee from parsav_rels where 463 + kind = <rel:exclude> and 464 + relatee = $1::bigint 528 465 ) 529 466 530 - select *, 531 - coalesce((select ct from counts as c where kind = 'like' and c.subject = posts.id),0)::integer, 532 - coalesce((select ct from counts as c where kind = 'rt' and c.subject = posts.id),0)::integer 533 - from posts 534 - ]] 467 + select (c.post).* 468 + from pg_temp.parsavpg_known_content as c 469 + 470 + where ($2::bigint = 0 or c.tltime <= $2::bigint) and 471 + ($3::bigint = 0 or $3::bigint < c.tltime) and 472 + (c.promoter in (table followed) or 473 + c.promoter = $1::bigint) and 474 + not ((c.post).author in (table avoided)) 475 + order by c.tltime desc 476 + 477 + limit case when $4::bigint = 0 then null 478 + else $4::bigint end 479 + offset $5::bigint 480 + ]]; 535 481 }; 536 482 537 483 artifact_instantiate = { 538 484 params = {binblob, binblob, pstring}, sql = [[ 539 485 insert into parsav_artifacts (content,hash,mime) values ( 540 486 $1::bytea, $2::bytea, $3::text 541 487 ) on conflict do nothing returning id ................................................................................ 744 690 buf[2] = tycode 745 691 buf[3] = sz 746 692 for j=0,sz do buf[4 + j] = i.v6[j] end -- 😬 747 693 return buf 748 694 end 749 695 end; 750 696 } 697 + 698 +local sqlvars = {} 699 +for i, n in ipairs(lib.store.noticetype.members) do 700 + sqlvars['notice:' .. n] = lib.store.noticetype[n] 701 +end 702 + 703 +for i, n in ipairs(lib.store.relation.members) do 704 + sqlvars['rel:' .. n] = lib.store.relation.idvmap[n] 705 +end 751 706 752 707 local con = symbol(&lib.pq.PGconn) 753 -local prep = {} 754 708 local function sqlsquash(s) return s 755 709 :gsub('%%include (.-)%%',function(f) 756 710 return sqlsquash(lib.util.ingest('backend/schema/' .. f)) 757 711 end) -- include dependencies 758 712 :gsub('%-%-.-\n','') -- remove disruptive line comments 759 713 :gsub('%-%-.-$','') -- remove unnecessary terminal comments 714 + :gsub('<(%g-)>',function(r) return tostring(sqlvars[r]) end) 760 715 :gsub('%s+',' ') -- remove whitespace 761 716 :gsub('^%s*(.-)%s*$','%1') -- chomp 762 717 end 718 + 719 +-- to simplify queries and reduce development headaches in general, we 720 +-- offload as much logic as possible into views. to avoid versioning 721 +-- difficulties, these views are not part of the schema, but are rather 722 +-- uploaded to the database at the start of a parsav connection, visible 723 +-- only to the connecting parsav instance, stored in memory, and dropped 724 +-- as soon as the connection session ends. 725 + 726 +local tempviews = sqlsquash(lib.util.ingest 'backend/schema/pgsql-views.sql') 727 +local prep = { quote 728 + var res = lib.pq.PQexec([con], tempviews) 729 + if lib.pq.PQresultStatus(res) == lib.pq.PGRES_COMMAND_OK then 730 + lib.dbg('uploading pgsql session views') 731 + else 732 + lib.bail('backend pgsql - failed to upload session views: \n', lib.pq.PQresultErrorMessage(res)) 733 + end 734 +end } 763 735 764 736 for k,q in pairs(queries) do 765 737 local qt = sqlsquash(q.sql) 766 738 local stmt = 'parsavpg_' .. k 767 739 terra q.prep([con]) 768 740 var res = lib.pq.PQprepare([con], stmt, qt, [#q.params], nil) 769 741 defer lib.pq.PQclear(res) ................................................................................ 979 951 lib.dbg(['searching for hashed password credentials in format SHA' .. tostring(hash)]) 980 952 var [out] 981 953 [vdrs] 982 954 lib.dbg(['could not find password hash']) 983 955 end 984 956 end 985 957 986 -local schema = sqlsquash(lib.util.ingest('backend/schema/pgsql.sql')) 987 -local obliterator = sqlsquash(lib.util.ingest('backend/schema/pgsql-drop.sql')) 958 +local schema = sqlsquash(lib.util.ingest 'backend/schema/pgsql.sql') 959 +local obliterator = sqlsquash(lib.util.ingest 'backend/schema/pgsql-drop.sql') 988 960 989 961 local privupdate = terra( 990 962 src: &lib.store.source, 991 963 ac: &lib.store.actor 992 964 ): {} 993 965 var pdef: lib.store.powerset pdef:clear() 994 966 var map = array([privmap]) ................................................................................ 1081 1053 return con 1082 1054 end]; 1083 1055 1084 1056 close = [terra(src: &lib.store.source) lib.pq.PQfinish([&lib.pq.PGconn](src.handle)) end]; 1085 1057 1086 1058 tx_enter = txdo, tx_complete = txdone; 1087 1059 1088 - conprep = [terra(src: &lib.store.source, mode: lib.store.prepmode.t) 1060 + conprep = [terra(src: &lib.store.source, mode: lib.store.prepmode.t): {} 1089 1061 var [con] = [&lib.pq.PGconn](src.handle) 1090 1062 if mode == lib.store.prepmode.full then [prep] 1091 1063 elseif mode == lib.store.prepmode.conf or 1092 1064 mode == lib.store.prepmode.admin then 1093 1065 queries.conf_get.prep(con) 1094 1066 queries.conf_set.prep(con) 1095 1067 queries.conf_reset.prep(con) 1096 1068 if mode == lib.store.prepmode.admin then 1097 1069 end 1098 1070 else lib.bail('unsupported connection preparation mode') end 1099 1071 end]; 1100 1072 1101 - dbsetup = [terra(src: &lib.store.source) 1073 + dbsetup = [terra(src: &lib.store.source): bool 1102 1074 var res = lib.pq.PQexec([&lib.pq.PGconn](src.handle), schema) 1103 1075 if lib.pq.PQresultStatus(res) == lib.pq.PGRES_COMMAND_OK then 1104 1076 lib.report('successfully instantiated schema in database') 1105 1077 return true 1106 1078 else 1107 1079 lib.warn('backend pgsql - failed to initialize database: \n', lib.pq.PQresultErrorMessage(res)) 1108 1080 return false 1109 1081 end 1110 1082 end]; 1111 1083 1112 - obliterate_everything = [terra(src: &lib.store.source) 1084 + obliterate_everything = [terra(src: &lib.store.source): bool 1113 1085 var res = lib.pq.PQexec([&lib.pq.PGconn](src.handle), obliterator) 1114 1086 if lib.pq.PQresultStatus(res) == lib.pq.PGRES_COMMAND_OK then 1115 1087 lib.report('successfully wiped out everything parsav-related in database') 1116 1088 return true 1117 1089 else 1118 1090 lib.warn('backend pgsql - failed to obliterate database: \n', lib.pq.PQresultErrorMessage(res)) 1119 1091 return false ................................................................................ 1242 1214 1243 1215 var a = row_to_actor(&r, 0) 1244 1216 a.ptr.source = src 1245 1217 1246 1218 var au = [lib.stat(lib.store.auth)] { ok = true } 1247 1219 au.val.aid = aid 1248 1220 au.val.uid = a.ptr.id 1249 - if not r:null(0,14) then -- restricted? 1221 + if not r:null(0,13) then -- restricted? 1250 1222 au.val.privs:clear() 1251 - (au.val.privs.post << r:bool(0,15)) 1252 - (au.val.privs.edit << r:bool(0,16)) 1253 - (au.val.privs.account << r:bool(0,17)) 1254 - (au.val.privs.upload << r:bool(0,18)) 1255 - (au.val.privs.moderate<< r:bool(0,19)) 1256 - (au.val.privs.admin << r:bool(0,20)) 1223 + (au.val.privs.post << r:bool(0,14)) 1224 + (au.val.privs.edit << r:bool(0,15)) 1225 + (au.val.privs.account << r:bool(0,16)) 1226 + (au.val.privs.upload << r:bool(0,17)) 1227 + (au.val.privs.moderate<< r:bool(0,18)) 1228 + (au.val.privs.admin << r:bool(0,19)) 1257 1229 else au.val.privs:fill() end 1258 1230 1259 1231 return au, a 1260 1232 end 1261 1233 1262 1234 ::fail:: return [lib.stat (lib.store.auth) ] { ok = false }, 1263 1235 [lib.mem.ptr(lib.store.actor)] { ptr = nil, ct = 0 } ................................................................................ 1301 1273 1302 1274 post_retweet = [terra( 1303 1275 src: &lib.store.source, 1304 1276 uid: uint64, 1305 1277 post: uint64, 1306 1278 undo: bool 1307 1279 ): {} 1280 + var time = lib.osclock.time(nil) 1308 1281 if not undo then 1309 - queries.post_react_simple.exec(src,uid,post,"rt") 1282 + queries.post_react_simple.exec(src,uid,post,"rt",time) 1310 1283 else 1311 1284 queries.post_react_cancel.exec(src,uid,post,"rt") 1312 1285 end 1313 1286 end]; 1314 1287 post_like = [terra( 1315 1288 src: &lib.store.source, 1316 1289 uid: uint64, 1317 1290 post: uint64, 1318 1291 undo: bool 1319 1292 ): {} 1293 + var time = lib.osclock.time(nil) 1320 1294 if not undo then 1321 - queries.post_react_simple.exec(src,uid,post,"like") 1295 + queries.post_react_simple.exec(src,uid,post,"like",time) 1322 1296 else 1323 1297 queries.post_react_cancel.exec(src,uid,post,"like") 1324 1298 end 1325 1299 end]; 1326 1300 post_liked_uid = [terra( 1327 1301 src: &lib.store.source, 1328 1302 uid: uint64, ................................................................................ 1391 1365 -- check against default rights, insert records for wherever powers differ 1392 1366 lib.dbg('created new actor, establishing powers') 1393 1367 privupdate(src,ac) 1394 1368 1395 1369 lib.dbg('powers established') 1396 1370 return ac.id 1397 1371 end]; 1372 + 1373 + actor_rel_create = [terra( 1374 + src: &lib.store.source, 1375 + kind: uint16, 1376 + relator: uint64, 1377 + relatee: uint64 1378 + ): {} queries.actor_rel_create.exec(src,kind,relator,relatee) end]; 1379 + 1380 + actor_rel_destroy = [terra( 1381 + src: &lib.store.source, 1382 + kind: uint16, 1383 + relator: uint64, 1384 + relatee: uint64 1385 + ): {} queries.actor_rel_destroy.exec(src,kind,relator,relatee) end]; 1386 + 1387 + actor_rel_calc = [terra( 1388 + src: &lib.store.source, 1389 + relator: uint64, 1390 + relatee: uint64 1391 + ): lib.store.relationship 1392 + var r = lib.store.relationship { 1393 + agent = relator, patient = relatee 1394 + } r.rel:clear() 1395 + r.recip:clear() 1396 + 1397 + var res = queries.actor_rel_enum.exec(src,relator,relatee) 1398 + var recip = queries.actor_rel_enum.exec(src,relatee,relator) 1399 + 1400 + if res.sz > 0 then defer res:free() 1401 + for i = 0, res.sz do 1402 + var bit = res:int(uint16, i, 0)-1 1403 + if bit < [#lib.store.relation.members] then r.rel:setbit(bit, true) 1404 + else lib.warn('unknown relationship type in database') end 1405 + end 1406 + end 1407 + 1408 + if recip.sz > 0 then defer recip:free() 1409 + for i = 0, recip.sz do 1410 + var bit = recip:int(uint16, i, 0)-1 1411 + if bit < [#lib.store.relation.members] then r.recip:setbit(bit, true) 1412 + else lib.warn('unknown relationship type in database') end 1413 + end 1414 + end 1415 + 1416 + return r 1417 + end]; 1398 1418 1399 1419 actor_purge_uid = [terra( 1400 1420 src: &lib.store.source, 1401 1421 uid: uint64 1402 1422 ) queries.actor_purge_uid.exec(src,uid) end]; 1403 1423 1404 1424 auth_enum_uid = [terra( ................................................................................ 1432 1452 ): {} 1433 1453 var hash: uint8[lib.crypt.algsz.sha256] 1434 1454 if lib.md.mbedtls_md(lib.md.mbedtls_md_info_from_type(lib.crypt.alg.sha256.id), 1435 1455 [&uint8](pw.ptr), pw.ct, &hash[0]) ~= 0 then 1436 1456 lib.bail('cannot hash password') 1437 1457 end 1438 1458 if reset then queries.auth_purge_type.exec(src, nil, uid, 'pw-%') end 1439 - queries.auth_create_pw.exec(src, uid, binblob {ptr = &hash[0], ct = [hash.type.N]}, comment) 1459 + queries.auth_create_pw.exec(src, uid, binblob {ptr = &hash[0], ct = [hash.type.N]}, lib.osclock.time(nil), comment) 1440 1460 end]; 1441 1461 1442 1462 auth_purge_pw = [terra(src: &lib.store.source, uid: uint64, handle: rawstring): {} 1443 1463 queries.auth_purge_type.exec(src, handle, uid, 'pw-%') 1444 1464 end]; 1445 1465 1446 1466 auth_purge_otp = [terra(src: &lib.store.source, uid: uint64, handle: rawstring): {}
Modified backend/schema/pgsql-auth.sql from [8bbbf24f23] to [09d655e8a5].
39 39 -- in from an IP address contained by this netmask. 40 40 41 41 blacklist bool not null default false, 42 42 -- if the credential matches, access will be denied, even if 43 43 -- non-blacklisted credentials match. most useful with 44 44 -- uid = null, kind = trust, cidr = (untrusted IP range) 45 45 46 - valperiod timestamp default now(), 46 + valperiod bigint not null, 47 47 -- cookies bearing timestamps earlier than this point in time 48 48 -- will be considered invalid and will not grant access 49 49 50 50 comment text, 51 51 -- a field the user can use to identify the specific credential, 52 52 -- in order to aid credential management 53 53 54 54 unique(name,kind,cred) 55 55 );
Modified backend/schema/pgsql-drop.sql from [fa02548662] to [6ff3d1c365].
3 3 drop table if exists parsav_config cascade; 4 4 drop table if exists parsav_servers cascade; 5 5 drop table if exists parsav_actors cascade; 6 6 drop table if exists parsav_actor_conf_strs cascade; 7 7 drop table if exists parsav_actor_conf_ints cascade; 8 8 drop table if exists parsav_rights cascade; 9 9 drop table if exists parsav_posts cascade; 10 -drop table if exists parsav_conversations cascade; 11 10 drop table if exists parsav_rels cascade; 12 11 drop table if exists parsav_acts cascade; 13 12 drop table if exists parsav_log cascade; 14 13 drop table if exists parsav_artifacts cascade; 15 14 drop table if exists parsav_artifact_claims cascade; 16 15 drop table if exists parsav_circles cascade; 17 16 drop table if exists parsav_rooms cascade; 18 17 drop table if exists parsav_room_members cascade; 19 18 drop table if exists parsav_invites cascade; 20 19 drop table if exists parsav_sanctions cascade; 21 20 drop table if exists parsav_auth cascade;
Added backend/schema/pgsql-views.sql version [47dae6d49a].
1 +-- these views are not part of the schema proper, but rather are uploaded 2 +-- into postgres' memory by parsav at the beginning of a connection. they 3 +-- are not visible to other clients and politely disappear once the 4 +-- connection terminates, allowing us to simultaneously avoid versioning 5 +-- headaches, limit the amount of data we need to send to the server, and 6 +-- reduce the compilation time of our prepared queries. 7 + 8 +create or replace temp view parsavpg_post_react_counts as ( 9 + with counts as ( 10 + select a.kind, p.id as subject, count(*) as ct from parsav_acts as a 11 + inner join parsav_posts as p on p.id = a.subject 12 + group by a.kind, p.id 13 + ) 14 + 15 + select p.id as post, 16 + coalesce((select counts.ct from counts where counts.subject = p.id 17 + and counts.kind = 'like'),0)::integer as likes, 18 + coalesce((select counts.ct from counts where counts.subject = p.id 19 + and counts.kind = 'rt' ),0)::integer as rts 20 + from parsav_posts as p 21 +); 22 + 23 +create type pg_temp.parsavpg_intern_notice as ( 24 + kind smallint, 25 + "when" bigint, 26 + who bigint, 27 + what bigint, 28 + reply bigint, 29 + reaction text 30 +); 31 + 32 +create type pg_temp.parsavpg_intern_actor as ( 33 + id bigint, 34 + nym text, 35 + handle text, 36 + origin bigint, 37 + bio text, 38 + avataruri text, 39 + rank smallint, 40 + quota integer, 41 + key bytea, 42 + epithet text, 43 + knownsince bigint, 44 + xid text, 45 + invites integer 46 +); 47 + 48 +create or replace function 49 +pg_temp.parsavpg_translate_actor(parsav_actors) 50 +returns pg_temp.parsavpg_intern_actor as $$ 51 + select 52 + ($1).id, ($1).nym, ($1).handle, ($1).origin, ($1).bio, 53 + ($1).avataruri, ($1).rank, ($1).quota, ($1).key, ($1).epithet, 54 + ($1).knownsince::bigint, 55 + coalesce(($1).handle || '@' || 56 + (select domain from parsav_servers as s where s.id = ($1).origin), 57 + '@' || ($1).handle) as xid, 58 + ($1).invites 59 +$$ language sql; 60 + 61 +--drop type if exists pg_temp.parsavpg_intern_post; 62 +create type pg_temp.parsavpg_intern_post as ( 63 + -- order is crucially important, and must match the order used 64 + -- in row_to_actor. names don't matter 65 + localpost bool, 66 + id bigint, 67 + author bigint, 68 + subject text, 69 + acl text, 70 + body text, 71 + posted bigint, 72 + discovered bigint, 73 + edited bigint, 74 + parent bigint, 75 + convoheaduri text, 76 + chgcount integer, 77 + accent smallint, 78 + rtdby bigint, -- note that these must be 0 if the record 79 + rtid bigint, -- in question does not represent an RT! 80 + n_likes integer, 81 + n_rts integer 82 +); 83 + 84 +create or replace function 85 +pg_temp.parsavpg_translate_post(parsav_posts,bigint,bigint) 86 +returns pg_temp.parsavpg_intern_post as $$ 87 + select a.origin is null, 88 + ($1).id, ($1).author, 89 + ($1).subject,($1).acl, ($1).body, 90 + ($1).posted, ($1).discovered, ($1).edited, 91 + ($1).parent, ($1).convoheaduri,($1).chgcount, 92 + coalesce(c.value, -1)::smallint, 93 + $2 as rtdby, $3 as rtid, 94 + re.likes, re.rts 95 + from parsav_actors as a 96 + left join parsav_actor_conf_ints as c 97 + on c.key = 'ui-accent' and 98 + c.uid = a.id 99 + left join pg_temp.parsavpg_post_react_counts as re 100 + on re.post = ($1).id 101 + where a.id = ($1).author 102 +$$ language sql; 103 + 104 +create or replace temp view parsavpg_known_content as ( 105 + with posts as ( 106 + select p as orig, 107 + null::bigint as promoter, 108 + null::bigint as promotion, 109 + coalesce(p.posted,p.discovered) as promotime 110 + from parsav_posts as p 111 + ), 112 + 113 + rts as ( 114 + select p as orig, 115 + a.actor as promoter, 116 + a.id as promotion, 117 + a.time as promotime 118 + from parsav_acts as a 119 + inner join parsav_posts as p on a.subject = p.id 120 + where a.kind = 'rt' 121 + ), 122 + 123 + content as (select * from posts union select * from rts) 124 + 125 + select pg_temp.parsavpg_translate_post(cn.orig, 126 + coalesce(cn.promoter,0), coalesce(cn.promotion,0) 127 + ) as post, 128 + cn.promotime::bigint as tltime, 129 + coalesce(cn.promoter, (cn.orig).author) as promoter 130 + from content as cn 131 + order by cn.promotime desc 132 +); 133 + 134 +-- 135 +--create temp view parsavpg_post_threads as ( 136 +-- 137 +--); 138 +-- 139 +create temp view parsavpg_notices as ( 140 + -- TODO add mentions 141 + with ntimes as ( 142 + select uid, value as when from parsav_actor_conf_ints where key = 'notice-clear-time' 143 + ), acts as ( 144 + select row( 145 + kmap.kind::smallint, 146 + a.time, 147 + a.actor, 148 + a.subject, 149 + null::bigint, 150 + null::text 151 + )::pg_temp.parsavpg_intern_notice as notice, 152 + p.author as rcpt 153 + from parsav_acts as a 154 + inner join parsav_posts as p on a.subject = p.id 155 + inner join (values 156 + ('rt', 4 ), 157 + ('like', 3 ), 158 + ('react', 5 ) 159 + ) as kmap(kstr,kind) on kmap.kstr = a.kind 160 + left join ntimes as nt on nt.uid = p.author 161 + where a.time >= coalesce(nt.when,0) 162 + ), replies as ( 163 + select row( 164 + 2::smallint, 165 + coalesce(p.posted,p.discovered), 166 + p.author, 167 + p.parent, 168 + p.id, 169 + null::text 170 + )::pg_temp.parsavpg_intern_notice as notice, 171 + par.author as rcpt 172 + from parsav_posts as p 173 + inner join parsav_posts as par on p.parent = par.id 174 + left join ntimes as nt on nt.uid = p.author 175 + where p.discovered >= coalesce(nt.when,0) 176 + ), allnotices as (select * from acts union select * from replies) 177 + 178 + table allnotices order by (notice).when desc 179 +); 180 +
Modified backend/schema/pgsql.sql from [347a4ab533] to [29d1b18fbc].
12 12 -- ('policy-self-register',:'regpol'), 13 13 -- ('master',:'admin'), 14 14 15 15 -- note that valid ids should always > 0, as 0 is reserved for null 16 16 -- on the client side, vastly simplifying code 17 17 create table parsav_servers ( 18 18 id bigint primary key default (1+random()*(2^63-1))::bigint, 19 - domain text not null, 19 + domain text not null unique, 20 20 key bytea, 21 - knownsince timestamp, 21 + knownsince bigint, 22 22 parsav boolean -- whether to use parsav protocol extensions 23 23 ); 24 24 25 25 create table parsav_actors ( 26 26 id bigint primary key default (1+random()*(2^63-1))::bigint, 27 27 nym text, 28 28 handle text not null, -- nym [@handle@origin] 29 29 origin bigint references parsav_servers(id) 30 30 on delete cascade, -- null origin = local actor 31 - knownsince timestamp not null default now(), 31 + knownsince bigint not null, 32 32 bio text, 33 33 avatarid bigint, -- artifact id, null if remote 34 34 avataruri text, -- null if local 35 35 rank smallint not null default 0, 36 36 quota integer not null default 1000, 37 37 invites integer not null default 0, 38 38 key bytea, -- private if localactor; public if remote 39 39 epithet text, 40 - authtime timestamp not null default now(), -- cookies earlier than this timepoint will not be accepted 40 + authtime bigint not null, -- cookies earlier than this timepoint will not be accepted 41 41 42 42 unique (handle,origin) 43 43 ); 44 44 45 45 create table parsav_rights ( 46 46 key text, 47 47 actor bigint references parsav_actors(id) 48 48 on delete cascade, 49 49 allow boolean not null, 50 50 scope bigint, -- for future expansion 51 51 52 52 primary key (key,actor) 53 53 ); 54 +create index on parsav_rights (actor); 54 55 55 56 create table parsav_posts ( 56 57 id bigint primary key default (1+random()*(2^63-1))::bigint, 57 - author bigint references parsav_actors(id) 58 - on delete cascade, 58 + author bigint references parsav_actors(id) on delete cascade, 59 59 subject text, 60 60 acl text not null default 'all', -- just store the script raw 🤷 61 61 body text, 62 - posted timestamp not null, 63 - discovered timestamp not null, 62 + posted bigint not null, 63 + discovered bigint not null, 64 64 chgcount integer not null default 0, 65 - edited timestamp, 65 + edited bigint, 66 66 parent bigint not null default 0, -- if post: part of conversation; if chatroom: top-level post 67 67 circles bigint[], -- TODO at edit or creation, iterate through each circle 68 68 mentions bigint[], -- a user has, check if it can see her post, and if so add 69 69 artifacts bigint[], 70 70 71 71 convoheaduri text 72 72 -- only used for tracking foreign conversations and tying them to post heads; 73 73 -- local conversations are tracked directly and mapped to URIs based on the 74 74 -- head's ID. null if native tweet or not the first tweet in convo 75 75 ); 76 +create index on parsav_posts (author); 77 +create index on parsav_posts (parent); 76 78 77 79 create table parsav_rels ( 78 80 relator bigint references parsav_actors(id) 79 81 on delete cascade, -- e.g. follower 80 82 relatee bigint references parsav_actors(id) 81 83 on delete cascade, -- e.g. followed 82 84 kind smallint, -- e.g. follow, block, mute ................................................................................ 83 85 84 86 primary key (relator, relatee, kind) 85 87 ); 86 88 87 89 create table parsav_acts ( 88 90 id bigint primary key default (1+random()*(2^63-1))::bigint, 89 91 kind text not null, -- like, rt, react, so on 90 - time timestamp not null default now(), 91 - actor bigint references parsav_actors(id) 92 - on delete cascade, 92 + time bigint not null, 93 + actor bigint references parsav_actors(id) on delete cascade, 93 94 subject bigint, -- may be post or act, depending on kind 94 95 body text -- emoji, if react 95 96 ); 97 +create index on parsav_acts (subject); 98 +create index on parsav_acts (actor); 99 +create index on parsav_acts (time); 96 100 97 101 create table parsav_log ( 98 102 -- accesses are tracked for security & sending delete acts 99 103 id bigint primary key default (1+random()*(2^63-1))::bigint, 100 - time timestamp not null default now(), 104 + time bigint not null, 101 105 actor bigint references parsav_actors(id) 102 106 on delete cascade, 103 107 post bigint not null 104 108 ); 105 109 106 110 create table parsav_artifacts ( 107 111 id bigint primary key default (1+random()*(2^63-1))::bigint, 108 - birth timestamp not null default now(), 112 + birth bigint not null, 109 113 content bytea, -- if null, this is a "ban record" preventing content matching the hash from being re-uploaded 110 114 hash bytea unique not null, -- sha256 hash of content 111 115 -- it would be cool to use a computed column for this, but i don't want 112 116 -- to lock people into PG12 or drag in the pgcrypto extension just for this 113 117 mime text -- null if unknown, will be reported as x-octet-stream 114 118 ); 115 119 create index on parsav_artifacts (mime); 116 120 117 121 create table parsav_artifact_claims ( 118 - birth timestamp not null default now(), 122 + birth bigint not null, 119 123 uid bigint references parsav_actors(id) on delete cascade, 120 124 rid bigint references parsav_artifacts(id) on delete cascade, 121 125 description text, 122 126 folder text, 123 127 124 128 unique (uid,rid) 125 129 ); 126 130 create index on parsav_artifact_claims (uid); 131 +create index on parsav_artifact_claims (uid,folder); 127 132 128 133 create table parsav_circles ( 129 134 id bigint primary key default (1+random()*(2^63-1))::bigint, 130 135 owner bigint not null references parsav_actors(id) on delete cascade, 131 136 name text not null, 132 137 members bigint[] not null default array[]::bigint[], 133 138 134 139 unique (owner,name) 135 140 ); 141 +create index on parsav_circles (owner); 136 142 137 143 create table parsav_rooms ( 138 144 id bigint primary key default (1+random()*(2^63-1))::bigint, 139 145 origin bigint references parsav_servers(id) on delete cascade, 140 146 name text not null, 141 147 description text not null, 142 148 policy smallint not null ................................................................................ 146 152 room bigint not null references parsav_rooms(id) on delete cascade, 147 153 member bigint not null references parsav_actors(id) on delete cascade, 148 154 rank smallint not null default 0, 149 155 admin boolean not null default false, -- non-admins with rank can only moderate + invite 150 156 title text, -- admin-granted title like reddit flair 151 157 vouchedby bigint references parsav_actors(id) on delete set null 152 158 ); 159 +create index on parsav_room_members (member); 160 +create index on parsav_room_members (room); 153 161 154 162 create table parsav_invites ( 155 163 id bigint primary key default (1+random()*(2^63-1))::bigint, 156 164 -- when a user is created from an invite, the invite is deleted and the invite 157 165 -- ID becomes the user ID. privileges granted on the invite ID during the invite 158 166 -- process are thus inherited by the user 159 167 issuer bigint references parsav_actors(id) on delete set null, ................................................................................ 164 172 165 173 create table parsav_sanctions ( 166 174 id bigint primary key default (1+random()*(2^63-1))::bigint, 167 175 issuer bigint references parsav_actors(id) on delete set null, 168 176 scope bigint, -- can be null or room for local actions 169 177 nature smallint not null, -- silence, suspend, disemvowel, censor, noreply, etc 170 178 victim bigint not null, -- can be user, room, or post 171 - expire timestamp, -- auto-expires if set 172 - review timestamp, -- brings up for review at given time if set 179 + expire bigint, -- auto-expires if set 180 + review bigint, -- brings up for review at given time if set 173 181 reason text, -- visible to victim if set 174 - context text -- admin-only note 182 + context text, -- admin-only note 183 + appeal text -- null if no appeal lodged 175 184 ); 185 +create index on parsav_sanctions (victim,scope); 186 +create index on parsav_sanctions (issuer); 176 187 177 188 create table parsav_actor_conf_strs ( 178 189 uid bigint not null references parsav_actors(id) on delete cascade, 179 190 key text not null, value text not null, unique (uid,key) 180 191 ); 181 192 create table parsav_actor_conf_ints ( 182 193 uid bigint not null references parsav_actors(id) on delete cascade, 183 194 key text not null, value bigint not null, unique (uid,key) 184 195 ); 185 196 186 197 -- create a temporary managed auth table; we can delete this later 187 198 -- if it ends up being replaced with a view 188 199 %include pgsql-auth.sql%
Modified doc/usr.md from [61c1a08ae0] to [614bf60d7c].
49 49 * **crier:** the user can promote content to the instance page (and thus the archives). note that by default only one post can be promoted per crier per day, though this can be changed (see [server configuration](srvcfg)). 50 50 * **elevate:** the user can increase the rank of lower-ranking actors up to one rank below his own, and can grant powers that he already possesses. 51 51 * **demote:** the user can decrease the rank of lower-ranking actors or strip them of rank entirely, and can revoke powers that she too possesses. 52 52 * **censor:** the user can eliminate undesirable content, remove posts from the instance page, and respond to badthink reports, whether by dismissing the report, by suppressing (but not deleting) the post in question, or by referring the matter upwards to someone with the discipline power. on smaller instances, moderators should probably hold this power and the discipline power simultaneously; on larger ones, it may be best to separate the two. 53 53 * **discipline:** the user can place *sanctions* on lower-ranking actors and cancel pending invites. sanctions are (usually temporary) [punishments](discipline) that strip certain abilities (or suspend certain conversations), and are intended as a less extreme, more flexible means of dealing with toxic behavior. most moderators should possess this power rather than `elevate` or `demote`, as sanctions leave a paper trail and can be summarily vacated by users of equal or higher rank with the `vacate` power. `discipline` also grants various other disciplinary abilities, such as issuing *demerits,* which can result in various penalties 54 54 * **vacate:** the user can rehabilitate disciplined actors, vacating sanctions, voiding demerits, and issuing temporary reprieves from restrictions. 55 55 * **purge:** the user can completely destroy lower-ranking accounts and all associated content, removing them entirely from the instance. best to keep this one for yourself. 56 - * **invite:** the user can issue invites without depleting their invite supply, even if they have none at all. users with both the `invite` and `elevate` powers can grant invites to others. 56 + * **invite:** the user can issue invites and create accounts without depleting their invite supply, even if they have none at all. users with both the `invite` and `elevate` powers can grant invites to others. 57 57 * **cred:** the user can add, change, and remove the credentials of lower-ranking users (think password resets). 58 58 * **config:** grants access to technical and security-related server settings, like `bind` or `domain`. be aware that changes made by users with this power affect *all* users, regardless of rank, and may affect how certain other powers function. 59 59 * **rebrand:** grants access to server settings that affect the appearance and livery of the site, like the `ui-accent` setting, the instance name, or the content of the instance page. 60 60 61 61 powers can be granted and revoked through the online interface, in the `users` section. they can also be controlled using the command line tool, with the commands `parsav user <handle> grant <power>…` and `revoke <power>…` (`all` can be used instead of a list of powers to grant or strip all powers simultaneously) 62 62 63 63 ### recommendations
Modified parsav.t from [2bbe093dad] to [5b9672ebb4].
183 183 var now = lib.osclock.time(nil) 184 184 var diff = now - lib.noise.lasttime 185 185 if diff > 30 then -- print cur time 186 186 lib.noise.lasttime = now 187 187 var curtime: int8[26] 188 188 lib.osclock.ctime_r(&now, &curtime[0]) 189 189 for i=0,26 do if curtime[i] == @'\n' then curtime[i] = 0 break end end -- :/ 190 - [ lib.emit(false, 2, '\27[1m[', `&curtime[0], ']\27[;36m\n +00 ') ] 190 + [ lib.emit(false, 2, '\27[1m', `&curtime[0], '\27[;36m\n +00 ') ] 191 191 else -- print time since last msg 192 192 var dfs = arrayof(int8, 0x30 + diff/10, 0x30 + diff%10, 0x20, 0) 193 193 [ lib.emit(false, 2, ' \27[36m+', `&dfs[0]) ] 194 194 end 195 195 end 196 196 197 197 local defrep = function(level,n,code) ................................................................................ 235 235 return n 236 236 end) 237 237 lib.enum = function(tbl) 238 238 local ty = uint8 239 239 if #tbl >= 2^32 then ty = uint64 -- hey, can't be too safe 240 240 elseif #tbl >= 2^16 then ty = uint32 241 241 elseif #tbl >= 2^8 then ty = uint16 end 242 - local o = { t = ty } 242 + local o = { t = ty, members = tbl } 243 243 local strings = {} 244 244 for i, name in ipairs(tbl) do 245 245 o[name] = i - 1 246 246 strings[i] = `[lib.mem.ref(int8)]{ptr=[name], ct=[#name]} 247 247 end 248 248 o._str = terra(val: ty) 249 249 var l = array([strings]) ................................................................................ 271 271 if (self._store[i/8] and (1 << i % 8)) ~= 0 then ct = ct + 1 end 272 272 end 273 273 return ct 274 274 end 275 275 set.methods.dump = macro(function(self) 276 276 local q = quote lib.io.say('dumping set:\n') end 277 277 for i,v in ipairs(tbl) do 278 - q = quote 279 - [q] 278 + q = quote [q] 280 279 if [bool](self.[v]) 281 280 then lib.io.say([' - ' .. v .. ': true\n']) 282 281 else lib.io.say([' - ' .. v .. ': false\n']) 283 282 end 284 283 end 285 284 end 286 285 return q
Modified render/conf/users.t from [4bed391611] to [0f343f8c98].
14 14 case [uint16](2) then acc:lpush('🔱') end 15 15 case [uint16](3) then acc:lpush('⚜️') end 16 16 case [uint16](4) then acc:lpush('🗡') end 17 17 case [uint16](5) then acc:lpush('🗝') end 18 18 else acc:lpush('🕴') 19 19 end 20 20 end 21 + 22 +local rnd = lib.crypt.random 23 +local terra 24 +suggest_handle(a: &lib.str.acc) 25 + var start = a.sz 26 + var puncts = array('.','_','-') 27 + var xXx = rnd(uint8, 0, 9) == 0 28 + var leet = rnd(uint8, 0, 8) == 0 29 + var caps = rnd(uint8, 0, 5) 30 + var punct: rawstring = nil 31 + var useadj = rnd(uint8, 0, 4) == 0 32 + if rnd(uint8, 0, 4) == 0 then 33 + punct = puncts[rnd(intptr,0,[puncts.type.N])] 34 + end 35 + 36 + var nouns = array( 37 + 'thunder','bride','blaze','doom','squad','gun','lord','blaster', 38 + 'fuck','hell','hound','piss','shit','killa','terror', 'horror', 39 + 'fear', 'slaughter','murder','general','commander', 'commissar', 40 + 'terrorist','infinity','slut','cunt','whore','bitch', 'bastard', 41 + 'cock','prince','princess','pimp','gay','cop','slayer', 'vampire', 42 + 'vampyre','blood','pain','brute','wolf','sword','star','sun','moon', 43 + 'killer','murderer','thief','arson','fire','ice','frost','hack', 44 + 'hacker','god','master','mistress','slave','rage','freeze','flayer', 45 + 'pirate','ninja','shadow','fog','mist','misery','glory','bear', 46 + 'king','queen','empress','emperor','majesty','space','martian', 47 + 'winter','fall','monk','katana','420','warrior','banana','demon', 48 + 'devil','ghost','wraith','cuck','legend','hero','heroine','goblin', 49 + 'gremlin','troll','dragon','evil','overlord','radiance' 50 + ) 51 + var adjs = array( 52 + 'dark','super','supreme','ultra','ultimate','total','infinite', 53 + 'omnipotent','crazy','final','deathless','immortal', 'elite', 54 + 'leet','1337','bloody','fearless','headless','screaming','insane', 55 + 'brutal','legendary','space','frozen','flaming','burning', 56 + 'mighty','flayed','hidden','secret','lost','mystery','glorious', 57 + 'nude','naked','bare','first','radiant','martian','fallen', 58 + 'wandering','dank','demonic','satanic','invisible','based','woke', 59 + 'deadly','lethal','heroic','evil','majestic','luminous' 60 + ) 61 + 62 + if xXx then a:lpush('xXx_') end 63 + 64 + if useadj then 65 + var len = rnd(uint8,1,3) 66 + for i = 0, len do 67 + var sz = a.sz 68 + a:push(adjs[rnd(intptr,0,[adjs.type.N])], 0) 69 + if punct ~= nil then a:push(punct, 1) end 70 + if caps == 1 then 71 + a.buf[sz] = lib.str.cupcase(a.buf[sz]) 72 + end 73 + end 74 + end 75 + var nounct = rnd(uint8,1,3) 76 + for i = 0, nounct do 77 + var sz = a.sz 78 + a:push(nouns[rnd(intptr,0,[nouns.type.N])], 0) 79 + if punct ~= nil and i+1 ~= nounct then a:push(punct, 1) end 80 + if caps == 1 then 81 + a.buf[sz] = lib.str.cupcase(a.buf[sz]) 82 + end 83 + end 84 + 85 + if leet or caps == 2 then for i=start, a.sz do 86 + if caps == 2 and rnd(uint8,0,5)==0 then 87 + a.buf[i] = lib.str.cupcase(a.buf[i]) 88 + end 89 + if leet then 90 + switch lib.str.cdowncase(a.buf[i]) do 91 + case [uint8]([string.byte('e')]) then a.buf[i] = @'3' end 92 + case [uint8]([string.byte('i')]) then a.buf[i] = @'1' end 93 + case [uint8]([string.byte('l')]) then a.buf[i] = @'1' end 94 + case [uint8]([string.byte('t')]) then a.buf[i] = @'7' end 95 + case [uint8]([string.byte('s')]) then a.buf[i] = @'5' end 96 + case [uint8]([string.byte('o')]) then a.buf[i] = @'0' end 97 + case [uint8]([string.byte('b')]) then a.buf[i] = @'6' end 98 + end 99 + end 100 + end end 101 + 102 + if (nounct == 1 and not useadj) or rnd(uint8, 0, 5) == 0 then 103 + if punct ~= nil then a:push(punct, 1) end 104 + a:ipush(rnd(uint16,0,65535)) 105 + end 106 + 107 + if xXx then a:lpush('_xXx') end 108 + 109 +end 21 110 22 111 local push_num_field = macro(function(acc,name,lbl,min,max,value,disable) 23 112 name = name:asvalue() 24 113 lbl = lbl:asvalue() 25 114 local start = '<div class="elem small">' 26 115 local enabled = start .. string.format('<label for="%s">%s</label><input type="number" id="%s" name="%s" min="', name, lbl, name, name) 27 116 local disabled = start .. string.format('<label>%s</label><div class="txtbox">', lbl) ................................................................................ 219 308 else 220 309 ulst:lpush('<span class="id">') 221 310 lib.render.nym(usr, 0, &ulst, false) 222 311 ulst:lpush('</span></li>') 223 312 end 224 313 ::skip::end 225 314 ulst:lpush('</ul>') 315 + 316 + if co.who.rights.powers.invite() or co.who.rights.invites > 0 then 317 + ulst:lpush('<details><summary>create new user</summary><form method="post"><div class="elem"><label for="handle">handle</label><input type="text" name="handle" id="handle" placeholder="') 318 + suggest_handle(&ulst) 319 + ulst:lpush('"></div><button name="act" value="create">create</button></form></details>') 320 + end 321 + ulst:lpush('<details><summary>instantiate remote actor</summary><form method="post"><div class="elem"><label for="xid">xid</label><input type="text" name="xid" id="xid" placeholder="tweetlord@website.tld"></div><button name="act" value="inst">instantiate</button></form></details>') 322 + 226 323 return ulst:finalize() 227 324 end 228 325 do return pstr.null() end 229 326 ::e404:: co:complain(404, 'not found', 'there is no user or resource by that identifier on this server') goto quit 230 327 ::e403:: co:complain(403, 'forbidden', 'you do not have sufficient authority to control that resource') 231 328 232 329 ::quit:: return pstr.null() 233 330 end 234 331 235 332 return render_conf_users
Modified render/profile.t from [531b5ac1cf] to [edefc21455].
1 1 -- vim: ft=terra 2 2 local pstr = lib.mem.ptr(int8) 3 3 local terra cs(s: rawstring) 4 4 return pstr { ptr = s, ct = lib.str.sz(s) } 5 5 end 6 6 7 7 local terra 8 -render_profile(co: &lib.srv.convo, actor: &lib.store.actor) 8 +render_profile( 9 + co: &lib.srv.convo, 10 + actor: &lib.store.actor, 11 + relationship: &lib.store.relationship 12 +): pstr 9 13 var aux: lib.str.acc 10 14 var followed = false -- FIXME 11 15 if co.aid ~= 0 and co.who.id == actor.id then 12 16 aux:compose('<a accesskey="a" class="button" href="/conf/profile?go=/@',actor.handle,'">alter</a>') 13 17 elseif co.aid ~= 0 then 14 - if not followed then 18 + if not relationship.rel.follow() then 15 19 aux:compose('<button accesskey="f" method="post" class="pos" name="act" value="follow">follow</button>') 16 - elseif followed then 20 + elseif relationship.rel.follow() then 17 21 aux:compose('<button accesskey="f" method="post" class="neg" name="act" value="unfollow">unfollow</button>') 18 22 end 19 23 aux:lpush('<a accesskey="h" class="button" href="/'):push(actor.xid,0):lpush('/chat">chat</a>') 20 24 if co.who.rights.powers:affect_users() and co.who:overpowers(actor) then 21 25 aux:lpush('<a accesskey="n" class="button" href="/'):push(actor.xid,0):lpush('/ctl">control</a>') 22 26 end 23 27 else ................................................................................ 67 71 end 68 72 69 73 if co.who:outranks(actor) then 70 74 comments:lpush('<li style="--co:50">underling</li>') 71 75 elseif actor:outranks(co.who) then 72 76 comments:lpush('<li style="--co:-50">outranks you</li>') 73 77 end 78 + 79 + if relationship.recip.follow() then 80 + comments:lpush('<li style="--co:30">follows you</li>') 81 + end 74 82 end 75 83 76 84 var profile = data.view.profile { 77 85 nym = fullname; 78 86 bio = bio; 79 87 xid = cs(actor.xid); 80 88 avatar = cs(actor.avatar);
Modified render/tweet.t from [d7574b82d8] to [8bee6c6ab8].
7 7 local terra 8 8 push_promo_header(co: &lib.srv.convo, acc: &lib.str.acc, rter: &lib.store.actor, rid: uint64) 9 9 acc:lpush('<div class="lede"><div class="promo"><img src="'):push(rter.avatar,0):lpush('"><a href="/') 10 10 if rter.origin ~= 0 then acc:lpush('@') end 11 11 acc:push(rter.xid,0):lpush('" class="username">') 12 12 lib.render.nym(rter, 0, acc, true) 13 13 acc:lpush('</a> retweeted</div>') 14 - if co.who.id == rter.id then 14 + if co.aid ~= 0 and co.who.id == rter.id then 15 15 acc:lpush('<a href="/post/'):shpush(rid):lpush('/del" class="del">✖</a>') 16 16 end 17 17 end 18 18 19 19 local terra 20 20 render_tweet(co: &lib.srv.convo, p: &lib.store.post, acc: &lib.str.acc) 21 21 var author: &lib.store.actor = nil ................................................................................ 35 35 end 36 36 37 37 ::foundauth:: 38 38 var avistr: lib.str.acc if author.origin == 0 then 39 39 avistr:compose('/avi/',author.handle) 40 40 end 41 41 var timestr: int8[26] lib.osclock.ctime_r(&p.posted, ×tr[0]) 42 + for i=0,26 do if timestr[i] == @'\n' then timestr[i] = 0 break end end -- 🙄 42 43 43 44 var bhtml = lib.smackdown.html([lib.mem.ptr(int8)] {ptr=p.body,ct=0}) 44 45 defer bhtml:free() 45 46 46 47 var idbuf: int8[lib.math.shorthand.maxlen] 47 48 var idlen = lib.math.shorthand.gen(p.id, idbuf) 48 49 var permalink: lib.str.acc permalink:compose('/post/',{idbuf,idlen}) ................................................................................ 57 58 permalink = permalink:finalize(); 58 59 attr = pstr{'',0}; 59 60 stats = pstr{'',0}; 60 61 } 61 62 if p.rts + p.likes > 0 then 62 63 var s: lib.str.acc s:init(128) 63 64 s:lpush('<div class="stats">') 64 - if p.rts > 0 then s:lpush('<div class="rt">' ):ipush(p.rts ):lpush('</div>') end 65 - if p.likes > 0 then s:lpush('<div class="like">'):ipush(p.likes):lpush('</div>') end 65 + if p.rts > 0 then s:lpush('<div class="rt">' ):dpush(p.rts ):lpush('</div>') end 66 + if p.likes > 0 then s:lpush('<div class="like">'):dpush(p.likes):lpush('</div>') end 66 67 s:lpush('</div>') 67 68 tpl.stats = s:finalize() 68 69 end 69 70 70 - var attrbuf: int8[48] 71 + var attrbuf: int8[64] 71 72 var attrcur = &attrbuf[0] 72 73 if p.accent ~= -1 and p.accent ~= co.ui_hue then 73 74 var hdecbuf: int8[21] 74 75 var hdec = lib.math.decstr(p.accent, &hdecbuf[20]) 75 76 attrcur = lib.str.cpy(attrcur,' style="--hue:') 76 77 attrcur = lib.str.cpy(attrcur, hdec) 77 78 -- var len = &hdecbuf[20] - hdec 78 79 attrcur = lib.str.cpy(attrcur, '"') 79 80 end 80 - if p.author == co.who.id then attrcur = lib.str.cpy(attrcur, ' data-own') end 81 + if co.aid ~= 0 and p.author == co.who.id then attrcur = lib.str.cpy(attrcur, ' data-own') end 82 + if retweeter ~= nil then attrcur = lib.str.cpy(attrcur, ' data-rt') end 81 83 82 84 if attrcur ~= &attrbuf[0] then tpl.attr = &attrbuf[0] end 83 85 84 86 defer tpl.permalink:free() 85 87 if acc ~= nil then 86 88 if retweeter ~= nil then push_promo_header(co, acc, retweeter, p.rtact) end 87 89 tpl:append(acc)
Modified render/user-page.t from [08cdf2fd9f] to [e4691ec838].
1 1 -- vim: ft=terra 2 2 local terra 3 -render_userpage(co: &lib.srv.convo, actor: &lib.store.actor) 3 +render_userpage( 4 + co : &lib.srv.convo, 5 + actor : &lib.store.actor, 6 + relationship: &lib.store.relationship 7 +): {} 4 8 var ti: lib.str.acc 5 9 if co.aid ~= 0 and co.who.id == actor.id then 6 10 ti:compose('my profile') 7 11 else 8 12 ti:compose('profile :: ', actor.handle) 9 13 end 10 14 var tiptr = ti:finalize() 11 15 12 16 var acc: lib.str.acc acc:init(1024) 13 - var pftxt = lib.render.profile(co,actor) defer pftxt:free() 17 + var pftxt = lib.render.profile(co,actor,relationship) defer pftxt:free() 14 18 acc:ppush(pftxt) 15 19 16 20 var stoptime = lib.osclock.time(nil) 17 21 var posts = co.srv:post_enum_author_uid(actor.id, lib.store.range { 18 22 mode = 1; -- T->I 19 23 from_time = stoptime; 20 24 to_idx = 64;
Modified route.t from [666eb021ed] to [cc5cf170ae].
3 3 local method = lib.http.method 4 4 local pstring = lib.mem.ptr(int8) 5 5 local rstring = lib.mem.ref(int8) 6 6 local hpath = lib.mem.ptr(rstring) 7 7 local http = {} 8 8 9 9 terra meth_get(meth: method.t) return (meth == method.get) or (meth == method.head) end 10 + 11 +terra http.actor_profile(co: &lib.srv.convo, actor: &lib.store.actor, meth: method.t) 12 + var rel: lib.store.relationship 13 + if co.aid ~= 0 then 14 + rel = co.srv:actor_rel_calc(co.who.id, actor.id) 15 + if meth == method.post then 16 + var act = co:ppostv('act') 17 + if act:cmp(lib.str.plit 'follow') and not rel.rel.follow() then 18 + if rel.recip.block() then 19 + co:complain(403,'blocked','you cannot follow a user you are blocked by') return 20 + end 21 + (rel.rel.follow << true) 22 + co.srv:actor_rel_create([lib.store.relation.idvmap.follow], co.who.id, actor.id) 23 + elseif act:cmp(lib.str.plit 'unfollow') and rel.rel.follow() then 24 + (rel.rel.follow << false) 25 + co.srv:actor_rel_destroy([lib.store.relation.idvmap.follow], co.who.id, actor.id) 26 + end 27 + end 28 + else 29 + rel.rel:clear() 30 + rel.recip:clear() 31 + end 32 + 33 + lib.render.user_page(co, actor, &rel) 34 +end 35 + 10 36 terra http.actor_profile_xid(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t) 11 37 var handle = [lib.mem.ptr(int8)] { ptr = &uri.ptr[2], ct = 0 } 12 38 for i=2,uri.ct do 13 39 if uri.ptr[i] == @'/' or uri.ptr[i] == 0 then handle.ct = i - 2 break end 14 40 end 15 41 if handle.ct == 0 then 16 42 handle.ct = uri.ct - 2 ................................................................................ 27 53 var actor = co.srv:actor_fetch_xid(handle) 28 54 if actor.ptr == nil then 29 55 co:complain(404,'no such user','no such user known to this server') 30 56 return 31 57 end 32 58 defer actor:free() 33 59 34 - lib.render.user_page(co, actor.ptr) 60 + http.actor_profile(co,actor.ptr,meth) 35 61 end 36 62 37 63 terra http.actor_profile_uid ( 38 64 co: &lib.srv.convo, 39 65 path: lib.mem.ptr(lib.mem.ref(int8)), 40 66 meth: method.t 41 67 ) ................................................................................ 53 79 var actor = co.srv:actor_fetch_uid(uid) 54 80 if actor.ptr == nil then 55 81 co:complain(404, 'no such user', 'no user by that ID is known to this instance') 56 82 return 57 83 end 58 84 defer actor:free() 59 85 60 - lib.render.user_page(co, actor.ptr) 86 + http.actor_profile(co,actor.ptr,meth) 61 87 end 62 88 63 89 terra http.login_form(co: &lib.srv.convo, meth: method.t) 64 90 if meth_get(meth) then 65 91 -- request a username 66 92 lib.render.login(co, nil, nil, lib.str.plit(nil)) 67 93 elseif meth == method.post then ................................................................................ 222 248 return 223 249 else goto badop end 224 250 end 225 251 else goto badurl end 226 252 end 227 253 228 254 if meth == method.post then 255 + if co.aid == 0 then goto noauth end 229 256 var act = co:ppostv('act') 230 257 if act:cmp(lib.str.plit 'like') and not co.srv:post_liked_uid(co.who.id,pid) then 231 258 co.srv:post_like(co.who.id, pid, false) 232 259 post.ptr.likes = post.ptr.likes + 1 233 260 elseif act:cmp(lib.str.plit 'dislike') and co.srv:post_liked_uid(co.who.id,pid) then 234 261 co.srv:post_like(co.who.id, pid, true) 235 262 post.ptr.likes = post.ptr.likes - 1 ................................................................................ 253 280 end 254 281 255 282 lib.render.tweet_page(co, path, post.ptr) 256 283 do return end 257 284 258 285 ::badurl:: do co:complain(404, 'invalid URL', 'this URL does not reference extant content or functionality') return end 259 286 ::badop :: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end 287 + ::noauth:: do co:complain(401, 'unauthorized', 'you have not supplied the necessary credentials to perform this operation') return end 260 288 end 261 289 262 290 terra http.configure(co: &lib.srv.convo, path: hpath, meth: method.t) 263 291 var msg = pstring.null() 264 292 -- first things first, do priv checks 265 293 if path.ct >= 1 then 266 294 if not co.who.rights.powers.config() and ( ................................................................................ 323 351 if act:cmp(lib.str.plit 'invalidate') then 324 352 lib.dbg('setting user\'s cookie validation time to now') 325 353 co.who.source:auth_sigtime_user_alter(co.who.id, lib.osclock.time(nil)) 326 354 -- the current session has been invalidated as well, so we need to immediately install a new authentication cookie with the same aid so the user doesn't need to log back in all over again 327 355 co:installkey('/conf/sec',co.aid) 328 356 return 329 357 end 330 - elseif path(1):cmp(lib.str.lit 'users') and path.ct >= 2 then 331 - var userid, ok = lib.math.shorthand.parse(path(2).ptr, path(2).ct) 332 - if ok then 333 - var usr = co.srv:actor_fetch_uid(userid) defer usr:free() 334 - if not co.who:overpowers(usr.ptr) then goto nopriv end 358 + elseif path(1):cmp(lib.str.lit 'users') then 359 + if path.ct >= 3 then 360 + var userid, ok = lib.math.shorthand.parse(path(2).ptr, path(2).ct) 361 + if ok then 362 + var usr = co.srv:actor_fetch_uid(userid) 363 + if usr:ref() then defer usr:free() 364 + if not co.who:overpowers(usr.ptr) then goto nopriv end 365 + end 366 + end 367 + elseif path.ct == 2 then 335 368 end 336 369 end 337 370 338 371 if user_refresh then -- refresh the user info for the renderer 339 372 var usr = co.srv:actor_fetch_uid(co.who.id) 340 373 lib.mem.heapf(co.who) 341 374 co.who = usr.ptr
Modified static/live.js from [f01ba9ae01] to [45ade869ea].
34 34 }) 35 35 } 36 36 37 37 /* div-based like and rt aren't very keyboard-friendly. add a replacement */ 38 38 if (document.querySelector('body.timeline, body.profile, body.post') != null) { 39 39 onkey(window, function(event) { 40 40 if (focused()) {return;} 41 - let cururl = window._liveTweetMap.cur; 41 + let tmap = window._liveTweetMap; 42 + let cururl = tmap.cur; 42 43 let nexturl = null; 43 44 if (event.key == 'j') { // down 44 45 if (cururl == null) { 45 - nexturl = window._liveTweetMap.first 46 + nexturl = tmap.first; 46 47 } else { 47 - nexturl = window._liveTweetMap.map.get(cururl).next 48 + nexturl = tmap.map.get(cururl).next; 48 49 } 49 50 } else if (event.key == 'k') { // up 50 51 if (cururl == null) { 51 - nexturl = window._liveTweetMap.last 52 + nexturl = tmap.last; 52 53 } else { 53 - nexturl = window._liveTweetMap.map.get(cururl).prev 54 + nexturl = tmap.map.get(cururl).prev; 54 55 } 55 56 } else if (cururl != null) { 56 - let post = window._liveTweetMap.map.get(cururl).me 57 + let post = tmap.map.get(cururl).me; 58 + let root = tmap.map.get(cururl).go; 57 59 if (event.key == 'f') { // fave 58 - postReq(cururl, 'like', post.querySelector('.stats>.like')) 60 + postReq(root, 'like', post.querySelector('.stats>.like')) 59 61 } else if (event.key == 'r') { // rt 60 - postReq(cururl, 'rt', post.querySelector('.stats>.rt')) 61 - } else if (event.key == 'd') { // rt 62 - if (post.attributes.getNamedItem('data-own')) { 63 - window.location = cururl + '/del'; 64 - } 62 + postReq(root, 'rt', post.querySelector('.stats>.rt')) 65 63 } else if (event.key == 'Enter') { // nav 66 - window.location = cururl; 64 + window.location = root; 67 65 return; 66 + } else if (post.attributes.getNamedItem('data-own')) { 67 + if (event.key == 'd') { window.location = root + '/del'; } 68 + else if (event.key == 'e') { window.location = root + '/edit'; } 69 + else if (event.key == 'u' && root != cururl) { window.location = cururl; } // detweet 68 70 } 69 71 } 70 72 if (nexturl != null) { 71 73 if (cururl != null) { 72 74 let cur = window._liveTweetMap.map.get(cururl); 73 75 cur.me.classList.remove('live-selected') 74 76 } ................................................................................ 105 107 } 106 108 }); 107 109 } 108 110 109 111 function attachButtons() { 110 112 let last = null; 111 113 let newmap = { cur: null, first: null, last: null, map: new Map() } 112 - document.querySelectorAll('main article.post').forEach(function(post){ 113 - let url = posturl(post); 114 + document.querySelectorAll('main article.post:not([data-rt]), main div.lede').forEach(function(post){ 115 + let ert = post.querySelector('article.post[data-rt]'); 116 + let lede = null; 117 + if (ert != null) { lede = post; post = ert; } 118 + let purl = posturl(post); 119 + let url = null; 120 + if (lede == null) {url = purl;} else { 121 + url = lede.querySelector('a[href].del'). 122 + attributes.getNamedItem('href').value; 123 + } 124 + console.log('post',post,'lede',lede,url); 125 + 114 126 if (last == null) { newmap.first = url; } else { 115 - newmap.map.get(last).next = url 127 + newmap.map.get(last).next = url; 116 128 } 117 - newmap.map.set(url, {me: post, prev: last, next: null}) 118 - last = url 119 - if (window._liveTweetMap && window._liveTweetMap.cur == url) { 129 + newmap.map.set(url, {me: post, go: purl, prev: last, next: null}) 130 + last = url; 131 + if (window._liveTweetMap && 132 + window._liveTweetMap.cur == url 133 + ) { 120 134 post.classList.add('live-selected'); 121 135 } 122 136 123 137 let stats = post.querySelector('.stats'); 124 138 if (stats == null) { 125 139 /* no stats box; create one */ 126 140 let n = mk('div');
Modified static/style.scss from [d34b63e467] to [5326e6a4a7].
544 544 justify-content: center; 545 545 > .like, > .rt { 546 546 margin: 0.5em 0.3em; 547 547 padding-left: 1.3em; 548 548 background-size: 1.1em; 549 549 background-repeat: no-repeat; 550 550 min-width: 0.3em; 551 + &:focus { 552 + outline: none; 553 + opacity: 0.9 !important; 554 + filter: brightness(1.7) drop-shadow(0 0 15px rgb(255,150,200)); 555 + } 551 556 &:empty { 552 557 transition: 0.3s; 553 558 opacity: 0.0001; // qutebrowser won't show hints if opacity=0 :( 554 559 &:hover, &:focus { opacity: 0.6 !important; } 555 560 } 556 561 } 557 562 > .like { background-image: url(/s/heart.webp); } ................................................................................ 789 794 } 790 795 791 796 ul.remarks { 792 797 margin: 0; padding: 0; 793 798 list-style-type: none; 794 799 li { 795 800 border-top: 1px solid otone(-22%); 796 - border-bottom: 2px solid otone(-55%); 801 + border-bottom: 1px solid otone(-53%); 802 + box-shadow: 0 1px 1px otone(-57%); 803 + text-shadow: 1px 1px otone(-60%); 797 804 border-radius: 3px; 798 805 background: otone(-25%,-0.4); 799 806 color: otone(25%); 800 807 text-align: center; 801 808 padding: 0.3em 0; 802 809 margin: 0.2em 0.1em; 803 810 cursor: default; ................................................................................ 911 918 div.lede { 912 919 display: grid; 913 920 grid-template-columns: 1fr min-content; 914 921 grid-template-rows: 1.5em 1fr; 915 922 padding: 0.1in 0.3in; 916 923 margin: 0 -0.2in; 917 924 margin-top: 0.2in; 925 + & + & { margin-top: 0; } 918 926 border-radius: 3px; 919 927 background: linear-gradient(to bottom, tone(-40%,-0.5), transparent); 920 928 border-top: 1px solid tone(-5%,-0.7); 921 929 > .promo { 922 930 grid-row: 1/2; grid-column: 1/2; 923 931 font-style: italic; 924 932 font-size: 90%;
Modified store.t from [6a465decce] to [194a7e6d10].
1 1 -- vim: ft=terra 2 2 local m = { 3 3 timepoint = lib.osclock.time_t; 4 4 scope = lib.enum { 5 5 'public', 'private', 'local'; 6 6 'personal', 'direct', 'circle'; 7 7 }; 8 - notiftype = lib.enum { 9 - 'none', 'mention', 'like', 'rt', 'react' 8 + noticetype = lib.enum { 9 + 'none', 'mention', 'reply', 'like', 'rt', 'react' 10 10 }; 11 11 12 12 relation = lib.set { 13 + 'follow', 14 + 'mute', -- posts will be completely hidden at all times 15 + 'block', -- no interactions will be permitted, but posts will remain visible 13 16 'silence', -- messages will not be accepted 14 17 'collapse', -- posts will be collapsed by default 15 18 'disemvowel', -- posts will be ritually humiliated, but shown 16 19 'avoid', -- posts will be kept out of the timeline but will show on users' posts and in conversations 17 - 'follow', 18 - 'mute', -- posts will be completely hidden at all times 19 - 'block', -- no interactions will be permitted, but posts will remain visible 20 + 'exclude', -- own posts will not be visible to this user 20 21 }; 21 22 credset = lib.set { 22 23 'pw', 'otp', 'challenge', 'trust' 23 24 }; 24 25 privset = lib.set { 25 26 'post', 'edit', 'account', 'upload', 'moderate', 'admin', 'invite' 26 27 }; ................................................................................ 242 243 } do 243 244 be.entries[#be.entries+1] = { 244 245 field = 'actor_conf_'..n..'_'..k, type = t 245 246 } 246 247 end 247 248 end 248 249 249 -struct m.notif { 250 - kind: m.notiftype.t 250 +struct m.notice { 251 + kind: m.noticetype.t 251 252 when: uint64 253 + who: uint64 254 + what: uint64 252 255 union { 253 - post: uint64 256 + reply: uint64 254 257 reaction: int8[16] 255 258 } 256 259 } 257 260 258 261 struct m.inet { 259 262 pv: uint8 -- 0 = null, 4 = ipv4, 6 = ipv6 260 263 union { ................................................................................ 343 346 344 347 actor_create: {&m.source, &m.actor} -> uint64 345 348 actor_save: {&m.source, &m.actor} -> {} 346 349 actor_save_privs: {&m.source, &m.actor} -> {} 347 350 actor_purge_uid: {&m.source, uint64} -> {} 348 351 actor_fetch_xid: {&m.source, lib.mem.ptr(int8)} -> lib.mem.ptr(m.actor) 349 352 actor_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.actor) 350 - actor_notif_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.notif) 351 353 actor_enum: {&m.source} -> lib.mem.lstptr(m.actor) 352 354 actor_enum_local: {&m.source} -> lib.mem.lstptr(m.actor) 353 355 actor_stats: {&m.source, uint64} -> m.actor_stats 354 356 actor_rel: {&m.source, uint64, uint64} -> m.relationship 355 357 356 358 actor_auth_how: {&m.source, m.inet, rawstring} -> {m.credset, bool} 357 359 -- returns a set of auth method categories that are available for a ................................................................................ 386 388 -- origin: inet 387 389 -- cookie issue time: m.timepoint 388 390 actor_auth_register_uid: {&m.source, uint64, uint64} -> {} 389 391 -- notifies the backend module of the UID that has been assigned for 390 392 -- an authentication ID 391 393 -- aid: uint64 392 394 -- uid: uint64 393 - actor_notifs_fetch: {&m.source, uint64} -> lib.mem.lstptr(m.notif) 395 + actor_notice_enum: {&m.source, uint64} -> lib.mem.lstptr(m.notice) 396 + actor_rel_create: {&m.source, uint16, uint64, uint64} -> {} 397 + actor_rel_destroy: {&m.source, uint16, uint64, uint64} -> {} 398 + actor_rel_calc: {&m.source, uint64, uint64} -> m.relationship 394 399 395 400 auth_enum_uid: {&m.source, uint64} -> lib.mem.lstptr(m.auth) 396 401 auth_enum_handle: {&m.source, rawstring} -> lib.mem.lstptr(m.auth) 397 402 auth_attach_pw: {&m.source, uint64, bool, pstr, pstr} -> {} 398 403 -- uid: uint64 399 404 -- reset: bool (delete other passwords?) 400 405 -- pw: pstring
Modified str.t from [638f6c2759] to [7fbe47e2ae].
179 179 end 180 180 lib.mem.cpy(self.buf + self.sz, str, len) 181 181 self.sz = self.sz + len 182 182 self.buf[self.sz] = 0 183 183 return self 184 184 end; 185 185 186 -terra m.acc:ipush(i: intptr) 186 +terra m.acc:dpush(i: intptr) 187 187 var decbuf: int8[21] 188 188 var si = lib.math.decstr_friendly(i, &decbuf[20]) 189 189 var len: intptr = [decbuf.type.N] - (si - &decbuf[0]) 190 - return self:push(si,len) 190 + return self:push(si,len-1) 191 +end 192 + 193 +terra m.acc:ipush(i: intptr) 194 + var decbuf: int8[21] 195 + var si = lib.math.decstr(i, &decbuf[20]) 196 + var len: intptr = [decbuf.type.N] - (si - &decbuf[0]) 197 + return self:push(si,len-1) 191 198 end 192 199 193 200 terra m.acc:shpush(i: uint64) 194 201 var sbuf: int8[lib.math.shorthand.maxlen] 195 202 var len = lib.math.shorthand.gen(i,&sbuf[0]) 196 203 return self:push(&sbuf[0], len) 197 204 end
Modified view/confirm.tpl from [8b0d6acfba] to [4c878ea4e6].
1 1 <form class="message" method="post"> 2 2 <img class="icon" src="/s/query.webp"> 3 3 <h1>@title</h1> 4 4 <p>@query</p> 5 5 <menu class="horizontal choice"> 6 - <a class="no button" href="@:cancel">cancel</a> 7 - <button name="act" value="confirm">confirm</button> 6 + <a accesskey="n" class="no button" href="@:cancel">cancel</a> 7 + <button accesskey="y" name="act" value="confirm">confirm</button> 8 8 </menu> 9 9 </form>
Modified view/profile.tpl from [4ac5403896] to [6a9a509b78].
12 12 <tr><td>@nposts</td> <td>@nmutuals</td></tr> 13 13 <tr><th>following</th> <th>followers</th></tr> 14 14 <tr><td>@nfollows</td> <td>@nfollowers</td></tr> 15 15 <tr><th>@timephrase</th> <td>@tweetday</td></tr> 16 16 </table> 17 17 <ul class="remarks">@remarks</ul> 18 18 </div> 19 - <form class="actions"> 19 + <form class="actions" method="post"> 20 20 <a class="button" href="/@:xid">posts</a> 21 21 <a class="button" href="/@:xid/arc">archive</a> 22 22 <a class="button" href="/@:xid/media">media</a> 23 23 <a class="button" href="/@:xid/social">associates</a> 24 24 <hr> 25 25 @auxbtn 26 26 </form> 27 27 </div>