parsav  Check-in [26937ca853]

Overview
Comment:first steps towards litepub support
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 26937ca853a6373e44a47f9114b99bca0d1dd75b60969185903da51dc50bc273
User & Date: lexi on 2021-01-25 12:40:08
Other Links: manifest | tags
Context
2021-01-25
12:41
commit missing file check-in: bd5794c0cc user: lexi tags: trunk
12:40
first steps towards litepub support check-in: 26937ca853 user: lexi tags: trunk
2021-01-24
23:18
enable webfinger check-in: 64ae6724c2 user: lexi tags: trunk
Changes

Added api/lp/actor.t version [b336ed6430].

            1  +-- vim: ft=terra
            2  +local tpl = lib.tpl.mk {
            3  +	sigil = '%';
            4  +	body = [[{
            5  +		"@context": "https://%+domain/s/litepub.jsonld",
            6  +		"type": "Person",
            7  +		"id": "%lpid",
            8  +		"url": "https://%+domain/@%+handle",
            9  +		"preferredUsername": %$handle,
           10  +		"name": %$nym,
           11  +		"summary": %$desc,
           12  +		"alsoKnownAs": ["https://%+domain/@%+handle"],
           13  +		"publicKey": {
           14  +			"id": "%lpid#ident-rsa",
           15  +			"owner": "%lpid",
           16  +			"publicKeyPem": %rsa
           17  +		},
           18  +		"icon": {
           19  +			"type": "Image",
           20  +			"url": "https://%+domain%+avi"
           21  +		},
           22  +		"capabilities": { "acceptsChatMessages": false },
           23  +		"discoverable": true,
           24  +		"manuallyApprovedFollowers": %locked,
           25  +		"inbox": "https://%+domain/api/lp/inbox/user/%uid",
           26  +		"outbox": "https://%+domain/api/lp/outbox/user/%uid",
           27  +		"followers": "https://%+domain/api/lp/rel/%uid/followers",
           28  +		"following": "https://%+domain/api/lp/rel/%uid/following"
           29  +	}]];
           30  +}
           31  +
           32  +local pstr = lib.str.t
           33  +terra cs(s: rawstring) return pstr {s, lib.str.sz(s)} end
           34  +
           35  +local terra 
           36  +api_lp_actor(co: &lib.srv.convo, actor: &lib.store.actor)
           37  +	var lpid = co:stra(64)
           38  +	lpid:lpush'https://':ppush(co.srv.cfg.domain):lpush'/user/':shpush(actor.id)
           39  +	var uid = co:stra(32) uid:shpush(actor.id) -- dumb hack bc lazy FIXME
           40  +
           41  +	var body = tpl {
           42  +		domain = co.srv.cfg.domain;
           43  +		uid = uid:finalize();
           44  +		lpid = lpid:finalize();
           45  +		handle = cs(actor.handle);
           46  +		nym = cs(actor.nym);
           47  +		desc = cs(actor.bio);
           48  +		avi = cs(actor.avatar);
           49  +		rsa = '';
           50  +		locked = 'false';
           51  +	}
           52  +
           53  +	co:json(body:poolstr(&co.srv.pool))
           54  +end
           55  +return api_lp_actor

Added api/lp/outbox.t version [7ed4c4752c].

            1  +-- vim: ft=terra
            2  +local pstr = lib.str.t
            3  +
            4  +local terra 
            5  +lp_outbox(co: &lib.srv.convo, uri: pstr, here: lib.mem.ptr(lib.str.ref))
            6  +	var path = lib.str.qesc(&co.srv.pool,uri,false)
            7  +	var json = co:stra(512)
            8  +	json:lpush '{"@context": "https://':ppush(co.srv.cfg.domain):lpush'/s/litepub.jsonld","id":"https://'
            9  +	    :ppush(co.srv.cfg.domain):ppush(path)
           10  +		:lpush '"'
           11  +	var at = co:pgetv('at')
           12  +	lib.dbg('api path ',
           13  +	{here(0).ptr,here(0).ct}, ' / ',
           14  +	{here(1).ptr,here(1).ct})
           15  +	json:lpush',"current":"https://':ppush(co.srv.cfg.domain):ppush(path):lpush'?at=top"'
           16  +	if not at then
           17  +		json:lpush',"type":"OrderedCollection","first":"https://':ppush(co.srv.cfg.domain):ppush(path):lpush'?at=top"'
           18  +	else
           19  +		if here(0):cmp 'user' and here.ct > 1 then
           20  +			var uid, uidok = lib.math.shorthand.parse(here(1).ptr, here(1).ct)
           21  +			if not uidok then goto e404 end
           22  +			var user = co.srv:actor_fetch_uid(uid)
           23  +			if not user then goto e404 end
           24  +			var time: lib.store.timepoint
           25  +			if at:cmp('top') then
           26  +				time = lib.osclock.time(nil)
           27  +			else
           28  +				var tp, ok = lib.math.decparse(at)
           29  +				if ok then time = tp end
           30  +			end
           31  +			lib.io.fmt('from-time: %llu\n', time)
           32  +			var posts = co.srv:post_enum_author_uid(uid, lib.store.range {
           33  +				mode = 1; -- time -> idx
           34  +				from_time = time;
           35  +				to_idx = 65;
           36  +			})
           37  +			var oldest = time
           38  +			json:lpush',"partOf":"https://':ppush(co.srv.cfg.domain):ppush(path)
           39  +			    :lpush'","type":"CollectionPage","orderedItems":['
           40  +			if posts.sz > 0 then defer posts:free()
           41  +				for i=0, lib.math.smallest(posts.sz,64) do
           42  +					if i~=0 then json:lpush',' end
           43  +					json:ppush(lib.api.lp.tweet(co,posts(i).ptr,true))
           44  +					oldest = lib.math.smallest(posts(i)().posted, oldest)
           45  +				end
           46  +			end
           47  +			json:lpush'],"totalItems":':ipush(posts.sz)
           48  +			if oldest ~= time and oldest > 0 and posts.sz > 64 then
           49  +				json:lpush',"next":"https://':ppush(co.srv.cfg.domain):ppush(path)
           50  +				    :lpush'?at=':ipush(oldest-1):lpush'"'
           51  +			end
           52  +
           53  +		else goto e404 end -- TODO
           54  +	end
           55  +	json:lpush[[}]]
           56  +	co:json(json:finalize())
           57  +	do return end
           58  +	::e404:: do co:fail(404) return end
           59  +end
           60  +
           61  +return lp_outbox

