Index: backend/pgsql.t ================================================================== --- backend/pgsql.t +++ backend/pgsql.t @@ -15,18 +15,18 @@ where id = $1::bigint ]]; }; conf_get = { - params = {rawstring}, sql = [[ + params = {pstring}, sql = [[ select value from parsav_config where key = $1::text limit 1 ]]; }; conf_set = { - params = {rawstring,rawstring}, cmd=true, sql = [[ + params = {pstring,pstring}, cmd=true, sql = [[ insert into parsav_config (key, value) values ($1::text, $2::text) on conflict (key) do update set value = $2::text ]]; }; @@ -293,17 +293,40 @@ ($1::bigint = 0 or $1::bigint = owner) and ($2::bigint = 0 or $2::bigint = id) ]]; }; - circle_members_fetch_cid = { - params = {uint64, uint64}, sql = [[ - select unnest(members) from parsav_circles where - ($1::bigint = 0 or owner = $1::bigint) and + circle_create = { + params = {uint64,pstring}, sql = [[ + insert into parsav_circles (owner,name) + values ($1::bigint, $2::text) + returning id + ]]; + }; + + circle_destroy = { + params = {uint64,uint64}, cmd = true, sql = [[ + delete from parsav_circles where + owner = $1::bigint and id = $2::bigint ]]; }; + + circle_memberships_uid = { + params = {uint64, uint64}, sql = [[ + select name, id, owner, array_length(members,1) from parsav_circles where + owner = $1::bigint and + members @> array[$2::bigint] + ]]; + }; + + circle_members_fetch_cid = { + params = {uint64}, sql = [[ + select unnest(members) from parsav_circles where + id = $1::bigint + ]]; + }; circle_members_fetch_name = { params = {uint64, pstring}, sql = [[ select unnest(members) from parsav_circles where ($1::bigint = 0 or owner = $1::bigint) and @@ -559,18 +582,40 @@ limit case when $3::bigint = 0 then null else $3::bigint end offset $4::bigint ]]; }; + + timeline_circle_fetch = { + params = {uint64, uint64, uint64, uint64, uint64}, sql = [[ + with circle as ( + select unnest(members) from parsav_circles where id = $1::bigint + ) + + 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 circle) or + c.promoter = (select owner from parsav_circles where id = $1::bigint)) + + order by c.tltime desc + + limit case when $4::bigint = 0 then null + else $4::bigint end + offset $5::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 ( + ), avoided as ( -- not strictly necessary but lets us minimize how much data needs to be sent back to parsav for filtering 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 @@ -582,11 +627,12 @@ 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)) + not ((c.post).author in (table avoided)) and + not (c.promoter in (table avoided)) order by c.tltime desc limit case when $4::bigint = 0 then null else $4::bigint end offset $5::bigint @@ -698,19 +744,19 @@ artifacts @> array[$2::bigint] ]]; }; actor_conf_str_get = { - params = {uint64, rawstring}, sql = [[ + params = {uint64, pstring}, sql = [[ select value from parsav_actor_conf_strs where uid = $1::bigint and key = $2::text limit 1 ]]; }; actor_conf_str_set = { - params = {uint64, rawstring, rawstring}, cmd = true, sql = [[ + params = {uint64, pstring, pstring}, cmd = true, sql = [[ insert into parsav_actor_conf_strs (uid,key,value) values ($1::bigint, $2::text, $3::text) on conflict (uid,key) do update set value = $3::text ]]; }; @@ -718,26 +764,26 @@ params = {uint64}, sql = [[ select value from parsav_actor_conf_strs where uid = $1::bigint ]]; }; actor_conf_str_reset = { - params = {uint64, rawstring}, cmd = true, sql = [[ + params = {uint64, pstring}, cmd = true, sql = [[ delete from parsav_actor_conf_strs where uid = $1::bigint and ($2::text is null or key = $2::text) ]] }; actor_conf_int_get = { - params = {uint64, rawstring}, sql = [[ + params = {uint64, pstring}, sql = [[ select value from parsav_actor_conf_ints where uid = $1::bigint and key = $2::text limit 1 ]]; }; actor_conf_int_set = { - params = {uint64, rawstring, uint64}, cmd = true, sql = [[ + params = {uint64, pstring, uint64}, cmd = true, sql = [[ insert into parsav_actor_conf_ints (uid,key,value) values ($1::bigint, $2::text, $3::bigint) on conflict (uid,key) do update set value = $3::bigint ]]; }; @@ -745,11 +791,11 @@ params = {uint64}, sql = [[ select value from parsav_actor_conf_ints where uid = $1::bigint ]]; }; actor_conf_int_reset = { - params = {uint64, rawstring}, cmd = true, sql = [[ + params = {uint64, pstring}, cmd = true, sql = [[ delete from parsav_actor_conf_ints where uid = $1::bigint and ($2::text is null or key = $2::text) ]] }; } @@ -1276,18 +1322,18 @@ lib.warn('backend pgsql - failed to obliterate database: \n', lib.pq.PQresultErrorMessage(res)) return false end end]; - conf_get = [terra(src: &lib.store.source, key: rawstring) + conf_get = [terra(src: &lib.store.source, key: pstring) 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() return r:String(0,0) end end]; - conf_set = [terra(src: &lib.store.source, key: rawstring, val: rawstring) + conf_set = [terra(src: &lib.store.source, key: pstring, val: pstring) queries.conf_set.exec(src, key, val):free() end]; conf_reset = [terra(src: &lib.store.source, key: rawstring) queries.conf_reset.exec(src, key):free() end]; actor_fetch_uid = [terra(src: &lib.store.source, uid: uint64) @@ -1548,10 +1594,28 @@ rg: lib.store.range ): lib.mem.lstptr(lib.store.post) var r = pqr { sz = 0 } var A,B,C,D = rg:matrix() -- :/ r = queries.timeline_instance_fetch.exec(src,A,B,C,D) + + var ret: lib.mem.ptr(lib.mem.ptr(lib.store.post)) ret:init(r.sz) + for i=0,r.sz do + ret.ptr[i] = row_to_post(&r, i) -- MUST FREE ALL + ret.ptr[i].ptr.source = src + end + + return ret + end]; + + timeline_circle_fetch = [terra( + src: &lib.store.source, + cid: uint64, + rg: lib.store.range + ): lib.mem.lstptr(lib.store.post) + var r = pqr { sz = 0 } + var A,B,C,D = rg:matrix() -- :/ + r = queries.timeline_circle_fetch.exec(src,cid,A,B,C,D) var ret: lib.mem.ptr(lib.mem.ptr(lib.store.post)) ret:init(r.sz) for i=0,r.sz do ret.ptr[i] = row_to_post(&r, i) -- MUST FREE ALL ret.ptr[i].ptr.source = src @@ -2034,36 +2098,39 @@ uid: uint64, time: lib.store.timepoint ): {} queries.auth_sigtime_user_alter.exec(src, uid, time) end]; actor_conf_str_enum = nil; - actor_conf_str_get = [terra(src: &lib.store.source, uid: uint64, key: rawstring): pstring + actor_conf_str_get = [terra( + src: &lib.store.source, + pool: &lib.mem.pool, + uid: uint64, + key: pstring + ): pstring var r = queries.actor_conf_str_get.exec(src, uid, key) if r.sz > 0 then - var ret = r:String(0,0) - r:free() - return ret + return r:_string(0,0):pdup(pool) else return pstring.null() end end]; - actor_conf_str_set = [terra(src: &lib.store.source, uid: uint64, key: rawstring, value: rawstring): {} + actor_conf_str_set = [terra(src: &lib.store.source, uid: uint64, key: pstring, value: pstring): {} queries.actor_conf_str_set.exec(src,uid,key,value) end]; - actor_conf_str_reset = [terra(src: &lib.store.source, uid: uint64, key: rawstring): {} + actor_conf_str_reset = [terra(src: &lib.store.source, uid: uint64, key: pstring): {} queries.actor_conf_str_reset.exec(src,uid,key) end]; actor_conf_int_enum = nil; - actor_conf_int_get = [terra(src: &lib.store.source, uid: uint64, key: rawstring) + actor_conf_int_get = [terra(src: &lib.store.source, uid: uint64, key: pstring) var r = queries.actor_conf_int_get.exec(src, uid, key) if r.sz > 0 then var ret = r:int(uint64,0,0) r:free() return ret, true end return 0, false end]; - actor_conf_int_set = [terra(src: &lib.store.source, uid: uint64, key: rawstring, value: uint64): {} + actor_conf_int_set = [terra(src: &lib.store.source, uid: uint64, key: pstring, value: uint64): {} queries.actor_conf_int_set.exec(src,uid,key,value) end]; - actor_conf_int_reset = [terra(src: &lib.store.source, uid: uint64, key: rawstring): {} + actor_conf_int_reset = [terra(src: &lib.store.source, uid: uint64, key: pstring): {} queries.actor_conf_int_reset.exec(src,uid,key) end]; circle_search = [terra( src: &lib.store.source, pool:&lib.mem.pool, @@ -2076,27 +2143,62 @@ var rt = pool:alloc(lib.store.circle, res.sz) for i = 0, res.sz do var name = res:_string(i,0) rt(i) = lib.store.circle { - name = name:pdup(pool); - cid = res:int(uint64,i,1); - owner = res:int(uint64,i,2); - memcount = res:int(uint64,i,3); + name = name:pdup(pool); cid = res:int(uint64,i,1); + owner = res:int(uint64,i,2); memcount = res:int(uint64,i,3); + } + end + + return rt + end]; + + circle_create = [terra( + src: &lib.store.source, + owner: uint64, + name: pstring + ): uint64 + var r = queries.circle_create.exec(src, owner, name) + if r.sz > 0 then defer r:free() return r:int(uint64,0,0) end + return 0 + end]; + + circle_destroy = [terra( + src: &lib.store.source, + owner: uint64, + cid: uint64 + ): {} queries.circle_destroy.exec(src, owner, cid) end]; + + circle_memberships_uid = [terra( + src: &lib.store.source, + pool:&lib.mem.pool, + owner: uint64, + subject: uint64 + ): lib.mem.ptr(lib.store.circle) + var res = queries.circle_memberships_uid.exec(src, owner, subject) + if res.sz == 0 then return [lib.mem.ptr(lib.store.circle)].null() end + defer res:free() + + var rt = pool:alloc(lib.store.circle, res.sz) + for i = 0, res.sz do + var name = res:_string(i,0) + rt(i) = lib.store.circle { + name = name:pdup(pool); cid = res:int(uint64,i,1); + owner = res:int(uint64,i,2); memcount = res:int(uint64,i,3); } end return rt end]; circle_members_fetch_cid = [terra( src: &lib.store.source, pool:&lib.mem.pool, - uid: uint64, cid: uint64 ): lib.mem.ptr(uint64) - var res = queries.circle_members_fetch_cid.exec(src,uid,cid) + var res = queries.circle_members_fetch_cid.exec(src,cid) if res.sz == 0 then return [lib.mem.ptr(uint64)].null() end defer res:free() var rt = pool:alloc(uint64, res.sz) for i = 0, res.sz do rt(i) = res:int(uint64,i,0) end Index: config.lua ================================================================== --- config.lua +++ config.lua @@ -53,10 +53,11 @@ -- the damn things before compiling (also making the binary smaller) {'style.css', 'text/css'}; {'live.js', 'text/javascript'}; -- rrrrrrrr {'default-avatar.webp', 'image/webp'}; -- needs inkscape-exclusive svg features {'bell.svg', 'image/svg+xml'}; + {'gear.svg', 'image/svg+xml'}; {'heart.webp', 'image/webp'}; {'retweet.webp', 'image/webp'}; {'padlock.svg', 'image/svg+xml'}; {'warn.svg', 'image/svg+xml'}; {'query.webp', 'image/webp'}; Index: mem.t ================================================================== --- mem.t +++ mem.t @@ -110,10 +110,27 @@ terra t:ref() return self.ptr ~= nil end t.metamethods.__not = macro(function(self) return `not self:ref() end) t.metamethods.__apply = macro(function(self,idx) return `self.ptr[ [idx or 0] ] end) t.metamethods.__update = macro(function(self,idx,rhs) return quote self.ptr[idx] = rhs end end) + t.metamethods.__cast = function(from,to,exp) + if to == t then + if from == niltype then return `t.null() + elseif from == &ty then return `t {ptr = exp, ct = 1} + elseif from == ty then return `t {ptr = &exp, ct = 1} + elseif from.N and from.type == ty then + return `t {ptr = &exp[0], ct = from.N } + end + error('invalid cast to ' .. t.name .. ' from ' .. tostring(from)) + elseif from == t then + if to == &ty then return `exp.ptr + elseif to == ty then return `@exp.ptr + elseif to == bool then return `exp:ref() end + error('invalid cast from ' .. t.name .. ' to ' .. tostring(to)) + end + error('invalid pointer cast') + end if not ty:isstruct() then terra t:cmp_raw(other: &ty) for i=0, self.ct do if self.ptr[i] ~= other[i] then return false end Index: parsav.md ================================================================== --- parsav.md +++ parsav.md @@ -144,6 +144,6 @@ * mariadb/mysql * the various nosql horrors, e.g. redis, mongo, and so on parsav urgently needs an internationalization framework as well. right now everything is just hardcoded in english. yuck. -parsav could be significantly improved by adjusting its memory management strategy. instead of allocating everything with lib.mem.heapa (which currently maps to malloc on all platforms), we should allocate a static buffer for the server overlord object which can simply be cleared and re-used for each http request, and enlarged with `realloc` when necessary. the entire region could be `mlock`ed for better performance, and it would no longer be necessary to track and free memory, as the entire buffer would simply be discarded after use (similar to PHP's original memory management strategy). this would remove possibly the largest source of latency in the codebase, as `parsav` is regrettably quite heavy on malloc, performing numerous allocations for each page rendered. **update:** this is now in progress +parsav could be significantly improved by adjusting its memory management strategy. instead of allocating everything with lib.mem.heapa (which currently maps to malloc on all platforms), we can allocate a static buffer for the server overlord object which can simply be cleared and re-used for each http request, and enlarged with `realloc` when necessary. the entire region could be `mlock`ed for better performance, and it would no longer be necessary to track and free memory, as the entire buffer would simply be discarded after use (similar to PHP's original memory management strategy). this would remove possibly the largest source of latency in the codebase, as `parsav` is regrettably quite heavy on malloc, performing numerous allocations for each page rendered. **update:** this is now in progress, and much of the UI code has been converted; the database code will also need to be converted, however, and this will be too time-consuming to be worth tackling any time soon. new functions should be written to use the memory pooling strategy, however. Index: parsav.t ================================================================== --- parsav.t +++ parsav.t @@ -492,10 +492,11 @@ 'render:media-gallery'; 'render:docpage'; 'render:conf:profile'; + 'render:conf:circles'; 'render:conf:sec'; 'render:conf:users'; 'render:conf:avi'; 'render:conf'; 'route'; ADDED render/conf/circles.t Index: render/conf/circles.t ================================================================== --- render/conf/circles.t +++ render/conf/circles.t @@ -0,0 +1,53 @@ +-- vim: ft=terra +local pstr = lib.mem.ptr(int8) +local pref = lib.mem.ref(int8) + + +local terra +render_conf_circles(co: &lib.srv.convo, path: lib.mem.ptr(pref)): pstr + if path.ct == 2 then + var circs = co.srv:circle_search(&co.srv.pool, co.who.id, 0) + var ui: data.view.conf_circles + if circs.ct == 0 then + ui.newattr = ' open'; + else + ui.newattr = ''; + var circlst = co:stra(86) + for i = 0, circs.ct do + circlst:lpush '
  • ' + :ppush(circs(i).name) + :lpush '
  • ' + end + ui.circles = circlst:finalize() + end + + return ui:poolstr(&co.srv.pool) + elseif path.ct == 3 then + var cid, cid_ok = lib.math.shorthand.parse(path(2).ptr, path(2).ct) + if not cid_ok then return pstr.null() end + var circ = co.srv:circle_search(&co.srv.pool, co.who.id, cid) + if not circ then return pstr.null() end + var actrs = co.srv:circle_members_fetch_cid(&co.srv.pool, cid) + + var acta = co:stra(86) + if actrs:ref() then + for i=0, actrs.ct do + var a = co:uid2actor(actrs(i)) + if a ~= nil then + acta:lpush '
  • ' + lib.render.nym(a, 0, &acta, false) + acta:lpush '
  • ' + end + end + end + + var ui = data.view.conf_circle_view { + circle = circ().name; + actors = acta:finalize(); + } + + return ui:poolstr(&co.srv.pool) + else return pstr.null() end +end + +return render_conf_circles Index: render/conf/profile.t ================================================================== --- render/conf/profile.t +++ render/conf/profile.t @@ -12,10 +12,12 @@ var c = data.view.conf_profile { handle = cs(co.who.handle); nym = cs(lib.coalesce(co.who.nym,'')); bio = cs(lib.coalesce(co.who.bio,'')); hue = lib.math.decstr(co.ui_hue, &hue[20]); + acl_follow = co:usercfg_str(co.who.id, 'acl-follow'); + acl_follow_req = co:usercfg_str(co.who.id, 'acl-follow-req'); } return c:poolstr(&co.srv.pool) end return render_conf_profile Index: render/conf/users.t ================================================================== --- render/conf/users.t +++ render/conf/users.t @@ -370,11 +370,11 @@ users = co.srv:actor_enum_local() else users = co.srv:actor_enum() end ulst:lpush('') - 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 Index: render/nav.t ================================================================== --- render/nav.t +++ render/nav.t @@ -1,18 +1,17 @@ -- vim: ft=terra local terra render_nav(co: &lib.srv.convo) var t = co:stra(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 profile media configure docs log out notices') + t:lpush(' log out configure notices (x)') else - t:lpush(' docs log in') + t:lpush(' docs log in') end return t:finalize() end return render_nav Index: render/profile.t ================================================================== --- render/profile.t +++ render/profile.t @@ -60,10 +60,18 @@ desc = "this user's posts will be ritually mutilated in a humorous fashion as appropriate to the script in which they are written; e.g. the removal of vowels in roman text and deletion of kana in japanese text"; }, stop = { text = 're-emvowel'; desc = "this user's posts will once again appear normally"; }}; + + { id = 'attenuate', start = { + text = 'attenuate'; + desc = "this user will no longer be able to retweet things into your timeline"; + }, stop = { + text = 'amplify'; + desc = "this user's retweets will be allowed to reach your timeline again"; + }}; { id = 'block', start = { text = 'block'; desc = "this user will not be able to interact with you in any fashion and they will be forced to unfollow you"; }, stop = { Index: render/timeline.t ================================================================== --- render/timeline.t +++ render/timeline.t @@ -36,43 +36,61 @@ var fetchmode = lib.store.range { mode = 1; -- T->I from_time = stoptime; to_idx = 64; } + var circ: lib.mem.ptr(lib.store.circle) = nil if mode == modes.follow or mode == modes.mutual then posts = co.srv:timeline_actor_fetch_uid(co.who.id,fetchmode) elseif mode == [modes['local']] then posts = co.srv:timeline_instance_fetch(fetchmode) elseif mode == modes.fedi then - elseif mode == modes.circle then + elseif mode == modes.circle and spec:ref() then + var cid, ok = lib.math.shorthand.parse(spec.ptr,spec.ct) + if ok then + circ = co.srv:circle_search(&co.srv.pool,co.who.id,cid) + if circ.ct == 1 then + posts = co.srv:timeline_circle_fetch(cid,fetchmode) + end + end end var acc = co:stra(1024) - var modelabels = arrayof(pstr, 'followed', 'mutuals', 'local instance', 'fediverse', 'circle') + var modelabels = arrayof(pstr, 'followed', 'mutuals', 'local instance', 'fediverse', 'circle') + var keybinds = arrayof(pstr, 'f', 'u', 'l', 'v', 'r') var modelinks = arrayof(pstr, [modes.members]) - acc:lpush('
        showing ') + acc:lpush('
        showing ') for i=0, [modelabels.type.N] do if co.aid ~= 0 or not requires_login(i) then if i > 0 then acc:lpush(' ยท ') end - if i == mode and not (mode == modes.circle and spec:ref()) then + if i == mode and not circ then acc:lpush(''):ppush(modelabels[i]):lpush('') else - acc:lpush(''):ppush(modelabels[i]):lpush('') + acc:lpush('') + if i == mode and circ:ref() then + acc:lpush'':ppush(modelabels[i]):lpush' (':ppush(circ().name):lpush(')') + else + acc:ppush(modelabels[i]) + end + acc:lpush('') end end end - acc:lpush('
        ') + acc:lpush('
        ') var newest: lib.store.timepoint = 0 if mode == modes.circle and not spec then var circles = co.srv:circle_search(&co.srv.pool, co.who.id, 0) acc:lpush '' for i:intptr = 0, circles.ct do - acc:lpush '
      • ' + if i <= 10 then + acc:lpush '" accesskey="':ipush((i+1) % 10) + end + acc:lpush '">' :ppush(circles(i).name) - :lpush '
      • ' + :lpush '' end -- TODO list circles acc:lpush '
        ' else acc:lpush('
        ') @@ -82,10 +100,17 @@ if not author.relationship.recip.follow() then goto skip end end if author.relationship.rel.mute() or author.relationship.rel.avoid() or author.relationship.recip.exclude() then goto skip end + if posts(i).ptr.rtdby ~= 0 then + var rter = co:uid2actor(posts(i).ptr.rtdby) + if rter.relationship.rel.mute() + or rter.relationship.rel.attenuate() + or rter.relationship.rel.avoid() + or rter.relationship.recip.exclude() then goto skip end + end lib.render.tweet(co, posts(i).ptr, &acc) var t = lib.math.biggest(lib.math.biggest(posts(i).ptr.posted, posts(i).ptr.discovered),posts(i).ptr.edited) if t > newest then newest = t end ::skip:: posts(i):free() end Index: render/tweet-page.t ================================================================== --- render/tweet-page.t +++ render/tweet-page.t @@ -40,17 +40,20 @@ if not co.srv:post_liked_uid(co.who.id, p.id) then pg:lpush('') else pg:lpush('') end pg:lpush('') + if co.who.rights.powers.crier() then + pg:lpush('') + end if p.author == co.who.id then if co.who.rights.powers.edit() then pg:lpush('edit') end pg:lpush('delete') elseif co.who.rights.powers.snitch() then - pg:lpush('report') + pg:lpush('report') end -- TODO list user's chosen reaction emoji pg:lpush('') end Index: route.t ================================================================== --- route.t +++ route.t @@ -256,11 +256,27 @@ defer post:free() -- NOP on null if path.ct == 3 then var lnk: lib.str.acc lnk:compose('/post/', path(1)) var lnkp = lnk:finalize() defer lnkp:free() - if post:ref() and post(0).author ~= co.who.id then + if post:ref() and path(2):cmp(lib.str.lit 'snitch') then + if meth_get(meth) then + var ui = data.view.report { + badtweet = lib.render.tweet(co, post.ptr, nil); + clnk = lnkp; + } + + co:stdpage([lib.srv.convo.page] { + title = 'post :: report'; + class = 'report'; + body = ui:poolstr(&co.srv.pool); + cache = false; + }) + else + end + return + elseif post:ref() and post(0).author ~= co.who.id then co:complain(403, 'forbidden', 'you cannot alter other people\'s posts') return elseif post:ref() and path(2):cmp(lib.str.lit 'edit') then if not co:assertpow('edit') then return end if meth_get(meth) then @@ -291,24 +307,22 @@ title = 'cancel retweet'; query = 'are you sure you want to undo this retweet?'; cancel = '/'; } end - var fr = co.srv.pool:frame() var body = conf:poolstr(&co.srv.pool) --defer body:free() co:stdpage([lib.srv.convo.page] { title = 'post :: delete'; class = 'query'; body = body; cache = false; }) - co.srv.pool:reset(fr) return elseif meth == method.post then var act = co:ppostv('act') - if act:cmp( 'confirm') then + if act:cmp('confirm') then if post:ref() then - post(0).source:post_destroy(post(0).id) + post().source:post_destroy(post().id) elseif rt.kind ~= 0 then co.srv:post_act_cancel(pid) end co:reroute('/') -- TODO maybe return to parent or conversation if possible return @@ -482,21 +496,22 @@ elseif not co.who.rights.powers.account() and ( path(1):cmp('profile') or path(1):cmp('sec') or path(1):cmp('avi') or - path(1):cmp('ui') + path(1):cmp('ui') or + path(1):cmp('circles') ) 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 + if path(1):cmp('profile') then lib.dbg('updating profile') co.who.bio = co:postv('bio')._0 co.who.nym = co:postv('nym')._0 if co.who.bio ~= nil and @co.who.bio == 0 then co.who.bio = nil end if co.who.nym ~= nil and @co.who.nym == 0 then co.who.nym = nil end @@ -521,10 +536,19 @@ end if resethue then co.srv:actor_conf_int_reset(co.who.id, 'ui-accent') co.ui_hue = co.srv.cfg.ui_hue end + + var aclfollow = co:ppostv('acl-follow') + var aclfollowreq = co:ppostv('acl-follow-req') + if aclfollow:ref() and aclfollow.ct > 0 then + co.srv:actor_conf_str_set(co.who.id, 'acl-follow', aclfollow) + end + if aclfollowreq:ref() and aclfollowreq.ct > 0 then + co.srv:actor_conf_str_set(co.who.id, 'acl-follow-req', aclfollowreq) + end msg = 'profile changes saved' --user_refresh = true -- not really necessary here, actually elseif path(1):cmp('sec') then @@ -534,10 +558,26 @@ if act:ref() and act:cmp('clear') then co.who.avatarid = 0 co.who.source:actor_save(co.who) msg = 'avatar reset to default' else goto badop end + elseif path(1):cmp('circles') then + if meth == method.post then + var act = co:ppostv('act') + if path.ct == 2 and act:cmp('create') then + var newcirc = co:ppostv('name') + if newcirc.ct > 0 then + co.srv:circle_create(co.who.id, newcirc) + end + elseif path.ct == 3 and act:cmp('del') then + var id, ok = lib.math.shorthand.parse(path(2).ptr,path(2).ct) + if not ok then goto e404 end + co.srv:circle_destroy(co.who.id, id) + co:reroute('/conf/circles') + return + else goto badop end + end elseif path(1):cmp('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) @@ -676,10 +716,11 @@ lib.render.conf(co,path,msg) do return end ::nopriv:: do co:complain(403,'insufficient privileges','you do not have the necessary powers to perform this action') return end ::badop:: do co:complain(400,'bad request','the operation you have requested is not meaningful in this context') return end + ::e404:: do co:complain(404,'not found','the resource you have requested is not known to this server') return end end terra http.user_notices(co: &lib.srv.convo, meth: method.t) if meth == method.post then var act = co:ppostv('act') @@ -850,24 +891,24 @@ co:complain(404, 'what the hell', 'how did you do that') elseif uri.ct == 1 then -- root if (co.srv.cfg.pol_sec == lib.srv.secmode.private or co.srv.cfg.pol_sec == lib.srv.secmode.lockdown) and co.aid == 0 then http.login_form(co, meth) - else http.timeline(co, hpath {ptr=nil}) end + else http.timeline(co, hpath {ptr=nil,ct=0}) end elseif uri.ptr[1] == @'@' then http.actor_profile_xid(co, uri, meth) elseif uri.ptr[1] == @'s' and uri.ptr[2] == @'/' and uri.ct > 3 then if not meth_get(meth) then goto wrongmeth end if not http.static_content(co, uri.ptr + 3, uri.ct - 3) then goto notfound end elseif lib.str.ncmp('/avi/', uri.ptr, 5) == 0 then http.local_avatar(co, [lib.mem.ptr(int8)] {ptr = uri.ptr + 5, ct = uri.ct - 5}) elseif lib.str.ncmp('/file/', uri.ptr, 6) == 0 then http.file_serve_raw(co, [lib.mem.ptr(int8)] {ptr = uri.ptr + 6, ct = uri.ct - 6}) - elseif uri:cmp( '/notices') then + elseif uri:cmp('/notices') then if co.aid == 0 then co:reroute('/login') return end http.user_notices(co,meth) - elseif uri:cmp( '/compose') then + elseif uri:cmp('/compose') then if co.aid == 0 then co:reroute('/login') return end http.post_compose(co,meth) elseif uri:cmp( '/login') then if co.aid == 0 then http.login_form(co, meth) Index: srv.t ================================================================== --- srv.t +++ srv.t @@ -17,10 +17,13 @@ ui_cue_founder: pstring ui_hue: uint16 nranks: uint16 maxinvites: uint16 master: uint64 + + usrdef_pol_follow: pstring + usrdef_pol_follow_req: pstring } local struct srv { sources: lib.mem.ptr(lib.store.source) webmgr: lib.net.mg_mgr webcon: &lib.net.mg_connection @@ -32,10 +35,12 @@ terra cfgcache:free() -- :/ self.secret:free() self.instance:free() self.ui_cue_staff:free() self.ui_cue_founder:free() + self.usrdef_pol_follow:free() + self.usrdef_pol_follow_req:free() end terra srv:post_enum_author_uid(uid: uint64, r: lib.store.range): lib.mem.vec(lib.mem.ptr(lib.store.post)) var all: lib.mem.vec(lib.mem.ptr(lib.store.post)) all:init(64) for i=0,self.sources.ct do var src = self.sources.ptr + i @@ -68,10 +73,11 @@ end end deftlfetch('instance_fetch') deftlfetch('actor_fetch_uid', uint64) +deftlfetch('circle_fetch', uint64) srv.metamethods.__methodmissing = macro(function(meth, self, ...) local primary, ptr, stat, simple, oid = 0,1,2,3,4 local tk, rt = primary local expr = {...} @@ -165,10 +171,38 @@ title: pstring body: pstring class: pstring cache: bool } + +local usrdefs = { + str = { + ['acl-follow' ] = {cfgfld = 'usrdef_pol_follow', fallback = 'local'}; + ['acl-follow-req'] = {cfgfld = 'usrdef_pol_follow_req', fallback = 'all'}; + }; +} + +terra convo:usercfg_str(uid: uint64, setting: pstring): pstring + var set = self.srv:actor_conf_str_get(&self.srv.pool, uid, setting) + if not set then + [(function() + local q = quote return pstring.null() end + for key, dfl in pairs(usrdefs.str) do + local rv + if dfl.cfgfld then + rv = quote + var cf = self.srv.cfg.[dfl.cfgfld] + in terralib.select(not cf, pstring([dfl.fallback]), cf) end + elseif dfl.lit then rv = dfl.lit end + q = quote + if setting:cmp([key]) then return [rv] else [q] end + end + end + return q + end)()] + else return set end +end -- this is unfortunately necessary to work around a terra bug -- it can't seem to handle forward-declarations of structs in C local getpeer @@ -1080,13 +1114,16 @@ end end self.ui_cue_staff = self.overlord:conf_get('ui-profile-cue-staff') self.ui_cue_founder = self.overlord:conf_get('ui-profile-cue-master') + + self.usrdef_pol_follow = self.overlord:conf_get('user-default-acl-follow') + self.usrdef_pol_follow_req = self.overlord:conf_get('user-default-acl-follow-req') end return { overlord = srv; convo = convo; route = route; secmode = secmode; } ADDED static/gear.svg Index: static/gear.svg ================================================================== --- static/gear.svg +++ static/gear.svg @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + ADDED static/logo.svg Index: static/logo.svg ================================================================== --- static/logo.svg +++ static/logo.svg @@ -0,0 +1,607 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Index: static/style.scss ================================================================== --- static/style.scss +++ static/style.scss @@ -35,10 +35,13 @@ color: white; text-shadow: 0 0 15px tone(20%); text-decoration-color: tone(10%,-0.1); outline: none; } + u { + text-decoration-color: tone(10%,-0.3); + } } a[href^="//"], a[href^="http://"], a[href^="https://"] { // external link &:hover::after { @@ -72,10 +75,11 @@ @extend %sans; font-size: 14pt; box-sizing: border-box; padding: 0.1in 0.2in; border: 1px solid black; + border-bottom: 2px solid black; color: otone(25%); text-shadow: 1px 1px black; text-decoration: none; text-align: center; cursor: default; @@ -82,50 +86,52 @@ user-select: none; -webkit-user-drag: none; -webkit-app-region: no-drag; --icon: url(/s/heart.webp); background-image: linear-gradient(to bottom, - otone(-47%), - otone(-50%) 15%, - otone(-50%) 75%, - otone(-53%) + otone(-41%), + otone(-43%) 15%, + otone(-46%) 75%, + otone(-50%) ); &:hover, &:focus { @extend %glow; outline: none; color: tone(-55%); text-shadow: none; background: linear-gradient(to bottom, otone(-27%), otone(-30%) 15%, - otone(-30%) 75%, + otone(-32%) 75%, otone(-35%) ); } &:active { color: black; padding-bottom: calc(0.1in - 2px); padding-top: calc(0.1in + 2px); + border: 1px solid black; + border-top: 2px solid black; background: linear-gradient(to top, otone(-25%), otone(-30%) 15%, - otone(-30%) 75%, + otone(-32%) 75%, otone(-35%) ); } } button { @extend .button; - &:first-of-type { + form > &:first-of-type, menu > &:first-of-type { @extend .button; color: white; box-shadow: inset 0 1px otone(-25%), inset 0 -1px otone(-50%); background: linear-gradient(to bottom, otone(-35%), otone(-40%) 15%, - otone(-40%) 75%, + otone(-43%) 75%, otone(-45%) ); &:hover, &:focus { box-shadow: inset 0 1px otone(-15%), inset 0 -1px otone(-40%); @@ -134,11 +140,11 @@ box-shadow: inset 0 1px otone(-50%), inset 0 -1px otone(-25%); background: linear-gradient(to top, otone(-30%), otone(-35%) 15%, - otone(-35%) 75%, + otone(-38%) 75%, otone(-40%) ); } } //&:hover { font-weight: bold; } @@ -211,15 +217,26 @@ all: unset; display: flex; justify-content: flex-end; align-items: center; grid-column: 2/3; grid-row: 1/2; - .ident { + hr { + width: 1px; + height: 1.5em; + border: none; + border-left: 1px solid tone(-40%); + margin-left: 0.5em; + } + a[href].ident { color: tone(-20%); margin-left: 0.2em; - border-left: 1px solid tone(-40%); padding-left: 0.5em; + &::before { + content: '@'; + display: inline-block; // remove underline - i don't want to know why this works + opacity: 0.7; + } } > a[href] { display: block; padding: 0.25in 0.10in; //padding: calc((25% - 1em)/2) 0.15in; @@ -227,20 +244,21 @@ text-shadow: 1px 1px 1px black; &:hover{ transform: scale(1.2); } } - > a[href].bell { - content: url(/s/bell.svg); + > a[href].bell, a[href].gear { height: 2em; - padding: 0.125in 0.10in; + padding: 0.125in 0.05in; filter: drop-shadow(1px 1px 3px tone(-5%)); &:hover { filter: drop-shadow(1px 1px 3px tone(-5%)) drop-shadow(0 0 10px tone(-5%)); } } + > a[href].bell { content: url(/s/bell.svg); } + > a[href].gear { content: url(/s/gear.svg); } } } } main { @@ -287,13 +305,14 @@ &.on { background-color: tone(-30%, -0.7); box-shadow: 0 0 10px tone(-30%); border-color: tone(-20%); } - > button, > p { display: block; } + > button, > p, > a[href] { display: block; } > p { text-align: center; font-size: 80%; margin: 0; margin-top: 0.1in; } - > button { + > button, > a[href] { + width: max-content; margin: auto; } &:last-child:nth-child(2n-1) { grid-column: 1/3; } @@ -481,15 +500,16 @@ > textarea { 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[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 } } + +input[name="acl"] { grid-column: 2/3; grid-row: 2/3; } a.help[href] { display: block; text-align: center; padding: 0.09in 0.2in; @@ -696,10 +716,13 @@ text-align: left; } > a[href] + a[href] { border-top: none; } + .button, .button:active { + border: 1px solid black; + } hr { border: none; } } @@ -889,11 +912,11 @@ @extend %box; label { text-shadow: 1px 1px black; } padding: 0.1in; } -ul.user-list { +ul.directory { list-style-type: none; margin: 0.5em 0; padding: 0; box-shadow: 0 0 10px -3px black inset; border: 1px solid tone(-50%); @@ -923,14 +946,12 @@ margin: 0.2em 0.1em; cursor: default; } } -.button, a[href] { - .neg { --co: 30 } - .pos { --co: -30 } -} +.neg { --co: 30 !important } +.pos { --co: -30 !important } .pick-list { display: flex; flex-flow: row wrap; padding: 0.1in; @@ -1212,5 +1233,37 @@ overflow-y: scroll; text-align: justify; } } } + +div.kind-picker { + text-align: right; + font-style: italic; + padding: 0.2em; +} + +body.timeline { + menu.circles { + @extend %box; + width: 3in; + margin-right: 0; + margin-left: auto; + padding: 0.1in; + a[href] { + transition: 0.4s; + text-align: center; + display: block; + padding: 0.4em; + background: linear-gradient(to right, tone(-30%, -0.6), transparent) no-repeat; + background-position: -3in 0; + text-decoration: none; + & + a[href] { + border-bottom: 1px solid tone(-40%); + border-image: linear-gradient(to right, transparent, tone(-45%), transparent) 1 0 0 / 1px; + } + &:hover { + background-position: 0 0; + } + } + } +} Index: store.t ================================================================== --- store.t +++ store.t @@ -13,10 +13,11 @@ '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 + 'attenuate', -- user's retweets will not be shown 'avoid', -- posts will be kept out of the timeline but will show on users' posts and in conversations 'exclude', -- own posts will not be visible to this user }; credset = lib.set { 'pw', 'otp', 'challenge', 'trust' @@ -256,22 +257,29 @@ folder: str mime: str url: str } -m.user_conf_funcs = function(be,n,ty,rty,rty2) +m.user_conf_funcs = function(be,n,mem,ty,rty,rty2) rty = rty or ty local gt - if not rty2 -- what the fuck? - then gt = {&m.source, uint64, rawstring} -> rty; - else gt = {&m.source, uint64, rawstring} -> {rty, rty2}; + if not mem then + if not rty2 -- what the fuck? + then gt = {&m.source, uint64, lib.str.t} -> rty; + else gt = {&m.source, uint64, lib.str.t} -> {rty, rty2}; + end + else + if not rty2 -- what the fuck? + then gt = {&m.source, &lib.mem.pool, uint64, lib.str.t} -> rty; + else gt = {&m.source, &lib.mem.pool, uint64, lib.str.t} -> {rty, rty2}; + end end for k, t in pairs { - enum = {&m.source, uint64, rawstring} -> lib.mem.ptr(rty); + enum = {&m.source, &lib.mem.pool, uint64, lib.str.t} -> lib.mem.ptr(rty); get = gt; - set = {&m.source, uint64, rawstring, ty} -> {}; - reset = {&m.source, uint64, rawstring} -> {}; + set = {&m.source, uint64, lib.str.t, ty} -> {}; + reset = {&m.source, uint64, lib.str.t} -> {}; } do be.entries[#be.entries+1] = { field = 'actor_conf_'..n..'_'..k, type = t } end @@ -347,10 +355,11 @@ privs: m.privset blacklist: bool } -- backends only handle content on the local server +local pstring = lib.str.t struct m.backend { id: rawstring open: &m.source -> &opaque 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 @@ -363,12 +372,12 @@ -- backends by the server; that is pathological behavior that will -- not have the desired effect server_setup_self: {&m.source, rawstring, lib.mem.ptr(uint8)} -> {} - conf_get: {&m.source, rawstring} -> lib.mem.ptr(int8) - conf_set: {&m.source, rawstring, rawstring} -> {} + conf_get: {&m.source, lib.str.t} -> lib.mem.ptr(int8) + conf_set: {&m.source, lib.str.t, lib.str.t} -> {} 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} -> {} @@ -473,16 +482,17 @@ post_liked_uid: {&m.source, uint64, uint64} -> bool post_reacted_uid: {&m.source, uint64, uint64} -> bool post_act_fetch_notice: {&m.source, uint64} -> m.notice circle_search: {&m.source, &lib.mem.pool, uint64, uint64} -> lib.mem.ptr(m.circle) - circle_create: {&m.source, uint64, pstring} -> {} + circle_create: {&m.source, uint64, lib.str.t} -> uint64 circle_destroy: {&m.source, uint64, uint64} -> {} - circle_members_fetch_cid: {&m.source, &lib.mem.pool, uint64, uint64} -> lib.mem.ptr(uint64) + circle_members_fetch_cid: {&m.source, &lib.mem.pool, uint64} -> lib.mem.ptr(uint64) circle_members_fetch_name: {&m.source, &lib.mem.pool, uint64, pstring} -> lib.mem.ptr(uint64) circle_members_add_uid: {&m.source, uint64, uint64} -> {} circle_members_del_uid: {&m.source, uint64, uint64} -> {} + circle_memberships_uid: {&m.source, &lib.mem.pool, uint64, uint64} -> lib.mem.ptr(m.circle) thread_latest_arrival_calc: {&m.source, uint64} -> m.timepoint artifact_instantiate: {&m.source, lib.mem.ptr(uint8), lib.mem.ptr(int8)} -> uint64 -- instantiate an artifact in the database, either installing a new @@ -541,14 +551,15 @@ 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.lstptr(m.post) timeline_instance_fetch: {&m.source, m.range} -> lib.mem.lstptr(m.post) + timeline_circle_fetch: {&m.source, uint64, 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) +m.user_conf_funcs(m.backend, 'str', true, lib.str.t, lib.str.t) +m.user_conf_funcs(m.backend, 'int', false, intptr, intptr, bool) struct m.source { backend: &m.backend id: lib.mem.ptr(int8) handle: &opaque Index: tpl.t ================================================================== --- tpl.t +++ tpl.t @@ -36,14 +36,14 @@ str = str:gsub(' ?', file) end) - for start, mode, key, stop in string.gmatch(str,'()'..tplchar..'([:!]?)(%w+)()') do + for start, mode, key, stop in string.gmatch(str,'()'..tplchar..'([:!]?)([-a-zA-Z0-9_]+)()') do if string.sub(str,start-1,start-1) ~= '\\' then segs[#segs+1] = string.sub(str,last,start-1) - fields[#segs] = { key = key, mode = (mode ~= '' and mode or nil) } + fields[#segs] = { key = key:gsub('-','_'), mode = (mode ~= '' and mode or nil) } last = stop end end segs[#segs+1] = string.sub(str,last) ADDED view/conf-circles.tpl Index: view/conf-circles.tpl ================================================================== --- view/conf-circles.tpl +++ view/conf-circles.tpl @@ -0,0 +1,14 @@ +
          + @circles +
        + + + create new circle +
        +
        + + +
        + +
        + Index: view/conf-profile.tpl ================================================================== --- view/conf-profile.tpl +++ view/conf-profile.tpl @@ -1,10 +1,22 @@
        @!handle
        +
        +
        + + +
        +
        + + +
        +
        Index: view/docskel.tpl ================================================================== --- view/docskel.tpl +++ view/docskel.tpl @@ -7,14 +7,14 @@

        @title

        @body
        Index: view/load.lua ================================================================== --- view/load.lua +++ view/load.lua @@ -8,10 +8,11 @@ 'confirm'; 'tweet'; 'profile'; 'compose'; 'notice'; + 'report'; 'media-gallery'; 'media-upload'; 'media-image'; 'media-text'; @@ -19,10 +20,12 @@ 'login-username'; 'login-challenge'; 'conf'; 'conf-profile'; + 'conf-circles'; + 'conf-circle-view'; 'conf-sec'; 'conf-sec-credmg'; 'conf-sec-pwnew'; 'conf-sec-keynew'; 'conf-user-ctl'; Index: view/profile.tpl ================================================================== --- view/profile.tpl +++ view/profile.tpl @@ -40,11 +40,11 @@
        sanctions @sanctions
        - + report

        if this user is violating instance rules, you can report this behavior to moderation staff and ask them to take action. please do not report users simply because you dislike them; this is what the above options are for.

        ADDED view/report.tpl Index: view/report.tpl ================================================================== --- view/report.tpl +++ view/report.tpl @@ -0,0 +1,12 @@ +
        +

        if you feel that this post has violated the rules of this instance, you can report it to the local moderation team and request that they take action. this may include suppressing the offending post, suspending the user, or passing complaints onwards to the offending user's instance, if remote. please explain how you believe the post violates instance rules and what, if any, action you believe is appropriate.

        + @badtweet +
        + + +
        + + + cancel + +