| Comment: | add likes, retweets, and iterate on a whole bunch of other shit |
|---|---|
| Downloads: | Tarball | ZIP archive | SQL archive |
| Timelines: | family | ancestors | descendants | both | trunk |
| Files: | files | file ages | folders |
| SHA3-256: |
78b0198f099355ea30a888c8100a75ce |
| User & Date: | lexi on 2021-01-04 06:44:13 |
| Other Links: | manifest | tags |
|
2021-01-04
| ||
| 15:29 | add like + retweets buttons, keyboard nav check-in: b9cf14c14b user: lexi tags: trunk | |
| 06:44 | add likes, retweets, and iterate on a whole bunch of other shit check-in: 78b0198f09 user: lexi tags: trunk | |
|
2021-01-02
| ||
| 18:32 | iterate on user mgmt UI check-in: f09cd18161 user: lexi tags: trunk | |
Modified backend/pgsql.t from [962a3e64e0] to [2e62d4947d].
213 213 select a.id, a.nym, a.handle, a.origin, a.bio, 214 214 a.avataruri, a.rank, a.quota, a.key, a.epithet, 215 215 extract(epoch from a.knownsince)::bigint, 216 216 coalesce(a.handle || '@' || s.domain, 217 217 '@' || a.handle) as xid, 218 218 219 219 au.restrict, 220 - array['post' ] <@ au.restrict as can_post, 221 - array['edit' ] <@ au.restrict as can_edit, 222 - array['acct' ] <@ au.restrict as can_acct, 223 - array['upload'] <@ au.restrict as can_upload, 224 - array['censor'] <@ au.restrict as can_censor, 225 - array['admin' ] <@ au.restrict as can_admin 220 + array['post' ] <@ au.restrict, 221 + array['edit' ] <@ au.restrict, 222 + array['account' ] <@ au.restrict, 223 + array['upload' ] <@ au.restrict, 224 + array['moderate'] <@ au.restrict, 225 + array['admin' ] <@ au.restrict 226 226 227 227 from parsav_auth au 228 228 left join parsav_actors a on au.uid = a.id 229 229 left join parsav_servers s on a.origin = s.id 230 230 231 231 where au.aid = $1::bigint and au.blacklist = false and 232 232 (au.netmask is null or au.netmask >> $2::inet) and ................................................................................ 349 349 params = {uint64}, cmd = true, sql = [[ 350 350 delete from parsav_posts where id = $1::bigint 351 351 ]] 352 352 }; 353 353 354 354 post_fetch = { 355 355 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 + 356 362 select a.origin is null, 357 363 p.id, p.author, p.subject, p.acl, p.body, 358 364 extract(epoch from p.posted )::bigint, 359 365 extract(epoch from p.discovered)::bigint, 360 366 extract(epoch from p.edited )::bigint, 361 367 p.parent, p.convoheaduri, p.chgcount, 362 - coalesce(c.value, -1)::smallint 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 363 371 364 372 from parsav_posts as p 365 373 inner join parsav_actors as a on p.author = a.id 366 374 left join parsav_actor_conf_ints as c on c.uid = a.id and c.key = 'ui-accent' 367 375 where p.id = $1::bigint 368 376 ]]; 369 377 }; 370 378 371 379 post_enum_parent = { 372 380 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 + 373 387 select a.origin is null, 374 388 p.id, p.author, p.subject, p.acl, p.body, 375 389 extract(epoch from p.posted )::bigint, 376 390 extract(epoch from p.discovered)::bigint, 377 391 extract(epoch from p.edited )::bigint, 378 392 p.parent, p.convoheaduri, p.chgcount, 379 - coalesce(c.value, -1)::smallint 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 380 396 381 397 from parsav_posts as p 382 398 inner join parsav_actors as a on a.id = p.author 383 399 left join parsav_actor_conf_ints as c on c.uid = a.id and c.key = 'ui-accent' 384 400 where p.parent = $1::bigint 385 401 order by p.posted, p.discovered asc 386 402 ]] ................................................................................ 401 417 inner join parsav_posts as p 402 418 on p.id = posts.id 403 419 ) 404 420 405 421 select extract(epoch from max(m))::bigint from maxes 406 422 ]]; 407 423 }; 424 + 425 + 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 429 + ) returning id 430 + ]]; 431 + }; 432 + 433 + post_react_cancel = { 434 + params = {uint64, uint64, pstring}, cmd = true, sql = [[ 435 + delete from parsav_acts where 436 + actor = $1::bigint and 437 + subject = $2::bigint and 438 + kind = $3::text 439 + ]]; 440 + }; 441 + 442 + post_reacts_fetch_uid = { 443 + params = {uint64, uint64, pstring}, sql = [[ 444 + select id, actor, subject, kind, body, time from parsav_acts where 445 + ($1::bigint = 0 or actor = $1::bigint) and 446 + ($2::bigint = 0 or subject = $2::bigint) and 447 + ($3::text is null or kind = $3::text ) 448 + ]] 449 + }; 408 450 409 451 post_enum_author_uid = { 410 452 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 + ), 459 + 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 + ), 468 + 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 + 411 478 select a.origin is null, 412 479 p.id, p.author, p.subject, p.acl, p.body, 413 480 extract(epoch from p.posted )::bigint, 414 481 extract(epoch from p.discovered)::bigint, 415 482 extract(epoch from p.edited )::bigint, 416 483 p.parent, p.convoheaduri, p.chgcount, 417 - coalesce((select value from parsav_actor_conf_ints as c where 418 - c.uid = $1::bigint and c.key = 'ui-accent'),-1)::smallint 419 - 420 - from parsav_posts as p 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 421 489 inner join parsav_actors as a on p.author = a.id 422 - where p.author = $5::bigint and 423 - ($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and 424 - ($2::bigint = 0 or to_timestamp($2::bigint) < p.posted) 490 + left join parsav_actor_conf_ints as c 491 + on c.key = 'ui-accent' and 492 + c.uid = a.id 425 493 order by (p.posted, p.discovered) desc 426 494 limit case when $3::bigint = 0 then null 427 - else $3::bigint end 495 + else $3::bigint end 428 496 offset $4::bigint 429 497 ]] 430 498 }; 431 499 432 500 -- maybe there's some way to unify these two, idk, im tired 433 501 434 502 timeline_instance_fetch = { 435 503 params = {uint64, uint64, uint64, uint64}, sql = [[ 436 - select true, 437 - p.id, p.author, p.subject, p.acl, p.body, 438 - extract(epoch from p.posted )::bigint, 439 - extract(epoch from p.discovered)::bigint, 440 - extract(epoch from p.edited )::bigint, 441 - p.parent, null::text, p.chgcount, 442 - coalesce(c.value, -1)::smallint 443 - 444 - from parsav_posts as p 445 - inner join parsav_actors as a on p.author = a.id 446 - left join parsav_actor_conf_ints as c on c.uid = a.id and c.key = 'ui-accent' 447 - where 448 - ($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and 449 - ($2::bigint = 0 or to_timestamp($2::bigint) < p.posted) and 450 - (a.origin is null) 451 - order by (p.posted, p.discovered) desc 452 - limit case when $3::bigint = 0 then null 453 - else $3::bigint end 454 - offset $4::bigint 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 528 + ) 529 + 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 455 534 ]] 456 535 }; 457 536 458 537 artifact_instantiate = { 459 538 params = {binblob, binblob, pstring}, sql = [[ 460 539 insert into parsav_artifacts (content,hash,mime) values ( 461 540 $1::bytea, $2::bytea, $3::text ................................................................................ 792 871 end 793 872 p.ptr.parent = r:int(uint64,row,9) 794 873 if r:null(row,11) 795 874 then p.ptr.chgcount = 0 796 875 else p.ptr.chgcount = r:int(uint32,row,11) 797 876 end 798 877 p.ptr.accent = r:int(int16,row,12) 878 + p.ptr.rtdby = r:int(uint64,row,13) 879 + p.ptr.rtact = r:int(uint64,row,14) 880 + p.ptr.likes = r:int(uint32,row,15) 881 + p.ptr.rts = r:int(uint32,row,16) 799 882 p.ptr.localpost = r:bool(row,0) 800 883 801 884 return p 802 885 end 803 886 local terra row_to_actor(r: &pqr, row: intptr): lib.mem.ptr(lib.store.actor) 804 887 var a: lib.mem.ptr(lib.store.actor) 805 888 var av: rawstring, avlen: intptr ................................................................................ 1161 1244 a.ptr.source = src 1162 1245 1163 1246 var au = [lib.stat(lib.store.auth)] { ok = true } 1164 1247 au.val.aid = aid 1165 1248 au.val.uid = a.ptr.id 1166 1249 if not r:null(0,14) then -- restricted? 1167 1250 au.val.privs:clear() 1168 - (au.val.privs.post << r:bool(0,15)) 1169 - (au.val.privs.edit << r:bool(0,16)) 1170 - (au.val.privs.acct << r:bool(0,17)) 1171 - (au.val.privs.upload << r:bool(0,18)) 1172 - (au.val.privs.censor << r:bool(0,19)) 1173 - (au.val.privs.admin << r:bool(0,20)) 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)) 1174 1257 else au.val.privs:fill() end 1175 1258 1176 1259 return au, a 1177 1260 end 1178 1261 1179 1262 ::fail:: return [lib.stat (lib.store.auth) ] { ok = false }, 1180 1263 [lib.mem.ptr(lib.store.actor)] { ptr = nil, ct = 0 } ................................................................................ 1211 1294 ): lib.mem.ptr(lib.store.post) 1212 1295 var r = queries.post_fetch.exec(src, post) 1213 1296 if r.sz == 0 then return [lib.mem.ptr(lib.store.post)].null() end 1214 1297 var p = row_to_post(&r, 0) 1215 1298 p.ptr.source = src 1216 1299 return p 1217 1300 end]; 1301 + 1302 + post_retweet = [terra( 1303 + src: &lib.store.source, 1304 + uid: uint64, 1305 + post: uint64, 1306 + undo: bool 1307 + ): {} 1308 + if not undo then 1309 + queries.post_react_simple.exec(src,uid,post,"rt") 1310 + else 1311 + queries.post_react_cancel.exec(src,uid,post,"rt") 1312 + end 1313 + end]; 1314 + post_like = [terra( 1315 + src: &lib.store.source, 1316 + uid: uint64, 1317 + post: uint64, 1318 + undo: bool 1319 + ): {} 1320 + if not undo then 1321 + queries.post_react_simple.exec(src,uid,post,"like") 1322 + else 1323 + queries.post_react_cancel.exec(src,uid,post,"like") 1324 + end 1325 + end]; 1326 + post_liked_uid = [terra( 1327 + src: &lib.store.source, 1328 + uid: uint64, 1329 + post: uint64 1330 + ): bool 1331 + var q = queries.post_reacts_fetch_uid.exec(src,uid,post,'like') 1332 + if q.sz > 0 then q:free() return true end 1333 + return false 1334 + end]; 1218 1335 1219 1336 timeline_instance_fetch = [terra(src: &lib.store.source, rg: lib.store.range) 1220 1337 var r = pqr { sz = 0 } 1221 1338 var A,B,C,D = rg:matrix() -- :/ 1222 1339 r = queries.timeline_instance_fetch.exec(src,A,B,C,D) 1223 1340 1224 1341 var ret: lib.mem.ptr(lib.mem.ptr(lib.store.post)) ret:init(r.sz)
Modified backend/schema/pgsql.sql from [72a3e65e6e] to [347a4ab533].
82 82 kind smallint, -- e.g. follow, block, mute 83 83 84 84 primary key (relator, relatee, kind) 85 85 ); 86 86 87 87 create table parsav_acts ( 88 88 id bigint primary key default (1+random()*(2^63-1))::bigint, 89 - kind text not null, -- like, react, so on 89 + kind text not null, -- like, rt, react, so on 90 90 time timestamp not null default now(), 91 91 actor bigint references parsav_actors(id) 92 92 on delete cascade, 93 - subject bigint -- may be post or act, depending on kind 93 + subject bigint, -- may be post or act, depending on kind 94 + body text -- emoji, if react 94 95 ); 95 96 96 97 create table parsav_log ( 97 98 -- accesses are tracked for security & sending delete acts 98 99 id bigint primary key default (1+random()*(2^63-1))::bigint, 99 100 time timestamp not null default now(), 100 101 actor bigint references parsav_actors(id)
Modified config.lua from [5a4f5a8d5b] to [8efc0b984f].
53 53 -- the damn things before compiling (also making the binary smaller) 54 54 {'style.css', 'text/css'}; 55 55 {'live.js', 'text/javascript'}; -- rrrrrrrr 56 56 {'default-avatar.webp', 'image/webp'}; -- needs inkscape-exclusive svg features 57 57 {'padlock.svg', 'image/svg+xml'}; 58 58 {'warn.svg', 'image/svg+xml'}; 59 59 {'query.webp', 'image/webp'}; 60 + {'heart.webp', 'image/webp'}; 61 + {'retweet.webp', 'image/webp'}; 60 62 }; 61 63 default_ui_accent = tonumber(default('parsav_ui_default_accent',323)); 62 64 } 63 65 if os.getenv('parsav_let_me_be_an_idiot') == "i know what i'm doing" then 64 66 conf.braingeniousmode = true -- SOUND GENERAL QUARTERS 65 67 end 66 68 if u.ping '.fslckout' or u.ping '_FOSSIL_' then
Modified doc/load.lua from [68f6a9a498] to [783a358256].
1 1 local path = ... 2 2 local sources = { 3 3 -- user section 4 4 acl = {title = 'access control lists'}; 5 5 -- admin section 6 6 --overview = {title = 'server overview', priv = 'config'}; 7 7 invocation = {title = 'daemon invocation', priv = 'config'}; 8 + usr = {title = 'user accounting', priv = {'elevate','demote','purge','herald'}}; 9 + --srvcfg = {title = 'server configuration policies', priv = 'config'}; 10 + --discipline = {title = 'disciplinary measures', priv = 'discipline'}; 8 11 --backends = {title = 'storage backends', priv = 'config'}; 9 12 --pgsql = {title = 'pgsql', priv = 'config', parent = 'backends'}; 10 13 } 11 14 12 15 local util = dofile 'common.lua' 13 16 local ingest = function(filename) 14 17 return (util.exec { 'cmark', '--smart', '--unsafe', (path..'/'..filename) }):gsub('\n','')
Added doc/usr.md version [61c1a08ae0].
1 +# user accounting 2 +parsav comes with sophisticated user management tools. you can manage users either from the command line (if you have shell and database access on the host), or using the web UI. both methods will be described in depth here. 3 + 4 +## core concepts 5 +in parsav, users are a subset of *actors,* entities that post content on the fediverse. users are the actors that a given parsav instance publishes. some aspects of parsav administration apply to all actors, meaning that remote actors as well as local users are subject to them; others apply only only to users. in the database, users are distinguished only from other actors in that they are marked as belonging to the local instance. 6 + 7 +every actor has several properties, most of which are fairly standard social media concepts. the first of these is the handle, the username that uniquely identifies a user on an instance and on the fediverse. due to technical limitations in the design of activitypub, handles cannot be changed, and are indelibly associated with a specific account. (as parsav is also intended to have a non-federating mode, and perhaps a mode to federate only with other parsav instances, it should be possible for non-federating instances to allow handle changes at some point.) the second is the nym, or "display name" in twitter parlance, a string that the user can set and change at will to identify himself, and which is displayed before the handle on posts. and of course, a user can give herself a *bio,* a block of markdown-formatted text that is displayed on her profile. neither the display name nor the bio can be changed by administrators. 8 + 9 +actors can also have an *epithet,* a secondary title which is displayed in emphasized fashion after the handle and nym. how you use epithets is entirely up to you; you might use them to indicate specific staff roles ("founder", "admin", "moderator" or so on), or to attach humorous labels to well-known users as a mark of respect (or disrespect). the key thing about the epithet is that it can be set only by administrators (specifically, users with the `herald` power), so bearing an epithet indicates some kind of status recognized by the operator of the instance. (at some point it may also be made possible to change the color of a user's epithet as well, but for now they all display alike.) epithets can also be assigned to members of a chatroom by chatroom staff, but these are scoped to the chatroom and do not display outside of it (nor can they be modified by instance administrators). epithets can be set from the command line with the `parsav actor <xid> bestow <epithet>` command. 10 + 11 +finally, every actor can be assigned a *rank.* this determines their level of authority on the server. when users are given powers, they can only exercise these powers over actors without rank or with lower ranks than their own; for instance, users of rank 2 can affect actors of rank 3 and 4, but not rank 1. rank 1 is special, being the highest possible rank, in that rank 1 users can affect other rank 1 users — this exception is to prevent the situation where a root user forgets their password and nobody else can reset it. however, the best practice is still to reserve rank 1 for the server owner and use lower ranks for all other users. it is important to note that all actors, not just local users, can be given ranks; while remote users cannot exercise power locally, they can be exempted from the power of lower-ranking administrators. rank can be set with the `parsav actor <xid> rank <number>` command; `degrade` will remove an actor's rank 12 + 13 +local users have additional properties, a set of *powers* that governs their ability to use the instance. some of these powers relate only to normal use (logging in, posting, editing one's posts, and so on), and are given to all users by default. others grant power over other users, such as `elevate`, `demote`, and `discipline` (see "powers" for a list). administrative powers can be exercised only over users of lower rank; users without rank cannot make any use of most administrative powers (though `herald` can be granted to allow a user to change his own epithet). local users also have a "quota" (defaulting to 1000) that governs how many posts they can publish each day. if you run a restricted instance that requires invitations to join, users are also assigned a certain number of those invitations, and once they run out this count must be increased by an administrator before they can invite more users. 14 + 15 +## creating administrative accounts 16 +when you first install parsav and initialize its database, there will be no user accounts (unless you're using postgresql and unmanaged authentication) and user self-registration will not be allowed. in order to begin using your new instance, you will need to create yourself a user with which to administrate it. in order to do this, you will need to use the `parsav` utility you used to initialize the database in the first place. (note that depending on your configuration, you may need to run `parsav` as the same user the `parsavd` process runs as for it to be able to connect to the database.) 17 + 18 +initial account creation is handled with the `parsav mkroot <handle>` command, where `<handle>` is the handle of your new user. issuing this command will create a new account with the given name, grant all powers to that account, assign it to rank 1, and generate a password you can use to log in. you can run this command multiple times and create multiple root users if you want, but note that these users have absolute power over the instance, including other root users! in most cases, there should be a single root account beloning to the instance owner, with lower-ranking accounts given out to moderators and other staff. (see "core concepts") 19 + 20 +`mkroot` is purely a convenience function, and is almost identical to the effect of the commands `user <handle> create`, `actor <xid> rank 1`, `user <handle> grant all`, `user <handle> auth pw new`, and `conf set master <handle>` issued in sequence. the only difference is that `mkroot` also `bestow`s a silly title on the new account. 21 + 22 +## creating user accounts 23 +the command `parsav user` is used to manage local user accounts, and you can create a new standard user with the command `parsav user <handle> create`. for example, a user named `@eve` could be created with `parsav user eve create`. this user will have no rank, default rights, default settings, and will not be able to log in. 24 + 25 +you can enable the user to log in by creating a credential for them. for instance, to issue `@eve` a password, you could use the command `parsav user eve auth pw new` or `pw reset`. (the difference between the two is that `reset` deletes existing credentials of that type, whereas `new` creates a new credential without disabling any others). this will generate a temporary password that `@eve` can use to log in. 26 + 27 +you can also create new users through the HTTP interface. log in to your administrative account, navigate to the configuration screen, and click "users" in the menu. (see "emergency recovery" if you don't have this option in your menu.) unfold the "new user" interface, and enter a handle for the new account; it will be created and you will be taken to a page where you can set its properties and create authentication tokens. note that administrators cannot edit other users' display names or bios; these are exclusively the prerogative of the user herself. 28 + 29 +nota bene: if you want to give an account to another user, creating an invitation link is generally the best way of doing it, rather than manually adding a new account. 30 + 31 +## powers 32 +the abilities a user can exercise on a server are governed by their *powers,* a set of flags administrators can set on their accounts. 33 + 34 +these powers are intended for ordinary users, and default to on: 35 + 36 + * **login:** the user can log in to the instance. revoking this power is equivalent to banning the user. 37 + * **visible:** the user and his posts can be seen by other users without navigating directly to his profile page 38 + * **post:** the user can publish new posts 39 + * **shout:** the user is visible on the local timeline 40 + * **propagate:** the user's posts federate 41 + * **artifact:** the user can upload artifacts and attach them to posts. users without this power can still add artifacts uploaded by others to their account, but cannot upload their own. 42 + * **account:** the user can configure their own account and profile 43 + * **edit:** the user can edit her own posts 44 + * **snitch:** the user can submit reports asking for moderator action 45 + 46 +these powers are intended for staff, and default to off: 47 + 48 + * **herald:** the user can change the epithets of lower-ranking actors, grant them badges, or revoke their badges. note that badges can also be restricted such that only heralds of a certain rank can grant or revoke them. 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 + * **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 + * **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 + * **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 + * **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 + * **vacate:** the user can rehabilitate disciplined actors, vacating sanctions, voiding demerits, and issuing temporary reprieves from restrictions. 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. 57 + * **cred:** the user can add, change, and remove the credentials of lower-ranking users (think password resets). 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 + * **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 + 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 + 63 +### recommendations 64 +on smaller servers, it is highly recommended that the `config`, `rebrand`, `purge`, `elevate`, and `demote` powers all rest with a single user. other administrators and moderators should be given `censor`, `discipline`, `vacate`, and possibly `invite` and `herald` depending on your intentions for the site. you should be the only rank-1 user, and other staff should be given rank 2. rank 3 might be useful to limit the damage new staff can do during a "probation period." `herald` and `crier` are useful powers to combine, as they create a "moderator" with powers related mostly to promotion of users and their work. 65 + 66 +on larger servers, it may be necessary to have more levels of administrative abstraction, or even to increase the maximum number of ranks from its default of 10. in this case, certain exceptional powers such as `rebrand` and `purge` should still remain exclusively with the founder, but it may be necessary to (carefully!) apportion out access to powers like `elevate` and `demote`. it may also be desirable to have a broader class of less-trusted moderators who can take minimally destructive measures on their own (say, `censor` and `herald`) to filter through the bulk of reports, with a smaller corps of highly trusted commissars who have powers like `discipline` and `vacate` to handle the small number of reports that censors believe deserve their attention. 67 + 68 +in both cases, it's very, very important to keep in mind that 99% of community management is social. parsav tries to provide you with effective tools for when use of force becomes unavoidable, but most of the time a good community leader can accomplish his goals with words alone (remember, IRC has none of this fancy shit, and they manage just fine most of the time!). apart from those relatively rare cases where you are faced with true bad-faith actors (in which cases immediate brutality is the only solution), a community can be handled effectively with just with judicious use of symbolic measures like rank, badges, and epithets. a gentle indication that a high-status user disapproves of her conduct is often all it takes to convince a lower-status user who truly cares about her community to shape up. all the power in the world won't give you a drop of authority, and if you're new to running communities, you may be surprised how much authority you can endow other members with without giving them anything besides maybe a fancy title (though even that is just a convenience) so long as the people in your community like, trust, and respect you. 69 + 70 +and if your users don't respect you, you might as well pack up right now. 71 + 72 +## emergencies 73 +shit happens. sometimes this shit results in getting locked out of your own instance. if so, don't panic quite yet. as long as you can get shell access to the host to run the `parsav` utility, you can resolve the situation. (note that `parsavd` does not need to be running to use commands that control the database, and for some backends such as sqlite `parsavd` may need to be shut down first.) 74 + 75 +### locked out 76 +if you are locked out of your administrator account, the fix is simple, as long as you can modify the underlying database: the `parsav` utility does not use instance credentials, but rather directly modifies the DB and sends IPC signals through the kernel. if you're locked out because you've forgotten your password or all your credentials have been deleted somehow, just issue yourself a new temporary password like you would for any other user, with the `parsav user <handle> auth pw reset` command. 77 + 78 +### missing privileges 79 +if you've been stripped of the `login` privilege by a bug or a rogue admin, you can restore it with `parsav user <handle> grant login`, and it may be worthwhile to issue a `revoke demote` to keep that rogue admin from immediately locking you out again. keep in mind that this won't affect sanctions that have been issued against your account; see below for these. 80 + 81 +### sanctions 82 +users with the `discipline` privilege cannot change user powers outright, but can issue sanctions that temporarily limit these powers in various ways, for instance preventing a user from posting for a few hours until they've cooled down. users with `discipline` can only affect users of lower rank unless they're rank 1, in which case they can affect all users. if you've fallen afoul of one of these users and need to get your instance back, you'll need to vacate all the sanctions against your account. this can be done with the `parsav actor <xid> sanction all vacate` command. alternately, you can list individual sanctions with `sanction`, and then delete them individually with `sanction <sid> vacate`. 83 + 84 +### lost account 85 +if your account has been completely deleted, rather than just suspended, things are decidedly more serious. everything associated with your account — posts, media, circles, relationships, all of it — is gone, irreversibly, unless you have a database backup around somewhere. (the `purge` power is so named because it is *serious business,* to be treated as the equivalent of a concealed carry permit — you should give it out to other users only out of specific justified need in exceptional circumstances, and revoke it proactively when it is no longer absolutely necessary, rather than as punishment for misuse. hopefully you have now learned this lesson.)
Modified makefile from [eedbd28993] to [5c5158676d].
1 1 dl = git 2 2 dbg-flags = $(if $(dbg),-g) 3 3 4 -images = static/default-avatar.webp static/query.webp 4 +images = static/default-avatar.webp static/query.webp static/heart.webp static/retweet.webp 5 5 #$(addsuffix .webp, $(basename $(wildcard static/*.svg))) 6 6 styles = $(addsuffix .css, $(basename $(wildcard static/*.scss))) 7 7 8 8 parsav parsavd: parsav.t config.lua pkgdata.lua $(images) $(styles) 9 9 terra $(dbg-flags) $< 10 10 parsav.o parsavd.o: parsav.t config.lua pkgdata.lua $(images) $(styles) 11 11 env parsav_link=no terra $(dbg-flags) $<
Modified mgtool.t from [ee3dfa15a8] to [4f69a41277].
385 385 root.handle = cfmode.arglist(0) 386 386 var epithets = array( 387 387 'root', 'god', 'regional jehovah', 'titan king', 388 388 'king of olympus', 'cyberpharaoh', 'electric ellimist', 389 389 "rampaging c'tan", 'deathless tweetlord', 'postmaster', 390 390 'faerie queene', 'lord of the posts', 'ruthless cybercrat', 391 391 'general secretary', 'commissar', 'kwisatz haderach', 392 - 'dedicated hyperturing' 392 + 'dedicated hyperturing', 'grand inquisitor', 'reverend mother', 393 + 'cyberpope', 'verified®', 'patent pending' 393 394 -- feel free to add more 394 395 ) 395 396 root.epithet = epithets[lib.crypt.random(intptr,0,[epithets.type.N])] 396 397 root.rights.powers:fill() -- grant omnipotence 397 398 root.rights.rank = 1 398 399 var ruid = dlg:actor_create(&root) 399 400 dlg:conf_set('master',root.handle)
Modified render/conf/users.t from [09fa445b20] to [4bed391611].
95 95 then sanitized = pstr {ptr='', ct=0} 96 96 else sanitized = lib.html.sanitize(cs(user.ptr.epithet),true) 97 97 end 98 98 cinp:lpush('<div class="elem"><label for="epithet">epithet</label><input type="text" id="epithet" name="epithet" value="'):ppush(sanitized):lpush('"></div>') 99 99 if user.ptr.epithet ~= nil then sanitized:free() end 100 100 end 101 101 if co.who.rights.powers.invite() or co.who.rights.powers.discipline() then 102 - var min = 0 102 + var min: uint32 = 0 103 103 if not (co.who.rights.powers.discipline() or 104 104 co.who.rights.powers.demote() and co.who.rights.powers.invite()) 105 105 then min = user.ptr.rights.invites end 106 - var max = co.srv.cfg.maxinvites 106 + var max: uint32 = co.srv.cfg.maxinvites 107 107 if not co.who.rights.powers.invite() then max = user.ptr.rights.invites end 108 108 109 109 push_num_field(cinp, 'invites', 'invites', min, max, user.ptr.rights.invites, false) 110 110 end 111 + if co.who.rights.powers.elevate() or co.who.rights.powers.demote() then 112 + var max: uint32 = 5000 113 + if not co.who.rights.powers.elevate() then max = user.ptr.rights.quota end 114 + var min: uint32 = 0 115 + if not co.who.rights.powers.demote() then min = user.ptr.rights.quota end 116 + 117 + push_num_field(cinp, 'quota', 'quota', min, max, user.ptr.rights.quota, user.ptr.id == co.who.id and co.who.rights.rank ~= 1) 118 + end 111 119 cinp:lpush('</div><div class="elem"><div class="check-panel">') 112 120 113 121 if user.ptr.id ~= co.who.id and 114 122 ((user.ptr.rights.rank == 0 and co.who.rights.powers.elevate()) or 115 123 (user.ptr.rights.rank > 0 and co.who.rights.powers.demote())) then 116 124 push_checkbox(&cinp, 'staff', pstr.null(), 'site staff member', user.ptr.rights.rank > 0, true, pstr.null()) 117 125 end ................................................................................ 150 158 var unym: lib.str.acc unym:init(64) 151 159 unym:lpush('<a href="/') 152 160 if user(0).origin ~= 0 then unym:lpush('@') end 153 161 do var sanxid = lib.html.sanitize(user(0).xid, true) 154 162 unym:ppush(sanxid) 155 163 sanxid:free() end 156 164 unym:lpush('" class="id">') 157 - lib.render.nym(user.ptr,0,&unym) 165 + lib.render.nym(user.ptr,0,&unym,false) 158 166 unym:lpush('</a>') 159 167 var pg = data.view.conf_user_ctl { 160 168 name = unym:finalize(); 161 169 inputcontent = cinpp; 162 170 linkcontent = clnkp; 163 171 } 164 172 var ret = pg:tostr() ................................................................................ 202 210 if usr.rights.rank ~= 0 then 203 211 ulst:lpush('<span class="regalia">') 204 212 regalia(&ulst, usr.rights.rank) 205 213 ulst:lpush('</span>') 206 214 end 207 215 if co.who:overpowers(usr) then 208 216 ulst:lpush('<a class="id" href="users/'):push(&idbuf[0],idlen):lpush('">') 209 - lib.render.nym(usr, 0, &ulst) 217 + lib.render.nym(usr, 0, &ulst, false) 210 218 ulst:lpush('</a></li>') 211 219 else 212 220 ulst:lpush('<span class="id">') 213 - lib.render.nym(usr, 0, &ulst) 221 + lib.render.nym(usr, 0, &ulst, false) 214 222 ulst:lpush('</span></li>') 215 223 end 216 224 ::skip::end 217 225 ulst:lpush('</ul>') 218 226 return ulst:finalize() 219 227 end 220 228 do return pstr.null() end
Modified render/docpage.t from [148acf7303] to [97c704e199].
67 67 68 68 local terra 69 69 pushbranches(list: &lib.str.acc, idx: intptr, ps: lib.store.powerset): {} 70 70 var [pages] = array([allpages]) 71 71 var started = false 72 72 for i=0,[pages.type.N] do 73 73 if pages[i].parent == idx+1 and (pages[i].priv:sz() == 0 or 74 - (ps and pages[i].priv) == pages[i].priv) then 74 + (ps and pages[i].priv):sz() ~= 0) then 75 75 if not started then 76 76 started = true 77 77 list:lpush('<ul>') 78 78 end 79 79 list:lpush('<li><a href="/doc/'):rpush(pages[i].name):lpush('">') 80 80 :rpush(pages[i].title):lpush('</a>') 81 81 pushbranches(list, i, ps)
Modified render/nym.t from [74775ce158] to [ea921b8ffe].
1 1 -- vim: ft=terra 2 2 local pstr = lib.str.t 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_nym(who: &lib.store.actor, scope: uint64, tgt: &lib.str.acc) 8 +render_nym(who: &lib.store.actor, scope: uint64, tgt: &lib.str.acc, minimal: bool) 9 9 var acc: lib.str.acc 10 10 var n: &lib.str.acc 11 11 if tgt ~= nil then n = tgt else 12 12 n = &acc 13 13 n:init(128) 14 14 end 15 15 var xidsan = lib.html.sanitize(cs(who.xid),false) ................................................................................ 18 18 n:lpush('<span class="nym">'):ppush(nymsan) 19 19 :lpush('</span> [<span class="handle">'):ppush(xidsan) 20 20 :lpush('</span>]') 21 21 nymsan:free() 22 22 else n:lpush('<span class="handle">'):ppush(xidsan):lpush('</span>') end 23 23 xidsan:free() 24 24 25 - if who.epithet ~= nil then 26 - var episan = lib.html.sanitize(cs(who.epithet),false) 27 - n:lpush('<span class="epithet">'):ppush(episan):lpush('</span>') 28 - episan:free() 25 + if not minimal then 26 + if who.epithet ~= nil then 27 + var episan = lib.html.sanitize(cs(who.epithet),false) 28 + n:lpush('<span class="epithet">'):ppush(episan):lpush('</span>') 29 + episan:free() 30 + end 29 31 end 30 32 31 33 -- TODO: if scope == chat room then lookup titles in room member db 32 34 if tgt == nil then 33 35 return n:finalize() 34 36 else return pstr.null() end 35 37 end 36 38 37 39 return render_nym
Modified render/profile.t from [5d5ed1c86e] to [08f3a58ce1].
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 36 if actor.bio ~= nil then 37 37 bio = lib.smackdown.html(cs(actor.bio)) 38 38 end 39 - var fullname = lib.render.nym(actor,0,nil) defer fullname:free() 39 + var fullname = lib.render.nym(actor,0,nil,false) defer fullname:free() 40 40 var comments: lib.str.acc comments:init(64) 41 41 -- this is really more what epithets are for, i think 42 42 --if actor.rights.rank > 0 then comments:lpush('<li>staff member</li>') end 43 + if co.srv.cfg.master == actor.id then 44 + comments:lpush('<li style="--co:-70">founder</li>') 45 + end 43 46 if co.aid ~= 0 and actor.rights.rank ~= 0 then 44 47 if co.who:outranks(actor) then 45 48 comments:lpush('<li style="--co:50">underling</li>') 46 49 elseif actor:outranks(co.who) then 47 50 comments:lpush('<li style="--co:-50">outranks you</li>') 48 51 end 49 52 end
Modified render/tweet-page.t from [005ba03599] to [8729ddd689].
31 31 var livetime = co.srv:thread_latest_arrival_calc(p.id) 32 32 33 33 var pg: lib.str.acc pg:init(256) 34 34 lib.render.tweet(co, p, &pg) 35 35 36 36 if co.aid ~= 0 then 37 37 pg:lpush('<form class="action-bar" method="post">') 38 - var liked = false -- FIXME 39 - var rtd = false 40 - if not liked 38 + if not co.srv:post_liked_uid(co.who.id, p.id) 41 39 then pg:lpush('<button class="pos" name="act" value="like">like</button>') 42 40 else pg:lpush('<button class="neg" name="act" value="dislike">dislike</button>') 43 41 end 44 - if not rtd 45 - then pg:lpush('<button class="pos" name="act" value="rt">retweet</button>') 46 - else pg:lpush('<button class="neg" name="act" value="unrt">detweet</button>') 47 - end 42 + pg:lpush('<button class="pos" name="act" value="rt">retweet</button>') 48 43 if p.author == co.who.id then 49 44 pg:lpush('<a class="button" href="/post/'):rpush(path(1)):lpush('/edit">edit</a><a class="neg button" href="/post/'):rpush(path(1)):lpush('/del">delete</a>') 50 45 end 51 46 -- TODO list user's chosen reaction emoji 52 47 pg:lpush('</form>') 53 48 54 49 end
Modified render/tweet.t from [43aca48007] to [ee058ed0af].
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 +local terra 8 +push_promo_header(co: &lib.srv.convo, acc: &lib.str.acc, rter: &lib.store.actor, rid: uint64) 9 + acc:lpush('<div class="lede"><div class="promo"><img src="'):push(rter.avatar,0):lpush('"><a href="/') 10 + if rter.origin ~= 0 then acc:lpush('@') end 11 + acc:push(rter.xid,0):lpush('" class="username">') 12 + lib.render.nym(rter, 0, acc, true) 13 + acc:lpush('</a> retweeted</div>') 14 + if co.who.id == rter.id then 15 + acc:lpush('<a href="/post/'):shpush(rid):lpush('/del" class="del">✖</a>') 16 + end 17 +end 18 + 7 19 local terra 8 20 render_tweet(co: &lib.srv.convo, p: &lib.store.post, acc: &lib.str.acc) 9 - var author: &lib.store.actor 21 + var author: &lib.store.actor = nil 22 + var retweeter: &lib.store.actor = nil 10 23 for j = 0, co.actorcache.top do 11 - if p.author == co.actorcache(j).ptr.id then 12 - author = co.actorcache(j).ptr 24 + if p.author == co.actorcache(j).ptr.id then author = co.actorcache(j).ptr end 25 + if p.rtdby == co.actorcache(j).ptr.id then retweeter = co.actorcache(j).ptr end 26 + if author ~= nil and (p.rtdby == 0 or retweeter ~= nil) then 13 27 goto foundauth 14 28 end 15 29 end 16 - author = co.actorcache:insert(co.srv:actor_fetch_uid(p.author)).ptr 30 + if author == nil then 31 + author = co.actorcache:insert(co.srv:actor_fetch_uid(p.author)).ptr 32 + end 33 + if p.rtdby ~= 0 and retweeter == nil then 34 + retweeter = co.actorcache:insert(co.srv:actor_fetch_uid(p.rtdby)).ptr 35 + end 17 36 18 37 ::foundauth:: 19 38 var avistr: lib.str.acc if author.origin == 0 then 20 39 avistr:compose('/avi/',author.handle) 21 40 end 22 41 var timestr: int8[26] lib.osclock.ctime_r(&p.posted, ×tr[0]) 23 42 24 - var bhtml = lib.smackdown.html([lib.mem.ptr(int8)] {ptr=p.body,ct=0}) defer bhtml:free() 43 + var bhtml = lib.smackdown.html([lib.mem.ptr(int8)] {ptr=p.body,ct=0}) 44 + defer bhtml:free() 25 45 26 46 var idbuf: int8[lib.math.shorthand.maxlen] 27 47 var idlen = lib.math.shorthand.gen(p.id, idbuf) 28 48 var permalink: lib.str.acc permalink:compose('/post/',{idbuf,idlen}) 29 - var fullname = lib.render.nym(author,0,nil) defer fullname:free() 49 + var fullname = lib.render.nym(author,0,nil, false) defer fullname:free() 30 50 var tpl = data.view.tweet { 31 51 text = bhtml; 32 52 subject = cs(lib.coalesce(p.subject,'')); 33 53 nym = fullname; 34 54 when = cs(×tr[0]); 35 55 avatar = cs(author.avatar); 36 56 acctlink = cs(author.xid); 37 57 permalink = permalink:finalize(); 38 - attr = '' 58 + attr = pstr{'',0}; 59 + stats = pstr{'',0}; 39 60 } 61 + if p.rts + p.likes > 0 then 62 + var s: lib.str.acc s:init(128) 63 + 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 66 + s:lpush('</div>') 67 + tpl.stats = s:finalize() 68 + end 40 69 41 70 var attrbuf: int8[32] 42 71 if p.accent ~= -1 and p.accent ~= co.ui_hue then 43 72 var hdecbuf: int8[21] 44 73 var hdec = lib.math.decstr(p.accent, &hdecbuf[20]) 45 74 lib.str.cpy(&attrbuf[0], ' style="--hue:') 46 75 lib.str.cpy(&attrbuf[14], hdec) 47 76 var len = &hdecbuf[20] - hdec 48 77 lib.str.cpy(&attrbuf[14] + len, '"') 49 78 tpl.attr = &attrbuf[0] 50 79 end 51 80 52 81 defer tpl.permalink:free() 53 - if acc ~= nil then tpl:append(acc) return [lib.mem.ptr(int8)]{ptr=nil,ct=0} end 54 - var txt = tpl:tostr() 55 - return txt 82 + if acc ~= nil then 83 + if retweeter ~= nil then push_promo_header(co, acc, retweeter, p.rtact) end 84 + tpl:append(acc) 85 + if retweeter ~= nil then acc:lpush('</div>') end 86 + if p.rts + p.likes > 0 then tpl.stats:free() end 87 + return [lib.mem.ptr(int8)]{ptr=nil,ct=0} 88 + end 89 + 90 + if retweeter ~= nil then 91 + var rta: lib.str.acc rta:init(512) 92 + push_promo_header(co, &rta, retweeter, p.rtact) 93 + tpl:append(&rta) rta:lpush('</div>') 94 + return rta:finalize() 95 + else 96 + var txt = tpl:tostr() 97 + if p.rts + p.likes > 0 then tpl.stats:free() end 98 + return txt 99 + end 56 100 end 57 101 return render_tweet
Modified route.t from [2f7668c3df] to [666eb021ed].
222 222 return 223 223 else goto badop end 224 224 end 225 225 else goto badurl end 226 226 end 227 227 228 228 if meth == method.post then 229 - var replytext = co:ppostv('post') 230 - var acl = co:ppostv('acl') 231 - var subj = co:ppostv('subject') 232 - if not acl then acl = lib.str.plit 'all' end 233 - if not replytext then goto badop end 234 - 235 - var reply = lib.store.post { 236 - author = co.who.id, parent = pid; 237 - subject = subj.ptr, acl = acl.ptr, body = replytext.ptr; 238 - } 229 + var act = co:ppostv('act') 230 + if act:cmp(lib.str.plit 'like') and not co.srv:post_liked_uid(co.who.id,pid) then 231 + co.srv:post_like(co.who.id, pid, false) 232 + post.ptr.likes = post.ptr.likes + 1 233 + elseif act:cmp(lib.str.plit 'dislike') and co.srv:post_liked_uid(co.who.id,pid) then 234 + co.srv:post_like(co.who.id, pid, true) 235 + post.ptr.likes = post.ptr.likes - 1 236 + elseif act:cmp(lib.str.plit 'rt') then 237 + co.srv:post_retweet(co.who.id, pid, false) 238 + post.ptr.rts = post.ptr.rts + 1 239 + elseif act:cmp(lib.str.plit 'post') then 240 + var replytext = co:ppostv('post') 241 + var acl = co:ppostv('acl') 242 + var subj = co:ppostv('subject') 243 + if not acl then acl = lib.str.plit 'all' end 244 + if not replytext then goto badop end 245 + 246 + var reply = lib.store.post { 247 + author = co.who.id, parent = pid; 248 + subject = subj.ptr, acl = acl.ptr, body = replytext.ptr; 249 + } 239 250 240 - reply:publish(co.srv) 251 + reply:publish(co.srv) 252 + else goto badop end 241 253 end 242 254 243 255 lib.render.tweet_page(co, path, post.ptr) 244 256 do return end 245 257 246 258 ::badurl:: do co:complain(404, 'invalid URL', 'this URL does not reference extant content or functionality') return end 247 259 ::badop :: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end ................................................................................ 257 269 path(1):cmp(lib.str.lit 'emoji') 258 270 ) then goto nopriv 259 271 260 272 elseif not co.who.rights.powers.rebrand() and ( 261 273 path(1):cmp(lib.str.lit 'brand') 262 274 ) then goto nopriv 263 275 264 - elseif not co.who.rights.powers.acct() and ( 276 + elseif not co.who.rights.powers.account() and ( 265 277 path(1):cmp(lib.str.lit 'profile') or 266 278 path(1):cmp(lib.str.lit 'acct') 267 279 ) then goto nopriv 268 280 269 281 elseif not co.who.rights.powers:affect_users() and ( 270 282 path(1):cmp(lib.str.lit 'users') 271 283 ) then goto nopriv end
Modified session.t from [78b2aad470] to [c79e9ffb10].
2 2 -- sessions are implemented so as to avoid any local data storage. they 3 3 -- are tracked by storing an encrypted cookie which contains an authid, 4 4 -- a login epoch time, and a truncated hmac code authenticating both, all 5 5 -- encoded using Shorthand. we need functions to generate and parse these 6 6 7 7 local m = { 8 8 maxlen = lib.math.shorthand.maxlen*3 + 2; 9 - maxage = 2 * 60 * 60; -- 2 hours 9 + maxage = 16 * 60 * 60; -- 16 hours 10 10 cookiename = 'auth'; 11 11 } 12 12 13 13 terra m.cookie_gen(secret: lib.mem.ptr(int8), authid: uint64, time: uint64, out: &int8): intptr 14 14 var ptr = out 15 15 ptr = ptr + lib.math.shorthand.gen(authid, ptr) 16 16 @ptr = @'.' ptr = ptr + 1
Modified srv.t from [34fad9fa1a] to [e0d52a1828].
10 10 credmgd: bool 11 11 maxupsz: intptr 12 12 instance: lib.mem.ptr(int8) 13 13 overlord: &srv 14 14 ui_hue: uint16 15 15 nranks: uint16 16 16 maxinvites: uint16 17 + master: uint64 17 18 } 18 19 local struct srv { 19 20 sources: lib.mem.ptr(lib.store.source) 20 21 webmgr: lib.net.mg_mgr 21 22 webcon: &lib.net.mg_connection 22 23 cfg: cfgcache 23 24 id: rawstring ................................................................................ 800 801 end 801 802 smode:free() 802 803 end 803 804 804 805 self.ui_hue = self:cfint('ui-accent',config.default_ui_accent) 805 806 self.nranks = self:cfint('user-ranks',10) 806 807 self.maxinvites = self:cfint('max-invites',64) 808 + 809 + var webmaster = self.overlord:conf_get('master') 810 + if webmaster:ref() then defer webmaster:free() 811 + var wma = self.overlord:actor_fetch_xid(webmaster) 812 + if not wma then 813 + lib.warn('the webmaster specified in the configuration store does not seem to exist or is not known to this instance; preceding as if no master defined. if the master is a remote user, you can rectify this with the `actor ',{webmaster.ptr,webmaster.ct},' instantiate` and `conf refresh` commands') 814 + else 815 + self.master = wma(0).id 816 + wma:free() 817 + end 818 + end 807 819 end 808 820 809 821 return { 810 822 overlord = srv; 811 823 convo = convo; 812 824 route = route; 813 825 secmode = secmode; 814 826 }
Added static/heart.svg version [c2edd21438].
1 +<?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 +<!-- Created with Inkscape (http://www.inkscape.org/) --> 3 + 4 +<svg 5 + xmlns:dc="http://purl.org/dc/elements/1.1/" 6 + xmlns:cc="http://creativecommons.org/ns#" 7 + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 8 + xmlns:svg="http://www.w3.org/2000/svg" 9 + xmlns="http://www.w3.org/2000/svg" 10 + xmlns:xlink="http://www.w3.org/1999/xlink" 11 + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 12 + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 13 + width="20" 14 + height="20" 15 + viewBox="0 0 5.2916668 5.2916668" 16 + version="1.1" 17 + id="svg8" 18 + inkscape:version="0.92.4 (5da689c313, 2019-01-14)" 19 + sodipodi:docname="heart.svg" 20 + inkscape:export-filename="/home/lexi/dev/parsav/static/heart.png" 21 + inkscape:export-xdpi="406.39999" 22 + inkscape:export-ydpi="406.39999"> 23 + <defs 24 + id="defs2"> 25 + <linearGradient 26 + id="linearGradient1395" 27 + inkscape:collect="always"> 28 + <stop 29 + id="stop1391" 30 + offset="0" 31 + style="stop-color:#ff1616;stop-opacity:1" /> 32 + <stop 33 + id="stop1393" 34 + offset="1" 35 + style="stop-color:#ff1d1d;stop-opacity:0" /> 36 + </linearGradient> 37 + <linearGradient 38 + inkscape:collect="always" 39 + id="linearGradient1383"> 40 + <stop 41 + style="stop-color:#980000;stop-opacity:1;" 42 + offset="0" 43 + id="stop1379" /> 44 + <stop 45 + style="stop-color:#980000;stop-opacity:0;" 46 + offset="1" 47 + id="stop1381" /> 48 + </linearGradient> 49 + <linearGradient 50 + inkscape:collect="always" 51 + id="linearGradient832"> 52 + <stop 53 + style="stop-color:#ffcfcf;stop-opacity:1;" 54 + offset="0" 55 + id="stop828" /> 56 + <stop 57 + style="stop-color:#ffcfcf;stop-opacity:0;" 58 + offset="1" 59 + id="stop830" /> 60 + </linearGradient> 61 + <radialGradient 62 + inkscape:collect="always" 63 + xlink:href="#linearGradient832" 64 + id="radialGradient834" 65 + cx="3.2286437" 66 + cy="286.62921" 67 + fx="3.2286437" 68 + fy="286.62921" 69 + r="1.0866126" 70 + gradientTransform="matrix(1.8608797,0.8147617,-0.38242057,0.87343168,106.71446,33.692223)" 71 + gradientUnits="userSpaceOnUse" /> 72 + <filter 73 + inkscape:collect="always" 74 + style="color-interpolation-filters:sRGB" 75 + id="filter1356" 76 + x="-0.044539396" 77 + width="1.0890788" 78 + y="-0.04671235" 79 + height="1.0934247"> 80 + <feGaussianBlur 81 + inkscape:collect="always" 82 + stdDeviation="0.040330888" 83 + id="feGaussianBlur1358" /> 84 + </filter> 85 + <radialGradient 86 + inkscape:collect="always" 87 + xlink:href="#linearGradient1383" 88 + id="radialGradient1385" 89 + cx="4.1787109" 90 + cy="286.89261" 91 + fx="4.1787109" 92 + fy="286.89261" 93 + r="1.2260786" 94 + gradientTransform="matrix(1.7016464,0,0,1.6348586,-2.9319775,-182.10895)" 95 + gradientUnits="userSpaceOnUse" /> 96 + <radialGradient 97 + inkscape:collect="always" 98 + xlink:href="#linearGradient1395" 99 + id="radialGradient1389" 100 + gradientUnits="userSpaceOnUse" 101 + gradientTransform="matrix(0.66230313,-1.6430738,1.0154487,0.40931507,-290.06307,177.39489)" 102 + cx="4.02069" 103 + cy="287.79269" 104 + fx="4.02069" 105 + fy="287.79269" 106 + r="1.0866126" /> 107 + </defs> 108 + <sodipodi:namedview 109 + id="base" 110 + pagecolor="#181818" 111 + bordercolor="#666666" 112 + borderopacity="1.0" 113 + inkscape:pageopacity="0" 114 + inkscape:pageshadow="2" 115 + inkscape:zoom="11.2" 116 + inkscape:cx="13.645085" 117 + inkscape:cy="22.307499" 118 + inkscape:document-units="mm" 119 + inkscape:current-layer="layer1" 120 + showgrid="false" 121 + units="px" 122 + inkscape:window-width="949" 123 + inkscape:window-height="1028" 124 + inkscape:window-x="963" 125 + inkscape:window-y="44" 126 + inkscape:window-maximized="0" 127 + showguides="false" 128 + fit-margin-top="0" 129 + fit-margin-left="0" 130 + fit-margin-right="0" 131 + fit-margin-bottom="0" /> 132 + <metadata 133 + id="metadata5"> 134 + <rdf:RDF> 135 + <cc:Work 136 + rdf:about=""> 137 + <dc:format>image/svg+xml</dc:format> 138 + <dc:type 139 + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> 140 + <dc:title></dc:title> 141 + </cc:Work> 142 + </rdf:RDF> 143 + </metadata> 144 + <g 145 + inkscape:label="Layer 1" 146 + inkscape:groupmode="layer" 147 + id="layer1" 148 + transform="translate(-2.9526324,-283.47435)"> 149 + <path 150 + sodipodi:type="inkscape:offset" 151 + inkscape:radius="0.14186843" 152 + inkscape:original="M 3.625 286.55273 C 3.0632316 286.5586 3.0996094 286.98633 3.0996094 286.98633 C 3.0996094 286.98633 3.0113255 287.32746 3.4589844 287.69727 C 3.9066436 288.06708 4.1796875 288.625 4.1796875 288.625 C 4.1796875 288.625 4.4507783 288.06708 4.8984375 287.69727 C 5.3460971 287.32746 5.2578125 286.98633 5.2578125 286.98633 C 5.2578125 286.98633 5.2941901 286.55873 4.7324219 286.55273 C 4.1706518 286.54711 4.1796875 287.19141 4.1796875 287.19141 C 4.1796875 287.19141 4.1867679 286.54673 3.625 286.55273 z " 153 + style="fill:url(#radialGradient1385);fill-opacity:1;stroke:none;stroke-width:0.12017766px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" 154 + id="path822" 155 + d="m 3.6230469,286.41016 c -0.3169165,0.003 -0.5116816,0.14302 -0.5957031,0.29101 -0.084022,0.14799 -0.068359,0.29688 -0.068359,0.29688 l 0.00391,-0.0469 c 0,0 -0.02958,0.12684 0.011719,0.28711 0.041298,0.16028 0.1506583,0.3669 0.3945312,0.56836 0.4154821,0.34323 0.6835938,0.88086 0.6835938,0.88086 a 0.14188261,0.14188261 0 0 0 0.2539062,0 c 0,0 0.2663147,-0.53776 0.6816406,-0.88086 0.2438734,-0.20146 0.3532329,-0.40808 0.3945313,-0.56836 0.041298,-0.16027 0.011719,-0.28711 0.011719,-0.28711 l 0.00391,0.0469 c 0,0 0.015663,-0.14891 -0.068359,-0.29688 -0.084023,-0.14797 -0.2787972,-0.28763 -0.5957031,-0.29101 -0.2850618,-0.003 -0.4543151,0.15732 -0.5546875,0.32226 -0.100738,-0.16498 -0.2713805,-0.32531 -0.5566406,-0.32226 z" 156 + transform="matrix(2.157964,0,0,2.2461218,-3.4190419,-359.83766)" /> 157 + <path 158 + id="path819" 159 + style="fill:#ff8080;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" 160 + d="m 4.0453212,286.36379 c -0.9660318,-0.83062 -0.7766137,-1.59419 -0.7766137,-1.59419 0,0 -0.075768,-0.96249 1.1365076,-0.97567 1.2122749,-0.0127 1.1933329,1.43711 1.1933329,1.43711 0,0 -0.018944,-1.44975 1.1933326,-1.43711 1.2122754,0.0127 1.1365076,0.97567 1.1365076,0.97567 0,0 0.1894205,0.76357 -0.7766137,1.59419 -0.9660323,0.83065 -1.5532265,2.08431 -1.5532265,2.08431 0,0 -0.5871945,-1.25366 -1.5532268,-2.08431 z" 161 + inkscape:connector-curvature="0" 162 + sodipodi:nodetypes="scccccscs" /> 163 + <path 164 + sodipodi:nodetypes="scccccscs" 165 + inkscape:connector-curvature="0" 166 + d="m 3.4589842,287.69653 c -0.4476589,-0.36981 -0.3598826,-0.70976 -0.3598826,-0.70976 0,0 -0.035111,-0.42851 0.5266574,-0.43438 0.5617679,-0.006 0.5529901,0.63982 0.5529901,0.63982 0,0 -0.00878,-0.64544 0.5529901,-0.63982 0.5617682,0.006 0.5266574,0.43438 0.5266574,0.43438 0,0 0.087777,0.33995 -0.3598826,0.70976 -0.4476592,0.36981 -0.7197649,0.92795 -0.7197649,0.92795 0,0 -0.2721057,-0.55814 -0.7197649,-0.92795 z" 167 + style="fill:url(#radialGradient834);fill-opacity:1;stroke:none;stroke-width:0.12017766px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter1356)" 168 + id="path826" 169 + transform="matrix(2.157964,0,0,2.2461218,-3.4190419,-359.83766)" /> 170 + <path 171 + id="path1387" 172 + style="fill:url(#radialGradient1389);fill-opacity:1;stroke:none;stroke-width:0.12017766px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter1356)" 173 + d="m 3.4589842,287.69653 c -0.4476589,-0.36981 -0.3598826,-0.70976 -0.3598826,-0.70976 0,0 -0.035111,-0.42851 0.5266574,-0.43438 0.5617679,-0.006 0.5529901,0.63982 0.5529901,0.63982 0,0 -0.00878,-0.64544 0.5529901,-0.63982 0.5617682,0.006 0.5266574,0.43438 0.5266574,0.43438 0,0 0.087777,0.33995 -0.3598826,0.70976 -0.4476592,0.36981 -0.7197649,0.92795 -0.7197649,0.92795 0,0 -0.2721057,-0.55814 -0.7197649,-0.92795 z" 174 + inkscape:connector-curvature="0" 175 + sodipodi:nodetypes="scccccscs" 176 + transform="matrix(2.157964,0,0,2.2461218,-3.4190419,-359.83766)" /> 177 + </g> 178 +</svg>
Modified static/live.js from [86bdd64b84] to [682908b4c8].
1 1 /* first things first, we need to scan over the document and see 2 2 * if there are any UI elements unfortunate enough to need 3 3 * interactivity beyond what native HTML+CSS can provide. if so, 4 4 * we attach the appropriate listeners to them. */ 5 5 window.addEventListener('load', function() { 6 + /* social media is less fun when you can't just click on a tweet 7 + * to insta-like or -retweet it. this is unfortunately not possible 8 + * (except in various hideously shitty ways) without javascript. */ 9 + function mk(elt) { return document.createElement(elt); } 10 + function attachButtons() { 11 + document.querySelectorAll('body:not(.post) main div.post').forEach(function(post){ 12 + let url = post.querySelector('.permalink').attributes.getNamedItem('href').value; 13 + function postReq(act,elt) { 14 + fetch(new Request(url, { 15 + method: 'POST', 16 + body: 'act='+act 17 + })).then(function(resp) { 18 + if (resp.ok && resp.status == 200) { 19 + var i = parseInt(elt.innerHTML) 20 + if (isNaN(i)) {i=0} 21 + elt.innerHTML = (i+1).toString() 22 + } 23 + }) 24 + } 25 + 26 + var stats = post.querySelector('.stats'); 27 + if (stats == null) { 28 + /* no stats box; create one */ 29 + var n = mk('div'); 30 + n.classList.add('stats'); 31 + post.appendChild(n); 32 + stats = n 33 + } 34 + function ensureElt(cls, before) { 35 + let s = stats.querySelector('.' + cls); 36 + if (s == null) { 37 + var n = mk('div'); 38 + n.classList.add(cls); 39 + if (before == null) { stats.appendChild(n) } else { 40 + stats.insertBefore(n,stats.querySelector(before)) 41 + } 42 + return n 43 + } else { return s } 44 + } 45 + var like = ensureElt('like', null); 46 + var rt = ensureElt('rt','.like'); 47 + function activate(elt,name) { 48 + elt.addEventListener('click', function(e) { postReq(name,elt) }); 49 + elt.style.setProperty('cursor','pointer'); 50 + } 51 + activate(like,'like'); 52 + activate(rt,'rt'); 53 + }); 54 + } 55 + 6 56 /* update hue-picker background when slider is adjusted */ 7 57 document.querySelectorAll('.color-picker').forEach(function(box) { 8 58 let slider = box.querySelector('[data-color-pick]'); 9 59 box.style.setProperty('--hue', slider.value); 10 60 slider.addEventListener('input', function(e) { 11 61 box.style.setProperty('--hue', e.target.value); 12 62 }); ................................................................................ 38 88 return; 39 89 } 40 90 container._liveLastArrival = newest 41 91 42 92 resp.text().then(function(htmlbody) { 43 93 var parser = new DOMParser(); 44 94 var newdoc = parser.parseFromString(htmlbody,'text/html') 45 - container.innerHTML = newdoc.getElementById(container.id).innerHTML 95 + container.innerHTML = newdoc.getElementById(container.id).innerHTML; 96 + attachButtons(); 46 97 }) 47 98 }) 48 99 }, interv) 49 100 }); 101 + 102 + attachButtons(); 50 103 });
Added static/retweet.svg version [c3fa459a24].
1 +<?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 +<!-- Created with Inkscape (http://www.inkscape.org/) --> 3 + 4 +<svg 5 + xmlns:dc="http://purl.org/dc/elements/1.1/" 6 + xmlns:cc="http://creativecommons.org/ns#" 7 + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 8 + xmlns:svg="http://www.w3.org/2000/svg" 9 + xmlns="http://www.w3.org/2000/svg" 10 + xmlns:xlink="http://www.w3.org/1999/xlink" 11 + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 12 + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 13 + width="20" 14 + height="20" 15 + viewBox="0 0 5.2916664 5.2916665" 16 + version="1.1" 17 + id="svg8" 18 + inkscape:version="0.92.4 (5da689c313, 2019-01-14)" 19 + sodipodi:docname="retweet.svg"> 20 + <defs 21 + id="defs2"> 22 + <linearGradient 23 + id="linearGradient2866" 24 + inkscape:collect="always"> 25 + <stop 26 + id="stop2862" 27 + offset="0" 28 + style="stop-color:#9a57ff;stop-opacity:1" /> 29 + <stop 30 + id="stop2864" 31 + offset="1" 32 + style="stop-color:#ab73ff;stop-opacity:0" /> 33 + </linearGradient> 34 + <linearGradient 35 + inkscape:collect="always" 36 + id="linearGradient1966"> 37 + <stop 38 + style="stop-color:#f0e7ff;stop-opacity:1;" 39 + offset="0" 40 + id="stop1962" /> 41 + <stop 42 + style="stop-color:#f0e7ff;stop-opacity:0;" 43 + offset="1" 44 + id="stop1964" /> 45 + </linearGradient> 46 + <linearGradient 47 + inkscape:collect="always" 48 + id="linearGradient1468"> 49 + <stop 50 + style="stop-color:#9955ff;stop-opacity:1;" 51 + offset="0" 52 + id="stop1464" /> 53 + <stop 54 + style="stop-color:#9955ff;stop-opacity:0;" 55 + offset="1" 56 + id="stop1466" /> 57 + </linearGradient> 58 + <linearGradient 59 + inkscape:collect="always" 60 + id="linearGradient1403"> 61 + <stop 62 + style="stop-color:#ccaaff;stop-opacity:1;" 63 + offset="0" 64 + id="stop1399" /> 65 + <stop 66 + style="stop-color:#ccaaff;stop-opacity:0;" 67 + offset="1" 68 + id="stop1401" /> 69 + </linearGradient> 70 + <linearGradient 71 + id="linearGradient1395" 72 + inkscape:collect="always"> 73 + <stop 74 + id="stop1391" 75 + offset="0" 76 + style="stop-color:#ff1616;stop-opacity:1" /> 77 + <stop 78 + id="stop1393" 79 + offset="1" 80 + style="stop-color:#ff1d1d;stop-opacity:0" /> 81 + </linearGradient> 82 + <linearGradient 83 + inkscape:collect="always" 84 + id="linearGradient1383"> 85 + <stop 86 + style="stop-color:#980000;stop-opacity:1;" 87 + offset="0" 88 + id="stop1379" /> 89 + <stop 90 + style="stop-color:#980000;stop-opacity:0;" 91 + offset="1" 92 + id="stop1381" /> 93 + </linearGradient> 94 + <linearGradient 95 + inkscape:collect="always" 96 + id="linearGradient832"> 97 + <stop 98 + style="stop-color:#ffcfcf;stop-opacity:1;" 99 + offset="0" 100 + id="stop828" /> 101 + <stop 102 + style="stop-color:#ffcfcf;stop-opacity:0;" 103 + offset="1" 104 + id="stop830" /> 105 + </linearGradient> 106 + <radialGradient 107 + inkscape:collect="always" 108 + xlink:href="#linearGradient832" 109 + id="radialGradient834" 110 + cx="3.2286437" 111 + cy="286.62921" 112 + fx="3.2286437" 113 + fy="286.62921" 114 + r="1.0866126" 115 + gradientTransform="matrix(1.8608797,0.8147617,-0.38242057,0.87343168,106.71446,33.692223)" 116 + gradientUnits="userSpaceOnUse" /> 117 + <radialGradient 118 + inkscape:collect="always" 119 + xlink:href="#linearGradient1383" 120 + id="radialGradient1385" 121 + cx="4.1787109" 122 + cy="286.89261" 123 + fx="4.1787109" 124 + fy="286.89261" 125 + r="1.2260786" 126 + gradientTransform="matrix(1.7016464,0,0,1.6348586,-2.9319775,-182.10895)" 127 + gradientUnits="userSpaceOnUse" /> 128 + <radialGradient 129 + inkscape:collect="always" 130 + xlink:href="#linearGradient1395" 131 + id="radialGradient1389" 132 + gradientUnits="userSpaceOnUse" 133 + gradientTransform="matrix(0.66230313,-1.6430738,1.0154487,0.40931507,-290.06307,177.39489)" 134 + cx="4.02069" 135 + cy="287.79269" 136 + fx="4.02069" 137 + fy="287.79269" 138 + r="1.0866126" /> 139 + <linearGradient 140 + inkscape:collect="always" 141 + xlink:href="#linearGradient1403" 142 + id="linearGradient1405" 143 + x1="8.3939333" 144 + y1="288.1091" 145 + x2="7.0158253" 146 + y2="287.32819" 147 + gradientUnits="userSpaceOnUse" /> 148 + <filter 149 + inkscape:collect="always" 150 + style="color-interpolation-filters:sRGB" 151 + id="filter2508" 152 + x="-0.24674278" 153 + width="1.4934856" 154 + y="-0.13581935" 155 + height="1.2716388"> 156 + <feGaussianBlur 157 + inkscape:collect="always" 158 + stdDeviation="0.056246184" 159 + id="feGaussianBlur2510" /> 160 + </filter> 161 + <filter 162 + inkscape:collect="always" 163 + style="color-interpolation-filters:sRGB" 164 + id="filter3064" 165 + x="-0.07694713" 166 + width="1.1538943" 167 + y="-0.14115551" 168 + height="1.282311"> 169 + <feGaussianBlur 170 + inkscape:collect="always" 171 + stdDeviation="0.065422039" 172 + id="feGaussianBlur3066" /> 173 + </filter> 174 + <linearGradient 175 + inkscape:collect="always" 176 + xlink:href="#linearGradient2866" 177 + id="linearGradient1533" 178 + gradientUnits="userSpaceOnUse" 179 + x1="8.3939333" 180 + y1="288.1091" 181 + x2="7.0097656" 182 + y2="287.25977" /> 183 + <radialGradient 184 + inkscape:collect="always" 185 + xlink:href="#linearGradient1468" 186 + id="radialGradient1535" 187 + gradientUnits="userSpaceOnUse" 188 + gradientTransform="matrix(1,0,0,1.5198212,0,-149.75763)" 189 + cx="8.525074" 190 + cy="288.10031" 191 + fx="8.525074" 192 + fy="288.10031" 193 + r="0.43142718" /> 194 + <linearGradient 195 + inkscape:collect="always" 196 + xlink:href="#linearGradient1403" 197 + id="linearGradient1537" 198 + gradientUnits="userSpaceOnUse" 199 + x1="8.3939333" 200 + y1="288.1091" 201 + x2="7.0158253" 202 + y2="287.32819" /> 203 + <radialGradient 204 + inkscape:collect="always" 205 + xlink:href="#linearGradient1966" 206 + id="radialGradient1539" 207 + gradientUnits="userSpaceOnUse" 208 + gradientTransform="matrix(1,0,0,1.2339206,0,-67.391253)" 209 + cx="8.7198324" 210 + cy="288.09686" 211 + fx="8.7198324" 212 + fy="288.09686" 213 + r="0.27354568" /> 214 + </defs> 215 + <sodipodi:namedview 216 + id="base" 217 + pagecolor="#181818" 218 + bordercolor="#666666" 219 + borderopacity="1.0" 220 + inkscape:pageopacity="0" 221 + inkscape:pageshadow="2" 222 + inkscape:zoom="7.919596" 223 + inkscape:cx="7.7101412" 224 + inkscape:cy="36.101286" 225 + inkscape:document-units="mm" 226 + inkscape:current-layer="layer1" 227 + showgrid="false" 228 + units="px" 229 + inkscape:window-width="949" 230 + inkscape:window-height="1028" 231 + inkscape:window-x="963" 232 + inkscape:window-y="44" 233 + inkscape:window-maximized="0" 234 + showguides="false" 235 + fit-margin-top="0" 236 + fit-margin-left="0" 237 + fit-margin-right="0" 238 + fit-margin-bottom="0" /> 239 + <metadata 240 + id="metadata5"> 241 + <rdf:RDF> 242 + <cc:Work 243 + rdf:about=""> 244 + <dc:format>image/svg+xml</dc:format> 245 + <dc:type 246 + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> 247 + <dc:title></dc:title> 248 + </cc:Work> 249 + </rdf:RDF> 250 + </metadata> 251 + <g 252 + inkscape:label="Layer 1" 253 + inkscape:groupmode="layer" 254 + id="layer1" 255 + transform="translate(-2.6134661,-283.36966)"> 256 + <g 257 + id="g1452" 258 + transform="matrix(2.0546825,0,0,1.965062,-10.834174,-279.0744)" 259 + style="stroke-width:0.49766773"> 260 + <path 261 + sodipodi:type="inkscape:offset" 262 + inkscape:radius="0.051069338" 263 + inkscape:original="M 7.015625 287.32812 L 6.5898438 287.41211 C 6.5898438 287.41211 6.7325506 288.06384 6.7910156 288.16406 C 6.8494806 288.26426 8.5195312 288.33789 8.5195312 288.33789 L 8.5273438 287.88867 C 8.5273438 287.88867 7.158409 287.98057 7.125 287.88867 C 7.0915917 287.7968 7.015625 287.32812 7.015625 287.32812 z " 264 + style="fill:url(#linearGradient1533);fill-opacity:1;stroke:none;stroke-width:0.13167457px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3064)" 265 + id="path2512" 266 + d="m 7.0058594,287.27734 -0.4257813,0.084 a 0.05107445,0.05107445 0 0 0 -0.041016,0.0625 c 0,0 0.036909,0.16168 0.080078,0.33789 0.021585,0.0881 0.044485,0.17955 0.066406,0.25586 0.021922,0.0763 0.038348,0.13382 0.060547,0.17187 0.016645,0.0285 0.035211,0.0334 0.054687,0.041 0.019476,0.008 0.043217,0.0134 0.070313,0.0195 0.054191,0.0123 0.125713,0.0226 0.2089843,0.0332 0.1665426,0.0212 0.3808629,0.0411 0.59375,0.0566 0.4257743,0.031 0.84375,0.0488 0.84375,0.0488 a 0.05107445,0.05107445 0 0 0 0.052734,-0.0508 l 0.00781,-0.44922 a 0.05107445,0.05107445 0 0 0 -0.054687,-0.0508 c 0,0 -0.3402778,0.0237 -0.6855469,0.0352 -0.1726345,0.006 -0.3476553,0.008 -0.4785156,0.004 -0.06543,-0.002 -0.1194572,-0.006 -0.15625,-0.0117 -0.015583,-0.002 -0.025917,-0.006 -0.033203,-0.008 -0.00622,-0.0201 -0.015078,-0.0559 -0.025391,-0.10547 -0.011663,-0.0561 -0.025573,-0.12344 -0.037109,-0.1875 -0.023072,-0.12812 -0.041016,-0.24414 -0.041016,-0.24414 a 0.05107445,0.05107445 0 0 0 -0.060547,-0.043 z" /> 267 + <path 268 + d="m 8.3984375,287.4707 a 0.12639828,0.12639828 0 0 0 -0.125,0.10157 c -0.077254,0.36258 -0.064406,0.70957 -0.00195,1.04296 a 0.12639828,0.12639828 0 0 0 0.234375,0.0391 c 0.1095654,-0.19397 0.2246157,-0.35162 0.4375,-0.44532 a 0.12639828,0.12639828 0 0 0 -0.011719,-0.23632 c -0.170448,-0.055 -0.3069446,-0.1953 -0.421875,-0.42969 a 0.12639828,0.12639828 0 0 0 -0.1113281,-0.0723 z" 269 + id="path1430" 270 + style="fill:url(#radialGradient1535);fill-opacity:1;stroke:none;stroke-width:0.13167457px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" 271 + inkscape:original="M 8.3964844 287.59766 C 8.3232974 287.94116 8.3363034 288.27053 8.3964844 288.5918 C 8.5106543 288.38968 8.6461782 288.2022 8.8925781 288.09375 C 8.6798713 288.02515 8.5201615 287.84989 8.3964844 287.59766 z " 272 + inkscape:radius="0.12638564" 273 + sodipodi:type="inkscape:offset" /> 274 + <path 275 + inkscape:connector-curvature="0" 276 + id="path1397" 277 + d="m 8.519216,288.33878 c 0,0 -1.6704347,-0.0752 -1.7288997,-0.1754 -0.058465,-0.10022 -0.200452,-0.75169 -0.200452,-0.75169 l 0.4259608,-0.0835 c 0,0 0.07517,0.46773 0.1085783,0.5596 0.033409,0.0919 1.403165,0 1.403165,0 z" 278 + style="fill:url(#linearGradient1537);fill-opacity:1;stroke:none;stroke-width:0.13167457px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> 279 + <path 280 + sodipodi:nodetypes="cccc" 281 + inkscape:connector-curvature="0" 282 + d="m 8.3960186,287.59752 c 0.1236771,0.25223 0.2842532,0.42835 0.49696,0.49695 -0.2463999,0.10845 -0.3827901,0.29483 -0.49696,0.49695 -0.060181,-0.32127 -0.073187,-0.6504 0,-0.9939 z" 283 + style="fill:#d8c0ff;fill-opacity:1;stroke:none;stroke-width:0.13167457px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" 284 + id="path1409" /> 285 + <path 286 + id="path1960" 287 + style="fill:url(#radialGradient1539);fill-opacity:1;stroke:none;stroke-width:0.13167457px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter2508)" 288 + d="m 8.3960186,287.59752 c 0.1236771,0.25223 0.2842532,0.42835 0.49696,0.49695 -0.2463999,0.10845 -0.3827901,0.29483 -0.49696,0.49695 -0.060181,-0.32127 -0.073187,-0.6504 0,-0.9939 z" 289 + inkscape:connector-curvature="0" 290 + sodipodi:nodetypes="cccc" /> 291 + </g> 292 + <use 293 + x="0" 294 + y="0" 295 + xlink:href="#g1452" 296 + id="use1460" 297 + transform="rotate(180,5.2593307,286.0155)" 298 + width="100%" 299 + height="100%" 300 + style="stroke-width:0.49766773" /> 301 + </g> 302 +</svg>
Modified static/style.scss from [4fd9d6949f] to [322d30fa17].
79 79 text-shadow: 1px 1px black; 80 80 text-decoration: none; 81 81 text-align: center; 82 82 cursor: default; 83 83 user-select: none; 84 84 -webkit-user-drag: none; 85 85 -webkit-app-region: no-drag; 86 - background: linear-gradient(to bottom, 86 + --icon: url(/s/heart.webp); 87 + background-image: linear-gradient(to bottom, 87 88 otone(-47%), 88 89 otone(-50%) 15%, 89 90 otone(-50%) 75%, 90 91 otone(-53%) 91 92 ); 92 93 &:hover, &:focus { 93 94 @extend %glow; ................................................................................ 482 483 } 483 484 484 485 div.thread { 485 486 margin-left: 0.3in; 486 487 & + div.post { margin-top: 0.3in; } 487 488 } 488 489 490 +a[href].username { 491 + >.nym { font-weight: bold; } 492 + color: tone(0%,-0.4); 493 + > span.nym { color: tone(10%) } 494 + > span.handle { color: tone(-5%) } 495 + &:hover { 496 + > span.nym { color: white; } 497 + > span.handle { color: tone(15%) } 498 + } 499 +} 489 500 div.post { 490 501 @extend %box; 491 502 display: grid; 492 - grid-template-columns: 1in 1fr max-content; 503 + margin: unset; 504 + grid-template-columns: 1in 1fr max-content max-content; 493 505 grid-template-rows: min-content max-content; 494 506 margin-bottom: 0.1in; 495 507 >.avatar { 496 508 grid-column: 1/2; grid-row: 1/2; 497 509 img { display: block; width: 1in; height: 1in; margin:0; } 498 510 background: linear-gradient(to bottom, tone(-53%), tone(-57%)); 499 511 } ................................................................................ 501 513 display: block; 502 514 grid-column: 1/3; 503 515 grid-row: 2/3; 504 516 text-align: left; 505 517 text-decoration: none; 506 518 padding: 0.1in; 507 519 padding-left: 0.15in; 508 - >.nym { font-weight: bold; } 509 - color: tone(0%,-0.4); 510 - > span.nym { color: tone(10%) } 511 - > span.handle { color: tone(-5%) } 512 520 background: linear-gradient(to right, tone(-55%), transparent); 513 - &:hover { 514 - > span.nym { color: white; } 515 - > span.handle { color: tone(15%) } 516 - } 517 521 } 518 522 >.content { 519 - grid-column: 2/4; grid-row: 1/2; 523 + grid-column: 2/5; grid-row: 1/2; 520 524 padding: 0.2in; 521 525 @extend %serif; 522 526 font-size: 110%; 523 527 text-align: justify; 524 528 color: tone(25%); 525 529 } 526 530 > a[href].permalink { 527 531 display: block; 528 - grid-column: 3/4; grid-row: 2/3; 532 + grid-column: 4/5; grid-row: 2/3; 529 533 font-size: 80%; 530 534 text-align: right; 531 535 padding: 0.1in; 532 536 padding-right: 0.15in; 533 537 font-style: italic; 534 538 background: linear-gradient(to left, tone(-55%,-0.5), transparent); 535 539 } 540 + div.stats { 541 + display: flex; 542 + grid-column: 3/4; grid-row: 2/3; 543 + justify-content: center; 544 + > .like, > .rt { 545 + margin: 0.5em 0.3em; 546 + padding-left: 1.3em; 547 + background-size: 1.1em; 548 + background-repeat: no-repeat; 549 + min-width: 0.3em; 550 + &:empty { 551 + transition: 0.3s; 552 + opacity: 0.1; 553 + &:hover { opacity: 0.6 !important; } 554 + } 555 + } 556 + > .like { 557 + background-image: url(/s/heart.webp); 558 + } 559 + > .rt { 560 + background-image: url(/s/retweet.webp); 561 + } 562 + } 536 563 } 564 + 565 +div.post:hover div.stats { > .like, > .rt { &:empty {opacity: 0.3;} } } 537 566 538 567 a[href].rawlink { 539 568 @extend %teletype; 540 569 } 541 570 542 571 body.doc main { 543 572 @extend %serif; ................................................................................ 810 839 &+label:hover { 811 840 background-color: otone(-35%); 812 841 color: white; 813 842 } 814 843 &:checked+label { 815 844 border-top: 1px solid otone(-10%); 816 845 border-bottom: 1px solid otone(-50%); 817 - background: linear-gradient(to bottom, otone(-25%,-0.2), otone(-28%,-0.4) 35%, otone(-30%,-0.7)); 846 + background: linear-gradient(to bottom, otone(-25%,-0.2), otone(-28%,-0.3) 35%, otone(-30%,-0.5)); 818 847 color: white; 819 848 box-shadow: 0 0 0 1px tone(-60%); 820 849 &:hover { 821 850 border-top: 1px solid otone(10%); 822 851 border-bottom: 1px solid otone(-60%); 823 852 font-weight: bold; 824 853 } ................................................................................ 870 899 transform: rotate(90deg) scale(1.1); 871 900 color: tone(-20%); 872 901 text-shadow: 0 0 8px tone(-30%); 873 902 } 874 903 } 875 904 } 876 905 } 906 + 907 +div.lede { 908 + display: grid; 909 + grid-template-columns: 1fr min-content; 910 + grid-template-rows: 1.5em 1fr; 911 + padding: 0.1in 0.3in; 912 + margin: 0 -0.2in; 913 + margin-top: 0.2in; 914 + border-radius: 3px; 915 + background: linear-gradient(to bottom, tone(-40%,-0.5), transparent); 916 + border-top: 1px solid tone(-5%,-0.7); 917 + > .promo { 918 + grid-row: 1/2; grid-column: 1/2; 919 + font-style: italic; 920 + font-size: 90%; 921 + color: tone(-10%); 922 + > img { 923 + vertical-align: middle; 924 + margin-right: 0.4em; 925 + width: 1em; height: 1em; 926 + } 927 + } 928 + > a[href].del { 929 + grid-row: 1/2; grid-column: 2/3; 930 + text-decoration: none; 931 + } 932 + > .post { 933 + grid-row: 2/3; grid-column: 1/3; 934 + } 935 +}
Modified store.t from [da1c9184b0] to [6a465decce].
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 8 notiftype = lib.enum { 9 - 'mention', 'like', 'rt', 'react' 9 + 'none', 'mention', 'like', 'rt', 'react' 10 10 }; 11 11 12 12 relation = lib.set { 13 13 'silence', -- messages will not be accepted 14 14 'collapse', -- posts will be collapsed by default 15 15 'disemvowel', -- posts will be ritually humiliated, but shown 16 16 'avoid', -- posts will be kept out of the timeline but will show on users' posts and in conversations ................................................................................ 18 18 'mute', -- posts will be completely hidden at all times 19 19 'block', -- no interactions will be permitted, but posts will remain visible 20 20 }; 21 21 credset = lib.set { 22 22 'pw', 'otp', 'challenge', 'trust' 23 23 }; 24 24 privset = lib.set { 25 - 'post', 'edit', 'acct', 'upload', 'censor', 'admin', 'invite' 25 + 'post', 'edit', 'account', 'upload', 'moderate', 'admin', 'invite' 26 26 }; 27 27 powerset = lib.set { 28 28 -- user powers -- default on 29 29 'login', -- not locked out 30 30 'visible', -- account & posts can be seen by others 31 31 'post', -- can do poasts 32 32 'shout', -- posts show up on local timeline 33 33 'propagate', -- posts are sent to other instances 34 34 'artifact', -- upload, claim, and manage artifacts 35 - 'acct', -- configure own account 35 + 'account', -- configure own account 36 36 'edit'; -- edit own poasts 37 + 'snitch'; -- can issue badthink reports 37 38 38 39 -- admin powers -- default off 39 40 'purge', -- permanently delete users 40 41 'config', -- change daemon policy & config UI 41 42 'censor', -- dispose of badthink 42 43 'discipline', -- enforced timeouts, stripping badges and epithets, punitive actions that do not permanently deprive of powers; can remove own injunctions but not others' 43 44 'vacate', -- can remove others' injunctions, but not apply them 44 45 'cred', -- alter credentials 45 46 'elevate', 'demote', -- change user rank, give and take powers, including the ability to log in 46 47 'rebrand', -- modify site's brand identity 47 48 'herald', -- grant serverwide epithets and badges 49 + 'crier', -- can promote content to the instance page 48 50 'invite' -- *unlimited* invites 49 51 }; 50 52 prepmode = lib.enum { 51 53 'full','conf','admin' 52 54 } 53 55 } 54 56 ................................................................................ 84 86 var pow: m.powerset pow:clear() 85 87 (pow.login << true) 86 88 (pow.visible << true) 87 89 (pow.post << true) 88 90 (pow.shout << true) 89 91 (pow.propagate << true) 90 92 (pow.artifact << true) 91 - (pow.acct << true) 93 + (pow.account << true) 92 94 (pow.edit << true) 93 95 return m.rights { rank = 0, quota = 1000, invites = 0, powers = pow; } 94 96 end 95 97 96 98 struct m.actor { 97 99 id: uint64 98 100 nym: str ................................................................................ 212 214 mentions: lib.mem.ptr(uint64) 213 215 circles: lib.mem.ptr(uint64) --only meaningful if scope is set to circle 214 216 convoheaduri: str 215 217 parent: uint64 216 218 -- ephemera 217 219 localpost: bool 218 220 accent: int16 219 - depth: uint16 -- used in conversations to indicate tree depth 221 + rts: uint32 222 + likes: uint32 223 + rtdby: uint64 -- 0 if not rt 224 + rtact: uint64 -- 0 if not rt, id of rt action otherwise 220 225 source: &m.source 221 226 222 227 -- save :: bool -> {} (defined in acl.t due to dep. hell) 223 228 } 224 229 225 230 m.user_conf_funcs = function(be,n,ty,rty,rty2) 226 231 rty = rty or ty ................................................................................ 381 386 -- origin: inet 382 387 -- cookie issue time: m.timepoint 383 388 actor_auth_register_uid: {&m.source, uint64, uint64} -> {} 384 389 -- notifies the backend module of the UID that has been assigned for 385 390 -- an authentication ID 386 391 -- aid: uint64 387 392 -- uid: uint64 393 + actor_notifs_fetch: {&m.source, uint64} -> lib.mem.lstptr(m.notif) 388 394 389 395 auth_enum_uid: {&m.source, uint64} -> lib.mem.lstptr(m.auth) 390 396 auth_enum_handle: {&m.source, rawstring} -> lib.mem.lstptr(m.auth) 391 397 auth_attach_pw: {&m.source, uint64, bool, pstr, pstr} -> {} 392 398 -- uid: uint64 393 399 -- reset: bool (delete other passwords?) 394 400 -- pw: pstring ................................................................................ 413 419 post_enum_author_uid: {&m.source, uint64, m.range} -> lib.mem.lstptr(m.post) 414 420 post_enum_parent: {&m.source, uint64} -> lib.mem.lstptr(m.post) 415 421 post_attach_ctl: {&m.source, uint64, uint64, bool} -> {} 416 422 -- attaches or detaches an existing database artifact 417 423 -- post id: uint64 418 424 -- artifact id: uint64 419 425 -- detach: bool 426 + post_retweet: {&m.source, uint64, uint64, bool} -> {} 427 + post_like: {&m.source, uint64, uint64, bool} -> {} 428 + -- undo: bool 429 + post_react: {&m.source, uint64, uint64, pstring} -> {} 430 + -- emoji: pstring (null to delete previous reaction, otherwise adds/changes) 431 + post_liked_uid: {&m.source, uint64, uint64} -> bool 432 + post_reacted_uid: {&m.source, uint64, uint64} -> bool 420 433 421 434 thread_latest_arrival_calc: {&m.source, uint64} -> m.timepoint 422 435 423 436 artifact_instantiate: {&m.source, lib.mem.ptr(uint8), lib.mem.ptr(int8)} -> uint64 424 437 -- instantiate an artifact in the database, either installing a new 425 438 -- artifact or returning the id of an existing artifact with the same hash 426 439 -- artifact: bytea
Modified str.t from [1e93ad8eb5] to [638f6c2759].
178 178 self.buf = [rawstring](lib.mem.heapr_raw(self.buf, self.space)) 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 + 186 +terra m.acc:ipush(i: intptr) 187 + var decbuf: int8[21] 188 + var si = lib.math.decstr_friendly(i, &decbuf[20]) 189 + var len: intptr = [decbuf.type.N] - (si - &decbuf[0]) 190 + return self:push(si,len) 191 +end 192 + 193 +terra m.acc:shpush(i: uint64) 194 + var sbuf: int8[lib.math.shorthand.maxlen] 195 + var len = lib.math.shorthand.gen(i,&sbuf[0]) 196 + return self:push(&sbuf[0], len) 197 +end 185 198 186 199 m.lit = macro(function(str) 187 200 if str:asvalue() ~= nil then 188 201 return `[lib.mem.ref(int8)] {ptr = [str:asvalue()], ct = [#(str:asvalue())]} 189 202 else 190 203 return `[lib.mem.ref(int8)] {ptr = nil, ct = 0} 191 204 end
Modified view/confirm.tpl from [3b921f59eb] to [0d2952df9c].
1 1 <form class="message" method="post"> 2 - <img class="icon" src="/s/query.svg"> 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 6 <a class="button" href="@:cancel">cancel</a> 7 7 <button name="act" value="confirm">confirm</button> 8 8 </menu> 9 9 </form>
Modified view/tweet.tpl from [e117899db4] to [aefe28dd4e].
1 1 <div class="post"@attr> 2 2 <div class="avatar"><img src="@:avatar"></div> 3 3 <a class="username" href="/@:acctlink">@nym</a> 4 4 <div class="content"> 5 5 <div class="subject">@!subject</div> 6 6 <div class="text">@text</div> 7 7 </div> 8 + @stats 8 9 <a class="permalink" href="@permalink">@when</a> 9 10 </div>