-- vim: ft=terra
local r = lib.srv.route
local method = lib.http.method
local pstring = lib.mem.ptr(int8)
local rstring = lib.mem.ref(int8)
local binblob = lib.mem.ptr(uint8)
local hpath = lib.mem.ptr(rstring)
local http = {}

terra meth_get(meth: method.t) return (meth == method.get) or (meth == method.head) end

terra http.actor_profile(co: &lib.srv.convo, actor: &lib.store.actor, meth: method.t)
	var rel: lib.store.relationship
	if co.aid ~= 0 then
		rel = co.srv:actor_rel_calc(co.who.id, actor.id)
		if meth == method.post then
			var act = co:ppostv('act')
			if rel.recip.block() then
				if act:cmp('follow') or act:cmp('subscribe') then
					co:complain(403,'blocked','you cannot follow a user you are blocked by') return
				end
			end
			if act:cmp('circle') then
				var allcircs = co.srv:circle_search(&co.srv.pool, co.who.id, 0)
				var mycircs = co.srv:circle_memberships_uid(&co.srv.pool, co.who.id, actor.id)
				var marked = co.srv.pool:alloc(bool, allcircs.ct)
				var member = co.srv.pool:alloc(bool, allcircs.ct)
				for i = 0, marked.ct do
					marked(i) = false
					member(i) = false
					-- search through list of memberships to see if this circle is in them
					-- if it is, set its membership flag
					for j = 0, mycircs.ct do
						if mycircs(j).cid == allcircs(i).cid then
							member(i) = true
						end
					end
				end

				-- which circles did the user mark this time?
				var iter = co:eachpostv('circle')
				for cid in iter do
					var decode, ok = lib.math.shorthand.parse(cid.ptr, cid.ct)
					if ok then
						for i=0, allcircs.ct do
							if allcircs(i).cid == decode then
								marked(i) = true
							end
						end
					end
				end

				-- compute the differential and carry out the appropriate action
				for i = 0, marked.ct do
					if marked(i) ~= member(i) then
						if marked(i) then -- newly marked, add
							co.srv:circle_members_add_uid(allcircs(i).cid, actor.id)
						elseif member(i) then -- unmarked, remove
							co.srv:circle_members_del_uid(allcircs(i).cid, actor.id)
						end
					end
				end

				-- thanks html for making this all so complicated
			elseif act:cmp('block') and not rel.rel.block() then
				rel.rel.block = true  rel.recip.follow = false
				co.srv:actor_rel_create([lib.store.relation.idvmap.block], co.who.id, actor.id)
				co.srv:actor_rel_destroy([lib.store.relation.idvmap.follow], actor.id, co.who.id)
			elseif not act:cmp('report') then
				[(function()
					local tests = quote co:complain(400,'bad request','the action you have attempted on this user is not meaningful') return end
					for i,v in ipairs(lib.store.relation.members) do
						tests = quote
							if [v ~= 'block'] and act:cmp(lib.str.plit([v])) and not rel.rel.[v]() then -- rely on dead code elimination :/
								rel.rel.[v] = true
								co.srv:actor_rel_create([lib.store.relation.idvmap[v]], co.who.id, actor.id)
							elseif act:cmp(lib.str.plit(['un'..v])) and rel.rel.[v]() then
								rel.rel.[v] = false
								co.srv:actor_rel_destroy([lib.store.relation.idvmap[v]], co.who.id, actor.id)
							else [tests] end
						end
					end
					return tests
				end)()]
			end
		end
	else
		rel.rel:clear()
		rel.recip:clear()
	end

	var go = co:pgetv('go')
	if not go or go(0) ~= @'/' then
		lib.render.user_page(co, actor, &rel)
	else
		co:reroute(go.ptr)
	end
end

terra http.actor_dispatch_mime(co: &lib.srv.convo, actor: &lib.store.actor)
	if co:matchmime(lib.http.mime.html) then
		http.actor_profile(co,actor,co.method)
	elseif co:matchmime(lib.http.mime.json) then
		lib.api.lp.actor(co, actor)
	else co:fail(406) end
end

terra http.actor_profile_xid(co: &lib.srv.convo, uri: lib.mem.ptr(int8))
	var handle = [lib.mem.ptr(int8)] { ptr = &uri.ptr[2], ct = 0 }
	for i=2,uri.ct do
		if uri.ptr[i] == @'/' or uri.ptr[i] == 0 then handle.ct = i - 2 break end
	end
	if handle.ct == 0 then
		handle.ct = uri.ct - 2
		uri:advance(uri.ct)
	elseif handle.ct + 2 < uri.ct then uri:advance(handle.ct + 2) end

	lib.dbg('looking up user by xid "', {handle.ptr,handle.ct} ,'", path: ', {uri.ptr,uri.ct})

	var path = lib.http.hier(&co.srv.pool, uri) --defer path:free()

	var actor = co.srv:actor_fetch_xid(handle)
	if actor.ptr == nil then
		co:complain(404,'no such user','no such user known to this server')
		return
	end
	defer actor:free()

	http.actor_dispatch_mime(co, actor.ptr)
