Index: backend/pgsql.t ================================================================== --- backend/pgsql.t +++ backend/pgsql.t @@ -24,46 +24,26 @@ ]]; }; actor_fetch_uid = { params = {uint64}, sql = [[ - 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, - a.invites - - from parsav_actors as a - left join parsav_servers as s - on a.origin = s.id - where a.id = $1::bigint + select (pg_temp.parsavpg_translate_actor(a)).* + from parsav_actors as a + where a.id = $1::bigint ]]; }; actor_fetch_xid = { params = {pstring}, sql = [[ - 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, - a.invites, - - coalesce(s.domain, - (select value from parsav_config - where key='domain' limit 1)) as domain - - from parsav_actors as a - left join parsav_servers as s - on a.origin = s.id - - where $1::text = (a.handle || '@' || domain) or - $1::text = ('@' || a.handle || '@' || domain) or + with txd as ( + select (pg_temp.parsavpg_translate_actor(a)).* from parsav_actors as a + ) + select * from txd as a where $1::text = xid or (a.origin is null and $1::text = a.handle or - $1::text = ('@' || a.handle)) + $1::text = (a.handle ||'@'|| + (select value from parsav_config where key='domain'))) ]]; }; actor_purge_uid = { params = {uint64}, cmd = true, sql = [[ @@ -104,25 +84,24 @@ actor_create = { params = { rawstring, rawstring, uint64, lib.store.timepoint, rawstring, rawstring, lib.mem.ptr(uint8), rawstring, uint16, uint32, uint32 - }; - sql = [[ + }, sql = [[ insert into parsav_actors ( nym,handle, origin,knownsince, bio,avataruri,key, epithet,rank,quota, - invites + invites,authtime ) values ($1::text, $2::text, case when $3::bigint = 0 then null else $3::bigint end, - to_timestamp($4::bigint), + $4::bigint, $5::bigint, $6::bigint, $7::bytea, $8::text, $9::smallint, $10::integer, - $11::integer + $11::integer,$4::bigint ) returning id ]]; }; actor_auth_pw = { @@ -140,44 +119,39 @@ actor_enum_local = { params = {}, sql = [[ select id, nym, handle, origin, bio, null::text, rank, quota, key, epithet, - extract(epoch from knownsince)::bigint, + knownsince::bigint, '@' || handle, invites from parsav_actors where origin is null order by nullif(rank,0) nulls last, handle ]]; }; actor_enum = { params = {}, sql = [[ - 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, - invites - from parsav_actors a - left join parsav_servers s on s.id = a.origin + select (pg_temp.parsavpg_translate_actor(a)).* + from parsav_actors as a + order by nullif(a.rank,0) nulls last, a.handle, a.origin ]]; }; actor_stats = { - params = {uint64}, sql = ([[ + params = {uint64}, sql = [[ with tweets as ( select from parsav_posts where author = $1::bigint ), follows as ( select relatee as user from parsav_rels - where relator = $1::bigint and kind = + where relator = $1::bigint and kind = ), followers as ( select relator as user from parsav_rels - where relatee = $1::bigint and kind = + where relatee = $1::bigint and kind = ), mutuals as ( select * from follows intersect select * from followers ) @@ -185,11 +159,11 @@ (select count(tweets.*)::bigint from tweets), (select count(follows.*)::bigint from follows), (select count(followers.*)::bigint from followers), (select count(mutuals.*)::bigint from mutuals) ) - ]]):gsub('<(%w+)>',function(r) return tostring(lib.store.relation.idvmap[r]) end) + ]] }; actor_auth_how = { params = {rawstring, lib.store.inet}, sql = [[ with mts as (select a.kind from parsav_auth as a @@ -208,15 +182,11 @@ ]]; -- cheat }; actor_session_fetch = { params = {uint64, lib.store.inet, int64}, sql = [[ - 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, + select (pg_temp.parsavpg_translate_actor(a)).*, au.restrict, array['post' ] <@ au.restrict, array['edit' ] <@ au.restrict, array['account' ] <@ au.restrict, @@ -224,17 +194,16 @@ 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 ($3::bigint = 0 or --slightly abusing the epoch time fmt here, but - ((a.authtime is null or a.authtime <= to_timestamp($3::bigint)) and - (au.valperiod is null or au.valperiod <= to_timestamp($3::bigint)))) + ((a.authtime is null or a.authtime <= $3::bigint) and + (au.valperiod is null or au.valperiod <= $3::bigint))) ]]; }; actor_powers_fetch = { params = {uint64}, sql = [[ @@ -255,33 +224,65 @@ delete from parsav_rights where actor = $1::bigint and key = $2::text ]] }; + + actor_rel_create = { + params = {uint16,uint64, uint64}, cmd = true, sql = [[ + insert into parsav_rels (kind,relator,relatee) + values($1::smallint, $2::bigint, $3::bigint) + on conflict do nothing + ]]; + }; + + actor_rel_destroy = { + params = {uint16,uint64, uint64}, cmd = true, sql = [[ + delete from parsav_rels where + kind = $1::smallint and + relator = $2::bigint and + relatee = $3::bigint + ]]; + }; + + actor_rel_enum = { + params = {uint64, uint64}, sql = [[ + select kind from parsav_rels where + relator = $1::bigint and + relatee = $2::bigint + ]]; + }; + + actor_notice_enum = { + params = {uint64}, sql = [[ + select (notice).* from pg_temp.parsavpg_notices + where rcpt = $1::bigint + ]]; + }; auth_sigtime_user_fetch = { params = {uint64}, sql = [[ - select extract(epoch from authtime)::bigint + select authtime::bigint from parsav_actors where id = $1::bigint ]]; }; auth_sigtime_user_alter = { params = {uint64,int64}, cmd = true, sql = [[ update parsav_actors set - authtime = to_timestamp($2::bigint) + authtime = $2::bigint where id = $1::bigint ]]; }; auth_create_pw = { - params = {uint64, binblob, pstring}, cmd = true, sql = [[ - insert into parsav_auth (uid, name, kind, cred, comment) values ( + params = {uint64, binblob, int64, pstring}, cmd = true, sql = [[ + insert into parsav_auth (uid, name, kind, cred, valperiod, comment) values ( $1::bigint, (select handle from parsav_actors where id = $1::bigint), 'pw-sha256', $2::bytea, - $3::text + $3::bigint, $4::text ) ]] }; auth_purge_type = { @@ -312,11 +313,11 @@ update parsav_posts set subject = $4::text, acl = $5::text, body = $6::text, chgcount = $2::integer, - edited = to_timestamp($3::bigint) + edited = $3::bigint where id = $1::bigint ]] }; post_create = { @@ -329,11 +330,11 @@ parent, posted, discovered, circles, mentions, convoheaduri ) values ( $1::bigint, case when $2::text = '' then null else $2::text end, $3::text, $4::text, - $5::bigint, to_timestamp($6::bigint), now(), + $5::bigint, $6::bigint, $6::bigint, array[]::bigint[], array[]::bigint[], $7::text ) returning id ]]; -- TODO array handling }; @@ -351,57 +352,23 @@ ]] }; 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 - ]]; + select (p.post).* + from pg_temp.parsavpg_known_content as p + where (p.post).id = $1::bigint and (p.post).rtdby = 0 + ]] }; 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 - ]] + select (p.post).* + from pg_temp.parsavpg_known_content as p + where (p.post).parent = $1::bigint and (p.post).rtdby = 0 + order by (p.post).posted, (p.post).discovered asc + ]]; }; thread_latest_arrival_calc = { params = {uint64}, sql = [[ with recursive posts(id) as ( @@ -416,18 +383,18 @@ from posts inner join parsav_posts as p on p.id = posts.id ) - select extract(epoch from max(m))::bigint from maxes + select 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 + params = {uint64, uint64, pstring, int64}, sql = [[ + insert into parsav_acts (kind,actor,subject,time) values ( + $3::text, $1::bigint, $2::bigint, $4::bigint ) returning id ]]; }; post_react_cancel = { @@ -448,92 +415,71 @@ ]] }; 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) - ), + select (c.post).* + from pg_temp.parsavpg_known_content as c - 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) - ), + where c.promoter = $5::bigint and + ($1::bigint = 0 or c.tltime <= $1::bigint) and + ($2::bigint = 0 or $2::bigint < c.tltime) + order by c.tltime desc - 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 (c.post).* + from pg_temp.parsavpg_known_content as c + + where (c.post).localpost = true and + ($1::bigint = 0 or c.tltime <= $1::bigint) and + ($2::bigint = 0 or $2::bigint < c.tltime) + order by c.tltime desc + + limit case when $3::bigint = 0 then null + else $3::bigint end + offset $4::bigint + ]]; + }; + + timeline_actor_fetch = { + params = {uint64, uint64, uint64, uint64, uint64}, sql = [[ + with followed as ( + select relatee from parsav_rels where + kind = and + relator = $1::bigint + ), avoided as ( + select relatee as avoidee from parsav_rels where + kind = or kind = and + relator = $1::bigint + union select relator as avoidee from parsav_rels where + kind = and + relatee = $1::bigint ) - 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 - ]] + select (c.post).* + from pg_temp.parsavpg_known_content as c + + where ($2::bigint = 0 or c.tltime <= $2::bigint) and + ($3::bigint = 0 or $3::bigint < c.tltime) and + (c.promoter in (table followed) or + c.promoter = $1::bigint) and + not ((c.post).author in (table avoided)) + order by c.tltime desc + + limit case when $4::bigint = 0 then null + else $4::bigint end + offset $5::bigint + ]]; }; artifact_instantiate = { params = {binblob, binblob, pstring}, sql = [[ insert into parsav_artifacts (content,hash,mime) values ( @@ -746,22 +692,48 @@ for j=0,sz do buf[4 + j] = i.v6[j] end -- 😬 return buf end end; } + +local sqlvars = {} +for i, n in ipairs(lib.store.noticetype.members) do + sqlvars['notice:' .. n] = lib.store.noticetype[n] +end + +for i, n in ipairs(lib.store.relation.members) do + sqlvars['rel:' .. n] = lib.store.relation.idvmap[n] +end local con = symbol(&lib.pq.PGconn) -local prep = {} local function sqlsquash(s) return s :gsub('%%include (.-)%%',function(f) return sqlsquash(lib.util.ingest('backend/schema/' .. f)) end) -- include dependencies :gsub('%-%-.-\n','') -- remove disruptive line comments :gsub('%-%-.-$','') -- remove unnecessary terminal comments + :gsub('<(%g-)>',function(r) return tostring(sqlvars[r]) end) :gsub('%s+',' ') -- remove whitespace :gsub('^%s*(.-)%s*$','%1') -- chomp end + +-- to simplify queries and reduce development headaches in general, we +-- offload as much logic as possible into views. to avoid versioning +-- difficulties, these views are not part of the schema, but are rather +-- uploaded to the database at the start of a parsav connection, visible +-- only to the connecting parsav instance, stored in memory, and dropped +-- as soon as the connection session ends. + +local tempviews = sqlsquash(lib.util.ingest 'backend/schema/pgsql-views.sql') +local prep = { quote + var res = lib.pq.PQexec([con], tempviews) + if lib.pq.PQresultStatus(res) == lib.pq.PGRES_COMMAND_OK then + lib.dbg('uploading pgsql session views') + else + lib.bail('backend pgsql - failed to upload session views: \n', lib.pq.PQresultErrorMessage(res)) + end +end } for k,q in pairs(queries) do local qt = sqlsquash(q.sql) local stmt = 'parsavpg_' .. k terra q.prep([con]) @@ -981,12 +953,12 @@ [vdrs] lib.dbg(['could not find password hash']) end end -local schema = sqlsquash(lib.util.ingest('backend/schema/pgsql.sql')) -local obliterator = sqlsquash(lib.util.ingest('backend/schema/pgsql-drop.sql')) +local schema = sqlsquash(lib.util.ingest 'backend/schema/pgsql.sql') +local obliterator = sqlsquash(lib.util.ingest 'backend/schema/pgsql-drop.sql') local privupdate = terra( src: &lib.store.source, ac: &lib.store.actor ): {} @@ -1083,11 +1055,11 @@ close = [terra(src: &lib.store.source) lib.pq.PQfinish([&lib.pq.PGconn](src.handle)) end]; tx_enter = txdo, tx_complete = txdone; - conprep = [terra(src: &lib.store.source, mode: lib.store.prepmode.t) + conprep = [terra(src: &lib.store.source, mode: lib.store.prepmode.t): {} var [con] = [&lib.pq.PGconn](src.handle) if mode == lib.store.prepmode.full then [prep] elseif mode == lib.store.prepmode.conf or mode == lib.store.prepmode.admin then queries.conf_get.prep(con) @@ -1096,11 +1068,11 @@ if mode == lib.store.prepmode.admin then end else lib.bail('unsupported connection preparation mode') end end]; - dbsetup = [terra(src: &lib.store.source) + dbsetup = [terra(src: &lib.store.source): bool var res = lib.pq.PQexec([&lib.pq.PGconn](src.handle), schema) if lib.pq.PQresultStatus(res) == lib.pq.PGRES_COMMAND_OK then lib.report('successfully instantiated schema in database') return true else @@ -1107,11 +1079,11 @@ lib.warn('backend pgsql - failed to initialize database: \n', lib.pq.PQresultErrorMessage(res)) return false end end]; - obliterate_everything = [terra(src: &lib.store.source) + obliterate_everything = [terra(src: &lib.store.source): bool var res = lib.pq.PQexec([&lib.pq.PGconn](src.handle), obliterator) if lib.pq.PQresultStatus(res) == lib.pq.PGRES_COMMAND_OK then lib.report('successfully wiped out everything parsav-related in database') return true else @@ -1244,18 +1216,18 @@ 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? + if not r:null(0,13) 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)) + (au.val.privs.post << r:bool(0,14)) + (au.val.privs.edit << r:bool(0,15)) + (au.val.privs.account << r:bool(0,16)) + (au.val.privs.upload << r:bool(0,17)) + (au.val.privs.moderate<< r:bool(0,18)) + (au.val.privs.admin << r:bool(0,19)) else au.val.privs:fill() end return au, a end @@ -1303,12 +1275,13 @@ src: &lib.store.source, uid: uint64, post: uint64, undo: bool ): {} + var time = lib.osclock.time(nil) if not undo then - queries.post_react_simple.exec(src,uid,post,"rt") + queries.post_react_simple.exec(src,uid,post,"rt",time) else queries.post_react_cancel.exec(src,uid,post,"rt") end end]; post_like = [terra( @@ -1315,12 +1288,13 @@ src: &lib.store.source, uid: uint64, post: uint64, undo: bool ): {} + var time = lib.osclock.time(nil) if not undo then - queries.post_react_simple.exec(src,uid,post,"like") + queries.post_react_simple.exec(src,uid,post,"like",time) else queries.post_react_cancel.exec(src,uid,post,"like") end end]; post_liked_uid = [terra( @@ -1393,10 +1367,56 @@ privupdate(src,ac) lib.dbg('powers established') return ac.id end]; + + actor_rel_create = [terra( + src: &lib.store.source, + kind: uint16, + relator: uint64, + relatee: uint64 + ): {} queries.actor_rel_create.exec(src,kind,relator,relatee) end]; + + actor_rel_destroy = [terra( + src: &lib.store.source, + kind: uint16, + relator: uint64, + relatee: uint64 + ): {} queries.actor_rel_destroy.exec(src,kind,relator,relatee) end]; + + actor_rel_calc = [terra( + src: &lib.store.source, + relator: uint64, + relatee: uint64 + ): lib.store.relationship + var r = lib.store.relationship { + agent = relator, patient = relatee + } r.rel:clear() + r.recip:clear() + + var res = queries.actor_rel_enum.exec(src,relator,relatee) + var recip = queries.actor_rel_enum.exec(src,relatee,relator) + + if res.sz > 0 then defer res:free() + for i = 0, res.sz do + var bit = res:int(uint16, i, 0)-1 + if bit < [#lib.store.relation.members] then r.rel:setbit(bit, true) + else lib.warn('unknown relationship type in database') end + end + end + + if recip.sz > 0 then defer recip:free() + for i = 0, recip.sz do + var bit = recip:int(uint16, i, 0)-1 + if bit < [#lib.store.relation.members] then r.recip:setbit(bit, true) + else lib.warn('unknown relationship type in database') end + end + end + + return r + end]; actor_purge_uid = [terra( src: &lib.store.source, uid: uint64 ) queries.actor_purge_uid.exec(src,uid) end]; @@ -1434,11 +1454,11 @@ if lib.md.mbedtls_md(lib.md.mbedtls_md_info_from_type(lib.crypt.alg.sha256.id), [&uint8](pw.ptr), pw.ct, &hash[0]) ~= 0 then lib.bail('cannot hash password') end if reset then queries.auth_purge_type.exec(src, nil, uid, 'pw-%') end - queries.auth_create_pw.exec(src, uid, binblob {ptr = &hash[0], ct = [hash.type.N]}, comment) + queries.auth_create_pw.exec(src, uid, binblob {ptr = &hash[0], ct = [hash.type.N]}, lib.osclock.time(nil), comment) end]; auth_purge_pw = [terra(src: &lib.store.source, uid: uint64, handle: rawstring): {} queries.auth_purge_type.exec(src, handle, uid, 'pw-%') end]; Index: backend/schema/pgsql-auth.sql ================================================================== --- backend/schema/pgsql-auth.sql +++ backend/schema/pgsql-auth.sql @@ -41,15 +41,15 @@ blacklist bool not null default false, -- if the credential matches, access will be denied, even if -- non-blacklisted credentials match. most useful with -- uid = null, kind = trust, cidr = (untrusted IP range) - valperiod timestamp default now(), + valperiod bigint not null, -- cookies bearing timestamps earlier than this point in time -- will be considered invalid and will not grant access comment text, -- a field the user can use to identify the specific credential, -- in order to aid credential management unique(name,kind,cred) ); Index: backend/schema/pgsql-drop.sql ================================================================== --- backend/schema/pgsql-drop.sql +++ backend/schema/pgsql-drop.sql @@ -5,11 +5,10 @@ drop table if exists parsav_actors cascade; drop table if exists parsav_actor_conf_strs cascade; drop table if exists parsav_actor_conf_ints cascade; drop table if exists parsav_rights cascade; drop table if exists parsav_posts cascade; -drop table if exists parsav_conversations cascade; drop table if exists parsav_rels cascade; drop table if exists parsav_acts cascade; drop table if exists parsav_log cascade; drop table if exists parsav_artifacts cascade; drop table if exists parsav_artifact_claims cascade; ADDED backend/schema/pgsql-views.sql Index: backend/schema/pgsql-views.sql ================================================================== --- backend/schema/pgsql-views.sql +++ backend/schema/pgsql-views.sql @@ -0,0 +1,180 @@ +-- these views are not part of the schema proper, but rather are uploaded +-- into postgres' memory by parsav at the beginning of a connection. they +-- are not visible to other clients and politely disappear once the +-- connection terminates, allowing us to simultaneously avoid versioning +-- headaches, limit the amount of data we need to send to the server, and +-- reduce the compilation time of our prepared queries. + +create or replace temp view parsavpg_post_react_counts as ( + 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 p.id as post, + coalesce((select counts.ct from counts where counts.subject = p.id + and counts.kind = 'like'),0)::integer as likes, + coalesce((select counts.ct from counts where counts.subject = p.id + and counts.kind = 'rt' ),0)::integer as rts + from parsav_posts as p +); + +create type pg_temp.parsavpg_intern_notice as ( + kind smallint, + "when" bigint, + who bigint, + what bigint, + reply bigint, + reaction text +); + +create type pg_temp.parsavpg_intern_actor as ( + id bigint, + nym text, + handle text, + origin bigint, + bio text, + avataruri text, + rank smallint, + quota integer, + key bytea, + epithet text, + knownsince bigint, + xid text, + invites integer +); + +create or replace function +pg_temp.parsavpg_translate_actor(parsav_actors) +returns pg_temp.parsavpg_intern_actor as $$ + select + ($1).id, ($1).nym, ($1).handle, ($1).origin, ($1).bio, + ($1).avataruri, ($1).rank, ($1).quota, ($1).key, ($1).epithet, + ($1).knownsince::bigint, + coalesce(($1).handle || '@' || + (select domain from parsav_servers as s where s.id = ($1).origin), + '@' || ($1).handle) as xid, + ($1).invites +$$ language sql; + +--drop type if exists pg_temp.parsavpg_intern_post; +create type pg_temp.parsavpg_intern_post as ( + -- order is crucially important, and must match the order used + -- in row_to_actor. names don't matter + localpost bool, + id bigint, + author bigint, + subject text, + acl text, + body text, + posted bigint, + discovered bigint, + edited bigint, + parent bigint, + convoheaduri text, + chgcount integer, + accent smallint, + rtdby bigint, -- note that these must be 0 if the record + rtid bigint, -- in question does not represent an RT! + n_likes integer, + n_rts integer +); + +create or replace function +pg_temp.parsavpg_translate_post(parsav_posts,bigint,bigint) +returns pg_temp.parsavpg_intern_post as $$ + select a.origin is null, + ($1).id, ($1).author, + ($1).subject,($1).acl, ($1).body, + ($1).posted, ($1).discovered, ($1).edited, + ($1).parent, ($1).convoheaduri,($1).chgcount, + coalesce(c.value, -1)::smallint, + $2 as rtdby, $3 as rtid, + re.likes, re.rts + from parsav_actors as a + left join parsav_actor_conf_ints as c + on c.key = 'ui-accent' and + c.uid = a.id + left join pg_temp.parsavpg_post_react_counts as re + on re.post = ($1).id + where a.id = ($1).author +$$ language sql; + +create or replace temp view parsavpg_known_content as ( + with posts as ( + select p as orig, + null::bigint as promoter, + null::bigint as promotion, + coalesce(p.posted,p.discovered) as promotime + from parsav_posts as p + ), + + rts as ( + select p as orig, + a.actor as promoter, + a.id as promotion, + a.time as promotime + from parsav_acts as a + inner join parsav_posts as p on a.subject = p.id + where a.kind = 'rt' + ), + + content as (select * from posts union select * from rts) + + select pg_temp.parsavpg_translate_post(cn.orig, + coalesce(cn.promoter,0), coalesce(cn.promotion,0) + ) as post, + cn.promotime::bigint as tltime, + coalesce(cn.promoter, (cn.orig).author) as promoter + from content as cn + order by cn.promotime desc +); + +-- +--create temp view parsavpg_post_threads as ( +-- +--); +-- +create temp view parsavpg_notices as ( + -- TODO add mentions + with ntimes as ( + select uid, value as when from parsav_actor_conf_ints where key = 'notice-clear-time' + ), acts as ( + select row( + kmap.kind::smallint, + a.time, + a.actor, + a.subject, + null::bigint, + null::text + )::pg_temp.parsavpg_intern_notice as notice, + p.author as rcpt + from parsav_acts as a + inner join parsav_posts as p on a.subject = p.id + inner join (values + ('rt', 4 ), + ('like', 3 ), + ('react', 5 ) + ) as kmap(kstr,kind) on kmap.kstr = a.kind + left join ntimes as nt on nt.uid = p.author + where a.time >= coalesce(nt.when,0) + ), replies as ( + select row( + 2::smallint, + coalesce(p.posted,p.discovered), + p.author, + p.parent, + p.id, + null::text + )::pg_temp.parsavpg_intern_notice as notice, + par.author as rcpt + from parsav_posts as p + inner join parsav_posts as par on p.parent = par.id + left join ntimes as nt on nt.uid = p.author + where p.discovered >= coalesce(nt.when,0) + ), allnotices as (select * from acts union select * from replies) + + table allnotices order by (notice).when desc +); + Index: backend/schema/pgsql.sql ================================================================== --- backend/schema/pgsql.sql +++ backend/schema/pgsql.sql @@ -14,32 +14,32 @@ -- note that valid ids should always > 0, as 0 is reserved for null -- on the client side, vastly simplifying code create table parsav_servers ( id bigint primary key default (1+random()*(2^63-1))::bigint, - domain text not null, + domain text not null unique, key bytea, - knownsince timestamp, + knownsince bigint, parsav boolean -- whether to use parsav protocol extensions ); create table parsav_actors ( id bigint primary key default (1+random()*(2^63-1))::bigint, nym text, handle text not null, -- nym [@handle@origin] origin bigint references parsav_servers(id) on delete cascade, -- null origin = local actor - knownsince timestamp not null default now(), + knownsince bigint not null, bio text, avatarid bigint, -- artifact id, null if remote avataruri text, -- null if local rank smallint not null default 0, quota integer not null default 1000, invites integer not null default 0, key bytea, -- private if localactor; public if remote epithet text, - authtime timestamp not null default now(), -- cookies earlier than this timepoint will not be accepted + authtime bigint not null, -- cookies earlier than this timepoint will not be accepted unique (handle,origin) ); create table parsav_rights ( @@ -49,22 +49,22 @@ allow boolean not null, scope bigint, -- for future expansion primary key (key,actor) ); +create index on parsav_rights (actor); create table parsav_posts ( id bigint primary key default (1+random()*(2^63-1))::bigint, - author bigint references parsav_actors(id) - on delete cascade, + author bigint references parsav_actors(id) on delete cascade, subject text, acl text not null default 'all', -- just store the script raw 🤷 body text, - posted timestamp not null, - discovered timestamp not null, + posted bigint not null, + discovered bigint not null, chgcount integer not null default 0, - edited timestamp, + edited bigint, parent bigint not null default 0, -- if post: part of conversation; if chatroom: top-level post circles bigint[], -- TODO at edit or creation, iterate through each circle mentions bigint[], -- a user has, check if it can see her post, and if so add artifacts bigint[], @@ -71,10 +71,12 @@ convoheaduri text -- only used for tracking foreign conversations and tying them to post heads; -- local conversations are tracked directly and mapped to URIs based on the -- head's ID. null if native tweet or not the first tweet in convo ); +create index on parsav_posts (author); +create index on parsav_posts (parent); create table parsav_rels ( relator bigint references parsav_actors(id) on delete cascade, -- e.g. follower relatee bigint references parsav_actors(id) @@ -85,56 +87,60 @@ ); 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, + time bigint not null, + 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 index on parsav_acts (subject); +create index on parsav_acts (actor); +create index on parsav_acts (time); 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(), + time bigint not null, actor bigint references parsav_actors(id) on delete cascade, post bigint not null ); create table parsav_artifacts ( id bigint primary key default (1+random()*(2^63-1))::bigint, - birth timestamp not null default now(), + birth bigint not null, content bytea, -- if null, this is a "ban record" preventing content matching the hash from being re-uploaded hash bytea unique not null, -- sha256 hash of content -- it would be cool to use a computed column for this, but i don't want -- to lock people into PG12 or drag in the pgcrypto extension just for this mime text -- null if unknown, will be reported as x-octet-stream ); create index on parsav_artifacts (mime); create table parsav_artifact_claims ( - birth timestamp not null default now(), + birth bigint not null, uid bigint references parsav_actors(id) on delete cascade, rid bigint references parsav_artifacts(id) on delete cascade, description text, folder text, unique (uid,rid) ); create index on parsav_artifact_claims (uid); +create index on parsav_artifact_claims (uid,folder); create table parsav_circles ( id bigint primary key default (1+random()*(2^63-1))::bigint, owner bigint not null references parsav_actors(id) on delete cascade, name text not null, members bigint[] not null default array[]::bigint[], unique (owner,name) ); +create index on parsav_circles (owner); create table parsav_rooms ( id bigint primary key default (1+random()*(2^63-1))::bigint, origin bigint references parsav_servers(id) on delete cascade, name text not null, @@ -148,10 +154,12 @@ rank smallint not null default 0, admin boolean not null default false, -- non-admins with rank can only moderate + invite title text, -- admin-granted title like reddit flair vouchedby bigint references parsav_actors(id) on delete set null ); +create index on parsav_room_members (member); +create index on parsav_room_members (room); create table parsav_invites ( id bigint primary key default (1+random()*(2^63-1))::bigint, -- when a user is created from an invite, the invite is deleted and the invite -- ID becomes the user ID. privileges granted on the invite ID during the invite @@ -166,15 +174,18 @@ id bigint primary key default (1+random()*(2^63-1))::bigint, issuer bigint references parsav_actors(id) on delete set null, scope bigint, -- can be null or room for local actions nature smallint not null, -- silence, suspend, disemvowel, censor, noreply, etc victim bigint not null, -- can be user, room, or post - expire timestamp, -- auto-expires if set - review timestamp, -- brings up for review at given time if set + expire bigint, -- auto-expires if set + review bigint, -- brings up for review at given time if set reason text, -- visible to victim if set - context text -- admin-only note + context text, -- admin-only note + appeal text -- null if no appeal lodged ); +create index on parsav_sanctions (victim,scope); +create index on parsav_sanctions (issuer); create table parsav_actor_conf_strs ( uid bigint not null references parsav_actors(id) on delete cascade, key text not null, value text not null, unique (uid,key) ); Index: doc/usr.md ================================================================== --- doc/usr.md +++ doc/usr.md @@ -51,11 +51,11 @@ * **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. + * **invite:** the user can issue invites and create accounts without depleting their invite supply, even if they have none at all. users with both the `invite` and `elevate` powers can grant invites to others. * **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 grant …` and `revoke …` (`all` can be used instead of a list of powers to grant or strip all powers simultaneously) Index: parsav.t ================================================================== --- parsav.t +++ parsav.t @@ -185,11 +185,11 @@ if diff > 30 then -- print cur time lib.noise.lasttime = now var curtime: int8[26] lib.osclock.ctime_r(&now, &curtime[0]) for i=0,26 do if curtime[i] == @'\n' then curtime[i] = 0 break end end -- :/ - [ lib.emit(false, 2, '\27[1m[', `&curtime[0], ']\27[;36m\n +00 ') ] + [ lib.emit(false, 2, '\27[1m', `&curtime[0], '\27[;36m\n +00 ') ] else -- print time since last msg var dfs = arrayof(int8, 0x30 + diff/10, 0x30 + diff%10, 0x20, 0) [ lib.emit(false, 2, ' \27[36m+', `&dfs[0]) ] end end @@ -237,11 +237,11 @@ lib.enum = function(tbl) local ty = uint8 if #tbl >= 2^32 then ty = uint64 -- hey, can't be too safe elseif #tbl >= 2^16 then ty = uint32 elseif #tbl >= 2^8 then ty = uint16 end - local o = { t = ty } + local o = { t = ty, members = tbl } local strings = {} for i, name in ipairs(tbl) do o[name] = i - 1 strings[i] = `[lib.mem.ref(int8)]{ptr=[name], ct=[#name]} end @@ -273,12 +273,11 @@ return ct end set.methods.dump = macro(function(self) local q = quote lib.io.say('dumping set:\n') end for i,v in ipairs(tbl) do - q = quote - [q] + q = quote [q] if [bool](self.[v]) then lib.io.say([' - ' .. v .. ': true\n']) else lib.io.say([' - ' .. v .. ': false\n']) end end Index: render/conf/users.t ================================================================== --- render/conf/users.t +++ render/conf/users.t @@ -16,10 +16,99 @@ case [uint16](4) then acc:lpush('🗡') end case [uint16](5) then acc:lpush('🗝') end else acc:lpush('🕴') end end + +local rnd = lib.crypt.random +local terra +suggest_handle(a: &lib.str.acc) + var start = a.sz + var puncts = array('.','_','-') + var xXx = rnd(uint8, 0, 9) == 0 + var leet = rnd(uint8, 0, 8) == 0 + var caps = rnd(uint8, 0, 5) + var punct: rawstring = nil + var useadj = rnd(uint8, 0, 4) == 0 + if rnd(uint8, 0, 4) == 0 then + punct = puncts[rnd(intptr,0,[puncts.type.N])] + end + + var nouns = array( + 'thunder','bride','blaze','doom','squad','gun','lord','blaster', + 'fuck','hell','hound','piss','shit','killa','terror', 'horror', + 'fear', 'slaughter','murder','general','commander', 'commissar', + 'terrorist','infinity','slut','cunt','whore','bitch', 'bastard', + 'cock','prince','princess','pimp','gay','cop','slayer', 'vampire', + 'vampyre','blood','pain','brute','wolf','sword','star','sun','moon', + 'killer','murderer','thief','arson','fire','ice','frost','hack', + 'hacker','god','master','mistress','slave','rage','freeze','flayer', + 'pirate','ninja','shadow','fog','mist','misery','glory','bear', + 'king','queen','empress','emperor','majesty','space','martian', + 'winter','fall','monk','katana','420','warrior','banana','demon', + 'devil','ghost','wraith','cuck','legend','hero','heroine','goblin', + 'gremlin','troll','dragon','evil','overlord','radiance' + ) + var adjs = array( + 'dark','super','supreme','ultra','ultimate','total','infinite', + 'omnipotent','crazy','final','deathless','immortal', 'elite', + 'leet','1337','bloody','fearless','headless','screaming','insane', + 'brutal','legendary','space','frozen','flaming','burning', + 'mighty','flayed','hidden','secret','lost','mystery','glorious', + 'nude','naked','bare','first','radiant','martian','fallen', + 'wandering','dank','demonic','satanic','invisible','based','woke', + 'deadly','lethal','heroic','evil','majestic','luminous' + ) + + if xXx then a:lpush('xXx_') end + + if useadj then + var len = rnd(uint8,1,3) + for i = 0, len do + var sz = a.sz + a:push(adjs[rnd(intptr,0,[adjs.type.N])], 0) + if punct ~= nil then a:push(punct, 1) end + if caps == 1 then + a.buf[sz] = lib.str.cupcase(a.buf[sz]) + end + end + end + var nounct = rnd(uint8,1,3) + for i = 0, nounct do + var sz = a.sz + a:push(nouns[rnd(intptr,0,[nouns.type.N])], 0) + if punct ~= nil and i+1 ~= nounct then a:push(punct, 1) end + if caps == 1 then + a.buf[sz] = lib.str.cupcase(a.buf[sz]) + end + end + + if leet or caps == 2 then for i=start, a.sz do + if caps == 2 and rnd(uint8,0,5)==0 then + a.buf[i] = lib.str.cupcase(a.buf[i]) + end + if leet then + switch lib.str.cdowncase(a.buf[i]) do + case [uint8]([string.byte('e')]) then a.buf[i] = @'3' end + case [uint8]([string.byte('i')]) then a.buf[i] = @'1' end + case [uint8]([string.byte('l')]) then a.buf[i] = @'1' end + case [uint8]([string.byte('t')]) then a.buf[i] = @'7' end + case [uint8]([string.byte('s')]) then a.buf[i] = @'5' end + case [uint8]([string.byte('o')]) then a.buf[i] = @'0' end + case [uint8]([string.byte('b')]) then a.buf[i] = @'6' end + end + end + end end + + if (nounct == 1 and not useadj) or rnd(uint8, 0, 5) == 0 then + if punct ~= nil then a:push(punct, 1) end + a:ipush(rnd(uint16,0,65535)) + end + + if xXx then a:lpush('_xXx') end + +end local push_num_field = macro(function(acc,name,lbl,min,max,value,disable) name = name:asvalue() lbl = lbl:asvalue() local start = '
' @@ -221,10 +310,18 @@ lib.render.nym(usr, 0, &ulst, false) ulst:lpush('') end ::skip::end ulst:lpush('') + + if co.who.rights.powers.invite() or co.who.rights.invites > 0 then + ulst:lpush('
create new user
') + end + ulst:lpush('
instantiate remote actor
') + return ulst:finalize() end do return pstr.null() end ::e404:: co:complain(404, 'not found', 'there is no user or resource by that identifier on this server') goto quit ::e403:: co:complain(403, 'forbidden', 'you do not have sufficient authority to control that resource') Index: render/profile.t ================================================================== --- render/profile.t +++ render/profile.t @@ -3,19 +3,23 @@ local terra cs(s: rawstring) return pstr { ptr = s, ct = lib.str.sz(s) } end local terra -render_profile(co: &lib.srv.convo, actor: &lib.store.actor) +render_profile( + co: &lib.srv.convo, + actor: &lib.store.actor, + relationship: &lib.store.relationship +): pstr var aux: lib.str.acc var followed = false -- FIXME if co.aid ~= 0 and co.who.id == actor.id then aux:compose('alter') elseif co.aid ~= 0 then - if not followed then + if not relationship.rel.follow() then aux:compose('') - elseif followed then + elseif relationship.rel.follow() then aux:compose('') end aux:lpush('chat') if co.who.rights.powers:affect_users() and co.who:overpowers(actor) then aux:lpush('control') @@ -69,10 +73,14 @@ if co.who:outranks(actor) then comments:lpush('
  • underling
  • ') elseif actor:outranks(co.who) then comments:lpush('
  • outranks you
  • ') end + + if relationship.recip.follow() then + comments:lpush('
  • follows you
  • ') + end end var profile = data.view.profile { nym = fullname; bio = bio; Index: render/tweet.t ================================================================== --- render/tweet.t +++ render/tweet.t @@ -9,11 +9,11 @@ acc:lpush('
    ') - if co.who.id == rter.id then + if co.aid ~= 0 and co.who.id == rter.id then acc:lpush('') end end local terra @@ -37,10 +37,11 @@ ::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]) + for i=0,26 do if timestr[i] == @'\n' then timestr[i] = 0 break end end -- 🙄 var bhtml = lib.smackdown.html([lib.mem.ptr(int8)] {ptr=p.body,ct=0}) defer bhtml:free() var idbuf: int8[lib.math.shorthand.maxlen] @@ -59,27 +60,28 @@ stats = pstr{'',0}; } if p.rts + p.likes > 0 then var s: lib.str.acc s:init(128) s:lpush('
    ') - if p.rts > 0 then s:lpush('
    ' ):ipush(p.rts ):lpush('
    ') end - if p.likes > 0 then s:lpush('') end + if p.rts > 0 then s:lpush('
    ' ):dpush(p.rts ):lpush('
    ') end + if p.likes > 0 then s:lpush('') end s:lpush('
    ') tpl.stats = s:finalize() end - var attrbuf: int8[48] + var attrbuf: int8[64] var attrcur = &attrbuf[0] if p.accent ~= -1 and p.accent ~= co.ui_hue then var hdecbuf: int8[21] var hdec = lib.math.decstr(p.accent, &hdecbuf[20]) attrcur = lib.str.cpy(attrcur,' style="--hue:') attrcur = lib.str.cpy(attrcur, hdec) -- var len = &hdecbuf[20] - hdec attrcur = lib.str.cpy(attrcur, '"') end - if p.author == co.who.id then attrcur = lib.str.cpy(attrcur, ' data-own') end + if co.aid ~= 0 and p.author == co.who.id then attrcur = lib.str.cpy(attrcur, ' data-own') end + if retweeter ~= nil then attrcur = lib.str.cpy(attrcur, ' data-rt') end if attrcur ~= &attrbuf[0] then tpl.attr = &attrbuf[0] end defer tpl.permalink:free() if acc ~= nil then Index: render/user-page.t ================================================================== --- render/user-page.t +++ render/user-page.t @@ -1,18 +1,22 @@ -- vim: ft=terra local terra -render_userpage(co: &lib.srv.convo, actor: &lib.store.actor) +render_userpage( + co : &lib.srv.convo, + actor : &lib.store.actor, + relationship: &lib.store.relationship +): {} var ti: lib.str.acc if co.aid ~= 0 and co.who.id == actor.id then ti:compose('my profile') else ti:compose('profile :: ', actor.handle) end var tiptr = ti:finalize() var acc: lib.str.acc acc:init(1024) - var pftxt = lib.render.profile(co,actor) defer pftxt:free() + var pftxt = lib.render.profile(co,actor,relationship) defer pftxt:free() acc:ppush(pftxt) var stoptime = lib.osclock.time(nil) var posts = co.srv:post_enum_author_uid(actor.id, lib.store.range { mode = 1; -- T->I Index: route.t ================================================================== --- route.t +++ route.t @@ -5,10 +5,36 @@ local rstring = lib.mem.ref(int8) local hpath = lib.mem.ptr(rstring) local http = {} terra meth_get(meth: method.t) return (meth == method.get) or (meth == method.head) end + +terra http.actor_profile(co: &lib.srv.convo, actor: &lib.store.actor, meth: method.t) + var rel: lib.store.relationship + if co.aid ~= 0 then + rel = co.srv:actor_rel_calc(co.who.id, actor.id) + if meth == method.post then + var act = co:ppostv('act') + if act:cmp(lib.str.plit 'follow') and not rel.rel.follow() then + if rel.recip.block() then + co:complain(403,'blocked','you cannot follow a user you are blocked by') return + end + (rel.rel.follow << true) + co.srv:actor_rel_create([lib.store.relation.idvmap.follow], co.who.id, actor.id) + elseif act:cmp(lib.str.plit 'unfollow') and rel.rel.follow() then + (rel.rel.follow << false) + co.srv:actor_rel_destroy([lib.store.relation.idvmap.follow], co.who.id, actor.id) + end + end + else + rel.rel:clear() + rel.recip:clear() + end + + lib.render.user_page(co, actor, &rel) +end + terra http.actor_profile_xid(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t) var handle = [lib.mem.ptr(int8)] { ptr = &uri.ptr[2], ct = 0 } for i=2,uri.ct do if uri.ptr[i] == @'/' or uri.ptr[i] == 0 then handle.ct = i - 2 break end end @@ -29,11 +55,11 @@ co:complain(404,'no such user','no such user known to this server') return end defer actor:free() - lib.render.user_page(co, actor.ptr) + http.actor_profile(co,actor.ptr,meth) end terra http.actor_profile_uid ( co: &lib.srv.convo, path: lib.mem.ptr(lib.mem.ref(int8)), @@ -55,11 +81,11 @@ co:complain(404, 'no such user', 'no user by that ID is known to this instance') return end defer actor:free() - lib.render.user_page(co, actor.ptr) + http.actor_profile(co,actor.ptr,meth) end terra http.login_form(co: &lib.srv.convo, meth: method.t) if meth_get(meth) then -- request a username @@ -224,10 +250,11 @@ end else goto badurl end end if meth == method.post then + if co.aid == 0 then goto noauth end 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 @@ -255,10 +282,11 @@ 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 + ::noauth:: do co:complain(401, 'unauthorized', 'you have not supplied the necessary credentials to perform this operation') return end end terra http.configure(co: &lib.srv.convo, path: hpath, meth: method.t) var msg = pstring.null() -- first things first, do priv checks @@ -325,15 +353,20 @@ co.who.source:auth_sigtime_user_alter(co.who.id, lib.osclock.time(nil)) -- the current session has been invalidated as well, so we need to immediately install a new authentication cookie with the same aid so the user doesn't need to log back in all over again co:installkey('/conf/sec',co.aid) return end - elseif path(1):cmp(lib.str.lit 'users') and path.ct >= 2 then - var userid, ok = lib.math.shorthand.parse(path(2).ptr, path(2).ct) - if ok then - var usr = co.srv:actor_fetch_uid(userid) defer usr:free() - if not co.who:overpowers(usr.ptr) then goto nopriv end + elseif path(1):cmp(lib.str.lit 'users') then + if path.ct >= 3 then + var userid, ok = lib.math.shorthand.parse(path(2).ptr, path(2).ct) + if ok then + var usr = co.srv:actor_fetch_uid(userid) + if usr:ref() then defer usr:free() + if not co.who:overpowers(usr.ptr) then goto nopriv end + end + end + elseif path.ct == 2 then end end if user_refresh then -- refresh the user info for the renderer var usr = co.srv:actor_fetch_uid(co.who.id) Index: static/live.js ================================================================== --- static/live.js +++ static/live.js @@ -36,37 +36,39 @@ /* div-based like and rt aren't very keyboard-friendly. add a replacement */ if (document.querySelector('body.timeline, body.profile, body.post') != null) { onkey(window, function(event) { if (focused()) {return;} - let cururl = window._liveTweetMap.cur; + let tmap = window._liveTweetMap; + let cururl = tmap.cur; let nexturl = null; if (event.key == 'j') { // down if (cururl == null) { - nexturl = window._liveTweetMap.first + nexturl = tmap.first; } else { - nexturl = window._liveTweetMap.map.get(cururl).next + nexturl = tmap.map.get(cururl).next; } } else if (event.key == 'k') { // up if (cururl == null) { - nexturl = window._liveTweetMap.last + nexturl = tmap.last; } else { - nexturl = window._liveTweetMap.map.get(cururl).prev + nexturl = tmap.map.get(cururl).prev; } } else if (cururl != null) { - let post = window._liveTweetMap.map.get(cururl).me + let post = tmap.map.get(cururl).me; + let root = tmap.map.get(cururl).go; if (event.key == 'f') { // fave - postReq(cururl, 'like', post.querySelector('.stats>.like')) + postReq(root, 'like', post.querySelector('.stats>.like')) } else if (event.key == 'r') { // rt - postReq(cururl, 'rt', post.querySelector('.stats>.rt')) - } else if (event.key == 'd') { // rt - if (post.attributes.getNamedItem('data-own')) { - window.location = cururl + '/del'; - } + postReq(root, 'rt', post.querySelector('.stats>.rt')) } else if (event.key == 'Enter') { // nav - window.location = cururl; + window.location = root; return; + } else if (post.attributes.getNamedItem('data-own')) { + if (event.key == 'd') { window.location = root + '/del'; } + else if (event.key == 'e') { window.location = root + '/edit'; } + else if (event.key == 'u' && root != cururl) { window.location = cururl; } // detweet } } if (nexturl != null) { if (cururl != null) { let cur = window._liveTweetMap.map.get(cururl); @@ -107,18 +109,30 @@ } function attachButtons() { let last = null; let newmap = { cur: null, first: null, last: null, map: new Map() } - document.querySelectorAll('main article.post').forEach(function(post){ - let url = posturl(post); + document.querySelectorAll('main article.post:not([data-rt]), main div.lede').forEach(function(post){ + let ert = post.querySelector('article.post[data-rt]'); + let lede = null; + if (ert != null) { lede = post; post = ert; } + let purl = posturl(post); + let url = null; + if (lede == null) {url = purl;} else { + url = lede.querySelector('a[href].del'). + attributes.getNamedItem('href').value; + } + console.log('post',post,'lede',lede,url); + if (last == null) { newmap.first = url; } else { - newmap.map.get(last).next = url + newmap.map.get(last).next = url; } - newmap.map.set(url, {me: post, prev: last, next: null}) - last = url - if (window._liveTweetMap && window._liveTweetMap.cur == url) { + newmap.map.set(url, {me: post, go: purl, prev: last, next: null}) + last = url; + if (window._liveTweetMap && + window._liveTweetMap.cur == url + ) { post.classList.add('live-selected'); } let stats = post.querySelector('.stats'); if (stats == null) { Index: static/style.scss ================================================================== --- static/style.scss +++ static/style.scss @@ -546,10 +546,15 @@ margin: 0.5em 0.3em; padding-left: 1.3em; background-size: 1.1em; background-repeat: no-repeat; min-width: 0.3em; + &:focus { + outline: none; + opacity: 0.9 !important; + filter: brightness(1.7) drop-shadow(0 0 15px rgb(255,150,200)); + } &:empty { transition: 0.3s; opacity: 0.0001; // qutebrowser won't show hints if opacity=0 :( &:hover, &:focus { opacity: 0.6 !important; } } @@ -791,11 +796,13 @@ ul.remarks { margin: 0; padding: 0; list-style-type: none; li { border-top: 1px solid otone(-22%); - border-bottom: 2px solid otone(-55%); + border-bottom: 1px solid otone(-53%); + box-shadow: 0 1px 1px otone(-57%); + text-shadow: 1px 1px otone(-60%); border-radius: 3px; background: otone(-25%,-0.4); color: otone(25%); text-align: center; padding: 0.3em 0; @@ -913,10 +920,11 @@ 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; + & + & { margin-top: 0; } 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; Index: store.t ================================================================== --- store.t +++ store.t @@ -3,22 +3,23 @@ timepoint = lib.osclock.time_t; scope = lib.enum { 'public', 'private', 'local'; 'personal', 'direct', 'circle'; }; - notiftype = lib.enum { - 'none', 'mention', 'like', 'rt', 'react' + noticetype = lib.enum { + 'none', 'mention', 'reply', 'like', 'rt', 'react' }; relation = lib.set { + 'follow', + 'mute', -- posts will be completely hidden at all times + 'block', -- no interactions will be permitted, but posts will remain visible '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 - 'follow', - 'mute', -- posts will be completely hidden at all times - 'block', -- no interactions will be permitted, but posts will remain visible + 'exclude', -- own posts will not be visible to this user }; credset = lib.set { 'pw', 'otp', 'challenge', 'trust' }; privset = lib.set { @@ -244,15 +245,17 @@ field = 'actor_conf_'..n..'_'..k, type = t } end end -struct m.notif { - kind: m.notiftype.t +struct m.notice { + kind: m.noticetype.t when: uint64 + who: uint64 + what: uint64 union { - post: uint64 + reply: uint64 reaction: int8[16] } } struct m.inet { @@ -345,11 +348,10 @@ actor_save: {&m.source, &m.actor} -> {} actor_save_privs: {&m.source, &m.actor} -> {} actor_purge_uid: {&m.source, uint64} -> {} actor_fetch_xid: {&m.source, lib.mem.ptr(int8)} -> lib.mem.ptr(m.actor) actor_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.actor) - actor_notif_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.notif) actor_enum: {&m.source} -> lib.mem.lstptr(m.actor) actor_enum_local: {&m.source} -> lib.mem.lstptr(m.actor) actor_stats: {&m.source, uint64} -> m.actor_stats actor_rel: {&m.source, uint64, uint64} -> m.relationship @@ -388,11 +390,14 @@ 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) + actor_notice_enum: {&m.source, uint64} -> lib.mem.lstptr(m.notice) + actor_rel_create: {&m.source, uint16, uint64, uint64} -> {} + actor_rel_destroy: {&m.source, uint16, uint64, uint64} -> {} + actor_rel_calc: {&m.source, uint64, uint64} -> m.relationship 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 Index: str.t ================================================================== --- str.t +++ str.t @@ -181,15 +181,22 @@ self.sz = self.sz + len self.buf[self.sz] = 0 return self end; -terra m.acc:ipush(i: intptr) +terra m.acc:dpush(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) + return self:push(si,len-1) +end + +terra m.acc:ipush(i: intptr) + var decbuf: int8[21] + var si = lib.math.decstr(i, &decbuf[20]) + var len: intptr = [decbuf.type.N] - (si - &decbuf[0]) + return self:push(si,len-1) end terra m.acc:shpush(i: uint64) var sbuf: int8[lib.math.shorthand.maxlen] var len = lib.math.shorthand.gen(i,&sbuf[0]) Index: view/confirm.tpl ================================================================== --- view/confirm.tpl +++ view/confirm.tpl @@ -1,9 +1,9 @@

    @title

    @query

    - cancel - + cancel +
    Index: view/profile.tpl ================================================================== --- view/profile.tpl +++ view/profile.tpl @@ -14,14 +14,14 @@ @nfollows @nfollowers @timephrase @tweetday
      @remarks
    -
    + posts archive media associates
    @auxbtn