Index: backend/pgsql.t ================================================================== --- backend/pgsql.t +++ backend/pgsql.t @@ -28,11 +28,12 @@ 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.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 @@ -44,10 +45,11 @@ 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 @@ -60,10 +62,19 @@ (a.origin is null and $1::text = a.handle or $1::text = ('@' || a.handle)) ]]; }; + + actor_purge_uid = { + params = {uint64}, cmd = true, sql = [[ + with d as ( -- cheating + delete from parsav_sanctions where victim = $1::bigint + ) + delete from parsav_actors where id = $1::bigint + ]]; + }; actor_save = { params = { uint64, --id rawstring, --nym @@ -71,44 +82,47 @@ rawstring, --bio rawstring, --epithet rawstring, --avataruri uint64, --avatarid uint16, --rank - uint32 --quota + uint32, --quota + uint32 --invites }, cmd = true, sql = [[ update parsav_actors set nym = $2::text, handle = $3::text, bio = $4::text, epithet = $5::text, avataruri = $6::text, avatarid = $7::bigint, rank = $8::smallint, - quota = $9::integer - --invites are controlled by their own specialized routines + quota = $9::integer, + invites = $10::integer where id = $1::bigint ]]; }; actor_create = { params = { rawstring, rawstring, uint64, lib.store.timepoint, rawstring, rawstring, lib.mem.ptr(uint8), - rawstring, uint16, uint32 + rawstring, uint16, uint32, uint32 }; sql = [[ insert into parsav_actors ( nym,handle, origin,knownsince, bio,avataruri,key, - epithet,rank,quota + epithet,rank,quota, + invites ) values ($1::text, $2::text, case when $3::bigint = 0 then null else $3::bigint end, to_timestamp($4::bigint), $5::bigint, $6::bigint, $7::bytea, - $8::text, $9::smallint, $10::integer + $8::text, $9::smallint, $10::integer, + $11::integer ) returning id ]]; }; actor_auth_pw = { @@ -127,26 +141,28 @@ actor_enum_local = { params = {}, sql = [[ select id, nym, handle, origin, bio, null::text, rank, quota, key, epithet, extract(epoch from knownsince)::bigint, - handle ||'@'|| - (select value from parsav_config - where key='domain' limit 1) as xid + '@' || 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 + '@' || a.handle) as xid, + invites from parsav_actors a left join parsav_servers s on s.id = a.origin + order by nullif(a.rank,0) nulls last, a.handle, a.origin ]]; }; actor_stats = { params = {uint64}, sql = ([[ @@ -788,14 +804,27 @@ var a: lib.mem.ptr(lib.store.actor) var av: rawstring, avlen: intptr var nym: rawstring, nymlen: intptr var bio: rawstring, biolen: intptr var epi: rawstring, epilen: intptr - if r:null(row,5) then avlen = 0 av = nil else + var origin: uint64 = 0 + var handle = r:_string(row, 2) + if not r:null(row,3) then origin = r:int(uint64,row,3) end + + var avia = lib.str.acc {buf=nil} + if origin == 0 then + avia:compose('/avi/',handle) + av = avia.buf + avlen = avia.sz+1 + elseif r:null(row,5) then av = r:string(row,5) avlen = r:len(row,5)+1 + else + av = '/s/default-avatar.webp' + avlen = 22 end + if r:null(row,1) then nymlen = 0 nym = nil else nym = r:string(row,1) nymlen = r:len(row,1)+1 end if r:null(row,4) then biolen = 0 bio = nil else @@ -809,25 +838,26 @@ a = [ lib.str.encapsulate(lib.store.actor, { nym = {`nym, `nymlen}; bio = {`bio, `biolen}; epithet = {`epi, `epilen}; avatar = {`av,`avlen}; - handle = {`r:string(row, 2); `r:len(row,2) + 1}; + handle = {`handle.ptr, `handle.ct + 1}; xid = {`r:string(row, 11); `r:len(row,11) + 1}; }) ] a.ptr.id = r:int(uint64, row, 0); a.ptr.rights = lib.store.rights_default(); a.ptr.rights.rank = r:int(uint16, row, 6); a.ptr.rights.quota = r:int(uint32, row, 7); + a.ptr.rights.invites = r:int(uint32, row, 12); a.ptr.knownsince = r:int(int64,row, 10); if r:null(row,8) then a.ptr.key.ct = 0 a.ptr.key.ptr = nil else a.ptr.key = r:bin(row,8) end - if r:null(row,3) then a.ptr.origin = 0 - else a.ptr.origin = r:int(uint64,row,3) end + a.ptr.origin = origin + if avia.buf ~= nil then avia:free() end return a end local privmap = lib.store.privmap @@ -875,11 +905,11 @@ local privupdate = terra( src: &lib.store.source, ac: &lib.store.actor ): {} - var pdef = lib.store.rights_default().powers + var pdef: lib.store.powerset pdef:clear() var map = array([privmap]) for i=0, [map.type.N] do var d = pdef and map[i].priv var u = ac.rights.powers and map[i].priv queries.actor_power_delete.exec(src, ac.id, map[i].name) @@ -1044,32 +1074,32 @@ end]; actor_enum = [terra(src: &lib.store.source) var r = queries.actor_enum.exec(src) if r.sz == 0 then - return [lib.mem.ptr(&lib.store.actor)] { ct = 0, ptr = nil } + return [lib.mem.lstptr(lib.store.actor)].null() else defer r:free() - var mem = lib.mem.heapa([&lib.store.actor], r.sz) + var mem = lib.mem.heapa([lib.mem.ptr(lib.store.actor)], r.sz) for i=0,r.sz do - mem.ptr[i] = row_to_actor(&r, i).ptr - mem.ptr[i].source = src + mem.ptr[i] = row_to_actor(&r, i) + mem(i).ptr.source = src end - return [lib.mem.ptr(&lib.store.actor)] { ct = r.sz, ptr = mem.ptr } + return [lib.mem.lstptr(lib.store.actor)] { ct = r.sz, ptr = mem.ptr } end end]; actor_enum_local = [terra(src: &lib.store.source) var r = queries.actor_enum_local.exec(src) if r.sz == 0 then - return [lib.mem.ptr(&lib.store.actor)] { ct = 0, ptr = nil } + return [lib.mem.lstptr(lib.store.actor)].null() else defer r:free() - var mem = lib.mem.heapa([&lib.store.actor], r.sz) + var mem = lib.mem.heapa([lib.mem.ptr(lib.store.actor)], r.sz) for i=0,r.sz do - mem.ptr[i] = row_to_actor(&r, i).ptr - mem.ptr[i].source = src + mem.ptr[i] = row_to_actor(&r, i) + mem(i).ptr.source = src end - return [lib.mem.ptr(&lib.store.actor)] { ct = r.sz, ptr = mem.ptr } + return [lib.mem.lstptr(lib.store.actor)] { ct = r.sz, ptr = mem.ptr } end end]; actor_auth_how = [terra( src: &lib.store.source, @@ -1131,18 +1161,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,13) then -- restricted? + if not r:null(0,14) then -- restricted? au.val.privs:clear() - (au.val.privs.post << r:bool(0,14)) - (au.val.privs.edit << r:bool(0,15)) - (au.val.privs.acct << r:bool(0,16)) - (au.val.privs.upload << r:bool(0,17)) - (au.val.privs.censor << r:bool(0,18)) - (au.val.privs.admin << r:bool(0,19)) + (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 @@ -1221,23 +1251,25 @@ actor_powers_fetch = getpow; actor_save = [terra( src: &lib.store.source, ac: &lib.store.actor ): {} + var avatar = ac.avatar + if ac.origin == 0 then avatar = nil end queries.actor_save.exec(src, ac.id, ac.nym, ac.handle, - ac.bio, ac.epithet, ac.avatar, - ac.avatarid, ac.rights.rank, ac.rights.quota) + ac.bio, ac.epithet, avatar, + ac.avatarid, ac.rights.rank, ac.rights.quota, ac.rights.invites) end]; actor_save_privs = privupdate; actor_create = [terra( src: &lib.store.source, ac: &lib.store.actor ): uint64 - var r = queries.actor_create.exec(src,ac.nym, ac.handle, ac.origin, ac.knownsince, ac.bio, ac.avatar, ac.key, ac.epithet, ac.rights.rank, ac.rights.quota) + var r = queries.actor_create.exec(src,ac.nym, ac.handle, ac.origin, ac.knownsince, ac.bio, ac.avatar, ac.key, ac.epithet, ac.rights.rank, ac.rights.quota,ac.rights.invites) if r.sz == 0 then lib.bail('failed to create actor!') end ac.id = r:int(uint64,0,0) -- check against default rights, insert records for wherever powers differ lib.dbg('created new actor, establishing powers') @@ -1245,10 +1277,15 @@ lib.dbg('powers established') return ac.id end]; + actor_purge_uid = [terra( + src: &lib.store.source, + uid: uint64 + ) queries.actor_purge_uid.exec(src,uid) end]; + auth_enum_uid = [terra( src: &lib.store.source, uid: uint64 ): lib.mem.ptr(lib.mem.ptr(lib.store.auth)) var r = queries.auth_enum_uid.exec(src,uid) Index: backend/schema/pgsql.sql ================================================================== --- backend/schema/pgsql.sql +++ backend/schema/pgsql.sql @@ -32,10 +32,11 @@ 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 unique (handle,origin) @@ -144,11 +145,11 @@ room bigint not null references parsav_rooms(id) on delete cascade, member bigint not null references parsav_actors(id) on delete cascade, 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) + vouchedby bigint references parsav_actors(id) on delete set null ); 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 Index: config.lua ================================================================== --- config.lua +++ config.lua @@ -54,11 +54,11 @@ {'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.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 Index: makefile ================================================================== --- makefile +++ makefile @@ -1,9 +1,9 @@ dl = git dbg-flags = $(if $(dbg),-g) -images = static/default-avatar.webp +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) $< Index: mem.t ================================================================== --- mem.t +++ mem.t @@ -125,10 +125,11 @@ return t end m.ptr = terralib.memoize(function(ty) return mkptr(ty, true) end) m.ref = terralib.memoize(function(ty) return mkptr(ty, false) end) +m.lstptr = function(ty) return m.ptr(m.ptr(ty)) end -- make code more readable m.vec = terralib.memoize(function(ty) local v = terralib.types.newstruct(string.format('vec<%s>', ty.name)) v.entries = { {field = 'storage', type = m.ptr(ty)}; Index: mgtool.t ================================================================== --- mgtool.t +++ mgtool.t @@ -23,16 +23,15 @@ { 'start', 'start a new instance of the server' }; { 'stop', 'stop a running instance' }; { 'ls', 'list all running instances' }; { 'attach', 'capture log output from a running instance' }; { 'db', 'set up and manage the database' }; - { 'user', 'manage users, privileges, and credentials'}; + { 'user', 'create and manage users, privileges, and credentials'}; + { 'actor', 'manage and purge actors, epithets, and ranks'}; { 'mkroot ', 'establish a new root user with the given handle' }; - { 'actor purge-all', 'remove all traces of a user from the database (except local user credentials -- use \27[1mauth all purge\27[m to prevent a user from accessing the instance)' }; - { 'actor create', 'instantiate a new actor' }; - { 'actor bestow ', 'bestow an epithet upon an actor' }; { 'conf', 'manage the server configuration'}; + { 'grow []', 'grant a new round of invites to all users, or those who match the given ACL' }; { 'serv dl', 'initiate an update cycle over foreign actors' }; { 'tl', 'print the current local timeline to standard out' }; { 'be pgsql setup-auth (managed|unmanaged)', '(PGSQL backends) select the authentication strategy to use' }; } @@ -132,10 +131,23 @@ else lib.report('instance #',num,' reports failed ',rep) end end end + +local terra gen_cfstr(cfmstr: rawstring, seed: intptr) + var confirmstrs = array( + 'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'eta', 'nu', 'kappa', + 'emerald', 'carnelian', 'sapphire', 'ruby', 'amethyst' + ) + var tdx = lib.osclock.time(nil) / 60 + cfmstr[0] = 0 + for i=0,3 do + if i ~= 0 then lib.str.cat(cfmstr, '-') end + lib.str.cat(cfmstr, confirmstrs[(seed ^ tdx ^ (173*i)) % [confirmstrs.type.N]]) + end +end local emp = lib.ipc.global_emperor local terra entry_mgtool(argc: int, argv: &rawstring): int if argc < 1 then lib.bail('bad invocation!') end @@ -274,29 +286,24 @@ if dbmode.arglist.ct < 1 then goto cmderr end srv:setup(cnf) if lib.str.cmp(dbmode.arglist(0),'init') == 0 and dbmode.arglist.ct == 2 then lib.report('initializing new database structure for domain ', dbmode.arglist(1)) + dlg:tx_enter() if dlg:dbsetup() then srv:conprep(lib.store.prepmode.conf) dlg:conf_set('instance-name', dbmode.arglist(1)) + dlg:conf_set('domain', dbmode.arglist(1)) do var sec: int8[65] gensec(&sec[0]) + dlg:conf_set('server-secret', &sec[0]) dlg:conf_set('server-secret', &sec[0]) end lib.report('database setup complete; use mkroot to create an administrative user') else lib.bail('initialization process interrupted') end + dlg:tx_complete() elseif lib.str.cmp(dbmode.arglist(0),'obliterate') == 0 then - var confirmstrs = array( - 'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'eta', 'nu', 'kappa', - 'emerald', 'carnelian', 'sapphire', 'ruby', 'amethyst' - ) - var cfmstr: int8[64] cfmstr[0] = 0 - var tdx = lib.osclock.time(nil) / 60 - for i=0,3 do - if i ~= 0 then lib.str.cat(&cfmstr[0], '-') end - lib.str.cat(&cfmstr[0], confirmstrs[(tdx ^ (173*i)) % [confirmstrs.type.N]]) - end + var cfmstr: int8[64] gen_cfstr(&cfmstr[0],0) if dbmode.arglist.ct == 1 then lib.bail('you are attempting to completely obliterate all data! make sure you have selected your target correctly. if you really want to do this, pass the confirmation string ', &cfmstr[0]) elseif dbmode.arglist.ct == 2 then if lib.str.cmp(dbmode.arglist(1), cfmstr) == 0 then @@ -394,31 +401,96 @@ if mg then var tmppw: int8[33] pwset(dlg, &tmppw, ruid, false) lib.report('temporary root pw: ', {&tmppw[0], 32}) end + else goto cmderr end + elseif lib.str.cmp(mode.arglist(0),'actor') == 0 then + var umode: pbasic umode:parse(mode.arglist.ct, &mode.arglist(0)) + if umode.help then + [ lib.emit(false, 1, 'usage: ', `argv[0], ' actor ', umode.type.helptxt.flags, ' [โ€ฆ]', umode.type.helptxt.opts, cmdhelp { + { 'actor rank ', 'set an actor\'s rank to (remote actors cannot exercise rank-related powers, but benefit from rank immunities)' }; + { 'actor degrade', 'alias for `actor rank 0`' }; + { 'actor bestow ', 'bestow an epithet upon an actor' }; + { 'actor instantiate', 'instantiate a remote actor, retrieving their profile and posts even if no one follows them' }; + { 'actor proscribe', 'globally ban an actor from interacting with your server' }; + { 'actor rehabilitate', 'lift a proscription on an actor' }; + { 'actor purge-all ', 'remove all traces of a user from the database (except local user credentials -- use \27[1mauth all purge\27[m to prevent a user from accessing the instance)' }; + }) ] + return 1 + end + if umode.arglist.ct >= 2 then + var degrade = lib.str.cmp(umode.arglist(1),'degrade') == 0 + var xid = umode.arglist(0) + var usr = dlg:actor_fetch_xid(pstr {ptr=xid, ct=lib.str.sz(xid)}) + if not usr then lib.bail('no such actor') end + if degrade or lib.str.cmp(umode.arglist(1),'rank') == 0 then + var rank: uint16 + if degrade and umode.arglist.ct == 2 then + rank = 0 + elseif (not degrade) and umode.arglist.ct == 3 then + var r, ok = lib.math.decparse(pstr { + ptr = umode.arglist(2); + ct = lib.str.sz(umode.arglist(2)); + }) + if not ok then goto cmderr end + rank = r + else goto cmderr end + usr.ptr.rights.rank = rank + dlg:actor_save(usr.ptr) + lib.report('set user rank') + elseif umode.arglist.ct == 3 and lib.str.cmp(umode.arglist(1),'bestow') == 0 then + if umode.arglist(2)[0] == 0 + then usr.ptr.epithet = nil + else usr.ptr.epithet = umode.arglist(2) + end + dlg:actor_save(usr.ptr) + lib.report('bestowed a new epithet on ', usr.ptr.xid) + elseif lib.str.cmp(umode.arglist(1),'purge-all') == 0 then + var cfmstr: int8[64] gen_cfstr(&cfmstr[0],usr.ptr.id) + if umode.arglist.ct == 2 then + lib.bail('you are attempting to completely purge the actor ', usr.ptr.xid, ' and all related content from the database! if you really want to do this, pass the confirmation string ', &cfmstr[0]) + elseif umode.arglist.ct == 3 then + if lib.str.ncmp(&cfmstr[0],umode.arglist(2),64) ~= 0 then + lib.bail('you have supplied an invalid confirmation string; if you really want to purge this actor, pass ', &cfmstr[0]) + end + lib.warn('completely purging actor ', usr.ptr.xid, ' and all related content from database') + dlg:actor_purge_uid(usr.ptr.id) + lib.report('actor purged') + else goto cmderr end + else goto cmderr end else goto cmderr end elseif lib.str.cmp(mode.arglist(0),'user') == 0 then var umode: pbasic umode:parse(mode.arglist.ct, &mode.arglist(0)) if umode.help then [ lib.emit(false, 1, 'usage: ', `argv[0], ' user ', umode.type.helptxt.flags, ' [โ€ฆ]', umode.type.helptxt.opts, cmdhelp { + { 'user create', 'add a new user' }; { 'user auth new', '(where applicable, managed auth only) create a new authentication token of the given type for a user' }; { 'user auth reset', '(where applicable, managed auth only) delete all of a user\'s authentication tokens of the given type and issue a new one' }; { 'user auth (|all) purge', 'delete all credentials that would allow this user to log in (where possible)' }; { 'user (grant|revoke) (|all)', 'grant or revoke a specific power to or from a user' }; - { 'user emasculate', 'strip all administrative powers from a user' }; + { 'user emasculate', 'strip all administrative powers and rank from a user' }; { 'user forgive', 'restore all default powers to a user' }; { 'user suspend []', '(e.g. \27[1muser jokester suspend 5d 6h 7m 3s\27[m to suspend "jokester" for five days, six hours, seven minutes, and three seconds) suspend a user'}; }) ] return 1 end - if umode.arglist.ct >= 3 then + var handle = umode.arglist(0) + var usr = dlg:actor_fetch_xid(pstr {ptr=handle, ct=lib.str.sz(handle)}) + if umode.arglist.ct == 2 and lib.str.cmp(umode.arglist(1),'create')==0 then + if usr:ref() then lib.bail('that user already exists') end + if not lib.store.actor.handle_validate(handle) then + lib.bail('invalid user handle') end + var kbuf: uint8[lib.crypt.const.maxdersz] + var na = lib.store.actor.mk(&kbuf[0]) + na.handle = handle + dlg:actor_create(&na) + lib.report('created new user @',na.handle,'; assign credentials to enable login') + elseif umode.arglist.ct >= 3 then var grant = lib.str.cmp(umode.arglist(1),'grant') == 0 - var handle = umode.arglist(0) - var usr = dlg:actor_fetch_xid(pstr {ptr=handle, ct=lib.str.sz(handle)}) + if not usr then lib.bail('no such user') end if grant or lib.str.cmp(umode.arglist(1),'revoke') == 0 then - if not usr then lib.bail('unknown handle') end var newprivs = usr.ptr.rights.powers var map = array([lib.store.privmap]) if umode.arglist.ct == 3 and lib.str.cmp(umode.arglist(2),'all') == 0 then if grant then newprivs:fill() Index: parsav.t ================================================================== --- parsav.t +++ parsav.t @@ -396,11 +396,11 @@ 'smackdown'; -- md-alike parser } local be = {} for _, b in pairs(config.backends) do - be[#be+1] = terralib.loadfile('backend/' .. b .. '.t')() + be[#be+1] = terralib.loadfile(string.format('backend/%s.t',b))() end lib.store.backends = global(`array([be])) lib.cmdparse = terralib.loadfile('cmdparse.t')() Index: render/conf.t ================================================================== --- render/conf.t +++ render/conf.t @@ -13,10 +13,12 @@ {url = 'rooms', title = 'chatrooms', render = 'rooms'}; {url = 'circles', title = 'circles', render = 'circles'}; {url = 'srv', title = 'server settings', render = 'srv'}; {url = 'brand', title = 'instance branding', render = 'rebrand'}; + {url = 'badge', title = 'user badges', render = 'badge'}; + {url = 'emoji', title = 'custom emoji packs', render = 'emojo'}; {url = 'censor', title = 'censorship & badthink suppression', render = 'rebrand'}; {url = 'users', title = 'user accounting', render = 'users'}; } @@ -43,14 +45,18 @@ render_conf([co], [path], notify: pstr) var menu: lib.str.acc menu:init(64):lpush('
') defer menu:free() -- build menu do var p = co.who.rights.powers - if p.config() then menu:lpush 'server settings' end - if p.rebrand() then menu:lpush 'instance branding' end + if p:affect_users() then menu:lpush 'users' end if p.censor() then menu:lpush 'badthink alerts' end - if p:affect_users() then menu:lpush 'users' end + if p.config() then menu:lpush([ + 'server & policy' .. + 'badges' .. + 'emoji packs' + ]) end + if p.rebrand() then menu:lpush 'instance branding' end end -- select the appropriate panel var [panel] = pstr { ptr = ''; ct = 0 } if path.ct >= 2 then [invoker] end Index: render/conf/users.t ================================================================== --- render/conf/users.t +++ render/conf/users.t @@ -1,42 +1,201 @@ -- vim: ft=terra local pstr = lib.mem.ptr(int8) local pref = lib.mem.ref(int8) +local P = lib.str.plit local terra cs(s: rawstring) return pstr { ptr = s, ct = lib.str.sz(s) } end local terra +regalia(acc: &lib.str.acc, rank: uint16) + switch rank do -- TODO customizability + case [uint16](1) then acc:lpush('๐Ÿ‘‘') end + case [uint16](2) then acc:lpush('๐Ÿ”ฑ') end + case [uint16](3) then acc:lpush('โšœ๏ธ') end + case [uint16](4) then acc:lpush('๐Ÿ—ก') end + case [uint16](5) then acc:lpush('๐Ÿ—') end + else acc:lpush('๐Ÿ•ด') + end +end + +local num_field = macro(function(acc,name,lbl,min,max,value) + name = name:asvalue() + lbl = lbl:asvalue() + return quote + var decbuf: int8[21] + in acc:lpush([string.format('
') + end +end) + +local terra +push_checkbox(acc: &lib.str.acc, name: pstr, lbl: pstr, on: bool, enabled: bool) + acc:lpush('') +end + +local mode_local, mode_remote, mode_staff, mode_peers, mode_peons, mode_all = 0,1,2,3,4,5 +local terra render_conf_users(co: &lib.srv.convo, path: lib.mem.ptr(pref)): pstr - if path.ct == 2 then - var uid, ok = lib.math.shorthand.parse(path(1).ptr,path(1).ct) + if path.ct == 3 then + var uid, ok = lib.math.shorthand.parse(path(2).ptr,path(2).ct) + if not ok then goto e404 end var user = co.srv:actor_fetch_uid(uid) + -- FIXME allow xids as well, for manual queries if not user then goto e404 end + defer user:free() + if not co.who:overpowers(user.ptr) then goto e403 end + var islinkct = false - var cinp: lib.str.acc + var cinp: lib.str.acc cinp:init(128) var clnk: lib.str.acc clnk:compose('
') + cinp:lpush('
') + if co.who.rights.powers.herald() then + var sanitized: pstr + if user.ptr.epithet == nil + then sanitized = pstr {ptr='', ct=0} + else sanitized = lib.html.sanitize(cs(user.ptr.epithet),true) + end + cinp:lpush('
') + if user.ptr.epithet ~= nil then sanitized:free() end + end + if user.ptr.rights.rank > 0 and (co.who.rights.powers.elevate() or co.who.rights.powers.demote()) then + var max = co.who.rights.rank + if not co.who.rights.powers.elevate() then max = user.ptr.rights.rank end + var min = co.srv.cfg.nranks + if not co.who.rights.powers.demote() then min = user.ptr.rights.rank end + + num_field(cinp, 'rank', 'rank', max, min, user.ptr.rights.rank) + 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 + + num_field(cinp, 'invites', 'invites', min, max, user.ptr.rights.invites) + end + cinp:lpush('
') + + if (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', 'site staff member', user.ptr.rights.rank > 0, true) + end + + cinp:lpush('
') + + if co.who.rights.powers.elevate() or + co.who.rights.powers.demote() then + var map = array([lib.store.privmap]) + cinp:lpush('
') + for i=0, [map.type.N] do + if (co.who.rights.powers and map[i].priv) == map[i].priv then + var name: int8[64] + var on = (user.ptr.rights.powers and map[i].priv) == map[i].priv + var enabled = (on and co.who.rights.powers.demote()) or + ((not on) and co.who.rights.powers.elevate()) + lib.str.cpy(&name[0], 'allow-') + lib.str.ncpy(&name[6], map[i].name.ptr, map[i].name.ct) + push_checkbox(&cinp, pstr{ptr=&name[0],ct=map[i].name.ct+6}, + map[i].name, on, enabled) + end + end + cinp:lpush('
') + end + + -- TODO black mark system? e.g. resolution option for badthink reports + -- adds a black mark to the offending user; they can be automatically banned + -- or brought up for review after a certain number of offenses; possibly lower + -- set of default privs for marked users var cinpp = cinp:finalize() defer cinpp:free() var clnkp: pstr if islinkct then clnkp = clnk:finalize() else clnk:free() clnkp = pstr { ptr='', ct=0 } end + var unym: lib.str.acc unym:init(64) + unym:lpush('') + lib.render.nym(user.ptr,0,&unym) + unym:lpush('') var pg = data.view.conf_user_ctl { - name = cs(user(0).handle); + name = unym:finalize(); inputcontent = cinpp; linkcontent = clnkp; } var ret = pg:tostr() + pg.name:free() if islinkct then clnkp:free() end return ret else - + var modes = array(P'local', P'remote', P'staff', P'titled', P'peons', P'all') + var idbuf: int8[lib.math.shorthand.maxlen] + var ulst: lib.str.acc ulst:init(256) + var mode: uint8 = mode_local + var modestr = co:pgetv('show') + ulst:lpush('
showing ') + for i=0,[modes.type.N] do + if modestr:ref() and modes[i]:cmp(modestr) then mode = i end + end + for i=0,[modes.type.N] do + if i > 0 then ulst:lpush(' ยท ') end + if mode == i then + ulst:lpush(''):ppush(modes[i]):lpush('') + else + ulst:lpush('') + :ppush(modes[i]):lpush('') + end + end + var users: lib.mem.lstptr(lib.store.actor) + if mode == mode_local then + users = co.srv:actor_enum_local() + else + users = co.srv:actor_enum() + end + ulst:lpush('
') + ulst:lpush('
    ') + for i=0,users.ct do var usr = users(i).ptr + if mode == mode_staff and usr.rights.rank == 0 then goto skip + elseif mode == mode_peons and usr.rights.rank ~= 0 then goto skip + elseif mode == mode_remote and usr.origin == 0 then goto skip + elseif mode == mode_peers and usr.epithet == nil then goto skip end + var idlen = lib.math.shorthand.gen(usr.id, &idbuf[0]) + ulst:lpush('
  • ') + if usr.rights.rank ~= 0 then + ulst:lpush('') + regalia(&ulst, usr.rights.rank) + ulst:lpush('') + end + if co.who:overpowers(usr) then + ulst:lpush('') + lib.render.nym(usr, 0, &ulst) + ulst:lpush('
  • ') + else + ulst:lpush('') + lib.render.nym(usr, 0, &ulst) + ulst:lpush('') + end + ::skip::end + ulst:lpush('
') + 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') + ::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') - do return pstr.null() end + ::quit:: return pstr.null() end return render_conf_users Index: render/nym.t ================================================================== --- render/nym.t +++ render/nym.t @@ -1,29 +1,37 @@ -- vim: ft=terra -local pstr = lib.mem.ptr(int8) +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) - var n: lib.str.acc n:init(128) +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) if who.nym ~= nil and who.nym[0] ~= 0 then var nymsan = lib.html.sanitize(cs(who.nym),false) - n:compose('',nymsan,' [', - xidsan,']') + n:lpush(''):ppush(nymsan) + :lpush(' ['):ppush(xidsan) + :lpush(']') nymsan:free() - else n:compose('',xidsan,'') end + else n:lpush(''):ppush(xidsan):lpush('') end xidsan:free() if who.epithet ~= nil then var episan = lib.html.sanitize(cs(who.epithet),false) - n:lpush(' '):ppush(episan):lpush('') + n:lpush(''):ppush(episan):lpush('') episan:free() end -- TODO: if scope == chat room then lookup titles in room member db - return n:finalize() + if tgt == nil then + return n:finalize() + else return pstr.null() end end return render_nym Index: render/profile.t ================================================================== --- render/profile.t +++ render/profile.t @@ -5,30 +5,27 @@ end local terra render_profile(co: &lib.srv.convo, actor: &lib.store.actor) var aux: lib.str.acc - var followed = true -- FIXME + var followed = false -- FIXME if co.aid ~= 0 and co.who.id == actor.id then - aux:compose('alter') + aux:compose('alter') elseif co.aid ~= 0 then if not followed then - aux:compose('') + elseif followed then + aux:compose('') end - aux:lpush('chat') - if co.who.rights.powers:affect_users() then + aux:lpush('chat') + if co.who.rights.powers:affect_users() and co.who:overpowers(actor) then aux:lpush('control') end else aux:compose('remote follow') end var auxp = aux:finalize() - var avistr: lib.str.acc if actor.origin == 0 then - avistr:compose('/avi/',actor.handle) - end var timestr: int8[26] lib.osclock.ctime_r(&actor.knownsince, ×tr[0]) var strfbuf: int8[28*4] var stats = co.srv:actor_stats(actor.id) var sn_posts = cs(lib.math.decstr_friendly(stats.posts, &strfbuf[ [strfbuf.type.N - 1] ])) @@ -37,29 +34,42 @@ var sn_mutuals = cs(lib.math.decstr_friendly(stats.mutuals, sn_followers.ptr - 1)) var bio = lib.str.plit "tall, dark, and mysterious" if actor.bio ~= nil then bio = lib.smackdown.html(cs(actor.bio)) end - var fullname = lib.render.nym(actor,0) defer fullname:free() + 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('
  • staff member
  • ') end + if co.aid ~= 0 and actor.rights.rank ~= 0 then + if co.who:outranks(actor) then + comments:lpush('
  • underling
  • ') + elseif actor:outranks(co.who) then + comments:lpush('
  • outranks you
  • ') + end + end + var profile = data.view.profile { nym = fullname; bio = bio; xid = cs(actor.xid); - avatar = lib.trn(actor.origin == 0, pstr{ptr=avistr.buf,ct=avistr.sz}, - cs(lib.coalesce(actor.avatar, '/s/default-avatar.svg'))); + avatar = cs(actor.avatar); nposts = sn_posts, nfollows = sn_follows; nfollowers = sn_followers, nmutuals = sn_mutuals; tweetday = cs(timestr); timephrase = lib.trn(actor.origin == 0, lib.str.plit'joined', lib.str.plit'known since'); + + remarks = ''; auxbtn = auxp; } + if comments.sz > 0 then profile.remarks = comments:finalize() end var ret = profile:tostr() - if actor.origin == 0 then avistr:free() end auxp:free() if actor.bio ~= nil then bio:free() end + if comments.sz > 0 then profile.remarks:free() end return ret end return render_profile Index: render/tweet.t ================================================================== --- render/tweet.t +++ render/tweet.t @@ -24,18 +24,17 @@ 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) defer fullname:free() + 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(lib.trn(author.origin == 0, avistr.buf, - lib.coalesce(author.avatar, '/s/default-avatar.svg'))); + avatar = cs(author.avatar); acctlink = cs(author.xid); permalink = permalink:finalize(); attr = '' } Index: route.t ================================================================== --- route.t +++ route.t @@ -247,10 +247,32 @@ ::badop :: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') 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 + if path.ct >= 1 then + if not co.who.rights.powers.config() and ( + path(1):cmp(lib.str.lit 'srv') or + path(1):cmp(lib.str.lit 'badge') or + 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 + end + if meth == method.post and path.ct >= 1 then var user_refresh = false var fail = false if path(1):cmp(lib.str.lit 'profile') then lib.dbg('updating profile') co.who.bio = co:postv('bio')._0 @@ -281,25 +303,25 @@ co.ui_hue = co.srv.cfg.ui_hue end msg = lib.str.plit 'profile changes saved' --user_refresh = true -- not really necessary here, actually - elseif path(1):cmp(lib.str.lit 'srv') then - if not co.who.rights.powers.config() then goto nopriv end - elseif path(1):cmp(lib.str.lit 'brand') then - if not co.who.rights.powers.rebrand() then goto nopriv end - elseif path(1):cmp(lib.str.lit 'users') then - if not co.who.rights.powers:affect_users() then goto nopriv end elseif path(1):cmp(lib.str.lit 'sec') then var act = co:ppostv('act') if act:cmp(lib.str.plit 'invalidate') then lib.dbg('setting user\'s cookie validation time to now') 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 end end if user_refresh then -- refresh the user info for the renderer var usr = co.srv:actor_fetch_uid(co.who.id) Index: srv.t ================================================================== --- srv.t +++ srv.t @@ -10,10 +10,12 @@ 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 @@ -646,10 +648,11 @@ if aid ~= 0 then if uid == 0 then lib.dbg('new user just logged in, creating account entry') var kbuf: uint8[lib.crypt.const.maxdersz] var na = lib.store.actor.mk(&kbuf[0]) + na.handle = newhnd.ptr var newuid: uint64 if self.sources(i).backend.actor_create ~= nil then newuid = self.sources(i):actor_create(&na) else newuid = self:actor_create(&na) end @@ -725,24 +728,45 @@ lib.report('closing data source ', src.id.ptr, '(', src.backend.id, ')') src:close() end self.sources:free() end + +terra cfgcache:cfint(name: rawstring, default: intptr) + var str = self.overlord:conf_get(name) + if str.ptr ~= nil then + var i,ok = lib.math.decparse(str) + if ok then default = i else + lib.warn('invalid configuration setting ',name,'="',{str.ptr,str.ct},'", expected integer; using default value instead') + end + str:free() + end + return default +end + +terra cfgcache:cfbool(name: rawstring, default: bool) + var str = self.overlord:conf_get(name) + if str.ptr ~= nil then + if str:cmp(lib.str.plit 'true') or str:cmp(lib.str.plit 'on') or + str:cmp(lib.str.plit 'yes') or str:cmp(lib.str.plit '1') then + default = true + elseif str:cmp(lib.str.plit 'false') or str:cmp(lib.str.plit 'off') or + str:cmp(lib.str.plit 'no') or str:cmp(lib.str.plit '0') then + default = false + else + lib.warn('invalid configuration setting ',name,'="',{str.ptr,str.ct},'", expected boolean; using default value instead') + end + str:free() + end + return default +end terra cfgcache:load() self.instance = self.overlord:conf_get('instance-name') self.secret = self.overlord:conf_get('server-secret') - do self.pol_reg = false - var sreg = self.overlord:conf_get('policy-self-register') - if sreg:ref() then - if lib.str.cmp(sreg.ptr, 'on') == 0 - then self.pol_reg = true - else self.pol_reg = false - end - sreg:free() - end end + self.pol_reg = self:cfbool('policy-self-register', false) do self.credmgd = false var sreg = self.overlord:conf_get('credential-store') if sreg:ref() then if lib.str.cmp(sreg.ptr, 'managed') == 0 @@ -775,20 +799,16 @@ self.pol_sec = secmode.isolate end smode:free() end - self.ui_hue = config.default_ui_accent - var shue = self.overlord:conf_get('ui-accent') - if shue.ptr ~= nil then - var hue,ok = lib.math.decparse(shue) - if ok then self.ui_hue = hue end - shue: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; } Index: static/style.scss ================================================================== --- static/style.scss +++ static/style.scss @@ -2,15 +2,15 @@ %sans { font-family: "Alegreya Sans", "Open Sans", sans-serif; } %serif { font-family: Alegreya, GaramondNo8, "Garamond Premier Pro", "Adobe Garamond", Garamond, Junicode, serif; } %teletype { font-family: "Inconsolata LGC", Inconsolata, monospace; font-size: 12pt !important; } // @function tone($pct, $alpha: 0) { @return adjust-color($color, $lightness: $pct, $alpha: $alpha) } -@function tone($pct, $alpha: 0) { - @return hsla(var(--hue), 100%, 65% + $pct, 1 + $alpha) -} +@function tone($pct, $alpha: 0) { @return hsla(var(--hue), 100%, 65% + $pct, 1 + $alpha) } +@function vtone($pct, $vary, $alpha: 0) { @return hsla(calc(var(--hue) + $vary), 100%, 65% + $pct, 1 + $alpha) } +@function otone($pct, $alpha: 0) { @return hsla(calc(var(--hue) + var(--co)), 100%, 65% + $pct, 1 + $alpha) } -:root { --hue: 323; } +:root { --hue: 323; --co: 0; } body { @extend %sans; background-color: tone(-55%); color: tone(25%); font-size: 14pt; @@ -26,10 +26,13 @@ font-style: italic; } a[href] { color: tone(10%); text-decoration-color: tone(10%,-0.5); + text-decoration-skip-ink: all; + text-decoration-thickness: 1px; + text-underline-offset: 0.1em; &:hover, &:focus { color: white; text-shadow: 0 0 15px tone(20%); text-decoration-color: tone(10%,-0.1); outline: none; @@ -70,85 +73,85 @@ @extend %sans; font-size: 14pt; box-sizing: border-box; padding: 0.1in 0.2in; border: 1px solid black; - color: tone(25%); + color: otone(25%); 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, - tone(-47%), - tone(-50%) 15%, - tone(-50%) 75%, - tone(-53%) + otone(-47%), + otone(-50%) 15%, + otone(-50%) 75%, + otone(-53%) ); &:hover, &:focus { @extend %glow; outline: none; color: tone(-55%); text-shadow: none; background: linear-gradient(to bottom, - tone(-27%), - tone(-30%) 15%, - tone(-30%) 75%, - tone(-35%) + otone(-27%), + otone(-30%) 15%, + otone(-30%) 75%, + otone(-35%) ); } &:active { color: black; padding-bottom: calc(0.1in - 2px); padding-top: calc(0.1in + 2px); background: linear-gradient(to top, - tone(-25%), - tone(-30%) 15%, - tone(-30%) 75%, - tone(-35%) + otone(-25%), + otone(-30%) 15%, + otone(-30%) 75%, + otone(-35%) ); } } button { @extend %button; &:first-of-type { @extend %button; color: white; - box-shadow: inset 0 1px tone(-25%), - inset 0 -1px tone(-50%); + box-shadow: inset 0 1px otone(-25%), + inset 0 -1px otone(-50%); background: linear-gradient(to bottom, - tone(-35%), - tone(-40%) 15%, - tone(-40%) 75%, - tone(-45%) + otone(-35%), + otone(-40%) 15%, + otone(-40%) 75%, + otone(-45%) ); &:hover, &:focus { - box-shadow: inset 0 1px tone(-15%), - inset 0 -1px tone(-40%); + box-shadow: inset 0 1px otone(-15%), + inset 0 -1px otone(-40%); } &:active { - box-shadow: inset 0 1px tone(-50%), - inset 0 -1px tone(-25%); + box-shadow: inset 0 1px otone(-50%), + inset 0 -1px otone(-25%); background: linear-gradient(to top, - tone(-30%), - tone(-35%) 15%, - tone(-35%) 75%, - tone(-40%) + otone(-30%), + otone(-35%) 15%, + otone(-35%) 75%, + otone(-40%) ); } } - &:hover { font-weight: bold; } + //&:hover { font-weight: bold; } } $grad-ui-focus: linear-gradient(to bottom, tone(-50%), tone(-35%) ); -input[type='text'], input[type='password'], textarea, select { +input[type='text'], input[type='number'], input[type='password'], textarea, select { @extend %serif; padding: 0.08in 0.1in; box-sizing: border-box; border: 1px solid black; background: linear-gradient(to bottom, tone(-55%), tone(-40%)); @@ -238,10 +241,23 @@ border: { left: 1px solid black; right: 1px solid black; } } + +.id { + color: tone(25%,-0.4); + > .nym { + font-weight: bold; + color: tone(25%); + } + > .xid { + color: tone(20%,-0.1); + font-size: 80%; + vertical-align: text-top; + } +} div.profile { padding: 0.1in; position: relative; display: grid; @@ -263,29 +279,23 @@ border: 1px solid black; } > .id { grid-column: 2 / 3; grid-row: 1 / 2; - color: tone(25%,-0.4); - > .nym { - font-weight: bold; - color: tone(25%); - } - > .xid { - color: tone(20%,-0.1); - font-size: 80%; - vertical-align: text-top; - } } > .bio { grid-column: 2 / 3; grid-row: 2 / 3; } } > .stats { grid-column: 3 / 4; grid-row: 1 / 3; + display: flex; + flex-flow: column; + > * { flex-grow: 1; } + table { td, th { text-align: center; } } } > form.actions { grid-column: 1 / 3; grid-row: 2 / 3; padding-top: 0.075in; flex-wrap: wrap; @@ -608,11 +618,22 @@ display: block; width: 100%; } textarea { resize: vertical; min-height: 2in; } } - .elem + %button { margin-left: 50%; width: 50%; } + :is(.elem,.elem-group) + %button { margin-left: 50%; width: 50%; } + .elem-group { + display: flex; + flex-flow: row; + > .elem { + flex-shrink: 1; + flex-grow: 1; + margin-left: 0.1in; + &:first-child { margin-left: 0; } + } + > .small { flex-shrink: 5; } + } } menu.choice { display: flex; &.horizontal { @@ -715,5 +736,40 @@ /* implemented using javascript, alas */ @extend %box; label { text-shadow: 1px 1px black; } padding: 0.1in; } + +ul.user-list { + list-style-type: none; + margin: 0.5em 0; + padding: 0; + box-shadow: 0 0 10px -3px black inset; + border: 1px solid tone(-50%); + li { + background-color: tone(-20%, -0.8); + padding: 0.5em; + .regalia { margin-right: 0.3em; vertical-align: bottom; } + &:nth-child(odd) { + background-color: tone(-30%, -0.8); + } + } +} + +ul.remarks { + margin: 0; padding: 0; + list-style-type: none; + li { + border-top: 1px solid otone(-22%); + border-bottom: 2px solid otone(-55%); + border-radius: 3px; + background: otone(-25%,-0.4); + color: otone(25%); + text-align: center; + padding: 0.3em 0; + margin: 0.2em 0.1em; + cursor: default; + } +} + +:is(%button, a[href]).neg { --co: 60 } +:is(%button, a[href]).pos { --co: -30 } Index: store.t ================================================================== --- store.t +++ store.t @@ -24,17 +24,29 @@ privset = lib.set { 'post', 'edit', 'acct', 'upload', 'censor', 'admin', 'invite' }; powerset = lib.set { -- user powers -- default on - 'login', 'visible', 'post', 'shout', - 'propagate', 'upload', 'acct', 'edit'; + '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', 'config', 'censor', 'suspend', - 'cred', 'elevate', 'demote', 'rebrand', -- modify site's brand identity - 'herald', -- grant serverwide epithets + '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' } @@ -48,11 +60,11 @@ (ps.[v] << true) in pt {name = lib.str.plit(v), priv = ps} end end end terra m.powerset:affect_users() - return self.purge() or self.censor() or self.suspend() or + return self.purge() or self.discipline() or self.herald() or self.elevate() or self.demote() or self.cred() end local str = rawstring local pstr = lib.mem.ptr(int8) @@ -66,18 +78,18 @@ invites: uint32 -- # of people left this user can invite powers: m.powerset } -terra m.rights_default() +terra m.rights_default() -- TODO make configurable var pow: m.powerset pow:clear() (pow.login << true) (pow.visible << true) (pow.post << true) (pow.shout << true) (pow.propagate << true) - (pow.upload << true) + (pow.artifact << true) (pow.acct << true) (pow.edit << true) return m.rights { rank = 0, quota = 1000, invites = 0, powers = pow; } end @@ -96,10 +108,53 @@ -- ephemera xid: str source: &m.source } + +terra m.actor:outranks(other: &m.actor) + -- this predicate determines where two users stand relative to + -- each other in the formal staff hierarchy. it is used in + -- authority calculations, but this function should only be + -- used directly in rendering code and by other predicates. + -- do not use it in authority calculation, as there are special + -- cases where formal rank does not fully determine a user's + -- capabilities (e.g. roots have the same rank, but can + -- exercise power over each other, unlike lower ranks) + if self.rights.rank == 0 then + -- peons never outrank anybody + return false + end + if other.rights.rank == 0 then + -- everybody outranks peons + return true + end + return self.rights.rank < other.rights.rank + -- rank 1 is the highest possible, rank 2 is second-highest, and so on +end + +terra m.actor:overpowers(other: &m.actor) + -- this predicate determines whether one user may exercise their + -- powers over another user. it does not affect what those powers + -- actually are (for instance, you cannot revoke a power you do + -- not have, no matter how much you outrank someone) + if self.rights.rank == 1 and other.rights.rank == 1 then + -- special case: root users always overpower each other + -- otherwise, nobody could reset their passwords + -- (also dissuades people from giving root lightly) + return true + end + return self:outranks(other) +end + +terra m.actor.methods.handle_validate(hnd: rawstring) + if hnd[0] == 0 then + return false + end + -- TODO validate fully + return true +end terra m.actor.methods.mk(kbuf: &uint8) var newkp = lib.crypt.genkp() var privsz = lib.crypt.der(false,&newkp,kbuf) return m.actor { @@ -282,15 +337,16 @@ conf_reset: {&m.source, rawstring} -> {} actor_create: {&m.source, &m.actor} -> uint64 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.ptr(&m.actor) - actor_enum_local: {&m.source} -> lib.mem.ptr(&m.actor) + 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 actor_auth_how: {&m.source, m.inet, rawstring} -> {m.credset, bool} -- returns a set of auth method categories that are available for a @@ -328,12 +384,12 @@ -- 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.ptr(lib.mem.ptr(m.auth)) - auth_enum_handle: {&m.source, rawstring} -> lib.mem.ptr(lib.mem.ptr(m.auth)) + 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 -- comment: pstring @@ -352,12 +408,12 @@ post_save: {&m.source, &m.post} -> {} post_create: {&m.source, &m.post} -> uint64 post_destroy: {&m.source, uint64} -> {} post_fetch: {&m.source, uint64} -> lib.mem.ptr(m.post) - post_enum_author_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post)) - post_enum_parent: {&m.source, uint64} -> lib.mem.ptr(lib.mem.ptr(m.post)) + 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 @@ -404,12 +460,12 @@ nkvd_sanction_vacate: {&m.source, uint64} -> {} nkvd_sanction_enum_target: {&m.source, uint64} -> {} nkvd_sanction_enum_issuer: {&m.source, uint64} -> {} nkvd_sanction_review: {&m.source, m.timepoint} -> {} - timeline_actor_fetch_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post)) - timeline_instance_fetch: {&m.source, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post)) + timeline_actor_fetch_uid: {&m.source, uint64, m.range} -> lib.mem.lstptr(m.post) + timeline_instance_fetch: {&m.source, m.range} -> lib.mem.lstptr(m.post) } m.user_conf_funcs(m.backend, 'str', rawstring, lib.mem.ptr(int8)) m.user_conf_funcs(m.backend, 'int', intptr, intptr, bool) Index: view/profile.tpl ================================================================== --- view/profile.tpl +++ view/profile.tpl @@ -4,21 +4,24 @@
    @nym
    @bio
    - - - - - - -
    posts @nposts
    following @nfollows
    followers @nfollowers
    mutuals @nmutuals
    @timephrase @tweetday
    +
    + + + + + + +
    posts mutuals
    @nposts @nmutuals
    following followers
    @nfollows @nfollowers
    @timephrase @tweetday
    +
      @remarks
    +
    posts archive media associates
    @auxbtn