end

terra http.actor_profile_uid (
	co: &lib.srv.convo,
	path: lib.mem.ptr(lib.mem.ref(int8))
)
	if path.ct < 2 then
		co:complain(404,'bad url','invalid user url')
		return
	end

	var uid, ok = lib.math.shorthand.parse(path.ptr[1].ptr, path.ptr[1].ct)
	if not ok then
		co:complain(400, 'bad user ID', 'that user ID is not valid')
		return
	end

	var actor = co.srv:actor_fetch_uid(uid)
	if actor.ptr == nil then
		co:complain(404, 'no such user', 'no user by that ID is known to this instance')
		return
	end
	defer actor:free()

	http.actor_dispatch_mime(co, actor.ptr)
end

terra http.login_form(co: &lib.srv.convo, meth: method.t)
	if meth_get(meth) then
		-- request a username
		lib.render.login(co, nil, nil, pstring.null())
	elseif meth == method.post then
		var usn, usnl = co:postv('user')
		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 = nil;
				origin = 0, bio = nil;
				key = [lib.mem.ptr(uint8)] {ptr=nil, ct=0};
				epithet = nil;
			}
			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, pstring.null())
		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,
					pstring {ptr=usn,ct=usnl},
					pstring {ptr=chrs,ct=chrsl})
			elseif lib.str.ncmp('challenge', am, lib.math.biggest(9,aml)) == 0 and chrs ~= nil then
				lib.dbg('challenge attempt beginning')
				var s_time = co:ppostv('time')
				var s_vfy = co:ppostv('vfy')
				var token = co:ppostv('token')
				if s_time:ref() and s_vfy:ref() and token:ref() then 
					lib.dbg('checking hmac validity')
					var vftok = co:stra(128) vftok:ppush(token):ppush(s_time)
					var hmac = lib.crypt.hmacp(&co.srv.pool, lib.crypt.alg.sha256, co.srv.cfg.secret:blob(), vftok:finalize())
					var vfy, vfyok = lib.math.shorthand.parse(s_vfy.ptr, s_vfy.ct)
					if vfyok and lib.math.truncate64(hmac.ptr,hmac.ct) == vfy then
						lib.dbg('checking expiration time')
						var time, timeok = lib.math.shorthand.parse(s_time.ptr, s_time.ct)
						if timeok and lib.osclock.time(nil) - time < [2 * 60] then -- two minutes
							lib.dbg('decoding base64')
							var bin = co.srv.pool:alloc(uint8, chrsl)
							var binlen: intptr
							if lib.b64.mbedtls_base64_decode(bin.ptr, bin.ct, &binlen, [&uint8](chrs), chrsl) == 0 then
								lib.dbg('running signature <',{chrs,chrsl},'> against challenge keys for token [', {token.ptr,token.ct}, ']')
								aid = co.srv:actor_auth_challenge(co.peer,
									pstring {usn,usnl}, binblob{bin.ptr,binlen}, token)
							end
						end
					end
				end
			elseif lib.str.ncmp('otp', am, lib.math.biggest(3,aml)) == 0 and chrs ~= nil then
				lib.dbg('using otp auth')
				-- ··· --
			else lib.dbg('invalid auth method') end

			-- error out
			if aid == 0 then
				lib.render.login(co, nil, nil,  'authentication failure')
			else
				co:installkey('/',aid)
			end
		end
		if act.ptr ~= nil and fakeact == false then act:free() end
	else
		::wrongmeth:: co:fail(405) do return end
	end
	return
end

terra http.post_compose(co: &lib.srv.convo, meth: method.t)
	if not co:assertpow('post') then return end
	--if co.who.rights.powers.post() == false then
		--co:complain(403,'insufficient privileges','you lack the <strong>post</strong> power and cannot perform this action')

	if meth_get(meth) then
		lib.render.compose(co, nil, nil)
	elseif meth == method.post then
		var text, textlen = co:postv("post")
		var acl, acllen = co:postv("acl")
		var subj, subjlen = co:postv("subject")
		if text == nil or acl == nil then
			co:complain(400, 'invalid post', 'every post must have at least body text and an ACL')
			return
		end
		if subj == nil then subj = '' end

		var p = lib.store.post {
			author = co.who.id, acl = acl;
			body = text, subject = subj;
			parent = 0;
		}
		var newid = p:publish(co.srv)

		var idbuf: int8[lib.math.shorthand.maxlen]
		var idlen = lib.math.shorthand.gen(newid, idbuf)
		var redirto: lib.str.acc redirto:compose('/post/',{idbuf,idlen}) defer redirto:free()
		co:reroute(redirto.buf)
	end
