| 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 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 ... 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 ... 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 ... 792 793 794 795 796 797 798 799 800 801 802 803 804 805 .... 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 .... 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 |
select a.id, a.nym, a.handle, a.origin, a.bio, a.avataruri, a.rank, a.quota, a.key, a.epithet, extract(epoch from a.knownsince)::bigint, coalesce(a.handle || '@' || s.domain, '@' || a.handle) as xid, au.restrict, array['post' ] <@ au.restrict as can_post, array['edit' ] <@ au.restrict as can_edit, array['acct' ] <@ au.restrict as can_acct, array['upload'] <@ au.restrict as can_upload, array['censor'] <@ au.restrict as can_censor, array['admin' ] <@ au.restrict as can_admin from parsav_auth au left join parsav_actors a on au.uid = a.id left join parsav_servers s on a.origin = s.id where au.aid = $1::bigint and au.blacklist = false and (au.netmask is null or au.netmask >> $2::inet) and ................................................................................ params = {uint64}, cmd = true, sql = [[ delete from parsav_posts where id = $1::bigint ]] }; post_fetch = { params = {uint64}, sql = [[ select a.origin is null, p.id, p.author, p.subject, p.acl, p.body, extract(epoch from p.posted )::bigint, extract(epoch from p.discovered)::bigint, extract(epoch from p.edited )::bigint, p.parent, p.convoheaduri, p.chgcount, coalesce(c.value, -1)::smallint from parsav_posts as p inner join parsav_actors as a on p.author = a.id left join parsav_actor_conf_ints as c on c.uid = a.id and c.key = 'ui-accent' where p.id = $1::bigint ]]; }; post_enum_parent = { params = {uint64}, sql = [[ select a.origin is null, p.id, p.author, p.subject, p.acl, p.body, extract(epoch from p.posted )::bigint, extract(epoch from p.discovered)::bigint, extract(epoch from p.edited )::bigint, p.parent, p.convoheaduri, p.chgcount, coalesce(c.value, -1)::smallint from parsav_posts as p inner join parsav_actors as a on a.id = p.author left join parsav_actor_conf_ints as c on c.uid = a.id and c.key = 'ui-accent' where p.parent = $1::bigint order by p.posted, p.discovered asc ]] ................................................................................ inner join parsav_posts as p on p.id = posts.id ) select extract(epoch from max(m))::bigint from maxes ]]; }; post_enum_author_uid = { params = {uint64,uint64,uint64,uint64, uint64}, sql = [[ select a.origin is null, p.id, p.author, p.subject, p.acl, p.body, extract(epoch from p.posted )::bigint, extract(epoch from p.discovered)::bigint, extract(epoch from p.edited )::bigint, p.parent, p.convoheaduri, p.chgcount, coalesce((select value from parsav_actor_conf_ints as c where c.uid = $1::bigint and c.key = 'ui-accent'),-1)::smallint from parsav_posts as p inner join parsav_actors as a on p.author = a.id where p.author = $5::bigint and ($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and ($2::bigint = 0 or to_timestamp($2::bigint) < p.posted) order by (p.posted, p.discovered) desc limit case when $3::bigint = 0 then null else $3::bigint end offset $4::bigint ]] }; -- maybe there's some way to unify these two, idk, im tired timeline_instance_fetch = { params = {uint64, uint64, uint64, uint64}, sql = [[ select true, p.id, p.author, p.subject, p.acl, p.body, extract(epoch from p.posted )::bigint, extract(epoch from p.discovered)::bigint, extract(epoch from p.edited )::bigint, p.parent, null::text, p.chgcount, coalesce(c.value, -1)::smallint from parsav_posts as p inner join parsav_actors as a on p.author = a.id left join parsav_actor_conf_ints as c on c.uid = a.id and c.key = 'ui-accent' where ($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and ($2::bigint = 0 or to_timestamp($2::bigint) < p.posted) and (a.origin is null) order by (p.posted, p.discovered) desc limit case when $3::bigint = 0 then null else $3::bigint end offset $4::bigint ]] }; artifact_instantiate = { params = {binblob, binblob, pstring}, sql = [[ insert into parsav_artifacts (content,hash,mime) values ( $1::bytea, $2::bytea, $3::text ................................................................................ end p.ptr.parent = r:int(uint64,row,9) if r:null(row,11) then p.ptr.chgcount = 0 else p.ptr.chgcount = r:int(uint32,row,11) end p.ptr.accent = r:int(int16,row,12) p.ptr.localpost = r:bool(row,0) return p end local terra row_to_actor(r: &pqr, row: intptr): lib.mem.ptr(lib.store.actor) var a: lib.mem.ptr(lib.store.actor) var av: rawstring, avlen: intptr ................................................................................ a.ptr.source = src var au = [lib.stat(lib.store.auth)] { ok = true } au.val.aid = aid au.val.uid = a.ptr.id if not r:null(0,14) then -- restricted? au.val.privs:clear() (au.val.privs.post << r:bool(0,15)) (au.val.privs.edit << r:bool(0,16)) (au.val.privs.acct << r:bool(0,17)) (au.val.privs.upload << r:bool(0,18)) (au.val.privs.censor << r:bool(0,19)) (au.val.privs.admin << r:bool(0,20)) else au.val.privs:fill() end return au, a end ::fail:: return [lib.stat (lib.store.auth) ] { ok = false }, [lib.mem.ptr(lib.store.actor)] { ptr = nil, ct = 0 } ................................................................................ ): lib.mem.ptr(lib.store.post) var r = queries.post_fetch.exec(src, post) if r.sz == 0 then return [lib.mem.ptr(lib.store.post)].null() end var p = row_to_post(&r, 0) p.ptr.source = src return p end]; timeline_instance_fetch = [terra(src: &lib.store.source, rg: lib.store.range) var r = pqr { sz = 0 } var A,B,C,D = rg:matrix() -- :/ r = queries.timeline_instance_fetch.exec(src,A,B,C,D) var ret: lib.mem.ptr(lib.mem.ptr(lib.store.post)) ret:init(r.sz) |
| | | | | | > > > > > > | > > > > > > > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | < > > | < | | > | > | | | | | | | | | | | | | | | | | | > > > > > > > > > > > > > > | | | | | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > |
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 ... 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 ... 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 ... 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 .... 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 .... 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 |
select a.id, a.nym, a.handle, a.origin, a.bio, a.avataruri, a.rank, a.quota, a.key, a.epithet, extract(epoch from a.knownsince)::bigint, coalesce(a.handle || '@' || s.domain, '@' || a.handle) as xid, au.restrict, array['post' ] <@ au.restrict, array['edit' ] <@ au.restrict, array['account' ] <@ au.restrict, array['upload' ] <@ au.restrict, array['moderate'] <@ au.restrict, array['admin' ] <@ au.restrict from parsav_auth au left join parsav_actors a on au.uid = a.id left join parsav_servers s on a.origin = s.id where au.aid = $1::bigint and au.blacklist = false and (au.netmask is null or au.netmask >> $2::inet) and ................................................................................ params = {uint64}, cmd = true, sql = [[ delete from parsav_posts where id = $1::bigint ]] }; post_fetch = { params = {uint64}, sql = [[ with counts as ( select a.kind, p.id as subject, count(*) as ct from parsav_acts as a inner join parsav_posts as p on p.id = a.subject group by a.kind, p.id ) select a.origin is null, p.id, p.author, p.subject, p.acl, p.body, extract(epoch from p.posted )::bigint, extract(epoch from p.discovered)::bigint, extract(epoch from p.edited )::bigint, p.parent, p.convoheaduri, p.chgcount, coalesce(c.value, -1)::smallint, 0::bigint, 0::bigint, coalesce((select ct from counts where kind = 'like' and counts.subject = p.id),0)::integer, coalesce((select ct from counts where kind = 'rt' and counts.subject = p.id),0)::integer from parsav_posts as p inner join parsav_actors as a on p.author = a.id left join parsav_actor_conf_ints as c on c.uid = a.id and c.key = 'ui-accent' where p.id = $1::bigint ]]; }; post_enum_parent = { params = {uint64}, sql = [[ with counts as ( select a.kind, p.id as subject, count(*) as ct from parsav_acts as a inner join parsav_posts as p on p.id = a.subject group by a.kind, p.id ) select a.origin is null, p.id, p.author, p.subject, p.acl, p.body, extract(epoch from p.posted )::bigint, extract(epoch from p.discovered)::bigint, extract(epoch from p.edited )::bigint, p.parent, p.convoheaduri, p.chgcount, coalesce(c.value, -1)::smallint, 0::bigint, 0::bigint, coalesce((select ct from counts where kind = 'like' and counts.subject = p.id),0)::integer, coalesce((select ct from counts where kind = 'rt' and counts.subject = p.id),0)::integer from parsav_posts as p inner join parsav_actors as a on a.id = p.author left join parsav_actor_conf_ints as c on c.uid = a.id and c.key = 'ui-accent' where p.parent = $1::bigint order by p.posted, p.discovered asc ]] ................................................................................ inner join parsav_posts as p on p.id = posts.id ) select extract(epoch from max(m))::bigint from maxes ]]; }; post_react_simple = { params = {uint64, uint64, pstring}, sql = [[ insert into parsav_acts (kind,actor,subject) values ( $3::text, $1::bigint, $2::bigint ) returning id ]]; }; post_react_cancel = { params = {uint64, uint64, pstring}, cmd = true, sql = [[ delete from parsav_acts where actor = $1::bigint and subject = $2::bigint and kind = $3::text ]]; }; post_reacts_fetch_uid = { params = {uint64, uint64, pstring}, sql = [[ select id, actor, subject, kind, body, time from parsav_acts where ($1::bigint = 0 or actor = $1::bigint) and ($2::bigint = 0 or subject = $2::bigint) and ($3::text is null or kind = $3::text ) ]] }; post_enum_author_uid = { params = {uint64,uint64,uint64,uint64, uint64}, sql = [[ with ownposts as ( select *, 0::bigint as rtid from parsav_posts as p where p.author = $5::bigint and ($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and ($2::bigint = 0 or to_timestamp($2::bigint) < p.posted) ), retweets as ( select p.*, a.id as rtid from parsav_acts as a inner join parsav_posts as p on a.subject = p.id where a.actor = $5::bigint and a.kind = 'rt' and ($1::bigint = 0 or a.time <= to_timestamp($1::bigint)) and ($2::bigint = 0 or to_timestamp($2::bigint) < a.time) ), allposts as (select *, 0::bigint as retweeter from ownposts union select *, $5::bigint as retweeter from retweets), counts as ( select a.kind, p.id as subject, count(*) as ct from parsav_acts as a inner join parsav_posts as p on p.id = a.subject group by a.kind, p.id ) select a.origin is null, p.id, p.author, p.subject, p.acl, p.body, extract(epoch from p.posted )::bigint, extract(epoch from p.discovered)::bigint, extract(epoch from p.edited )::bigint, p.parent, p.convoheaduri, p.chgcount, coalesce(c.value,-1)::smallint, p.retweeter, p.rtid, coalesce((select ct from counts where kind = 'like' and counts.subject = p.id),0)::integer, coalesce((select ct from counts where kind = 'rt' and counts.subject = p.id),0)::integer from allposts as p inner join parsav_actors as a on p.author = a.id left join parsav_actor_conf_ints as c on c.key = 'ui-accent' and c.uid = a.id order by (p.posted, p.discovered) desc limit case when $3::bigint = 0 then null else $3::bigint end offset $4::bigint ]] }; -- maybe there's some way to unify these two, idk, im tired timeline_instance_fetch = { params = {uint64, uint64, uint64, uint64}, sql = [[ with posts as ( select true, p.id, p.author, p.subject, p.acl, p.body, extract(epoch from p.posted )::bigint, extract(epoch from p.discovered)::bigint, extract(epoch from p.edited )::bigint, p.parent, null::text, p.chgcount, coalesce(c.value, -1)::smallint, 0::bigint, 0::bigint from parsav_posts as p inner join parsav_actors as a on p.author = a.id left join parsav_actor_conf_ints as c on c.uid = a.id and c.key = 'ui-accent' where ($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and ($2::bigint = 0 or to_timestamp($2::bigint) < p.posted) and (a.origin is null) order by (p.posted, p.discovered) desc limit case when $3::bigint = 0 then null else $3::bigint end offset $4::bigint ), counts as ( select a.kind, p.id as subject, count(*) as ct from parsav_acts as a inner join parsav_posts as p on p.id = a.subject group by a.kind, p.id ) select *, coalesce((select ct from counts as c where kind = 'like' and c.subject = posts.id),0)::integer, coalesce((select ct from counts as c where kind = 'rt' and c.subject = posts.id),0)::integer from posts ]] }; artifact_instantiate = { params = {binblob, binblob, pstring}, sql = [[ insert into parsav_artifacts (content,hash,mime) values ( $1::bytea, $2::bytea, $3::text ................................................................................ end p.ptr.parent = r:int(uint64,row,9) if r:null(row,11) then p.ptr.chgcount = 0 else p.ptr.chgcount = r:int(uint32,row,11) end p.ptr.accent = r:int(int16,row,12) p.ptr.rtdby = r:int(uint64,row,13) p.ptr.rtact = r:int(uint64,row,14) p.ptr.likes = r:int(uint32,row,15) p.ptr.rts = r:int(uint32,row,16) p.ptr.localpost = r:bool(row,0) return p end local terra row_to_actor(r: &pqr, row: intptr): lib.mem.ptr(lib.store.actor) var a: lib.mem.ptr(lib.store.actor) var av: rawstring, avlen: intptr ................................................................................ a.ptr.source = src var au = [lib.stat(lib.store.auth)] { ok = true } au.val.aid = aid au.val.uid = a.ptr.id if not r:null(0,14) then -- restricted? au.val.privs:clear() (au.val.privs.post << r:bool(0,15)) (au.val.privs.edit << r:bool(0,16)) (au.val.privs.account << r:bool(0,17)) (au.val.privs.upload << r:bool(0,18)) (au.val.privs.moderate<< r:bool(0,19)) (au.val.privs.admin << r:bool(0,20)) else au.val.privs:fill() end return au, a end ::fail:: return [lib.stat (lib.store.auth) ] { ok = false }, [lib.mem.ptr(lib.store.actor)] { ptr = nil, ct = 0 } ................................................................................ ): lib.mem.ptr(lib.store.post) var r = queries.post_fetch.exec(src, post) if r.sz == 0 then return [lib.mem.ptr(lib.store.post)].null() end var p = row_to_post(&r, 0) p.ptr.source = src return p end]; post_retweet = [terra( src: &lib.store.source, uid: uint64, post: uint64, undo: bool ): {} if not undo then queries.post_react_simple.exec(src,uid,post,"rt") else queries.post_react_cancel.exec(src,uid,post,"rt") end end]; post_like = [terra( src: &lib.store.source, uid: uint64, post: uint64, undo: bool ): {} if not undo then queries.post_react_simple.exec(src,uid,post,"like") else queries.post_react_cancel.exec(src,uid,post,"like") end end]; post_liked_uid = [terra( src: &lib.store.source, uid: uint64, post: uint64 ): bool var q = queries.post_reacts_fetch_uid.exec(src,uid,post,'like') if q.sz > 0 then q:free() return true end return false end]; timeline_instance_fetch = [terra(src: &lib.store.source, rg: lib.store.range) var r = pqr { sz = 0 } var A,B,C,D = rg:matrix() -- :/ r = queries.timeline_instance_fetch.exec(src,A,B,C,D) var ret: lib.mem.ptr(lib.mem.ptr(lib.store.post)) ret:init(r.sz) |
Modified backend/schema/pgsql.sql from [72a3e65e6e] to [347a4ab533].
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
kind smallint, -- e.g. follow, block, mute primary key (relator, relatee, kind) ); create table parsav_acts ( id bigint primary key default (1+random()*(2^63-1))::bigint, kind text not null, -- like, react, so on time timestamp not null default now(), actor bigint references parsav_actors(id) on delete cascade, subject bigint -- may be post or act, depending on kind ); create table parsav_log ( -- accesses are tracked for security & sending delete acts id bigint primary key default (1+random()*(2^63-1))::bigint, time timestamp not null default now(), actor bigint references parsav_actors(id) |
| | > |
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
kind smallint, -- e.g. follow, block, mute primary key (relator, relatee, kind) ); create table parsav_acts ( id bigint primary key default (1+random()*(2^63-1))::bigint, kind text not null, -- like, rt, react, so on time timestamp not null default now(), actor bigint references parsav_actors(id) on delete cascade, subject bigint, -- may be post or act, depending on kind body text -- emoji, if react ); create table parsav_log ( -- accesses are tracked for security & sending delete acts id bigint primary key default (1+random()*(2^63-1))::bigint, time timestamp not null default now(), actor bigint references parsav_actors(id) |
Modified config.lua from [5a4f5a8d5b] to [8efc0b984f].
53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
-- the damn things before compiling (also making the binary smaller)
{'style.css', 'text/css'};
{'live.js', 'text/javascript'}; -- rrrrrrrr
{'default-avatar.webp', 'image/webp'}; -- needs inkscape-exclusive svg features
{'padlock.svg', 'image/svg+xml'};
{'warn.svg', 'image/svg+xml'};
{'query.webp', 'image/webp'};
};
default_ui_accent = tonumber(default('parsav_ui_default_accent',323));
}
if os.getenv('parsav_let_me_be_an_idiot') == "i know what i'm doing" then
conf.braingeniousmode = true -- SOUND GENERAL QUARTERS
end
if u.ping '.fslckout' or u.ping '_FOSSIL_' then
|
> > |
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
-- the damn things before compiling (also making the binary smaller)
{'style.css', 'text/css'};
{'live.js', 'text/javascript'}; -- rrrrrrrr
{'default-avatar.webp', 'image/webp'}; -- needs inkscape-exclusive svg features
{'padlock.svg', 'image/svg+xml'};
{'warn.svg', 'image/svg+xml'};
{'query.webp', 'image/webp'};
{'heart.webp', 'image/webp'};
{'retweet.webp', 'image/webp'};
};
default_ui_accent = tonumber(default('parsav_ui_default_accent',323));
}
if os.getenv('parsav_let_me_be_an_idiot') == "i know what i'm doing" then
conf.braingeniousmode = true -- SOUND GENERAL QUARTERS
end
if u.ping '.fslckout' or u.ping '_FOSSIL_' then
|
Modified doc/load.lua from [68f6a9a498] to [783a358256].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
local path = ...
local sources = {
-- user section
acl = {title = 'access control lists'};
-- admin section
--overview = {title = 'server overview', priv = 'config'};
invocation = {title = 'daemon invocation', priv = 'config'};
--backends = {title = 'storage backends', priv = 'config'};
--pgsql = {title = 'pgsql', priv = 'config', parent = 'backends'};
}
local util = dofile 'common.lua'
local ingest = function(filename)
return (util.exec { 'cmark', '--smart', '--unsafe', (path..'/'..filename) }):gsub('\n','')
|
> > > |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
local path = ...
local sources = {
-- user section
acl = {title = 'access control lists'};
-- admin section
--overview = {title = 'server overview', priv = 'config'};
invocation = {title = 'daemon invocation', priv = 'config'};
usr = {title = 'user accounting', priv = {'elevate','demote','purge','herald'}};
--srvcfg = {title = 'server configuration policies', priv = 'config'};
--discipline = {title = 'disciplinary measures', priv = 'discipline'};
--backends = {title = 'storage backends', priv = 'config'};
--pgsql = {title = 'pgsql', priv = 'config', parent = 'backends'};
}
local util = dofile 'common.lua'
local ingest = function(filename)
return (util.exec { 'cmark', '--smart', '--unsafe', (path..'/'..filename) }):gsub('\n','')
|
Added doc/usr.md version [61c1a08ae0].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
# user accounting 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. ## core concepts 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. 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. 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. 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 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. ## creating administrative accounts 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.) 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") `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. ## creating user accounts 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. 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. 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. 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. ## powers the abilities a user can exercise on a server are governed by their *powers,* a set of flags administrators can set on their accounts. these powers are intended for ordinary users, and default to on: * **login:** the user can log in to the instance. revoking this power is equivalent to banning the user. * **visible:** the user and his posts can be seen by other users without navigating directly to his profile page * **post:** the user can publish new posts * **shout:** the user is visible on the local timeline * **propagate:** the user's posts federate * **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. * **account:** the user can configure their own account and profile * **edit:** the user can edit her own posts * **snitch:** the user can submit reports asking for moderator action these powers are intended for staff, and default to off: * **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. * **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)). * **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. * **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. * **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. * **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 * **vacate:** the user can rehabilitate disciplined actors, vacating sanctions, voiding demerits, and issuing temporary reprieves from restrictions. * **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. * **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. * **cred:** the user can add, change, and remove the credentials of lower-ranking users (think password resets). * **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. * **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. 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) ### recommendations 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. 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. 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. and if your users don't respect you, you might as well pack up right now. ## emergencies 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.) ### locked out 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. ### missing privileges 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. ### sanctions 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`. ### lost account 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 2 3 4 5 6 7 8 9 10 11 |
dl = git dbg-flags = $(if $(dbg),-g) images = static/default-avatar.webp static/query.webp #$(addsuffix .webp, $(basename $(wildcard static/*.svg))) styles = $(addsuffix .css, $(basename $(wildcard static/*.scss))) parsav parsavd: parsav.t config.lua pkgdata.lua $(images) $(styles) terra $(dbg-flags) $< parsav.o parsavd.o: parsav.t config.lua pkgdata.lua $(images) $(styles) env parsav_link=no terra $(dbg-flags) $< |
| |
1 2 3 4 5 6 7 8 9 10 11 |
dl = git
dbg-flags = $(if $(dbg),-g)
images = static/default-avatar.webp static/query.webp static/heart.webp static/retweet.webp
#$(addsuffix .webp, $(basename $(wildcard static/*.svg)))
styles = $(addsuffix .css, $(basename $(wildcard static/*.scss)))
parsav parsavd: parsav.t config.lua pkgdata.lua $(images) $(styles)
terra $(dbg-flags) $<
parsav.o parsavd.o: parsav.t config.lua pkgdata.lua $(images) $(styles)
env parsav_link=no terra $(dbg-flags) $<
|
Modified mgtool.t from [ee3dfa15a8] to [4f69a41277].
385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 |
root.handle = cfmode.arglist(0)
var epithets = array(
'root', 'god', 'regional jehovah', 'titan king',
'king of olympus', 'cyberpharaoh', 'electric ellimist',
"rampaging c'tan", 'deathless tweetlord', 'postmaster',
'faerie queene', 'lord of the posts', 'ruthless cybercrat',
'general secretary', 'commissar', 'kwisatz haderach',
'dedicated hyperturing'
-- feel free to add more
)
root.epithet = epithets[lib.crypt.random(intptr,0,[epithets.type.N])]
root.rights.powers:fill() -- grant omnipotence
root.rights.rank = 1
var ruid = dlg:actor_create(&root)
dlg:conf_set('master',root.handle)
|
| > |
385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 |
root.handle = cfmode.arglist(0) var epithets = array( 'root', 'god', 'regional jehovah', 'titan king', 'king of olympus', 'cyberpharaoh', 'electric ellimist', "rampaging c'tan", 'deathless tweetlord', 'postmaster', 'faerie queene', 'lord of the posts', 'ruthless cybercrat', 'general secretary', 'commissar', 'kwisatz haderach', 'dedicated hyperturing', 'grand inquisitor', 'reverend mother', 'cyberpope', 'verified®', 'patent pending' -- feel free to add more ) root.epithet = epithets[lib.crypt.random(intptr,0,[epithets.type.N])] root.rights.powers:fill() -- grant omnipotence root.rights.rank = 1 var ruid = dlg:actor_create(&root) dlg:conf_set('master',root.handle) |
Modified render/conf/users.t from [09fa445b20] to [4bed391611].
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 ... 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 ... 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
then sanitized = pstr {ptr='', ct=0}
else sanitized = lib.html.sanitize(cs(user.ptr.epithet),true)
end
cinp:lpush('<div class="elem"><label for="epithet">epithet</label><input type="text" id="epithet" name="epithet" value="'):ppush(sanitized):lpush('"></div>')
if user.ptr.epithet ~= nil then sanitized:free() end
end
if co.who.rights.powers.invite() or co.who.rights.powers.discipline() then
var min = 0
if not (co.who.rights.powers.discipline() or
co.who.rights.powers.demote() and co.who.rights.powers.invite())
then min = user.ptr.rights.invites end
var max = co.srv.cfg.maxinvites
if not co.who.rights.powers.invite() then max = user.ptr.rights.invites end
push_num_field(cinp, 'invites', 'invites', min, max, user.ptr.rights.invites, false)
end
cinp:lpush('</div><div class="elem"><div class="check-panel">')
if user.ptr.id ~= co.who.id and
((user.ptr.rights.rank == 0 and co.who.rights.powers.elevate()) or
(user.ptr.rights.rank > 0 and co.who.rights.powers.demote())) then
push_checkbox(&cinp, 'staff', pstr.null(), 'site staff member', user.ptr.rights.rank > 0, true, pstr.null())
end
................................................................................
var unym: lib.str.acc unym:init(64)
unym:lpush('<a href="/')
if user(0).origin ~= 0 then unym:lpush('@') end
do var sanxid = lib.html.sanitize(user(0).xid, true)
unym:ppush(sanxid)
sanxid:free() end
unym:lpush('" class="id">')
lib.render.nym(user.ptr,0,&unym)
unym:lpush('</a>')
var pg = data.view.conf_user_ctl {
name = unym:finalize();
inputcontent = cinpp;
linkcontent = clnkp;
}
var ret = pg:tostr()
................................................................................
if usr.rights.rank ~= 0 then
ulst:lpush('<span class="regalia">')
regalia(&ulst, usr.rights.rank)
ulst:lpush('</span>')
end
if co.who:overpowers(usr) then
ulst:lpush('<a class="id" href="users/'):push(&idbuf[0],idlen):lpush('">')
lib.render.nym(usr, 0, &ulst)
ulst:lpush('</a></li>')
else
ulst:lpush('<span class="id">')
lib.render.nym(usr, 0, &ulst)
ulst:lpush('</span></li>')
end
::skip::end
ulst:lpush('</ul>')
return ulst:finalize()
end
do return pstr.null() end
|
| | > > > > > > > > | | | |
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 ... 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 ... 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
then sanitized = pstr {ptr='', ct=0}
else sanitized = lib.html.sanitize(cs(user.ptr.epithet),true)
end
cinp:lpush('<div class="elem"><label for="epithet">epithet</label><input type="text" id="epithet" name="epithet" value="'):ppush(sanitized):lpush('"></div>')
if user.ptr.epithet ~= nil then sanitized:free() end
end
if co.who.rights.powers.invite() or co.who.rights.powers.discipline() then
var min: uint32 = 0
if not (co.who.rights.powers.discipline() or
co.who.rights.powers.demote() and co.who.rights.powers.invite())
then min = user.ptr.rights.invites end
var max: uint32 = co.srv.cfg.maxinvites
if not co.who.rights.powers.invite() then max = user.ptr.rights.invites end
push_num_field(cinp, 'invites', 'invites', min, max, user.ptr.rights.invites, false)
end
if co.who.rights.powers.elevate() or co.who.rights.powers.demote() then
var max: uint32 = 5000
if not co.who.rights.powers.elevate() then max = user.ptr.rights.quota end
var min: uint32 = 0
if not co.who.rights.powers.demote() then min = user.ptr.rights.quota end
push_num_field(cinp, 'quota', 'quota', min, max, user.ptr.rights.quota, user.ptr.id == co.who.id and co.who.rights.rank ~= 1)
end
cinp:lpush('</div><div class="elem"><div class="check-panel">')
if user.ptr.id ~= co.who.id and
((user.ptr.rights.rank == 0 and co.who.rights.powers.elevate()) or
(user.ptr.rights.rank > 0 and co.who.rights.powers.demote())) then
push_checkbox(&cinp, 'staff', pstr.null(), 'site staff member', user.ptr.rights.rank > 0, true, pstr.null())
end
................................................................................
var unym: lib.str.acc unym:init(64)
unym:lpush('<a href="/')
if user(0).origin ~= 0 then unym:lpush('@') end
do var sanxid = lib.html.sanitize(user(0).xid, true)
unym:ppush(sanxid)
sanxid:free() end
unym:lpush('" class="id">')
lib.render.nym(user.ptr,0,&unym,false)
unym:lpush('</a>')
var pg = data.view.conf_user_ctl {
name = unym:finalize();
inputcontent = cinpp;
linkcontent = clnkp;
}
var ret = pg:tostr()
................................................................................
if usr.rights.rank ~= 0 then
ulst:lpush('<span class="regalia">')
regalia(&ulst, usr.rights.rank)
ulst:lpush('</span>')
end
if co.who:overpowers(usr) then
ulst:lpush('<a class="id" href="users/'):push(&idbuf[0],idlen):lpush('">')
lib.render.nym(usr, 0, &ulst, false)
ulst:lpush('</a></li>')
else
ulst:lpush('<span class="id">')
lib.render.nym(usr, 0, &ulst, false)
ulst:lpush('</span></li>')
end
::skip::end
ulst:lpush('</ul>')
return ulst:finalize()
end
do return pstr.null() end
|
Modified render/docpage.t from [148acf7303] to [97c704e199].
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
local terra
pushbranches(list: &lib.str.acc, idx: intptr, ps: lib.store.powerset): {}
var [pages] = array([allpages])
var started = false
for i=0,[pages.type.N] do
if pages[i].parent == idx+1 and (pages[i].priv:sz() == 0 or
(ps and pages[i].priv) == pages[i].priv) then
if not started then
started = true
list:lpush('<ul>')
end
list:lpush('<li><a href="/doc/'):rpush(pages[i].name):lpush('">')
:rpush(pages[i].title):lpush('</a>')
pushbranches(list, i, ps)
|
| |
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
local terra
pushbranches(list: &lib.str.acc, idx: intptr, ps: lib.store.powerset): {}
var [pages] = array([allpages])
var started = false
for i=0,[pages.type.N] do
if pages[i].parent == idx+1 and (pages[i].priv:sz() == 0 or
(ps and pages[i].priv):sz() ~= 0) then
if not started then
started = true
list:lpush('<ul>')
end
list:lpush('<li><a href="/doc/'):rpush(pages[i].name):lpush('">')
:rpush(pages[i].title):lpush('</a>')
pushbranches(list, i, ps)
|
Modified render/nym.t from [74775ce158] to [ea921b8ffe].
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
..
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
-- vim: ft=terra
local pstr = lib.str.t
local terra cs(s: rawstring)
return pstr { ptr = s, ct = lib.str.sz(s) }
end
local terra
render_nym(who: &lib.store.actor, scope: uint64, tgt: &lib.str.acc)
var acc: lib.str.acc
var n: &lib.str.acc
if tgt ~= nil then n = tgt else
n = &acc
n:init(128)
end
var xidsan = lib.html.sanitize(cs(who.xid),false)
................................................................................
n:lpush('<span class="nym">'):ppush(nymsan)
:lpush('</span> [<span class="handle">'):ppush(xidsan)
:lpush('</span>]')
nymsan:free()
else n:lpush('<span class="handle">'):ppush(xidsan):lpush('</span>') end
xidsan:free()
if who.epithet ~= nil then
var episan = lib.html.sanitize(cs(who.epithet),false)
n:lpush('<span class="epithet">'):ppush(episan):lpush('</span>')
episan:free()
end
-- TODO: if scope == chat room then lookup titles in room member db
if tgt == nil then
return n:finalize()
else return pstr.null() end
end
return render_nym
|
|
>
|
|
|
|
>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
..
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
-- vim: ft=terra
local pstr = lib.str.t
local terra cs(s: rawstring)
return pstr { ptr = s, ct = lib.str.sz(s) }
end
local terra
render_nym(who: &lib.store.actor, scope: uint64, tgt: &lib.str.acc, minimal: bool)
var acc: lib.str.acc
var n: &lib.str.acc
if tgt ~= nil then n = tgt else
n = &acc
n:init(128)
end
var xidsan = lib.html.sanitize(cs(who.xid),false)
................................................................................
n:lpush('<span class="nym">'):ppush(nymsan)
:lpush('</span> [<span class="handle">'):ppush(xidsan)
:lpush('</span>]')
nymsan:free()
else n:lpush('<span class="handle">'):ppush(xidsan):lpush('</span>') end
xidsan:free()
if not minimal then
if who.epithet ~= nil then
var episan = lib.html.sanitize(cs(who.epithet),false)
n:lpush('<span class="epithet">'):ppush(episan):lpush('</span>')
episan:free()
end
end
-- TODO: if scope == chat room then lookup titles in room member db
if tgt == nil then
return n:finalize()
else return pstr.null() end
end
return render_nym
|
Modified render/profile.t from [5d5ed1c86e] to [08f3a58ce1].
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
var sn_follows = cs(lib.math.decstr_friendly(stats.follows, sn_posts.ptr - 1))
var sn_followers = cs(lib.math.decstr_friendly(stats.followers, sn_follows.ptr - 1))
var sn_mutuals = cs(lib.math.decstr_friendly(stats.mutuals, sn_followers.ptr - 1))
var bio = lib.str.plit "<em>tall, dark, and mysterious</em>"
if actor.bio ~= nil then
bio = lib.smackdown.html(cs(actor.bio))
end
var fullname = lib.render.nym(actor,0,nil) defer fullname:free()
var comments: lib.str.acc comments:init(64)
-- this is really more what epithets are for, i think
--if actor.rights.rank > 0 then comments:lpush('<li>staff member</li>') end
if co.aid ~= 0 and actor.rights.rank ~= 0 then
if co.who:outranks(actor) then
comments:lpush('<li style="--co:50">underling</li>')
elseif actor:outranks(co.who) then
comments:lpush('<li style="--co:-50">outranks you</li>')
end
end
|
| > > > |
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
var sn_follows = cs(lib.math.decstr_friendly(stats.follows, sn_posts.ptr - 1)) var sn_followers = cs(lib.math.decstr_friendly(stats.followers, sn_follows.ptr - 1)) var sn_mutuals = cs(lib.math.decstr_friendly(stats.mutuals, sn_followers.ptr - 1)) var bio = lib.str.plit "<em>tall, dark, and mysterious</em>" if actor.bio ~= nil then bio = lib.smackdown.html(cs(actor.bio)) end var fullname = lib.render.nym(actor,0,nil,false) defer fullname:free() var comments: lib.str.acc comments:init(64) -- this is really more what epithets are for, i think --if actor.rights.rank > 0 then comments:lpush('<li>staff member</li>') end if co.srv.cfg.master == actor.id then comments:lpush('<li style="--co:-70">founder</li>') end if co.aid ~= 0 and actor.rights.rank ~= 0 then if co.who:outranks(actor) then comments:lpush('<li style="--co:50">underling</li>') elseif actor:outranks(co.who) then comments:lpush('<li style="--co:-50">outranks you</li>') end end |
Modified render/tweet-page.t from [005ba03599] to [8729ddd689].
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
var livetime = co.srv:thread_latest_arrival_calc(p.id)
var pg: lib.str.acc pg:init(256)
lib.render.tweet(co, p, &pg)
if co.aid ~= 0 then
pg:lpush('<form class="action-bar" method="post">')
var liked = false -- FIXME
var rtd = false
if not liked
then pg:lpush('<button class="pos" name="act" value="like">like</button>')
else pg:lpush('<button class="neg" name="act" value="dislike">dislike</button>')
end
if not rtd
then pg:lpush('<button class="pos" name="act" value="rt">retweet</button>')
else pg:lpush('<button class="neg" name="act" value="unrt">detweet</button>')
end
if p.author == co.who.id then
pg:lpush('<a class="button" href="/post/'):rpush(path(1)):lpush('/edit">edit</a><a class="neg button" href="/post/'):rpush(path(1)):lpush('/del">delete</a>')
end
-- TODO list user's chosen reaction emoji
pg:lpush('</form>')
end
|
| < < < | < < |
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
var livetime = co.srv:thread_latest_arrival_calc(p.id)
var pg: lib.str.acc pg:init(256)
lib.render.tweet(co, p, &pg)
if co.aid ~= 0 then
pg:lpush('<form class="action-bar" method="post">')
if not co.srv:post_liked_uid(co.who.id, p.id)
then pg:lpush('<button class="pos" name="act" value="like">like</button>')
else pg:lpush('<button class="neg" name="act" value="dislike">dislike</button>')
end
pg:lpush('<button class="pos" name="act" value="rt">retweet</button>')
if p.author == co.who.id then
pg:lpush('<a class="button" href="/post/'):rpush(path(1)):lpush('/edit">edit</a><a class="neg button" href="/post/'):rpush(path(1)):lpush('/del">delete</a>')
end
-- TODO list user's chosen reaction emoji
pg:lpush('</form>')
end
|
Modified render/tweet.t from [43aca48007] to [ee058ed0af].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
-- vim: ft=terra
local pstr = lib.mem.ptr(int8)
local terra cs(s: rawstring)
return pstr { ptr = s, ct = lib.str.sz(s) }
end
local terra
render_tweet(co: &lib.srv.convo, p: &lib.store.post, acc: &lib.str.acc)
var author: &lib.store.actor
for j = 0, co.actorcache.top do
if p.author == co.actorcache(j).ptr.id then
author = co.actorcache(j).ptr
goto foundauth
end
end
author = co.actorcache:insert(co.srv:actor_fetch_uid(p.author)).ptr
::foundauth::
var avistr: lib.str.acc if author.origin == 0 then
avistr:compose('/avi/',author.handle)
end
var timestr: int8[26] lib.osclock.ctime_r(&p.posted, ×tr[0])
var bhtml = lib.smackdown.html([lib.mem.ptr(int8)] {ptr=p.body,ct=0}) defer bhtml:free()
var idbuf: int8[lib.math.shorthand.maxlen]
var idlen = lib.math.shorthand.gen(p.id, idbuf)
var permalink: lib.str.acc permalink:compose('/post/',{idbuf,idlen})
var fullname = lib.render.nym(author,0,nil) defer fullname:free()
var tpl = data.view.tweet {
text = bhtml;
subject = cs(lib.coalesce(p.subject,''));
nym = fullname;
when = cs(×tr[0]);
avatar = cs(author.avatar);
acctlink = cs(author.xid);
permalink = permalink:finalize();
attr = ''
}
var attrbuf: int8[32]
if p.accent ~= -1 and p.accent ~= co.ui_hue then
var hdecbuf: int8[21]
var hdec = lib.math.decstr(p.accent, &hdecbuf[20])
lib.str.cpy(&attrbuf[0], ' style="--hue:')
lib.str.cpy(&attrbuf[14], hdec)
var len = &hdecbuf[20] - hdec
lib.str.cpy(&attrbuf[14] + len, '"')
tpl.attr = &attrbuf[0]
end
defer tpl.permalink:free()
if acc ~= nil then tpl:append(acc) return [lib.mem.ptr(int8)]{ptr=nil,ct=0} end
var txt = tpl:tostr()
return txt
end
return render_tweet
|
> > > > > > > > > > > > | > | | > > | > > > > | > | | > > > > > > > > > > > > > > | > > > > > > > > | > | | > |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
-- vim: ft=terra
local pstr = lib.mem.ptr(int8)
local terra cs(s: rawstring)
return pstr { ptr = s, ct = lib.str.sz(s) }
end
local terra
push_promo_header(co: &lib.srv.convo, acc: &lib.str.acc, rter: &lib.store.actor, rid: uint64)
acc:lpush('<div class="lede"><div class="promo"><img src="'):push(rter.avatar,0):lpush('"><a href="/')
if rter.origin ~= 0 then acc:lpush('@') end
acc:push(rter.xid,0):lpush('" class="username">')
lib.render.nym(rter, 0, acc, true)
acc:lpush('</a> retweeted</div>')
if co.who.id == rter.id then
acc:lpush('<a href="/post/'):shpush(rid):lpush('/del" class="del">✖</a>')
end
end
local terra
render_tweet(co: &lib.srv.convo, p: &lib.store.post, acc: &lib.str.acc)
var author: &lib.store.actor = nil
var retweeter: &lib.store.actor = nil
for j = 0, co.actorcache.top do
if p.author == co.actorcache(j).ptr.id then author = co.actorcache(j).ptr end
if p.rtdby == co.actorcache(j).ptr.id then retweeter = co.actorcache(j).ptr end
if author ~= nil and (p.rtdby == 0 or retweeter ~= nil) then
goto foundauth
end
end
if author == nil then
author = co.actorcache:insert(co.srv:actor_fetch_uid(p.author)).ptr
end
if p.rtdby ~= 0 and retweeter == nil then
retweeter = co.actorcache:insert(co.srv:actor_fetch_uid(p.rtdby)).ptr
end
::foundauth::
var avistr: lib.str.acc if author.origin == 0 then
avistr:compose('/avi/',author.handle)
end
var timestr: int8[26] lib.osclock.ctime_r(&p.posted, ×tr[0])
var bhtml = lib.smackdown.html([lib.mem.ptr(int8)] {ptr=p.body,ct=0})
defer bhtml:free()
var idbuf: int8[lib.math.shorthand.maxlen]
var idlen = lib.math.shorthand.gen(p.id, idbuf)
var permalink: lib.str.acc permalink:compose('/post/',{idbuf,idlen})
var fullname = lib.render.nym(author,0,nil, false) defer fullname:free()
var tpl = data.view.tweet {
text = bhtml;
subject = cs(lib.coalesce(p.subject,''));
nym = fullname;
when = cs(×tr[0]);
avatar = cs(author.avatar);
acctlink = cs(author.xid);
permalink = permalink:finalize();
attr = pstr{'',0};
stats = pstr{'',0};
}
if p.rts + p.likes > 0 then
var s: lib.str.acc s:init(128)
s:lpush('<div class="stats">')
if p.rts > 0 then s:lpush('<div class="rt">' ):ipush(p.rts ):lpush('</div>') end
if p.likes > 0 then s:lpush('<div class="like">'):ipush(p.likes):lpush('</div>') end
s:lpush('</div>')
tpl.stats = s:finalize()
end
var attrbuf: int8[32]
if p.accent ~= -1 and p.accent ~= co.ui_hue then
var hdecbuf: int8[21]
var hdec = lib.math.decstr(p.accent, &hdecbuf[20])
lib.str.cpy(&attrbuf[0], ' style="--hue:')
lib.str.cpy(&attrbuf[14], hdec)
var len = &hdecbuf[20] - hdec
lib.str.cpy(&attrbuf[14] + len, '"')
tpl.attr = &attrbuf[0]
end
defer tpl.permalink:free()
if acc ~= nil then
if retweeter ~= nil then push_promo_header(co, acc, retweeter, p.rtact) end
tpl:append(acc)
if retweeter ~= nil then acc:lpush('</div>') end
if p.rts + p.likes > 0 then tpl.stats:free() end
return [lib.mem.ptr(int8)]{ptr=nil,ct=0}
end
if retweeter ~= nil then
var rta: lib.str.acc rta:init(512)
push_promo_header(co, &rta, retweeter, p.rtact)
tpl:append(&rta) rta:lpush('</div>')
return rta:finalize()
else
var txt = tpl:tostr()
if p.rts + p.likes > 0 then tpl.stats:free() end
return txt
end
end
return render_tweet
|
Modified route.t from [2f7668c3df] to [666eb021ed].
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
...
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
|
return
else goto badop end
end
else goto badurl end
end
if meth == method.post then
var replytext = co:ppostv('post')
var acl = co:ppostv('acl')
var subj = co:ppostv('subject')
if not acl then acl = lib.str.plit 'all' end
if not replytext then goto badop end
var reply = lib.store.post {
author = co.who.id, parent = pid;
subject = subj.ptr, acl = acl.ptr, body = replytext.ptr;
}
reply:publish(co.srv)
end
lib.render.tweet_page(co, path, post.ptr)
do return end
::badurl:: do co:complain(404, 'invalid URL', 'this URL does not reference extant content or functionality') return end
::badop :: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
................................................................................
path(1):cmp(lib.str.lit 'emoji')
) then goto nopriv
elseif not co.who.rights.powers.rebrand() and (
path(1):cmp(lib.str.lit 'brand')
) then goto nopriv
elseif not co.who.rights.powers.acct() and (
path(1):cmp(lib.str.lit 'profile') or
path(1):cmp(lib.str.lit 'acct')
) then goto nopriv
elseif not co.who.rights.powers:affect_users() and (
path(1):cmp(lib.str.lit 'users')
) then goto nopriv end
|
>
>
>
>
>
>
>
>
>
>
>
|
|
|
|
|
|
|
|
|
|
|
>
|
|
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
...
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
|
return else goto badop end end else goto badurl end end if meth == method.post then var act = co:ppostv('act') if act:cmp(lib.str.plit 'like') and not co.srv:post_liked_uid(co.who.id,pid) then co.srv:post_like(co.who.id, pid, false) post.ptr.likes = post.ptr.likes + 1 elseif act:cmp(lib.str.plit 'dislike') and co.srv:post_liked_uid(co.who.id,pid) then co.srv:post_like(co.who.id, pid, true) post.ptr.likes = post.ptr.likes - 1 elseif act:cmp(lib.str.plit 'rt') then co.srv:post_retweet(co.who.id, pid, false) post.ptr.rts = post.ptr.rts + 1 elseif act:cmp(lib.str.plit 'post') then var replytext = co:ppostv('post') var acl = co:ppostv('acl') var subj = co:ppostv('subject') if not acl then acl = lib.str.plit 'all' end if not replytext then goto badop end var reply = lib.store.post { author = co.who.id, parent = pid; subject = subj.ptr, acl = acl.ptr, body = replytext.ptr; } reply:publish(co.srv) else goto badop end end lib.render.tweet_page(co, path, post.ptr) do return end ::badurl:: do co:complain(404, 'invalid URL', 'this URL does not reference extant content or functionality') return end ::badop :: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end ................................................................................ path(1):cmp(lib.str.lit 'emoji') ) then goto nopriv elseif not co.who.rights.powers.rebrand() and ( path(1):cmp(lib.str.lit 'brand') ) then goto nopriv elseif not co.who.rights.powers.account() and ( path(1):cmp(lib.str.lit 'profile') or path(1):cmp(lib.str.lit 'acct') ) then goto nopriv elseif not co.who.rights.powers:affect_users() and ( path(1):cmp(lib.str.lit 'users') ) then goto nopriv end |
Modified session.t from [78b2aad470] to [c79e9ffb10].
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
-- sessions are implemented so as to avoid any local data storage. they
-- are tracked by storing an encrypted cookie which contains an authid,
-- a login epoch time, and a truncated hmac code authenticating both, all
-- encoded using Shorthand. we need functions to generate and parse these
local m = {
maxlen = lib.math.shorthand.maxlen*3 + 2;
maxage = 2 * 60 * 60; -- 2 hours
cookiename = 'auth';
}
terra m.cookie_gen(secret: lib.mem.ptr(int8), authid: uint64, time: uint64, out: &int8): intptr
var ptr = out
ptr = ptr + lib.math.shorthand.gen(authid, ptr)
@ptr = @'.' ptr = ptr + 1
|
| |
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
-- sessions are implemented so as to avoid any local data storage. they
-- are tracked by storing an encrypted cookie which contains an authid,
-- a login epoch time, and a truncated hmac code authenticating both, all
-- encoded using Shorthand. we need functions to generate and parse these
local m = {
maxlen = lib.math.shorthand.maxlen*3 + 2;
maxage = 16 * 60 * 60; -- 16 hours
cookiename = 'auth';
}
terra m.cookie_gen(secret: lib.mem.ptr(int8), authid: uint64, time: uint64, out: &int8): intptr
var ptr = out
ptr = ptr + lib.math.shorthand.gen(authid, ptr)
@ptr = @'.' ptr = ptr + 1
|
Modified srv.t from [34fad9fa1a] to [e0d52a1828].
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
|
credmgd: bool
maxupsz: intptr
instance: lib.mem.ptr(int8)
overlord: &srv
ui_hue: uint16
nranks: uint16
maxinvites: uint16
}
local struct srv {
sources: lib.mem.ptr(lib.store.source)
webmgr: lib.net.mg_mgr
webcon: &lib.net.mg_connection
cfg: cfgcache
id: rawstring
................................................................................
end
smode:free()
end
self.ui_hue = self:cfint('ui-accent',config.default_ui_accent)
self.nranks = self:cfint('user-ranks',10)
self.maxinvites = self:cfint('max-invites',64)
end
return {
overlord = srv;
convo = convo;
route = route;
secmode = secmode;
}
|
>
>
>
>
>
>
>
>
>
>
>
>
|
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
...
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
|
credmgd: bool maxupsz: intptr instance: lib.mem.ptr(int8) overlord: &srv ui_hue: uint16 nranks: uint16 maxinvites: uint16 master: uint64 } local struct srv { sources: lib.mem.ptr(lib.store.source) webmgr: lib.net.mg_mgr webcon: &lib.net.mg_connection cfg: cfgcache id: rawstring ................................................................................ end smode:free() end self.ui_hue = self:cfint('ui-accent',config.default_ui_accent) self.nranks = self:cfint('user-ranks',10) self.maxinvites = self:cfint('max-invites',64) var webmaster = self.overlord:conf_get('master') if webmaster:ref() then defer webmaster:free() var wma = self.overlord:actor_fetch_xid(webmaster) if not wma then 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') else self.master = wma(0).id wma:free() end end end return { overlord = srv; convo = convo; route = route; secmode = secmode; } |
Added static/heart.svg version [c2edd21438].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <!-- Created with Inkscape (http://www.inkscape.org/) --> <svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="20" height="20" viewBox="0 0 5.2916668 5.2916668" version="1.1" id="svg8" inkscape:version="0.92.4 (5da689c313, 2019-01-14)" sodipodi:docname="heart.svg" inkscape:export-filename="/home/lexi/dev/parsav/static/heart.png" inkscape:export-xdpi="406.39999" inkscape:export-ydpi="406.39999"> <defs id="defs2"> <linearGradient id="linearGradient1395" inkscape:collect="always"> <stop id="stop1391" offset="0" style="stop-color:#ff1616;stop-opacity:1" /> <stop id="stop1393" offset="1" style="stop-color:#ff1d1d;stop-opacity:0" /> </linearGradient> <linearGradient inkscape:collect="always" id="linearGradient1383"> <stop style="stop-color:#980000;stop-opacity:1;" offset="0" id="stop1379" /> <stop style="stop-color:#980000;stop-opacity:0;" offset="1" id="stop1381" /> </linearGradient> <linearGradient inkscape:collect="always" id="linearGradient832"> <stop style="stop-color:#ffcfcf;stop-opacity:1;" offset="0" id="stop828" /> <stop style="stop-color:#ffcfcf;stop-opacity:0;" offset="1" id="stop830" /> </linearGradient> <radialGradient inkscape:collect="always" xlink:href="#linearGradient832" id="radialGradient834" cx="3.2286437" cy="286.62921" fx="3.2286437" fy="286.62921" r="1.0866126" gradientTransform="matrix(1.8608797,0.8147617,-0.38242057,0.87343168,106.71446,33.692223)" gradientUnits="userSpaceOnUse" /> <filter inkscape:collect="always" style="color-interpolation-filters:sRGB" id="filter1356" x="-0.044539396" width="1.0890788" y="-0.04671235" height="1.0934247"> <feGaussianBlur inkscape:collect="always" stdDeviation="0.040330888" id="feGaussianBlur1358" /> </filter> <radialGradient inkscape:collect="always" xlink:href="#linearGradient1383" id="radialGradient1385" cx="4.1787109" cy="286.89261" fx="4.1787109" fy="286.89261" r="1.2260786" gradientTransform="matrix(1.7016464,0,0,1.6348586,-2.9319775,-182.10895)" gradientUnits="userSpaceOnUse" /> <radialGradient inkscape:collect="always" xlink:href="#linearGradient1395" id="radialGradient1389" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.66230313,-1.6430738,1.0154487,0.40931507,-290.06307,177.39489)" cx="4.02069" cy="287.79269" fx="4.02069" fy="287.79269" r="1.0866126" /> </defs> <sodipodi:namedview id="base" pagecolor="#181818" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:zoom="11.2" inkscape:cx="13.645085" inkscape:cy="22.307499" inkscape:document-units="mm" inkscape:current-layer="layer1" showgrid="false" units="px" inkscape:window-width="949" inkscape:window-height="1028" inkscape:window-x="963" inkscape:window-y="44" inkscape:window-maximized="0" showguides="false" fit-margin-top="0" fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0" /> <metadata id="metadata5"> <rdf:RDF> <cc:Work rdf:about=""> <dc:format>image/svg+xml</dc:format> <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> <dc:title></dc:title> </cc:Work> </rdf:RDF> </metadata> <g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(-2.9526324,-283.47435)"> <path sodipodi:type="inkscape:offset" inkscape:radius="0.14186843" 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 " style="fill:url(#radialGradient1385);fill-opacity:1;stroke:none;stroke-width:0.12017766px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" id="path822" 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" transform="matrix(2.157964,0,0,2.2461218,-3.4190419,-359.83766)" /> <path id="path819" style="fill:#ff8080;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" 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" inkscape:connector-curvature="0" sodipodi:nodetypes="scccccscs" /> <path sodipodi:nodetypes="scccccscs" inkscape:connector-curvature="0" 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" 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)" id="path826" transform="matrix(2.157964,0,0,2.2461218,-3.4190419,-359.83766)" /> <path id="path1387" 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)" 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" inkscape:connector-curvature="0" sodipodi:nodetypes="scccccscs" transform="matrix(2.157964,0,0,2.2461218,-3.4190419,-359.83766)" /> </g> </svg> |
Modified static/live.js from [86bdd64b84] to [682908b4c8].
1
2
3
4
5
6
7
8
9
10
11
12
..
38
39
40
41
42
43
44
45
46
47
48
49
50
|
/* first things first, we need to scan over the document and see
* if there are any UI elements unfortunate enough to need
* interactivity beyond what native HTML+CSS can provide. if so,
* we attach the appropriate listeners to them. */
window.addEventListener('load', function() {
/* update hue-picker background when slider is adjusted */
document.querySelectorAll('.color-picker').forEach(function(box) {
let slider = box.querySelector('[data-color-pick]');
box.style.setProperty('--hue', slider.value);
slider.addEventListener('input', function(e) {
box.style.setProperty('--hue', e.target.value);
});
................................................................................
return;
}
container._liveLastArrival = newest
resp.text().then(function(htmlbody) {
var parser = new DOMParser();
var newdoc = parser.parseFromString(htmlbody,'text/html')
container.innerHTML = newdoc.getElementById(container.id).innerHTML
})
})
}, interv)
});
});
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
>
>
>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
..
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
|
/* first things first, we need to scan over the document and see
* if there are any UI elements unfortunate enough to need
* interactivity beyond what native HTML+CSS can provide. if so,
* we attach the appropriate listeners to them. */
window.addEventListener('load', function() {
/* social media is less fun when you can't just click on a tweet
* to insta-like or -retweet it. this is unfortunately not possible
* (except in various hideously shitty ways) without javascript. */
function mk(elt) { return document.createElement(elt); }
function attachButtons() {
document.querySelectorAll('body:not(.post) main div.post').forEach(function(post){
let url = post.querySelector('.permalink').attributes.getNamedItem('href').value;
function postReq(act,elt) {
fetch(new Request(url, {
method: 'POST',
body: 'act='+act
})).then(function(resp) {
if (resp.ok && resp.status == 200) {
var i = parseInt(elt.innerHTML)
if (isNaN(i)) {i=0}
elt.innerHTML = (i+1).toString()
}
})
}
var stats = post.querySelector('.stats');
if (stats == null) {
/* no stats box; create one */
var n = mk('div');
n.classList.add('stats');
post.appendChild(n);
stats = n
}
function ensureElt(cls, before) {
let s = stats.querySelector('.' + cls);
if (s == null) {
var n = mk('div');
n.classList.add(cls);
if (before == null) { stats.appendChild(n) } else {
stats.insertBefore(n,stats.querySelector(before))
}
return n
} else { return s }
}
var like = ensureElt('like', null);
var rt = ensureElt('rt','.like');
function activate(elt,name) {
elt.addEventListener('click', function(e) { postReq(name,elt) });
elt.style.setProperty('cursor','pointer');
}
activate(like,'like');
activate(rt,'rt');
});
}
/* update hue-picker background when slider is adjusted */
document.querySelectorAll('.color-picker').forEach(function(box) {
let slider = box.querySelector('[data-color-pick]');
box.style.setProperty('--hue', slider.value);
slider.addEventListener('input', function(e) {
box.style.setProperty('--hue', e.target.value);
});
................................................................................
return;
}
container._liveLastArrival = newest
resp.text().then(function(htmlbody) {
var parser = new DOMParser();
var newdoc = parser.parseFromString(htmlbody,'text/html')
container.innerHTML = newdoc.getElementById(container.id).innerHTML;
attachButtons();
})
})
}, interv)
});
attachButtons();
});
|
Added static/retweet.svg version [c3fa459a24].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 |
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <!-- Created with Inkscape (http://www.inkscape.org/) --> <svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="20" height="20" viewBox="0 0 5.2916664 5.2916665" version="1.1" id="svg8" inkscape:version="0.92.4 (5da689c313, 2019-01-14)" sodipodi:docname="retweet.svg"> <defs id="defs2"> <linearGradient id="linearGradient2866" inkscape:collect="always"> <stop id="stop2862" offset="0" style="stop-color:#9a57ff;stop-opacity:1" /> <stop id="stop2864" offset="1" style="stop-color:#ab73ff;stop-opacity:0" /> </linearGradient> <linearGradient inkscape:collect="always" id="linearGradient1966"> <stop style="stop-color:#f0e7ff;stop-opacity:1;" offset="0" id="stop1962" /> <stop style="stop-color:#f0e7ff;stop-opacity:0;" offset="1" id="stop1964" /> </linearGradient> <linearGradient inkscape:collect="always" id="linearGradient1468"> <stop style="stop-color:#9955ff;stop-opacity:1;" offset="0" id="stop1464" /> <stop style="stop-color:#9955ff;stop-opacity:0;" offset="1" id="stop1466" /> </linearGradient> <linearGradient inkscape:collect="always" id="linearGradient1403"> <stop style="stop-color:#ccaaff;stop-opacity:1;" offset="0" id="stop1399" /> <stop style="stop-color:#ccaaff;stop-opacity:0;" offset="1" id="stop1401" /> </linearGradient> <linearGradient id="linearGradient1395" inkscape:collect="always"> <stop id="stop1391" offset="0" style="stop-color:#ff1616;stop-opacity:1" /> <stop id="stop1393" offset="1" style="stop-color:#ff1d1d;stop-opacity:0" /> </linearGradient> <linearGradient inkscape:collect="always" id="linearGradient1383"> <stop style="stop-color:#980000;stop-opacity:1;" offset="0" id="stop1379" /> <stop style="stop-color:#980000;stop-opacity:0;" offset="1" id="stop1381" /> </linearGradient> <linearGradient inkscape:collect="always" id="linearGradient832"> <stop style="stop-color:#ffcfcf;stop-opacity:1;" offset="0" id="stop828" /> <stop style="stop-color:#ffcfcf;stop-opacity:0;" offset="1" id="stop830" /> </linearGradient> <radialGradient inkscape:collect="always" xlink:href="#linearGradient832" id="radialGradient834" cx="3.2286437" cy="286.62921" fx="3.2286437" fy="286.62921" r="1.0866126" gradientTransform="matrix(1.8608797,0.8147617,-0.38242057,0.87343168,106.71446,33.692223)" gradientUnits="userSpaceOnUse" /> <radialGradient inkscape:collect="always" xlink:href="#linearGradient1383" id="radialGradient1385" cx="4.1787109" cy="286.89261" fx="4.1787109" fy="286.89261" r="1.2260786" gradientTransform="matrix(1.7016464,0,0,1.6348586,-2.9319775,-182.10895)" gradientUnits="userSpaceOnUse" /> <radialGradient inkscape:collect="always" xlink:href="#linearGradient1395" id="radialGradient1389" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.66230313,-1.6430738,1.0154487,0.40931507,-290.06307,177.39489)" cx="4.02069" cy="287.79269" fx="4.02069" fy="287.79269" r="1.0866126" /> <linearGradient inkscape:collect="always" xlink:href="#linearGradient1403" id="linearGradient1405" x1="8.3939333" y1="288.1091" x2="7.0158253" y2="287.32819" gradientUnits="userSpaceOnUse" /> <filter inkscape:collect="always" style="color-interpolation-filters:sRGB" id="filter2508" x="-0.24674278" width="1.4934856" y="-0.13581935" height="1.2716388"> <feGaussianBlur inkscape:collect="always" stdDeviation="0.056246184" id="feGaussianBlur2510" /> </filter> <filter inkscape:collect="always" style="color-interpolation-filters:sRGB" id="filter3064" x="-0.07694713" width="1.1538943" y="-0.14115551" height="1.282311"> <feGaussianBlur inkscape:collect="always" stdDeviation="0.065422039" id="feGaussianBlur3066" /> </filter> <linearGradient inkscape:collect="always" xlink:href="#linearGradient2866" id="linearGradient1533" gradientUnits="userSpaceOnUse" x1="8.3939333" y1="288.1091" x2="7.0097656" y2="287.25977" /> <radialGradient inkscape:collect="always" xlink:href="#linearGradient1468" id="radialGradient1535" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1,0,0,1.5198212,0,-149.75763)" cx="8.525074" cy="288.10031" fx="8.525074" fy="288.10031" r="0.43142718" /> <linearGradient inkscape:collect="always" xlink:href="#linearGradient1403" id="linearGradient1537" gradientUnits="userSpaceOnUse" x1="8.3939333" y1="288.1091" x2="7.0158253" y2="287.32819" /> <radialGradient inkscape:collect="always" xlink:href="#linearGradient1966" id="radialGradient1539" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1,0,0,1.2339206,0,-67.391253)" cx="8.7198324" cy="288.09686" fx="8.7198324" fy="288.09686" r="0.27354568" /> </defs> <sodipodi:namedview id="base" pagecolor="#181818" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:zoom="7.919596" inkscape:cx="7.7101412" inkscape:cy="36.101286" inkscape:document-units="mm" inkscape:current-layer="layer1" showgrid="false" units="px" inkscape:window-width="949" inkscape:window-height="1028" inkscape:window-x="963" inkscape:window-y="44" inkscape:window-maximized="0" showguides="false" fit-margin-top="0" fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0" /> <metadata id="metadata5"> <rdf:RDF> <cc:Work rdf:about=""> <dc:format>image/svg+xml</dc:format> <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> <dc:title></dc:title> </cc:Work> </rdf:RDF> </metadata> <g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(-2.6134661,-283.36966)"> <g id="g1452" transform="matrix(2.0546825,0,0,1.965062,-10.834174,-279.0744)" style="stroke-width:0.49766773"> <path sodipodi:type="inkscape:offset" inkscape:radius="0.051069338" 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 " 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)" id="path2512" 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" /> <path 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" id="path1430" style="fill:url(#radialGradient1535);fill-opacity:1;stroke:none;stroke-width:0.13167457px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" 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 " inkscape:radius="0.12638564" sodipodi:type="inkscape:offset" /> <path inkscape:connector-curvature="0" id="path1397" 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" style="fill:url(#linearGradient1537);fill-opacity:1;stroke:none;stroke-width:0.13167457px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> <path sodipodi:nodetypes="cccc" inkscape:connector-curvature="0" 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" style="fill:#d8c0ff;fill-opacity:1;stroke:none;stroke-width:0.13167457px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" id="path1409" /> <path id="path1960" 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)" 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" inkscape:connector-curvature="0" sodipodi:nodetypes="cccc" /> </g> <use x="0" y="0" xlink:href="#g1452" id="use1460" transform="rotate(180,5.2593307,286.0155)" width="100%" height="100%" style="stroke-width:0.49766773" /> </g> </svg> |
Modified static/style.scss from [4fd9d6949f] to [322d30fa17].
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 ... 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 ... 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 ... 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 ... 870 871 872 873 874 875 876 |
text-shadow: 1px 1px black;
text-decoration: none;
text-align: center;
cursor: default;
user-select: none;
-webkit-user-drag: none;
-webkit-app-region: no-drag;
background: linear-gradient(to bottom,
otone(-47%),
otone(-50%) 15%,
otone(-50%) 75%,
otone(-53%)
);
&:hover, &:focus {
@extend %glow;
................................................................................
}
div.thread {
margin-left: 0.3in;
& + div.post { margin-top: 0.3in; }
}
div.post {
@extend %box;
display: grid;
grid-template-columns: 1in 1fr max-content;
grid-template-rows: min-content max-content;
margin-bottom: 0.1in;
>.avatar {
grid-column: 1/2; grid-row: 1/2;
img { display: block; width: 1in; height: 1in; margin:0; }
background: linear-gradient(to bottom, tone(-53%), tone(-57%));
}
................................................................................
display: block;
grid-column: 1/3;
grid-row: 2/3;
text-align: left;
text-decoration: none;
padding: 0.1in;
padding-left: 0.15in;
>.nym { font-weight: bold; }
color: tone(0%,-0.4);
> span.nym { color: tone(10%) }
> span.handle { color: tone(-5%) }
background: linear-gradient(to right, tone(-55%), transparent);
&:hover {
> span.nym { color: white; }
> span.handle { color: tone(15%) }
}
}
>.content {
grid-column: 2/4; grid-row: 1/2;
padding: 0.2in;
@extend %serif;
font-size: 110%;
text-align: justify;
color: tone(25%);
}
> a[href].permalink {
display: block;
grid-column: 3/4; grid-row: 2/3;
font-size: 80%;
text-align: right;
padding: 0.1in;
padding-right: 0.15in;
font-style: italic;
background: linear-gradient(to left, tone(-55%,-0.5), transparent);
}
}
a[href].rawlink {
@extend %teletype;
}
body.doc main {
@extend %serif;
................................................................................
&+label:hover {
background-color: otone(-35%);
color: white;
}
&:checked+label {
border-top: 1px solid otone(-10%);
border-bottom: 1px solid otone(-50%);
background: linear-gradient(to bottom, otone(-25%,-0.2), otone(-28%,-0.4) 35%, otone(-30%,-0.7));
color: white;
box-shadow: 0 0 0 1px tone(-60%);
&:hover {
border-top: 1px solid otone(10%);
border-bottom: 1px solid otone(-60%);
font-weight: bold;
}
................................................................................
transform: rotate(90deg) scale(1.1);
color: tone(-20%);
text-shadow: 0 0 8px tone(-30%);
}
}
}
}
|
> | > > > > > > > > > > > | < < < < < < < | < | | > > > > > > > > > > > > > > | > > > > > > > > > > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > |
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 ... 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 ... 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 ... 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 ... 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 |
text-shadow: 1px 1px black; text-decoration: none; text-align: center; cursor: default; user-select: none; -webkit-user-drag: none; -webkit-app-region: no-drag; --icon: url(/s/heart.webp); background-image: linear-gradient(to bottom, otone(-47%), otone(-50%) 15%, otone(-50%) 75%, otone(-53%) ); &:hover, &:focus { @extend %glow; ................................................................................ } div.thread { margin-left: 0.3in; & + div.post { margin-top: 0.3in; } } a[href].username { >.nym { font-weight: bold; } color: tone(0%,-0.4); > span.nym { color: tone(10%) } > span.handle { color: tone(-5%) } &:hover { > span.nym { color: white; } > span.handle { color: tone(15%) } } } div.post { @extend %box; display: grid; margin: unset; grid-template-columns: 1in 1fr max-content max-content; grid-template-rows: min-content max-content; margin-bottom: 0.1in; >.avatar { grid-column: 1/2; grid-row: 1/2; img { display: block; width: 1in; height: 1in; margin:0; } background: linear-gradient(to bottom, tone(-53%), tone(-57%)); } ................................................................................ display: block; grid-column: 1/3; grid-row: 2/3; text-align: left; text-decoration: none; padding: 0.1in; padding-left: 0.15in; background: linear-gradient(to right, tone(-55%), transparent); } >.content { grid-column: 2/5; grid-row: 1/2; padding: 0.2in; @extend %serif; font-size: 110%; text-align: justify; color: tone(25%); } > a[href].permalink { display: block; grid-column: 4/5; grid-row: 2/3; font-size: 80%; text-align: right; padding: 0.1in; padding-right: 0.15in; font-style: italic; background: linear-gradient(to left, tone(-55%,-0.5), transparent); } div.stats { display: flex; grid-column: 3/4; grid-row: 2/3; justify-content: center; > .like, > .rt { margin: 0.5em 0.3em; padding-left: 1.3em; background-size: 1.1em; background-repeat: no-repeat; min-width: 0.3em; &:empty { transition: 0.3s; opacity: 0.1; &:hover { opacity: 0.6 !important; } } } > .like { background-image: url(/s/heart.webp); } > .rt { background-image: url(/s/retweet.webp); } } } div.post:hover div.stats { > .like, > .rt { &:empty {opacity: 0.3;} } } a[href].rawlink { @extend %teletype; } body.doc main { @extend %serif; ................................................................................ &+label:hover { background-color: otone(-35%); color: white; } &:checked+label { border-top: 1px solid otone(-10%); border-bottom: 1px solid otone(-50%); background: linear-gradient(to bottom, otone(-25%,-0.2), otone(-28%,-0.3) 35%, otone(-30%,-0.5)); color: white; box-shadow: 0 0 0 1px tone(-60%); &:hover { border-top: 1px solid otone(10%); border-bottom: 1px solid otone(-60%); font-weight: bold; } ................................................................................ transform: rotate(90deg) scale(1.1); color: tone(-20%); text-shadow: 0 0 8px tone(-30%); } } } } div.lede { display: grid; grid-template-columns: 1fr min-content; grid-template-rows: 1.5em 1fr; padding: 0.1in 0.3in; margin: 0 -0.2in; margin-top: 0.2in; border-radius: 3px; background: linear-gradient(to bottom, tone(-40%,-0.5), transparent); border-top: 1px solid tone(-5%,-0.7); > .promo { grid-row: 1/2; grid-column: 1/2; font-style: italic; font-size: 90%; color: tone(-10%); > img { vertical-align: middle; margin-right: 0.4em; width: 1em; height: 1em; } } > a[href].del { grid-row: 1/2; grid-column: 2/3; text-decoration: none; } > .post { grid-row: 2/3; grid-column: 1/3; } } |
Modified store.t from [da1c9184b0] to [6a465decce].
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 .. 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 .. 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 ... 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 ... 381 382 383 384 385 386 387 388 389 390 391 392 393 394 ... 413 414 415 416 417 418 419 420 421 422 423 424 425 426 |
local m = {
timepoint = lib.osclock.time_t;
scope = lib.enum {
'public', 'private', 'local';
'personal', 'direct', 'circle';
};
notiftype = lib.enum {
'mention', 'like', 'rt', 'react'
};
relation = lib.set {
'silence', -- messages will not be accepted
'collapse', -- posts will be collapsed by default
'disemvowel', -- posts will be ritually humiliated, but shown
'avoid', -- posts will be kept out of the timeline but will show on users' posts and in conversations
................................................................................
'mute', -- posts will be completely hidden at all times
'block', -- no interactions will be permitted, but posts will remain visible
};
credset = lib.set {
'pw', 'otp', 'challenge', 'trust'
};
privset = lib.set {
'post', 'edit', 'acct', 'upload', 'censor', 'admin', 'invite'
};
powerset = lib.set {
-- user powers -- default on
'login', -- not locked out
'visible', -- account & posts can be seen by others
'post', -- can do poasts
'shout', -- posts show up on local timeline
'propagate', -- posts are sent to other instances
'artifact', -- upload, claim, and manage artifacts
'acct', -- configure own account
'edit'; -- edit own poasts
-- admin powers -- default off
'purge', -- permanently delete users
'config', -- change daemon policy & config UI
'censor', -- dispose of badthink
'discipline', -- enforced timeouts, stripping badges and epithets, punitive actions that do not permanently deprive of powers; can remove own injunctions but not others'
'vacate', -- can remove others' injunctions, but not apply them
'cred', -- alter credentials
'elevate', 'demote', -- change user rank, give and take powers, including the ability to log in
'rebrand', -- modify site's brand identity
'herald', -- grant serverwide epithets and badges
'invite' -- *unlimited* invites
};
prepmode = lib.enum {
'full','conf','admin'
}
}
................................................................................
var pow: m.powerset pow:clear()
(pow.login << true)
(pow.visible << true)
(pow.post << true)
(pow.shout << true)
(pow.propagate << true)
(pow.artifact << true)
(pow.acct << true)
(pow.edit << true)
return m.rights { rank = 0, quota = 1000, invites = 0, powers = pow; }
end
struct m.actor {
id: uint64
nym: str
................................................................................
mentions: lib.mem.ptr(uint64)
circles: lib.mem.ptr(uint64) --only meaningful if scope is set to circle
convoheaduri: str
parent: uint64
-- ephemera
localpost: bool
accent: int16
depth: uint16 -- used in conversations to indicate tree depth
source: &m.source
-- save :: bool -> {} (defined in acl.t due to dep. hell)
}
m.user_conf_funcs = function(be,n,ty,rty,rty2)
rty = rty or ty
................................................................................
-- origin: inet
-- cookie issue time: m.timepoint
actor_auth_register_uid: {&m.source, uint64, uint64} -> {}
-- notifies the backend module of the UID that has been assigned for
-- an authentication ID
-- aid: uint64
-- uid: uint64
auth_enum_uid: {&m.source, uint64} -> lib.mem.lstptr(m.auth)
auth_enum_handle: {&m.source, rawstring} -> lib.mem.lstptr(m.auth)
auth_attach_pw: {&m.source, uint64, bool, pstr, pstr} -> {}
-- uid: uint64
-- reset: bool (delete other passwords?)
-- pw: pstring
................................................................................
post_enum_author_uid: {&m.source, uint64, m.range} -> lib.mem.lstptr(m.post)
post_enum_parent: {&m.source, uint64} -> lib.mem.lstptr(m.post)
post_attach_ctl: {&m.source, uint64, uint64, bool} -> {}
-- attaches or detaches an existing database artifact
-- post id: uint64
-- artifact id: uint64
-- detach: bool
thread_latest_arrival_calc: {&m.source, uint64} -> m.timepoint
artifact_instantiate: {&m.source, lib.mem.ptr(uint8), lib.mem.ptr(int8)} -> uint64
-- instantiate an artifact in the database, either installing a new
-- artifact or returning the id of an existing artifact with the same hash
-- artifact: bytea
|
| | | > > | | > > > > > > > > > > > |
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 .. 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 .. 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 ... 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 ... 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 ... 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 |
local m = {
timepoint = lib.osclock.time_t;
scope = lib.enum {
'public', 'private', 'local';
'personal', 'direct', 'circle';
};
notiftype = lib.enum {
'none', 'mention', 'like', 'rt', 'react'
};
relation = lib.set {
'silence', -- messages will not be accepted
'collapse', -- posts will be collapsed by default
'disemvowel', -- posts will be ritually humiliated, but shown
'avoid', -- posts will be kept out of the timeline but will show on users' posts and in conversations
................................................................................
'mute', -- posts will be completely hidden at all times
'block', -- no interactions will be permitted, but posts will remain visible
};
credset = lib.set {
'pw', 'otp', 'challenge', 'trust'
};
privset = lib.set {
'post', 'edit', 'account', 'upload', 'moderate', 'admin', 'invite'
};
powerset = lib.set {
-- user powers -- default on
'login', -- not locked out
'visible', -- account & posts can be seen by others
'post', -- can do poasts
'shout', -- posts show up on local timeline
'propagate', -- posts are sent to other instances
'artifact', -- upload, claim, and manage artifacts
'account', -- configure own account
'edit'; -- edit own poasts
'snitch'; -- can issue badthink reports
-- admin powers -- default off
'purge', -- permanently delete users
'config', -- change daemon policy & config UI
'censor', -- dispose of badthink
'discipline', -- enforced timeouts, stripping badges and epithets, punitive actions that do not permanently deprive of powers; can remove own injunctions but not others'
'vacate', -- can remove others' injunctions, but not apply them
'cred', -- alter credentials
'elevate', 'demote', -- change user rank, give and take powers, including the ability to log in
'rebrand', -- modify site's brand identity
'herald', -- grant serverwide epithets and badges
'crier', -- can promote content to the instance page
'invite' -- *unlimited* invites
};
prepmode = lib.enum {
'full','conf','admin'
}
}
................................................................................
var pow: m.powerset pow:clear()
(pow.login << true)
(pow.visible << true)
(pow.post << true)
(pow.shout << true)
(pow.propagate << true)
(pow.artifact << true)
(pow.account << true)
(pow.edit << true)
return m.rights { rank = 0, quota = 1000, invites = 0, powers = pow; }
end
struct m.actor {
id: uint64
nym: str
................................................................................
mentions: lib.mem.ptr(uint64)
circles: lib.mem.ptr(uint64) --only meaningful if scope is set to circle
convoheaduri: str
parent: uint64
-- ephemera
localpost: bool
accent: int16
rts: uint32
likes: uint32
rtdby: uint64 -- 0 if not rt
rtact: uint64 -- 0 if not rt, id of rt action otherwise
source: &m.source
-- save :: bool -> {} (defined in acl.t due to dep. hell)
}
m.user_conf_funcs = function(be,n,ty,rty,rty2)
rty = rty or ty
................................................................................
-- origin: inet
-- cookie issue time: m.timepoint
actor_auth_register_uid: {&m.source, uint64, uint64} -> {}
-- notifies the backend module of the UID that has been assigned for
-- an authentication ID
-- aid: uint64
-- uid: uint64
actor_notifs_fetch: {&m.source, uint64} -> lib.mem.lstptr(m.notif)
auth_enum_uid: {&m.source, uint64} -> lib.mem.lstptr(m.auth)
auth_enum_handle: {&m.source, rawstring} -> lib.mem.lstptr(m.auth)
auth_attach_pw: {&m.source, uint64, bool, pstr, pstr} -> {}
-- uid: uint64
-- reset: bool (delete other passwords?)
-- pw: pstring
................................................................................
post_enum_author_uid: {&m.source, uint64, m.range} -> lib.mem.lstptr(m.post)
post_enum_parent: {&m.source, uint64} -> lib.mem.lstptr(m.post)
post_attach_ctl: {&m.source, uint64, uint64, bool} -> {}
-- attaches or detaches an existing database artifact
-- post id: uint64
-- artifact id: uint64
-- detach: bool
post_retweet: {&m.source, uint64, uint64, bool} -> {}
post_like: {&m.source, uint64, uint64, bool} -> {}
-- undo: bool
post_react: {&m.source, uint64, uint64, pstring} -> {}
-- emoji: pstring (null to delete previous reaction, otherwise adds/changes)
post_liked_uid: {&m.source, uint64, uint64} -> bool
post_reacted_uid: {&m.source, uint64, uint64} -> bool
thread_latest_arrival_calc: {&m.source, uint64} -> m.timepoint
artifact_instantiate: {&m.source, lib.mem.ptr(uint8), lib.mem.ptr(int8)} -> uint64
-- instantiate an artifact in the database, either installing a new
-- artifact or returning the id of an existing artifact with the same hash
-- artifact: bytea
|
Modified str.t from [1e93ad8eb5] to [638f6c2759].
178 179 180 181 182 183 184 185 186 187 188 189 190 191 |
self.buf = [rawstring](lib.mem.heapr_raw(self.buf, self.space))
end
lib.mem.cpy(self.buf + self.sz, str, len)
self.sz = self.sz + len
self.buf[self.sz] = 0
return self
end;
m.lit = macro(function(str)
if str:asvalue() ~= nil then
return `[lib.mem.ref(int8)] {ptr = [str:asvalue()], ct = [#(str:asvalue())]}
else
return `[lib.mem.ref(int8)] {ptr = nil, ct = 0}
end
|
> > > > > > > > > > > > > |
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 |
self.buf = [rawstring](lib.mem.heapr_raw(self.buf, self.space)) end lib.mem.cpy(self.buf + self.sz, str, len) self.sz = self.sz + len self.buf[self.sz] = 0 return self end; terra m.acc:ipush(i: intptr) var decbuf: int8[21] var si = lib.math.decstr_friendly(i, &decbuf[20]) var len: intptr = [decbuf.type.N] - (si - &decbuf[0]) return self:push(si,len) end terra m.acc:shpush(i: uint64) var sbuf: int8[lib.math.shorthand.maxlen] var len = lib.math.shorthand.gen(i,&sbuf[0]) return self:push(&sbuf[0], len) end m.lit = macro(function(str) if str:asvalue() ~= nil then return `[lib.mem.ref(int8)] {ptr = [str:asvalue()], ct = [#(str:asvalue())]} else return `[lib.mem.ref(int8)] {ptr = nil, ct = 0} end |
Modified view/confirm.tpl from [3b921f59eb] to [0d2952df9c].
1 2 3 4 5 6 7 8 9 |
<form class="message" method="post">
<img class="icon" src="/s/query.svg">
<h1>@title</h1>
<p>@query</p>
<menu class="horizontal choice">
<a class="button" href="@:cancel">cancel</a>
<button name="act" value="confirm">confirm</button>
</menu>
</form>
|
| |
1 2 3 4 5 6 7 8 9 |
<form class="message" method="post">
<img class="icon" src="/s/query.webp">
<h1>@title</h1>
<p>@query</p>
<menu class="horizontal choice">
<a class="button" href="@:cancel">cancel</a>
<button name="act" value="confirm">confirm</button>
</menu>
</form>
|
Modified view/tweet.tpl from [e117899db4] to [aefe28dd4e].
1 2 3 4 5 6 7 8 9 |
<div class="post"@attr> <div class="avatar"><img src="@:avatar"></div> <a class="username" href="/@:acctlink">@nym</a> <div class="content"> <div class="subject">@!subject</div> <div class="text">@text</div> </div> <a class="permalink" href="@permalink">@when</a> </div> |
> |
1 2 3 4 5 6 7 8 9 10 |
<div class="post"@attr>
<div class="avatar"><img src="@:avatar"></div>
<a class="username" href="/@:acctlink">@nym</a>
<div class="content">
<div class="subject">@!subject</div>
<div class="text">@text</div>
</div>
@stats
<a class="permalink" href="@permalink">@when</a>
</div>
|