parsav  Check-in [59e1d7d56a]

Overview
Comment:iterating
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 59e1d7d56aea6174c52dbedafd74e94bef506690949dd0a3728404456fdbb120
User & Date: lexi on 2020-12-16 08:46:02
Other Links: manifest | tags
Context
2020-12-16
08:46
add nix build file check-in: df4ae251ef user: lexi tags: trunk
08:46
iterating check-in: 59e1d7d56a user: lexi tags: trunk
2020-12-14
14:40
more boilerplate, add template framework check-in: 6f17de4767 user: lexi tags: trunk
Changes

Added backend/pgsql.t version [0360541ecf].

            1  +-- vim: ft=terra
            2  +local queries = {
            3  +	conf_get = {
            4  +		params = {rawstring}, sql = [[
            5  +			select value from parsav_config
            6  +				where key = $1::text limit 1
            7  +		]];
            8  +	};
            9  +
           10  +	conf_set = {
           11  +		params = {rawstring,rawstring}, sql = [[
           12  +			insert into parsav_config (key, value)
           13  +				values ($1::text, $2::text)
           14  +				on conflict (key) do update set value = $2::text
           15  +		]];
           16  +	};
           17  +
           18  +	conf_reset = {
           19  +		params = {rawstring}, sql = [[
           20  +			delete from parsav_config where
           21  +				key = $1::text 
           22  +		]];
           23  +	};
           24  +
           25  +	actor_fetch_uid = {
           26  +		params = {uint64}, sql = [[
           27  +			select
           28  +				id, nym, handle, origin,
           29  +				bio, rank, quota, key
           30  +			from parsav_actors
           31  +				where id = $1::bigint
           32  +		]];
           33  +	};
           34  +
           35  +	actor_fetch_xid = {
           36  +		params = {rawstring}, sql = [[
           37  +			select a.id, a.nym, a.handle, a.origin,
           38  +			       a.bio, a.rank, a.quota, a.key,
           39  +
           40  +				coalesce(s.domain,
           41  +				        (select value from parsav_config
           42  +							where key='domain' limit 1)) as domain
           43  +
           44  +			from      parsav_actors  as a
           45  +			left join parsav_servers as s
           46  +				on a.origin = s.id
           47  +
           48  +			where $1::text = (a.handle || '@' || domain) or
           49  +			      $1::text = ('@' || a.handle || '@' || domain) or
           50  +				  (a.origin is null and $1::text = ('@' || a.handle))
           51  +		]];
           52  +	};
           53  +}
           54  +
           55  +local struct pqr {
           56  +	sz: intptr
           57  +	res: &lib.pq.PGresult
           58  +}
           59  +terra pqr:free() if self.sz > 0 then lib.pq.PQclear(self.res) end end
           60  +terra pqr:null(row: intptr, col: intptr)
           61  +	return (lib.pq.PQgetisnull(self.res, row, col) == 1)
           62  +end
           63  +terra pqr:string(row: intptr, col: intptr)
           64  +	var v = lib.pq.PQgetvalue(self.res, row, col)
           65  +	var r: lib.mem.ptr(int8)
           66  +	r.ct = lib.str.sz(v)
           67  +	r.ptr = lib.str.ndup(v, r.ct)
           68  +	return r
           69  +end
           70  +pqr.methods.int = macro(function(self, ty, row, col)
           71  +	return quote
           72  +		var i: ty:astype()
           73  +		var v = lib.pq.PQgetvalue(self.res, row, col)
           74  +		lib.math.netswap_ip(ty, v, &i)
           75  +	in i end
           76  +end)
           77  +
           78  +local con = symbol(&lib.pq.PGconn)
           79  +local prep = {}
           80  +for k,q in pairs(queries) do
           81  +	local qt = (q.sql):gsub('%s+',' '):gsub('^%s*(.-)%s*$','%1')
           82  +	local stmt = 'parsavpg_' .. k
           83  +	prep[#prep + 1] = quote
           84  +		var res = lib.pq.PQprepare([con], stmt, qt, [#q.params], nil)
           85  +		defer lib.pq.PQclear(res)
           86  +		if res == nil or lib.pq.PQresultStatus(res) ~= lib.pq.PGRES_COMMAND_OK then
           87  +			if res == nil then
           88  +				lib.bail('grievous error occurred preparing ',k,' statement')
           89  +			end
           90  +			lib.bail('could not prepare PGSQL statement ',k,': ',lib.pq.PQresultErrorMessage(res))
           91  +		end
           92  +		lib.dbg('prepared PGSQL statement ',k) 
           93  +	end
           94  +
           95  +	local args, casts, counters, fixers, ft, yield = {}, {}, {}, {}, {}, {}
           96  +	for i, ty in ipairs(q.params) do
           97  +		args[i] = symbol(ty)
           98  +		ft[i] = `1
           99  +		if ty == rawstring then
          100  +			counters[i] = `lib.trn([args[i]] == nil, 0, lib.str.sz([args[i]]))
          101  +			casts[i] = `[&int8]([args[i]])
          102  +		elseif ty:isintegral() then
          103  +			counters[i] = ty.bytes
          104  +			casts[i] = `[&int8](&[args[i]])
          105  +			fixers[#fixers + 1] = quote
          106  +				--lib.io.fmt('uid=%llu(%llx)\n',[args[i]],[args[i]])
          107  +				[args[i]] = lib.math.netswap(ty, [args[i]])
          108  +			end
          109  +		end
          110  +	end
          111  +
          112  +	q.exec = terra(src: &lib.store.source, [args])
          113  +		var params = arrayof([&int8], [casts])
          114  +		var params_sz = arrayof(int, [counters])
          115  +		var params_ft = arrayof(int, [ft])
          116  +		[fixers]
          117  +		var res = lib.pq.PQexecPrepared([&lib.pq.PGconn](src.handle), stmt,
          118  +			[#args], params, params_sz, params_ft, 1)
          119  +		if res == nil then
          120  +			lib.bail(['grievous error occurred executing '..k..' against database'])
          121  +		elseif lib.pq.PQresultStatus(res) ~= lib.pq.PGRES_TUPLES_OK then
          122  +			lib.bail(['PGSQL database procedure '..k..' failed\n'],
          123  +			lib.pq.PQresultErrorMessage(res))
          124  +		end
          125  +
          126  +		var ct = lib.pq.PQntuples(res)
          127  +		if ct == 0 then
          128  +			lib.pq.PQclear(res)
          129  +			return pqr {0, nil}
          130  +		else
          131  +			return pqr {ct, res}
          132  +		end
          133  +	end
          134  +end
          135  +
          136  +local terra row_to_actor(r: &pqr, row: intptr): lib.store.actor
          137  +	var a = lib.store.actor {
          138  +		id = r:int(uint64, row, 0);
          139  +		nym = r:string(row, 1);
          140  +		handle = r:string(row, 2);
          141  +		bio = r:string(row, 4);
          142  +		key = r:string(row, 7);
          143  +		rights = lib.store.rights_default();
          144  +	}
          145  +	a.rights.rank = r:int(uint16, 0, 5);
          146  +	a.rights.quota = r:int(uint32, 0, 6);
          147  +	if r:null(0,3) then a.origin = 0
          148  +	else a.origin = r:int(uint64,0,3) end
          149  +	return a
          150  +end
          151  +
          152  +local b = `lib.store.backend {
          153  +	id = "pgsql";
          154  +	open = [terra(src: &lib.store.source): &opaque
          155  +		lib.report('connecting to postgres database: ', src.string.ptr)
          156  +		var [con] = lib.pq.PQconnectdb(src.string.ptr)
          157  +		if lib.pq.PQstatus(con) ~= lib.pq.CONNECTION_OK then
          158  +			lib.warn('postgres backend connection failed')
          159  +			lib.pq.PQfinish(con)
          160  +			return nil
          161  +		end
          162  +		var res = lib.pq.PQexec(con, [[
          163  +			select pg_catalog.set_config('search_path', 'public', false)
          164  +		]])
          165  +		if res ~= nil then defer lib.pq.PQclear(res) end
          166  +		if res == nil or lib.pq.PQresultStatus(res) ~= lib.pq.PGRES_TUPLES_OK then
          167  +			lib.warn('failed to secure postgres connection')
          168  +			lib.pq.PQfinish(con)
          169  +			return nil
          170  +		end
          171  +
          172  +		[prep]
          173  +		return con
          174  +	end];
          175  +	close = [terra(src: &lib.store.source) lib.pq.PQfinish([&lib.pq.PGconn](src.handle)) end];
          176  +
          177  +	conf_get = [terra(src: &lib.store.source, key: rawstring)
          178  +		var r = queries.conf_get.exec(src, key)
          179  +		if r.sz == 0 then return [lib.mem.ptr(int8)] { ptr = nil, ct = 0 } else
          180  +			defer r:free()
          181  +			return r:string(0,0)
          182  +		end
          183  +	end];
          184  +	conf_set = [terra(src: &lib.store.source, key: rawstring, val: rawstring)
          185  +		queries.conf_set.exec(src, key, val):free() end];
          186  +	conf_reset = [terra(src: &lib.store.source, key: rawstring)
          187  +		queries.conf_reset.exec(src, key):free() end];
          188  +	
          189  +	actor_fetch_uid = [terra(src: &lib.store.source, uid: uint64)
          190  +		var r = queries.actor_fetch_uid.exec(src, uid)
          191  +		if r.sz == 0 then
          192  +			return [lib.stat(lib.store.actor)] { ok = false, error = 1}
          193  +		else
          194  +			defer r:free()
          195  +			var a = [lib.stat(lib.store.actor)] { ok = true }
          196  +			a.val = row_to_actor(&r, 0)
          197  +			a.val.source = src
          198  +			return a
          199  +		end
          200  +	end];
          201  +}
          202  +
          203  +return b

Modified cmdparse.t from [8abc7a6fc7] to [c7f162fae3].

            1  +-- vim: ft=terra
     1      2   return function(tbl)
     2      3   	local options = terralib.types.newstruct('options') do
     3      4   		local flags = '' for _,d in pairs(tbl) do flags = flags .. d[1] end
     4      5   		local helpstr = 'usage: parsav [-' .. flags .. '] [<arg>...]\n'
     5      6   		options.entries = {
     6      7   			{field = 'arglist', type = lib.mem.ptr(rawstring)}
     7      8   		}

Modified config.lua from [85408f7c8a] to [60797f7bc4].

    19     19   	dist      = default('parsav_dist', coalesce(
    20     20   		os.getenv('NIX_PATH')  and 'nixos',
    21     21   		os.getenv('NIX_STORE') and 'nixos',
    22     22   	''));
    23     23   	tgttrip   = default('parsav_arch_triple'); -- target triple, used in xcomp
    24     24   	tgtcpu    = default('parsav_arch_cpu'); -- target cpu, used in xcomp
    25     25   	tgthf     = u.tobool(default('parsav_arch_armhf',true)); 
           26  +	endian    = default('parsav_arch_endian', 'little');
    26     27   	build     = {
    27     28   		id = u.rndstr(6);
    28     29   		release = u.ingest('release');
    29     30   		when = os.date();
    30     31   	};
    31     32   	feat = {};
    32     33   }

Added file.t version [fc7770c3f7].

            1  +-- vim: ft=terra
            2  +-- TODO: add support for windows IO calls
            3  +local handle_type = int
            4  +local posix = terralib.includec 'fcntl.h'
            5  +local unistd = terralib.includec 'unistd.h'
            6  +
            7  +struct file {
            8  +	handle: handle_type
            9  +	read: bool
           10  +	write: bool
           11  +}
           12  +
           13  +file.mode = { read = 0, write = 1, rw = 2 }
           14  +file.seek = { abs = 0, ofs = 1, eof = 2 }
           15  +
           16  +file.methods = {
           17  +	open = terra(path: rawstring, mode: uint8)
           18  +		var f: file
           19  +		var flag: int
           20  +		if mode == [file.mode.rw] then
           21  +			flag = posix.O_RDWR
           22  +			f.read = true f.write = true
           23  +		elseif mode == [file.mode.read] then
           24  +			flag = posix.O_RDONLY
           25  +			f.read = true f.write = false
           26  +		elseif mode == [file.mode.read] then
           27  +			flag = posix.O_WRONLY
           28  +			f.read = false f.write = true
           29  +		else lib.bail('invalid file mode') end
           30  +		lib.dbg('opening file ', path)
           31  +		f.handle = posix.open(path, flag) 
           32  +
           33  +		var r: lib.stat(file)
           34  +		if f.handle == -1 then
           35  +			r.ok = false
           36  +			r.error = 1 -- TODO get errno somehow?
           37  +		else
           38  +			r.ok = true
           39  +			r.val = f
           40  +		end
           41  +		return r
           42  +	end;
           43  +	close = terra(self: &file)
           44  +		unistd.close(self.handle)
           45  +		self.handle = -1
           46  +		self.read = false
           47  +		self.write = false
           48  +	end;
           49  +	read = terra(self: &file, dest: rawstring, sz: intptr): ptrdiff
           50  +		return unistd.read(self.handle,dest,sz)
           51  +	end;
           52  +	write = terra(self: &file, data: &opaque, sz: intptr): ptrdiff
           53  +		return unistd.write(self.handle,data,sz)
           54  +	end;
           55  +	seek = terra(self: &file, ofs: ptrdiff, wh: int)
           56  +		var whence: int
           57  +		if wh == [file.seek.abs] then
           58  +			whence = unistd.SEEK_SET
           59  +		elseif wh == [file.seek.ofs] then
           60  +			whence = unistd.SEEK_CUR
           61  +		elseif wh == [file.seek.eof] then
           62  +			whence = unistd.SEEK_END
           63  +		else lib.bail('invalid seek mode') end
           64  +	
           65  +		return unistd.lseek(self.handle, ofs, whence)
           66  +	end;
           67  +}
           68  +
           69  +return file

Modified makefile from [0f98e9b572] to [40c8adaeb1].

    18     18   	lib/mbedtls/library/libmbedcrypto.a \
    19     19   	lib/mbedtls/library/libmbedx509.a
    20     20   dep.mongoose: lib/mongoose/libmongoose.a
    21     21   dep.json-c: lib/json-c/libjson-c.a
    22     22   
    23     23   lib:
    24     24   	mkdir $@
    25         -# parsav is designed to be fronted by a real web
    26         -# server like nginx if SSL is to be used
    27     25   # generate a shim static library so mongoose cooperates
    28         -# with the build apparatus
           26  +# with the build apparatus. note that parsav is designed
           27  +# to be fronted by a real web server like nginx if SSL
           28  +# is to be used, so we don't turn on SSL in mongoose
    29     29   lib/mongoose/libmongoose.a: lib/mongoose lib/mongoose/mongoose.c lib/mongoose/mongoose.h
    30     30   	$(CC) -c $</mongoose.c -o lib/mongoose/mongoose.o \
    31     31   		-DMG_ENABLE_THREADS \
    32     32   		-DMG_ENABLE_IPV6 \
    33     33   		-DMG_ENABLE_HTTP_WEBDAV \
    34     34   		-DMG_ENABLE_HTTP_WEBSOCKET=0
    35     35   	ar rcs $@ lib/mongoose/*.o
................................................................................
    40     40   lib/json-c/libjson-c.a: lib/json-c/Makefile
    41     41   	$(MAKE) -C lib/json-c
    42     42   lib/mbedtls/library/%.a: lib/mbedtls 
    43     43   	$(MAKE) -C lib/mbedtls/library $*.a
    44     44   
    45     45   ifeq ($(dl), git)
    46     46   lib/mongoose: lib
    47         -	cd lib && git clone https://github.com/cesanta/mongoose
           47  +	cd lib && git clone https://github.com/cesanta/mongoose.git
    48     48   lib/mbedtls: lib
    49     49   	cd lib && git clone https://github.com/ARMmbed/mbedtls.git
    50     50   lib/json-c: lib
    51     51   	cd lib && git clone https://github.com/json-c/json-c.git
    52     52   else
    53     53   lib/%: lib/%.tar.gz
    54     54   	cd lib && tar zxf $*.tar.gz

Added math.t version [fe958b6645].

            1  +-- vim: ft=terra
            2  +local m = {
            3  +	shorthand = {maxlen = 14}
            4  +}
            5  +
            6  +-- swap in place -- faster on little endian
            7  +m.netswap_ip = macro(function(ty, src, dest)
            8  +	if ty:astype().type ~= 'integer' then error('bad type') end
            9  +	local bytes = ty:astype().bytes
           10  +	src = `[&uint8](src)
           11  +	dest = `[&uint8](dest)
           12  +	if config.endian == 'little' then
           13  +		return quote for i = 0, bytes do dest[i] = src[bytes - (i+1)] end end
           14  +	elseif config.endian == 'big' then
           15  +		return quote for i = 0, bytes do dest[i] = src[i] end end
           16  +	else error('unknown endianness '..config.endian) end
           17  +end)
           18  +
           19  +-- swap out of place -- safer, more flexible, and optimized to an intrinsic call; trivial on big endian
           20  +m.netswap = macro(function(tyq, src)
           21  +	if config.endian == 'little' then
           22  +		local ty = tyq:astype()
           23  +		local a,b = symbol(ty), symbol(ty)
           24  +		local bytes = ty.bytes
           25  +		local steps = {}
           26  +		for i=0,bytes-1 do
           27  +			steps[#steps + 1] = quote
           28  +				b = b << 8
           29  +				b = b or (a and 0xff)
           30  +				a = a >> 8
           31  +			end
           32  +		end
           33  +		return quote
           34  +			var [a] = src
           35  +			var [b] = 0
           36  +			[steps]
           37  +		in b end
           38  +	elseif config.endian == 'big' then return `src
           39  +	else error('unknown endianness '..config.endian) end
           40  +end)
           41  +
           42  +terra m.shorthand.cval(character: int8): {uint8, bool}
           43  +	var ch = [uint8](character)
           44  +
           45  +	if ch >= 0x30 and ch <= 0x39 then
           46  +		ch = 00 + (ch - 0x30)
           47  +	elseif ch == 0x2d then ch = 10
           48  +	elseif ch >= 0x41 and ch <= 0x5a then
           49  +		ch = 11 + (ch - 0x41)
           50  +	elseif ch == 0x3a then ch = 37
           51  +	elseif ch >= 0x61 and ch <= 0x7a then
           52  +		ch = 38 + (ch - 0x61)
           53  +	else return 0, false end
           54  +
           55  +	return ch, true 
           56  +end
           57  +
           58  +terra m.shorthand.gen(val: uint64, dest: rawstring): ptrdiff
           59  +	var lst = "0123456789-ABCDEFGHIJKLMNOPQRSTUVWXYZ:abcdefghijklmnopqrstuvwxyz"
           60  +	var buf: int8[m.shorthand.maxlen]
           61  +	var ptr = [&int8](buf)
           62  +	while val ~= 0 do
           63  +		var v = val % 64
           64  +		@ptr = lst[v]
           65  +		ptr = ptr + 1
           66  +		val = val / 64
           67  +	end
           68  +	var len = ptr - buf
           69  +	for i = 0, len do
           70  +		dest[i] = buf[len - (i+1)]
           71  +	end
           72  +	dest[len] = 0
           73  +	return len
           74  +end
           75  +
           76  +terra m.shorthand.parse(s: rawstring, len: intptr): {uint64, bool}
           77  +	var val: uint64 = 0
           78  +	for i = 0, len do
           79  +		var v, ok = m.shorthand.cval(s[i])
           80  +		if ok == false then return 0, false end
           81  +		val = (val * 64) + v
           82  +	end
           83  +	return val, true
           84  +end
           85  +
           86  +return m

Modified parsav.t from [889180c92d] to [ce122c09dc].

    34     34   				code[#code+1] = quote var n = v in
    35     35   					lib.io.send(2, n, lib.str.sz(n)) end
    36     36   			end
    37     37   		end
    38     38   		code[#code+1] = `lib.io.send(2, '\n', 1)
    39     39   		return code
    40     40   	end;
           41  +	trn = macro(function(cond, i, e)
           42  +		return quote
           43  +			var c: bool = [cond]
           44  +			var r: i.tree.type
           45  +			if c == true then r = i else r = e end
           46  +		in r end
           47  +	end);
    41     48   	proc = {
    42     49   		exit = terralib.externfunction('exit', int -> {});
    43     50   		getenv = terralib.externfunction('getenv', rawstring -> rawstring);
    44     51   	};
    45     52   	io = {
    46         -		open = terralib.externfunction('open', {rawstring, int} -> int);
    47         -		close = terralib.externfunction('close',  {int} -> int);
    48     53   		send = terralib.externfunction('write', {int, rawstring, intptr} -> ptrdiff);
    49     54   		recv = terralib.externfunction('read',  {int, rawstring, intptr} -> ptrdiff);
    50     55   		say = macro(function(msg) return `lib.io.send(2, msg, [#(msg:asvalue())]) end);
    51     56   		fmt = terralib.externfunction('printf',
    52     57   			terralib.types.funcpointer({rawstring},{int},true));
    53     58   	};
    54     59   	str = {
    55     60   		sz = terralib.externfunction('strlen', rawstring -> intptr);
    56     61   		cmp = terralib.externfunction('strcmp', {rawstring, rawstring} -> int);
           62  +		ncmp = terralib.externfunction('strncmp', {rawstring, rawstring, intptr} -> int);
    57     63   		cpy = terralib.externfunction('stpcpy',{rawstring, rawstring} -> rawstring);
    58     64   		ncpy = terralib.externfunction('stpncpy',{rawstring, rawstring, intptr} -> rawstring);
           65  +		ndup = terralib.externfunction('strndup',{rawstring, intptr} -> rawstring);
    59     66   		fmt = terralib.externfunction('asprintf',
    60     67   			terralib.types.funcpointer({&rawstring},{int},true));
    61     68   	};
           69  +	copy = function(tbl)
           70  +		local new = {}
           71  +		for k,v in pairs(tbl) do new[k] = v end
           72  +		setmetatable(new, getmetatable(tbl))
           73  +		return new
           74  +	end;
    62     75   	mem = {
    63     76   		zero = macro(function(r)
    64     77   			return quote
    65     78   				for i = 0, [r.tree.type.N] do r[i] = 0 end
    66     79   			end
    67     80   		end);
    68     81   		heapa_raw = terralib.externfunction('malloc', intptr -> &opaque);
................................................................................
    76     89   				ct = sz;
    77     90   			}
    78     91   		end)
    79     92   	};
    80     93   }
    81     94   
    82     95   local noise = global(uint8,1)
           96  +local noise_header = function(code,txt,mod)
           97  +	if mod then
           98  +		return string.format('\27[%s;1m(parsav::%s %s)\27[m ', code,mod,txt)
           99  +	else
          100  +		return string.format('\27[%s;1m(parsav %s)\27[m ', code,txt)
          101  +	end
          102  +end
    83    103   local defrep = function(level,n,code)
    84    104   	return macro(function(...)
    85         -		local q = lib.emit("\27["..code..";1m(parsav "..n..")\27[m ", ...)
          105  +		local q = lib.emit(noise_header(code,n), ...)
    86    106   		return quote
    87    107   			if noise >= level then [q] end
    88    108   		end
    89    109   	end);
    90    110   end
    91    111   lib.dbg = defrep(3,'debug', '32')
    92    112   lib.report = defrep(2,'info', '35')
    93    113   lib.warn = defrep(1,'warn', '33')
    94    114   lib.bail = macro(function(...)
    95         -	local q = lib.emit("\27[31;1m(parsav fatal)\27[m ", ...)
          115  +	local q = lib.emit(noise_header('31','fatal'), ...)
    96    116   	return quote
    97    117   		[q]
    98    118   		lib.proc.exit(1)
    99    119   	end
   100    120   end);
          121  +lib.stat = terralib.memoize(function(ty)
          122  +	local n = struct {
          123  +		ok: bool
          124  +		union {
          125  +			error: uint8
          126  +			val: ty
          127  +		}
          128  +	}
          129  +	n.name = string.format("stat<%s>", ty.name)
          130  +	n.stat_basetype = ty
          131  +	return n
          132  +end)
          133  +lib.enum = function(tbl)
          134  +	local ty = uint8
          135  +	if #tbl >= 2^32 then ty = uint64 -- hey, can't be too safe
          136  +	elseif #tbl >= 2^16 then ty = uint32
          137  +	elseif #tbl >= 2^8 then ty = uint16 end
          138  +	local o = { t = ty }
          139  +	for i, name in ipairs(tbl) do
          140  +		o[name] = i
          141  +	end
          142  +	return o
          143  +end
   101    144   lib.mem.ptr = terralib.memoize(function(ty)
   102    145   	local t = terralib.types.newstruct(string.format('ptr<%s>', ty))
   103    146   	t.entries = {
   104    147   		{'ptr', &ty};
   105    148   		{'ct', intptr};
   106    149   	}
          150  +	t.ptr_basetype = ty
   107    151   	local recurse = false
   108    152   	if ty:isstruct() then
   109    153   		if ty.methods.free then recurse = true end
   110    154   	end
   111    155   	t.methods = {
   112    156   		free = terra(self: &t): bool
   113    157   			[recurse and quote
................................................................................
   139    183   				self.ct = newct
   140    184   				return true
   141    185   			else return false end
   142    186   		end;
   143    187   	}
   144    188   	return t
   145    189   end)
          190  +lib.mem.vec = terralib.memoize(function(ty)
          191  +	local v = terralib.types.newstruct(string.format('vec<%s>', ty.name))
          192  +	v.entries = {
          193  +		{field = 'storage', type = lib.mem.ptr(ty)};
          194  +		{field = 'sz', type = intptr};
          195  +		{field = 'run', type = intptr};
          196  +	}
          197  +	local terra biggest(a: intptr, b: intptr)
          198  +		if a > b then return a else return b end
          199  +	end
          200  +	terra v:assure(n: intptr)
          201  +		if self.storage.ct < n then
          202  +			self.storage:resize(biggest(n, self.storage.ct + self.run))
          203  +		end
          204  +	end
          205  +	v.methods = {
          206  +		init = terra(self: &v, run: intptr): bool
          207  +			if not self.storage:init(run) then return false end
          208  +			self.run = run
          209  +			self.sz = 0
          210  +			return true
          211  +		end;
          212  +		new = terra(self: &v): &ty
          213  +			self:assure(self.sz + 1)
          214  +			self.sz = self.sz + 1
          215  +			return self.storage.ptr + (self.sz - 1)
          216  +		end;
          217  +		push = terra(self: &v, val: ty)
          218  +			self:assure(self.sz + 1)
          219  +			self.storage.ptr[self.sz] = val
          220  +			self.sz = self.sz + 1
          221  +		end;
          222  +		free = terra(self: &v) self.storage:free() end;
          223  +		last = terra(self: &v, idx: intptr): &ty
          224  +			if self.sz > idx then
          225  +				return self.storage.ptr + (self.sz - (idx+1))
          226  +			else lib.bail('vector underrun!') end
          227  +		end;
          228  +		crush = terra(self: &v)
          229  +			self.storage:resize(self.sz)
          230  +			return self.storage
          231  +		end;
          232  +	}
          233  +	v.metamethods.__apply = terra(self: &v, idx: intptr): &ty -- no index??
          234  +		if self.sz > idx then
          235  +			return self.storage.ptr + idx
          236  +		else lib.bail('vector overrun!') end
          237  +	end
          238  +	return v 
          239  +end)
   146    240   
   147    241   lib.err = lib.loadlib('mbedtls','mbedtls/error.h')
   148    242   lib.rsa = lib.loadlib('mbedtls','mbedtls/rsa.h')
   149    243   lib.pk = lib.loadlib('mbedtls','mbedtls/pk.h')
   150    244   lib.md = lib.loadlib('mbedtls','mbedtls/md.h')
   151    245   lib.b64 = lib.loadlib('mbedtls','mbedtls/base64.h')
   152    246   lib.net = lib.loadlib('mongoose','mongoose.h')
   153    247   lib.pq = lib.loadlib('libpq','libpq-fe.h')
          248  +lib.file = terralib.loadfile('file.t')()
          249  +lib.math = terralib.loadfile('math.t')()
   154    250   lib.crypt = terralib.loadfile('crypt.t')()
   155    251   lib.http = terralib.loadfile('http.t')()
   156    252   lib.tpl = terralib.loadfile('tpl.t')()
   157    253   lib.string = terralib.loadfile('string.t')()
   158    254   lib.store = terralib.loadfile('store.t')()
          255  +
          256  +local be = {}
          257  +for _, b in pairs { 'pgsql' } do
          258  +	be[#be+1] = terralib.loadfile('backend/' .. b .. '.t')()
          259  +end
          260  +lib.store.backends = global(`array([be]))
          261  +
   159    262   lib.cmdparse = terralib.loadfile('cmdparse.t')()
          263  +lib.srv = terralib.loadfile('srv.t')()
   160    264   
   161    265   do local collate = function(path,f, ...)
   162    266   	return loadfile(path..'/'..f..'.lua')(path, ...)
   163    267   end
   164    268   data = {
   165    269   	view = collate('view','load');
   166    270   } end
................................................................................
   181    285   		lib.crypt.pem(pub, &kp, buf)
   182    286   		lib.io.send(1, msg, [#msg])
   183    287   		lib.io.send(1, [rawstring](&buf), lib.str.sz([rawstring](&buf)))
   184    288   		lib.io.send(1, '\n', 1)
   185    289   	end
   186    290   end)
   187    291   
   188         -local handle = {
   189         -	http = terra(con: &lib.net.mg_connection, event: int, p: &opaque, ext: &opaque)
   190         -		switch event do
   191         -			case lib.net.MG_EV_HTTP_MSG then
   192         -				lib.dbg('routing HTTP request')
   193         -				var msg = [&lib.net.mg_http_message](p)
   194         -
   195         -			end
   196         -		end
   197         -	end;
   198         -}
   199    292   do
   200    293   	local p = string.format('parsav: %s\nbuilt on %s\n', config.build.str, config.build.when)
   201    294   	terra version() lib.io.send(1, p, [#p]) end
   202    295   end
   203    296   terra noise_init()
   204    297   	var n = lib.proc.getenv('parsav_noise')
   205    298   	if n ~= nil then
................................................................................
   217    310   	help = {'h', 'display this list'}
   218    311   }
   219    312   
   220    313   terra entry(argc: int, argv: &rawstring): int
   221    314   	noise_init()
   222    315   	[lib.init]
   223    316   
          317  +	-- shut mongoose the fuck up
          318  +	lib.net.mg_log_set_callback([terra(msg: &opaque, sz: int, u: &opaque) end], nil)
          319  +
   224    320   	var mode: options
   225    321   	mode:parse(argc,argv)
   226    322   	if mode.version then
   227    323   		version()
   228    324   		return 0
   229    325   	end
   230    326   	if mode.help then
   231    327   		lib.io.send(1,  [options.helptxt], [#options.helptxt])
   232    328   		return 0
   233    329   	end
   234         -
   235         -	var bind = lib.proc.getenv('parsav_bind')
   236         -	if bind == nil then bind = '[::]:10917' end
   237         -
   238         -	var nm: lib.net.mg_mgr
   239         -	lib.net.mg_mgr_init(&nm)
   240         -
   241         -	var nmc = lib.net.mg_http_listen(&nm, bind, handle.http, nil)
   242         -
          330  +	var srv: lib.srv
          331  +	srv:start('backend.conf')
          332  +	lib.report('listening for requests')
   243    333   	while true do
   244         -		lib.net.mg_mgr_poll(&nm,1000)
          334  +		srv:poll()
   245    335   	end
          336  +	srv:shutdown()
   246    337   
   247         -	lib.net.mg_mgr_free(&nm)
   248    338   	return 0
   249    339   end
   250    340   
   251    341   local bflag = function(long,short)
   252    342   	if short and util.has(buildopts, short) then return true end
   253    343   	if long and util.has(buildopts, long) then return true end
   254    344   	return false

Added schema.sql version [2da83887bf].

            1  +\prompt 'domain name: ' domain
            2  +\prompt 'bind to socket: ' bind
            3  +\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.'
            4  +\prompt 'admin actor: ' admin
            5  +\qecho 'you will need to create an authentication view mapping your user database to something parsav can understand; see auth.sql for an example. enter the name of the view to use.'
            6  +\prompt 'auth view: ' auth
            7  +
            8  +begin;
            9  +
           10  +drop table if exists parsav_config;
           11  +create table if not exists parsav_config (
           12  +	key text primary key,
           13  +	value text
           14  +);
           15  +
           16  +insert into parsav_config (key,value) values
           17  +	('bind',:'bind'),
           18  +	('domain',:'domain'),
           19  +	('auth-source',:'auth'),
           20  +	('administrator',:'admin');
           21  +
           22  +-- note that valid ids should always > 0, as 0 is reserved for null
           23  +-- on the client side, vastly simplifying code
           24  +drop table if exists parsav_servers cascade;
           25  +create table parsav_servers (
           26  +	id bigint primary key default (1+random()*(2^63-1))::bigint,
           27  +	domain text not null,
           28  +	key bytea
           29  +);
           30  +drop table if exists parsav_actors cascade;
           31  +create table parsav_actors (
           32  +	id bigint primary key default (1+random()*(2^63-1))::bigint,
           33  +	nym text,
           34  +	handle text not null, -- nym [@handle@origin] 
           35  +	origin bigint references parsav_servers(id)
           36  +		on delete cascade, -- null origin = local actor
           37  +	bio text,
           38  +	rank smallint not null default 0,
           39  +	quota integer not null default 1000,
           40  +	key bytea, -- private if localactor; public if remote
           41  +	
           42  +	unique (handle,origin)
           43  +);
           44  +
           45  +drop table if exists parsav_rights cascade;
           46  +create table parsav_rights (
           47  +	key text,
           48  +	actor bigint references parsav_actors(id)
           49  +		on delete cascade,
           50  +	allow boolean,
           51  +
           52  +	primary key (key,actor)
           53  +);
           54  +
           55  +insert into parsav_actors (handle,rank,quota) values (:'admin',1,0);
           56  +insert into parsav_rights (actor,key,allow)
           57  +	select (select id from parsav_actors where handle=:'admin'), a.column1, a.column2 from (values
           58  +		('ban',true),
           59  +		('config',true),
           60  +		('censor',true),
           61  +		('suspend',true),
           62  +		('rebrand',true)
           63  +	) as a;
           64  +
           65  +drop table if exists parsav_posts cascade;
           66  +create table parsav_posts (
           67  +	id bigint primary key default (1+random()*(2^63-1))::bigint,
           68  +	author bigint references parsav_actors(id)
           69  +		on delete cascade,
           70  +	subject text,
           71  +	body text,
           72  +	posted timestamp not null,
           73  +	discovered timestamp not null,
           74  +	scope smallint not null,
           75  +	convo bigint, parent bigint,
           76  +	circles bigint[], mentions bigint[]
           77  +);
           78  +
           79  +drop table if exists parsav_conversations cascade;
           80  +create table parsav_conversations (
           81  +	id bigint primary key default (1+random()*(2^63-1))::bigint,
           82  +	uri text not null,
           83  +	discovered timestamp not null,
           84  +	head bigint references parsav_posts(id)
           85  +);
           86  +
           87  +drop table if exists parsav_rels cascade;
           88  +create table parsav_rels (
           89  +	relator bigint references parsav_actors(id)
           90  +		on delete cascade, -- e.g. follower
           91  +	relatee bigint references parsav_actors(id)
           92  +		on delete cascade, -- e.g. follower
           93  +	kind smallint, -- e.g. follow, block, mute
           94  +
           95  +	primary key (relator, relatee, kind)
           96  +);
           97  +
           98  +drop table if exists parsav_acts cascade;
           99  +create table parsav_acts (
          100  +	id bigint primary key default (1+random()*(2^63-1))::bigint,
          101  +	kind text not null, -- like, react, so on
          102  +	time timestamp not null,
          103  +	actor bigint references parsav_actors(id)
          104  +		on delete cascade,
          105  +	subject bigint -- may be post or act, depending on kind
          106  +);
          107  +
          108  +drop table if exists parsav_log cascade;
          109  +create table parsav_log (
          110  +	-- accesses are tracked for security & sending delete acts
          111  +	id bigint primary key default (1+random()*(2^63-1))::bigint,
          112  +	time timestamp not null,
          113  +	actor bigint references parsav_actors(id)
          114  +		on delete cascade,
          115  +	post bigint not null
          116  +);
          117  +end;

Added srv.t version [350801ad24].

            1  +-- vim: ft=terra
            2  +local util = dofile 'common.lua'
            3  +local struct srv {
            4  +	sources: lib.mem.ptr(lib.store.source)
            5  +	webmgr: lib.net.mg_mgr
            6  +	webcon: &lib.net.mg_connection
            7  +}
            8  +
            9  +local handle = {
           10  +	http = terra(con: &lib.net.mg_connection, event: int, p: &opaque, ext: &opaque)
           11  +		switch event do
           12  +			case lib.net.MG_EV_HTTP_MSG then
           13  +				lib.dbg('routing HTTP request')
           14  +				var msg = [&lib.net.mg_http_message](p)
           15  +
           16  +			end
           17  +		end
           18  +	end;
           19  +}
           20  +local char = macro(function(ch) return `[string.byte(ch:asvalue())] end)
           21  +local terra cfg(s: &srv, befile: rawstring)
           22  +	lib.report('configuring backends from ', befile)
           23  +
           24  +	var fr = lib.file.open(befile, [lib.file.mode.read])
           25  +	if fr.ok == false then
           26  +		lib.bail('could not open configuration file ', befile)
           27  +	end
           28  +
           29  +	var f = fr.val
           30  +	var c: lib.mem.vec(lib.store.source) c:init(8)
           31  +	var text: lib.string.acc text:init(64)
           32  +	do var buf: int8[64]
           33  +		while true do
           34  +			var ct = f:read(buf, [buf.type.N])
           35  +			if ct == 0 then break end
           36  +			text:push(buf, ct)
           37  +		end
           38  +	end
           39  +	f:close()
           40  +
           41  +	var cur = text.buf
           42  +	var segs: tuple(&int8, &int8)[3] = array(
           43  +		{[&int8](0),[&int8](0)},
           44  +		{[&int8](0),[&int8](0)},
           45  +		{[&int8](0),[&int8](0)}
           46  +	)
           47  +	var segdup = [terra(s: {rawstring, rawstring})
           48  +		var sz = s._1 - s._0
           49  +		var str = s._0
           50  +		return [lib.mem.ptr(int8)] {
           51  +			ptr = lib.str.ndup(str, sz);
           52  +			ct = sz;
           53  +		}
           54  +	end]
           55  +	var fld = 0
           56  +	while (cur - text.buf) < text.sz do
           57  +		if segs[fld]._0 == nil then
           58  +			if not (@cur == char(' ') or @cur == char('\t') or @cur == char('\n')) then
           59  +				segs[fld] = {cur, nil}
           60  +			end
           61  +		else
           62  +			if fld < 2 and @cur == char(' ') or @cur == char('\t') then
           63  +				segs[fld]._1 = cur
           64  +				fld = fld + 1
           65  +				segs[fld] = {nil, nil}
           66  +			elseif @cur == char('\n') or cur == text.buf + (text.sz-1) then
           67  +				if fld < 2 then lib.bail('incomplete backend line in ', befile) else
           68  +					segs[fld]._1 = cur
           69  +					var src = c:new()
           70  +					src.id = segdup(segs[0])
           71  +					src.string = segdup(segs[2])
           72  +					src.backend = nil
           73  +					for i = 0,[lib.store.backends.type.N] do
           74  +						if lib.str.ncmp(segs[1]._0, lib.store.backends[i].id, segs[1]._1 - segs[1]._0) == 0 then
           75  +							src.backend = &lib.store.backends[i]
           76  +							break
           77  +						end
           78  +					end
           79  +					if src.backend == nil then
           80  +						lib.bail('unknown backend in ', befile)
           81  +					end
           82  +					src.handle = nil
           83  +					fld = 0
           84  +					segs[0] = {nil, nil}
           85  +				end
           86  +			end
           87  +		end
           88  +		cur = cur + 1
           89  +	end
           90  +	text:free()
           91  +
           92  +	s.sources = c:crush()
           93  +end
           94  +
           95  +--srv.methods.conf_set = terra(self: &srv, key: rawstring, val:rawstring)
           96  +--	self.sources.ptr[0]:conf_set(key, val)
           97  +--end
           98  +
           99  +srv.metamethods.__methodmissing = macro(function(meth, self, ...)
          100  +	local primary, ptr, stat, simple = 0,1,2,3
          101  +	local tk, rt = primary
          102  +	local expr = {...}
          103  +	for _,f in pairs(lib.store.backend.entries) do
          104  +		local fn = f.field or f[1]
          105  +		local ft = f.type or f[2]
          106  +		if fn == meth then
          107  +			rt = ft.type.returntype
          108  +			if rt == bool then tk = simple
          109  +			elseif rt.stat_basetype then tk = stat
          110  +			elseif rt.ptr_basetype then tk = ptr end
          111  +			break
          112  +		end
          113  +	end
          114  +	
          115  +	if tk == primary then
          116  +		return `self.sources.ptr[0]:[meth]([expr])
          117  +	else local ok, empty
          118  +		local r = symbol(rt)
          119  +		if tk == ptr then
          120  +			ok = `r.ptr ~= nil
          121  +			empty = `[rt]{ptr=nil,ct=0}
          122  +		elseif tk == stat then
          123  +			ok = `r.ok ~= false
          124  +			empty = `[rt]{ok=false,error=1}
          125  +		elseif tk == simple then
          126  +			ok = `r == true
          127  +			empty = `false
          128  +		end
          129  +		return quote
          130  +			var [r] = empty
          131  +			for i=0,self.sources.ct do var src = self.sources.ptr + i
          132  +				if src.handle ~= nil then
          133  +					r = src:[meth]([expr])
          134  +					if [ok] then break
          135  +						else r = empty end
          136  +				end
          137  +			end
          138  +		in r end
          139  +	end
          140  +end)
          141  +
          142  +srv.methods.start = terra(self: &srv, befile: rawstring)
          143  +	cfg(self, befile)
          144  +	var success = false
          145  +	for i=0,self.sources.ct do var src = self.sources.ptr + i
          146  +		lib.report('opening data source ', src.id.ptr, '(', src.backend.id, ')')
          147  +		src.handle = src.backend.open(src)
          148  +		if src.handle ~= nil then success = true end
          149  +	end
          150  +	if not success then
          151  +		lib.bail('could not connect to any data sources!')
          152  +	end
          153  +
          154  +	var dbbind = self:conf_get('bind')
          155  +	var envbind = lib.proc.getenv('parsav_bind')
          156  +	var bind: rawstring
          157  +	if envbind ~= nil then
          158  +		bind = envbind
          159  +	elseif dbbind.ptr ~= nil then
          160  +		bind = dbbind.ptr
          161  +	else bind = '[::]:10917' end
          162  +
          163  +	lib.report('binding to ', bind)
          164  +	lib.net.mg_mgr_init(&self.webmgr)
          165  +	self.webcon = lib.net.mg_http_listen(&self.webmgr, bind, handle.http, nil)
          166  +	dbbind:free()
          167  +
          168  +end
          169  +
          170  +srv.methods.poll = terra(self: &srv)
          171  +	lib.net.mg_mgr_poll(&self.webmgr,1000)
          172  +end
          173  +
          174  +srv.methods.shutdown = terra(self: &srv)
          175  +	lib.net.mg_mgr_free(&self.webmgr)
          176  +	for i=0,self.sources.ct do var src = self.sources.ptr + i
          177  +		lib.report('closing data source ', src.id.ptr, '(', src.backend.id, ')')
          178  +		src:close()
          179  +	end
          180  +	self.sources:free()
          181  +end
          182  +
          183  +return srv

Modified store.t from [a0814e5c61] to [ed1d490f12].

     1      1   -- vim: ft=terra
     2         -local m = {}
     3         -
     4         -local backend = {
     5         -	pgsql = {
            2  +local m = {
            3  +	timepoint = uint64;
            4  +	scope = lib.enum {
            5  +		'public', 'private', 'local';
            6  +		'personal', 'direct', 'circle';
            7  +	};
            8  +	notiftype = lib.enum {
            9  +		'mention', 'like', 'rt', 'react'
           10  +	};
           11  +	relation = lib.enum {
           12  +		'follow', 'mute', 'block'
     6     13   	};
     7     14   }
     8     15   
     9         -struct m.user {
    10         -	uid: rawstring
    11         -	nym: rawstring
    12         -	handle: rawstring
           16  +local str = lib.mem.ptr(int8)
           17  +str:complete()
           18  +
           19  +struct m.source
           20  +
           21  +struct m.rights {
           22  +	rank: uint16 -- lower = more powerful except 0 = regular user
           23  +	-- creating staff automatically assigns rank immediately below you
           24  +	quota: uint32 -- # of allowed tweets per day; 0 = no limit
           25  +	
           26  +	-- user powers -- default on
           27  +	login: bool
           28  +	visible: bool
           29  +	post: bool
           30  +	shout: bool
           31  +	propagate: bool
           32  +	upload: bool
           33  +
           34  +	-- admin powers -- default off
           35  +	ban: bool
           36  +	config: bool
           37  +	censor: bool
           38  +	suspend: bool
           39  +	rebrand: bool -- modify site's brand identity
           40  +}
           41  +
           42  +terra m.rights_default()
           43  +	return m.rights {
           44  +		rank = 0, quota = 1000;
           45  +		
           46  +		login = true, visible = true, post = true;
           47  +		shout = true, propagate = true, upload = true;
           48  +
           49  +		ban = false, config = false, censor = false;
           50  +		suspend = false, rebrand = false;
           51  +	}
           52  +end
           53  +
           54  +struct m.actor {
           55  +	id: uint64
           56  +	nym: str
           57  +	handle: str
           58  +	origin: uint64
           59  +	bio: str
           60  +	rights: m.rights
           61  +	key: str
           62  +
           63  +	source: &m.source
           64  +}
           65  +terra m.actor:free()
           66  +	self.nym:free()
           67  +	self.handle:free()
           68  +	self.bio:free()
           69  +	self.key:free()
           70  +end
           71  +
           72  +struct m.range {
           73  +	time: bool
           74  +	union {
           75  +		from_time: m.timepoint
           76  +		from_idx: uint64
           77  +	}
           78  +	union {
           79  +		to_time: m.timepoint
           80  +		to_idx: uint64
           81  +	}
           82  +}
           83  +
           84  +struct m.post {
           85  +	id: uint64
           86  +	author: uint64
           87  +	subject: str
           88  +	body: str
           89  +	posted: m.timepoint
           90  +	discovered: m.timepoint
           91  +	scope: m.scope.t
           92  +	mentions: lib.mem.ptr(uint64)
           93  +	circles: lib.mem.ptr(uint64) --only meaningful if scope is set to circle
           94  +	convo: uint64
           95  +	parent: uint64
           96  +
           97  +	source: &m.source
           98  +}
           99  +
          100  +local cnf = terralib.memoize(function(ty,rty)
          101  +	rty = rty or ty
          102  +	return struct {
          103  +		enum: {&opaque, uint64, rawstring} -> intptr
          104  +		get: {&opaque, uint64, rawstring} -> rty
          105  +		set: {&opaque, uint64, rawstring, ty} -> {}
          106  +		reset: {&opaque, uint64, rawstring} -> {}
          107  +	}
          108  +end)
          109  +
          110  +struct m.notif {
          111  +	kind: m.notiftype.t
          112  +	when: uint64
          113  +	union {
          114  +		post: uint64
          115  +		reaction: int8[8]
          116  +	}
          117  +}
          118  +
          119  +-- backends only handle content on the local server
          120  +struct m.backend { id: rawstring
          121  +	open: &m.source -> &opaque
          122  +	close: &m.source -> {}
          123  +
          124  +	conf_get: {&m.source, rawstring} -> lib.mem.ptr(int8)
          125  +	conf_set: {&m.source, rawstring, rawstring} -> {}
          126  +	conf_reset: {&m.source, rawstring} -> {}
          127  +
          128  +	actor_save: {&m.source, m.actor} -> bool
          129  +	actor_create: {&m.source, m.actor} -> bool
          130  +	actor_fetch_xid: {&m.source, rawstring} -> lib.stat(m.actor)
          131  +	actor_fetch_uid: {&m.source, uint64} -> lib.stat(m.actor)
          132  +	actor_notif_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.notif)
          133  +	actor_auth: {&m.source, rawstring, rawstring} -> lib.stat(m.actor)
          134  +	actor_enum: {&m.source} -> lib.mem.ptr(m.actor)
          135  +	actor_enum_local: {&m.source} -> lib.mem.ptr(m.actor)
          136  +
          137  +	actor_conf_str: cnf(rawstring, lib.mem.ptr(int8))
          138  +	actor_conf_int: cnf(intptr, lib.stat(intptr))
          139  +
          140  +	post_save: {&m.source, &m.post} -> bool
          141  +	post_create: {&m.source, &m.post} -> bool
          142  +	actor_post_fetch_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(m.post)
          143  +	convo_fetch_xid: {&m.source,rawstring} -> lib.mem.ptr(m.post)
          144  +	convo_fetch_uid: {&m.source,uint64} -> lib.mem.ptr(m.post)
          145  +
          146  +	actor_timeline_fetch_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(m.post)
          147  +	instance_timeline_fetch: {&m.source, m.range} -> lib.mem.ptr(m.post)
          148  +}
    13    149   
    14         -	localuser: bool
          150  +struct m.source {
          151  +	backend: &m.backend
          152  +	id: lib.mem.ptr(int8)
          153  +	handle: &opaque
          154  +	string: lib.mem.ptr(int8)
    15    155   }
          156  +terra m.source:free()
          157  +	self.id:free()
          158  +	self.string:free()
          159  +end
          160  +m.source.metamethods.__methodmissing = macro(function(meth, obj, ...)
          161  +	local q = {...}
          162  +	-- syntax sugar to forward unrecognized calls onto the backend
          163  +	return `obj.backend.[meth](&obj, [q])
          164  +end)
          165  +
          166  +return m