end

terra http.timeline(co: &lib.srv.convo, mode: hpath)
	lib.render.timeline(co,mode)
end

terra http.documentation(co: &lib.srv.convo, path: hpath)
	if path.ct == 2 then
		lib.render.docpage(co,path(1))
	elseif path.ct == 1 then
		lib.render.docpage(co, rstring.null())
	else
		co:complain(404, 'no such documentation', 'invalid documentation URL')
	end
end

terra http.tweet_page(co: &lib.srv.convo, path: hpath, meth: method.t)
	var pid, ok = lib.math.shorthand.parse(path(1).ptr, path(1).ct)
	if not ok then
		co:complain(400, 'bad post ID', 'that post ID is not valid')
		return
	end
	var post = co.srv:post_fetch(pid)
	var rt: lib.store.notice
	if not post then
		rt = co.srv:post_act_fetch_notice(pid)
		if rt.kind ~= lib.store.noticetype.rt then
			co:complain(404, 'post not found', 'no such post is known to this server')
			return
		elseif rt.who ~= co.who.id then
			co:complain(403, 'forbidden', 'you cannot cancel other people\'s retweets')
			return
		end
	end
	defer post:free() -- NOP on null

	if path.ct == 3 then
		var lnk: lib.str.acc lnk:compose('/post/', path(1))
		var lnkp = lnk:finalize() defer lnkp:free()
		if post:ref() and path(2):cmp(lib.str.lit 'snitch') then
			if meth_get(meth) then
				var ui = data.view.report {
					badtweet = lib.render.tweet(co, post.ptr, nil);
					clnk = lnkp;
				}

				co:stdpage([lib.srv.convo.page] {
					title = 'post :: report';
					class = 'report';
					body = ui:poolstr(&co.srv.pool);
					cache = false;
				})
			else
			end
			return
		elseif post:ref() and post(0).author ~= co.who.id then
			co:complain(403, 'forbidden', 'you cannot alter other people\'s posts')
			return
		elseif post:ref() and path(2):cmp(lib.str.lit 'edit') then
			if not co:assertpow('edit') then return end
			if meth_get(meth) then
				lib.render.compose(co, post.ptr, nil)
				return
			elseif meth == method.post then
				var newbody = co:postv('post')._0
				var newacl = co:postv('acl')._0
				var newsubj = co:postv('subject')._0
				if newbody ~= nil then post(0).body = newbody end
				if newacl  ~= nil then post(0).acl = newacl end
				if newsubj ~= nil then post(0).subject = newsubj end
				post(0):save(true)
				co:reroute(lnkp.ptr)
			end
			return
		elseif path(2):cmp(lib.str.lit 'del') then
			if meth_get(meth) then
				var conf: data.view.confirm
				if post:ref() then
					conf = data.view.confirm {
						title =  'delete post';
						query =  'are you sure you want to delete this post?';
						cancel = lnkp
					}
				else
					conf = data.view.confirm {
						title =  'cancel retweet';
						query =  'are you sure you want to undo this retweet?';
						cancel = '/';
					}
				end
				var body = conf:poolstr(&co.srv.pool) --defer body:free()
				co:stdpage([lib.srv.convo.page] {
					title =  'post :: delete';
					class =  'query';
					body = body; cache = false;
				})
				return
			elseif meth == method.post then
				var act = co:ppostv('act')
				if act:cmp('confirm') then
					if post:ref() then
						post().source:post_destroy(post().id)
					elseif rt.kind ~= 0 then
						co.srv:post_act_cancel(pid)
					end
					co:reroute('/') -- TODO maybe return to parent or conversation if possible
					return
				else goto badop end
			end
		else goto badurl end
	end

	if post:ref() and meth == method.post then
		if co.aid == 0 then goto noauth end
		var act = co:ppostv('act')
		if act:cmp( 'like') and not co.srv:post_liked_uid(co.who.id,pid) then
			co.srv:post_like(co.who.id, pid, false)
			post.ptr.likes = post.ptr.likes + 1
		elseif act:cmp( 'dislike') and co.srv:post_liked_uid(co.who.id,pid) then
			co.srv:post_like(co.who.id, pid, true)
			post.ptr.likes = post.ptr.likes - 1
		elseif act:cmp( 'rt') then
			co.srv:post_retweet(co.who.id, pid, false)
			post.ptr.rts = post.ptr.rts + 1
		elseif act:cmp( 'post') then
			var replytext = co:ppostv('post')
			var acl = co:ppostv('acl')
			var subj = co:ppostv('subject')
			if not acl then acl =  'all' end
			if not replytext then goto badop end
			
			var reply = lib.store.post {
				author = co.who.id, parent = pid;
				subject = subj.ptr, acl = acl.ptr, body = replytext.ptr;
			}

			reply:publish(co.srv)
		else goto badop end
	end

	if not post then goto badurl end

	lib.render.tweet_page(co, path, post.ptr)
	do return end

	::noauth:: do co:fail(401) return end
	::badurl:: do co:fail(404) return end
	::badop :: do co:fail(405) return end
