Index: backend/pgsql.t ================================================================== --- backend/pgsql.t +++ backend/pgsql.t @@ -23,21 +23,24 @@ }; actor_fetch_uid = { params = {uint64}, sql = [[ select - id, nym, handle, origin, - bio, rank, quota, key + id, nym, handle, origin, bio, + avataruri, rank, quota, key, + extract(epoch from knownsince)::bigint + from parsav_actors where id = $1::bigint ]]; }; actor_fetch_xid = { params = {lib.mem.ptr(int8)}, sql = [[ - select a.id, a.nym, a.handle, a.origin, - a.bio, a.rank, a.quota, a.key, + select a.id, a.nym, a.handle, a.origin, a.bio, + a.avataruri, a.rank, a.quota, a.key, + extract(epoch from knownsince)::bigint, coalesce(a.handle || '@' || s.domain, '@' || a.handle) as xid, coalesce(s.domain, (select value from parsav_config @@ -52,32 +55,70 @@ (a.origin is null and $1::text = a.handle or $1::text = ('@' || a.handle)) ]]; }; + + actor_auth_pw = { + params = {lib.mem.ptr(int8),rawstring,lib.mem.ptr(int8),lib.store.inet}, sql = [[ + select a.aid from parsav_auth as a + left join parsav_actors as u on u.id = a.uid + where (a.uid is null or u.handle = $1::text or ( + a.uid = 0 and a.name = $1::text + )) and + (a.kind = 'trust' or (a.kind = $2::text and a.cred = $3::bytea)) and + (a.netmask is null or a.netmask >> $4::inet) + order by blacklist desc limit 1 + ]]; + }; actor_enum_local = { params = {}, sql = [[ - select id, nym, handle, origin, - bio, rank, quota, key, + select id, nym, handle, origin, bio, + null::text, rank, quota, key, + extract(epoch from knownsince)::bigint, handle ||'@'|| (select value from parsav_config where key='domain' limit 1) as xid from parsav_actors where origin is null ]]; }; actor_enum = { params = {}, sql = [[ - select a.id, a.nym, a.handle, a.origin, - a.bio, a.rank, a.quota, a.key, + select a.id, a.nym, a.handle, a.origin, a.bio, + a.avataruri, a.rank, a.quota, a.key, + extract(epoch from knownsince)::bigint, coalesce(a.handle || '@' || s.domain, '@' || a.handle) as xid from parsav_actors a left join parsav_servers s on s.id = a.origin ]]; }; + + actor_stats = { + params = {uint64}, sql = ([[ + with tweets as ( + select from parsav_posts where author = $1::bigint + ), + follows as ( + select relatee as user from parsav_rels + where relator = $1::bigint and kind = + ), + followers as ( + select relator as user from parsav_rels + where relatee = $1::bigint and kind = + ), + mutuals as (select * from follows intersect select * from followers) + + select count(tweets.*)::bigint, + count(follows.*)::bigint, + count(followers.*)::bigint, + count(mutuals.*)::bigint + from tweets, follows, followers, mutuals + ]]):gsub('<(%w+)>',function(r) return tostring(lib.store.relation[r]) end) + }; actor_auth_how = { params = {rawstring, lib.store.inet}, sql = [[ with mts as (select a.kind from parsav_auth as a left join parsav_actors as u on u.id = a.uid @@ -95,12 +136,13 @@ ]]; -- cheat }; actor_session_fetch = { params = {uint64, lib.store.inet}, sql = [[ - select a.id, a.nym, a.handle, a.origin, - a.bio, a.rank, a.quota, a.key, + select a.id, a.nym, a.handle, a.origin, a.bio, + a.avataruri, a.rank, a.quota, a.key, + extract(epoch from knownsince)::bigint, coalesce(a.handle || '@' || s.domain, '@' || a.handle) as xid, au.restrict, array['post' ] <@ au.restrict as can_post, @@ -173,10 +215,11 @@ end pqr.methods.int = macro(function(self, ty, row, col) return quote var i: ty:astype() var v = lib.pq.PQgetvalue(self.res, row, col) + --i = @[&uint64](v) lib.math.netswap_ip(ty, v, &i) in i end end) local pqt = { @@ -218,28 +261,33 @@ end lib.dbg('prepared PGSQL statement ',k) end local args, casts, counters, fixers, ft, yield = {}, {}, {}, {}, {}, {} + local dumpers = {} for i, ty in ipairs(q.params) do args[i] = symbol(ty) ft[i] = `1 if ty == rawstring then counters[i] = `lib.trn([args[i]] == nil, 0, lib.str.sz([args[i]])) casts[i] = `[&int8]([args[i]]) + dumpers[#dumpers+1] = `lib.io.fmt([tostring(i)..'. got rawstr %s\n'], [args[i]]) elseif ty == lib.store.inet then -- assume not CIDR counters[i] = `lib.trn([args[i]].pv == 4,4,16)+4 casts[i] = quote var ipbuf: int8[20] ;[pqt[lib.store.inet](false)]([args[i]], [&uint8](&ipbuf)) in &ipbuf[0] end + dumpers[#dumpers+1] = `lib.io.fmt([tostring(i)..'. got inet\n']) elseif ty.ptr_basetype == int8 or ty.ptr_basetype == uint8 then counters[i] = `[args[i]].ct casts[i] = `[&int8]([args[i]].ptr) + dumpers[#dumpers+1] = `lib.io.fmt([tostring(i)..'. got ptr %llu %.*s\n'], [args[i]].ct, [args[i]].ct, [args[i]].ptr) elseif ty:isintegral() then counters[i] = ty.bytes casts[i] = `[&int8](&[args[i]]) + dumpers[#dumpers+1] = `lib.io.fmt([tostring(i)..'. got int %llu\n'], [args[i]]) fixers[#fixers + 1] = quote --lib.io.fmt('uid=%llu(%llx)\n',[args[i]],[args[i]]) [args[i]] = lib.math.netswap(ty, [args[i]]) end end @@ -248,10 +296,11 @@ terra q.exec(src: &lib.store.source, [args]) var params = arrayof([&int8], [casts]) var params_sz = arrayof(int, [counters]) var params_ft = arrayof(int, [ft]) [fixers] + --[dumpers] var res = lib.pq.PQexecPrepared([&lib.pq.PGconn](src.handle), stmt, [#args], params, params_sz, params_ft, 1) if res == nil then lib.bail(['grievous error occurred executing '..k..' against database']) elseif lib.pq.PQresultStatus(res) ~= lib.pq.PGRES_TUPLES_OK then @@ -270,64 +319,55 @@ end local terra row_to_actor(r: &pqr, row: intptr): lib.mem.ptr(lib.store.actor) var a: lib.mem.ptr(lib.store.actor) - if r:cols() >= 8 then + if r:cols() >= 9 then a = [ lib.str.encapsulate(lib.store.actor, { nym = {`r:string(row,1), `r:len(row,1)+1}; bio = {`r:string(row,4), `r:len(row,4)+1}; + avatar = {`r:string(row,5), `r:len(row,5)+1}; handle = {`r:string(row, 2); `r:len(row,2) + 1}; - xid = {`r:string(row, 8); `r:len(row,8) + 1}; + xid = {`r:string(row, 10); `r:len(row,10) + 1}; }) ] else a = [ lib.str.encapsulate(lib.store.actor, { nym = {`r:string(row,1), `r:len(row,1)+1}; bio = {`r:string(row,4), `r:len(row,4)+1}; + avatar = {`r:string(row,5), `r:len(row,5)+1}; handle = {`r:string(row, 2); `r:len(row,2) + 1}; }) ] a.ptr.xid = nil end a.ptr.id = r:int(uint64, row, 0); a.ptr.rights = lib.store.rights_default(); - a.ptr.rights.rank = r:int(uint16, row, 5); - a.ptr.rights.quota = r:int(uint32, row, 6); - if r:null(row,7) then + a.ptr.rights.rank = r:int(uint16, row, 6); + a.ptr.rights.quota = r:int(uint32, row, 7); + a.ptr.knownsince = r:int(int64,row, 9); + if r:null(row,8) then a.ptr.key.ct = 0 a.ptr.key.ptr = nil else - a.ptr.key = r:bin(row,7) + a.ptr.key = r:bin(row,8) end if r:null(row,3) then a.ptr.origin = 0 else a.ptr.origin = r:int(uint64,row,3) end return a end -local checksha = function(hnd, query, hash, origin, username, pw) - local inet_buf = symbol(uint8[4 + 16]) +local checksha = function(src, hash, origin, username, pw) local validate = function(kind, cred, credlen) return quote - var osz: intptr if origin.pv == 4 then osz = 4 else osz = 16 end - var formats = arrayof([int], 1,1,1,1) - var params = arrayof([&int8], username, kind, - [&int8](&cred), [&int8](&inet_buf)) - var lens = arrayof(int, lib.str.sz(username), [#kind], credlen, osz + 4) - var res = lib.pq.PQexecParams([&lib.pq.PGconn](hnd), query, 4, nil, - params, lens, formats, 1) - if res == nil then - lib.bail('grievous failure checking pwhash') - elseif lib.pq.PQresultStatus(res) ~= lib.pq.PGRES_TUPLES_OK then - lib.warn('pwhash query failed: ', lib.pq.PQresultErrorMessage(res), '\n', query) - else - var r = pqr { - sz = lib.pq.PQntuples(res); - res = res; - } - if r.sz > 0 then -- found a record! stop here - var aid = r:int(uint64, 0,0) - r:free() - return aid - end + var r = queries.actor_auth_pw.exec( + [&lib.store.source](src), + username, + kind, + [lib.mem.ptr(int8)] {ptr=[&int8](cred), ct=credlen}, + origin) + if r.sz > 0 then -- found a record! stop here + var aid = r:int(uint64, 0,0) + r:free() + return aid end end end local out = symbol(uint8[64]) @@ -334,20 +374,18 @@ local vdrs = {} local alg = lib.md['MBEDTLS_MD_SHA' .. tostring(hash)] vdrs[#vdrs+1] = quote if lib.md.mbedtls_md(lib.md.mbedtls_md_info_from_type(alg), - [&uint8](pw), lib.str.sz(pw), out) ~= 0 then + [&uint8](pw.ptr), pw.ct, out) ~= 0 then lib.bail('hashing failure!') end - [ validate(string.format('pw-sha%u', hash), out, hash / 8) ] + [ validate(string.format('pw-sha%u', hash), `&out[0], hash / 8) ] end return quote lib.dbg(['searching for hashed password credentials in format SHA' .. tostring(hash)]) - var [inet_buf] - [pqt[lib.store.inet](false)](origin, inet_buf) var [out] [vdrs] lib.dbg(['could not find password hash']) end end @@ -441,46 +479,49 @@ actor_auth_how = [terra( src: &lib.store.source, ip: lib.store.inet, username: rawstring - ) + ): {lib.store.credset, bool} var cs: lib.store.credset cs:clear(); var r = queries.actor_auth_how.exec(src, username, ip) - if r.sz == 0 then return cs end -- just in case + if r.sz == 0 then return cs, false end -- just in case defer r:free() (cs.pw << r:bool(0,0)) (cs.otp << r:bool(0,1)) (cs.challenge << r:bool(0,2)) (cs.trust << r:bool(0,3)) - return cs + return cs, true end]; actor_auth_pw = [terra( src: &lib.store.source, ip: lib.store.inet, - username: rawstring, - cred: rawstring - ) - var q = [[select a.aid from parsav_auth as a - left join parsav_actors as u on u.id = a.uid - where (a.uid is null or u.handle = $1::text or ( - a.uid = 0 and a.name = $1::text - )) and - (a.kind = 'trust' or (a.kind = $2::text and a.cred = $3::bytea)) and - (a.netmask is null or a.netmask >> $4::inet) - order by blacklist desc limit 1]] + username: lib.mem.ptr(int8), + cred: lib.mem.ptr(int8) + ): uint64 - [ checksha(`src.handle, q, 256, ip, username, cred) ] -- most common - [ checksha(`src.handle, q, 512, ip, username, cred) ] -- most secure - [ checksha(`src.handle, q, 384, ip, username, cred) ] -- weird - [ checksha(`src.handle, q, 224, ip, username, cred) ] -- weirdest + [ checksha(`src, 256, ip, username, cred) ] -- most common + [ checksha(`src, 512, ip, username, cred) ] -- most secure + [ checksha(`src, 384, ip, username, cred) ] -- weird + [ checksha(`src, 224, ip, username, cred) ] -- weirdest -- TODO: check pbkdf2-hmac -- TODO: check OTP return 0 end]; + + actor_stats = [terra(src: &lib.store.source, uid: uint64) + var r = queries.actor_stats.exec(src, uid) + if r.sz == 0 then lib.bail('error fetching actor stats!') end + var s: lib.store.actor_stats + s.posts = r:int(uint64, 0, 0) + s.follows = r:int(uint64, 0, 1) + s.followers = r:int(uint64, 0, 2) + s.mutuals = r:int(uint64, 0, 3) + return s + end]; actor_session_fetch = [terra( src: &lib.store.source, aid: uint64, ip : lib.store.inet @@ -495,18 +536,18 @@ a.ptr.source = src var au = [lib.stat(lib.store.auth)] { ok = true } au.val.aid = aid au.val.uid = a.ptr.id - if not r:null(0,10) then -- restricted? + if not r:null(0,12) then -- restricted? au.val.privs:clear() - (au.val.privs.post << r:bool(0,11)) - (au.val.privs.edit << r:bool(0,12)) - (au.val.privs.acct << r:bool(0,13)) - (au.val.privs.upload << r:bool(0,14)) - (au.val.privs.censor << r:bool(0,15)) - (au.val.privs.admin << r:bool(0,16)) + (au.val.privs.post << r:bool(0,13)) + (au.val.privs.edit << r:bool(0,14)) + (au.val.privs.acct << r:bool(0,15)) + (au.val.privs.upload << r:bool(0,16)) + (au.val.privs.censor << r:bool(0,17)) + (au.val.privs.admin << r:bool(0,18)) else au.val.privs:fill() end return au, a end Index: config.lua ================================================================== --- config.lua +++ config.lua @@ -29,10 +29,11 @@ os.getenv('NIX_STORE') and 'nixos', '')); tgttrip = default('parsav_arch_triple'); -- target triple, used in xcomp tgtcpu = default('parsav_arch_cpu'); -- target cpu, used in xcomp tgthf = u.tobool(default('parsav_arch_armhf',true)); + outform = default('parsav_emit_type', 'o'); endian = default('parsav_arch_endian', 'little'); build = { id = u.rndstr(6); release = u.ingest('release'); when = os.date(); @@ -40,10 +41,13 @@ feat = {}; backends = defaultlist('parsav_backends', 'pgsql'); braingeniousmode = false; embeds = { {'style.css', 'text/css'}; + {'default-avatar.webp', 'image/webp'}; + {'padlock.webp', 'image/webp'}; + {'warn.webp', 'image/webp'}; }; } if u.ping '.fslckout' or u.ping '_FOSSIL_' then if u.ping '_FOSSIL_' then default_os = 'windows' end conf.build.branch = u.exec { 'fossil', 'branch', 'current' } Index: http.t ================================================================== --- http.t +++ http.t @@ -1,10 +1,18 @@ -- vim: ft=terra local m = {} local util = dofile('common.lua') m.method = lib.enum { 'get', 'post', 'head', 'options', 'put', 'delete' } +m.mime = lib.enum { + 'html'; -- default + 'json'; + 'mkdown'; + 'text'; + 'ansi'; + 'none'; +} m.findheader = terralib.externfunction('mg_http_get_header', {&lib.net.mg_http_message, rawstring} -> &lib.mem.ref(int8)) -- unfortunately necessary to access this function, as its return type conflicts with a function name struct m.header { key: rawstring Index: makefile ================================================================== --- makefile +++ makefile @@ -1,12 +1,26 @@ dl = git dbg-flags = $(if $(dbg),-g) -parsav: parsav.t config.lua pkgdata.lua +images = $(addsuffix .webp, $(basename $(wildcard static/*.svg))) +styles = $(addsuffix .css, $(basename $(wildcard static/*.scss))) + +parsav: parsav.t config.lua pkgdata.lua $(images) $(styles) terra $(dbg-flags) $< -parsav.o: parsav.t config.lua pkgdata.lua +parsav.o: parsav.t config.lua pkgdata.lua $(images) $(styles) env parsav_link=no terra $(dbg-flags) $< +parsav.ll: parsav.t config.lua pkgdata.lua $(images) $(styles) + env parsav_emit_type=ll parsav_link=no terra $(dbg-flags) $< +parsav.s: parsav.ll + llc --march=$(target) $< + +static/%.webp: static/%.png + cwebp -q 90 $< -o $@ +static/%.png: static/%.svg + inkscape -f $< -C -d 180 -e $@ +static/%.css: static/%.scss + sassc -t compressed $< $@ clean: rm parsav parsav.o install: parsav @@ -41,16 +55,17 @@ $(MAKE) -C lib/json-c lib/mbedtls/library/%.a: lib/mbedtls $(MAKE) -C lib/mbedtls/library $*.a ifeq ($(dl), git) +clone = git clone --depth 1 # save time lib/mongoose: lib - cd lib && git clone https://github.com/cesanta/mongoose.git + cd lib && $(clone) https://github.com/cesanta/mongoose.git lib/mbedtls: lib - cd lib && git clone https://github.com/ARMmbed/mbedtls.git + cd lib && $(clone) https://github.com/ARMmbed/mbedtls.git lib/json-c: lib - cd lib && git clone https://github.com/json-c/json-c.git + cd lib && $(clone) https://github.com/json-c/json-c.git else lib/%: lib/%.tar.gz cd lib && tar zxf $*.tar.gz mv lib/$$(tar tf $< | head -n1) $@ Index: math.t ================================================================== --- math.t +++ math.t @@ -147,7 +147,44 @@ end terra m.b32str(a: lib.mem.ptr(uint64)) end + +terra m.decstr(val: intptr, buf: &int8): rawstring +-- works backwards to avoid copies. log10(2^64) ≈ 19.2 and we +-- need a byte for NUL so buf MUST point to THE END OF a buffer +-- at least 21 bytes long + @buf = 0 + if val > 0 then while val > 0 do + buf = buf - 1 + var dgt = val % 10 + val = val / 10 + @buf = 0x30 + dgt + end else + buf = buf - 1 + @buf = 0x30 + end + return buf +end + +terra m.decstr_friendly(val: intptr, buf: &int8): rawstring +-- as above except needs size-28 buffers, on account of all the commas + @buf = 0 + var dgtct: uint8 = 0 + if val > 0 then while val > 0 do + buf = buf - 1 + var dgt = val % 10 + val = val / 10 + @buf = 0x30 + dgt + if dgtct == 2 and val > 0 then + buf = buf - 1 @buf = @',' + dgtct = 0 + else dgtct = dgtct + 1 end + end else + buf = buf - 1 + @buf = 0x30 + end + return buf +end return m Index: parsav.md ================================================================== --- parsav.md +++ parsav.md @@ -13,13 +13,25 @@ * json-c * mbedtls * **postgresql backend:** * postgresql-libs +additional build-time 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: + +* inkscape, for rendering out UI graphics +* 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. + +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 -first, either install any missing dependencies as shared libraries, or build them as static libraries with the command `make dep.$LIBRARY`. as a shortcut, `make dep` will build all dependencies as static libraries. note that if the build system finds a static version of a librari in the `lib/` folder, it will use that instead of any system library. +first, either install any missing dependencies as shared libraries, or build them as static libraries with the command `make dep.$LIBRARY`. as a shortcut, `make dep` will build all dependencies as static libraries. note that if the build system finds a static version of a library in the `lib/` folder, it will use that instead of any system library. note that these commands require GNU make (it may be installed as `gmake` on your system), although this is a fairly soft dependency -- if you really need to build it on BSD make, you can probably translate it with a minute or so of work; you'll just have to do some of the various gmake functions' work manually. this may be worthwhile if you're packaging for a BSD. postgresql-libs must be installed systemwide, as `parsav` does not currently provide for statically compiling and linking it ## configuring Index: parsav.t ================================================================== --- parsav.t +++ parsav.t @@ -199,12 +199,18 @@ local ty = uint8 if #tbl >= 2^32 then ty = uint64 -- hey, can't be too safe elseif #tbl >= 2^16 then ty = uint32 elseif #tbl >= 2^8 then ty = uint16 end local o = { t = ty } + local strings = {} for i, name in ipairs(tbl) do - o[name] = i + o[name] = i - 1 + strings[i] = `[lib.mem.ref(int8)]{ptr=[name], ct=[#name]} + end + o._str = terra(val: ty) + var l = array([strings]) + return l[val] end return o end lib.set = function(tbl) local bytes = math.ceil(#tbl / 8) @@ -216,12 +222,19 @@ terra set:fill() for i=0,bytes do self._store[i] = 0xFF end end set.members = tbl set.name = string.format('set<%s>', table.concat(tbl, '|')) set.metamethods.__entrymissing = macro(function(val, obj) if o[val] == nil then error('value ' .. val .. ' not in set') end - return `bit { _v=[o[val] - 1], _set = &obj } + return `bit { _v=[o[val] - 1], _set = &(obj) } end) + terra set:sz() + var ct: intptr = 0 + for i = 0, [#tbl] do + if (self._store[i/8] and (1 << i % 8)) ~= 0 then ct = ct + 1 end + end + return ct + end set.methods.dump = macro(function(self) local q = quote lib.io.say('dumping set:\n') end for i,v in ipairs(tbl) do q = quote [q] @@ -308,12 +321,15 @@ data.view[k] = t end lib.load { 'srv'; + 'render:nav'; + 'render:login'; 'render:profile'; 'render:userpage'; + 'render:compose'; 'route'; } do local p = string.format('parsav: %s\nbuilt on %s\n', config.build.str, config.build.when) @@ -429,12 +445,12 @@ print(util.dump(config)) os.exit(0) end local holler = print -local out = config.exe and 'parsav' or 'parsav.o' -local linkargs = {} +local out = config.exe and 'parsav' or ('parsav.' .. config.outform) +local linkargs = {'-O4'} if bflag('quiet','q') then holler = function() end end if bflag('asan','s') then linkargs[#linkargs+1] = '-fsanitize=address' end if bflag('lsan','S') then linkargs[#linkargs+1] = '-fsanitize=leak' end ADDED render/compose.t Index: render/compose.t ================================================================== --- render/compose.t +++ render/compose.t @@ -0,0 +1,29 @@ +-- vim: ft=terra +local terra +render_compose(co: &lib.srv.convo, edit: &lib.store.post) + var target, tgtlen = co:getv('to') + var form: data.view.compose + if edit == nil then + form = data.view.compose { + content = lib.coalesce(target, ''); + acl = lib.trn(target == nil, 'all', 'mentioned'); -- TODO default acl setting? + handle = co.who.handle; + } + end + var cotxt = form:tostr() defer cotxt:free() + + var doc = data.view.docskel { + instance = co.srv.cfg.instance.ptr; + title = 'compose'; + body = cotxt.ptr; + class = 'compose'; + navlinks = co.navbar.ptr; + } + + var hdrs = array( + lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' } + ) + doc:send(co.con,200,[lib.mem.ptr(lib.http.header)] {ct = 1, ptr = &hdrs[0]}) +end + +return render_compose ADDED render/login.t Index: render/login.t ================================================================== --- render/login.t +++ render/login.t @@ -0,0 +1,62 @@ +-- vim: ft=terra +local terra +login_form(co: &lib.srv.convo, user: &lib.store.actor, creds: &lib.store.credset, msg: &int8) + var doc = data.view.docskel { + instance = co.srv.cfg.instance.ptr; + title = 'instance logon'; + class = 'login'; + navlinks = co.navbar.ptr; + } + + if user == nil then + var form = data.view.login_username { + loginmsg = msg; + } + if form.loginmsg == nil then + form.loginmsg = 'identify yourself for access to this instance.' + end + var formtxt = form:tostr() + doc.body = formtxt.ptr + elseif creds:sz() == 0 then + co:complain(403,'access denied','your host is not eligible to authenticate as this user') + return + elseif creds:sz() == 1 then + if creds.trust() then + -- TODO log in immediately + return + end + + var ch = data.view.login_challenge { + handle = user.handle; + name = lib.coalesce(user.nym, user.handle); + } + if creds.pw() then + ch.challenge = 'enter the password associated with your account' + ch.label = 'password' + ch.method = 'pw' + elseif creds.otp() then + ch.challenge = 'enter a valid one-time password for your account' + ch.label = 'OTP code' + ch.method = 'otp' + elseif creds.challenge() then + ch.challenge = 'sign the challenge token: ...' + ch.label = 'digest' + ch.method = 'challenge' + else + co:complain(500,'login failure','unknown login method') + return + end + + doc.body = ch:tostr().ptr + else + -- pick a method + end + + var hdrs = array( + lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' } + ) + doc:send(co.con,200,[lib.mem.ptr(lib.http.header)] {ct = 1, ptr = &hdrs[0]}) + lib.mem.heapf(doc.body) +end + +return login_form ADDED render/nav.t Index: render/nav.t ================================================================== --- render/nav.t +++ render/nav.t @@ -0,0 +1,16 @@ +-- vim: ft=terra +local terra +render_nav(co: &lib.srv.convo) + var t: lib.str.acc t:init(64) + if co.who ~= nil or co.srv.cfg.pol_sec == lib.srv.secmode.public then + t:lpush('timeline') + end + if co.who ~= nil then + t:lpush('compose profile configure log out') + else + t:lpush('log in') + end + return t:finalize() +end +return render_nav Index: render/profile.t ================================================================== --- render/profile.t +++ render/profile.t @@ -1,18 +1,51 @@ -- vim: ft=terra local terra -render_profile(actor: &lib.store.actor) +render_profile(co: &lib.srv.convo, actor: &lib.store.actor) + var aux: lib.str.acc + var auxp: rawstring + if co.aid ~= 0 and co.who.id == actor.id then + auxp = 'alter' + elseif co.aid ~= 0 then + aux:compose('followchat') + if co.who.rights.powers:affect_users() then + aux:push('control',17) + end + auxp = aux.buf + else + aux:compose('remote follow') + end + var avistr: lib.str.acc if actor.origin == 0 then + avistr:compose('/avi/',actor.handle) + end + var timestr: int8[26] lib.osclock.ctime_r(&actor.knownsince, ×tr[0]) + + var strfbuf: int8[28*4] + var stats = co.srv:actor_stats(actor.id) + var sn_posts = lib.math.decstr_friendly(stats.posts, &strfbuf[ [strfbuf.type.N - 1] ]) + var sn_follows = lib.math.decstr_friendly(stats.follows, sn_posts - 1) + var sn_followers = lib.math.decstr_friendly(stats.followers, sn_follows - 1) + var sn_mutuals = lib.math.decstr_friendly(stats.mutuals, sn_followers - 1) + var profile = data.view.profile { nym = lib.coalesce(actor.nym, actor.handle); - bio = lib.coalesce(actor.bio, "tall, dark, and mysterious"); + bio = lib.coalesce(actor.bio, "tall, dark, and mysterious"); xid = actor.xid; - avatar = "/no-avatars-yet.png"; + avatar = lib.trn(actor.origin == 0, avistr.buf, + lib.coalesce(actor.avatar, '/s/default-avatar.webp')); + + nposts = sn_posts, nfollows = sn_follows; + nfollowers = sn_followers, nmutuals = sn_mutuals; + tweetday = timestr; + timephrase = lib.trn(actor.origin == 0, 'joined', 'known since'); - nposts = '0', nfollows = '0'; - nfollowers = '0', nmutuals = '0'; - tweetday = 'novembuary 67th'; + auxbtn = auxp; } - return profile:tostr() + var ret = profile:tostr() + if actor.origin == 0 then avistr:free() end + if not (co.aid ~= 0 and co.who.id == actor.id) then aux:free() end + return ret end return render_profile Index: render/userpage.t ================================================================== --- render/userpage.t +++ render/userpage.t @@ -5,21 +5,22 @@ if co.aid ~= 0 and co.who.id == actor.id then ti:compose('my profile') else ti:compose('profile :: ', actor.handle) end - var pftxt = lib.render.profile(actor) defer pftxt:free() + var pftxt = lib.render.profile(co,actor) defer pftxt:free() var doc = data.view.docskel { instance = co.srv.cfg.instance.ptr; title = ti.buf; body = pftxt.ptr; class = 'profile'; + navlinks = co.navbar.ptr; } var hdrs = array( lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' } ) doc:send(co.con,200,[lib.mem.ptr(lib.http.header)] {ct = 1, ptr = &hdrs[0]}) end return render_userpage Index: route.t ================================================================== --- route.t +++ route.t @@ -55,10 +55,94 @@ end defer actor:free() lib.render.userpage(co, actor.ptr) end + +terra http.login_form(co: &lib.srv.convo, meth: method.t) + if meth == method.get then + -- request a username + lib.render.login(co, nil, nil, nil) + elseif meth == method.post then + var usn, usnl = co:postv('user') + lib.dbg('got name ',{usn,usnl}) + lib.io.fmt('name len %llu\n',usnl) + var am, aml = co:postv('authmethod') + var chrs, chrsl = co:postv('response') + var cs, authok = co.srv:actor_auth_how(co.peer, usn) + var act = co.srv:actor_fetch_xid([lib.mem.ptr(int8)] { + ptr = usn, ct = usnl + }) + if authok == false then + lib.render.login(co, nil, nil, 'access denied') + return + end + var fakeact = false + var fakeactor: lib.store.actor + if act.ptr == nil then + -- the user is known to us but has not yet claimed an + -- account on the server. create a template for the + -- account that will be created once they log in + fakeact = true + fakeactor = lib.store.actor { + id = 0, handle = usn, nym = usn; + origin = 0, bio = nil; + key = [lib.mem.ptr(uint8)] {ptr=nil, ct=0} + } + act.ct = 1 + act.ptr = &fakeactor + act.ptr.rights = lib.store.rights_default() + end + if am == nil then + -- pick an auth method + lib.render.login(co, act.ptr, &cs, nil) + else var aid: uint64 = 0 + lib.dbg('authentication attempt beginning') + -- attempt login with provided method + if lib.str.ncmp('pw', am, lib.math.biggest(2,aml)) == 0 and chrs ~= nil then + aid = co.srv:actor_auth_pw(co.peer, + [lib.mem.ptr(int8)]{ptr=usn,ct=usnl}, + [lib.mem.ptr(int8)]{ptr=chrs,ct=chrsl}) + elseif lib.str.ncmp('otp', am, lib.math.biggest(2,aml)) == 0 and chrs ~= nil then + lib.dbg('using otp auth') + -- ··· -- + else + lib.dbg('invalid auth method') + end + + lib.io.fmt('login got aid = %llu\n', aid) + -- error out + if aid == 0 then + lib.render.login(co, nil, nil, 'authentication failure') + else + var sesskey: int8[lib.session.maxlen + #lib.session.cookiename + #"=; Path=/" + 1] + do var p = &sesskey[0] + p = lib.str.ncpy(p, [lib.session.cookiename .. '='], [#lib.session.cookiename + 1]) + p = p + lib.session.cookie_gen(co.srv.cfg.secret, aid, lib.osclock.time(nil), p) + lib.dbg('sending cookie',&sesskey[0]) + p = lib.str.ncpy(p, '; Path=/', 9) + end + co:reroute_cookie('/', &sesskey[0]) + end + end + if act.ptr ~= nil and fakeact == false then act:free() end + else + ::wrongmeth:: co:complain(405, 'method not allowed', 'that method is not meaningful for this endpoint') do return end + end + return +end + +terra http.post_compose(co: &lib.srv.convo, meth: method.t) + if meth == method.get then + lib.render.compose(co, nil) + elseif meth == method.post then + if co.who.rights.powers.post() == false then + co:complain(401,'insufficient privileges','you lack the post power and cannot perform this action') return + end + + end +end do local branches = quote end local filename, flen = symbol(&int8), symbol(intptr) local page = symbol(lib.http.page) local send = label() @@ -88,26 +172,67 @@ do return false end ::[send]:: page:send(co.con) return true end end -http.static_content:printpretty() + +terra http.local_avatar(co: &lib.srv.convo, handle: lib.mem.ptr(int8)) + -- TODO retrieve user avatars + co:reroute('/s/default-avatar.webp') +end -- entry points 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 co:complain(404, 'what the hell', 'how did you do that') + return + elseif uri.ct == 1 then -- root + lib.io.fmt('root directory, aid is %llu\n', co.aid) + 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 + goto notfound + end + return 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 http.static_content(co, uri.ptr + 3, uri.ct - 3) then goto notfound end - else + 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.ptr[0]:cmp(lib.str.lit('user')) then http.actor_profile_uid(co, path, meth) else goto notfound end + return end - ::wrongmeth:: co:complain(405, 'method not allowed', 'that method is not meaningful for this path') 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 end Index: schema.sql ================================================================== --- schema.sql +++ schema.sql @@ -1,48 +1,58 @@ \prompt 'domain name: ' domain \prompt 'instance name: ' inst \prompt 'bind to socket: ' bind -\qecho 'by default, parsav tracks rights on its own. you can override this later by replacing the rights table with a view, but you''ll then need to set appropriate rules on the view to allow administrators to modify rights from the web UI, or set the rights-readonly flag in the config table to true. for now, enter the name of an actor who will be granted full rights when she logs in.' -\prompt 'admin actor: ' admin +\qecho 'how locked down should this server be? public = anyone can see public timeline and tweets, private = anyone can see tweets with a link but login required for everything else, lockdown = login required for all activities, isolate = like lockdown but with federation protocols completely disabled' +\prompt 'security mode: ' secmode +\qecho 'should user self-registration be allowed? yes or no' +\prompt 'registration: ' regpol +\qecho 'by default, parsav tracks rights on its own. you can override this later by replacing the rights table with a view, but you''ll then need to set appropriate rules on the view to allow administrators to modify rights from the web UI, or set the rights-readonly flag in the config table to true. for now, enter the name of an actor who will be granted full rights when she logs in and identified as the server owner.' +\prompt 'master actor: ' admin \qecho 'you will need to create an authentication view named parsav_auth mapping your user database to something parsav can understand; see auth.sql for an example.' begin; drop table if exists parsav_config; create table if not exists parsav_config ( - key text primary key, + key text primary key, value text ); insert into parsav_config (key,value) values ('bind',:'bind'), ('domain',:'domain'), ('instance-name',:'inst'), - ('administrator',:'admin'), + ('policy-security',:'secmode'), + ('policy-self-register',:'regpol'), + ('master',:'admin'), ('server-secret', encode( digest(int8send((2^63 * (random()*2 - 1))::bigint), 'sha512'), 'base64')); -- note that valid ids should always > 0, as 0 is reserved for null -- on the client side, vastly simplifying code drop table if exists parsav_servers cascade; create table parsav_servers ( - id bigint primary key default (1+random()*(2^63-1))::bigint, + id bigint primary key default (1+random()*(2^63-1))::bigint, domain text not null, - key bytea + key bytea, + parsav boolean -- whether to use parsav protocol extensions ); + drop table if exists parsav_actors cascade; create table parsav_actors ( - id bigint primary key default (1+random()*(2^63-1))::bigint, - nym text, - handle text not null, -- nym [@handle@origin] - origin bigint references parsav_servers(id) + id bigint primary key default (1+random()*(2^63-1))::bigint, + nym text, + handle text not null, -- nym [@handle@origin] + origin bigint references parsav_servers(id) on delete cascade, -- null origin = local actor - bio text, - rank smallint not null default 0, - quota integer not null default 1000, - key bytea, -- private if localactor; public if remote + bio text, + avataruri text, -- null if local + rank smallint not null default 0, + quota integer not null default 1000, + key bytea, -- private if localactor; public if remote + title text unique (handle,origin) ); drop table if exists parsav_rights cascade; @@ -65,56 +75,119 @@ ('rebrand',true) ) as a; drop table if exists parsav_posts cascade; create table parsav_posts ( - id bigint primary key default (1+random()*(2^63-1))::bigint, - author bigint references parsav_actors(id) + id bigint primary key default (1+random()*(2^63-1))::bigint, + author bigint references parsav_actors(id) on delete cascade, - subject text, - body text, - posted timestamp not null, + subject text, + acl text not null default 'all', -- just store the script raw 🤷 + body text, + posted timestamp not null, discovered timestamp not null, - scope smallint not null, - convo bigint, parent bigint, - circles bigint[], mentions bigint[] + scope smallint not null, + convo bigint, + parent bigint, + circles bigint[], + mentions bigint[] ); drop table if exists parsav_conversations cascade; create table parsav_conversations ( - id bigint primary key default (1+random()*(2^63-1))::bigint, - uri text not null, + id bigint primary key default (1+random()*(2^63-1))::bigint, + uri text not null, discovered timestamp not null, - head bigint references parsav_posts(id) + head bigint references parsav_posts(id) ); drop table if exists parsav_rels cascade; create table parsav_rels ( relator bigint references parsav_actors(id) on delete cascade, -- e.g. follower relatee bigint references parsav_actors(id) - on delete cascade, -- e.g. follower - kind smallint, -- e.g. follow, block, mute + on delete cascade, -- e.g. followed + kind smallint, -- e.g. follow, block, mute primary key (relator, relatee, kind) ); drop table if exists parsav_acts cascade; create table parsav_acts ( - id bigint primary key default (1+random()*(2^63-1))::bigint, - kind text not null, -- like, react, so on - time timestamp not null, - actor bigint references parsav_actors(id) + id bigint primary key default (1+random()*(2^63-1))::bigint, + kind text not null, -- like, react, so on + time timestamp not null default now(), + actor bigint references parsav_actors(id) on delete cascade, subject bigint -- may be post or act, depending on kind ); drop table if exists parsav_log cascade; create table parsav_log ( -- accesses are tracked for security & sending delete acts - id bigint primary key default (1+random()*(2^63-1))::bigint, - time timestamp not null, + id bigint primary key default (1+random()*(2^63-1))::bigint, + time timestamp not null default now(), actor bigint references parsav_actors(id) on delete cascade, - post bigint not null + post bigint not null +); + +drop table if exists parsav_attach cascade; +create table parsav_attach ( + id bigint primary key default (1+random()*(2^63-1))::bigint, + birth timestamp not null default now(), + content bytea not null, + mime text, -- null if unknown, will be reported as x-octet-stream + description text, + parent bigint -- post id, or userid for avatars +); + +drop table if exists parsav_circles cascade; +create table parsav_circles ( + id bigint primary key default (1+random()*(2^63-1))::bigint, + owner bigint not null references parsav_actors(id), + name text not null, + members bigint[] not null default array[], + + unique (owner,name) +); + +drop table if exists parsav_rooms cascade; +create table parsav_rooms ( + id bigint primary key default (1+random()*(2^63-1))::bigint, + origin bigint references parsav_servers(id), + name text not null, + description text not null, + policy smallint not null +); + +drop table if exists parsav_room_members cascade; +create table parsav_room_members ( + room bigint references parsav_rooms(id), + member bigint references parsav_actors(id), + rank smallint not null default 0, + admin boolean not null default false, -- non-admins with rank can only moderate + invite + title text -- admin-granted title like reddit flair +); + +drop table if exists parsav_invites cascade; +create table parsav_invites ( + id bigint primary key default (1+random()*(2^63-1))::bigint, + -- when a user is created from an invite, the invite is deleted and the invite + -- ID becomes the user ID. privileges granted on the invite ID during the invite + -- process are thus inherited by the user + handle text, -- admin can lock invite to specific handle + rank smallint not null default 0, + quota integer not null default 1000 +}; + +drop table if exists parsav_interventions cascade; +create table parsav_interventions ( + id bigint primary key default (1+random()*(2^63-1))::bigint, + issuer bigint references parsav_actors(id) not null, + scope bigint, -- can be null or room for local actions + nature smallint not null, -- silence, suspend, disemvowel, etc + victim bigint not null, -- could potentially target group as well + expire timestamp -- auto-expires if set ); + end; Index: session.t ================================================================== --- session.t +++ session.t @@ -5,10 +5,11 @@ -- encoded using Shorthand. we need functions to generate and parse these local m = { maxlen = lib.math.shorthand.maxlen*3 + 2; maxage = 2 * 60 * 60; -- 2 hours + cookiename = 'auth'; } terra m.cookie_gen(secret: lib.mem.ptr(int8), authid: uint64, time: uint64, out: &int8): intptr var ptr = out ptr = ptr + lib.math.shorthand.gen(authid, ptr) Index: srv.t ================================================================== --- srv.t +++ srv.t @@ -1,13 +1,15 @@ -- vim: ft=terra local util = dofile 'common.lua' - +local secmode = lib.enum { 'public', 'private', 'lockdown', 'isolate' } local struct srv local struct cfgcache { secret: lib.mem.ptr(int8) instance: lib.mem.ptr(int8) overlord: &srv + pol_sec: secmode.t + pol_reg: bool } local struct srv { sources: lib.mem.ptr(lib.store.source) webmgr: lib.net.mg_mgr webcon: &lib.net.mg_connection @@ -70,10 +72,17 @@ srv: &srv con: &lib.net.mg_connection msg: &lib.net.mg_http_message aid: uint64 -- 0 if logged out 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 +-- cache + navbar: lib.mem.ptr(int8) +-- private + varbuf: lib.mem.ptr(int8) + vbofs: &int8 } -- this is unfortunately necessary to work around a terra bug -- it can't seem to handle forward-declarations of structs in C @@ -86,19 +95,43 @@ terra getpeer(con: &lib.net.mg_connection) return [&strucheader](con).peer end 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 } + ) + + var body = data.view.docskel { + instance = self.srv.cfg.instance.ptr; + title = 'rerouting'; + body = 'you are being redirected'; + class = 'error'; + navlinks = ''; + } + + body:send(self.con, 303, [lib.mem.ptr(lib.http.header)] { + ptr = &hdrs[0], ct = [hdrs.type.N] - lib.trn(cookie == nil,1,0) + }) +end + +terra convo:reroute(dest: rawstring) self:reroute_cookie(dest,nil) end + terra convo:complain(code: uint16, title: rawstring, msg: rawstring) var hdrs = array(lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' }) var ti: lib.str.acc ti:compose('error :: ', title) defer ti:free() + var bo: lib.str.acc bo:compose('

error

',msg,'

') defer bo:free() var body = data.view.docskel { instance = self.srv.cfg.instance.ptr; title = ti.buf; - body = msg; + body = bo.buf; class = 'error'; + navlinks = lib.coalesce(self.navbar.ptr, ''); } if body.body == nil then body.body = "i'm sorry, dave. i can't let you do that" end @@ -105,17 +138,65 @@ body:send(self.con, code, [lib.mem.ptr(lib.http.header)] { ptr = &hdrs[0], ct = [hdrs.type.N] }) 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 + end + var o = lib.net.mg_http_get_var(&self.msg.body, name, self.vbofs, self.varbuf.ct - (self.vbofs - self.varbuf.ptr)) + if o > 0 then + var r = self.vbofs + self.vbofs = self.vbofs + o + return r, o + else return nil, 0 end +end + +terra convo:getv(name: rawstring) + if self.varbuf.ptr == nil then + self.varbuf = lib.mem.heapa(int8, self.msg.query.len + self.msg.body.len) + self.vbofs = self.varbuf.ptr + end + var o = lib.net.mg_http_get_var(&self.msg.query, name, self.vbofs, self.varbuf.ct - (self.vbofs - self.varbuf.ptr)) + if o > 0 then + var r = self.vbofs + self.vbofs = self.vbofs + o + return r, o + else return nil, 0 end +end local urimatch = macro(function(uri, ptn) return `lib.net.mg_globmatch(ptn, [#ptn], uri.ptr, uri.ct+1) end) local route = {} -- these are defined in route.t, as they need access to renderers terra route.dispatch_http :: {&convo, lib.mem.ptr(int8), lib.http.method.t} -> {} + +local mimetypes = { + {'html', 'text/html'}; + {'json', 'application/json'}; + {'mkdown', 'text/markdown'}; + {'text', 'text/plain'}; + {'ansi', 'text/x-ansi'}; +} + +local mimevar = symbol(lib.mem.ref(int8)) +local mimeneg = `lib.http.mime.none + +for i, t in ipairs(mimetypes) do + local name, mime = t[1], t[2] + mimeneg = quote + var ret: lib.http.mime.t + if lib.str.ncmp(mimevar.ptr, mime, lib.math.biggest(mimevar.ct, [#mime])) == 0 then + ret = [lib.http.mime[name]] + 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) var mgpeer = getpeer(con) @@ -130,18 +211,64 @@ -- little i can do about this -- it always reports a peer v4 IP -- of 0.0.0.0, altho the port seems to come through correctly. -- for now i'm leaving it as is, but note that netmask restrictions -- WILL NOT WORK until upstream gets its shit together. FIXME + -- 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 case lib.net.MG_EV_HTTP_MSG then lib.dbg('routing HTTP request') var msg = [&lib.net.mg_http_message](p) var co = convo { con = con, srv = server, msg = msg; - aid = 0, who = nil; - } + aid = 0, who = nil, peer = peer; + reqtype = lib.http.mime.none; + } co.varbuf.ptr = nil + co.navbar.ptr = nil + + -- 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 + 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] + if t ~= lib.http.mime.none then + co.reqtype = t + goto foundtype + end + + if acc.ptr[i] == @';' then -- fast-forward over q + for j=i+1,acc.ct do i=j + if acc.ptr[j] == @',' then break end + end + end + + while i < acc.ct and -- fast-forward over ws + acc.ptr[i+1] == @' ' or + acc.ptr[i+1] == @'\t' + do i=i+1 end + + mimevar.ptr = acc.ptr + i + 1 + end + i=i+1 + end + if co.reqtype == lib.http.mime.none then + mimevar.ct = acc.ct - (mimevar.ptr - acc.ptr) + co.reqtype = [mimeneg] + if co.reqtype == lib.http.mime.none then + co.reqtype = lib.http.mime.html + end + end + else co.reqtype = lib.http.mime.html end + ::foundtype::end -- 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') @@ -160,11 +287,11 @@ end i = i + 1 else if cookies[i] == @';' then val.ct = (cookies + i) - val.ptr - if lib.str.ncmp(key.ptr, 'auth', key.ct) == 0 then + if lib.str.ncmp(key.ptr, lib.session.cookiename, lib.math.biggest([#lib.session.cookiename],key.ct)) == 0 then goto foundcookie end i = i + 1 i = lib.str.ffw(cookies + i, cookies_p.ct - i) - cookies @@ -173,11 +300,11 @@ else i = i + 1 end end end if val.ptr == nil then goto nocookie end val.ct = (cookies + i) - val.ptr - if lib.str.ncmp(key.ptr, 'auth', key.ct) ~= 0 then + if lib.str.ncmp(key.ptr, lib.session.cookiename, lib.math.biggest([#lib.session.cookiename], key.ct)) ~= 0 then goto nocookie end ::foundcookie:: do var aid = lib.session.cookie_interpret(server.cfg.secret, [lib.mem.ptr(int8)]{ptr=val.ptr,ct=val.ct}, @@ -206,15 +333,23 @@ 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 route.dispatch_http(&co, uri, [lib.http.method.get]) + elseif lib.str.ncmp('POST', msg.method.ptr, msg.method.len) == 0 then + route.dispatch_http(&co, uri, [lib.http.method.post]) + elseif lib.str.ncmp('HEAD', msg.method.ptr, msg.method.len) == 0 then + route.dispatch_http(&co, uri, [lib.http.method.head]) + elseif lib.str.ncmp('OPTIONS', msg.method.ptr, msg.method.len) == 0 then + route.dispatch_http(&co, uri, [lib.http.method.options]) else co:complain(400,'unknown method','you have submitted an invalid http request') end if co.aid ~= 0 then lib.mem.heapf(co.who) end + if co.varbuf.ptr ~= nil then co.varbuf:free() end + if co.navbar.ptr ~= nil then co.navbar:free() end end end end; } @@ -294,18 +429,37 @@ else s.sources.ptr = nil s.sources.ct = 0 end end + +terra srv:actor_stats(uid: uint64) + var stats = lib.store.actor_stats { + posts = 0, mutuals = 0; + follows = 0, followers = 0; + } + for i=0,self.sources.ct do + var s = self.sources.ptr[i]:actor_stats(uid) + stats.posts = stats.posts + s.posts + stats.mutuals = stats.mutuals + s.mutuals + stats.followers = stats.followers + s.followers + stats.follows = stats.follows + s.follows + end + return stats +end terra srv:actor_auth_how(ip: lib.store.inet, usn: rawstring) var cs: lib.store.credset cs:clear() + var ok = false for i=0,self.sources.ct do - var set: lib.store.credset = self.sources.ptr[i]:actor_auth_how(ip, usn) - cs = cs + set + var set, iok = self.sources.ptr[i]:actor_auth_how(ip, usn) + if iok then + cs = cs + set + ok = iok + end end - return cs + return cs, ok end terra cfgcache.methods.load :: {&cfgcache} -> {} terra cfgcache:init(o: &srv) self.overlord = o @@ -338,17 +492,10 @@ lib.report('binding to ', bind) lib.net.mg_mgr_init(&self.webmgr) self.webcon = lib.net.mg_http_listen(&self.webmgr, bind, handle.http, self) - var buf: int8[lib.session.maxlen] - var len = lib.session.cookie_gen(self.cfg.secret, 9139084444658983115ULL, lib.osclock.time(nil), &buf[0]) - buf[len] = 0 - - var authid = lib.session.cookie_interpret(self.cfg.secret, [lib.mem.ptr(int8)] {ptr=buf, ct=len}, lib.osclock.time(nil)) - lib.io.fmt('generated cookie %s -- got authid %llu\n', buf, authid) - if dbbind.ptr ~= nil then dbbind:free() end end srv.methods.poll = terra(self: &srv) lib.net.mg_mgr_poll(&self.webmgr,1000) @@ -364,12 +511,38 @@ end terra cfgcache:load() self.instance = self.overlord:conf_get('instance-name') self.secret = self.overlord:conf_get('server-secret') + + self.pol_reg = false + var sreg = self.overlord:conf_get('policy-self-register') + if sreg.ptr ~= nil then + if lib.str.cmp(sreg.ptr, 'on') == 0 + then self.pol_reg = true + else self.pol_reg = false + end + end + sreg:free() + + 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 + self.pol_sec = secmode.public + elseif lib.str.cmp(smode.ptr, 'private') == 0 then + self.pol_sec = secmode.private + 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 + end + smode:free() end return { overlord = srv; convo = convo; route = route; + secmode = secmode; } ADDED static/default-avatar.svg Index: static/default-avatar.svg ================================================================== --- static/default-avatar.svg +++ static/default-avatar.svg @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + ADDED static/padlock.svg Index: static/padlock.svg ================================================================== --- static/padlock.svg +++ static/padlock.svg @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + Index: static/style.scss ================================================================== --- static/style.scss +++ static/style.scss @@ -0,0 +1,421 @@ +$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) { @return adjust-color($color, $lightness: $pct) } + +body { + @extend %sans; + background-color: adjust-color($color, $lightness: -55%); + color: adjust-color($color, $lightness: 25%); + font-size: 14pt; + margin: 0; + padding: 0; +} +a[href] { + color: adjust-color($color, $lightness: 10%); + &:hover { + color: white; + text-shadow: 0 0 15px adjust-color($color, $lightness: 20%); + } +} +a[href^="//"], +a[href^="http://"], +a[href^="https://"] { // external link + &:hover::after { + color: black; + background-color: white; + } + &::after { + content: "↗"; + display: inline-block; + color: black; + margin-left: 4pt; + background-color: adjust-color($color, $lightness: 10%); + padding: 0 4px; + text-shadow: none; + padding-right: 5px; + vertical-align: baseline; + font-size: 80%; + } +} + +%content { + width: 8in; + margin: auto; +} + +%glow { + box-shadow: 0 0 20px adjust-color($color, $alpha: -0.8); +} + +%button { + @extend %sans; + font-size: 14pt; + padding: 0.1in 0.2in; + border: 1px solid black; + color: adjust-color($color, $lightness: 25%); + text-shadow: 1px 1px black; + text-decoration: none; + text-align: center; + background: linear-gradient(to bottom, + adjust-color($color, $lightness: -45%), + adjust-color($color, $lightness: -50%) 15%, + adjust-color($color, $lightness: -50%) 75%, + adjust-color($color, $lightness: -55%) + ); + &:hover, &:focus { + @extend %glow; + outline: none; + color: adjust-color($color, $lightness: -55%); + text-shadow: none; + background: linear-gradient(to bottom, + adjust-color($color, $lightness: -25%), + adjust-color($color, $lightness: -30%) 15%, + adjust-color($color, $lightness: -30%) 75%, + adjust-color($color, $lightness: -35%) + ); + } + &:active { + color: black; + padding-bottom: calc(0.1in - 2px); + padding-top: calc(0.1in + 2px); + background: linear-gradient(to top, + adjust-color($color, $lightness: -25%), + adjust-color($color, $lightness: -30%) 15%, + adjust-color($color, $lightness: -30%) 75%, + adjust-color($color, $lightness: -35%) + ); + } +} + +button { @extend %button; + &:first-of-type { + @extend %button; + color: white; + box-shadow: inset 0 1px adjust-color($color, $lightness: -25%), + inset 0 -1px adjust-color($color, $lightness: -50%); + background: linear-gradient(to bottom, + adjust-color($color, $lightness: -35%), + adjust-color($color, $lightness: -40%) 15%, + adjust-color($color, $lightness: -40%) 75%, + adjust-color($color, $lightness: -45%) + ); + &:hover, &:focus { + box-shadow: inset 0 1px adjust-color($color, $lightness: -15%), + inset 0 -1px adjust-color($color, $lightness: -40%); + } + &:active { + box-shadow: inset 0 1px adjust-color($color, $lightness: -50%), + inset 0 -1px adjust-color($color, $lightness: -25%); + background: linear-gradient(to top, + adjust-color($color, $lightness: -30%), + adjust-color($color, $lightness: -35%) 15%, + adjust-color($color, $lightness: -35%) 75%, + adjust-color($color, $lightness: -40%) + ); + } + } + &:hover { font-weight: bold; } +} + +$grad-ui-focus: linear-gradient(to bottom, + adjust-color($color, $lightness: -50%), + adjust-color($color, $lightness: -35%) +); + +input[type='text'], input[type='password'], textarea { + @extend %serif; + padding: 0.08in 0.1in; + border: 1px solid black; + background: linear-gradient(to bottom, + adjust-color($color, $lightness: -55%), + adjust-color($color, $lightness: -40%) + ); + font-size: 16pt; + color: adjust-color($color, $lightness: 25%); + box-shadow: inset 0 0 20px -3px adjust-color($color, $lightness: -55%); + &:focus { + color: white; + border-image: linear-gradient(to bottom, + adjust-color($color, $lightness: -10%), + adjust-color($color, $lightness: -30%) + ) 1 / 1px; + background: $grad-ui-focus; + outline: none; + @extend %glow; + } +} + +@mixin glass { + @supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) { + backdrop-filter: blur(40px); + -webkit-backdrop-filter: blur(40px); + background-color: adjust-color($color, $lightness: -53%, $alpha: -0.7); + } + @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { + background-color: adjust-color($color, $lightness: -53%, $alpha: -0.1); + } +} + +header { + position: fixed; + height: min-content; + width: 100vw; + margin: 0; + padding: 0; + border-bottom: 1px solid black; + z-index: 1; + @include glass; + background: linear-gradient(to bottom, transparent, rgba(0,0,0,0.3)); + > div { + position: relative; + max-width: 10in; + margin: auto; + + display: grid; + grid-template-columns: 1fr max-content; + grid-template-rows: 1fr; + h1 { + all: unset; + display: block; + font-size: 1.4em; + padding: 0.25in 0; + text-shadow: 2px 2px 1px black; + grid-column: 1/2; grid-row: 1/2; + } + nav { + all: unset; + display: flex; + justify-content: flex-end; + align-items: center; + grid-column: 2/3; grid-row: 1/2; + > a[href] { + display: block; + padding: 0.25in 0.15in; + //padding: calc((25% - 1em)/2) 0.15in; + &, &::after { transition: 0.3s; } + text-shadow: 1px 1px 1px black; + &:hover{ + transform: scale(120%); + } + } + } + } +} + +main { + @extend %content; + display: block; + position: relative; + min-height: calc(100vh - 1.1in); + margin-top: 0; + margin-bottom: 0; + padding: 0 0.4in; + padding-top: 1.1in; + background-color: adjust-color($color, $lightness: -45%, $alpha: 0.4); + border: { + left: 1px solid black; + right: 1px solid black; + } +} + +div.profile { + @extend %box; + padding: 0.1in; + position: relative; + display: grid; + grid-template-columns: 2fr 1fr; + grid-template-rows: 1fr 1fr; + width: 100%; + > .banner { + grid-column: 1 / 3; + grid-row: 1 / 2; + display: grid; + grid-template-columns: 1.1in 1fr; + grid-template-rows: 0.3in 1fr; + > .avatar { + display: block; + width: 1in; height: 1in; + grid-column: 1 / 2; + grid-row: 1 / 3; + border: 1px solid black; + } + > .id { + grid-column: 2 / 3; + grid-row: 1 / 2; + color: adjust-color($color, $lightness: 25%, $alpha: -0.4); + > .nym { + font-weight: bold; + color: adjust-color($color, $lightness: 25%); + } + > .xid { + color: adjust-color($color, $lightness: 20%, $alpha: -0.1); + font-size: 80%; + vertical-align: text-top; + } + } + > .bio { + grid-column: 2 / 3; + grid-row: 2 / 3; + } + } + > .stats { + grid-column: 3 / 4; + grid-row: 1 / 3; + } + > .menu { + grid-column: 1 / 3; + grid-row: 2 / 3; + display: flex; + justify-content: center; + align-items: center; + > a[href] { + @extend %button; + display: block; + margin: 0 0.05in; + } + > hr { + all: unset; + display: block; + height: 0.3in; + width: 1px; + border-left: 1px solid rgba(0,0,0,0.6); + } + } +} + +%box { + margin: auto; + border: 1px solid adjust-color($color, $lightness: -55%); + border-bottom: 3px solid black; + box-shadow: 0 0 1px black; + border-image: linear-gradient(to bottom, + adjust-color($color, $lightness: -40%), + adjust-color($color, $lightness: -52%) 10%, + adjust-color($color, $lightness: -55%) 90%, + adjust-color($color, $lightness: -60%) + ) 1 / 1px; + background: linear-gradient(to bottom, + adjust-color($color, $lightness: -58%), + adjust-color($color, $lightness: -55%) 10%, + adjust-color($color, $lightness: -50%) 80%, + adjust-color($color, $lightness: -45%) + ); + // outline: 1px solid black; +} + +body.error .message { + @extend %box; + width: 4in; + margin:auto; + padding: 0.5in; + text-align: center; +} + +div.login { + @extend %box; + width: 4in; + padding: 0.4in; + > .msg { + text-align: center; + padding: 0.3in; + } + > .msg:first-child { padding-top: 0; } + > .user { + width: min-content; margin: auto; + background: adjust-color($color, $lightness: -20%, $alpha: -0.3); + border: 1px solid black; + color: adjust-color($color, $lightness: -50%); + padding: 0.1in; + > img { width: 1in; height: 1in; border: 1px solid black; } + > .name { @extend %serif; text-align: center; font-size: 130%; font-weight: bold; margin-top: 0.08in; } + } + >form { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: 1.2em 1fr 1fr; + grid-gap: 5px; + > label, input, button { display: block; } + > label { grid-column: 1 / 3; grid-row: 1/2; font-weight: bold } + > input { grid-column: 1 / 3; grid-row: 2/3; } + > button { grid-column: 2 / 3; grid-row: 3/4; } + > a { @extend %button; grid-column: 1 / 2; grid-row: 3/4; } + } +} + +form.compose { + @extend %box; + display: grid; + grid-template-columns: 1.1in 2fr min-content 1fr; + grid-template-rows: 1fr min-content; + grid-gap: 2px; + padding: 0.1in; + > img { grid-column: 1/2; grid-row: 1/3; width: 1in; height: 1in;} + > textarea { grid-column: 2/5; grid-row: 1/2; height: 3in;} + > input[name="acl"] { grid-column: 2/3; grid-row: 2/3; } + > button { grid-column: 4/5; grid-row: 2/3; } + a.help[href] { margin-right: 0.05in } +} + +a.help[href] { + display: block; + text-align: center; + padding: 0.09in 0.2in; + background: tone(-40%); + border: 1px solid black; + font-weight: bold; + text-decoration: none; + cursor: help; +} + +input.acl { + @extend %teletype; + background: url(/s/padlock.webp) no-repeat; + background-size: 20pt; + background-position: 0.05in 50%; + &:focus { + background: url(/s/padlock.webp) no-repeat, $grad-ui-focus; + background-size: 20pt; + background-position: 0.05in 50%; + }; + padding-left: 0.40in; +} + +div.modal { + @extend %box; + position: fixed; + display: none; + left: 0; right: 0; bottom: 0; top: 0; + max-width: 7in; + margin: 1in auto; + padding: 0.2in 0.3in; + &:target { display: block; } + box-shadow: 0 0 4in 5in rgba(0,0,0,0.5); + z-index: 2; + > div { + height: 100%; + overflow-y: scroll; + >p:first-of-type { margin-top: 0; } + } + >a[href="#0"] { // close link + @extend %button; + cursor: default; + display: block; + position: absolute; + top: -0.3in; + right: 0.1in; + margin: 0.1in; + padding: 0.1in; + &:hover { font-weight: bold; } + } +} + +code { + @extend %teletype; + background: adjust-color($color, $lightness: -50%); + border: 1px solid adjust-color($color, $lightness: -20%); + padding: 2px 6px; + text-shadow: 2px 2px black; +} ADDED static/warn.svg Index: static/warn.svg ================================================================== --- static/warn.svg +++ static/warn.svg @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + Index: store.t ================================================================== --- store.t +++ store.t @@ -14,12 +14,27 @@ credset = lib.set { 'pw', 'otp', 'challenge', 'trust' }; privset = lib.set { 'post', 'edit', 'acct', 'upload', 'censor', 'admin' + }; + powerset = lib.set { + -- user powers -- default on + 'login', 'visible', 'post', 'shout', + 'propagate', 'upload', 'acct', 'edit'; + + -- admin powers -- default off + 'purge', 'config', 'censor', 'suspend', + 'cred', 'elevate', 'demote', 'rebrand' -- modify site's brand identity } } + +terra m.powerset:affect_users() + return self.purge() or self.censor() or self.suspend() or + self.elevate() or self.demote() or self.rebrand() or + self.cred() +end local str = rawstring --lib.mem.ptr(int8) struct m.source @@ -26,53 +41,48 @@ struct m.rights { rank: uint16 -- lower = more powerful except 0 = regular user -- creating staff automatically assigns rank immediately below you quota: uint32 -- # of allowed tweets per day; 0 = no limit - -- user powers -- default on - login: bool - visible: bool - post: bool - shout: bool - propagate: bool - upload: bool - acct: bool - edit: bool - - -- admin powers -- default off - ban: bool - config: bool - censor: bool - suspend: bool - rebrand: bool -- modify site's brand identity + powers: m.powerset } terra m.rights_default() - return m.rights { - rank = 0, quota = 1000; - - login = true, visible = true, post = true; - shout = true, propagate = true, upload = true; - - ban = false, config = false, censor = false; - suspend = false, rebrand = false; - } + var pow: m.powerset pow:fill() + (pow.purge << false) + (pow.config << false) + (pow.censor << false) + (pow.suspend << false) + (pow.elevate << false) + (pow.demote << false) + (pow.cred << false) + (pow.rebrand << false) + return m.rights { rank = 0, quota = 1000, powers = pow; } end struct m.actor { id: uint64 nym: str handle: str origin: uint64 bio: str + avatar: str + knownsince: int64 rights: m.rights key: lib.mem.ptr(uint8) +-- ephemera xid: str - source: &m.source } + +struct m.actor_stats { + posts: intptr + follows: intptr + followers: intptr + mutuals: intptr +} struct m.range { time: bool union { from_time: m.timepoint @@ -167,18 +177,19 @@ actor_fetch_xid: {&m.source, lib.mem.ptr(int8)} -> lib.mem.ptr(m.actor) actor_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.actor) actor_notif_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.notif) actor_enum: {&m.source} -> lib.mem.ptr(&m.actor) actor_enum_local: {&m.source} -> lib.mem.ptr(&m.actor) + actor_stats: {&m.source, uint64} -> m.actor_stats - actor_auth_how: {&m.source, m.inet, rawstring} -> m.credset + actor_auth_how: {&m.source, m.inet, rawstring} -> {m.credset, bool} -- returns a set of auth method categories that are available for a -- given user from a certain origin -- origin: inet - -- handle: rawstring + -- username: rawstring actor_auth_otp: {&m.source, m.inet, rawstring, rawstring} -> uint64 - actor_auth_pw: {&m.source, m.inet, rawstring, rawstring} -> uint64 + actor_auth_pw: {&m.source, m.inet, lib.mem.ptr(int8), lib.mem.ptr(int8) } -> uint64 -- handles password-based logins against hashed passwords -- origin: inet -- handle: rawstring -- token: rawstring actor_auth_tls: {&m.source, m.inet, rawstring} -> uint64 @@ -221,9 +232,9 @@ self.string:free() end m.source.metamethods.__methodmissing = macro(function(meth, obj, ...) local q = {...} -- syntax sugar to forward unrecognized calls onto the backend - return `obj.backend.[meth](&obj, [q]) + return quote var r = obj.backend.[meth](&obj, [q]) in r end end) return m Index: str.t ================================================================== --- str.t +++ str.t @@ -15,15 +15,36 @@ bfmt = terralib.externfunction('sprintf', terralib.types.funcpointer({rawstring,rawstring},{int},true)); span = terralib.externfunction('strspn',{rawstring, rawstring} -> rawstring); } -(lib.mem.ptr(int8)).metamethods.__cast = function(from,to,e) - if from == &int8 then - return `[lib.mem.ptr(int8)]{ptr = e, ct = m.sz(e)} - elseif to == &int8 then - return e.ptr +do local strptr = (lib.mem.ptr(int8)) + local byteptr = (lib.mem.ptr(uint8)) + strptr.metamethods.__cast = function(from,to,e) + if from == &int8 then + return `strptr {ptr = e, ct = m.sz(e)} + elseif to == &int8 then + return e.ptr + end + end + + terra strptr:cmp(other: strptr) + var sz = lib.math.biggest(self.ct, other.ct) + for i = 0, sz do + if self.ptr[i] == 0 and other.ptr[i] == 0 then return true end + if self.ptr[i] ~= other.ptr[i] then return false end + end + return true + end + + terra byteptr:cmp(other: byteptr) + var sz = lib.math.biggest(self.ct, other.ct) + for i = 0, sz do + if self.ptr[i] == 0 and other.ptr[i] == 0 then return true end + if self.ptr[i] ~= other.ptr[i] then return false end + end + return true end end struct m.acc { buf: rawstring @@ -67,10 +88,19 @@ pt.ct = self.sz self.buf = nil self.sz = 0 return pt end; + +terra m.acc:cue(sz: intptr) + if sz <= self.run then return end + self.run = sz + if self.space - self.sz < self.run then + self.space = self.sz + self.run + self.buf = [rawstring](lib.mem.heapr_raw(self.buf, self.space)) + end +end terra m.acc:push(str: rawstring, len: intptr) --var llen = len if str == nil then return self end --if str[len - 1] == 0xA then llen = llen - 1 end -- don't display newlines in debug output @@ -89,10 +119,12 @@ m.lit = macro(function(str) return `[lib.mem.ref(int8)] {ptr = [str:asvalue()], ct = [#(str:asvalue())]} end) +m.acc.methods.lpush = macro(function(self,str) + return `self:push([str:asvalue()], [#(str:asvalue())]) end) m.acc.methods.ppush = terra(self: &m.acc, str: lib.mem.ptr(int8)) self:push(str.ptr, str.ct) return self end; m.acc.methods.merge = terra(self: &m.acc, str: lib.mem.ptr(int8)) self:push(str.ptr, str.ct) str:free() return self end; m.acc.methods.compose = macro(function(self, ...) Index: tpl.t ================================================================== --- tpl.t +++ tpl.t @@ -29,10 +29,11 @@ -- strip out all irrelevant whitespace to tidy things up -- TODO: find way to exclude
 tags?
 	str = str:gsub('[\n^]%s+','')
 	str = str:gsub('%s+[\n$]','')
 	str = str:gsub('\n','')
+	str = str:gsub(' 
+	
+	
+	
+		?
+	
+
+
+

Index: view/docskel.tpl
==================================================================
--- view/docskel.tpl
+++ view/docskel.tpl
@@ -3,9 +3,17 @@
 	
 		@instance :: @title
 		
 	
 	
-		

@title

- @body +
+

@title

+ +
+
+ @body +
Index: view/load.lua ================================================================== --- view/load.lua +++ view/load.lua @@ -5,10 +5,13 @@ local path = ... local sources = { 'docskel'; 'tweet'; 'profile'; + 'compose'; + 'login-username'; + 'login-challenge'; } local ingest = function(filename) local hnd = io.open(path..'/'..filename) local txt = hnd:read('*a') @@ -20,7 +23,7 @@ return txt end local views = {} -for _,n in pairs(sources) do views[n] = ingest(n .. '.tpl') end +for _,n in pairs(sources) do views[n:gsub('-','_')] = ingest(n .. '.tpl') end return views ADDED view/login-challenge.tpl Index: view/login-challenge.tpl ================================================================== --- view/login-challenge.tpl +++ view/login-challenge.tpl @@ -0,0 +1,14 @@ + ADDED view/login-username.tpl Index: view/login-username.tpl ================================================================== --- view/login-username.tpl +++ view/login-username.tpl @@ -0,0 +1,8 @@ + Index: view/profile.tpl ================================================================== --- view/profile.tpl +++ view/profile.tpl @@ -1,22 +1,23 @@
- +
posts @nposts
following @nfollows
followers @nfollowers
mutuals @nmutuals
account created @tweetday
@timephrase @tweetday