Index: backend/pgsql.t ================================================================== --- backend/pgsql.t +++ backend/pgsql.t @@ -242,13 +242,13 @@ key = $2::text ]] }; actor_rel_create = { - params = {uint16,uint64, uint64}, cmd = true, sql = [[ - insert into parsav_rels (kind,relator,relatee) - values($1::smallint, $2::bigint, $3::bigint) + params = {uint16,uint64, uint64, int64}, cmd = true, sql = [[ + insert into parsav_rels (kind,relator,relatee,since) + values($1::smallint, $2::bigint, $3::bigint, $4::bigint) on conflict do nothing ]]; }; actor_rel_destroy = { @@ -1501,11 +1501,11 @@ actor_rel_create = [terra( src: &lib.store.source, kind: uint16, relator: uint64, relatee: uint64 - ): {} queries.actor_rel_create.exec(src,kind,relator,relatee) end]; + ): {} queries.actor_rel_create.exec(src,kind,relator,relatee,lib.osclock.time(nil)) end]; actor_rel_destroy = [terra( src: &lib.store.source, kind: uint16, relator: uint64, Index: backend/schema/pgsql-views.sql ================================================================== --- backend/schema/pgsql-views.sql +++ backend/schema/pgsql-views.sql @@ -55,13 +55,14 @@ ($1).subject, null::bigint, ($1).body )::pg_temp.parsavpg_intern_notice as notice from (values - ('rt', ), - ('like', ), - ('react', ) + ('rt', ), + ('like', ), + ('react', ), + ('follow',) ) as kmap(kstr,kind) where kmap.kstr = ($1).kind $$ language sql; create type pg_temp.parsavpg_intern_actor as ( id bigint, @@ -210,10 +211,25 @@ par.author as rcpt from parsav_posts as p inner join parsav_posts as par on p.parent = par.id left join ntimes as nt on nt.uid = p.author where p.discovered >= coalesce(nt.when,0) - ), allnotices as (select * from acts union select * from replies) + ), follows as ( + select row( + ::smallint, + r.since, + r.relator, + r.relatee, + null::bigint, + null::text + )::pg_temp.parsavpg_intern_notice as notice, + r.relatee as rcpt + from parsav_rels as r + left join ntimes as nt on nt.uid = r.relatee + where + r.since >= coalesce(nt.when,0) and + r.kind = + ), allnotices as (table acts union table replies union table follows) table allnotices order by (notice).when desc ); Index: backend/schema/pgsql.sql ================================================================== --- backend/schema/pgsql.sql +++ backend/schema/pgsql.sql @@ -87,11 +87,12 @@ 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. followed - kind smallint, -- e.g. follow, block, mute + kind smallint not null, -- e.g. follow, block, mute + since bigint not null, primary key (relator, relatee, kind) ); comment on table parsav_rels is 'all relationships, positive and negative, between local users and other users; kind is a version-specific integer mapping to a type-of-relationship enum in store.t'; Index: config.lua ================================================================== --- config.lua +++ config.lua @@ -60,10 +60,11 @@ {'padlock.svg', 'image/svg+xml'}; {'warn.svg', 'image/svg+xml'}; {'query.webp', 'image/webp'}; {'reply.webp', 'image/webp'}; {'file.webp', 'image/webp'}; + {'follow.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 @@ -61,16 +61,16 @@ var [resptext] var [resplen] switch code do [respbranches] end return resptext, resplen end -terra m.hier(uri: lib.mem.ptr(int8)): lib.mem.ptr(lib.mem.ref(int8)) +terra m.hier(pool: &lib.mem.pool, uri: lib.mem.ptr(int8)): lib.mem.ptr(lib.mem.ref(int8)) if uri.ct == 0 then return [lib.mem.ptr(lib.mem.ref(int8))] { ptr = nil, ct = 0 } end var sz = 1 var start = 0 if uri.ptr[0] == @'/' then start = 1 end for i = start, uri.ct do if uri.ptr[i] == @'/' then sz = sz + 1 end end - var lst = lib.mem.heapa([lib.mem.ref(int8)], sz) + var lst = pool:alloc([lib.mem.ref(int8)], sz) if sz == 0 then lst.ptr[0].ptr = uri.ptr lst.ptr[0].ct = uri.ct return lst end 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 static/file.webp +images = static/default-avatar.webp static/query.webp static/heart.webp static/retweet.webp static/reply.webp static/file.webp static/follow.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: mem.t ================================================================== --- mem.t +++ mem.t @@ -209,11 +209,10 @@ self.debris.storage = nil return self end terra m.pool:free(): {} -lib.io.fmt('DRAINING POOL %p\n',self.storage) if self.storage == nil then return end if self.debris.storage ~= nil then self.debris:free() end m.heapf(self.debris) -- storage + debris field allocated in one block self.storage = nil self.cursor = nil @@ -226,22 +225,20 @@ self.cursor = self.storage return self end terra m.pool:alloc_bytes(sz: intptr): &opaque - var space = self.sz - ([&uint8](self.cursor) - [&uint8](self.storage)) -lib.io.fmt('%p / %p @ allocating %llu bytes in %llu of space\n',self.storage,self.cursor,sz,space) + var space: intptr = self.sz - ([&uint8](self.cursor) - [&uint8](self.storage)) if space < sz then -lib.dbg('reserving more space') - self:cue(space + sz + 256) end + self:cue(self.sz + sz + 256) end var ptr = self.cursor self.cursor = [&opaque]([&uint8](self.cursor) + sz) return ptr end terra m.pool:realloc_bytes(oldptr: &opaque, oldsz: intptr, newsz: intptr): &opaque - var space = self.sz - ([&uint8](self.cursor) - [&uint8](self.storage)) + var space: intptr = self.sz - ([&uint8](self.cursor) - [&uint8](self.storage)) var cur = [&uint8](self.cursor) if cur - [&uint8](oldptr) == oldsz and newsz - oldsz < space then lib.dbg('moving pool cursor') cur = cur + (newsz - oldsz) self.cursor = [&opaque](cur) Index: parsav.md ================================================================== --- parsav.md +++ parsav.md @@ -137,6 +137,6 @@ * mariadb/mysql * the various nosql horrors, e.g. redis, mongo, and so on parsav urgently needs an internationalization framework as well. right now everything is just hardcoded in english. yuck. -parsav could be significantly improved by adjusting its memory management strategy. instead of allocating everything with lib.mem.heapa (which currently maps to malloc on all platforms), we should allocate a static buffer for the server overlord object which can simply be cleared and re-used for each http request, and enlarged with `realloc` when necessary. the entire region could be `mlock`ed for better performance, and it would no longer be necessary to track and free memory, as the entire buffer would simply be discarded after use (similar to PHP's original memory management strategy). this would remove possibly the largest source of latency in the codebase, as `parsav` is regrettably quite heavy on malloc, performing numerous allocations for each page rendered. +parsav could be significantly improved by adjusting its memory management strategy. instead of allocating everything with lib.mem.heapa (which currently maps to malloc on all platforms), we should allocate a static buffer for the server overlord object which can simply be cleared and re-used for each http request, and enlarged with `realloc` when necessary. the entire region could be `mlock`ed for better performance, and it would no longer be necessary to track and free memory, as the entire buffer would simply be discarded after use (similar to PHP's original memory management strategy). this would remove possibly the largest source of latency in the codebase, as `parsav` is regrettably quite heavy on malloc, performing numerous allocations for each page rendered. **update:** this is now in progress Index: render/notices.t ================================================================== --- render/notices.t +++ render/notices.t @@ -32,12 +32,11 @@ var n = data.view.notice { avatar = cs(who(0).avatar); nym = lib.render.nym(who.ptr,0,nil,true); pflink = pstr{ptr = pflink.buf; ct = pflink.sz}; } - var notweet = true - var what = co.srv:post_fetch(notes(i).what) defer what:free() + var notweet, nopost = true, false switch notes(i).kind do case lib.store.noticetype.rt then n.kind = P'rt' n.act = P'retweeted your post' end @@ -47,25 +46,30 @@ end case lib.store.noticetype.reply then n.kind = P'reply' n.act = P'replied to your post' notweet = false + end + case lib.store.noticetype.follow then + n.kind = P'follow' + n.act = P'followed you!' + nopost = true end else goto skip end - do var idbuf: int8[lib.math.shorthand.maxlen] - var idlen = lib.math.shorthand.gen(notes(i).what, idbuf) + if not nopost then + var what = co.srv:post_fetch(notes(i).what) defer what:free() var b = lib.smackdown.html(&co.srv.pool, pstr {ptr=what(0).body,ct=0},true) --defer b:free() - body:lpush(' '):ppush(b):lpush('') + body:lpush(' '):ppush(b):lpush('') end if not notweet then var reply = co.srv:post_fetch(notes(i).reply) lib.render.tweet(co,reply.ptr,&body) reply:free() end n.ref = pstr {ptr = body.buf, ct = body.sz} - n:append(&pg) + ::skip:: n.nym:free() pflink:reset() body:reset() end --pflink:free() Index: render/profile.t ================================================================== --- render/profile.t +++ render/profile.t @@ -68,14 +68,16 @@ -- this is really more what epithets are for, i think if actor.rights.rank > 0 and stafftxt:ref() then comments:lpush('
  • '):ppush(stafftxt):lpush('
  • ') end - if co.who:outranks(actor) then - comments:lpush('
  • underling
  • ') - elseif actor:outranks(co.who) then - comments:lpush('
  • outranks you
  • ') + if co.who.rights.rank ~= 0 then + if co.who:outranks(actor) then + comments:lpush('
  • underling
  • ') + elseif actor:outranks(co.who) then + comments:lpush('
  • outranks you
  • ') + end end if relationship.recip.follow() then comments:lpush('
  • follows you
  • ') end Index: route.t ================================================================== --- route.t +++ route.t @@ -13,19 +13,35 @@ var rel: lib.store.relationship if co.aid ~= 0 then rel = co.srv:actor_rel_calc(co.who.id, actor.id) if meth == method.post then var act = co:ppostv('act') - if act:cmp(lib.str.plit 'follow') and not rel.rel.follow() then - if rel.recip.block() then + if rel.recip.block() then + if act:cmp(lib.str.plit 'follow') or act:cmp(lib.str.plit 'subscribe') then co:complain(403,'blocked','you cannot follow a user you are blocked by') return end - (rel.rel.follow << true) - co.srv:actor_rel_create([lib.store.relation.idvmap.follow], co.who.id, actor.id) - elseif act:cmp(lib.str.plit 'unfollow') and rel.rel.follow() then - (rel.rel.follow << false) - co.srv:actor_rel_destroy([lib.store.relation.idvmap.follow], co.who.id, actor.id) + end + if act:cmp(lib.str.plit 'block') and not rel.rel.block() then + (rel.rel.block << true) ; (rel.recip.follow << false) + co.srv:actor_rel_create([lib.store.relation.idvmap.block], co.who.id, actor.id) + co.srv:actor_rel_destroy([lib.store.relation.idvmap.follow], actor.id, co.who.id) + else + [(function() + local tests = quote co:complain(400,'bad request','the action you have attempted on this user is not meaningful') return end + for i,v in ipairs(lib.store.relation.members) do + tests = quote + if [v ~= 'block'] and act:cmp(lib.str.plit([v])) and not rel.rel.[v]() then -- rely on dead code elimination :/ + (rel.rel.[v] << true) + co.srv:actor_rel_create([lib.store.relation.idvmap[v]], co.who.id, actor.id) + elseif act:cmp(lib.str.plit(['un'..v])) and rel.rel.[v]() then + (rel.rel.[v] << false) + co.srv:actor_rel_destroy([lib.store.relation.idvmap[v]], co.who.id, actor.id) + else [tests] end + end + end + return tests + end)()] end end else rel.rel:clear() rel.recip:clear() @@ -44,11 +60,11 @@ uri:advance(uri.ct) elseif handle.ct + 2 < uri.ct then uri:advance(handle.ct + 2) end lib.dbg('looking up user by xid "', {handle.ptr,handle.ct} ,'", path: ', {uri.ptr,uri.ct}) - var path = lib.http.hier(uri) defer path:free() + var path = lib.http.hier(&co.srv.pool, uri) --defer path:free() for i=0,path.ct do lib.dbg('got path component ', {path.ptr[i].ptr, path.ptr[i].ct}) end var actor = co.srv:actor_fetch_xid(handle) @@ -320,11 +336,11 @@ lib.dbg('showing credentials') if act:cmp(lib.str.plit 'invalidate') then lib.dbg('setting user\'s cookie validation time to now') co.who.source:auth_sigtime_user_alter(uid, lib.osclock.time(nil)) -- the current session has been invalidated as well, so we need to immediately install a new authentication cookie with the same aid so the user doesn't need to log back in all over again - co:installkey('/conf/sec',co.aid) + co:installkey('?',co.aid) return elseif act:cmp(lib.str.plit 'newcred') then var cmt = co:ppostv('comment') var pw = co:ppostv('newpw') var aid: uint64 = 0 @@ -694,11 +710,11 @@ if co.aid == 0 then goto notfound else co:reroute_cookie('/','auth=; Path=/') end else -- hierarchical routes - var path = lib.http.hier(uri) defer path:free() + var path = lib.http.hier(&co.srv.pool, uri) --defer path:free() if path.ct > 1 and path(0):cmp(lib.str.lit('user')) then 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 ADDED static/follow.svg Index: static/follow.svg ================================================================== --- static/follow.svg +++ static/follow.svg @@ -0,0 +1,513 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + Index: static/style.scss ================================================================== --- static/style.scss +++ static/style.scss @@ -587,12 +587,12 @@ transition: 0.3s; opacity: 0.0001; // qutebrowser won't show hints if opacity=0 :( &:hover, &:focus { opacity: 0.6 !important; } } } - > .like { background-image: url(/s/heart.webp); } - > .rt { background-image: url(/s/retweet.webp); } + > .like { background-image: url(/s/heart.webp); } + > .rt { background-image: url(/s/retweet.webp); } } // used for keyboard navigation &.live-selected { //margin-left: 0.4in; margin-right: -0.4in; @@ -990,21 +990,22 @@ div.notice { padding: 0.15in; background: linear-gradient(to bottom, tone(10%, -0.9), transparent); border: 1px solid tone(-60%); & + div.notice { border-top: none; } - &.rt, &.like, &.reply { &::before { + &.rt, &.like, &.reply, &.follow { &::before { display: inline-block; width: 1em; height: 1em; margin-right: 1ex; background-size: contain; vertical-align: bottom; content: ""; // 🙄 }} - &.rt::before { background-image: url(/s/retweet.webp); } - &.like::before { background-image: url(/s/heart.webp); } - &.reply::before { background-image: url(/s/reply.webp); } + &.rt::before { background-image: url(/s/retweet.webp); } + &.like::before { background-image: url(/s/heart.webp); } + &.reply::before { background-image: url(/s/reply.webp); } + &.follow::before { background-image: url(/s/follow.webp); } > .action { display: inline-block; color: tone(5%); > .id { display: inline-block; Index: store.t ================================================================== --- store.t +++ store.t @@ -4,11 +4,11 @@ scope = lib.enum { 'public', 'private', 'local'; 'personal', 'direct', 'circle'; }; noticetype = lib.enum { - 'none', 'mention', 'reply', 'like', 'rt', 'react' + 'none', 'mention', 'reply', 'like', 'rt', 'react', 'follow' }; relation = lib.set { 'follow', 'subscribe', -- get a notification for every post