end

local terra 
credsec_for_uid(co: &lib.srv.convo, uid: uint64)
	var act = co:ppostv('act')
	if not act then return true end
	lib.dbg('handling credential action')
	if act:cmp( 'invalidate') then
		lib.dbg('setting user\'s cookie validation time to now')
		co.who.source:auth_sigtime_user_alter(uid, lib.osclock.time(nil))
		-- the current session has been invalidated as well, so we need to immediately install a new authentication cookie with the same aid so the user doesn't need to log back in all over again
		co:installkey('?',co.aid)
		return false
	elseif act:cmp('revoke') then
		var s_cred = co:ppostv('cred')
		if s_cred:ref() then
			var cred, ok = lib.math.shorthand.parse(s_cred.ptr, s_cred.ct)
			if ok then
				co.srv:auth_destroy_aid_uid(cred,co.who.id)
			end
		end
		return true
	elseif act:cmp('newcred') then
		var cmt = co:ppostv('comment')
		var pw = co:ppostv('newpw')
		var rsapub = co:ppostv('newrsa'):blob()
		var aid: uint64 = 0
		if pw:ref() then
			var cpw = co:ppostv('rptpw')
			if not pw:cmp(cpw) then
				co:complain(400,'enrollment failure','the passwords you supplied do not match')
				return false
			end
			aid = co.srv:auth_attach_pw(uid, false, pw, cmt)
		elseif rsapub:ref() then
			var sig = co:ppostv('sig')
			var nonce = co:ppostv('nonce')
			var s_noncevld = co:ppostv('noncevld')
			var noncevld, ok = lib.math.shorthand.parse(s_noncevld.ptr, s_noncevld.ct)
			if not ok then
				co:complain(403,'try harder next time','you call that cryptanalysis?')
				return false
			end

			var fr = co.srv.pool:frame()
			var hmac = lib.crypt.hmacp(&co.srv.pool, lib.crypt.alg.sha256, co.srv.cfg.secret:blob(), nonce)
			if not lib.math.truncate64(hmac.ptr, hmac.ct) == noncevld then
				co:complain(403,'nice try','what exactly are you trying to accomplish here, buddy')
				return false
			end

			var pkres = lib.crypt.loadpub(rsapub.ptr,rsapub.ct+1) -- needs NUL
			if not pkres.ok then
				co:complain(400,'invalid key','the key you have supplied is not a valid PEM or DER file')
				return false
			end
			var pk = pkres.val
			defer pk:free()

			var decoded = co.srv.pool:alloc(uint8,sig.ct)
			var decoded_sz: intptr = 0
			if lib.b64.mbedtls_base64_decode(decoded.ptr,sig.ct,&decoded_sz,[&uint8](sig.ptr),sig.ct) ~= 0 then
				co:complain(400,'invalid signature','the signature you supplied is not encoded in valid base64')
				return false
			end

			var vfy, secl = lib.crypt.verify(&pk, nonce.ptr, nonce.ct, decoded.ptr, decoded_sz)
			if not vfy then
				co:complain(403,'verification failed','the signature you supplied does not match the required nonce')
				return false
			end

			var dbuf: uint8[lib.crypt.const.maxdersz]
			var derkey = lib.crypt.der(true, &pk, &dbuf[0])
			aid = co.srv:auth_attach_rsa(co.who.id, false, derkey, cmt)
			co.srv.pool:reset(fr)
		end
		if aid ~= 0 then
			lib.dbg('setting credential restrictions')
			var privs = [(function()
				local check = quote end
				local me = symbol(lib.store.privset)
				for i,v in ipairs(lib.store.privset.members) do
					check = quote [check]
						var val = co:pgetv(['allow-' .. v])
						if val:ref() and val:cmp('on')
							then ([me].[v] << true)
							else ([me].[v] << false)
						end
					end
				end
				return quote
					var [me]
					[check]
				in [me] end
			end)()]
			if privs:sz() > 0 then
				lib.dbg('installing credential restrictions')
				lib.io.fmt('on priv %llu\n',aid)
				co.srv:auth_privs_set(aid, privs)
			end

			lib.dbg('setting netmask restrictions')
			var nm = co:pgetv('netmask')
		end
		co:reroute('?')
		return false
	end
	co:complain(400,'bad request','the operation you have requested is not meaningful in this context')
	return false
