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.who.rights.powers.post() then
- lib.render.compose(co, nil, &pg)
- end
+ end
+ 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 @@
Index: view/conf.tpl
==================================================================
--- view/conf.tpl
+++ view/conf.tpl
@@ -1,8 +1,9 @@