Index: backend/pgsql.t
==================================================================
--- backend/pgsql.t
+++ backend/pgsql.t
@@ -479,20 +479,20 @@
offset $5::bigint
]];
};
artifact_instantiate = {
- params = {binblob, binblob, pstring}, sql = [[
- insert into parsav_artifacts (content,hash,mime) values (
- $1::bytea, $2::bytea, $3::text
+ params = {binblob, binblob, pstring, int64}, sql = [[
+ insert into parsav_artifacts (content,hash,mime,birth) values (
+ $1::bytea, $2::bytea, $3::text,$4::bigint
) on conflict do nothing returning id
]];
};
artifact_expropriate = {
- params = {uint64, uint64, pstring}, cmd = true, sql = [[
- insert into parsav_artifact_claims (uid,rid,description,folder) values (
- $1::bigint, $2::bigint, $3::text, 'new'
+ params = {uint64, uint64, pstring, pstring, int64}, cmd = true, sql = [[
+ insert into parsav_artifact_claims (uid,rid,description,folder,birth) values (
+ $1::bigint, $2::bigint, $3::text, $4::text, $5::bigint
) on conflict do nothing
]];
};
artifact_quicksearch = {
params = {binblob}, sql = [[
@@ -531,10 +531,31 @@
params = {uint64}, sql = [[
delete from parsav_artifact_claims where
rid = $1::bigint
returning uid, description, birth, folder;
]];
+ };
+ artifact_enum_uid = {
+ params = {uint64, pstring}, sql = [[
+ select (pg_temp.parsavpg_translate_artifact(a)).*
+ from parsav_artifact_claims as a where uid = $1::bigint and
+ ($2::text is null or
+ ($2::text = '' and folder is null) or
+ $2::text = folder)
+ order by birth desc
+ ]];
+ };
+ artifact_fetch = {
+ params = {uint64, uint64}, sql = [[
+ select (pg_temp.parsavpg_translate_artifact(a)).*
+ from parsav_artifact_claims as a where uid = $1::bigint and rid = $2::bigint
+ ]];
+ };
+ artifact_load = {
+ params = {uint64}, sql = [[
+ select content, mime from parsav_artifacts where id = $1::bigint
+ ]];
};
post_attach_ctl_ins = {
params = {uint64, uint64}, cmd=true, sql = [[
update parsav_posts set
artifacts = artifacts || $2::bigint
@@ -1532,20 +1553,59 @@
return 0
end
var oldid = srec:int(uint64,0,0)
return oldid
else -- not in db, insert
- var nrec = queries.artifact_instantiate.exec(src, artifact, hashb, mime)
+ var nrec = queries.artifact_instantiate.exec(src, artifact, hashb, mime, lib.osclock.time(nil))
if nrec.sz == 0 then
lib.warn('failed to instantiate artifact -- are you running out of storage?')
return 0
else defer nrec:free()
var newid = nrec:int(uint64,0,0)
return newid
end
end
end];
+
+ artifact_expropriate = [terra(
+ src: &lib.store.source,
+ uid: uint64,
+ artifact: uint64,
+ desc: pstring,
+ folder: pstring
+ ): {}
+ queries.artifact_expropriate.exec(src,uid,artifact,desc,folder, lib.osclock.time(nil))
+ end];
+
+ artifact_enum_uid = [terra(
+ src: &lib.store.source,
+ uid: uint64,
+ folder: pstring
+ )
+ var res = queries.artifact_enum_uid.exec(src,uid,folder)
+ if res.sz > 0 then
+ var m = lib.mem.heapa([lib.mem.ptr(lib.store.artifact)], res.sz)
+ for i=0,res.sz do
+ var id = res:int(uint64,i,0)
+ var idbuf: int8[lib.math.shorthand.maxlen]
+ var idlen = lib.math.shorthand.gen(id, &idbuf[0])
+ var desc = res:_string(i,2)
+ var folder = res:_string(i,3)
+ var mime = res:_string(i,4)
+ var url = lib.str.acc{}:init(48):lpush('/media/a/'):push(&idbuf[0],idlen):finalize() defer url:free()
+ m.ptr[i] = [ lib.str.encapsulate(lib.store.artifact, {
+ desc = {`desc.ptr, `desc.ct + 1};
+ folder = {`folder.ptr, `folder.ct + 1};
+ mime = {`mime.ptr, `mime.ct + 1};
+ url = {`url.ptr, `url.ct + 1};
+ }) ]
+ m(i).ptr.rid = id
+ m(i).ptr.owner = uid
+ end
+ return m
+ else return [lib.mem.lstptr(lib.store.artifact)].null() end
+ end];
post_attach_ctl = [terra(
src: &lib.store.source,
post: uint64,
artifact: uint64,
Index: backend/schema/pgsql-views.sql
==================================================================
--- backend/schema/pgsql-views.sql
+++ backend/schema/pgsql-views.sql
@@ -17,10 +17,26 @@
and counts.kind = 'like'),0)::integer as likes,
coalesce((select counts.ct from counts where counts.subject = p.id
and counts.kind = 'rt' ),0)::integer as rts
from parsav_posts as p
);
+
+create type pg_temp.parsavpg_intern_artifact as (
+ rid bigint,
+ owner bigint,
+ "desc" text,
+ folder text,
+ mime text
+);
+
+create or replace function
+pg_temp.parsavpg_translate_artifact(parsav_artifact_claims)
+returns pg_temp.parsavpg_intern_artifact as $$
+ select ($1).rid, ($1).uid, ($1).description, ($1).folder, a.mime
+ from parsav_artifacts a where
+ a.id = ($1).rid limit 1
+$$ language sql;
create type pg_temp.parsavpg_intern_notice as (
kind smallint,
"when" bigint,
who bigint,
Index: config.lua
==================================================================
--- config.lua
+++ config.lua
@@ -59,10 +59,11 @@
{'retweet.webp', 'image/webp'};
{'padlock.svg', 'image/svg+xml'};
{'warn.svg', 'image/svg+xml'};
{'query.webp', 'image/webp'};
{'reply.webp', 'image/webp'};
+ {'file.webp', 'image/webp'};
-- keep in mind before you add anything to this list: these are not
-- just files parsav can access, they are files that are *kept in
-- memory* for fast access the entire time parsav is running, and
-- which need to be loaded into memory before the program can even
-- start. it's imperative to keep these as small and few in number
Index: http.t
==================================================================
--- http.t
+++ http.t
@@ -1,10 +1,10 @@
-- vim: ft=terra
local m = {}
local util = lib.util
-m.method = lib.enum { 'get', 'post', 'head', 'options', 'put', 'delete' }
+m.method = lib.enum { 'get', 'post', 'post_file', 'head', 'options', 'put', 'delete' }
m.mime = lib.enum {
'html'; -- default
'json';
'mkdown';
'text';
@@ -20,10 +20,16 @@
}
struct m.page {
respcode: uint16
body: lib.mem.ptr(int8)
headers: lib.mem.ptr(m.header)
+}
+struct m.upload {
+ ctype: lib.str.t;
+ filename: lib.str.t;
+ field: lib.str.t;
+ body: lib.str.t;
}
local resps = {
[200] = 'OK';
[201] = 'Created';
Index: makefile
==================================================================
--- makefile
+++ makefile
@@ -1,9 +1,9 @@
dl = git
dbg-flags = $(if $(dbg),-g)
-images = static/default-avatar.webp static/query.webp static/heart.webp static/retweet.webp static/reply.webp
+images = static/default-avatar.webp static/query.webp static/heart.webp static/retweet.webp static/reply.webp static/file.webp
#$(addsuffix .webp, $(basename $(wildcard static/*.svg)))
styles = $(addsuffix .css, $(basename $(wildcard static/*.scss)))
parsav parsavd: parsav.t config.lua pkgdata.lua $(images) $(styles)
terra $(dbg-flags) $<
Index: parsav.t
==================================================================
--- parsav.t
+++ parsav.t
@@ -436,10 +436,12 @@
'render:tweet';
'render:tweet-page';
'render:user-page';
'render:timeline';
'render:notices';
+
+ 'render:media-gallery';
'render:docpage';
'render:conf:profile';
'render:conf:sec';
ADDED render/media-gallery.t
Index: render/media-gallery.t
==================================================================
--- render/media-gallery.t
+++ render/media-gallery.t
@@ -0,0 +1,91 @@
+-- vim: ft=terra
+local pstr = lib.str.t
+local P = lib.str.plit
+local terra cs(s: rawstring)
+ return pstr { ptr = s, ct = lib.str.sz(s) }
+end
+
+local show_all,show_new,show_files,show_vid,show_img=1,2,3,4,5
+
+local terra
+render_media_gallery(co: &lib.srv.convo, path: lib.mem.ptr(lib.mem.ref(int8)), uid: uint64, acc: &lib.str.acc)
+ -- note that when calling this function, path must be adjusted so that path(0)
+ -- eq "media"
+ var owner = false
+ if co.aid ~= 0 and co.who.id == uid then owner = true end
+ var ou = co.srv:actor_fetch_uid(uid)
+ if not ou then goto e404 end
+
+ var view = data.view.media_gallery {
+ menu = pstr{'',0};
+ folders = pstr{'',0};
+ directory = pstr{'',0};
+ images = pstr{'',0};
+ }
+
+ if owner then
+ view.menu = P'upload
'
+ end
+ var mode: uint8 = show_new
+ var folder: pstr
+ if mode == show_new then
+ folder = lib.str.plit''
+ elseif mode == show_all then
+ folder = pstr.null()
+ -- else get folder from query str
+ end
+
+ var md = co.srv:artifact_enum_uid(uid, folder)
+ var gallery: lib.str.acc gallery:init(256)
+ var files: lib.str.acc files:init(256)
+ for i=0,md.ct do
+ if lib.str.ncmp(md(i)(0).mime, 'image/', 6) == 0 then
+ gallery:lpush('
')
+ else
+ files:lpush(''):push(md(i)(0).desc,0)
+ :lpush(''):push(md(i)(0).mime,0)
+ :lpush('')
+ end
+ md(i):free()
+ end
+
+ view.images = gallery:finalize()
+ view.directory = files:finalize()
+
+ if acc ~= nil then
+ view:append(acc)
+ else
+ var pg = view:tostr() defer pg:free()
+ co:stdpage([lib.srv.convo.page] {
+ title = P'media';
+ class = P'media manager';
+ cache = false;
+ body = pg;
+ })
+ end
+
+ view.images:free()
+ view.directory:free()
+ if md:ref() then md:free() end
+ do return end
+
+ ::e404:: co:complain(404,'media not found','no such media exists on this server')
+end
+
+return render_media_gallery
Index: render/nav.t
==================================================================
--- render/nav.t
+++ render/nav.t
@@ -5,12 +5,12 @@
if co.who ~= nil or co.srv.cfg.pol_sec == lib.srv.secmode.public then
t:lpush(' timeline')
end
if co.who ~= nil then
t:lpush(' composeprofileconfiguredocslog outnotices')
+ t:lpush('">profile mediaconfiguredocslog outnotices')
else
t:lpush(' docslog in')
end
return t:finalize()
end
return render_nav
Index: route.t
==================================================================
--- route.t
+++ route.t
@@ -1,10 +1,11 @@
-- 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
@@ -397,11 +398,77 @@
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
+ ::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)
+ if meth == method.post then
+ goto badop
+ end
+
+ if path.ct == 1 or (path.ct >= 3 and path(1):cmp(lib.str.lit'a')) then
+ if meth == method.post then goto badop end
+ lib.render.media_gallery(co,path,co.who.id,nil)
+ elseif path.ct == 2 then
+ if 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:tostr() defer pg:free()
+ co:stdpage([lib.srv.convo.page] {
+ title = lib.str.plit'media :: upload';
+ class = lib.str.plit'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(lib.str.plit'desc') then
+ desc = up.body
+ elseif up.field:cmp(lib.str.plit'folder') then
+ folder = up.body
+ elseif up.field:cmp(lib.str.plit'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
+ end
+ else goto e404 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)
@@ -485,10 +552,12 @@
http.actor_profile_uid(co, path, meth)
elseif path.ct > 1 and path(0):cmp(lib.str.lit('post')) then
http.tweet_page(co, path, meth)
elseif path(0):cmp(lib.str.lit('tl')) then
http.timeline(co, path)
+ elseif path(0):cmp(lib.str.lit('media')) then
+ http.media_manager(co, path, meth)
elseif path(0):cmp(lib.str.lit('doc')) then
if not meth_get(meth) then goto wrongmeth end
http.documentation(co, path)
elseif path(0):cmp(lib.str.lit('conf')) then
if co.aid == 0 then goto unauth end
Index: srv.t
==================================================================
--- srv.t
+++ srv.t
@@ -136,10 +136,12 @@
who: &lib.store.actor -- who we're logged in as, if aid ~= 0
peer: lib.store.inet
reqtype: lib.http.mime.t -- negotiated content type
method: lib.http.method.t
live_last: lib.store.timepoint
+ uploads: lib.mem.vec(lib.http.upload)
+ body: lib.str.t
-- cache
ui_hue: uint16
navbar: lib.mem.ptr(int8)
actorcache: lib.mem.cache(lib.mem.ptr(lib.store.actor),32) -- naive cache to avoid unnecessary queries
-- private
@@ -330,14 +332,10 @@
terra convo:pgetv(name: rawstring)
var s,l = self:getv(name)
return pstring { ptr = s, ct = l }
end
-local urimatch = macro(function(uri, ptn)
- return `lib.net.mg_globmatch(ptn, [#ptn], uri.ptr, uri.ct+1)
-end)
-
local route = {} -- these are defined in route.t, as they need access to renderers
terra route.dispatch_http :: {&convo, lib.mem.ptr(int8), lib.http.method.t} -> {}
local mimetypes = {
{'html', 'text/html'};
@@ -393,10 +391,11 @@
} co.varbuf.ptr = nil
co.navbar.ptr = nil
co.actorcache.top = 0
co.actorcache.cur = 0
co.ui_hue = server.cfg.ui_hue
+ co.body.ptr = msg.body.ptr co.body.ct = msg.body.len
-- first, check for an accept header. if it's there, we need to
-- iterate over the values and pick the highest-priority one
do var acc = lib.http.findheader(msg, 'Accept')
-- TODO handle q-value
@@ -513,24 +512,132 @@
else uri.ct = urideclen end
lib.dbg('routing URI ', {uri.ptr, uri.ct})
if lib.str.ncmp('GET', msg.method.ptr, msg.method.len) == 0 then
co.method = [lib.http.method.get]
- route.dispatch_http(&co, uri, [lib.http.method.get])
elseif lib.str.ncmp('POST', msg.method.ptr, msg.method.len) == 0 then
- co.method = [lib.http.method.get]
- route.dispatch_http(&co, uri, [lib.http.method.post])
+ co.method = [lib.http.method.post]
elseif lib.str.ncmp('HEAD', msg.method.ptr, msg.method.len) == 0 then
co.method = [lib.http.method.head]
- route.dispatch_http(&co, uri, [lib.http.method.head])
elseif lib.str.ncmp('OPTIONS', msg.method.ptr, msg.method.len) == 0 then
co.method = [lib.http.method.options]
- route.dispatch_http(&co, uri, [lib.http.method.options])
else
co:complain(400,'unknown method','you have submitted an invalid http request')
+ goto fail
+ end
+ -- check for a content-type header, and see if it's a multipart/
+ -- form-data encoded POST request so we can handle file uploads
+ co.uploads.sz = 0 co.uploads.run = 0
+ if co.method == [lib.http.method.post] then
+ var ctt = lib.http.findheader(msg, 'Content-Type')
+ if ctt ~= nil then
+ lib.dbg('found content type', {ctt.ptr,ctt.ct})
+ if lib.str.ncmp(ctt.ptr,'multipart/form-data;',20) == 0 then
+ var p = lib.str.ffw(ctt.ptr + 20,ctt.ct-20)
+ if lib.str.ncmp(p,'boundary=',9) ~= 0 then
+ co:complain(400,'bad request','unrecognized content-type')
+ goto fail
+ end
+ var boundary = pstring {ptr=p+9,ct=ctt.ct - ((p - ctt.ptr) + 9)}
+ lib.dbg('got boundary ',{boundary.ptr,boundary.ct})
+ co.method = lib.http.method.post_file
+ co.uploads:init(8)
+
+ var bsr = (lib.str.acc{}):compose('\r\n--',boundary,'\r\n'):finalize()
+
+ var upmap = lib.str.splitmap(co.body,bsr,8)
+ -- first entry may not be preceded by header-break
+ if lib.str.find(upmap(0), pstring {
+ ptr = bsr.ptr + 2, ct = bsr.ct - 2
+ }):ref() then
+ upmap(0).ptr = upmap(0).ptr + (bsr.ct - 2)
+ upmap(0).ct = upmap(0).ct - (bsr.ct - 2)
+ end
+
+ -- last entry is weird
+ do var lsr = (lib.str.acc{}):compose('\r\n--',boundary,'--\r\n'):finalize()
+ var lsent = upmap.ptr + (upmap.ct - 1)
+ var halt = lib.str.find(@lsent, lsr)
+ if halt:ref() then
+ lsent.ct = halt.ptr - lsent.ptr
+ end
+ lsr:free() end
+
+ for i=0,upmap.ct do
+ var hdrbrk = lib.str.find(upmap(i), lib.str.plit'\r\n\r\n')
+ if hdrbrk:ref() then
+ lib.dbg('got new entry')
+ var hdrtxt = pstring {upmap(i).ptr,upmap(i).ct - hdrbrk.ct}
+ var hdrs = lib.str.splitmap(hdrtxt, '\r\n',6)
+ var ctt = pstring.null()
+ var ctd = pstring.null()
+ for j=0, hdrs.ct do
+ var brk = lib.str.find(hdrs(j),lib.str.plit':')
+ if brk:ref() then
+ var hdr = pstring{hdrs(j).ptr,hdrs(j).ct - brk.ct}
+ var val = pstring{brk.ptr+1, brk.ct-1}:ffw()
+ if hdr:cmp(lib.str.plit'Content-Type') then
+ ctt = val
+ elseif hdr:cmp(lib.str.plit'Content-Disposition') then
+ ctd = val
+ end
+ end
+ end
+ if ctd:ref() then
+ var ctdvals = lib.str.splitmap(ctd, ';', 4) defer ctdvals:free()
+ if ctdvals(0):cmp(lib.str.plit'form-data') and ctdvals.ct > 1 then
+ lib.dbg('found form data')
+ var fld = pstring.null()
+ var file = pstring.null()
+ for j=1, ctdvals.ct do var v = ctdvals(j):ffw()
+ var x = lib.str.find(v,lib.str.plit'=')
+ if x:ref() then
+ var key = pstring{v.ptr, v.ct - x.ct}
+ var val = pstring{x.ptr + 1, x.ct - 1}
+ var decval, ofs, sp = lib.str.toknext(val,@';',true)
+ if key:cmp(lib.str.plit'name') then
+ fld = decval
+ elseif key:cmp(lib.str.plit'filename') then
+ file = decval
+ else decval:free() end
+ end
+ end
+ if fld:ref() then
+ var nextup = co.uploads:new()
+ if ctt:ref() then
+ nextup.ctype = ctt
+ else
+ nextup.ctype = pstring.null()
+ end
+ nextup.body = pstring {
+ ptr = hdrbrk.ptr + 4;
+ ct = hdrbrk.ct - 4;
+ }
+ nextup.ctype = ctt
+ nextup.field = fld
+ nextup.filename = file
+ end
+ end
+ end
+ end
+ end
+ bsr:free()
+ upmap:free()
+ end
+ end
+ end
+
+ route.dispatch_http(&co, uri, co.method)
+ if co.uploads.run > 0 then
+ for i=0,co.uploads.sz do
+ co.uploads(i).filename:free()
+ co.uploads(i).field:free()
+ end
+ co.uploads:free()
end
+ ::fail::
if co.aid ~= 0 then lib.mem.heapf(co.who) end
if co.varbuf.ptr ~= nil then co.varbuf:free() end
if co.navbar.ptr ~= nil then co.navbar:free() end
co.actorcache:free()
end
ADDED static/file.svg
Index: static/file.svg
==================================================================
--- static/file.svg
+++ static/file.svg
@@ -0,0 +1,332 @@
+
+
+
+
Index: static/style.scss
==================================================================
--- static/style.scss
+++ static/style.scss
@@ -1004,5 +1004,67 @@
margin: 0.1in 0.2in;
margin-left: 0.4in;
}
}
}
+
+.media.manager main, .media.gallery {
+ display: grid;
+ grid-template-columns: 2in 1fr;
+ grid-template-rows: max-content 1fr;
+ menu {
+ @extend %navmenu;
+ }
+ .gallery, .dir {
+ background: tone(-55%,-0.5);
+ border: 1px solid tone(-60%);
+ padding: 0.2in;
+ display: flex;
+ flex-wrap: wrap;
+ }
+ .gallery {
+ grid-row: 1/2; grid-column: 2/3;
+ margin-left: 0.1in;
+ flex-flow: row;
+ > a[href].thumb {
+ display: block;
+ width: 1.5in;
+ padding: 0.1in;
+ height: max-content;
+ > img {
+ width: 1.5in; height: 1.5in;
+ }
+ > .caption {
+ text-align: center;
+ font-size: 80%;
+ }
+ }
+ }
+ .dir {
+ grid-row: 2/3; grid-column: 1/3;
+ margin-top: 0.1in;
+ flex-flow: column;
+ flex-grow: 1;
+ > a[href].file {
+ padding: 0.1in 0.15in;
+ text-decoration: none;
+ height: max-content;
+ background-image: url(/s/file.webp); //TODO different icons for different mime types
+ background-repeat: no-repeat;
+ background-position: left;
+ padding-left: 0.4in;
+ > .label {
+ text-decoration: underline;
+ }
+ > .mime {
+ font-style: italic;
+ opacity: 60%;
+ margin-left: 0.5ex;
+ }
+ }
+ }
+}
+
+.media.upload form {
+ padding: 0.1in 0.2in;
+ @extend %box;
+}
Index: store.t
==================================================================
--- store.t
+++ store.t
@@ -228,10 +228,19 @@
isreply: bool
source: &m.source
-- save :: bool -> {} (defined in acl.t due to dep. hell)
}
+
+struct m.artifact {
+ rid: uint64
+ owner: uint64
+ desc: str
+ folder: str
+ mime: str
+ url: str
+}
m.user_conf_funcs = function(be,n,ty,rty,rty2)
rty = rty or ty
local gt
if not rty2 -- what the fuck?
@@ -449,15 +458,19 @@
artifact_quicksearch: {&m.source, lib.mem.ptr(uint8)} -> {uint64,bool}
-- checks whether a hash is already in the database without uploading
-- the entire file to the database server
-- hash: bytea
--> artifact id (0 if null), suppressed?
- artifact_expropriate: {&m.source, uint64, uint64, lib.mem.ptr(int8)} -> {}
+ artifact_expropriate: {&m.source, uint64, uint64, lib.str.t, lib.str.t} -> {}
-- claims an existing artifact for the user's own collection
-- uid: uint64
-- artifact id: uint64
-- description: pstring
+ -- folder: pstring
+ artifact_claim_alter: {&m.source, uint64, uint64, lib.str.t, lib.str.t} -> {}
+ -- edits an existing claim to an artifact
+ -- ibid
artifact_disclaim: {&m.source, uint64, uint64} -> {}
-- a user disclaims their ownership stake in an artifact, removing it from
-- the database entirely if they were the only owner, and removing their
-- description of it either way
-- uid: uint64
@@ -468,10 +481,19 @@
-- the artifact will be banned and attempts to upload it in the future
-- will fail, triggering a report. mainly intended for dealing with spam,
-- IP violations, That Which Shall Not Be Named, and various other infohazards.
-- artifact id: uint64
-- blacklist: bool
+ artifact_enum_uid: {&m.source, uint64, lib.str.t} -> lib.mem.lstptr(m.artifact)
+ -- produces a list of artifacts claimed by a user, optionally
+ -- restricted by folder (empty string = new only)
+ artifact_fetch: {&m.source, uint64, uint64} -> lib.mem.ptr(m.artifact)
+ -- fetch a user's view of an artifact
+ -- uid: uint64
+ -- rid: uint64
+ artifact_load: {&m.source, uint64} -> {lib.mem.ptr(uint8),lib.str.t}
+ -- load the body of an artifact into memory (also returns mime)
nkvd_report_issue: {&m.source, &m.kompromat} -> {}
-- an incidence of Badthink has been detected. report it immediately
-- to the Supreme Soviet
nkvd_reports_enum: {&m.source, &m.kompromat} -> lib.mem.ptr(m.kompromat)
Index: str.t
==================================================================
--- str.t
+++ str.t
@@ -19,10 +19,21 @@
terralib.types.funcpointer({&rawstring,rawstring},{int},true));
bfmt = terralib.externfunction('sprintf',
terralib.types.funcpointer({rawstring,rawstring},{int},true));
span = terralib.externfunction('strspn',{rawstring, rawstring} -> rawstring);
}
+
+terra m.ffw(str: &int8, maxlen: intptr)
+ if maxlen == 0 then maxlen = m.sz(str) end
+ while maxlen > 0 and @str ~= 0 and
+ (@str == @' ' or @str == @'\t' or @str == @'\n') do
+ str = str + 1
+ maxlen = maxlen - 1
+ end
+ return str
+end
+
do local strptr = (lib.mem.ptr(int8))
local strref = (lib.mem.ref(int8))
local byteptr = (lib.mem.ptr(uint8))
strptr.metamethods.__cast = function(from,to,e)
@@ -53,11 +64,15 @@
if self.ptr[i] == 0 and other.ptr[i] == 0 then return true end
if self.ptr[i] ~= other.ptr[i] then return false end
end
return true
end
-
+ terra strptr:ffw()
+ var newp = m.ffw(self.ptr,self.ct)
+ var newct = self.ct - (newp - self.ptr)
+ return strptr { ptr = newp, ct = newct }
+ end
strptr.methods.cmpl = macro(function(self,other)
return `self:cmp(strptr { ptr = [other:asvalue()], ct = [#(other:asvalue())] })
end)
strref.methods.cmpl = macro(function(self,other)
return `self:cmp(strref { ptr = [other:asvalue()], ct = [#(other:asvalue())] })
@@ -359,23 +374,79 @@
end
end
return maxlen
end
-terra m.ffw(str: &int8, maxlen: intptr)
- while maxlen > 0 and @str ~= 0 and
- (@str == @' ' or @str == @'\t' or @str == @'\n') do
- str = str + 1
- maxlen = maxlen - 1
- end
- return str
-end
-
terra m.ffw_unsafe(str: &int8)
while @str ~= 0 and
(@str == @' ' or @str == @'\t' or @str == @'\n') do
str = str + 1
end
return str
end
+
+terra m.find(haystack: pstr, needle: pstr): pstr
+ for i=0,haystack.ct do
+ for j=0, needle.ct do
+ if haystack(i + j) ~= needle(j) then goto nomatch end
+ end
+ do return pstr {
+ ptr = haystack.ptr + i;
+ ct = haystack.ct - i;
+ } end
+ ::nomatch::end
+ return pstr.null()
+end
+
+terra m.splitmap(str: pstr, delim: pstr, expect: uint16)
+ var vec: lib.mem.vec(pstr) vec:init(expect)
+ var start = pstr{str.ptr, str.ct}
+ while true do
+ var n = m.find(start, delim)
+ if not n then break end
+ vec:push(pstr {ptr = start.ptr, ct = start.ct - n.ct})
+ n.ptr = n.ptr + delim.ct
+ n.ct = n.ct - delim.ct
+ start = n
+ end
+ vec:push(start)
+ return vec:crush()
+end
+
+terra m.toknext(str: m.t, delim: int8, brkspace: bool): {pstr,intptr,bool}
+ var b: m.acc b:init(48)
+ var mode: int8 = 0
+ var esc = false
+ var spacebroke = false
+ var max = 0
+ for i=0, str.ct do
+ max = i
+ if str(i) == 0 then break
+ elseif esc == true then b:push(str.ptr + i,1) esc = false
+ elseif str(i) == @'\\' then esc = true
+
+ elseif mode == 0 and str(i) == delim then break
+ elseif mode ~= 2 and str(i) == @'"' then
+ if mode == 1
+ then mode = 0
+ else mode = 1
+ end
+ elseif mode ~= 1 and str(i) == @"'" then
+ if mode == 2
+ then mode = 0
+ else mode = 2
+ end
+
+ elseif brkspace and mode == 0 and (
+ str(i) == @' ' or str(i) == @'\t' or
+ str(i) == @'\r' or str(i) == @'\n') then
+ spacebroke = true
+ break
+
+ else b:push(str.ptr + i,1) end
+ end
+ if mode ~= 0 then return m.t.null(), 0, false end
+
+ return b:finalize(), max, spacebroke
+end
return m
Index: view/load.lua
==================================================================
--- view/load.lua
+++ view/load.lua
@@ -8,10 +8,13 @@
'confirm';
'tweet';
'profile';
'compose';
'notice';
+
+ 'media-gallery';
+ 'media-upload';
'login-username';
'login-challenge';
'conf';
ADDED view/media-gallery.tpl
Index: view/media-gallery.tpl
==================================================================
--- view/media-gallery.tpl
+++ view/media-gallery.tpl
@@ -0,0 +1,19 @@
+
+
+