end

terra http.configure(co: &lib.srv.convo, path: hpath, meth: method.t)
	var msg = pstring.null()
	-- first things first, do priv checks
	if path.ct >= 2 then
		if not co.who.rights.powers.config() and (
			path(1):cmp('srv')   or
			path(1):cmp('badge') or
			path(1):cmp('emoji')
		) then goto nopriv

		elseif not co.who.rights.powers.rebrand() and (
			path(1):cmp('brand')
		) then goto nopriv

		elseif not co.who.rights.powers.account() and (
			path(1):cmp('profile') or
			path(1):cmp('sec') or
			path(1):cmp('avi') or
			path(1):cmp('ui') or
			path(1):cmp('circles')
		) then goto nopriv

		elseif not co.who.rights.powers:affect_users() and (
			path(1):cmp(lib.str.lit 'users')
		) then goto nopriv end
	end

	if meth == method.post and path.ct >= 1 then
		var user_refresh = false var fail = false
		if path(1):cmp('profile') then
			lib.dbg('updating profile')
			co.who.bio = co:postv('bio')._0
			co.who.nym = co:postv('nym')._0
			if co.who.bio ~= nil and @co.who.bio == 0 then co.who.bio = nil end
			if co.who.nym ~= nil and @co.who.nym == 0 then co.who.nym = nil end
			co.who.source:actor_save(co.who)

			var act = co:ppostv('act')
			var resethue = false
			if act:ref() then
				resethue = act:cmp( 'reset-hue')
			end

			if not resethue then
				var shue = co:ppostv('hue')
				var nhue, okhue = lib.math.decparse(shue)
				if okhue and nhue ~= co.ui_hue then
					if nhue == co.srv.cfg.ui_hue
						then resethue = true
						else co.srv:actor_conf_int_set(co.who.id, 'ui-accent', nhue)
					end
					co.ui_hue = nhue
				end
			end
			if resethue then
				co.srv:actor_conf_int_reset(co.who.id, 'ui-accent')
				co.ui_hue = co.srv.cfg.ui_hue
			end

			var aclfollow = co:ppostv('acl-follow')
			var aclfollowreq = co:ppostv('acl-follow-req')
			if aclfollow:ref() and aclfollow.ct > 0 then
				co.srv:actor_conf_str_set(co.who.id, 'acl-follow', aclfollow)
			end
			if aclfollowreq:ref() and aclfollowreq.ct > 0 then
				co.srv:actor_conf_str_set(co.who.id, 'acl-follow-req', aclfollowreq)
			end

			msg = 'profile changes saved'
			--user_refresh = true -- not really necessary here, actually

		elseif path(1):cmp('sec') then
			if not credsec_for_uid(co, co.who.id) then return end
		elseif path(1):cmp('avi') then
			var act = co:ppostv('act')
			if act:ref() and act:cmp('clear') then
				co.who.avatarid = 0
				co.who.source:actor_save(co.who)
				msg = 'avatar reset to default'
			else goto badop end
		elseif path(1):cmp('circles') then
			if meth == method.post then
				var act = co:ppostv('act')
				if path.ct == 2 and act:cmp('create') then
					var newcirc = co:ppostv('name')
					if newcirc.ct > 0 then
						co.srv:circle_create(co.who.id, newcirc)
					end
				elseif path.ct == 3 and act:cmp('del') then
					var id, ok = lib.math.shorthand.parse(path(2).ptr,path(2).ct)
					if not ok then goto e404 end
					co.srv:circle_destroy(co.who.id, id)
					co:reroute('/conf/circles')
					return
				else goto badop end
			end
		elseif path(1):cmp('users') then
			if path.ct >= 3 then
				var userid, ok = lib.math.shorthand.parse(path(2).ptr, path(2).ct)
				if ok then
					var usr = co.srv:actor_fetch_uid(userid)
					if usr:ref() then --defer usr:free()
						if not co.who:overpowers(usr.ptr) then
							usr:free()
							goto nopriv
						end
					else goto badop end
					defer usr:free()

					if path.ct == 4 then
						if path(3):cmp(lib.str.lit 'cred') then
							if not credsec_for_uid(co, userid) then return end 
						end
					elseif path.ct == 3 then
						var purgestr = co:ppostv("purgestr")
						var purgekey = co:ppostv("purgekey")
						if purgestr:ref() and purgekey:ref() and purgestr(0) ~= 0 then
							if purgestr:cmp(purgekey) then -- destroying account! :O
								co.srv:actor_purge_uid(userid)
								co:reroute('/conf/users')
								return
							else msg = 'purge confirmation failed' end
						end

						var epithet = co:ppostv("epithet")
						var s_rank = co:ppostv("rank")
						var s_invites = co:ppostv("invites")
						var s_quota = co:ppostv("quota")
						var ch_staff = co:ppostv("staff")
						var torank: uint16 = usr(0).rights.rank
						if ch_staff:ref() and ch_staff:cmp('on') then
							if s_rank:ref() then
								var rank, rok = lib.math.decparse(s_rank)
								if rok and rank <= co.srv.cfg.nranks then
									torank = rank
								end
							elseif usr(0).rights.rank == 0 then
								torank = co.who.rights.rank + 1
							end
						else torank = 0 end

						if co.who.id ~= userid and co.who.rights.rank > 0 then
							if (co.who.rights.powers.elevate() and
								 (torank < usr(0).rights.rank or usr(0).rights.rank == 0) and
								  (torank > co.who.rights.rank or co.who.rights.rank == 1))
							or (co.who.rights.powers.demote()  and
								 (torank > usr(0).rights.rank or             torank == 0))
							then usr(0).rights.rank = torank end
						end

						if s_invites:ref() then
							var n_invites, n_invites_ok = lib.math.decparse(s_invites)
							if n_invites_ok and n_invites ~= usr(0).rights.invites then
								if (n_invites > usr(0).rights.invites and
									co.who.rights.powers.elevate() and
									co.who.rights.powers.invite())
								or (n_invites < usr(0).rights.invites and
									co.who.rights.powers.demote())
								then usr(0).rights.invites = n_invites end
							end
						end
						
						if (co.who.id ~= userid or co.who.rights.rank == 1) and s_quota:ref() then
							var n_quota, n_quota_ok = lib.math.decparse(s_quota)
							if n_quota_ok and n_quota ~= usr(0).rights.quota then
								if (co.who.rights.powers.elevate() and 
									((n_quota == 0 and  co.who.rights.quota == 0 or co.who.rights.rank == 1) or
									 (n_quota ~= 0 and (n_quota > usr(0).rights.quota and
										(co.who.rights.quota == 0 or
										 co.who.rights.quota >= n_quota or
										 co.who.rights.rank  == 1)))))
								or (co.who.rights.powers.demote() and n_quota ~= 0 and 
									(n_quota < usr(0).rights.quota or
									 co.who.rights.rank == 1))
								then usr(0).rights.quota = n_quota end
							end
						end

						if co.who.rights.powers.herald() and
						  (co.who.id ~= userid or
						   co.srv.cfg.pol_autoherald or
						   co.who.rights.rank == 1) then 
							if epithet:ref() and epithet(0) ~= 0 then
								usr(0).epithet = epithet.ptr
							else
								usr(0).epithet = nil
							end
						end

						if co.who.id ~= userid then
							-- update powers
						end
						co.srv:actor_save(usr.ptr)
						if not msg then msg = 'user record updated' end
					end
				end
			elseif path.ct == 2 and meth == method.post then
				var act = co:ppostv('act')
				if act:cmp('create') then
					var newname = co:ppostv('handle')
					if not newname or not lib.store.actor.handle_validate(newname.ptr) then
						co:complain(400,'invalid handle','the handle you have requested is not valid')
					end
					var tu = co.srv:actor_fetch_xid(newname)
					if tu:ref() then tu:free()
						co:complain(409,'handle clash','that handle conflicts with one that already exists')
						return
					end
					var kbuf: uint8[lib.crypt.const.maxdersz]
					var na = lib.store.actor.mk(&kbuf[0])
					na.handle = newname.ptr
					var newuid = co.srv:actor_create(&na)
					var shid: int8[lib.math.shorthand.maxlen]
					var shidlen = lib.math.shorthand.gen(newuid, &shid[0])
					var url = lib.str.acc{}:compose('/conf/users/',pstring{&shid[0],shidlen}):finalize() defer url:free()
					co:reroute(url.ptr)
					return
				elseif act:cmp('inst') then
				else goto badop end
			end
		end

		if user_refresh then -- refresh the user info for the renderer
			var usr = co.srv:actor_fetch_uid(co.who.id)
			lib.mem.heapf(co.who)
			co.who = usr.ptr
		end
		var go,golen = co:getv('go')
		if not fail and go ~= nil then
			co:reroute(go)
			return
		end
	end
	lib.render.conf(co,path,msg)
	do return end

	::nopriv:: do co:complain(403,'insufficient privileges','you do not have the necessary powers to perform this action') return end
	::badop:: do co:complain(400,'bad request','the operation you have requested is not meaningful in this context') return end
	::e404:: do co:complain(404,'not found','the resource you have requested is not known to this server') return end
