Index: backend/pgsql.t ================================================================== --- backend/pgsql.t +++ backend/pgsql.t @@ -133,17 +133,20 @@ ), followers as ( select relator as user from parsav_rels where relatee = $1::bigint and kind = ), - mutuals as (select * from follows intersect select * from followers) + mutuals as ( + select * from follows intersect select * from followers + ) - select count(tweets.*)::bigint, - count(follows.*)::bigint, - count(followers.*)::bigint, - count(mutuals.*)::bigint - from tweets, follows, followers, mutuals + values ( + (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[r]) end) }; actor_auth_how = { params = {rawstring, lib.store.inet}, sql = [[ @@ -280,12 +283,82 @@ limit case when $3::bigint = 0 then null else $3::bigint end offset $4::bigint ]] }; + + artifact_instantiate = { + params = {binblob, binblob, pstring}, sql = [[ + insert into parsav_artifacts (content,hash,mime) values ( + $1::bytea, $2::bytea, $3::text + ) on conflict do nothing returning id + ]]; + }; + artifact_expropriate = { + params = {uint64, uint64, pstring}, cmd = true, sql = [[ + insert into parsav_artifact_claims (uid,rid,description,folder) values ( + $1::bigint, $2::bigint, $3::text, 'new' + ) on conflict do nothing + ]]; + }; + artifact_quicksearch = { + params = {binblob}, sql = [[ + select id, (content is null) from parsav_artifacts where hash = $1::bytea + limit 1 + ]]; + }; + artifact_disclaim = { + params = {uint64, uint64}, cmd = true, sql = [[ + delete from parsav_artifact_claims where + uid = $1::bigint and + rid = $2::bigint + ]]; + }; + artifact_excise_forget = { + -- delete the blasted thing and pretend it never existed + params = {uint64}, cmd=true, sql = [[ + delete from parsav_artifacts where id = $1::bigint + ]]; + }; + artifact_excise_suppress_nullify = { + -- banish the thing into the outer darkness, preventing + -- it from ever being admitted into our databases, and + -- tabulate a -- list of the degenerates who befouled + -- their accounts with such wanton and execrable filth, + -- the better to ensure their long-overdue punishment + params = {uint64}, cmd=true, sql = [[ + update parsav_artifacts + set content = null + where id = $1::bigint; + ]]; + }; + artifact_excise_suppress_breaklinks = { + -- "ERROR: cannot insert multiple commands into a prepared + -- statement" are you fucking shitting me with this shit + params = {uint64}, sql = [[ + delete from parsav_artifact_claims where + rid = $1::bigint + returning uid, description, birth, folder; + ]]; + }; + post_attach_ctl_ins = { + params = {uint64, uint64}, cmd=true, sql = [[ + update parsav_posts set + artifacts = artifacts || $2::bigint + where id = $1::bigint and not + artifacts @> array[$2::bigint] + ]]; + }; + post_attach_ctl_del = { + params = {uint64, uint64}, cmd=true, sql = [[ + update parsav_posts set + artifacts = array_remove(artifacts, $2::bigint) + where id = $1::bigint and + artifacts @> array[$2::bigint] + ]]; + }; } - --($5::bool = false or p.parent is null) and local struct pqr { sz: intptr res: &lib.pq.PGresult } @@ -687,10 +760,32 @@ else lib.warn('backend pgsql - failed to obliterate database: \n', lib.pq.PQresultErrorMessage(res)) return false end end]; + + tx_enter = [terra(src: &lib.store.source) + var res = lib.pq.PQexec([&lib.pq.PGconn](src.handle), 'begin') + if lib.pq.PQresultStatus(res) == lib.pq.PGRES_COMMAND_OK then + lib.dbg('beginning pgsql transaction') + return true + else + lib.warn('backend pgsql - failed to begin transaction: \n', lib.pq.PQresultErrorMessage(res)) + return false + end + end]; + + tx_complete = [terra(src: &lib.store.source) + var res = lib.pq.PQexec([&lib.pq.PGconn](src.handle), 'end') + if lib.pq.PQresultStatus(res) == lib.pq.PGRES_COMMAND_OK then + lib.dbg('completing pgsql transaction') + return true + else + lib.warn('backend pgsql - failed to complete transaction: \n', lib.pq.PQresultErrorMessage(res)) + return false + end + end]; conf_get = [terra(src: &lib.store.source, key: rawstring) var r = queries.conf_get.exec(src, key) if r.sz == 0 then return [lib.mem.ptr(int8)] { ptr = nil, ct = 0 } else defer r:free() @@ -907,10 +1002,68 @@ auth_purge_trust = [terra(src: &lib.store.source, uid: uint64, handle: rawstring): {} queries.auth_purge_type.exec(src, handle, uid, 'trust') end]; - actor_auth_register_uid = nil; -- not necessary for view-based auth + artifact_quicksearch = [terra( + src: &lib.store.source, + hash: binblob + ): {uint64, bool} + var srec = queries.artifact_quicksearch.exec(src, hash) + if srec.sz > 0 then + defer srec:free() + var id = srec:int(uint64,0,0) + var ban = srec:bool(0,1) + return id, ban + else return 0, false end + end]; + + artifact_instantiate = [terra( + src: &lib.store.source, + artifact: binblob, + mime: pstring + ): uint64 + var arthash: uint8[lib.crypt.algsz.sha256] + if lib.md.mbedtls_md(lib.md.mbedtls_md_info_from_type(lib.crypt.alg.sha256.id), + artifact.ptr, artifact.ct, &arthash[0]) ~= 0 then + lib.bail('could not hash artifact to be instantiated') + end + var hashb = binblob{ptr=&arthash[0],ct=[arthash.type.N]} + + var srec = queries.artifact_quicksearch.exec(src, hashb) + if srec.sz > 0 then + defer srec:free() + var ban = srec:bool(0,1) + if ban then + lib.report('user attempted to instantiate forsaken artifact') + return 0 + end + var oldid = srec:int(uint64,0,0) + return oldid + else -- not in db, insert + var nrec = queries.artifact_instantiate.exec(src, artifact, hashb, mime) + if nrec.sz == 0 then + lib.warn('failed to instantiate artifact -- are you running out of storage?') + return 0 + else defer nrec:free() + var newid = nrec:int(uint64,0,0) + return newid + end + end + end]; + + post_attach_ctl = [terra( + src: &lib.store.source, + post: uint64, + artifact: uint64, + detach: bool + ): {} + if detach + then queries.post_attach_ctl_del.exec(src,post,artifact) + else queries.post_attach_ctl_ins.exec(src,post,artifact) + end + end]; + actor_auth_register_uid = nil; -- TODO better support non-view based auth } return b Index: backend/schema/pgsql-drop.sql ================================================================== --- backend/schema/pgsql-drop.sql +++ backend/schema/pgsql-drop.sql @@ -7,12 +7,13 @@ 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_attach cascade; +drop table if exists parsav_artifacts cascade; +drop table if exists parsav_artifact_claims cascade; drop table if exists parsav_circles cascade; drop table if exists parsav_rooms cascade; drop table if exists parsav_room_members cascade; drop table if exists parsav_invites cascade; -drop table if exists parsav_interventions cascade; +drop table if exists parsav_sanctions cascade; drop table if exists parsav_auth cascade; Index: backend/schema/pgsql.sql ================================================================== --- backend/schema/pgsql.sql +++ backend/schema/pgsql.sql @@ -28,10 +28,11 @@ handle text not null, -- nym [@handle@origin] origin bigint references parsav_servers(id) on delete cascade, -- null origin = local actor knownsince timestamp, 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, key bytea, -- private if localactor; public if remote epithet text, @@ -57,13 +58,14 @@ subject text, acl text not null default 'all', -- just store the script raw 🤷 body text, posted timestamp not null, discovered timestamp not null, - parent bigint not null default 0, + 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[], 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 @@ -95,39 +97,52 @@ actor bigint references parsav_actors(id) on delete cascade, post bigint not null ); -create table parsav_attach ( +create table parsav_artifacts ( id bigint primary key default (1+random()*(2^63-1))::bigint, birth timestamp not null default now(), - content bytea not null, - mime text, -- null if unknown, will be reported as x-octet-stream + 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(), + uid bigint references parsav_actors(id) on delete cascade, + rid bigint references parsav_artifacts(id) on delete cascade, description text, - parent bigint -- post id, or userid for avatars + folder text, + + unique (uid,rid) ); +create index on parsav_artifact_claims (uid); create table parsav_circles ( id bigint primary key default (1+random()*(2^63-1))::bigint, - owner bigint not null references parsav_actors(id), + 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 table parsav_rooms ( id bigint primary key default (1+random()*(2^63-1))::bigint, - origin bigint references parsav_servers(id), + origin bigint references parsav_servers(id) on delete cascade, name text not null, description text not null, policy smallint not null ); create table parsav_room_members ( - room bigint references parsav_rooms(id), - member bigint references parsav_actors(id), + 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) ); @@ -135,23 +150,26 @@ 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 -- process are thus inherited by the user - issuer bigint references parsav_actors(id), + issuer bigint references parsav_actors(id) on delete set null, handle text, -- admin can lock invite to specific handle rank smallint not null default 0, quota integer not null default 1000 ); -create table parsav_interventions ( +create table parsav_sanctions ( id bigint primary key default (1+random()*(2^63-1))::bigint, - issuer bigint references parsav_actors(id) not null, + 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, etc - victim bigint not null, -- could potentially target group as well - expire timestamp -- auto-expires if set + 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 + reason text, -- visible to victim if set + context text -- admin-only note ); -- create a temporary managed auth table; we can delete this later -- if it ends up being replaced with a view %include pgsql-auth.sql% Index: mgtool.t ================================================================== --- mgtool.t +++ mgtool.t @@ -69,11 +69,11 @@ if fn == meth then rt = ft.type.returntype break end end return quote var r: rt - if self.all + if self.all or (self.srv ~= nil and self.srv.sources.ct == 1) then r=self.srv:[meth]([expr]) elseif self.src ~= nil then r=self.src:[meth]([expr]) else lib.bail('no data source specified') end in r end @@ -273,20 +273,22 @@ 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:dbsetup() - srv:conprep(lib.store.prepmode.conf) - dlg:conf_set('instance-name', dbmode.arglist(1)) - do var sec: int8[65] gensec(&sec[0]) - dlg:conf_set('server-secret', &sec[0]) - end - lib.report('database setup complete; use mkroot to create an administrative user') + if dlg:dbsetup() then + srv:conprep(lib.store.prepmode.conf) + dlg:conf_set('instance-name', dbmode.arglist(1)) + do var sec: int8[65] gensec(&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 elseif lib.str.cmp(dbmode.arglist(0),'obliterate') == 0 then var confirmstrs = array( - 'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'eta', 'nu', 'kappa' + '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 Index: render/conf.t ================================================================== --- render/conf.t +++ render/conf.t @@ -64,11 +64,12 @@ var pgt = pg:tostr() defer pgt:free() co:stdpage([lib.srv.convo.page] { title = 'configure'; body = pgt; class = lib.str.plit 'conf'; + cache = false; }) if panel.ct ~= 0 then panel:free() end end return render_conf Index: render/docpage.t ================================================================== --- render/docpage.t +++ render/docpage.t @@ -46,10 +46,11 @@ title = R(t.meta.title); content = page { title = ['documentation :: ' .. t.meta.title]; body = [ t.text ]; class = P'doc article'; + cache = true; }; } end end local terra @@ -109,11 +110,12 @@ var bp = list:finalize() co:stdpage(page { title = 'documentation'; body = bp; class = P'doc listing'; + cache = false; }) bp:free() else showpage(co, pg) end end return render_docpage Index: render/login.t ================================================================== --- render/login.t +++ render/login.t @@ -1,15 +1,14 @@ -- vim: ft=terra local pstr = lib.mem.ptr(int8) local P = lib.str.plit local terra login_form(co: &lib.srv.convo, user: &lib.store.actor, creds: &lib.store.credset, msg: pstr) - var doc = data.view.docskel { - instance = co.srv.cfg.instance; + var doc = [lib.srv.convo.page] { title = lib.str.plit 'instance logon'; class = lib.str.plit 'login'; - navlinks = co.navbar; + cache = false; } if user == nil then var form = data.view.login_username { loginmsg = msg; @@ -54,13 +53,10 @@ doc.body = ch:tostr() else -- pick a method end - var hdrs = array( - lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' } - ) - doc:send(co.con,200,[lib.mem.ptr(lib.http.header)] {ct = 1, ptr = &hdrs[0]}) + co:stdpage(doc) doc.body:free() end return login_form Index: render/nav.t ================================================================== --- render/nav.t +++ render/nav.t @@ -1,16 +1,16 @@ -- vim: ft=terra local terra render_nav(co: &lib.srv.convo) var t: lib.str.acc t:init(64) if co.who ~= nil or co.srv.cfg.pol_sec == lib.srv.secmode.public then - t:lpush('timeline') + t:lpush(' timeline') end if co.who ~= nil then - t:lpush('compose compose profile configure docs log out') else - t:lpush('docs log in') + t:lpush(' docs log in') end return t:finalize() end return render_nav Index: render/nym.t ================================================================== --- render/nym.t +++ render/nym.t @@ -1,21 +1,29 @@ -- vim: ft=terra local pstr = lib.mem.ptr(int8) +local terra cs(s: rawstring) + return pstr { ptr = s, ct = lib.str.sz(s) } +end local terra render_nym(who: &lib.store.actor, scope: uint64) var n: lib.str.acc n:init(128) + var xidsan = lib.html.sanitize(cs(who.xid),false) if who.nym ~= nil and who.nym[0] ~= 0 then - n:compose('',who.nym,' [', - who.xid,']') - else n:compose('',who.xid,'') end + var nymsan = lib.html.sanitize(cs(who.nym),false) + n:compose('',nymsan,' [', + xidsan,']') + nymsan:free() + else n:compose('',xidsan,'') end + xidsan:free() if who.epithet ~= nil then - n:lpush(' '):push(who.epithet,0):lpush('') + var episan = lib.html.sanitize(cs(who.epithet),false) + n:lpush(' '):ppush(episan):lpush('') + episan:free() end -- TODO: if scope == chat room then lookup titles in room member db - return n:finalize() end return render_nym Index: render/timeline.t ================================================================== --- render/timeline.t +++ render/timeline.t @@ -31,19 +31,15 @@ lib.render.tweet(co, posts(i).ptr, &acc) posts(i):free() end posts:free() - var doc = data.view.docskel { - instance = co.srv.cfg.instance; + var doc = [lib.srv.convo.page] { title = lib.str.plit'timeline'; body = acc:finalize(); class = lib.str.plit'timeline'; - navlinks = co.navbar; + cache = false; } - var hdrs = array( - lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' } - ) - doc:send(co.con,200,[lib.mem.ptr(lib.http.header)] {ct = 1, ptr = &hdrs[0]}) + co:stdpage(doc) doc.body:free() end return render_timeline Index: render/userpage.t ================================================================== --- render/userpage.t +++ render/userpage.t @@ -28,12 +28,13 @@ var bdf = acc:finalize() co:stdpage([lib.srv.convo.page] { title = tiptr; body = bdf; class = lib.str.plit 'profile'; + cache = false; }) tiptr:free() bdf:free() end return render_userpage Index: route.t ================================================================== --- route.t +++ route.t @@ -117,11 +117,11 @@ else var sesskey: int8[lib.session.maxlen + #lib.session.cookiename + #"=; Path=/" + 1] do var p = &sesskey[0] p = lib.str.ncpy(p, [lib.session.cookiename .. '='], [#lib.session.cookiename + 1]) p = p + lib.session.cookie_gen(co.srv.cfg.secret, aid, lib.osclock.time(nil), p) - lib.dbg('sending cookie',&sesskey[0]) + lib.dbg('sending cookie ',{&sesskey[0],15}) p = lib.str.ncpy(p, '; Path=/', 9) end co:reroute_cookie('/', &sesskey[0]) end end Index: srv.t ================================================================== --- srv.t +++ srv.t @@ -158,11 +158,14 @@ end terra convo:reroute(dest: rawstring) self:reroute_cookie(dest,nil) end terra convo:complain(code: uint16, title: rawstring, msg: rawstring) - var hdrs = array(lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' }) + var hdrs = array( + lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' }, + lib.http.header { key = 'Cache-Control', value = 'no-store' } + ) var ti: lib.str.acc ti:compose('error :: ', title) var bo: lib.str.acc bo:compose('

',title,'

',msg,'

') var body = data.view.docskel { instance = self.srv.cfg.instance; @@ -196,10 +199,11 @@ struct convo.page { title: pstring body: pstring class: pstring + cache: bool } terra convo:stdpage(pg: convo.page) var doc = data.view.docskel { instance = self.srv.cfg.instance; @@ -208,14 +212,15 @@ class = pg.class; navlinks = self.navbar; } var hdrs = array( - lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' } + lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' }, + lib.http.header { key = 'Cache-Control', value = 'no-store' } ) - doc:send(self.con,200,[lib.mem.ptr(lib.http.header)] {ct = [hdrs.type.N], ptr = &hdrs[0]}) + doc:send(self.con,200,[lib.mem.ptr(lib.http.header)] {ct = [hdrs.type.N] - lib.trn(pg.cache,1,0), ptr = &hdrs[0]}) end -- CALL ONLY ONCE PER VAR terra convo:postv(name: rawstring) if self.varbuf.ptr == nil then Index: static/style.scss ================================================================== --- static/style.scss +++ static/style.scss @@ -129,10 +129,11 @@ ); input[type='text'], input[type='password'], textarea { @extend %serif; padding: 0.08in 0.1in; + box-sizing: border-box; border: 1px solid black; background: linear-gradient(to bottom, tone(-55%), tone(-40%)); font-size: 16pt; color: tone(25%); box-shadow: inset 0 0 20px -3px tone(-55%); @@ -364,22 +365,23 @@ } form.compose { @extend %box; display: grid; - grid-template-columns: 1.1in 2fr min-content 1fr; + grid-template-columns: 1.1in 2fr min-content 1fr 1.5fr; grid-template-rows: 1fr min-content; grid-gap: 2px; padding: 0.1in; > img { grid-column: 1/2; grid-row: 1/3; width: 1in; height: 1in;} > textarea { - grid-column: 2/5; grid-row: 1/2; height: 3in; + grid-column: 2/6; grid-row: 1/2; height: 3in; resize: vertical; margin-bottom: 0.08in; } > input[name="acl"] { grid-column: 2/3; grid-row: 2/3; } - > button { grid-column: 4/5; grid-row: 2/3; } + > button[value="post"] { grid-column: 5/6; grid-row: 2/3; } + > button[value="attach"] { grid-column: 4/5; grid-row: 2/3; } a.help[href] { margin-right: 0.05in } } a.help[href] { display: block; @@ -543,6 +545,26 @@ border: 1px solid tone(-55%); border-left: none; text-shadow: 1px 1px 0 black; } } + +} + +form { + .elem { + margin: 0.1in 0; + label { display:block; font-weight: bold; padding: 0.03in 0; } + .txtbox { + @extend %serif; + box-sizing: border-box; + padding: 0.08in 0.1in; + border: 1px solid black; + background: tone(-55%); + } + input, textarea, .txtbox { + display: block; + width: 100%; + } + button { float: right; width: 50%; } + } } Index: store.t ================================================================== --- store.t +++ store.t @@ -14,21 +14,22 @@ }; credset = lib.set { 'pw', 'otp', 'challenge', 'trust' }; privset = lib.set { - 'post', 'edit', 'acct', 'upload', 'censor', 'admin' + 'post', 'edit', 'acct', 'upload', 'censor', 'admin', 'invite' }; powerset = lib.set { -- user powers -- default on 'login', 'visible', 'post', 'shout', 'propagate', 'upload', 'acct', 'edit'; -- admin powers -- default off 'purge', 'config', 'censor', 'suspend', 'cred', 'elevate', 'demote', 'rebrand', -- modify site's brand identity - 'herald' -- grant serverwide epithets + 'herald', -- grant serverwide epithets + 'invite' -- *unlimited* invites }; prepmode = lib.enum { 'full','conf','admin' } } @@ -54,25 +55,26 @@ struct m.rights { rank: uint16 -- lower = more powerful except 0 = regular user -- creating staff automatically assigns rank immediately below you quota: uint32 -- # of allowed tweets per day; 0 = no limit + invites: intptr -- # of people left this user can invite powers: m.powerset } terra m.rights_default() - var pow: m.powerset pow:fill() - (pow.purge << false) - (pow.config << false) - (pow.censor << false) - (pow.suspend << false) - (pow.elevate << false) - (pow.demote << false) - (pow.cred << false) - (pow.rebrand << false) - return m.rights { rank = 0, quota = 1000, powers = pow; } + 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.acct << true) + (pow.edit << true) + return m.rights { rank = 0, quota = 1000, invites = 0, powers = pow; } end struct m.actor { id: uint64 nym: str @@ -193,12 +195,36 @@ var segs = hexchs / 4 var seps = segs - 1 var maxsz = hexchs + seps + 1 else return nil end end + +struct m.kompromat { +-- The Evidence + id: uint64 + perp: uint64 -- whodunnit + desc: str + post: uint64 -- the post in question, if any + reporter: uint64 -- 0 = originated automatically by the System itself + resolution: str -- null for unresolved + -- as proto: set resolution to empty string to search for resolved incidents +} + +struct m.sanction { + id: uint64 + issuer: uint64 + scope: uint64 + nature: uint16 + victim: uint64 + autoexpire: bool expire: m.timepoint + timedreview: bool review: m.timepoint + reason: str + context: str +} struct m.auth { +-- a credential record aid: uint64 uid: uint64 aname: str netmask: m.inet privs: m.privset @@ -211,10 +237,17 @@ close: &m.source -> {} dbsetup: &m.source -> bool -- creates the schema needed to call conprep (called only once per database e.g. with `parsav db init`) conprep: {&m.source, m.prepmode.t} -> {} -- prepares queries and similar tasks that require the schema to already be in place obliterate_everything: &m.source -> bool -- wipes everything parsav-related out of the database + tx_enter: &m.source -> bool + tx_complete: &m.source -> bool + -- these two functions are special, in that they should be called + -- directly on a specific backend, rather than passed down to the + -- backends by the server; that is pathological behavior that will + -- not have the desired effect + conf_get: {&m.source, rawstring} -> lib.mem.ptr(int8) conf_set: {&m.source, rawstring, rawstring} -> {} conf_reset: {&m.source, rawstring} -> {} actor_create: {&m.source, &m.actor} -> uint64 @@ -275,12 +308,59 @@ auth_purge_trust: {&m.source, uint64, rawstring} -> {} post_save: {&m.source, &m.post} -> {} post_create: {&m.source, &m.post} -> uint64 post_enum_author_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(lib.mem.ptr(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 + artifact_instantiate: {&m.source, lib.mem.ptr(uint8), lib.mem.ptr(int8)} -> uint64 + -- instantiate an artifact in the database, either installing a new + -- artifact or returning the id of an existing artifact with the same hash + -- artifact: bytea + -- mime: pstring + artifact_quicksearch: {&m.source, lib.mem.ptr(uint8)} -> {uint64,bool} + -- checks whether a hash is already in the database without uploading + -- the entire file to the database server + -- hash: bytea + --> artifact id (0 if null), suppressed? + artifact_expropriate: {&m.source, uint64, uint64, lib.mem.ptr(int8)} -> {} + -- claims an existing artifact for the user's own collection + -- uid: uint64 + -- artifact id: uint64 + -- description: pstring + artifact_disclaim: {&m.source, uint64, uint64} -> {} + -- a user disclaims their ownership stake in an artifact, removing it from + -- the database entirely if they were the only owner, and removing their + -- description of it either way + -- uid: uint64 + -- artifact id: uint64 + artifact_excise: {&m.source, uint64, bool} -> {} + -- (admin action) forcibly excise an artifact from the database, deleting + -- all links to it and removing it from users' collections. if "blacklist," + -- the artifact will be banned and attempts to upload it in the future + -- will fail, triggering a report. mainly intended for dealing with spam, + -- IP violations, That Which Shall Not Be Named, and various other infohazards. + -- artifact id: uint64 + -- blacklist: bool + + nkvd_report_issue: {&m.source, &m.kompromat} -> {} + -- an incidence of Badthink has been detected. report it immediately + -- to the Supreme Soviet + nkvd_reports_enum: {&m.source, &m.kompromat} -> lib.mem.ptr(m.kompromat) + -- search through the Archives + -- proto: kompromat (null for all records, or a prototype describing the records to return) + nkvd_sanction_issue: {&m.source, &m.sanction} -> uint64 + 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} -> {} + convo_fetch_xid: {&m.source,rawstring} -> lib.mem.ptr(m.post) - convo_fetch_uid: {&m.source,uint64} -> lib.mem.ptr(m.post) + convo_fetch_cid: {&m.source,uint64} -> lib.mem.ptr(m.post) 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)) } Index: view/compose.tpl ================================================================== --- view/compose.tpl +++ view/compose.tpl @@ -1,10 +1,11 @@
@?acl - + +
Index: view/conf-profile.tpl ================================================================== --- view/conf-profile.tpl +++ view/conf-profile.tpl @@ -1,6 +1,6 @@
- - - - +
@!handle
+
+
+