Added api/lp/tweet.t version [e0805dcd2f].

            1  +-- vim: ft=terra
            2  +local pstr = lib.str.t
            3  +
            4  +local obj = lib.tpl.mk [[{
            5  +	"\@context": "https://@+domain/s/litepub.jsonld",
            6  +	"type": "Note",
            7  +	"id": "https://@+domain/post/@^pid",
            8  +	"content": @$html,
            9  +	"source": @$raw,
           10  +	"attributedTo": "https://@+domain/user/@^uid",
           11  +	"published": "@pubtime"
           12  +	@extra
           13  +}]]
           14  +
           15  +local wrap = lib.tpl.mk [[{
           16  +	"\@context": "https://@+domain/s/litepub.jsonld",
           17  +	"type": "@kind",
           18  +	"actor": "https://@+domain/user/@^uid",
           19  +	"published": "@$pubtime",
           20  +	"id": "https://@+domain/api/lp/act/@^aid",
           21  +	"object": @obj
           22  +}]]
           23  +
           24  +local terra 
           25  +lp_tweet(co: &lib.srv.convo, p: &lib.store.post, act_wrap: bool)
           26  +	
           27  +	var tweet = (obj {
           28  +		domain = co.srv.cfg.domain, uid = p.author, pid = p.id;
           29  +		html = lib.smackdown.html(&co.srv.pool, p.body, false);
           30  +		raw = p.body, pubtime = '', extra = '';
           31  +	}):poolstr(&co.srv.pool)
           32  +
           33  +	if act_wrap then
           34  +		return (wrap {
           35  +			domain = co.srv.cfg.domain;
           36  +			kind = lib.trn(p.rtdby == 0, 'Create', 'Announce');
           37  +			uid = lib.trn(p.rtdby == 0, p.author, p.rtdby);
           38  +			aid = lib.trn(p.rtdby == 0, p.id, p.rtact);
           39  +			pubtime = '', obj = tweet;
           40  +		}):poolstr(&co.srv.pool)
           41  +	else
           42  +		return tweet
           43  +	end
           44  +end
           45  +
           46  +return lp_tweet

Added api/webfinger.t version [c64d390bdc].

            1  +-- vim: ft=terra
            2  +wftpl = lib.tpl.mk [[{
            3  +	"subject": @$subj,
            4  +	"aliases": [ @$href, @$pfp ],
            5  +	"links": [
            6  +		{ "rel": "self", "type": "application/activity+json", "href": @$href },
            7  +		{ "rel": "self",
            8  +			"type": "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", "href": @$href },
            9  +		{ "rel": "http://webfinger.net/rel/profile-page",
           10  +			"type": "text/html", "href": @$pfp }
           11  +	]
           12  +}]]
           13  +
           14  +local terra 
           15  +webfinger(co: &lib.srv.convo)
           16  +	var res = co:pgetv('resource')
           17  +	if (not res) or not res:startswith 'acct:' then goto err end
           18  +	
           19  +	var acct = res + 5
           20  +	var svp = lib.str.find(acct, '@')
           21  +	if svp:ref() then
           22  +		acct.ct = (svp.ptr - acct.ptr)
           23  +		svp:advance(1)
           24  +		if not svp:cmp(co.srv.cfg.domain) then goto err end
           25  +	end
           26  +
           27  +	var actor = co.srv:actor_fetch_xid(acct)
           28  +	if not actor then goto err end
           29  +	do defer actor:free()
           30  +		if actor().origin ~= 0 then goto err end
           31  +		var href = co:stra(64)
           32  +		var pfp = co:stra(64)
           33  +		href:lpush'https://':ppush(co.srv.cfg.domain):lpush'/user/':shpush(actor().id)
           34  +		pfp:lpush'https://':ppush(co.srv.cfg.domain):lpush'/@':ppush(acct)
           35  +
           36  +		var tp = wftpl {
           37  +			subj = res;
           38  +			href = href:finalize();
           39  +			pfp = pfp:finalize();
           40  +		}
           41  +		co:json(tp:poolstr(&co.srv.pool))
           42  +
           43  +		return
           44  +	end
           45  +	-- error conditions
           46  +	::err:: do co:json('{}') return end
           47  +end
           48  +return webfinger

Modified config.lua from [74b801cbf1] to [f9545a425d].

    62     62   		{'icon.svg', 'image/svg+xml'};
    63     63   		{'padlock.svg', 'image/svg+xml'};
    64     64   		{'warn.svg', 'image/svg+xml'};
    65     65   		{'query.webp', 'image/webp'};
    66     66   		{'reply.webp', 'image/webp'};
    67     67   		{'file.webp', 'image/webp'};
    68     68   		{'follow.webp', 'image/webp'};
           69  +
           70  +		{'litepub.jsonld', 'application/ld+json; charset=utf-8'};
    69     71   		-- keep in mind before you add anything to this list: these are not
    70     72   		-- just files parsav can access, they are files that are *kept in
    71     73   		-- memory* for fast access the entire time parsav is running, and
    72     74   		-- which need to be loaded into memory before the program can even
    73     75   		-- start. it's imperative to keep these as small and few in number
    74     76   		-- as is realistically possible.
    75     77   	};

