ADDED backend/pgsql.t Index: backend/pgsql.t ================================================================== --- backend/pgsql.t +++ backend/pgsql.t @@ -0,0 +1,203 @@ +-- vim: ft=terra +local queries = { + conf_get = { + params = {rawstring}, sql = [[ + select value from parsav_config + where key = $1::text limit 1 + ]]; + }; + + conf_set = { + params = {rawstring,rawstring}, sql = [[ + insert into parsav_config (key, value) + values ($1::text, $2::text) + on conflict (key) do update set value = $2::text + ]]; + }; + + conf_reset = { + params = {rawstring}, sql = [[ + delete from parsav_config where + key = $1::text + ]]; + }; + + actor_fetch_uid = { + params = {uint64}, sql = [[ + select + id, nym, handle, origin, + bio, rank, quota, key + from parsav_actors + where id = $1::bigint + ]]; + }; + + actor_fetch_xid = { + params = {rawstring}, sql = [[ + select a.id, a.nym, a.handle, a.origin, + a.bio, a.rank, a.quota, a.key, + + coalesce(s.domain, + (select value from parsav_config + where key='domain' limit 1)) as domain + + from parsav_actors as a + left join parsav_servers as s + on a.origin = s.id + + where $1::text = (a.handle || '@' || domain) or + $1::text = ('@' || a.handle || '@' || domain) or + (a.origin is null and $1::text = ('@' || a.handle)) + ]]; + }; +} + +local struct pqr { + sz: intptr + res: &lib.pq.PGresult +} +terra pqr:free() if self.sz > 0 then lib.pq.PQclear(self.res) end end +terra pqr:null(row: intptr, col: intptr) + return (lib.pq.PQgetisnull(self.res, row, col) == 1) +end +terra pqr:string(row: intptr, col: intptr) + var v = lib.pq.PQgetvalue(self.res, row, col) + var r: lib.mem.ptr(int8) + r.ct = lib.str.sz(v) + r.ptr = lib.str.ndup(v, r.ct) + return r +end +pqr.methods.int = macro(function(self, ty, row, col) + return quote + var i: ty:astype() + var v = lib.pq.PQgetvalue(self.res, row, col) + lib.math.netswap_ip(ty, v, &i) + in i end +end) + +local con = symbol(&lib.pq.PGconn) +local prep = {} +for k,q in pairs(queries) do + local qt = (q.sql):gsub('%s+',' '):gsub('^%s*(.-)%s*$','%1') + local stmt = 'parsavpg_' .. k + prep[#prep + 1] = quote + var res = lib.pq.PQprepare([con], stmt, qt, [#q.params], nil) + defer lib.pq.PQclear(res) + if res == nil or lib.pq.PQresultStatus(res) ~= lib.pq.PGRES_COMMAND_OK then + if res == nil then + lib.bail('grievous error occurred preparing ',k,' statement') + end + lib.bail('could not prepare PGSQL statement ',k,': ',lib.pq.PQresultErrorMessage(res)) + end + lib.dbg('prepared PGSQL statement ',k) + end + + local args, casts, counters, fixers, ft, yield = {}, {}, {}, {}, {}, {} + for i, ty in ipairs(q.params) do + args[i] = symbol(ty) + ft[i] = `1 + if ty == rawstring then + counters[i] = `lib.trn([args[i]] == nil, 0, lib.str.sz([args[i]])) + casts[i] = `[&int8]([args[i]]) + elseif ty:isintegral() then + counters[i] = ty.bytes + casts[i] = `[&int8](&[args[i]]) + fixers[#fixers + 1] = quote + --lib.io.fmt('uid=%llu(%llx)\n',[args[i]],[args[i]]) + [args[i]] = lib.math.netswap(ty, [args[i]]) + end + end + end + + q.exec = terra(src: &lib.store.source, [args]) + var params = arrayof([&int8], [casts]) + var params_sz = arrayof(int, [counters]) + var params_ft = arrayof(int, [ft]) + [fixers] + var res = lib.pq.PQexecPrepared([&lib.pq.PGconn](src.handle), stmt, + [#args], params, params_sz, params_ft, 1) + if res == nil then + lib.bail(['grievous error occurred executing '..k..' against database']) + elseif lib.pq.PQresultStatus(res) ~= lib.pq.PGRES_TUPLES_OK then + lib.bail(['PGSQL database procedure '..k..' failed\n'], + lib.pq.PQresultErrorMessage(res)) + end + + var ct = lib.pq.PQntuples(res) + if ct == 0 then + lib.pq.PQclear(res) + return pqr {0, nil} + else + return pqr {ct, res} + end + end +end + +local terra row_to_actor(r: &pqr, row: intptr): lib.store.actor + var a = lib.store.actor { + id = r:int(uint64, row, 0); + nym = r:string(row, 1); + handle = r:string(row, 2); + bio = r:string(row, 4); + key = r:string(row, 7); + rights = lib.store.rights_default(); + } + a.rights.rank = r:int(uint16, 0, 5); + a.rights.quota = r:int(uint32, 0, 6); + if r:null(0,3) then a.origin = 0 + else a.origin = r:int(uint64,0,3) end + return a +end + +local b = `lib.store.backend { + id = "pgsql"; + open = [terra(src: &lib.store.source): &opaque + lib.report('connecting to postgres database: ', src.string.ptr) + var [con] = lib.pq.PQconnectdb(src.string.ptr) + if lib.pq.PQstatus(con) ~= lib.pq.CONNECTION_OK then + lib.warn('postgres backend connection failed') + lib.pq.PQfinish(con) + return nil + end + var res = lib.pq.PQexec(con, [[ + select pg_catalog.set_config('search_path', 'public', false) + ]]) + if res ~= nil then defer lib.pq.PQclear(res) end + if res == nil or lib.pq.PQresultStatus(res) ~= lib.pq.PGRES_TUPLES_OK then + lib.warn('failed to secure postgres connection') + lib.pq.PQfinish(con) + return nil + end + + [prep] + return con + end]; + close = [terra(src: &lib.store.source) lib.pq.PQfinish([&lib.pq.PGconn](src.handle)) end]; + + conf_get = [terra(src: &lib.store.source, key: rawstring) + var r = queries.conf_get.exec(src, key) + if r.sz == 0 then return [lib.mem.ptr(int8)] { ptr = nil, ct = 0 } else + defer r:free() + return r:string(0,0) + end + end]; + conf_set = [terra(src: &lib.store.source, key: rawstring, val: rawstring) + queries.conf_set.exec(src, key, val):free() end]; + conf_reset = [terra(src: &lib.store.source, key: rawstring) + queries.conf_reset.exec(src, key):free() end]; + + actor_fetch_uid = [terra(src: &lib.store.source, uid: uint64) + var r = queries.actor_fetch_uid.exec(src, uid) + if r.sz == 0 then + return [lib.stat(lib.store.actor)] { ok = false, error = 1} + else + defer r:free() + var a = [lib.stat(lib.store.actor)] { ok = true } + a.val = row_to_actor(&r, 0) + a.val.source = src + return a + end + end]; +} + +return b Index: cmdparse.t ================================================================== --- cmdparse.t +++ cmdparse.t @@ -1,5 +1,6 @@ +-- vim: ft=terra return function(tbl) local options = terralib.types.newstruct('options') do local flags = '' for _,d in pairs(tbl) do flags = flags .. d[1] end local helpstr = 'usage: parsav [-' .. flags .. '] [...]\n' options.entries = { Index: config.lua ================================================================== --- config.lua +++ config.lua @@ -21,10 +21,11 @@ os.getenv('NIX_STORE') and 'nixos', '')); tgttrip = default('parsav_arch_triple'); -- target triple, used in xcomp tgtcpu = default('parsav_arch_cpu'); -- target cpu, used in xcomp tgthf = u.tobool(default('parsav_arch_armhf',true)); + endian = default('parsav_arch_endian', 'little'); build = { id = u.rndstr(6); release = u.ingest('release'); when = os.date(); }; ADDED file.t Index: file.t ================================================================== --- file.t +++ file.t @@ -0,0 +1,69 @@ +-- vim: ft=terra +-- TODO: add support for windows IO calls +local handle_type = int +local posix = terralib.includec 'fcntl.h' +local unistd = terralib.includec 'unistd.h' + +struct file { + handle: handle_type + read: bool + write: bool +} + +file.mode = { read = 0, write = 1, rw = 2 } +file.seek = { abs = 0, ofs = 1, eof = 2 } + +file.methods = { + open = terra(path: rawstring, mode: uint8) + var f: file + var flag: int + if mode == [file.mode.rw] then + flag = posix.O_RDWR + f.read = true f.write = true + elseif mode == [file.mode.read] then + flag = posix.O_RDONLY + f.read = true f.write = false + elseif mode == [file.mode.read] then + flag = posix.O_WRONLY + f.read = false f.write = true + else lib.bail('invalid file mode') end + lib.dbg('opening file ', path) + f.handle = posix.open(path, flag) + + var r: lib.stat(file) + if f.handle == -1 then + r.ok = false + r.error = 1 -- TODO get errno somehow? + else + r.ok = true + r.val = f + end + return r + end; + close = terra(self: &file) + unistd.close(self.handle) + self.handle = -1 + self.read = false + self.write = false + end; + read = terra(self: &file, dest: rawstring, sz: intptr): ptrdiff + return unistd.read(self.handle,dest,sz) + end; + write = terra(self: &file, data: &opaque, sz: intptr): ptrdiff + return unistd.write(self.handle,data,sz) + end; + seek = terra(self: &file, ofs: ptrdiff, wh: int) + var whence: int + if wh == [file.seek.abs] then + whence = unistd.SEEK_SET + elseif wh == [file.seek.ofs] then + whence = unistd.SEEK_CUR + elseif wh == [file.seek.eof] then + whence = unistd.SEEK_END + else lib.bail('invalid seek mode') end + + return unistd.lseek(self.handle, ofs, whence) + end; +} + +return file Index: makefile ================================================================== --- makefile +++ makefile @@ -20,14 +20,14 @@ dep.mongoose: lib/mongoose/libmongoose.a dep.json-c: lib/json-c/libjson-c.a lib: mkdir $@ -# parsav is designed to be fronted by a real web -# server like nginx if SSL is to be used # generate a shim static library so mongoose cooperates -# with the build apparatus +# with the build apparatus. note that parsav is designed +# to be fronted by a real web server like nginx if SSL +# is to be used, so we don't turn on SSL in mongoose lib/mongoose/libmongoose.a: lib/mongoose lib/mongoose/mongoose.c lib/mongoose/mongoose.h $(CC) -c $> 8 + end + end + return quote + var [a] = src + var [b] = 0 + [steps] + in b end + elseif config.endian == 'big' then return `src + else error('unknown endianness '..config.endian) end +end) + +terra m.shorthand.cval(character: int8): {uint8, bool} + var ch = [uint8](character) + + if ch >= 0x30 and ch <= 0x39 then + ch = 00 + (ch - 0x30) + elseif ch == 0x2d then ch = 10 + elseif ch >= 0x41 and ch <= 0x5a then + ch = 11 + (ch - 0x41) + elseif ch == 0x3a then ch = 37 + elseif ch >= 0x61 and ch <= 0x7a then + ch = 38 + (ch - 0x61) + else return 0, false end + + return ch, true +end + +terra m.shorthand.gen(val: uint64, dest: rawstring): ptrdiff + var lst = "0123456789-ABCDEFGHIJKLMNOPQRSTUVWXYZ:abcdefghijklmnopqrstuvwxyz" + var buf: int8[m.shorthand.maxlen] + var ptr = [&int8](buf) + while val ~= 0 do + var v = val % 64 + @ptr = lst[v] + ptr = ptr + 1 + val = val / 64 + end + var len = ptr - buf + for i = 0, len do + dest[i] = buf[len - (i+1)] + end + dest[len] = 0 + return len +end + +terra m.shorthand.parse(s: rawstring, len: intptr): {uint64, bool} + var val: uint64 = 0 + for i = 0, len do + var v, ok = m.shorthand.cval(s[i]) + if ok == false then return 0, false end + val = (val * 64) + v + end + return val, true +end + +return m Index: parsav.t ================================================================== --- parsav.t +++ parsav.t @@ -36,31 +36,44 @@ end end code[#code+1] = `lib.io.send(2, '\n', 1) return code end; + trn = macro(function(cond, i, e) + return quote + var c: bool = [cond] + var r: i.tree.type + if c == true then r = i else r = e end + in r end + end); proc = { exit = terralib.externfunction('exit', int -> {}); getenv = terralib.externfunction('getenv', rawstring -> rawstring); }; io = { - open = terralib.externfunction('open', {rawstring, int} -> int); - close = terralib.externfunction('close', {int} -> int); send = terralib.externfunction('write', {int, rawstring, intptr} -> ptrdiff); recv = terralib.externfunction('read', {int, rawstring, intptr} -> ptrdiff); say = macro(function(msg) return `lib.io.send(2, msg, [#(msg:asvalue())]) end); fmt = terralib.externfunction('printf', terralib.types.funcpointer({rawstring},{int},true)); }; str = { sz = terralib.externfunction('strlen', rawstring -> intptr); cmp = terralib.externfunction('strcmp', {rawstring, rawstring} -> int); + ncmp = terralib.externfunction('strncmp', {rawstring, rawstring, intptr} -> int); cpy = terralib.externfunction('stpcpy',{rawstring, rawstring} -> rawstring); ncpy = terralib.externfunction('stpncpy',{rawstring, rawstring, intptr} -> rawstring); + ndup = terralib.externfunction('strndup',{rawstring, intptr} -> rawstring); fmt = terralib.externfunction('asprintf', terralib.types.funcpointer({&rawstring},{int},true)); }; + copy = function(tbl) + local new = {} + for k,v in pairs(tbl) do new[k] = v end + setmetatable(new, getmetatable(tbl)) + return new + end; mem = { zero = macro(function(r) return quote for i = 0, [r.tree.type.N] do r[i] = 0 end end @@ -78,34 +91,65 @@ end) }; } local noise = global(uint8,1) +local noise_header = function(code,txt,mod) + if mod then + return string.format('\27[%s;1m(parsav::%s %s)\27[m ', code,mod,txt) + else + return string.format('\27[%s;1m(parsav %s)\27[m ', code,txt) + end +end local defrep = function(level,n,code) return macro(function(...) - local q = lib.emit("\27["..code..";1m(parsav "..n..")\27[m ", ...) + local q = lib.emit(noise_header(code,n), ...) return quote if noise >= level then [q] end end end); end lib.dbg = defrep(3,'debug', '32') lib.report = defrep(2,'info', '35') lib.warn = defrep(1,'warn', '33') lib.bail = macro(function(...) - local q = lib.emit("\27[31;1m(parsav fatal)\27[m ", ...) + local q = lib.emit(noise_header('31','fatal'), ...) return quote [q] lib.proc.exit(1) end end); +lib.stat = terralib.memoize(function(ty) + local n = struct { + ok: bool + union { + error: uint8 + val: ty + } + } + n.name = string.format("stat<%s>", ty.name) + n.stat_basetype = ty + return n +end) +lib.enum = function(tbl) + local ty = uint8 + if #tbl >= 2^32 then ty = uint64 -- hey, can't be too safe + elseif #tbl >= 2^16 then ty = uint32 + elseif #tbl >= 2^8 then ty = uint16 end + local o = { t = ty } + for i, name in ipairs(tbl) do + o[name] = i + end + return o +end lib.mem.ptr = terralib.memoize(function(ty) local t = terralib.types.newstruct(string.format('ptr<%s>', ty)) t.entries = { {'ptr', &ty}; {'ct', intptr}; } + t.ptr_basetype = ty local recurse = false if ty:isstruct() then if ty.methods.free then recurse = true end end t.methods = { @@ -141,24 +185,84 @@ else return false end end; } return t end) +lib.mem.vec = terralib.memoize(function(ty) + local v = terralib.types.newstruct(string.format('vec<%s>', ty.name)) + v.entries = { + {field = 'storage', type = lib.mem.ptr(ty)}; + {field = 'sz', type = intptr}; + {field = 'run', type = intptr}; + } + local terra biggest(a: intptr, b: intptr) + if a > b then return a else return b end + end + terra v:assure(n: intptr) + if self.storage.ct < n then + self.storage:resize(biggest(n, self.storage.ct + self.run)) + end + end + v.methods = { + init = terra(self: &v, run: intptr): bool + if not self.storage:init(run) then return false end + self.run = run + self.sz = 0 + return true + end; + new = terra(self: &v): &ty + self:assure(self.sz + 1) + self.sz = self.sz + 1 + return self.storage.ptr + (self.sz - 1) + end; + push = terra(self: &v, val: ty) + self:assure(self.sz + 1) + self.storage.ptr[self.sz] = val + self.sz = self.sz + 1 + end; + free = terra(self: &v) self.storage:free() end; + last = terra(self: &v, idx: intptr): &ty + if self.sz > idx then + return self.storage.ptr + (self.sz - (idx+1)) + else lib.bail('vector underrun!') end + end; + crush = terra(self: &v) + self.storage:resize(self.sz) + return self.storage + end; + } + v.metamethods.__apply = terra(self: &v, idx: intptr): &ty -- no index?? + if self.sz > idx then + return self.storage.ptr + idx + else lib.bail('vector overrun!') end + end + return v +end) lib.err = lib.loadlib('mbedtls','mbedtls/error.h') lib.rsa = lib.loadlib('mbedtls','mbedtls/rsa.h') lib.pk = lib.loadlib('mbedtls','mbedtls/pk.h') lib.md = lib.loadlib('mbedtls','mbedtls/md.h') lib.b64 = lib.loadlib('mbedtls','mbedtls/base64.h') lib.net = lib.loadlib('mongoose','mongoose.h') lib.pq = lib.loadlib('libpq','libpq-fe.h') +lib.file = terralib.loadfile('file.t')() +lib.math = terralib.loadfile('math.t')() lib.crypt = terralib.loadfile('crypt.t')() lib.http = terralib.loadfile('http.t')() lib.tpl = terralib.loadfile('tpl.t')() lib.string = terralib.loadfile('string.t')() lib.store = terralib.loadfile('store.t')() + +local be = {} +for _, b in pairs { 'pgsql' } do + be[#be+1] = terralib.loadfile('backend/' .. b .. '.t')() +end +lib.store.backends = global(`array([be])) + lib.cmdparse = terralib.loadfile('cmdparse.t')() +lib.srv = terralib.loadfile('srv.t')() do local collate = function(path,f, ...) return loadfile(path..'/'..f..'.lua')(path, ...) end data = { @@ -183,21 +287,10 @@ lib.io.send(1, [rawstring](&buf), lib.str.sz([rawstring](&buf))) lib.io.send(1, '\n', 1) end end) -local handle = { - http = terra(con: &lib.net.mg_connection, event: int, p: &opaque, ext: &opaque) - switch event do - case lib.net.MG_EV_HTTP_MSG then - lib.dbg('routing HTTP request') - var msg = [&lib.net.mg_http_message](p) - - end - end - end; -} do local p = string.format('parsav: %s\nbuilt on %s\n', config.build.str, config.build.when) terra version() lib.io.send(1, p, [#p]) end end terra noise_init() @@ -219,10 +312,13 @@ terra entry(argc: int, argv: &rawstring): int noise_init() [lib.init] + -- shut mongoose the fuck up + lib.net.mg_log_set_callback([terra(msg: &opaque, sz: int, u: &opaque) end], nil) + var mode: options mode:parse(argc,argv) if mode.version then version() return 0 @@ -229,24 +325,18 @@ end if mode.help then lib.io.send(1, [options.helptxt], [#options.helptxt]) return 0 end - - var bind = lib.proc.getenv('parsav_bind') - if bind == nil then bind = '[::]:10917' end - - var nm: lib.net.mg_mgr - lib.net.mg_mgr_init(&nm) - - var nmc = lib.net.mg_http_listen(&nm, bind, handle.http, nil) - + var srv: lib.srv + srv:start('backend.conf') + lib.report('listening for requests') while true do - lib.net.mg_mgr_poll(&nm,1000) + srv:poll() end + srv:shutdown() - lib.net.mg_mgr_free(&nm) return 0 end local bflag = function(long,short) if short and util.has(buildopts, short) then return true end ADDED schema.sql Index: schema.sql ================================================================== --- schema.sql +++ schema.sql @@ -0,0 +1,117 @@ +\prompt 'domain name: ' domain +\prompt 'bind to socket: ' bind +\qecho 'by default, parsav tracks rights on its own. you can override this later by replacing the rights table with a view, but you''ll then need to set appropriate rules on the view to allow administrators to modify rights from the web UI, or set the rights-readonly flag in the config table to true. for now, enter the name of an actor who will be granted full rights when she logs in.' +\prompt 'admin actor: ' admin +\qecho '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.' +\prompt 'auth view: ' auth + +begin; + +drop table if exists parsav_config; +create table if not exists parsav_config ( + key text primary key, + value text +); + +insert into parsav_config (key,value) values + ('bind',:'bind'), + ('domain',:'domain'), + ('auth-source',:'auth'), + ('administrator',:'admin'); + +-- note that valid ids should always > 0, as 0 is reserved for null +-- on the client side, vastly simplifying code +drop table if exists parsav_servers cascade; +create table parsav_servers ( + id bigint primary key default (1+random()*(2^63-1))::bigint, + domain text not null, + key bytea +); +drop table if exists parsav_actors cascade; +create table parsav_actors ( + id bigint primary key default (1+random()*(2^63-1))::bigint, + nym text, + handle text not null, -- nym [@handle@origin] + origin bigint references parsav_servers(id) + on delete cascade, -- null origin = local actor + bio text, + rank smallint not null default 0, + quota integer not null default 1000, + key bytea, -- private if localactor; public if remote + + unique (handle,origin) +); + +drop table if exists parsav_rights cascade; +create table parsav_rights ( + key text, + actor bigint references parsav_actors(id) + on delete cascade, + allow boolean, + + primary key (key,actor) +); + +insert into parsav_actors (handle,rank,quota) values (:'admin',1,0); +insert into parsav_rights (actor,key,allow) + select (select id from parsav_actors where handle=:'admin'), a.column1, a.column2 from (values + ('ban',true), + ('config',true), + ('censor',true), + ('suspend',true), + ('rebrand',true) + ) as a; + +drop table if exists parsav_posts cascade; +create table parsav_posts ( + id bigint primary key default (1+random()*(2^63-1))::bigint, + author bigint references parsav_actors(id) + on delete cascade, + subject text, + body text, + posted timestamp not null, + discovered timestamp not null, + scope smallint not null, + convo bigint, parent bigint, + circles bigint[], mentions bigint[] +); + +drop table if exists parsav_conversations cascade; +create table parsav_conversations ( + id bigint primary key default (1+random()*(2^63-1))::bigint, + uri text not null, + discovered timestamp not null, + head bigint references parsav_posts(id) +); + +drop table if exists parsav_rels cascade; +create table parsav_rels ( + relator bigint references parsav_actors(id) + on delete cascade, -- e.g. follower + relatee bigint references parsav_actors(id) + on delete cascade, -- e.g. follower + kind smallint, -- e.g. follow, block, mute + + primary key (relator, relatee, kind) +); + +drop table if exists parsav_acts cascade; +create table parsav_acts ( + id bigint primary key default (1+random()*(2^63-1))::bigint, + kind text not null, -- like, react, so on + time timestamp not null, + actor bigint references parsav_actors(id) + on delete cascade, + subject bigint -- may be post or act, depending on kind +); + +drop table if exists parsav_log cascade; +create table parsav_log ( + -- accesses are tracked for security & sending delete acts + id bigint primary key default (1+random()*(2^63-1))::bigint, + time timestamp not null, + actor bigint references parsav_actors(id) + on delete cascade, + post bigint not null +); +end; ADDED srv.t Index: srv.t ================================================================== --- srv.t +++ srv.t @@ -0,0 +1,183 @@ +-- vim: ft=terra +local util = dofile 'common.lua' +local struct srv { + sources: lib.mem.ptr(lib.store.source) + webmgr: lib.net.mg_mgr + webcon: &lib.net.mg_connection +} + +local handle = { + http = terra(con: &lib.net.mg_connection, event: int, p: &opaque, ext: &opaque) + switch event do + case lib.net.MG_EV_HTTP_MSG then + lib.dbg('routing HTTP request') + var msg = [&lib.net.mg_http_message](p) + + end + end + end; +} +local char = macro(function(ch) return `[string.byte(ch:asvalue())] end) +local terra cfg(s: &srv, befile: rawstring) + lib.report('configuring backends from ', befile) + + var fr = lib.file.open(befile, [lib.file.mode.read]) + if fr.ok == false then + lib.bail('could not open configuration file ', befile) + end + + var f = fr.val + var c: lib.mem.vec(lib.store.source) c:init(8) + var text: lib.string.acc text:init(64) + do var buf: int8[64] + while true do + var ct = f:read(buf, [buf.type.N]) + if ct == 0 then break end + text:push(buf, ct) + end + end + f:close() + + var cur = text.buf + var segs: tuple(&int8, &int8)[3] = array( + {[&int8](0),[&int8](0)}, + {[&int8](0),[&int8](0)}, + {[&int8](0),[&int8](0)} + ) + var segdup = [terra(s: {rawstring, rawstring}) + var sz = s._1 - s._0 + var str = s._0 + return [lib.mem.ptr(int8)] { + ptr = lib.str.ndup(str, sz); + ct = sz; + } + end] + var fld = 0 + while (cur - text.buf) < text.sz do + if segs[fld]._0 == nil then + if not (@cur == char(' ') or @cur == char('\t') or @cur == char('\n')) then + segs[fld] = {cur, nil} + end + else + if fld < 2 and @cur == char(' ') or @cur == char('\t') then + segs[fld]._1 = cur + fld = fld + 1 + segs[fld] = {nil, nil} + elseif @cur == char('\n') or cur == text.buf + (text.sz-1) then + if fld < 2 then lib.bail('incomplete backend line in ', befile) else + segs[fld]._1 = cur + var src = c:new() + src.id = segdup(segs[0]) + src.string = segdup(segs[2]) + src.backend = nil + for i = 0,[lib.store.backends.type.N] do + if lib.str.ncmp(segs[1]._0, lib.store.backends[i].id, segs[1]._1 - segs[1]._0) == 0 then + src.backend = &lib.store.backends[i] + break + end + end + if src.backend == nil then + lib.bail('unknown backend in ', befile) + end + src.handle = nil + fld = 0 + segs[0] = {nil, nil} + end + end + end + cur = cur + 1 + end + text:free() + + s.sources = c:crush() +end + +--srv.methods.conf_set = terra(self: &srv, key: rawstring, val:rawstring) +-- self.sources.ptr[0]:conf_set(key, val) +--end + +srv.metamethods.__methodmissing = macro(function(meth, self, ...) + local primary, ptr, stat, simple = 0,1,2,3 + local tk, rt = primary + local expr = {...} + for _,f in pairs(lib.store.backend.entries) do + local fn = f.field or f[1] + local ft = f.type or f[2] + if fn == meth then + rt = ft.type.returntype + if rt == bool then tk = simple + elseif rt.stat_basetype then tk = stat + elseif rt.ptr_basetype then tk = ptr end + break + end + end + + if tk == primary then + return `self.sources.ptr[0]:[meth]([expr]) + else local ok, empty + local r = symbol(rt) + if tk == ptr then + ok = `r.ptr ~= nil + empty = `[rt]{ptr=nil,ct=0} + elseif tk == stat then + ok = `r.ok ~= false + empty = `[rt]{ok=false,error=1} + elseif tk == simple then + ok = `r == true + empty = `false + end + return quote + var [r] = empty + for i=0,self.sources.ct do var src = self.sources.ptr + i + if src.handle ~= nil then + r = src:[meth]([expr]) + if [ok] then break + else r = empty end + end + end + in r end + end +end) + +srv.methods.start = terra(self: &srv, befile: rawstring) + cfg(self, befile) + var success = false + for i=0,self.sources.ct do var src = self.sources.ptr + i + lib.report('opening data source ', src.id.ptr, '(', src.backend.id, ')') + src.handle = src.backend.open(src) + if src.handle ~= nil then success = true end + end + if not success then + lib.bail('could not connect to any data sources!') + end + + var dbbind = self:conf_get('bind') + var envbind = lib.proc.getenv('parsav_bind') + var bind: rawstring + if envbind ~= nil then + bind = envbind + elseif dbbind.ptr ~= nil then + bind = dbbind.ptr + else bind = '[::]:10917' end + + lib.report('binding to ', bind) + lib.net.mg_mgr_init(&self.webmgr) + self.webcon = lib.net.mg_http_listen(&self.webmgr, bind, handle.http, nil) + dbbind:free() + +end + +srv.methods.poll = terra(self: &srv) + lib.net.mg_mgr_poll(&self.webmgr,1000) +end + +srv.methods.shutdown = terra(self: &srv) + lib.net.mg_mgr_free(&self.webmgr) + for i=0,self.sources.ct do var src = self.sources.ptr + i + lib.report('closing data source ', src.id.ptr, '(', src.backend.id, ')') + src:close() + end + self.sources:free() +end + +return srv Index: store.t ================================================================== --- store.t +++ store.t @@ -1,15 +1,166 @@ -- vim: ft=terra -local m = {} - -local backend = { - pgsql = { +local m = { + timepoint = uint64; + scope = lib.enum { + 'public', 'private', 'local'; + 'personal', 'direct', 'circle'; + }; + notiftype = lib.enum { + 'mention', 'like', 'rt', 'react' + }; + relation = lib.enum { + 'follow', 'mute', 'block' }; } -struct m.user { - uid: rawstring - nym: rawstring - handle: rawstring +local str = lib.mem.ptr(int8) +str:complete() + +struct m.source + +struct m.rights { + rank: uint16 -- lower = more powerful except 0 = regular user + -- creating staff automatically assigns rank immediately below you + quota: uint32 -- # of allowed tweets per day; 0 = no limit + + -- user powers -- default on + login: bool + visible: bool + post: bool + shout: bool + propagate: bool + upload: bool + + -- admin powers -- default off + ban: bool + config: bool + censor: bool + suspend: bool + rebrand: bool -- modify site's brand identity +} + +terra m.rights_default() + return m.rights { + rank = 0, quota = 1000; + + login = true, visible = true, post = true; + shout = true, propagate = true, upload = true; + + ban = false, config = false, censor = false; + suspend = false, rebrand = false; + } +end + +struct m.actor { + id: uint64 + nym: str + handle: str + origin: uint64 + bio: str + rights: m.rights + key: str + + source: &m.source +} +terra m.actor:free() + self.nym:free() + self.handle:free() + self.bio:free() + self.key:free() +end + +struct m.range { + time: bool + union { + from_time: m.timepoint + from_idx: uint64 + } + union { + to_time: m.timepoint + to_idx: uint64 + } +} + +struct m.post { + id: uint64 + author: uint64 + subject: str + body: str + posted: m.timepoint + discovered: m.timepoint + scope: m.scope.t + mentions: lib.mem.ptr(uint64) + circles: lib.mem.ptr(uint64) --only meaningful if scope is set to circle + convo: uint64 + parent: uint64 + + source: &m.source +} + +local cnf = terralib.memoize(function(ty,rty) + rty = rty or ty + return struct { + enum: {&opaque, uint64, rawstring} -> intptr + get: {&opaque, uint64, rawstring} -> rty + set: {&opaque, uint64, rawstring, ty} -> {} + reset: {&opaque, uint64, rawstring} -> {} + } +end) + +struct m.notif { + kind: m.notiftype.t + when: uint64 + union { + post: uint64 + reaction: int8[8] + } +} + +-- backends only handle content on the local server +struct m.backend { id: rawstring + open: &m.source -> &opaque + close: &m.source -> {} + + conf_get: {&m.source, rawstring} -> lib.mem.ptr(int8) + conf_set: {&m.source, rawstring, rawstring} -> {} + conf_reset: {&m.source, rawstring} -> {} + + actor_save: {&m.source, m.actor} -> bool + actor_create: {&m.source, m.actor} -> bool + actor_fetch_xid: {&m.source, rawstring} -> lib.stat(m.actor) + actor_fetch_uid: {&m.source, uint64} -> lib.stat(m.actor) + actor_notif_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.notif) + actor_auth: {&m.source, rawstring, rawstring} -> lib.stat(m.actor) + actor_enum: {&m.source} -> lib.mem.ptr(m.actor) + actor_enum_local: {&m.source} -> lib.mem.ptr(m.actor) + + actor_conf_str: cnf(rawstring, lib.mem.ptr(int8)) + actor_conf_int: cnf(intptr, lib.stat(intptr)) + + post_save: {&m.source, &m.post} -> bool + post_create: {&m.source, &m.post} -> bool + actor_post_fetch_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(m.post) + convo_fetch_xid: {&m.source,rawstring} -> lib.mem.ptr(m.post) + convo_fetch_uid: {&m.source,uint64} -> lib.mem.ptr(m.post) + + actor_timeline_fetch_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(m.post) + instance_timeline_fetch: {&m.source, m.range} -> lib.mem.ptr(m.post) +} - localuser: bool +struct m.source { + backend: &m.backend + id: lib.mem.ptr(int8) + handle: &opaque + string: lib.mem.ptr(int8) } +terra m.source:free() + self.id:free() + self.string:free() +end +m.source.metamethods.__methodmissing = macro(function(meth, obj, ...) + local q = {...} + -- syntax sugar to forward unrecognized calls onto the backend + return `obj.backend.[meth](&obj, [q]) +end) + +return m