end

terra http.user_notices(co: &lib.srv.convo, meth: method.t)
	if meth == method.post then
		var act = co:ppostv('act')
		if act:cmp('clear') then
			co.srv:actor_conf_int_set(co.who.id, 'notice-clear-time', lib.osclock.time(nil))
			co:reroute('/')
			return
		else goto badop end
	end

	lib.render.notices(co)
	do return end

	::badop:: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
end

terra http.media_manager(co: &lib.srv.convo, path: hpath, meth: method.t, uid: uint64)
	if co.aid ~= 0 and co.who.id == uid and path.ct == 2 and path(1):cmp(lib.str.lit'upload') and co.who.rights.powers.artifact() then
		if meth == method.get then
			var view = data.view.media_upload {
				folders = ''
			}
			var pg = view:poolstr(&co.srv.pool) -- defer pg:free()
			co:stdpage([lib.srv.convo.page] {
				title = 'media :: upload';
				class = 'media upload';
				cache = false; body = pg;
			})
		elseif meth == method.post_file then
			var desc = pstring.null()
			var folder = pstring.null()
			var mime = pstring.null()
			var name = pstring.null()
			var body = binblob.null()
			for i=0, co.uploads.sz do var up = co.uploads.storage.ptr + i
				if up.body.ct > 0 then
					if up.field:cmp('desc') then
						desc = up.body
					elseif up.field:cmp('folder') then
						folder = up.body
					elseif up.field:cmp('file') then
						mime = up.ctype
						body = binblob {ptr = [&uint8](up.body.ptr), ct = up.body.ct}
						name = up.filename
					end
				end
			end
			if not body then goto badop end
			if body.ct > co.srv.cfg.maxupsz then
				co:complain(403, 'file too long', "the file you have attempted to upload exceeds the maximum length permitted by this server's upload policy. if it is an image or video, try compressing it at a lower quality setting or resolution")
				return
			end
			var id = co.srv:artifact_instantiate(body,mime)
			if id == 0 then
				co:complain(500,'upload failed','artifact rejected. either the server is running out of space or this file is banned from the server')
				return
			end
			co.srv:artifact_expropriate(co.who.id,id,desc,folder)

			var idbuf: int8[lib.math.shorthand.maxlen]
			var idlen = lib.math.shorthand.gen(id,&idbuf[0])

			var url = lib.str.acc{}:compose('/media/a/',pstring{&idbuf[0],idlen}):finalize()
			co:reroute(url.ptr)
			url:free()
		else goto badop end
	elseif co.aid ~= 0 and path.ct == 4 and path(1):cmp(lib.str.lit'a') and meth==method.post then 
		var act = co:ppostv('act')
		if not act or not act:cmp('confirm') then goto badop end
		var artid, aok = lib.math.shorthand.parse(path(2).ptr,path(2).ct)
		if not aok then goto e404 end
		var art = co.srv:artifact_fetch(uid,artid)
		if not art then goto e404 end
		defer art:free()

		if path(3):cmp(lib.str.lit'avi') then
		 -- user wants to set avatar
			co.who.avatarid = artid
			co.srv:actor_save(co.who)
			co:reroute('/conf/avi')
		elseif path(3):cmp(lib.str.lit'del') then 
			co.srv:artifact_disclaim(co.who.id, artid)
			co:reroute('/media')
		else goto badop end
	else
		if meth == method.post then goto badop end
		lib.render.media_gallery(co,path,uid,nil)
	end
	do return end

	::badop:: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
	::e404:: do co:complain(404, 'artifact not found', 'no such artifact has been uploaded by this user') return end