Modified math.t from [12858be43d] to [60b64e2aea].

     1      1   -- vim: ft=terra
     2      2   local m = {
     3      3   	shorthand = {maxlen = 14};
     4         -	ll = {
     5         -		ctpop_u8 = terralib.intrinsic('llvm.ctpop.i8', uint8 -> uint8);
     6         -	};
     7      4   }
            5  +m.shorthand.t = int8[m.shorthand.maxlen]
     8      6   
     9      7   local pstring = lib.mem.ptr(int8)
    10      8   
    11      9   -- swap in place -- faster on little endian
    12     10   m.netswap_ip = macro(function(ty, src, dest)
    13     11   	if ty:astype().type ~= 'integer' then error('bad type') end
    14     12   	local bytes = ty:astype().bytes
................................................................................
    64     62   	var o = n
    65     63   	for i=0,fac do n = n * o end
    66     64   	return n
    67     65   end
    68     66   
    69     67   terra m.shorthand.gen(val: uint64, dest: rawstring): ptrdiff
    70     68   	var lst = "0123456789-ABCDEFGHIJKLMNOPQRSTUVWXYZ:abcdefghijklmnopqrstuvwxyz"
    71         -	var buf: int8[m.shorthand.maxlen]
           69  +	var buf: m.shorthand.t
    72     70   	var ptr = [&int8](buf)
    73     71   	while val ~= 0 do
    74     72   		var v = val % 64
    75     73   		@ptr = lst[v]
    76     74   		ptr = ptr + 1
    77     75   		val = val / 64
    78     76   	end

Modified parsav.t from [dd405c9e82] to [1d36a792dc].

    13     13   			for m in l:gmatch('([^:]+)') do path[#path+1]=m end
    14     14   			local tgt = lib
    15     15   			for i=1,#path-1 do
    16     16   				if tgt[path[i]] == nil then tgt[path[i]] = {} end
    17     17   				tgt = tgt[path[i]]
    18     18   			end
    19     19   			local chunk = terralib.loadfile(l:gsub(':','/') .. '.t')
    20         -			if chunk ~= nil then
           20  +			if chunk ~= nil then 
    21     21   				tgt[path[#path]:gsub('-','_')] = chunk()
    22     22   				print(' \27[1m[ \27[32mok\27[;1m ]\27[m')
    23     23   			else
    24     24   				print(' \27[1m[\27[31mfail\27[;1m]\27[m')
    25     25   				os.exit(2)
    26     26   			end
    27     27   		end
................................................................................
   106    106   	trn = macro(function(cond, i, e)
   107    107   		return quote
   108    108   			var c: bool = [cond]
   109    109   			var r: i.tree.type
   110    110   			if c == true then r = i else r = e end
   111    111   		in r end
   112    112   	end);
          113  +	typeof = macro(function(exp) return exp.tree.type end);
   113    114   	coalesce = macro(function(...)
   114    115   		local args = {...}
   115    116   		local ty = args[1].tree.type
   116    117   		local val = symbol(ty)
   117    118   		local empty
   118    119   		if ty.ptr_basetype then empty = `[ty]{ptr=nil,ct=0}
   119    120   		elseif ty.type == 'integer' then empty = `0
................................................................................
   496    497   
   497    498   	'render:conf:profile';
   498    499   	'render:conf:circles';
   499    500   	'render:conf:sec';
   500    501   	'render:conf:users';
   501    502   	'render:conf:avi';
   502    503   	'render:conf';
          504  +
          505  +	'api:lp:actor';
          506  +	'api:lp:tweet';
          507  +	'api:lp:outbox';
          508  +	'api:webfinger';
   503    509   	'route';
   504    510   }
   505    511   
   506    512   do
   507    513   	local p = string.format('parsav: %s\nbuilt on %s\n', config.build.str, config.build.when)
   508    514   	terra version() lib.io.send(1, p, [#p]) end
   509    515   end

Modified render/profile.t from [c63a8aaea6] to [74b4c77645].

   195    195   	end
   196    196   
   197    197   	if relationship.recip.follow() then
   198    198   		comments:lpush('<li style="--co:30">follows you</li>')
   199    199   	end
   200    200   
   201    201   	var circpanel: lib.str.acc
          202  +	var circstr = pstr.null()
   202    203   	if co.aid ~= 0 then
   203    204   		circpanel = co:stra(128)
   204    205   		var allcircs = co.srv:circle_search(&co.srv.pool, co.who.id, 0)
   205    206   		if allcircs:ref() then
   206    207   			var mycircs = co.srv:circle_memberships_uid(&co.srv.pool, co.who.id, actor.id)
   207    208   			for i=0, allcircs.ct do
   208    209   				circpanel:lpush '<label><input type="checkbox" name="circle" value="'
................................................................................
   215    216   					end
   216    217   				end
   217    218   				circpanel:lpush '> '
   218    219   						 :ppush(allcircs(i).name)
   219    220   						 :lpush '</label>'
   220    221   			end
   221    222   		end
          223  +		circstr = circpanel:finalize()
   222    224   	end
   223    225   
   224    226   	var profile = data.view.profile {
   225    227   		nym = fullname;
   226    228   		bio = bio;
   227    229   		xid = cs(actor.xid);
   228    230   		avatar = cs(actor.avatar);
................................................................................
   231    233   		nfollowers = sn_followers, nmutuals = sn_mutuals;
   232    234   		tweetday = cs(timestr);
   233    235   		timephrase = lib.trn(actor.origin == 0, pstr 'joined', pstr 'known since');
   234    236   
   235    237   		remarks = '';
   236    238   
   237    239   		auxbtn = auxp;
   238         -		circles = circpanel:finalize();
          240  +		circles = circstr;
   239    241   		relations = relbtns:finalize();
   240    242   		sanctions = sancbtns:finalize();
   241    243   	}
   242    244   	if comments.sz > 0 then profile.remarks = comments:finalize() end
   243    245   
   244    246   	var ret = profile:poolstr(&co.srv.pool)
   245    247   	-- auxp:free() 
   246    248   	--if actor.bio ~= nil then bio:free() end
   247    249   	--if comments.sz > 0 then profile.remarks:free() end
   248    250   	return ret
   249    251   end
   250    252   
   251    253   return render_profile

Modified route.t from [325df84c8e] to [db07af2616].

    93     93   	if not go or go(0) ~= @'/' then
    94     94   		lib.render.user_page(co, actor, &rel)
    95     95   	else
    96     96   		co:reroute(go.ptr)
    97     97   	end
    98     98   end
    99     99   
   100         -terra http.actor_profile_xid(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t)
          100  +terra http.actor_dispatch_mime(co: &lib.srv.convo, actor: &lib.store.actor)
          101  +	if co:matchmime(lib.http.mime.html) then
          102  +		http.actor_profile(co,actor,co.method)
          103  +	elseif co:matchmime(lib.http.mime.json) then
          104  +		lib.api.lp.actor(co, actor)
          105  +	else co:fail(406) end
          106  +end
          107  +
          108  +terra http.actor_profile_xid(co: &lib.srv.convo, uri: lib.mem.ptr(int8))
   101    109   	var handle = [lib.mem.ptr(int8)] { ptr = &uri.ptr[2], ct = 0 }
   102    110   	for i=2,uri.ct do
   103    111   		if uri.ptr[i] == @'/' or uri.ptr[i] == 0 then handle.ct = i - 2 break end
   104    112   	end
   105    113   	if handle.ct == 0 then
   106    114   		handle.ct = uri.ct - 2
   107    115   		uri:advance(uri.ct)
................................................................................
   114    122   	var actor = co.srv:actor_fetch_xid(handle)
   115    123   	if actor.ptr == nil then
   116    124   		co:complain(404,'no such user','no such user known to this server')
   117    125   		return
   118    126   	end
   119    127   	defer actor:free()
   120    128   
   121         -	http.actor_profile(co,actor.ptr,meth)
          129  +	http.actor_dispatch_mime(co, actor.ptr)
   122    130   end
   123    131   
   124    132   terra http.actor_profile_uid (
   125    133   	co: &lib.srv.convo,
   126         -	path: lib.mem.ptr(lib.mem.ref(int8)),
   127         -	meth: method.t
          134  +	path: lib.mem.ptr(lib.mem.ref(int8))
   128    135   )
   129    136   	if path.ct < 2 then
   130    137   		co:complain(404,'bad url','invalid user url')
   131    138   		return
   132    139   	end
   133    140   
   134    141   	var uid, ok = lib.math.shorthand.parse(path.ptr[1].ptr, path.ptr[1].ct)
................................................................................
   140    147   	var actor = co.srv:actor_fetch_uid(uid)
   141    148   	if actor.ptr == nil then
   142    149   		co:complain(404, 'no such user', 'no user by that ID is known to this instance')
   143    150   		return
   144    151   	end
   145    152   	defer actor:free()
   146    153   
   147         -	http.actor_profile(co,actor.ptr,meth)
          154  +	http.actor_dispatch_mime(co, actor.ptr)
   148    155   end
   149    156   
   150    157   terra http.login_form(co: &lib.srv.convo, meth: method.t)
   151    158   	if meth_get(meth) then
   152    159   		-- request a username
   153    160   		lib.render.login(co, nil, nil, pstring.null())
   154    161   	elseif meth == method.post then
................................................................................
   225    232   				lib.render.login(co, nil, nil,  'authentication failure')
   226    233   			else
   227    234   				co:installkey('/',aid)
   228    235   			end
   229    236   		end
   230    237   		if act.ptr ~= nil and fakeact == false then act:free() end
   231    238   	else
   232         -		::wrongmeth:: co:complain(405, 'method not allowed', 'that method is not meaningful for this endpoint') do return end
          239  +		::wrongmeth:: co:fail(405) do return end
   233    240   	end
   234    241   	return
   235    242   end
   236    243   
   237    244   terra http.post_compose(co: &lib.srv.convo, meth: method.t)
   238    245   	if not co:assertpow('post') then return end
   239    246   	--if co.who.rights.powers.post() == false then
................................................................................
   242    249   	if meth_get(meth) then
   243    250   		lib.render.compose(co, nil, nil)
   244    251   	elseif meth == method.post then
   245    252   		var text, textlen = co:postv("post")
   246    253   		var acl, acllen = co:postv("acl")
   247    254   		var subj, subjlen = co:postv("subject")
   248    255   		if text == nil or acl == nil then
   249         -			co:complain(405, 'invalid post', 'every post must have at least body text and an ACL')
          256  +			co:complain(400, 'invalid post', 'every post must have at least body text and an ACL')
   250    257   			return
   251    258   		end
   252    259   		if subj == nil then subj = '' end
   253    260   
   254    261   		var p = lib.store.post {
   255    262   			author = co.who.id, acl = acl;
   256    263   			body = text, subject = subj;
................................................................................
   404    411   	end
   405    412   
   406    413   	if not post then goto badurl end
   407    414   
   408    415   	lib.render.tweet_page(co, path, post.ptr)
   409    416   	do return end
   410    417   
   411         -	::badurl:: do co:complain(404, 'invalid URL', 'this URL does not reference extant content or functionality') return end
   412         -	::badop :: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
   413         -	::noauth:: do co:complain(401, 'unauthorized', 'you have not supplied the necessary credentials to perform this operation') return end
          418  +	::noauth:: do co:fail(401) return end
          419  +	::badurl:: do co:fail(404) return end
          420  +	::badop :: do co:fail(405) return end
   414    421   end
   415    422   
   416    423   local terra 
   417    424   credsec_for_uid(co: &lib.srv.convo, uid: uint64)
   418    425   	var act = co:ppostv('act')
   419    426   	if not act then return true end
   420    427   	lib.dbg('handling credential action')
................................................................................
   921    928   	do defer data:free() defer mime:free()
   922    929   		co:bytestream(mime,data)
   923    930   	return end
   924    931   
   925    932   	::e404:: do co:complain(404, 'artifact not found', 'no such artifact has been uploaded to this instance') return end
   926    933   end
   927    934   
   928         -local json = {}
   929         -
   930         -do wftpl = lib.tpl.mk [[{
   931         -		"subject": @$subj,
   932         -		"links": [
   933         -			{ "rel": "self", "type": "application/ld+json", "href": @$href }
   934         -		]
   935         -	}]]
   936         -	terra json.webfinger(co: &lib.srv.convo)
   937         -		var res = co:pgetv('resource')
   938         -		if (not res) or not res:startswith 'acct:' then goto err end
   939         -		
   940         -		-- technically we should look this user up in the database to make sure
   941         -		-- they actually exist, buuut that's costly and i doubt that's actually
   942         -		-- necessary for webfinger to do its job. so we cheat and just do string
   943         -		-- munging so lookups are as cheap as possible. TODO make sure this works
   944         -		-- in practice and doesn't cause any weird security problems
   945         -		var acct = res + 5
   946         -		var svp = lib.str.find(acct, '@')
   947         -		if svp:ref() then
   948         -			acct.ct = (svp.ptr - acct.ptr)
   949         -			svp:advance(1)
   950         -			if not svp:cmp(co.srv.cfg.domain) then goto err end
   951         -		end
   952         -		var tp = wftpl {
   953         -			subj = res;
   954         -			href = co:qstr('https://', co.srv.cfg.domain, '/@', acct);
   955         -		}
   956         -		co:json(tp:poolstr(&co.srv.pool))
   957         -
   958         -		do return end -- error conditions
   959         -		::err:: do co:json('{}') return end
   960         -	end
   961         -end
   962    935   
   963    936   -- entry points
   964         -terra r.dispatch_http(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t)
          937  +terra r.dispatch_http(co: &lib.srv.convo, uri: lib.mem.ptr(int8))
   965    938   	lib.dbg('handling URI of form ', {uri.ptr,uri.ct})
   966    939   	co.navbar = lib.render.nav(co)
   967    940   	-- some routes are non-hierarchical, and can be resolved with a simple strcmp
   968    941   	-- we run through those first before giving up and parsing the URI
          942  +	var meth = co.method -- TODO unfuck this legacy bat shit
   969    943   	if uri.ptr == nil or uri.ptr[0] ~= @'/' then
   970    944   		co:complain(404, 'what the hell', 'how did you do that')
   971    945   	elseif uri.ct == 1 then -- root
   972    946   		if (co.srv.cfg.pol_sec == lib.srv.secmode.private or
   973    947   		   co.srv.cfg.pol_sec == lib.srv.secmode.lockdown) and co.aid == 0 then
   974    948   		   http.login_form(co, meth)
   975    949   		else http.timeline(co, hpath {ptr=nil,ct=0}) end
   976    950   	elseif uri.ptr[1] == @'@' then
   977         -		http.actor_profile_xid(co, uri, meth)
          951  +		http.actor_profile_xid(co, uri)
   978    952   	elseif uri.ptr[1] == @'s' and uri.ptr[2] == @'/' and uri.ct > 3 then
   979    953   		if not meth_get(meth) then goto wrongmeth end
   980    954   		if not http.static_content(co, uri.ptr + 3, uri.ct - 3) then goto notfound end
   981    955   	elseif lib.str.ncmp('/avi/', uri.ptr, 5) == 0 then
   982    956   		http.local_avatar(co, [lib.mem.ptr(int8)] {ptr = uri.ptr + 5, ct = uri.ct - 5})
   983    957   	elseif lib.str.ncmp('/file/', uri.ptr, 6) == 0 then
   984    958   		http.file_serve_raw(co, [lib.mem.ptr(int8)] {ptr = uri.ptr + 6, ct = uri.ct - 6})
................................................................................
   997    971   		if co.aid == 0
   998    972   			then goto notfound
   999    973   			else co:reroute_cookie('/','auth=; Path=/')
  1000    974   		end
  1001    975   	else -- hierarchical routes
  1002    976   		var path = lib.http.hier(&co.srv.pool, uri) --defer path:free()
  1003    977   		if path.ct > 1 and path(0):cmp('user') then
  1004         -			http.actor_profile_uid(co, path, meth)
          978  +			http.actor_profile_uid(co, path)
  1005    979   		elseif path.ct > 1 and path(0):cmp('post') then
  1006    980   			http.tweet_page(co, path, meth)
  1007    981   		elseif path(0):cmp('tl') then
  1008    982   			http.timeline(co, path)
  1009    983   		elseif path(0):cmp('.well-known') then
  1010    984   			if path(1):cmp('webfinger') then
  1011         -				json.webfinger(co)
          985  +				if not co:matchmime(lib.http.mime.json) then goto nacc end
          986  +				lib.api.webfinger(co)
  1012    987   			end
          988  +		elseif path(0):cmp('api') then
          989  +			if path(1):cmp('parsav') then -- native API
          990  +			elseif path(1):cmp('v1') then -- mastodon client api :/
          991  +			elseif path(1):cmp('lp') then -- litepub endpoints
          992  +				if path(2):cmp('outbox') then
          993  +					lib.api.lp.outbox(co,uri,path + 3)
          994  +				elseif path(2):cmp('inbox') then
          995  +				end
          996  +			else goto notfound end
  1013    997   		elseif path(0):cmp('media') then
  1014    998   			if co.aid == 0 then goto unauth end
  1015    999   			http.media_manager(co, path, meth, co.who.id)
  1016   1000   		elseif path(0):cmp('doc') then
  1017   1001   			if not meth_get(meth) then goto wrongmeth end
  1018   1002   			http.documentation(co, path)
  1019   1003   		elseif path(0):cmp('conf') then
  1020   1004   			if co.aid == 0 then goto unauth end
  1021   1005   			http.configure(co,path,meth)
  1022   1006   		else goto notfound end
  1023   1007   	end
  1024   1008   	do return end
  1025   1009   
  1026         -	::wrongmeth:: co:complain(405, 'method not allowed', 'that method is not meaningful for this endpoint') do return end
  1027         -	::notfound:: co:complain(404, 'not found', 'no such resource available') do return end
  1028         -	::unauth:: co:complain(401, 'unauthorized', 'this content is not available at your clearance level') do return end
         1010  +	::wrongmeth:: co:fail(405) do return end
         1011  +	::nacc     :: co:fail(406) do return end
         1012  +	::notfound :: co:fail(404) do return end
         1013  +	::unauth   :: co:fail(401) do return end
  1029   1014   end

Modified srv.t from [557555fd0b] to [68c9cc33d4].

     1      1   -- vim: ft=terra
     2      2   local util = lib.util
     3      3   local secmode = lib.enum { 'public', 'private', 'lockdown', 'isolate' }
     4      4   local pstring = lib.mem.ptr(int8)
            5  +local mimetypes = {
            6  +	{'html', 'text/html'};
            7  +	{'json', 'application/json'};
            8  +	{'json', 'application/activity+json'};
            9  +	{'json', 'application/ld+json'};
           10  +	{'mkdown', 'text/markdown'};
           11  +	{'text', 'text/plain'};
           12  +	{'ansi', 'text/x-ansi'};
           13  +}
           14  +
     5     15   local struct srv
     6     16   local struct cfgcache {
     7     17   	secret: pstring
     8     18   	pol_sec: secmode.t
     9     19   	pol_reg: bool
    10     20   	pol_autoherald: bool
    11     21   	credmgd: bool
................................................................................
   178    188   
   179    189   local usrdefs = {
   180    190   	str = {
   181    191   		['acl-follow'    ] = {cfgfld = 'usrdef_pol_follow', fallback = 'local'};
   182    192   		['acl-follow-req'] = {cfgfld = 'usrdef_pol_follow_req', fallback = 'all'};
   183    193   	};
   184    194   }
          195  +
          196  +terra convo:matchmime(mime: lib.http.mime.t): bool
          197  +	return self.reqtype == [lib.http.mime.none]
          198  +		or self.reqtype == mime
          199  +end
   185    200   
   186    201   terra convo:usercfg_str(uid: uint64, setting: pstring): pstring
   187    202   	var set = self.srv:actor_conf_str_get(&self.srv.pool, uid, setting)
   188    203   	if not set then
   189    204   		[(function()
   190    205   			local q = quote return pstring.null() end
   191    206   			for key, dfl in pairs(usrdefs.str) do
................................................................................
   316    331   	if not lockdown then lockhdr = "" end
   317    332   	lib.net.mg_printf(self.con, "HTTP/1.1 200 OK\r\nContent-Type: %.*s\r\nContent-Length: %llu\r\n%sX-Content-Options: nosniff\r\n\r\n", mime.ct, mime.ptr, data.ct + 2, lockhdr)
   318    333   	lib.net.mg_send(self.con, data.ptr, data.ct)
   319    334   	lib.net.mg_send(self.con, '\r\n', 2)
   320    335   end
   321    336   
   322    337   terra convo:json(data: pstring)
   323         -	self:bytestream_trusted(false, 'application/ld+json', data:blob())
          338  +	self:bytestream_trusted(false, 'application/activity+json; charset=utf-8', data:blob())
   324    339   end
   325    340   
   326    341   terra convo:bytestream(mime: pstring, data: lib.mem.ptr(uint8))
   327    342   	-- TODO this is not a satisfactory solution; it's a bandaid on a gaping
   328    343   	-- chest wound. ultimately we need to compile a whitelist of safe mime
   329    344   	-- types as part of mimelib, but that is no small task. for now, this
   330    345   	-- will keep the patient from immediately bleeding out
................................................................................
   370    385   		p = p + lib.session.cookie_gen(self.srv.cfg.secret, aid, lib.osclock.time(nil), p)
   371    386   		lib.dbg('sending cookie ',{&sesskey[0],15})
   372    387   		p = lib.str.ncpy(p, '; Path=/', 9)
   373    388   	end
   374    389   	self:reroute_cookie(dest, &sesskey[0])
   375    390   end
   376    391    
          392  +terra convo:stra(sz: intptr) -- convenience function
          393  +	var s: lib.str.acc
          394  +	s:pool(&self.srv.pool,sz)
          395  +	return s
          396  +end
          397  +
          398  +convo.methods.qstr = macro(function(self, ...) -- convenience string builder
          399  +	local exp = {...}
          400  +	return `lib.str.acc{}:pcompose(&self.srv.pool, [exp]):finalize()
          401  +end)
          402  +
   377    403   terra convo:complain(code: uint16, title: rawstring, msg: rawstring)
   378    404   	if msg == nil then msg = "i'm sorry, dave. i can't let you do that" end
   379    405   
   380         -	var ti: lib.str.acc ti:compose('error :: ', title)
   381         -	var bo: lib.str.acc bo:compose('<div class="message"><img class="icon" src="/s/warn.svg"><h1>',title,'</h1><p>',msg,'</p></div>')
   382         -	var body = [convo.page] {
   383         -		title = ti:finalize();
   384         -		body = bo:finalize();
   385         -		class = 'error';
   386         -		cache = false;
   387         -	}
   388         -
   389         -	self:statpage(code, body)
   390         -
   391         -	body.title:free()
   392         -	body.body:free()
          406  +	if self:matchmime(lib.http.mime.html) then
          407  +		var body = [convo.page] {
          408  +			title = self:qstr('error :: ', title);
          409  +			body = self:qstr('<div class="message"><img class="icon" src="/s/warn.svg"><h1>',title,'</h1><p>',msg,'</p></div>');
          410  +			class = 'error';
          411  +			cache = false;
          412  +		}
          413  +
          414  +		self:statpage(code, body)
          415  +	else
          416  +		var pg = lib.http.page { respcode = code, body = pstring.null() }
          417  +		var ctt = lib.http.mime.none
          418  +		if self:matchmime(lib.http.mime.json) then ctt = lib.http.mime.json
          419  +			pg.body = ([lib.tpl.mk'{"_parsav_error":@$ekind, "_parsav_error_desc":@$edesc}']
          420  +				{ekind = title, edesc = msg}):poolstr(&self.srv.pool)
          421  +		elseif self:matchmime(lib.http.mime.text) then ctt = lib.http.mime.text
          422  +			pg.body = self:qstr('error: ',title,'\n',msg)
          423  +		elseif self:matchmime(lib.http.mime.mkdown) then ctt = lib.http.mime.mkdown
          424  +			pg.body = self:qstr('# error :: ',title,'\n\n',msg)
          425  +		elseif self:matchmime(lib.http.mime.ansi) then ctt = lib.http.mime.ansi
          426  +			pg.body = self:qstr('\27[1;31merror :: ',title,'\27[m\n',msg)
          427  +		end
          428  +		var cthdr = lib.http.header { 'Content-Type', 'text/plain' }
          429  +		if ctt == lib.http.mime.none then
          430  +			pg.headers.ct = 0
          431  +		else
          432  +			pg.headers = lib.typeof(pg.headers) { &cthdr, 1 }
          433  +			switch ctt do
          434  +				case [ctt.type](lib.http.mime.json) then
          435  +					cthdr.value = 'application/json'
          436  +				end
          437  +				escape
          438  +					for i,v in ipairs(mimetypes) do local key,mime = v[1],v[2]
          439  +						if key ~= 'json' then
          440  +							emit quote case [ctt.type](lib.http.mime.[key]) then cthdr.value = [mime] end end
          441  +						end
          442  +					end
          443  +				end
          444  +			end
          445  +		end
          446  +		pg:send(self.con)
          447  +	end
          448  +end
          449  +
          450  +terra convo:fail(code: uint16)
          451  +	switch code do
          452  +		escape
          453  +			local stderrors = {
          454  +				{400, 'bad request', "the action you have attempted on this resource is not meaningful"};
          455  +				{401, 'unauthorized', "this resource is not available at your clearance level"};
          456  +				{403, 'forbidden', "we can neither confirm nor deny the existence of this resource"};
          457  +				{404, 'resource not found', "that resource is not extant on or known to this server"};
          458  +				{405, 'method not allowed', "the method you have attempted on this resource is not meaningful"};
          459  +				{406, 'not acceptable', "none of the suggested content types are a viable representation of this resource"};
          460  +				{500, 'internal server error', "parsav did a fucksy wucksy"};
          461  +			}
          462  +
          463  +			for i,v in ipairs(stderrors) do
          464  +				emit quote case uint16([v[1]]) then
          465  +					self:complain([v])
          466  +				end end
          467  +			end
          468  +		end
          469  +		else self:complain(500,'unknown error','an unrecognized error was thrown. this is a bug')
          470  +	end
   393    471   end
   394    472   
   395    473   terra convo:confirm(title: pstring, msg: pstring, cancel: pstring)
   396    474   	var conf = data.view.confirm {
   397    475   		title = title;
   398    476   		query = msg;
   399    477   		cancel = cancel;
................................................................................
   405    483   		class = 'query';
   406    484   		body = body; cache = false;
   407    485   	}
   408    486   	self:stdpage(cf)
   409    487   	--cf.title:free()
   410    488   end
   411    489   
   412         -terra convo:stra(sz: intptr) -- convenience function
   413         -	var s: lib.str.acc
   414         -	s:pool(&self.srv.pool,sz)
   415         -	return s
   416         -end
   417         -
   418         -convo.methods.qstr = macro(function(self, ...) -- convenience string builder
   419         -	local exp = {...}
   420         -	return `lib.str.acc{}:pcompose(&self.srv.pool, [exp]):finalize()
   421         -end)
   422         -
   423    490   convo.methods.assertpow = macro(function(self, pow)
   424    491   	return quote
   425    492   		var ok = true
   426    493   		if self.aid == 0 or self.who.rights.powers.[pow:asvalue()]() == false then
   427    494   			ok = false
   428    495   			self:complain(403,'insufficient privileges',['you lack the <strong>'..pow:asvalue()..'</strong> power and cannot perform this action'])
   429    496   		end
................................................................................
   504    571   end
   505    572   terra convo:pgetv(name: rawstring)
   506    573   	var s,l = self:getv(name)
   507    574   	return pstring { ptr = s, ct = l }
   508    575   end
   509    576   
   510    577   local route = {} -- these are defined in route.t, as they need access to renderers
   511         -terra route.dispatch_http ::  {&convo, lib.mem.ptr(int8), lib.http.method.t} -> {}
   512         -
   513         -local mimetypes = {
   514         -	{'html', 'text/html'};
   515         -	{'json', 'application/json'};
   516         -	{'json', 'application/ld+json'};
   517         -	{'json', 'application/activity+json'};
   518         -	{'mkdown', 'text/markdown'};
   519         -	{'text', 'text/plain'};
   520         -	{'ansi', 'text/x-ansi'};
   521         -}
          578  +terra route.dispatch_http ::  {&convo, lib.mem.ptr(int8)} -> {}
   522    579   
   523    580   local mimevar = symbol(lib.mem.ref(int8))
   524    581   local mimeneg = `lib.http.mime.none
   525    582   
   526    583   for i, t in ipairs(mimetypes) do
   527    584   	local name, mime = t[1], t[2]
   528    585   	mimeneg = quote
................................................................................
   821    878   							end
   822    879   							bsr:free()
   823    880   							upmap:free()
   824    881   						end
   825    882   					end
   826    883   				end
   827    884   
   828         -				route.dispatch_http(&co, uri, co.method)
          885  +				route.dispatch_http(&co, uri)
   829    886   
   830    887   				::fail::
   831    888   				if co.uploads.run > 0 then
   832    889   					for i=0,co.uploads.sz do
   833    890   						co.uploads(i).filename:free()
   834    891   						co.uploads(i).field:free()
   835    892   					end

Modified str.t from [6c699847d5] to [072253bf19].

   180    180   	end
   181    181   	if self.buf ~= nil and self.space > 0 then
   182    182   		lib.mem.heapf(self.buf)
   183    183   	end
   184    184   end;
   185    185   
   186    186   terra m.acc:crush()
   187         -	--lib.dbg('crushing string accumulator')
   188    187   	if self.pool ~= nil then return self end -- no point unless at end of buffer
          188  +	--lib.dbg('crushing string accumulator', &self.buf[0])
   189    189   	self.buf = [rawstring](lib.mem.heapr_raw(self.buf, self.sz))
   190    190   	self.space = self.sz
   191    191   	return self
   192    192   end;
   193    193   
   194    194   terra m.acc:finalize()
   195    195   	--lib.dbg('finalizing string accumulator')
................................................................................
   567    567   		var add, cont = disemvowel_codepoint(cur)
   568    568   		if add:ref() then acc:ppush(add) end
   569    569   		cur = cont
   570    570   	end
   571    571   	return acc:finalize()
   572    572   end
   573    573   
   574         -terra m.qesc(pool: &lib.mem.pool, str: m.t): m.t
          574  +terra m.qesc(pool: &lib.mem.pool, str: m.t, wrap: bool): m.t
   575    575    -- escape double-quotes
   576    576   	var a: m.acc a:pool(pool, str.ct + str.ct/2)
   577         -	a:lpush '"'
          577  +	if wrap then a:lpush '"' end
   578    578   	for i=0, str.ct do
   579    579   		if     str(i) == @'"'  then a:lpush '\\"'
   580    580   		elseif str(i) == @'\\' then a:lpush '\\\\'
   581    581   		elseif str(i) < 0x20 then -- for json
   582    582   			var hex = lib.math.hexbyte(str(i))
   583    583   			a:lpush('\\u00'):push(&hex[0], 2)
   584    584   		else   a:push(str.ptr + i,1) end
   585    585   	end
   586         -	a:lpush '"'
          586  +	if wrap then a:lpush '"' end
   587    587   	return a:finalize()
   588    588   end
   589    589   
   590    590   return m

Modified tpl.t from [ce74a1083d] to [2f4ebceee0].

    34     34   	str = str:gsub('%s+[\n$]','')
    35     35   	str = str:gsub('\n','')
    36     36   	str = str:gsub('</a><a ','</a> <a ') -- keep nav links from getting smooshed
    37     37   	str = str:gsub(tplchar .. '%?([-%w]+)', function(file)
    38     38   		if not docs[file] then docs[file] = data.doc[file] end
    39     39   		return string.format('<a href="#help-%s" class="help">?</a>', file)
    40     40   	end)
    41         -	for start, mode, key, stop in string.gmatch(str,'()'..tplchar..'([:!$]?)([-a-zA-Z0-9_]+)()') do
           41  +	for start, mode, key, stop in string.gmatch(str,'()'..tplchar..'([+:!$#%^]?)([-a-zA-Z0-9_]+)()') do
    42     42   		if string.sub(str,start-1,start-1) ~= '\\' then
    43     43   			segs[#segs+1] = string.sub(str,last,start-1)
    44     44   			fields[#segs] = { key = key:gsub('-','_'), mode = (mode ~= '' and mode or nil) }
    45     45   			last = stop
    46     46   		end
    47     47   	end
    48     48   	segs[#segs+1] = string.sub(str,last)
................................................................................
    65     65   	local tallyup = {quote
    66     66   		var [runningtally] = 1 + constlen
    67     67   	end}
    68     68   	local rec = terralib.types.newstruct(string.format('template<%s>',tplspec.id or ''))
    69     69   	local symself = symbol(&rec)
    70     70   	do local kfac = {}
    71     71   		local sanmode = {}
           72  +		local types = { ['^'] = uint64, ['#'] = uint64 }
           73  +		local recmap = {}
    72     74   		for afterseg,fld in ipairs(fields) do
    73     75   			if not kfac[fld.key] then
    74     76   				rec.entries[#rec.entries + 1] = {
    75     77   					field = fld.key;
    76         -					type = lib.mem.ptr(int8);
           78  +					type = types[fld.mode] or pstr;
    77     79   				}
           80  +				recmap[fld.key] = rec.entries[#rec.entries]
    78     81   			end
    79     82   			kfac[fld.key] = (kfac[fld.key] or 0) + 1
    80     83   			sanmode[fld.key] = fld.mode == ':' and 6
    81     84   				or fld.mode == '!' and 5
    82         -				or fld.mode == '$' and 2 or 1
           85  +				or (fld.mode == '$' or fld.mode == '+') and 2
           86  +				or fld.mode == '^' and lib.math.shorthand.maxlen
           87  +				or fld.mode == '#' and 20
           88  +				or 1
    83     89   		end
    84     90   		for key, fac in pairs(kfac) do
    85     91   			local sanfac = sanmode[key]
    86         -			
    87         -			tallyup[#tallyup + 1] = quote
    88         -				[runningtally] = [runningtally] + ([symself].[key].ct)*fac*sanfac
           92  +			if recmap[key].type ~= pstr then
           93  +				tallyup[#tallyup + 1] = quote
           94  +					[runningtally] = [runningtally] + fac*sanfac
           95  +				end
           96  +			else
           97  +				tallyup[#tallyup + 1] = quote
           98  +					[runningtally] = [runningtally] + ([symself].[key].ct)*fac*sanfac
           99  +				end
    89    100   			end
    90    101   		end
    91    102   	end
    92    103   
    93    104   	local copiers = {}
    94    105   	local senders = {}
    95    106   	local appenders = {}
................................................................................
   102    113   		copiers[#copiers+1] = quote [cpypos] = lib.mem.cpy([cpypos], [&opaque]([seg]), [#seg]) end
   103    114   		senders[#senders+1] = quote lib.net.mg_send([destcon], [seg], [#seg]) end
   104    115   		appenders[#appenders+1] = quote [accumulator]:push([seg], [#seg]) end
   105    116   		if fields[idx] and fields[idx].mode then
   106    117   			local f = fields[idx]
   107    118   			local fp = `symself.[f.key]
   108    119   			local sanexp
   109         -			if f.mode == '$' then
   110         -				sanexp = `lib.str.qesc(pool, fp)
   111         -			else
   112         -				sanexp = `lib.html.sanitize(pool, fp, [f.mode == ':'])
   113         -			end
          120  +			local nulexp
          121  +			if f.mode == '$' then sanexp = `lib.str.qesc(pool, fp, true)
          122  +			elseif f.mode == '+' then sanexp = `lib.str.qesc(pool, fp, false)
          123  +			elseif f.mode == '#' then
          124  +				sanexp = quote
          125  +					var ibuf: int8[21]
          126  +					var ptr = lib.math.decstr(fp, &ibuf[20])
          127  +				in pstr {ptr=ptr, ct=&ibuf[20] - ptr} end
          128  +			elseif f.mode == '^' then
          129  +
          130  +				sanexp = quote
          131  +					var shbuf: lib.math.shorthand.t
          132  +					var len = lib.math.shorthand.gen(fp, &shbuf[0])
          133  +				in pstr {ptr=&shbuf[0],ct=len} end
          134  +			else sanexp = `lib.html.sanitize(pool, fp, [f.mode == ':']) end
          135  +			if f.mode == '^' or f.mode == '#' then nulexp = `true
          136  +			else nulexp = `fp.ct > 0 end
   114    137   			copiers[#copiers+1] = quote 
   115         -				if fp.ct > 0 then
          138  +				if [nulexp] then
   116    139   					var san = sanexp
   117    140   					[cpypos] = lib.mem.cpy([cpypos], [&opaque](san.ptr), san.ct)
   118    141   					--san:free()
   119    142   				end
   120    143   			end
   121    144   			senders[#senders+1] = quote
   122         -				if fp.ct > 0 then
          145  +				if [nulexp] then
   123    146   					var san = sanexp
   124    147   					lib.net.mg_send([destcon], san.ptr, san.ct)
   125    148   					--san:free()
   126    149   				end
   127    150   			end
   128    151   			appenders[#appenders+1] = quote
   129         -				if fp.ct > 0 then
          152  +				if [nulexp] then
   130    153   					var san = sanexp
   131    154   					[accumulator]:ppush(san)
   132    155   					--san:free()
   133    156   				end
   134    157   			end
   135    158   		elseif fields[idx] then
   136    159   			local f = fields[idx]