Index: acl.t ================================================================== --- acl.t +++ acl.t @@ -12,18 +12,26 @@ terra m.eval(expr: lib.str.t, agent: m.agent) end + +terra lib.store.post:comp() + -- TODO extract mentions from body, circles from acl + self.mentions = [lib.mem.ptr(uint64)].null() + self.circles = [lib.mem.ptr(uint64)].null() + self.convoheaduri = nil +end + terra lib.store.post:save(ctupdate: bool) --- this post handles the messy details of registering a post's --- circles and actors, and increments the edit-count if ctupdate --- is true, which is should be in almost all cases. + -- this post handles the messy details of registering a post's + -- circles and actors, and increments the edit-count if ctupdate + -- is true, which is should be in almost all cases. if ctupdate then self.chgcount = self.chgcount + 1 self.edited = lib.osclock.time(nil) end - -- TODO extract mentions from body, circles from acl + self:comp() self.source:post_save(self) end return m Index: backend/pgsql.t ================================================================== --- backend/pgsql.t +++ backend/pgsql.t @@ -256,15 +256,16 @@ where id = $1::bigint ]]; }; auth_create_pw = { - params = {uint64, lib.mem.ptr(uint8)}, cmd = true, sql = [[ - insert into parsav_auth (uid, name, kind, cred) values ( + params = {uint64, binblob, pstring}, cmd = true, sql = [[ + insert into parsav_auth (uid, name, kind, cred, comment) values ( $1::bigint, (select handle from parsav_actors where id = $1::bigint), - 'pw-sha256', $2::bytea + 'pw-sha256', $2::bytea, + $3::text ) ]] }; auth_purge_type = { @@ -272,10 +273,22 @@ delete from parsav_auth where ((uid = 0 and name = $1::text) or uid = $2::bigint) and kind like $3::text ]] }; + + auth_enum_uid = { + params = {uint64}, sql = [[ + select aid, kind, comment, netmask, blacklist from parsav_auth where uid = $1::bigint + ]]; + }; + + auth_enum_handle = { + params = {rawstring}, sql = [[ + select aid, kind, comment, netmask, blacklist from parsav_auth where name = $1::text + ]]; + }; post_save = { params = { uint64, uint32, int64; rawstring, rawstring, rawstring; @@ -289,19 +302,23 @@ where id = $1::bigint ]] }; post_create = { - params = {uint64, rawstring, rawstring, rawstring}, sql = [[ + params = { + uint64, rawstring, rawstring, rawstring, + uint64, uint64, rawstring + }, sql = [[ insert into parsav_posts ( author, subject, acl, body, - posted, discovered, - circles, mentions + parent, posted, discovered, + circles, mentions, convoheaduri ) values ( $1::bigint, case when $2::text = '' then null else $2::text end, $3::text, $4::text, - now(), now(), array[]::bigint[], array[]::bigint[] + $5::bigint, to_timestamp($6::bigint), now(), + array[]::bigint[], array[]::bigint[], $7::text ) returning id ]]; -- TODO array handling }; post_destroy_prepare = { @@ -323,25 +340,69 @@ select a.origin is null, p.id, p.author, p.subject, p.acl, p.body, extract(epoch from p.posted )::bigint, extract(epoch from p.discovered)::bigint, extract(epoch from p.edited )::bigint, - p.parent, p.convoheaduri, p.chgcount + p.parent, p.convoheaduri, p.chgcount, + coalesce(c.value, -1)::smallint + from parsav_posts as p - inner join parsav_actors as a on p.author = a.id + inner join parsav_actors as a on p.author = a.id + left join parsav_actor_conf_ints as c on c.uid = a.id and c.key = 'ui-accent' where p.id = $1::bigint ]]; }; + + post_enum_parent = { + params = {uint64}, sql = [[ + select a.origin is null, + p.id, p.author, p.subject, p.acl, p.body, + extract(epoch from p.posted )::bigint, + extract(epoch from p.discovered)::bigint, + extract(epoch from p.edited )::bigint, + p.parent, p.convoheaduri, p.chgcount, + coalesce(c.value, -1)::smallint + + from parsav_posts as p + inner join parsav_actors as a on a.id = p.author + left join parsav_actor_conf_ints as c on c.uid = a.id and c.key = 'ui-accent' + where p.parent = $1::bigint + order by p.posted, p.discovered asc + ]] + }; + + thread_latest_arrival_calc = { + params = {uint64}, sql = [[ + with recursive posts(id) as ( + select id from parsav_posts where parent = $1::bigint + union + select p.id from parsav_posts as p + inner join posts on posts.id = p.parent + ), + + maxes as ( + select unnest(array[max(p.posted), max(p.discovered), max(p.edited)]) as m + from posts + inner join parsav_posts as p + on p.id = posts.id + ) + + select extract(epoch from max(m))::bigint from maxes + ]]; + }; post_enum_author_uid = { params = {uint64,uint64,uint64,uint64, uint64}, sql = [[ select a.origin is null, p.id, p.author, p.subject, p.acl, p.body, extract(epoch from p.posted )::bigint, extract(epoch from p.discovered)::bigint, extract(epoch from p.edited )::bigint, - p.parent, p.convoheaduri, p.chgcount + p.parent, p.convoheaduri, p.chgcount, + coalesce((select value from parsav_actor_conf_ints as c where + c.uid = $1::bigint and c.key = 'ui-accent'),-1)::smallint + from parsav_posts as p inner join parsav_actors as a on p.author = a.id where p.author = $5::bigint and ($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and ($2::bigint = 0 or to_timestamp($2::bigint) < p.posted) @@ -359,13 +420,16 @@ select true, p.id, p.author, p.subject, p.acl, p.body, extract(epoch from p.posted )::bigint, extract(epoch from p.discovered)::bigint, extract(epoch from p.edited )::bigint, - p.parent, null::text, p.chgcount + p.parent, null::text, p.chgcount, + coalesce(c.value, -1)::smallint + from parsav_posts as p - inner join parsav_actors as a on p.author = a.id + inner join parsav_actors as a on p.author = a.id + left join parsav_actor_conf_ints as c on c.uid = a.id and c.key = 'ui-accent' where ($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and ($2::bigint = 0 or to_timestamp($2::bigint) < p.posted) and (a.origin is null) order by (p.posted, p.discovered) desc @@ -432,11 +496,11 @@ 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] + artifacts @> array[$2::bigint] -- prevent duplication ]]; }; post_attach_ctl_del = { params = {uint64, uint64}, cmd=true, sql = [[ update parsav_posts set @@ -443,10 +507,64 @@ artifacts = array_remove(artifacts, $2::bigint) where id = $1::bigint and artifacts @> array[$2::bigint] ]]; }; + + actor_conf_str_get = { + params = {uint64, rawstring}, 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 = [[ + 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 + ]]; + }; + actor_conf_str_enum = { + params = {uint64}, sql = [[ + select value from parsav_actor_conf_strs where uid = $1::bigint + ]]; + }; + actor_conf_str_reset = { + params = {uint64, rawstring}, 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 = [[ + 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 = [[ + 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 + ]]; + }; + actor_conf_int_enum = { + params = {uint64}, sql = [[ + select value from parsav_actor_conf_ints where uid = $1::bigint + ]]; + }; + actor_conf_int_reset = { + params = {uint64, rawstring}, cmd = true, sql = [[ + delete from parsav_actor_conf_ints where + uid = $1::bigint and ($2::text is null or key = $2::text) + ]] + }; } local struct pqr { sz: intptr res: &lib.pq.PGresult @@ -653,10 +771,11 @@ end if r:null(row,11) then p.ptr.chgcount = 0 else p.ptr.chgcount = r:int(uint32,row,11) end + p.ptr.accent = r:int(int16,row,12) p.ptr.localpost = r:bool(row,0) return p end local terra row_to_actor(r: &pqr, row: intptr): lib.mem.ptr(lib.store.actor) @@ -1027,14 +1146,18 @@ post_create = [terra( src: &lib.store.source, post: &lib.store.post ): uint64 - var r = queries.post_create.exec(src,post.author,post.subject,post.acl,post.body) + var r = queries.post_create.exec(src, + post.author,post.subject,post.acl,post.body, + post.parent,post.posted,post.convoheaduri + ) if r.sz == 0 then return 0 end defer r:free() var id = r:int(uint64,0,0) + post.source = src return id end]; post_destroy = [terra( src: &lib.store.source, @@ -1116,23 +1239,46 @@ lib.dbg('powers established') return ac.id end]; - auth_create_pw = [terra( + 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) + if r.sz == 0 then return [lib.mem.ptr(lib.mem.ptr(lib.store.auth))].null() end + var ret = lib.mem.heapa([lib.mem.ptr(lib.store.auth)], r.sz) + for i=0, r.sz do + var kind = r:_string(i, 1) + var comment = r:_string(i, 2) + var a = [ lib.str.encapsulate(lib.store.auth, { + kind = {`kind.ptr, `kind.ct}; + comment = {`comment.ptr, `comment.ct}; + }) ] + a.ptr.aid = r:int(uint64, i, 0) + a.ptr.netmask = r:cidr(i, 3) + a.ptr.blacklist = r:bool(i, 4) + ret.ptr[i] = a + end + return ret + end]; + + auth_attach_pw = [terra( src: &lib.store.source, uid: uint64, reset: bool, - pw: lib.mem.ptr(int8) + pw: pstring, + comment: pstring ): {} var hash: uint8[lib.crypt.algsz.sha256] if lib.md.mbedtls_md(lib.md.mbedtls_md_info_from_type(lib.crypt.alg.sha256.id), [&uint8](pw.ptr), pw.ct, &hash[0]) ~= 0 then lib.bail('cannot hash password') end if reset then queries.auth_purge_type.exec(src, nil, uid, 'pw-%') end - queries.auth_create_pw.exec(src, uid, [lib.mem.ptr(uint8)] {ptr = &hash[0], ct = [hash.type.N]}) + queries.auth_create_pw.exec(src, uid, binblob {ptr = &hash[0], ct = [hash.type.N]}, comment) end]; auth_purge_pw = [terra(src: &lib.store.source, uid: uint64, handle: rawstring): {} queries.auth_purge_type.exec(src, handle, uid, 'pw-%') end]; @@ -1210,10 +1356,37 @@ ): {} queries.post_save.exec(src, post.id, post.chgcount, post.edited, post.subject, post.acl, post.body) end]; + + post_enum_parent = [terra( + src: &lib.store.source, + post: uint64 + ): lib.mem.ptr(lib.mem.ptr(lib.store.post)) + var r = queries.post_enum_parent.exec(src,post) + if r.sz == 0 then + return [lib.mem.ptr(lib.mem.ptr(lib.store.post))].null() + end + defer r:free() + var lst = lib.mem.heapa([lib.mem.ptr(lib.store.post)], r.sz) + + for i=0, r.sz do lst.ptr[i] = row_to_post(&r, i) end + + return lst + end]; + + thread_latest_arrival_calc = [terra( + src: &lib.store.source, + post: uint64 + ): lib.store.timepoint + var r = queries.thread_latest_arrival_calc.exec(src,post) + if r.sz == 0 or r:null(0,0) then return 0 end + var tp: lib.store.timepoint = r:int(int64,0,0) + r:free() + return tp + end]; auth_sigtime_user_fetch = [terra( src: &lib.store.source, uid: uint64 ): lib.store.timepoint @@ -1228,9 +1401,38 @@ src: &lib.store.source, 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 + 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 + else return pstring.null() end + end]; + actor_conf_str_set = [terra(src: &lib.store.source, uid: uint64, key: rawstring, value: rawstring): {} + queries.actor_conf_str_set.exec(src,uid,key,value) end]; + actor_conf_str_reset = [terra(src: &lib.store.source, uid: uint64, key: rawstring): {} + 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) + 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): {} + queries.actor_conf_int_set.exec(src,uid,key,value) end]; + actor_conf_int_reset = [terra(src: &lib.store.source, uid: uint64, key: rawstring): {} + queries.actor_conf_int_reset.exec(src,uid,key) 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 @@ -1,10 +1,12 @@ -- destroy absolutely everything drop table if exists parsav_config cascade; drop table if exists parsav_servers cascade; drop table if exists parsav_actors cascade; +drop table if exists parsav_actor_conf_strs cascade; +drop table if exists parsav_actor_conf_ints cascade; drop table if exists parsav_rights cascade; drop table if exists parsav_posts cascade; drop table if exists parsav_conversations cascade; drop table if exists parsav_rels cascade; drop table if exists parsav_acts cascade; Index: backend/schema/pgsql.sql ================================================================== --- backend/schema/pgsql.sql +++ backend/schema/pgsql.sql @@ -169,9 +169,18 @@ 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 table parsav_actor_conf_strs ( + uid bigint not null references parsav_actors(id) on delete cascade, + key text not null, value text not null, unique (uid,key) +); +create table parsav_actor_conf_ints ( + uid bigint not null references parsav_actors(id) on delete cascade, + key text not null, value bigint not null, unique (uid,key) +); -- 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: config.lua ================================================================== --- config.lua +++ config.lua @@ -52,15 +52,17 @@ -- we should have a build-time option to serve svg so instances -- proxied behind nginx can serve svgz, or possibly just straight-up -- add support for content-encoding headers and pre-compress the -- damn things before compiling {'style.css', 'text/css'}; + {'live.js', 'text/javascript'}; -- rrrrrrrr {'default-avatar.webp', 'image/webp'}; {'padlock.webp', 'image/webp'}; {'warn.webp', 'image/webp'}; {'query.webp', 'image/webp'}; }; + default_ui_accent = tonumber(default('parsav_ui_default_accent',323)); } if os.getenv('parsav_let_me_be_an_idiot') == "i know what i'm doing" then conf.braingeniousmode = true -- SOUND GENERAL QUARTERS end if u.ping '.fslckout' or u.ping '_FOSSIL_' then Index: math.t ================================================================== --- math.t +++ math.t @@ -186,10 +186,28 @@ buf = buf - 1 @buf = 0x30 end return buf end + +terra m.decparse(s: pstring): {intptr, bool} + if not s then return 0, false end + var val:intptr = 0 + var c = s.ptr + while @c ~= 0 do + if @c >= 0x30 and @c <= 0x39 then + val = val * 10 + val = val + (@c - 0x30) + else + return 0, false + end + + c = c + 1 + if s.ct ~= 0 and (c - s.ptr > s.ct) then lib.dbg('reached end') return val, true end + end + return val, true +end terra m.ndigits(n: intptr, base: intptr): intptr var c = base var i = 1 while true do Index: mgtool.t ================================================================== --- mgtool.t +++ mgtool.t @@ -61,10 +61,11 @@ iname: rawstring } idelegate.metamethods.__methodmissing = macro(function(meth, self, ...) local expr = {...} local rt + for _,f in pairs(lib.store.backend.entries) do local fn = f.field or f[1] local ft = f.type or f[2] if fn == meth then rt = ft.type.returntype break end end @@ -115,13 +116,13 @@ elseif tmppw[i] >= 10 then tmppw[i] = tmppw[i] + (0x41 - 10) else tmppw[i] = tmppw[i] + 0x30 end end lib.dbg('assigning temporary password') - dlg:auth_create_pw(uid, reset, pstr { - ptr = [rawstring](tmppw), ct = 32 - }) + dlg:auth_attach_pw(uid, reset, + pstr { ptr = [rawstring](tmppw), ct = 32 }, + lib.str.plit 'temporary password'); end local terra ipc_report(acks: lib.mem.ptr(lib.ipc.ack), rep: rawstring) var decbuf: int8[21] for i=0,acks.ct do @@ -336,10 +337,16 @@ dlg:conf_set('server-secret', &sec[0]) lib.report('server secret reset') elseif lib.str.cmp(cfmode.arglist(0),'refresh') == 0 then cfmode.no_notify = false -- duh else goto cmderr end + elseif cfmode.arglist.ct == 2 and + lib.str.cmp(cfmode.arglist(0),'reset') == 0 or + lib.str.cmp(cfmode.arglist(0),'clear') == 0 or + lib.str.cmp(cfmode.arglist(0),'unset') == 0 then + dlg:conf_reset(cfmode.arglist(1)) + lib.report('parameter cleared') elseif cfmode.arglist.ct == 3 and lib.str.cmp(cfmode.arglist(0),'set') == 0 then dlg:conf_set(cfmode.arglist(1),cfmode.arglist(2)) lib.report('parameter set') else goto cmderr end Index: parsav.md ================================================================== --- parsav.md +++ parsav.md @@ -12,11 +12,11 @@ * runtime * mongoose * json-c * mbedtls * **postgresql backend:** - * postgresql-libs + * postgresql-libs * compile-time * cmark (commonmark implementation), for transformation of the help files, whose source is in commonmark. online documentation transforms these into html and embeds them in the binary; cmark is also used to to produce the troff source which is used to build the offline documentation. disable with `parsav_online_documentation=no parsav_offline_documentation=no` * troff implementation (tested with groff but as far as i know we don't need any groff-specific extensions) to produce PDFs and manpages from the cmark-generated intermediate forms. disable with `parsav_offline_documentation=no` additional preconfigure dependencies are necessary if you are building directly from trunk, rather than from a release tarball that includes certain build artifacts which need to be embedded in the binary: @@ -25,11 +25,11 @@ * cwebp (libwebp package), for transforming inkscape PNGs to webp * sassc, for compiling the SCSS stylesheet into its final CSS all builds require terra, which, unfortunately, requires installing an older version of llvm, v9 at the latest (which i develop parsav under). with any luck, your distro will be clever enough to package terra and its dependencies properly (it's trivial on nix, tho you'll need to tweak the terra expression to select a more recent llvm package); Arch Linux is one of those distros which is not so clever, and whose (AUR) terra package is totally broken. due to these unfortunate circumstances, terra is distributed not just in source form, but also in the the form of LLVM IR. distributions will also be made in the form of tarballed object code and assembly listings for various common platforms, currently including x86-32/64, arm7hf, aarch64, riscv, mips32/64, and ppc64/64le. -i've noticed that terra (at least with llvm9) seems to get a bit cantankerous and trigger llvm to fail with bizarre errors when you try to cross-compile parsav from x86-64 to any other platform, even x86-32. i don't know if this problem exists on other architectures or in what form, but as a workaround, the current cross-compile process consists of generating LLVM IR (ostensible for x86-64, though this is in reality an architecture-independent language), and then compiling that down to an object file with llc. this is an enormous hassle; hopefully the terra people will fix this eventually. +i've noticed that terra (at least with llvm9) seems to get a bit cantankerous and trigger llvm to fail with bizarre errors when you try to cross-compile parsav from x86-64 to any other platform, even x86-32. i don't know if this problem exists on other architectures or in what form, but as a workaround, the current cross-compile process consists of generating LLVM IR (ostensibly for x86-64, though this is in reality an architecture-independent language), and then compiling that down to an object file with llc. this is an enormous hassle; hopefully the terra (or llvm?) people will fix this eventually. also note that, while parsav has a flag to build with ASAN, ASAN has proven unusable for most purposes as it routinely reports false positive buffer-heap-overflows. if you figure out how to defuckulate this, i will be overjoyed. ## building Index: parsav.t ================================================================== --- parsav.t +++ parsav.t @@ -604,11 +604,11 @@ end local holler = print local suffix = config.exe and '' or ('.'..config.outform) local out = 'parsavd' .. suffix -local linkargs = {'-O4'} +local linkargs = {} local target = config.tgttrip and terralib.newtarget { Triple = config.tgttrip; CPU = config.tgtcpu; FloatABIHard = config.tgthf; } or nil Index: render/conf.t ================================================================== --- render/conf.t +++ render/conf.t @@ -3,10 +3,11 @@ local pref = lib.mem.ref(int8) local mappings = { {url = 'profile', title = 'account profile', render = 'profile'}; {url = 'avi', title = 'avatar', render = 'avatar'}; + {url = 'ui', title = 'user interface', render = 'ui'}; {url = 'sec', title = 'security', render = 'sec'}; {url = 'rel', title = 'relationships', render = 'rel'}; {url = 'qnt', title = 'quarantine', render = 'quarantine'}; {url = 'acl', title = 'access control shortcuts', render = 'acl'}; {url = 'rooms', title = 'chatrooms', render = 'rooms'}; Index: render/conf/profile.t ================================================================== --- render/conf/profile.t +++ render/conf/profile.t @@ -6,14 +6,16 @@ return pstr { ptr = s, ct = lib.str.sz(s) } end local terra render_conf_profile(co: &lib.srv.convo, path: lib.mem.ptr(pref)): pstr + var hue: int8[21] 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]); } return c:tostr() end return render_conf_profile Index: render/timeline.t ================================================================== --- render/timeline.t +++ render/timeline.t @@ -25,21 +25,26 @@ elseif mode == modes.fediglobal then elseif mode == modes.circle then end var acc: lib.str.acc acc:init(1024) + acc:lpush('
') + var newest: lib.store.timepoint = 0 for i = 0, posts.sz do 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 posts(i):free() end posts:free() + acc:lpush('
') var doc = [lib.srv.convo.page] { title = lib.str.plit'timeline'; body = acc:finalize(); class = lib.str.plit'timeline'; cache = false; } - co:stdpage(doc) + co:livepage(doc,newest) doc.body:free() end return render_timeline Index: render/tweet-page.t ================================================================== --- render/tweet-page.t +++ render/tweet-page.t @@ -2,22 +2,41 @@ local pstr = lib.mem.ptr(int8) local pref = lib.mem.ref(int8) local terra cs(s: rawstring) return pstr { ptr = s, ct = lib.str.sz(s) } end + +local terra +render_tweet_replies( + co: &lib.srv.convo, + acc: &lib.str.acc, + id: uint64 +): {} + var replies = co.srv:post_enum_parent(id) + if replies.ct == 0 then return end + acc:lpush('
') + for i=0, replies.ct do + var post = replies(i).ptr + lib.render.tweet(co, post, acc) + render_tweet_replies(co, acc, post.id) + end + acc:lpush('
') +end local terra render_tweet_page( co: &lib.srv.convo, path: lib.mem.ptr(pref), p: &lib.store.post ): {} + var livetime = co.srv:thread_latest_arrival_calc(p.id) + var pg: lib.str.acc pg:init(256) lib.render.tweet(co, p, &pg) - pg:lpush('
') if co.aid ~= 0 then + pg:lpush('') var liked = false -- FIXME var rtd = false if not liked then pg:lpush('') else pg:lpush('') @@ -30,21 +49,25 @@ pg:lpush('editdelete') end -- TODO list user's chosen reaction emoji pg:lpush('
') - if co.who.rights.powers.post() then - lib.render.compose(co, nil, &pg) - end + end + pg:lpush('
') + render_tweet_replies(co, &pg, p.id) + pg:lpush('
') + + if co.aid ~= 0 and co.who.rights.powers.post() then + lib.render.compose(co, nil, &pg) end var ppg = pg:finalize() defer ppg:free() - co:stdpage([lib.srv.convo.page] { + co:livepage([lib.srv.convo.page] { title = lib.str.plit 'post'; cache = false; class = lib.str.plit 'post'; body = ppg; - }) + }, livetime) -- TODO display conversation -- perhaps display descendant nodes here, and have a link to the top of the whole tree? end return render_tweet_page Index: render/tweet.t ================================================================== --- render/tweet.t +++ render/tweet.t @@ -34,12 +34,25 @@ when = cs(×tr[0]); avatar = cs(lib.trn(author.origin == 0, avistr.buf, lib.coalesce(author.avatar, '/s/default-avatar.webp'))); acctlink = cs(author.xid); permalink = permalink:finalize(); + attr = '' } + + var attrbuf: int8[32] + if p.accent ~= -1 and p.accent ~= co.ui_hue then + var hdecbuf: int8[21] + var hdec = lib.math.decstr(p.accent, &hdecbuf[20]) + lib.str.cpy(&attrbuf[0], ' style="--hue:') + lib.str.cpy(&attrbuf[14], hdec) + var len = &hdecbuf[20] - hdec + lib.str.cpy(&attrbuf[14] + len, '"') + tpl.attr = &attrbuf[0] + end + defer tpl.permalink:free() if acc ~= nil then tpl:append(acc) return [lib.mem.ptr(int8)]{ptr=nil,ct=0} end var txt = tpl:tostr() return txt end return render_tweet Index: render/user-page.t ================================================================== --- render/user-page.t +++ render/user-page.t @@ -18,23 +18,28 @@ mode = 1; -- T->I from_time = stoptime; to_idx = 64; }) + acc:lpush('
') + var newest: lib.store.timepoint = 0 for i = 0, posts.sz do 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 posts(i):free() end posts:free() + acc:lpush('
') var bdf = acc:finalize() - co:stdpage([lib.srv.convo.page] { + co:livepage([lib.srv.convo.page] { title = tiptr; body = bdf; class = lib.str.plit 'profile'; cache = false; - }) + }, newest) tiptr:free() bdf:free() end return render_userpage Index: route.t ================================================================== --- route.t +++ route.t @@ -4,10 +4,11 @@ local pstring = lib.mem.ptr(int8) local rstring = lib.mem.ref(int8) local hpath = lib.mem.ptr(rstring) local http = {} +terra meth_get(meth: method.t) return (meth == method.get) or (meth == method.head) end terra http.actor_profile_xid(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t) var handle = [lib.mem.ptr(int8)] { ptr = &uri.ptr[2], ct = 0 } for i=2,uri.ct do if uri.ptr[i] == @'/' or uri.ptr[i] == 0 then handle.ct = i - 2 break end end @@ -58,11 +59,11 @@ lib.render.user_page(co, actor.ptr) end terra http.login_form(co: &lib.srv.convo, meth: method.t) - if meth == method.get then + if meth_get(meth) then -- request a username lib.render.login(co, nil, nil, lib.str.plit(nil)) elseif meth == method.post then var usn, usnl = co:postv('user') var am, aml = co:postv('authmethod') @@ -124,11 +125,11 @@ terra http.post_compose(co: &lib.srv.convo, meth: method.t) if not co:assertpow('post') then return end --if co.who.rights.powers.post() == false then --co:complain(403,'insufficient privileges','you lack the post power and cannot perform this action') - if meth == method.get then + if meth_get(meth) then lib.render.compose(co, nil, nil) elseif meth == method.post then var text, textlen = co:postv("post") var acl, acllen = co:postv("acl") var subj, subjlen = co:postv("subject") @@ -140,11 +141,11 @@ var p = lib.store.post { author = co.who.id, acl = acl; body = text, subject = subj; } - var newid = co.srv:post_create(&p) + var newid = p:publish(co.srv) var idbuf: int8[lib.math.shorthand.maxlen] var idlen = lib.math.shorthand.gen(newid, idbuf) var redirto: lib.str.acc redirto:compose('/post/',{idbuf,idlen}) defer redirto:free() co:reroute(redirto.buf) @@ -183,11 +184,11 @@ var lnkp = lnk:finalize() defer lnkp:free() if post(0).author ~= co.who.id then co:complain(403, 'forbidden', 'you cannot alter other people\'s posts') return elseif path(2):cmp(lib.str.lit 'edit') then - if meth == method.get then + if meth_get(meth) then lib.render.compose(co, post.ptr, nil) return elseif meth == method.post then var newbody = co:postv('post')._0 var newacl = co:postv('acl')._0 @@ -198,11 +199,11 @@ post(0):save(true) co:reroute(lnkp.ptr) end return elseif path(2):cmp(lib.str.lit 'del') then - if meth == method.get then + if meth_get(meth) then var conf = data.view.confirm { title = lib.str.plit 'delete post'; query = lib.str.plit 'are you sure you want to delete this post?'; cancel = lnkp } @@ -222,11 +223,24 @@ else goto badop end end else goto badurl end end - if meth == method.post then goto badop end + if meth == method.post then + var replytext = co:ppostv('post') + var acl = co:ppostv('acl') + var subj = co:ppostv('subject') + if not acl then acl = lib.str.plit 'all' end + if not replytext then goto badop end + + var reply = lib.store.post { + author = co.who.id, parent = pid; + subject = subj.ptr, acl = acl.ptr, body = replytext.ptr; + } + + reply:publish(co.srv) + end lib.render.tweet_page(co, path, post.ptr) do return end ::badurl:: do co:complain(404, 'invalid URL', 'this URL does not reference extant content or functionality') return end @@ -242,10 +256,33 @@ 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 co.who.source:actor_save(co.who) + + var act = co:ppostv('act') + var resethue = false + if act:ref() then + resethue = act:cmp(lib.str.plit 'reset-hue') + end + + if not resethue then + var shue = co:ppostv('hue') + var nhue, okhue = lib.math.decparse(shue) + if okhue and nhue ~= co.ui_hue then + if nhue == co.srv.cfg.ui_hue + then resethue = true + else co.srv:actor_conf_int_set(co.who.id, 'ui-accent', nhue) + end + co.ui_hue = nhue + end + end + if resethue then + co.srv:actor_conf_int_reset(co.who.id, 'ui-accent') + 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 @@ -324,66 +361,54 @@ terra r.dispatch_http(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t) lib.dbg('handling URI of form ', {uri.ptr,uri.ct}) co.navbar = lib.render.nav(co) -- some routes are non-hierarchical, and can be resolved with a simple strcmp -- we run through those first before giving up and parsing the URI - if uri.ptr[0] ~= @'/' then + if uri.ptr == nil or uri.ptr[0] ~= @'/' then co:complain(404, 'what the hell', 'how did you do that') - return 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 - -- FIXME display home screen - http.timeline(co, hpath {ptr=nil}) - goto notfound - end - return + else http.timeline(co, hpath {ptr=nil}) end elseif uri.ptr[1] == @'@' then http.actor_profile_xid(co, uri, meth) - return elseif uri.ptr[1] == @'s' and uri.ptr[2] == @'/' and uri.ct > 3 then - if meth ~= method.get then goto wrongmeth end + 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 - return 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}) - return elseif lib.str.ncmp('/compose', uri.ptr, lib.math.biggest(uri.ct,8)) == 0 then if co.aid == 0 then co:reroute('/login') return end http.post_compose(co,meth) - return elseif lib.str.ncmp('/login', uri.ptr, lib.math.biggest(uri.ct,6)) == 0 then if co.aid == 0 then http.login_form(co, meth) else co:reroute('/') end - return elseif lib.str.ncmp('/logout', uri.ptr, lib.math.biggest(uri.ct,7)) == 0 then if co.aid == 0 then goto notfound else co:reroute_cookie('/','auth=; Path=/') end - return else -- hierarchical routes var path = lib.http.hier(uri) defer path:free() if path.ct > 1 and path(0):cmp(lib.str.lit('user')) then http.actor_profile_uid(co, path, meth) elseif path.ct > 1 and path(0):cmp(lib.str.lit('post')) then http.tweet_page(co, path, meth) elseif path(0):cmp(lib.str.lit('tl')) then http.timeline(co, path) elseif path(0):cmp(lib.str.lit('doc')) then - if meth ~= method.get and meth ~= method.head then goto wrongmeth end + if not meth_get(meth) then goto wrongmeth end http.documentation(co, path) elseif path(0):cmp(lib.str.lit('conf')) then if co.aid == 0 then goto unauth end http.configure(co,path,meth) else goto notfound end - return end + do return end ::wrongmeth:: co:complain(405, 'method not allowed', 'that method is not meaningful for this endpoint') do return end ::notfound:: co:complain(404, 'not found', 'no such resource available') do return end ::unauth:: co:complain(401, 'unauthorized', 'this content is not available at your clearance level') do return end end Index: srv.t ================================================================== --- srv.t +++ srv.t @@ -9,10 +9,11 @@ pol_reg: bool credmgd: bool maxupsz: intptr instance: lib.mem.ptr(int8) overlord: &srv + ui_hue: uint16 } local struct srv { sources: lib.mem.ptr(lib.store.source) webmgr: lib.net.mg_mgr webcon: &lib.net.mg_connection @@ -106,10 +107,20 @@ end end in r end end end) + +terra lib.store.post:publish(s: &srv) + self:comp() + self.posted = lib.osclock.time(nil) + self.discovered = self.posted + self.chgcount = 0 + self.edited = 0 + self.id = s:post_create(self) + return self.id +end local struct convo { srv: &srv con: &lib.net.mg_connection msg: &lib.net.mg_http_message @@ -116,17 +127,27 @@ aid: uint64 -- 0 if logged out aid_issue: lib.store.timepoint who: &lib.store.actor -- who we're logged in as, if aid ~= 0 peer: lib.store.inet reqtype: lib.http.mime.t -- negotiated content type + method: lib.http.method.t + live_last: lib.store.timepoint -- cache + ui_hue: uint16 navbar: lib.mem.ptr(int8) actorcache: lib.mem.cache(lib.mem.ptr(lib.store.actor),32) -- naive cache to avoid unnecessary queries -- private varbuf: lib.mem.ptr(int8) vbofs: &int8 } + +struct convo.page { + title: pstring + body: pstring + class: pstring + cache: bool +} -- this is unfortunately necessary to work around a terra bug -- it can't seem to handle forward-declarations of structs in C local getpeer @@ -138,10 +159,73 @@ terra getpeer(con: &lib.net.mg_connection) return [&strucheader](con).peer end end +terra convo:rawpage(code: uint16, pg: convo.page, hdrs: lib.mem.ptr(lib.http.header)) + var doc = data.view.docskel { + instance = self.srv.cfg.instance; + title = pg.title; + body = pg.body; + class = pg.class; + navlinks = self.navbar; + attr = ''; + } + var attrbuf: int8[32] + if self.aid ~= 0 and self.ui_hue ~= 323 then + var hdecbuf: int8[21] + var hdec = lib.math.decstr(self.ui_hue, &hdecbuf[20]) + lib.str.cpy(&attrbuf[0], ' style="--hue:') + lib.str.cpy(&attrbuf[14], hdec) + var len = &hdecbuf[20] - hdec + lib.str.cpy(&attrbuf[14] + len, '"') + doc.attr = &attrbuf[0] + end + + if self.method == [lib.http.method.head] + then doc:head(self.con,code,hdrs) + else doc:send(self.con,code,hdrs) + end +end + +terra convo:statpage(code: uint16, pg: convo.page) + var hdrs = array( + lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' }, + lib.http.header { key = 'Cache-Control', value = 'no-store' } + ) + self:rawpage(code,pg, [lib.mem.ptr(lib.http.header)] { + ptr = &hdrs[0]; + ct = [hdrs.type.N] - lib.trn(pg.cache,1,0); + }) +end + +terra convo:livepage(pg: convo.page, lastup: lib.store.timepoint) + var nbuf: int8[21] + var hdrs = array( + lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' }, + lib.http.header { key = 'Cache-Control', value = 'no-store' }, + lib.http.header { + key = 'X-Live-Newest-Artifact'; + value = lib.math.decstr(lastup, &nbuf[20]); + }, + lib.http.header { key = 'Content-Length', value = '0' } + ) + if self.live_last ~= 0 and self.live_last <= lastup then + lib.net.mg_printf(self.con, 'HTTP/1.1 %s', lib.http.codestr(200)) + for i = 0, [hdrs.type.N] do + lib.net.mg_printf(self.con, '%s: %s\r\n', hdrs[i].key, hdrs[i].value) + end + lib.net.mg_printf(self.con, '\r\n') + else + self:rawpage(200, pg, [lib.mem.ptr(lib.http.header)] { + ptr = &hdrs[0], ct = 3 + }) + end +end + +terra convo:stdpage(pg: convo.page) self:statpage(200, pg) end + terra convo:reroute_cookie(dest: rawstring, cookie: rawstring) var hdrs = array( lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' }, lib.http.header { key = 'Location', value = dest }, lib.http.header { key = 'Set-Cookie', value = cookie } @@ -151,10 +235,11 @@ instance = self.srv.cfg.instance.ptr; title = 'rerouting'; body = 'you are being redirected'; class = 'error'; navlinks = ''; + attr = ''; } body:send(self.con, 303, [lib.mem.ptr(lib.http.header)] { ptr = &hdrs[0], ct = [hdrs.type.N] - lib.trn(cookie == nil,1,0) }) @@ -172,32 +257,22 @@ end self:reroute_cookie(dest, &sesskey[0]) 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' }, - lib.http.header { key = 'Cache-Control', value = 'no-store' } - ) + if msg == nil then msg = "i'm sorry, dave. i can't let you do that" end 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; + var body = [convo.page] { title = ti:finalize(); body = bo:finalize(); class = lib.str.plit 'error'; - navlinks = lib.coalesce(self.navbar, [lib.mem.ptr(int8)]{ptr='',ct=0}); + cache = false; } - if body.body.ptr == nil then - body.body = lib.str.plit"i'm sorry, dave. i can't let you do that" - end - - body:send(self.con, code, [lib.mem.ptr(lib.http.header)] { - ptr = &hdrs[0], ct = [hdrs.type.N] - }) + self:statpage(code, body) body.title:free() body.body:free() end @@ -209,34 +284,10 @@ self:complain(403,'insufficient privileges',['you lack the '..pow:asvalue()..' power and cannot perform this action']) end in ok end end) -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; - title = pg.title; - body = pg.body; - class = pg.class; - navlinks = self.navbar; - } - - var hdrs = array( - 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] - 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 self.varbuf = lib.mem.heapa(int8, self.msg.body.len + self.msg.query.len) self.vbofs = self.varbuf.ptr @@ -301,12 +352,12 @@ else ret = [mimeneg] end in ret end end local handle = { - http = terra(con: &lib.net.mg_connection, event: int, p: &opaque, ext: &opaque) - var server = [&srv](ext) + http = terra(con: &lib.net.mg_connection, event_kind: int, event: &opaque, userdata: &opaque) + var server = [&srv](userdata) var mgpeer = getpeer(con) var peer = lib.store.inet { port = mgpeer.port; } if mgpeer.is_ip6 then peer.pv = 6 else peer.pv = 4 end if peer.pv == 6 then for i = 0, 16 do peer.v6[i] = mgpeer.ip6[i] end @@ -321,29 +372,30 @@ -- needs to check for an X-Forwarded-For header from nginx and -- use that instead of the peer iff peer is ::1/127.1 FIXME -- maybe also haproxy support? - switch event do + switch event_kind do case lib.net.MG_EV_HTTP_MSG then lib.dbg('routing HTTP request') - var msg = [&lib.net.mg_http_message](p) + var msg = [&lib.net.mg_http_message](event) var co = convo { con = con, srv = server, msg = msg; aid = 0, aid_issue = 0, who = nil; reqtype = lib.http.mime.none; - peer = peer; + peer = peer, live_last = 0; } co.varbuf.ptr = nil co.navbar.ptr = nil co.actorcache.top = 0 co.actorcache.cur = 0 + co.ui_hue = server.cfg.ui_hue -- first, check for an accept header. if it's there, we need to -- iterate over the values and pick the highest-priority one do var acc = lib.http.findheader(msg, 'Accept') -- TODO handle q-value - if acc.ptr ~= nil then + if acc ~= nil and acc.ptr ~= nil then var [mimevar] = [lib.mem.ref(int8)] { ptr = acc.ptr } var i = 0 while i < acc.ct do if acc.ptr[i] == @',' or acc.ptr[i] == @';' then mimevar.ct = (acc.ptr+i) - mimevar.ptr var t = [mimeneg] @@ -379,11 +431,11 @@ -- we need to check if there's any cookies sent with the request, -- and if so, whether they contain any credentials. this will be -- used to set the auth parameters in the http conversation var cookies_p = lib.http.findheader(msg, 'Cookie') - if cookies_p ~= nil then + if cookies_p ~= nil and cookies_p.ptr ~= nil then var cookies = cookies_p.ptr var key = [lib.mem.ref(int8)] {ptr = cookies, ct = 0} var val = [lib.mem.ref(int8)] {ptr = nil, ct = 0} var i = 0 while i < cookies_p.ct and cookies[i] ~= 0 and @@ -425,12 +477,21 @@ if co.aid ~= 0 then var sess, usr = co.srv:actor_session_fetch(co.aid, peer, co.aid_issue) if sess.ok == false then co.aid = 0 co.aid_issue = 0 else co.who = usr.ptr co.who.rights.powers = server:actor_powers_fetch(co.who.id) + var userhue, hueok = server:actor_conf_int_get(co.who.id, 'ui-accent') + if hueok then co.ui_hue = userhue end end end + + var livelast_p = lib.http.findheader(msg, 'X-Live-Last-Arrival') + if livelast_p ~= nil and livelast_p.ptr ~= nil then + var ll, ok = lib.math.decparse(pstring{ptr = livelast_p.ptr, ct = livelast_p.ct - 1}) + if ok then co.live_last = ll end + end + var uridec = lib.mem.heapa(int8, msg.uri.len) defer uridec:free() var urideclen = lib.net.mg_url_decode(msg.uri.ptr, msg.uri.len, uridec.ptr, uridec.ct, 1) var uri = uridec @@ -444,16 +505,20 @@ uri.ct = msg.uri.len else uri.ct = urideclen end lib.dbg('routing URI ', {uri.ptr, uri.ct}) if lib.str.ncmp('GET', msg.method.ptr, msg.method.len) == 0 then + co.method = [lib.http.method.get] route.dispatch_http(&co, uri, [lib.http.method.get]) elseif lib.str.ncmp('POST', msg.method.ptr, msg.method.len) == 0 then + co.method = [lib.http.method.get] route.dispatch_http(&co, uri, [lib.http.method.post]) elseif lib.str.ncmp('HEAD', msg.method.ptr, msg.method.len) == 0 then + co.method = [lib.http.method.head] route.dispatch_http(&co, uri, [lib.http.method.head]) elseif lib.str.ncmp('OPTIONS', msg.method.ptr, msg.method.len) == 0 then + co.method = [lib.http.method.options] route.dispatch_http(&co, uri, [lib.http.method.options]) else co:complain(400,'unknown method','you have submitted an invalid http request') end @@ -672,32 +737,32 @@ if sreg:ref() then if lib.str.cmp(sreg.ptr, 'on') == 0 then self.pol_reg = true else self.pol_reg = false end - end - sreg:free() end + sreg:free() + end end do self.credmgd = false var sreg = self.overlord:conf_get('credential-store') if sreg:ref() then if lib.str.cmp(sreg.ptr, 'managed') == 0 then self.credmgd = true else self.credmgd = false end - end - sreg:free() end + sreg:free() + end end do self.maxupsz = [1024 * 100] -- 100 kilobyte default var sreg = self.overlord:conf_get('maximum-artifact-size') if sreg:ref() then var sz, ok = lib.math.fsz_parse(sreg) if ok then self.maxupsz = sz else lib.warn('invalid configuration value for maximum-artifact-size; keeping default 100K upload limit') end + sreg:free() end end - sreg:free() end self.pol_sec = secmode.lockdown var smode = self.overlord:conf_get('policy-security') if smode.ptr ~= nil then if lib.str.cmp(smode.ptr, 'public') == 0 then @@ -707,15 +772,23 @@ elseif lib.str.cmp(smode.ptr, 'lockdown') == 0 then self.pol_sec = secmode.lockdown elseif lib.str.cmp(smode.ptr, 'isolate') == 0 then self.pol_sec = secmode.isolate end + smode:free() end - smode:free() + + 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 end return { overlord = srv; convo = convo; route = route; secmode = secmode; } ADDED static/live.js Index: static/live.js ================================================================== --- static/live.js +++ static/live.js @@ -0,0 +1,51 @@ +/* first things first, we need to scan over the document and see + * if there are any UI elements unfortunate enough to need + * interactivity beyond what native HTML+CSS can provide. if so, + * we attach the appropriate listeners to them. */ +window.addEventListener('load', function() { + /* update hue-picker background when slider is adjusted */ + document.querySelectorAll('.color-picker').forEach(function(box) { + let slider = box.querySelector('[data-color-pick]'); + box.style.setProperty('--hue', slider.value); + slider.addEventListener('input', function(e) { + box.style.setProperty('--hue', e.target.value); + }); + }); + + /* the main purpose of this script -- by marking itself with the + * data-live property, an html element registers itself for live + * updates from the server. this is pretty straightforward: we + * retrieve this url from the server as a get request, create a + * tree from its html, find the element in question, ferret out + * any deltas, and apply them. */ + document.querySelectorAll('*[data-live]').forEach(function(container) { + let interv = parseFloat(container.attributes.getNamedItem('data-live').nodeValue) * 1000; + container._liveLastArrival = '0'; /* TODO include header for this */ + + window.setInterval(function() { + var req = new Request(window.location, { + method: 'GET', + headers: { + 'X-Live-Last-Arrival': container._liveLastArrival + } + }) + + fetch(req).then(function(resp) { + if (!resp.ok) return; + let newest = resp.headers.get('X-Live-Newest-Artifact'); + if (newest <= container._liveLastArrival) { + resp.body.cancel(); + return; + } + container._liveLastArrival = newest + + resp.text().then(function(htmlbody) { + var parser = new DOMParser(); + var newdoc = parser.parseFromString(htmlbody,'text/html') + // console.log(newdoc.getElementById(container.id).innerHTML) + container.innerHTML = newdoc.getElementById(container.id).innerHTML + }) + }) + }, interv) + }); +}); Index: static/style.scss ================================================================== --- static/style.scss +++ static/style.scss @@ -1,12 +1,16 @@ -$color: hsl(323,100%,65%); +$default-color: hsl(323,100%,65%); %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 adjust-color($color, $lightness: $pct, $alpha: $alpha) } +@function tone($pct, $alpha: 0) { + @return hsla(var(--hue), 100%, 65% + $pct, 1 + $alpha) +} +:root { --hue: 323; } body { @extend %sans; background-color: tone(-55%); color: tone(25%); font-size: 14pt; @@ -22,14 +26,15 @@ font-style: italic; } a[href] { color: tone(10%); text-decoration-color: tone(10%,-0.5); - &:hover { + &:hover, &:focus { color: white; text-shadow: 0 0 15px tone(20%); text-decoration-color: tone(10%,-0.1); + outline: none; } &.button { @extend %button; } } a[href^="//"], a[href^="http://"], @@ -71,10 +76,12 @@ 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%) @@ -208,11 +215,11 @@ padding: 0.25in 0.10in; //padding: calc((25% - 1em)/2) 0.15in; &, &::after { transition: 0.3s; } text-shadow: 1px 1px 1px black; &:hover{ - transform: scale(120%); + transform: scale(1.2); } } } } } @@ -343,10 +350,11 @@ width: 4in; margin:auto; padding: 0.5in; text-align: center; menu:first-of-type { margin-top: 0.3in; } + img.icon { width: 1.875in; height: 1.875in; } } div.login { @extend %box; width: 4in; @@ -460,20 +468,25 @@ padding-bottom: 3px; border-radius: 2px; vertical-align: baseline; box-shadow: 1px 1px 1px black; } + +div.thread { + margin-left: 0.3in; + & + div.post { margin-top: 0.3in; } +} div.post { @extend %box; display: grid; grid-template-columns: 1in 1fr max-content; grid-template-rows: min-content max-content; margin-bottom: 0.1in; >.avatar { grid-column: 1/2; grid-row: 1/2; - img { display: block; width: 1in; margin:0; } + img { display: block; width: 1in; height: 1in; margin:0; } background: linear-gradient(to bottom, tone(-53%), tone(-57%)); } >a[href].username { display: block; grid-column: 1/3; @@ -496,10 +509,11 @@ grid-column: 2/4; grid-row: 1/2; padding: 0.2in; @extend %serif; font-size: 110%; text-align: justify; + color: tone(25%); } > a[href].permalink { display: block; grid-column: 3/4; grid-row: 2/3; font-size: 80%; @@ -525,34 +539,36 @@ margin-left: -0.4in; padding-left: 0.2in; text-shadow: 0 2px 0 black; } } + +%navmenu, body.profile main > menu { + margin-left: -0.25in; + grid-column: 1/2; grid-row: 1/2; + background: linear-gradient(to bottom, tone(-45%),tone(-55%)); + border: 1px solid black; + padding: 0.1in; + > a[href] { + @extend %button; + display: block; + text-align: left; + } + > a[href] + a[href] { + border-top: none; + } + hr { + border: none; + } +} menu { all: unset; display: block; } body.conf main { display: grid; grid-template-columns: 2in 1fr; grid-template-rows: max-content 1fr; - > menu { - margin-left: -0.25in; - grid-column: 1/2; grid-row: 1/2; - background: linear-gradient(to bottom, tone(-45%),tone(-55%)); - border: 1px solid black; - padding: 0.1in; - > a[href] { - @extend %button; - display: block; - text-align: left; - } - > a[href] + a[href] { - border-top: none; - } - hr { - border: none; - } - } + > menu { @extend %navmenu; } > .panel { grid-column: 2/3; grid-row: 1/3; padding-left: 0.15in; > h1 { padding-bottom: 0.1in; @@ -612,11 +628,11 @@ float: right; width: 40%; margin-left: 0.1in; } > %button { - flex-basis: 0; + flex-basis: min-content; flex-grow: 1; display: block; margin: 2px; } } @@ -666,11 +682,11 @@ .flashmsg { display: block; position: fixed; top: 1.3in; max-width: 3in; - padding: 0.5in 0.2in; + padding: 0.4in 0.2in; left: 0; right: 0; text-align: center; text-shadow: 0 0 15px tone(10%); margin: auto; background: linear-gradient(to bottom, tone(-49%), tone(-43%,-0.1)); @@ -678,11 +694,11 @@ border-radius: 3px; box-shadow: 0 0 50px tone(-55%); color: white; animation: ease forwards flashup; //cubic-bezier(0.4, 0.63, 0.6, 0.31) - animation-duration: 3s; + animation-duration: 2.5s; } form.action-bar { display: flex; > * { @@ -692,5 +708,12 @@ } > *:first-child { margin-left: 0; } } + +.color-picker { + /* implemented using javascript, alas */ + @extend %box; + label { text-shadow: 1px 1px black; } + padding: 0.1in; +} Index: store.t ================================================================== --- store.t +++ store.t @@ -158,31 +158,42 @@ circles: lib.mem.ptr(uint64) --only meaningful if scope is set to circle convoheaduri: str parent: uint64 -- ephemera localpost: bool + accent: int16 + depth: uint16 -- used in conversations to indicate tree depth source: &m.source -- save :: bool -> {} (defined in acl.t due to dep. hell) } -local cnf = terralib.memoize(function(ty,rty) +m.user_conf_funcs = function(be,n,ty,rty,rty2) rty = rty or ty - return struct { - enum: {&opaque, uint64, rawstring} -> intptr - get: {&opaque, uint64, rawstring} -> rty - set: {&opaque, uint64, rawstring, ty} -> {} - reset: {&opaque, uint64, rawstring} -> {} - } -end) + local gt + if not rty2 -- what the fuck? + then gt = {&m.source, uint64, rawstring} -> rty; + else gt = {&m.source, uint64, rawstring} -> {rty, rty2}; + end + for k, t in pairs { + enum = {&m.source, uint64, rawstring} -> lib.mem.ptr(rty); + get = gt; + set = {&m.source, uint64, rawstring, ty} -> {}; + reset = {&m.source, uint64, rawstring} -> {}; + } do + be.entries[#be.entries+1] = { + field = 'actor_conf_'..n..'_'..k, type = t + } + end +end struct m.notif { kind: m.notiftype.t when: uint64 union { post: uint64 - reaction: int8[8] + reaction: int8[16] } } struct m.inet { pv: uint8 -- 0 = null, 4 = ipv4, 6 = ipv6 @@ -234,11 +245,13 @@ struct m.auth { -- a credential record aid: uint64 uid: uint64 + kind: str aname: str + comment: str netmask: m.inet privs: m.privset blacklist: bool } @@ -315,17 +328,17 @@ -- notifies the backend module of the UID that has been assigned for -- an authentication ID -- aid: uint64 -- uid: uint64 - actor_conf_str: cnf(rawstring, lib.mem.ptr(int8)) - actor_conf_int: cnf(intptr, lib.stat(intptr)) - - auth_create_pw: {&m.source, uint64, bool, lib.mem.ptr(int8)} -> {} + 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_attach_pw: {&m.source, uint64, bool, pstr, pstr} -> {} -- uid: uint64 -- reset: bool (delete other passwords?) -- pw: pstring + -- comment: pstring auth_purge_pw: {&m.source, uint64, rawstring} -> {} auth_purge_otp: {&m.source, uint64, rawstring} -> {} auth_purge_trust: {&m.source, uint64, rawstring} -> {} auth_sigtime_user_fetch: {&m.source, uint64} -> m.timepoint -- authentication tokens and accounts have a property that controls @@ -340,15 +353,19 @@ 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_attach_ctl: {&m.source, uint64, uint64, bool} -> {} -- attaches or detaches an existing database artifact -- post id: uint64 -- artifact id: uint64 -- detach: bool + + thread_latest_arrival_calc: {&m.source, uint64} -> m.timepoint + artifact_instantiate: {&m.source, lib.mem.ptr(uint8), lib.mem.ptr(int8)} -> uint64 -- instantiate an artifact in the database, either installing a new -- artifact or returning the id of an existing artifact with the same hash -- artifact: bytea -- mime: pstring @@ -387,16 +404,16 @@ 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_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)) } + +m.user_conf_funcs(m.backend, 'str', rawstring, lib.mem.ptr(int8)) +m.user_conf_funcs(m.backend, 'int', intptr, intptr, bool) struct m.source { backend: &m.backend id: lib.mem.ptr(int8) handle: &opaque Index: tpl.t ================================================================== --- tpl.t +++ tpl.t @@ -158,21 +158,32 @@ [tallyup] accumulator:cue([runningtally]) [appenders] return accumulator end - rec.methods.send = terra([symself], [destcon], code: uint16, hd: lib.mem.ptr(lib.http.header)) - lib.dbg(['transmitting template ' .. tid]) + rec.methods.head = terra([symself], [destcon], code: uint16, hd: lib.mem.ptr(lib.http.header)) + lib.dbg(['transmitting template headers ' .. tid]) [tallyup] lib.net.mg_printf([destcon], 'HTTP/1.1 %s', lib.http.codestr(code)) for i = 0, hd.ct do lib.net.mg_printf([destcon], '%s: %s\r\n', hd.ptr[i].key, hd.ptr[i].value) end lib.net.mg_printf([destcon],'Content-Length: %llu\r\n\r\n', [runningtally] + 1) + end + rec.methods.send = terra([symself], [destcon], code: uint16, hd: lib.mem.ptr(lib.http.header)) + lib.dbg(['transmitting template ' .. tid]) + + symself:head(destcon,code,hd) + [senders] lib.net.mg_send([destcon],'\r\n',2) + end + rec.methods.sz = terra([symself]) + lib.dbg(['tallying template ' .. tid]) + [tallyup] + return [runningtally] + 1 end return rec end return m Index: view/conf-profile.tpl ================================================================== --- view/conf-profile.tpl +++ view/conf-profile.tpl @@ -1,6 +1,10 @@
@!handle
- +
+ + + +
Index: view/conf.tpl ================================================================== --- view/conf.tpl +++ view/conf.tpl @@ -1,8 +1,9 @@ profile avatar + interface security relationships quarantine ACL shortcuts chatrooms Index: view/docskel.tpl ================================================================== --- view/docskel.tpl +++ view/docskel.tpl @@ -1,12 +1,13 @@ @instance :: @title - + + - +

@title

ADDED view/tweet-mini.tpl Index: view/tweet-mini.tpl ================================================================== --- view/tweet-mini.tpl +++ view/tweet-mini.tpl @@ -0,0 +1,4 @@ +
+ [@when] + @nym @text +
Index: view/tweet.tpl ================================================================== --- view/tweet.tpl +++ view/tweet.tpl @@ -1,9 +1,9 @@ -
+
@nym
@!subject
@text