end

do local branches = quote end
	local filename, flen = symbol(&int8), symbol(intptr)
	local page = symbol(lib.http.page)
	local send = label()
	local storage = data.stmap
	for i,e in ipairs(config.embeds) do local id,mime = e[1],e[2]
		local d = data.static[id]
		branches = quote [branches];
			if lib.str.ncmp(filename, id, lib.math.biggest([#id], flen)) == 0 then
				page.headers.ptr[0].value = mime;
				page.body = [lib.mem.ptr(int8)] {
					ptr = storage[([i-1])].ptr;
					ct  = storage[([i-1])].ct;
				}
				goto [send]
			end
		end
	end
	terra http.static_content(co: &lib.srv.convo, [filename], [flen])
		var hdrs = array(
			lib.http.header{'Content-Type',nil},
			lib.http.header{'Cache-Control','max-age=2592000'} -- TODO immutable?
		)
		var [page] = lib.http.page {
			respcode = 200;
			headers = [lib.mem.ptr(lib.http.header)] {
				ptr = &hdrs[0], ct = 1
			}
		}
		[branches]
		do return false end
		::[send]:: page:send(co.con) return true
	end
end


terra http.local_avatar(co: &lib.srv.convo, handle: lib.mem.ptr(int8))
	-- TODO retrieve user avatars
	var usr = co.srv:actor_fetch_xid(handle)
	if not usr then
	goto default end
	if usr(0).origin == 0 then
		if usr(0).avatarid == 0 then goto default end
		var avi, mime = co.srv:artifact_load(usr(0).avatarid)
		if not avi then goto default end
		defer avi:free() defer mime:free()
		co:bytestream(mime,avi)
	else
		co:reroute(usr(0).avatar)
	end
	do return end
	::default:: co:reroute('/s/default-avatar.webp')
end

terra http.file_serve_raw(co: &lib.srv.convo, id: lib.mem.ptr(int8))
	var id, idok = lib.math.shorthand.parse(id.ptr, id.ct)
	if not idok then goto e404 end
	var data, mime = co.srv:artifact_load(id)
	if not data then goto e404 end
	do defer data:free() defer mime:free()
		co:bytestream(mime,data)
	return end

	::e404:: do co:complain(404, 'artifact not found', 'no such artifact has been uploaded to this instance') return end
end


-- entry points
terra r.dispatch_http(co: &lib.srv.convo, uri: lib.mem.ptr(int8))
	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
	var meth = co.method -- TODO unfuck this legacy bat shit
	if uri.ptr == nil or uri.ptr[0] ~= @'/' then
		co:complain(404, 'what the hell', 'how did you do that')
	elseif uri.ct == 1 then -- root
		if (co.srv.cfg.pol_sec == lib.srv.secmode.private or
		   co.srv.cfg.pol_sec == lib.srv.secmode.lockdown) and co.aid == 0 then
		   http.login_form(co, meth)
		else http.timeline(co, hpath {ptr=nil,ct=0}) end
	elseif uri.ptr[1] == @'@' then
		http.actor_profile_xid(co, uri)
	elseif uri.ptr[1] == @'s' and uri.ptr[2] == @'/' and uri.ct > 3 then
		if not meth_get(meth) then goto wrongmeth end
		if not http.static_content(co, uri.ptr + 3, uri.ct - 3) then goto notfound end
	elseif lib.str.ncmp('/avi/', uri.ptr, 5) == 0 then
		http.local_avatar(co, [lib.mem.ptr(int8)] {ptr = uri.ptr + 5, ct = uri.ct - 5})
	elseif lib.str.ncmp('/file/', uri.ptr, 6) == 0 then
		http.file_serve_raw(co, [lib.mem.ptr(int8)] {ptr = uri.ptr + 6, ct = uri.ct - 6})
	elseif uri:cmp('/notices') then
		if co.aid == 0 then co:reroute('/login') return end
		http.user_notices(co,meth)
	elseif uri:cmp('/compose') then
		if co.aid == 0 then co:reroute('/login') return end
		http.post_compose(co,meth)
	elseif uri:cmp( '/login') then
		if co.aid == 0
			then http.login_form(co, meth)
			else co:reroute('/')
		end
	elseif uri:cmp( '/logout') then
		if co.aid == 0
			then goto notfound
			else co:reroute_cookie('/','auth=; Path=/')
		end
	else -- hierarchical routes
		var path = lib.http.hier(&co.srv.pool, uri) --defer path:free()
		if path.ct > 1 and path(0):cmp('user') then
			http.actor_profile_uid(co, path)
		elseif path.ct > 1 and path(0):cmp('post') then
			http.tweet_page(co, path, meth)
		elseif path(0):cmp('tl') then
			http.timeline(co, path)
		elseif path(0):cmp('.well-known') then
			if path(1):cmp('webfinger') then
				if not co:matchmime(lib.http.mime.json) then goto nacc end
				lib.api.webfinger(co)
			end
		elseif path(0):cmp('api') then
			if path(1):cmp('parsav') then -- native API
			elseif path(1):cmp('v1') then -- mastodon client api :/
			elseif path(1):cmp('lp') then -- litepub endpoints
				if path(2):cmp('outbox') then
					lib.api.lp.outbox(co,uri,path + 3)
				elseif path(2):cmp('inbox') then
				end
			else goto notfound end
		elseif path(0):cmp('media') then
			if co.aid == 0 then goto unauth end
			http.media_manager(co, path, meth, co.who.id)
		elseif path(0):cmp('doc') then
			if not meth_get(meth) then goto wrongmeth end
			http.documentation(co, path)
		elseif path(0):cmp('conf') then
			if co.aid == 0 then goto unauth end
			http.configure(co,path,meth)
		else goto notfound end
	end
	do return end

	::wrongmeth:: co:fail(405) do return end
	::nacc     :: co:fail(406) do return end
	::notfound :: co:fail(404) do return end
	::unauth   :: co:fail(401) do return end
end
