diff --git a/pkg/arvo/app/graph-pull-hook.hoon b/pkg/arvo/app/graph-pull-hook.hoon new file mode 100644 index 000000000..5971ef948 --- /dev/null +++ b/pkg/arvo/app/graph-pull-hook.hoon @@ -0,0 +1,48 @@ +/- *resource +/+ store=graph-store, graph, default-agent, verb, dbug, pull-hook +~% %graph-pull-hook-top ..is ~ +|% ++$ card card:agent:gall +++ config + ^- config:pull-hook + :* %graph-store + update:store + %graph-update + %graph-push-hook + == +-- +:: +%- agent:dbug +^- agent:gall +%- (agent:pull-hook config) +^- (pull-hook:pull-hook config) +|_ =bowl:gall ++* this . + def ~(. (default-agent this %|) bowl) + dep ~(. (default:pull-hook this config) bowl) +:: +++ on-init on-init:def +++ on-save !>(~) +++ on-load on-load:def +++ on-poke on-poke:def +++ on-peek on-peek:def +++ on-arvo on-arvo:def +++ on-fail on-fail:def +++ on-agent on-agent:def +++ on-watch on-watch:def +++ on-leave on-leave:def +++ on-pull-nack + |= [=resource =tang] + ^- (quip card _this) + :_ this + =- [%pass /pull-nack %agent [our.bowl %graph-store] %poke %graph-update -]~ + !> ^- update:store + [%0 now.bowl [%archive-graph resource]] +:: +++ on-pull-kick + |= =resource + ^- (unit path) + =/ maybe-time (peek-update-log:graph resource) + ?~ maybe-time `/ + `/(scot %da u.maybe-time) +-- diff --git a/pkg/arvo/app/graph-push-hook.hoon b/pkg/arvo/app/graph-push-hook.hoon new file mode 100644 index 000000000..5b4ea54b3 --- /dev/null +++ b/pkg/arvo/app/graph-push-hook.hoon @@ -0,0 +1,128 @@ +/+ store=graph-store +/+ metadata +/+ res=resource +/+ graph +/+ group +/+ default-agent +/+ dbug +/+ push-hook +~% %graph-push-hook-top ..is ~ +|% ++$ card card:agent:gall +++ config + ^- config:push-hook + :* %graph-store + /updates + update:store + %graph-update + %graph-pull-hook + == +:: ++$ agent (push-hook:push-hook config) +:: +++ is-allowed + |= [=resource:res =bowl:gall requires-admin=?] + ^- ? + =/ grp ~(. group bowl) + =/ met ~(. metadata bowl) + =/ group-paths (groups-from-resource:met [%graph (en-path:res resource)]) + ?~ group-paths %.n + ?: requires-admin + (is-admin:grp src.bowl i.group-paths) + ?| (is-member:grp src.bowl i.group-paths) + (is-admin:grp src.bowl i.group-paths) + == +-- +:: +%- agent:dbug +^- agent:gall +%- (agent:push-hook config) +^- agent +|_ =bowl:gall ++* this . + def ~(. (default-agent this %|) bowl) + grp ~(. group bowl) + gra ~(. graph bowl) +:: +++ on-init on-init:def +++ on-save !>(~) +++ on-load on-load:def +++ on-poke on-poke:def +++ on-agent on-agent:def +++ on-watch on-watch:def +++ on-leave on-leave:def +++ on-peek on-peek:def +++ on-arvo on-arvo:def +++ on-fail on-fail:def +:: +++ should-proxy-update + |= =vase + ^- ? + =/ =update:store !<(update:store vase) + ?- -.q.update + %add-graph (is-allowed resource.q.update bowl %.y) + %remove-graph (is-allowed resource.q.update bowl %.y) + %add-nodes (is-allowed resource.q.update bowl %.n) + %remove-nodes (is-allowed resource.q.update bowl %.y) + %add-signatures (is-allowed resource.uid.q.update bowl %.n) + %remove-signatures (is-allowed resource.uid.q.update bowl %.y) + %archive-graph (is-allowed resource.q.update bowl %.y) + %unarchive-graph %.n + %add-tag %.n + %remove-tag %.n + %keys %.n + %tags %.n + %tag-queries %.n + %run-updates (is-allowed resource.q.update bowl %.y) + == +:: +++ resource-for-update + |= =vase + ^- (unit resource:res) + =/ =update:store !<(update:store vase) + ?- -.q.update + %add-graph `resource.q.update + %remove-graph `resource.q.update + %add-nodes `resource.q.update + %remove-nodes `resource.q.update + %add-signatures `resource.uid.q.update + %remove-signatures `resource.uid.q.update + %archive-graph `resource.q.update + %unarchive-graph ~ + %add-tag ~ + %remove-tag ~ + %keys ~ + %tags ~ + %tag-queries ~ + %run-updates `resource.q.update + == +:: +++ initial-watch + |= [=path =resource:res] + ^- vase + ?> (is-allowed resource bowl %.n) + !> ^- update:store + ?~ path + :: new subscribe + :: + (get-graph:gra resource) + :: resubscribe + :: + =/ =time (slav %da i.path) + =/ =update-log:store (get-update-log-subset:gra resource time) + [%0 now.bowl [%run-updates resource update-log]] +:: +++ take-update + |= =vase + ^- [(list card) agent] + =/ =update:store !<(update:store vase) + ?+ -.q.update [~ this] + %remove-graph + :_ this + [%give %kick ~[resource+(en-path:res resource.q.update)] ~]~ + :: + %archive-graph + :_ this + [%give %kick ~[resource+(en-path:res resource.q.update)] ~]~ + == +-- diff --git a/pkg/arvo/app/graph-store.hoon b/pkg/arvo/app/graph-store.hoon index b30c6f7b4..bdfabe744 100644 --- a/pkg/arvo/app/graph-store.hoon +++ b/pkg/arvo/app/graph-store.hoon @@ -532,6 +532,15 @@ ^- [index:store node:store] [(snoc index atom) node] == + :: + [%x %update-log-subset @ @ @ @ ~] + =/ =ship (slav %p i.t.t.path) + =/ =term i.t.t.t.path + =/ start=(unit time) (slaw %da i.t.t.t.t.path) + =/ end=(unit time) (slaw %da i.t.t.t.t.t.path) + =/ update-log=(unit update-log:store) (~(get by update-logs) [ship term]) + ?~ update-log [~ ~] + ``noun+!>((subset:orm-log u.update-log start end)) :: [%x %update-log @ @ ~] =/ =ship (slav %p i.t.t.path) diff --git a/pkg/arvo/app/hood.hoon b/pkg/arvo/app/hood.hoon index 1bc70ed80..a816843c0 100644 --- a/pkg/arvo/app/hood.hoon +++ b/pkg/arvo/app/hood.hoon @@ -2,7 +2,7 @@ /+ drum=hood-drum, helm=hood-helm, kiln=hood-kiln |% +$ state - $: %9 + $: %10 drum=state:drum helm=state:helm kiln=state:kiln @@ -12,6 +12,7 @@ [ver=?(%1 %2 %3 %4 %5 %6) lac=(map @tas fin-any-state)] [%7 drum=state:drum helm=state:helm kiln=state:kiln] [%8 drum=state:drum helm=state:helm kiln=state:kiln] + [%9 drum=state:drum helm=state:helm kiln=state:kiln] == +$ any-state-tuple $: drum=any-state:drum diff --git a/pkg/arvo/app/link-listen-hook.hoon b/pkg/arvo/app/link-listen-hook.hoon index 8fe602d2d..b9cd26266 100644 --- a/pkg/arvo/app/link-listen-hook.hoon +++ b/pkg/arvo/app/link-listen-hook.hoon @@ -1,646 +1,46 @@ -:: link-listen-hook [landscape]: +:: link-listen-hook: no longer in use :: -:: get your friends' bookmarks -:: -:: keeps track of a listening=(set app-path). users can manually add to and -:: remove from this set. -:: -:: for all ships in groups associated with those resources, we subscribe to -:: their link's local-pages and annotations at the resource path (through -:: link-proxy-hook), and forward all entries into our link-store as -:: submissions and comments. -:: -:: if a subscription to a target fails, we assume it's because their -:: metadata+groups definition hasn't been updated to include us yet. -:: we retry with exponential backoff, maxing out at one hour timeouts. -:: to expede this process, we prod other potential listeners when we add -:: them to our metadata+groups definition. -:: -:: -/- listen-hook=link-listen-hook, *metadata-store, *group, *link -/+ mdl=metadata, default-agent, verb, dbug, group-store, grpl=group, resource, store=link-store +/+ default-agent, verb, dbug :: ~% %link-listen-hook-top ..is ~ |% +$ versioned-state - $% [%0 state-0] - [%1 state-1] - [%2 state-2] - [%3 state-3] + $% [%0 *] + [%1 *] + [%2 *] + [%3 *] + [%4 ~] == -+$ state-3 state-1 -+$ state-2 state-1 -+$ state-1 - $: listening=(set app-path) - state-0 - == -+$ state-0 - $: retry-timers=(map target @dr) - :: reasoning: the resources we're subscribed to, - :: and the groups that cause that. - :: - :: we don't strictly need to track this in state, but doing so heavily - :: simplifies logic and reduces the amount of big scries we do. - :: this also gives us the option to check & restore subscriptions, - :: should we ever need that. - :: - reasoning=(jug [ship app-path] group-path) - == -:: -+$ what-target ?(%local-pages %annotations) -+$ target - $: what=what-target - who=ship - where=path - == -++ wire-to-target - |= =wire - ^- target - ?> ?=([what-target @ ^] wire) - [i.wire (slav %p i.t.wire) t.t.wire] -++ target-to-wire - |= target - ^- wire - [what (scot %p who) where] -:: +$ card card:agent:gall -- :: -=| [%3 state-3] +=| [%4 ~] =* state - :: %- agent:dbug %+ verb | ^- agent:gall -=< - |_ =bowl:gall - +* this . - do ~(. +> bowl) - def ~(. (default-agent this %|) bowl) - :: - ++ on-init - ^- (quip card _this) - :_ this - ~[watch-metadata:do watch-groups:do] - :: - ++ on-save !>(state) - ++ on-load - |= =vase - ^- (quip card _this) - =/ old=versioned-state - !<(versioned-state vase) - =| cards=(list card) - |- - =* upgrade-loop $ - ?- -.old - %3 [cards this(state old)] - :: - %2 - :_ this(state [%3 +.old]) - %+ welp cards - :~ [%pass /groups %agent [our.bowl %group-store] %leave ~] - watch-groups:do - == - :: - %1 - :: the upgrade from 0 left out local-only collections. - :: here, we pull those back in. - :: - =. listening.old - (~(run in ~(key by reasoning.old)) tail) - =/ resources=(list [=group-path =app-path]) - %~ tap in - %. %link - %~ get ju - .^ (jug app-name [group-path app-path]) - %gy - (scot %p our.bowl) - %metadata-store - (scot %da now.bowl) - /app-indices - == - |- - ?~ resources - upgrade-loop(old [%2 +.old]) - =, i.resources - =/ members=(set ship) - (members-from-path:grp:do group-path) - :: if we're the only group member, this got incorrectly ignored - :: during 0's upgrade logic. watch it now. - :: - ?. &(=(1 ~(wyt in members)) (~(has in members) our.bowl)) - $(resources t.resources) - =^ more-cards state - (handle-listen-action:do %watch app-path) - $(resources t.resources, cards (weld more-cards cards)) - :: - %0 - =/ listening=(set app-path) - (~(run in ~(key by reasoning.old)) tail) - $(old [%1 listening +.old]) - == - :: - ++ on-agent - |= [=wire =sign:agent:gall] - ^- (quip card _this) - =^ cards state - ?+ wire ~|([dap.bowl %weird-agent-wire wire] !!) - [%metadata ~] - (take-metadata-sign:do sign) - :: - [%groups ~] - (take-groups-sign:do sign) - :: - [%links ?(%local-pages %annotations) @ ^] - (take-link-sign:do (wire-to-target t.wire) sign) - :: - [%forward ^] - (take-forward-sign:do t.wire sign) - :: - [%prod *] - ?> ?=(%poke-ack -.sign) - ?~ p.sign [~ state] - %- (slog leaf+"prod failed" u.p.sign) - [~ state] - == - [cards this] - :: - ++ on-poke - |= [=mark =vase] - ?+ mark (on-poke:def mark vase) - %link-listen-poke - =/ =path !<(path vase) - :_ this - %+ weld - (take-retry:do %local-pages src.bowl path) - (take-retry:do %annotations src.bowl path) - :: - %link-listen-action - ?> (team:title [our src]:bowl) - =^ cards state - ~| p.vase - (handle-listen-action:do !<(action:listen-hook vase)) - [cards this] - == - :: - ++ on-arvo - |= [=wire =sign-arvo] - ^- (quip card _this) - ?+ sign-arvo (on-arvo:def wire sign-arvo) - [%g %done *] - ?~ error.sign-arvo [~ this] - =/ =tank leaf+"{(trip dap.bowl)}'s message went wrong!" - %- (slog tank tang.u.error.sign-arvo) - [~ this] - :: - [%b %wake *] - ?> ?=([%retry @ @ ^] wire) - ?^ error.sign-arvo - =/ =tank leaf+"wake on {(spud wire)} went wrong!" - %- (slog tank u.error.sign-arvo) - [~ this] - :_ this - (take-retry:do (wire-to-target t.wire)) - == - :: - ++ on-peek - |= =path - ^- (unit (unit cage)) - ?+ path ~ - [%x %listening ~] ``noun+!>(listening) - [%x %listening ^] ``noun+!>((~(has in listening) t.t.path)) - == - :: - ++ on-watch - |= =path - ^- (quip card _this) - ?. ?=([%listening ~] path) (on-watch:def path) - ?> (team:title [our src]:bowl) - :_ this - [%give %fact ~ %link-listen-update !>([%listening listening])]~ - :: - ++ on-leave on-leave:def - ++ on-fail on-fail:def - -- -:: -:: |_ =bowl:gall -+* md ~(. mdl bowl) -++ grp ~(. grpl bowl) ++* this . + def ~(. (default-agent this %|) bowl) :: -:: user actions & updates -:: -++ handle-listen-action - |= =action:listen-hook - ^- (quip card _state) - ::NOTE no-opping where appropriate happens further down the call stack. - :: we *could* no-op here, as %watch when we're already listening should - :: result in no-ops all the way down, but walking through everything - :: makes this a nice "resurrect if broken unexpectedly" option. - :: - =* app-path path.action - =^ cards listening - ^- (quip card _listening) - =/ had=? (~(has in listening) app-path) - ?- -.action - %watch - :_ (~(put in listening) app-path) - ?:(had ~ [(send-update action)]~) - :: - %leave - :_ (~(del in listening) app-path) - ?.(had ~ [(send-update action)]~) - == - =/ groups=(list group-path) - (groups-from-resource:md %link app-path) - |- - ?~ groups [cards state] - =^ more-cards state - ?- -.action - %watch (listen-to-group app-path i.groups) - %leave (leave-from-group app-path i.groups) - == - $(cards (weld cards more-cards), groups t.groups) -:: -++ send-update - |= =update:listen-hook +++ on-init [~ this] +++ on-save !>(state) +++ on-load + |= =vase + ^- (quip card _this) + :_ this + :- [%pass /groups %agent [our.bowl %group-store] %leave ~] + %+ turn ~(tap in ~(key by wex.bowl)) + |= [=wire =ship =term] ^- card - [%give %fact ~[/listening] %link-listen-update !>(update)] + [%pass wire %agent [ship term] %leave ~] :: -:: metadata subscription -:: -++ watch-metadata - ^- card - [%pass /metadata %agent [our.bowl %metadata-store] %watch /app-name/link] -:: -++ take-metadata-sign - |= =sign:agent:gall - ^- (quip card _state) - ?- -.sign - %poke-ack ~|([dap.bowl %unexpected-poke-ack /metadata] !!) - %kick [[watch-metadata]~ state] - :: - %watch-ack - ?~ p.sign [~ state] - =/ =tank - :- %leaf - "{(trip dap.bowl)} failed subscribe to metadata store. very wrong!" - %- (slog tank u.p.sign) - [~ state] - :: - %fact - =* mark p.cage.sign - =* vase q.cage.sign - ?. ?=(%metadata-update mark) - ~| [dap.bowl %unexpected-mark mark] - !! - %- handle-metadata-update - !<(metadata-update vase) - == -:: -++ handle-metadata-update - |= upd=metadata-update - ^- (quip card _state) - ?+ -.upd [~ state] - %add - ?> =(%link app-name.resource.upd) - :: auto-listen to collections in unmanaged groups only - :: - =/ rid=resource - (de-path:resource group-path.upd) - =/ =group - (need (scry-group:grp rid)) - ?. hidden.group - [~ state] - =, resource.upd - =^ update listening - ^- (quip card _listening) - ?: (~(has in listening) app-path) - [~ listening] - :- [(send-update %watch app-path)]~ - (~(put in listening) app-path) - =^ cards state - (listen-to-group app-path group-path.upd) - [(weld update cards) state] - :: - %remove - ?> =(%link app-name.resource.upd) - =? listening - ?=(~ (groups-from-resource:md %link app-path.resource.upd)) - (~(del in listening) app-path.resource.upd) - (leave-from-group app-path.resource.upd group-path.upd) - == -:: -:: groups subscriptions -:: -++ watch-groups - ^- card - [%pass /groups %agent [our.bowl %group-store] %watch /groups] -:: -++ take-groups-sign - |= =sign:agent:gall - ^- (quip card _state) - ?- -.sign - %poke-ack ~|([dap.bowl %unexpected-poke-ack /groups] !!) - %kick [[watch-groups]~ state] - :: - %watch-ack - ?~ p.sign [~ state] - =/ =tank - :- %leaf - "{(trip dap.bowl)} failed subscribe to groups. very wrong!" - %- (slog tank u.p.sign) - [~ state] - :: - %fact - =* mark p.cage.sign - =* vase q.cage.sign - ?+ mark ~|([dap.bowl %unexpected-mark mark] !!) - %group-initial [~ state] ::NOTE initial handled using metadata - %group-update (handle-group-update !<(update:group-store vase)) - == - == -:: -++ handle-group-update - |= upd=update:group-store - ^- (quip card _state) - ?. ?=(?(%add-members %initial-group %remove-members) -.upd) - [~ state] - =/ =path - (en-path:resource resource.upd) - =/ socs=(list app-path) - (app-paths-from-group:md %link path) - =/ whos=(list ship) - ?- -.upd - %add-members ~(tap in ships.upd) - %remove-members ~(tap in ships.upd) - %initial-group ~(tap in members.group.upd) - == - =| cards=(list card) - |- - =* loop-socs $ - ?~ socs [cards state] - ?. (~(has in listening) i.socs) - loop-socs(socs t.socs) - |- - =* loop-whos $ - ?~ whos loop-socs(socs t.socs) - =^ caz state - ?. ?=(%remove-members -.upd) - (listen-to-peer i.socs path i.whos) - ?: =(our.bowl i.whos) - (handle-listen-action %leave i.socs) - (leave-from-peer i.socs path i.whos) - loop-whos(whos t.whos, cards (weld cards caz)) -:: -:: link subscriptions -:: -++ listen-to-group - |= [=app-path =group-path] - ^- (quip card _state) - =/ peers=(list ship) - ~| group-path - %~ tap in - (members-from-path:grp group-path) - =| cards=(list card) - |- - ?~ peers [cards state] - =^ caz state - (listen-to-peer app-path group-path i.peers) - $(peers t.peers, cards (weld cards caz)) -:: -++ leave-from-group - |= [=app-path =group-path] - ^- (quip card _state) - =/ peers=(list ship) - %~ tap in - (members-from-path:grp group-path) - =| cards=(list card) - |- - ?~ peers [cards state] - =^ caz state - (leave-from-peer app-path group-path i.peers) - $(peers t.peers, cards (weld cards caz)) -:: -++ listen-to-peer - |= [=app-path =group-path who=ship] - ^- (quip card _state) - ?: =(our.bowl who) - [~ state] - :_ =- state(reasoning -) - (~(put ju reasoning) [who app-path] group-path) - :- (prod-other-listener who app-path) - ?^ (~(get ju reasoning) [who app-path]) - ~ - (start-link-subscriptions who app-path) -:: -++ leave-from-peer - |= [=app-path =group-path who=ship] - ^- (quip card _state) - ?: =(our.bowl who) - [~ state] - =. reasoning (~(del ju reasoning) [who app-path] group-path) - ::NOTE leaving is always safe, so we just do it unconditionally - (end-link-subscriptions who app-path) -:: -++ start-link-subscriptions - |= [=ship =app-path] - ^- (list card) - :~ (start-link-subscription %local-pages ship app-path) - (start-link-subscription %annotations ship app-path) - == -:: -++ start-link-subscription - |= =target - ^- card - :* %pass - [%links (target-to-wire target)] - %agent - [who.target %link-proxy-hook] - %watch - ?- what.target - %local-pages [what where]:target - %annotations [what %$ where]:target - == - == -:: -++ end-link-subscriptions - |= [who=ship where=path] - ^- (quip card _state) - =. retry-timers (~(del by retry-timers) [%local-pages who where]) - =. retry-timers (~(del by retry-timers) [%annotations who where]) - :_ state - |^ ~[(end %local-pages) (end %annotations)] - :: - ++ end - |= what=what-target - :* %pass - [%links (target-to-wire what who where)] - %agent - [who %link-proxy-hook] - %leave - ~ - == - -- -:: -++ prod-other-listener - |= [who=ship where=path] - ^- card - :* %pass - [%prod (scot %p who) where] - %agent - [who %link-listen-hook] - %poke - %link-listen-poke - !>(where) - == -:: -++ take-link-sign - |= [=target =sign:agent:gall] - ^- (quip card _state) - ?- -.sign - %poke-ack ~|([dap.bowl %unexpected-poke-ack /links target] !!) - %kick [[(start-link-subscription target)]~ state] - :: - %watch-ack - ?~ p.sign - =. retry-timers (~(del by retry-timers) target) - [~ state] - :: our subscription request got rejected, - :: most likely because our group definition is out of sync with theirs. - :: set timer for retry. - :: - (start-retry target) - :: - %fact - =* mark p.cage.sign - =* vase q.cage.sign - ?+ mark ~|([dap.bowl %unexpected-mark mark] !!) - %link-initial - %- handle-link-initial - [who.target where.target !<(initial:store vase)] - :: - %link-update - %- handle-link-update - [who.target where.target !<(update:store vase)] - == - == -:: -++ start-retry - |= =target - ^- (quip card _state) - =/ timer=@dr - %+ min ~h1 - %+ mul 2 - (~(gut by retry-timers) target ~s15) - =. retry-timers - (~(put by retry-timers) target timer) - :_ state - :_ ~ - :* %pass - [%retry (target-to-wire target)] - [%arvo %b %wait (add now.bowl timer)] - == -:: -++ take-retry - |= =target - ^- (list card) - :: relevant: whether :who is still associated with resource :where - :: - =; relevant=? - ?. relevant ~ - [(start-link-subscription target)]~ - ?. (~(has in listening) where.target) - | - ?: %- ~(has by wex.bowl) - [[%links (target-to-wire target)] who.target %link-proxy-hook] - | - %+ lien (groups-from-resource:md %link where.target) - |= =group-path - ^- ? - %. who.target - ~(has in (members-from-path:grp group-path)) - -:: -++ do-link-action - |= [=wire =action:store] - ^- card - :* %pass - wire - %agent - [our.bowl %link-store] - %poke - %link-action - !>(action) - == -:: -++ handle-link-initial - |= [who=ship where=path =initial:store] - ^- (quip card _state) - ?> =(src.bowl who) - ?+ -.initial ~|([dap.bowl %unexpected-initial -.initial] !!) - %local-pages - =/ =pages (~(got by pages.initial) where) - (handle-link-update who where [%local-pages where pages]) - :: - %annotations - =/ urls=(list [=url =notes]) - ~(tap by (~(got by notes.initial) where)) - =| cards=(list card) - |- ^- (quip card _state) - ?~ urls [cards state] - =^ caz state - ^- (quip card _state) - =, i.urls - (handle-link-update who where [%annotations where url notes]) - $(urls t.urls, cards (weld cards caz)) - == -:: -++ handle-link-update - |= [who=ship where=path =update:store] - ^- (quip card _state) - ?> =(src.bowl who) - :_ state - ?+ -.update ~|([dap.bowl %unexpected-update -.update] !!) - %local-pages - %+ turn pages.update - |= =page - %+ do-link-action - [%forward %local-page (scot %p who) where] - [%hear where who page] - :: - %annotations - %+ turn notes.update - |= =note - ^- card - %+ do-link-action - `wire`[%forward %annotation (scot %p who) where] - `action:store`[%read where url.update `comment`[who note]] - == -:: -++ take-forward-sign - |= [=wire =sign:agent:gall] - ^- (quip card _state) - ~| [%unexpected-sign on=[%forward wire] -.sign] - ?> ?=(%poke-ack -.sign) - ?~ p.sign [~ state] - =/ =tank - :- %leaf - ;: weld - (trip dap.bowl) - " failed to save submission from " - (spud wire) - == - %- (slog tank u.p.sign) - [~ state] -:: -++ scry-for - |* [=mold =app-name =path] - .^ mold - %gx - (scot %p our.bowl) - app-name - (scot %da now.bowl) - (snoc `^path`path %noun) - == +++ on-agent on-agent:def +++ on-poke on-poke:def +++ on-arvo on-arvo:def +++ on-peek on-peek:def +++ on-watch on-watch:def +++ on-leave on-leave:def +++ on-fail on-fail:def -- diff --git a/pkg/arvo/app/link-proxy-hook.hoon b/pkg/arvo/app/link-proxy-hook.hoon index 4d325e11f..9557d0f18 100644 --- a/pkg/arvo/app/link-proxy-hook.hoon +++ b/pkg/arvo/app/link-proxy-hook.hoon @@ -1,339 +1,46 @@ -:: link-proxy-hook [landscape]: +:: link-proxy-hook: no longer in use :: -:: make local pages available to foreign ships -:: -:: this is a "proxy" style hook, relaying foreign subscriptions into local -:: stores if permission conditions are met. -:: the patterns herein should one day be generalized into a proxy-hook lib. -:: -:: this uses metadata-store to discover resources and their associated -:: groups. it sets the permission condition to be that a ship must be in a -:: group associated with the resource it's subscribing to. -:: we check this on-watch, but also subscribe to metadata & groups so that -:: we can kick subscriptions if needed (eg ship removed from group). -:: -:: we deduplicate incoming subscriptions on the same path, ensuring we have -:: exactly one local subscription per unique incoming subscription path. -:: this comes at the cost of assuming that the store's initial response is -:: whatever's returned by the scry at that path, but perhaps that should -:: become part of the stores standard anyway. -:: -:: when adding support for new paths, the only things you'll likely want -:: to touch are +permitted, +initial-response, & +kick-proxies. -:: -/- *link, *metadata-store, *group -/+ metadata, default-agent, verb, dbug, group-store, grpl=group, - resource, store=link-store +/+ default-agent, verb, dbug ~% %link-proxy-hook-top ..is ~ |% -+$ state-0 - $: %0 - ::TODO we use this to detect "first sub started" and "last sub left", - :: but can't we use [wex sup]:bowl for that? - active=(map path (set ship)) - == -+$ state-1 - $: %1 - active=(map path (set ship)) - == -:: +$ versioned-state - $% state-0 - state-1 + $% [%0 *] + [%1 *] + [%2 ~] == :: +$ card card:agent:gall -- :: -=| state-1 +=| [%2 ~] =* state - :: %- agent:dbug %+ verb | ^- agent:gall -=< - |_ =bowl:gall - +* this . - do ~(. +> bowl) - def ~(. (default-agent this %&) bowl) - :: - ++ on-init - ^- (quip card _this) - :_ this - ~[watch-groups:do watch-metadata:do] - :: - ++ on-save !>(state) - ++ on-load - |= old-vase=vase - ^- (quip card _this) - =/ old - !<(versioned-state old-vase) - ?- -.old - %1 [~ this(state old)] - :: - %0 - :_ this(state [%1 +.old]) - :~ [%pass /groups %agent [our.bowl %group-store] %leave ~] - watch-groups:do - == - == - - :: - ++ on-watch - |= =path - ^- (quip card _this) - :: the local ship should just use link-store directly - ::TODO do we want to allow this anyway, to avoid client-side target checks? - :: - ?< (team:title [our src]:bowl) - ?> (permitted:do src.bowl path) - =^ cards state - (start-proxy:do src.bowl path) - [cards this] - :: - ++ on-leave - |= =path - ^- (quip card _this) - =^ cards state - (stop-proxy:do src.bowl path) - [cards this] - :: - ++ on-agent - |= [=wire =sign:agent:gall] - ^- (quip card _this) - ?: ?=([%groups ~] wire) - =^ cards state - (take-groups-sign:do sign) - [cards this] - ?: ?=([%proxy ^] wire) - =^ cards state - (handle-proxy-sign t.wire sign) - [cards this] - ~| [dap.bowl %weird-wire wire] - !! - :: - ++ on-poke on-poke:def - ++ on-peek on-peek:def - ++ on-arvo on-arvo:def - ++ on-fail on-fail:def - -- -:: |_ =bowl:gall -+* md ~(. metadata bowl) - grp ~(. grpl bowl) ++* this . + def ~(. (default-agent this %&) bowl) :: -:: permissions +++ on-init on-init:def +++ on-save !>(state) +++ on-load + |= old-vase=vase + ^- (quip card _this) + =/ paths + %+ turn ~(val by sup.bowl) + |=([=ship =path] path) + :_ this + :- [%pass /groups %agent [our.bowl %group-store] %leave ~] + ?~ paths ~ + [%give %kick paths ~]~ :: -++ permitted - |= [who=ship =path] - ^- ? - :: we only expose /local-pages and /annotations, - :: to ships in the groups associated with the resource. - :: (no url-specific annotations subscriptions, either.) - :: - =/ target=(unit ^path) - ?: ?=([%local-pages ^] path) - `t.path - ?: ?=([%annotations ~ ^] path) - `t.t.path - ~ - ?~ target | - %+ lien (groups-from-resource:md %link u.target) - |= =group-path - ^- ? - (~(has in (members-from-path:grp group-path)) who) -:: -++ kick-revoked-permissions - |= [=path who=(list ship)] - ^- (list card) - %+ murn who - |= =ship - ^- (unit card) - :: no need to remove to ourselves - :: - ?: =(our.bowl ship) ~ - ?: (permitted ship path) ~ - `(kick-proxies ship path) -:: -:: metadata subscription -:: -++ watch-metadata - ^- card - [%pass /metadata %agent [our.bowl %metadata-store] %watch /app-name/link] -:: -++ take-metadata-sign - |= =sign:agent:gall - ^- (quip card _state) - ?- -.sign - %poke-ack ~|([dap.bowl %unexpected-poke-ack /metadata] !!) - %kick [[watch-metadata]~ state] - :: - %watch-ack - ?~ p.sign [~ state] - =/ =tank - :- %leaf - "{(trip dap.bowl)} failed subscribe to metadata store. very wrong!" - %- (slog tank u.p.sign) - [~ state] - :: - %fact - =* mark p.cage.sign - =* vase q.cage.sign - ?. ?=(%metadata-update mark) - ~| [dap.bowl %unexpected-mark mark] - !! - %- handle-metadata-update - !<(metadata-update vase) - == -:: -++ handle-metadata-update - |= upd=metadata-update - ^- (quip card _state) - :_ state - ?. ?=(%remove -.upd) ~ - ?> =(%link app-name.resource.upd) - :: if a group is no longer associated with a resource, - :: we need to re-check permissions for everyone in that group. - :: - %+ kick-revoked-permissions - app-path.resource.upd - %~ tap in - (members-from-path:grp group-path.upd) -:: -:: groups subscription -::TODO largely copied from link-listen-hook. maybe make a store-listener lib? -:: -++ watch-groups - ^- card - [%pass /groups %agent [our.bowl %group-store] %watch /groups] -:: -++ take-groups-sign - |= =sign:agent:gall - ^- (quip card _state) - ?- -.sign - %poke-ack ~|([dap.bowl %unexpected-poke-ack /groups] !!) - %kick [[watch-groups]~ state] - :: - %watch-ack - ?~ p.sign [~ state] - =/ =tank - :- %leaf - "{(trip dap.bowl)} failed subscribe to group store. very wrong!" - %- (slog tank u.p.sign) - [~ state] - :: - %fact - =* mark p.cage.sign - =* vase q.cage.sign - ?+ mark ~|([dap.bowl %unexpected-mark mark] !!) - %group-initial [~ state] - %group-update (handle-group-update !<(update:group-store vase)) - == - == -:: -++ handle-group-update - |= upd=update:group-store - ^- (quip card _state) - :_ state - ?. ?=(%remove-members -.upd) ~ - :: if someone was removed from a group, find all link resources associated - :: with that group, then kick their subscriptions if they're no longer - :: - %- zing - %+ turn (app-paths-from-group:md %link (en-path:resource resource.upd)) - |= =app-path - ^- (list card) - %+ kick-revoked-permissions - app-path - ~(tap in ships.upd) -:: -:: proxy subscriptions -:: -++ kick-proxies - |= [who=ship =path] - ^- card - =- [%give %kick - `who] - :~ [%local-pages path] - [%annotations %$ path] - == -:: -++ handle-proxy-sign - |= [=wire =sign:agent:gall] - ^- (quip card _state) - ?- -.sign - %poke-ack ~|([dap.bowl %unexpected-poke-ack wire] !!) - %fact [[%give %fact ~[wire] cage.sign]~ state] - %kick [[(proxy-pass-link-store wire %watch wire)]~ state] - :: - %watch-ack - ?~ p.sign [~ state] - =/ =tank - :- %leaf - "{(trip dap.bowl)} failed subscribe to link-store. very wrong!" - %- (slog tank u.p.sign) - [~ state] - == -:: -++ proxy-pass-link-store - |= [=path =task:agent:gall] - ^- card - :* %pass - [%proxy path] - %agent - [our.bowl %link-store] - task - == -:: -++ initial-response - |= =path - ^- card - =; =initial:store - [%give %fact ~ %link-initial !>(initial)] - ?+ path !! - [%local-pages ^] - [%local-pages (scry-for (map ^path pages) %link-store path)] - :: - [%annotations %$ ^] - [%annotations (scry-for (per-path-url notes) %link-store path)] - == -:: -++ start-proxy - |= [who=ship =path] - ^- (quip card _state) - :_ state(active (~(put ju active) path who)) - :_ ~ - :: if we already have a local subscription open, - :: - ?. =(~ (~(get ju active) path)) - :: gather the initial response ourselves, and send that. - :: - (initial-response path) - :: else, open a local subscription, - :: sending outward its initial response when we hear it. - :: - (proxy-pass-link-store path %watch path) -:: -++ stop-proxy - |= [who=ship =path] - ^- (quip card _state) - =. active (~(del ju active) path who) - :_ state - :: if there are still subscriptions remaining, do nothing. - :: - ?. =(~ (~(get ju active) path)) ~ - :: else, close the local subscription. - :: - [(proxy-pass-link-store path %leave ~)]~ -:: -:: helpers -:: -++ scry-for - |* [=mold =app-name =path] - .^ mold - %gx - (scot %p our.bowl) - app-name - (scot %da now.bowl) - (snoc `^path`path %noun) - == +++ on-watch on-watch:def +++ on-leave on-leave:def +++ on-agent on-agent:def +++ on-poke on-poke:def +++ on-peek on-peek:def +++ on-arvo on-arvo:def +++ on-fail on-fail:def -- + diff --git a/pkg/arvo/app/link-store.hoon b/pkg/arvo/app/link-store.hoon index 8fe1c54a2..6dfcc4ab8 100644 --- a/pkg/arvo/app/link-store.hoon +++ b/pkg/arvo/app/link-store.hoon @@ -52,10 +52,12 @@ :: ? :: /seen/wood-url/some-path have we seen this here :: -/- *link -/+ store=link-store, default-agent, verb, dbug +/- *link, gra=graph-store, *resource +/+ store=link-store, graph-store, default-agent, verb, dbug :: |% ++$ state-any $%(state-1 state-0) ++$ state-1 [%1 ~] +$ state-0 $: %0 by-group=(map path links) @@ -78,414 +80,107 @@ +$ card card:agent:gall -- :: -=| state-0 +=| state-1 =* state - :: %- agent:dbug %+ verb | ^- agent:gall -=< - |_ =bowl:gall - +* this . - do ~(. +> bowl) - def ~(. (default-agent this %|) bowl) - :: - ++ on-init on-init:def - ++ on-save !>(state) - ++ on-load - |= old=vase - ^- (quip card _this) - [~ this(state !<(state-0 old))] - :: - ++ on-poke - |= [=mark =vase] - ^- (quip card _this) - ?> (team:title [our src]:bowl) ::TODO /lib/store - =^ cards state - ?+ mark (on-poke:def mark vase) - ::TODO move json conversion into mark once mark performance improves - %json (do-action:do (action:dejs:store !<(json vase))) - %link-action (do-action:do !<(action:store vase)) - == - [cards this] - :: - ++ on-peek - |= =path - ^- (unit (unit cage)) - ?+ path (on-peek:def path) - [%y ?(%local-pages %submissions) ~] - ``noun+!>(~(key by by-group)) - :: - [%x %local-pages *] - ``noun+!>((get-local-pages:do t.t.path)) - :: - [%x %submissions *] - ``noun+!>((get-submissions:do t.t.path)) - :: - [%y ?(%annotations %discussions) *] - =/ [spath=^path surl=url] - (break-discussion-path:store t.t.path) - =- ``noun+!>(-) - :: - ?: =(~ surl) - :: no url, provide urls that have comments - :: - ^- (set url) - ?~ spath - :: no path, find urls accross all paths - :: - %- ~(rep by discussions) - |= [[* discussions=(map url discussion)] urls=(set url)] - %- ~(uni in urls) - ~(key by discussions) - :: specified path, find urls for that specific path - :: - %~ key by - (~(gut by discussions) spath *(map url *)) - :: specified url and path, nothing to list here - :: - ?^ spath !! - :: no path, find paths with comments for this url - :: - ^- (set ^path) - %- ~(rep by discussions) - |= [[=^path urls=(map url discussion)] paths=(set ^path)] - ?. (~(has by urls) surl) paths - (~(put in paths) path) - :: - [%x %annotations *] - ``noun+!>((get-annotations:do t.t.path)) - :: - [%x %discussions *] - ``noun+!>((get-discussions:do t.t.path)) - :: - [%x %seen @ ^] - ``noun+!>((is-seen:do t.t.path)) - :: - [%x %unseen ~] - ``noun+!>(get-all-unseen:do) - :: - [%x %unseen ^] - ``noun+!>((get-unseen:do t.t.path)) - == - :: - ++ on-watch - |= =path - ^- (quip card _this) - ?> (team:title [our src]:bowl) ::TODO /lib/store - :_ this - |^ ?+ path (on-watch:def path) - [%local-pages *] - %+ give %link-initial - ^- initial:store - [%local-pages (get-local-pages:do t.path)] - :: - [%submissions *] - %+ give %link-initial - ^- initial:store - [%submissions (get-submissions:do t.path)] - :: - [%annotations *] - %+ give %link-initial - ^- initial:store - [%annotations (get-annotations:do t.path)] - :: - [%discussions *] - %+ give %link-initial - ^- initial:store - [%discussions (get-discussions:do t.path)] - :: - [%seen ~] - ~ - == - :: - ++ give - |* [=mark =noun] - ^- (list card) - [%give %fact ~ mark !>(noun)]~ - :: - ++ give-single - |* [=mark =noun] - ^- card - [%give %fact ~ mark !>(noun)] - -- - :: - ++ on-leave on-leave:def - ++ on-agent on-agent:def - ++ on-arvo on-arvo:def - ++ on-fail on-fail:def - -- -:: |_ =bowl:gall ++* this . + do ~(. +> bowl) + def ~(. (default-agent this %|) bowl) :: -:: writing -:: -++ do-action - |= =action:store - ^- (quip card _state) - ?- -.action - %save (save-page +.action) - %note (note-note +.action) - %seen (seen-submission +.action) +++ on-init on-init:def +++ on-save !>(state) +++ on-load + |= old=vase + ^- (quip card _this) + =/ s !<(state-any old) + ?: ?=(%1 -.s) + [~ this(state s)] :: - %hear (hear-submission +.action) - %read (read-comment +.action) - == -:: +save-page: save a page ourselves -:: -++ save-page - |= [=path title=@t =url] - ^- (quip card _state) - ?< |(=(~ path) =(~ title) =(~ url)) - :: add page to group ours - :: - =/ =links (~(gut by by-group) path *links) - =/ =page [title url now.bowl] - =. ours.links [page ours.links] - =. by-group (~(put by by-group) path links) - :: do generic submission logic - :: - =^ submission-cards state - (hear-submission path [our.bowl page]) - :: mark page as seen (because we submitted it ourselves) - :: - =^ seen-cards state - (seen-submission path `url) - :: send updates to subscribers - :: - :_ state - :_ (weld submission-cards seen-cards) - :+ %give %fact - :+ :~ /local-pages - [%local-pages path] - == - %link-update - !>([%local-pages path [page]~]) -:: +note-note: save a note for a url -:: -++ note-note - |= [=path =url udon=@t] - ^- (quip card _state) - ?< |(=(~ path) =(~ url) =(~ udon)) - :: add note to discussion ours - :: - =/ urls (~(gut by discussions) path *(map ^url discussion)) - =/ =discussion (~(gut by urls) url *discussion) - =/ =note [now.bowl udon] - =. ours.discussion [note ours.discussion] - =. urls (~(put by urls) url discussion) - =. discussions (~(put by discussions) path urls) - :: do generic comment logic - :: - =^ cards state - (read-comment path url [our.bowl note]) - :: send updates to subscribers - :: - :_ state + :_ this(state *state-1) + =/ orm orm:graph-store + |^ ^- (list card) + %- zing + %+ turn ~(tap by by-group.s) + |= [=path =links] ^- (list card) - :_ cards - :+ %give %fact - :+ :~ /annotations - [%annotations %$ path] - [%annotations (build-discussion-path:store url)] - [%annotations (build-discussion-path:store path url)] + ?. ?=([@ @ *] path) + (on-bad-path path links) + =/ =resource [(slav %p i.path) i.t.path] + :_ [(archive-graph resource)]~ + %+ add-graph resource + ^- graph:gra + %+ gas:orm ~ + =/ comments (~(gut by discussions.s) path *(map url discussion)) + %+ turn submissions.links + |= sub=submission + ^- [atom node:gra] + :- time.sub + =/ contents ~[text+title.sub url+url.sub] + =/ parent-hash `@ux`(sham ~ ship.sub time.sub contents) + :- ^- post:gra + :* author=ship.sub + index=~[time.sub] + time-sent=time.sub + contents + hash=`parent-hash + signatures=~ == - %link-update - !>([%annotations path url [note]~]) -:: +seen-submission: mark url as seen/read -:: -:: if no url specified, all under path are marked as read -:: -++ seen-submission - |= [=path murl=(unit url)] - ^- (quip card _state) - =/ =links (~(gut by by-group) path *links) - :: new: urls we want to, but haven't yet, marked as seen + ^- internal-graph:gra + =/ dis (~(get by comments) url.sub) + ?~ dis + [%empty ~] + :- %graph + ^- graph:gra + %+ gas:orm ~ + %+ turn comments.u.dis + |= [=ship =time udon=@t] + ^- [atom node:gra] + :- time + :_ `internal-graph:gra`[%empty ~] + =/ contents ~[text+udon] + :* author=ship + index=~[time.sub time] + time-sent=time + contents + hash=``@ux`(sham `parent-hash ship time contents) + signatures=~ + == :: - =/ new=(set url) - %. seen.links - %~ dif in - ^- (set url) - ?^ murl (sy ~[u.murl]) - %- ~(gas in *(set url)) - %+ turn submissions.links - |=(submission url) - ?: =(~ new) [~ state] - =. seen.links (~(uni in seen.links) new) - :_ state(by-group (~(put by by-group) path links)) - [%give %fact ~[/seen] %link-update !>([%observation path new])]~ -:: +hear-submission: record page someone else saved -:: -++ hear-submission - |= [=path =submission] - ^- (quip card _state) - ?< =(~ path) - :: add link to group submissions + ++ on-bad-path + |= [=path =links] + ^- (list card) + ~| discarding-malformed-links+[path links] + ~ :: - =/ =links (~(gut by by-group) path *links) - =^ added submissions.links - ?: ?=(^ (find ~[submission] submissions.links)) - [| submissions.links] - :- & - (submissions:merge:store submissions.links ~[submission]) - =. by-group (~(put by by-group) path links) - :: add submission to global sites + ++ add-graph + |= [=resource =graph:gra] + ^- card + %- poke-graph-store + [%0 now.bowl %add-graph resource graph `%graph-validator-link] :: - =/ =site (site-from-url:store url.submission) - =. by-site (~(add ja by-site) site [path submission]) - :: send updates to subscribers + ++ archive-graph + |= =resource + ^- card + %- poke-graph-store + [%0 now.bowl %archive-graph resource] :: - :_ state - ?. added ~ - :_ ~ - :+ %give %fact - :+ :~ /submissions - [%submissions path] - == - %link-update - !>([%submissions path [submission]~]) -:: +read-comment: record a comment someone else made -:: -++ read-comment - |= [=path =url =comment] - ^- (quip card _state) - :: add comment to url's discussion - :: - =/ urls (~(gut by discussions) path *(map ^url discussion)) - =/ =discussion (~(gut by urls) url *discussion) - =^ added comments.discussion - ?: ?=(^ (find ~[comment] comments.discussion)) - [| comments.discussion] - :- & - (comments:merge:store comments.discussion ~[comment]) - =. urls (~(put by urls) url discussion) - =. discussions (~(put by discussions) path urls) - :: send updates to subscribers - :: - :_ state - ?. added ~ - :_ ~ - :+ %give %fact - :+ :~ /discussions - [%discussions '' path] - [%discussions (build-discussion-path:store url)] - [%discussions (build-discussion-path:store path url)] - == - %link-update - !>([%discussions path url [comment]~]) -:: -:: reading -:: -++ get-local-pages - |= =path - ^- (map ^path pages) - ?~ path - :: all paths - :: - %- ~(run by by-group) - |=(links ours) - :: specific path - :: - %+ ~(put by *(map ^path pages)) path - ours:(~(gut by by-group) path *links) -:: -++ get-submissions - |= =path - ^- (map ^path submissions) - ?~ path - :: all paths - :: - %- ~(run by by-group) - |=(links submissions) - :: specific path - :: - %+ ~(put by *(map ^path submissions)) path - submissions:(~(gut by by-group) path *links) -:: -++ get-all-unseen - ^- (jug path url) - %- ~(rut by by-group) - |= [=path *] - (get-unseen path) -:: -++ get-unseen - |= =path - ^- (set url) - =/ =links - (~(gut by by-group) path *links) - %- ~(gas in *(set url)) - %+ murn submissions.links - |= submission - ?: (~(has in seen.links) url) ~ - (some url) -:: -++ is-seen - |= =path - ^- ? - =/ [=^path =url] - (break-discussion-path:store path) - %. url - %~ has in - seen:(~(gut by by-group) path *links) -:: -:: -++ get-annotations - |= =path - ^- (per-path-url notes) - =/ args=[=^path =url] - (break-discussion-path:store path) - |^ ?~ path - :: all paths - :: - (~(run by discussions) get-ours) - :: specific path - :: - %+ ~(put by *(per-path-url notes)) path.args - %- get-ours - %+ ~(gut by discussions) path.args - *(map url discussion) - :: - ++ get-ours - |= m=(map url discussion) - ^- (map url notes) - ?: =(~ url.args) - :: all urls - :: - %- ~(run by m) - |=(discussion ours) - :: specific url - :: - %+ ~(put by *(map url notes)) url.args - ours:(~(gut by m) url.args *discussion) + ++ poke-graph-store + |= =update:gra + ^- card + :* %pass /migrate-link %agent [our.bowl %graph-store] + %poke %graph-update !>(update) + == -- :: -++ get-discussions - |= =path - ^- (per-path-url comments) - =/ args=[=^path =url] - (break-discussion-path:store path) - |^ ?~ path - :: all paths - :: - (~(run by discussions) get-comments) - :: specific path - :: - %+ ~(put by *(per-path-url comments)) path.args - %- get-comments - %+ ~(gut by discussions) path.args - *(map url discussion) - :: - ++ get-comments - |= m=(map url discussion) - ^- (map url comments) - ?: =(~ url.args) - :: all urls - :: - %- ~(run by m) - |=(discussion comments) - :: specific url - :: - %+ ~(put by *(map url comments)) url.args - comments:(~(gut by m) url.args *discussion) - -- +++ on-poke on-poke:def +++ on-peek on-peek:def +++ on-watch on-watch:def +++ on-leave on-leave:def +++ on-agent on-agent:def +++ on-arvo on-arvo:def +++ on-fail on-fail:def -- diff --git a/pkg/arvo/app/link-view.hoon b/pkg/arvo/app/link-view.hoon index 3487e4e92..085086212 100644 --- a/pkg/arvo/app/link-view.hoon +++ b/pkg/arvo/app/link-view.hoon @@ -1,628 +1,39 @@ -:: link-view [landscape]: -:: -::frontend endpoints -:: -:: endpoints, mapping onto link-store's paths. p is for page as in pagination. -:: only the /0/submissions endpoint provides updates. -:: as with link-store, urls are expected to use +wood encoding. -:: -:: /json/0/submissions initial + updates for all -:: /json/[p]/submissions/[collection] page for one collection -:: /json/[p]/discussions/[wood-url]/[collection] page for url in collection -:: /json/[n]/submission/[wood-url]/[collection] nth matching submission -:: /json/seen mark-as-read updates -:: -/- *link, view=link-view -/- *invite-store, group-store -/- listen-hook=link-listen-hook -/- group-hook, permission-hook, permission-group-hook -/- metadata-hook, contact-view -/- pull-hook, *group -/+ store=link-store, metadata, *server, default-agent, verb, dbug, grpl=group -/+ group-store, resource +:: link-view: no longer in use +/+ default-agent, verb, dbug ~% %link-view-top ..is ~ -:: -:: |% +$ versioned-state - $% state-0 - state-1 - == -+$ state-0 - $: %0 - ~ - == -:: -+$ state-1 - $: %1 - ~ + $% [%0 ~] + [%1 ~] + [%2 ~] == :: +$ card card:agent:gall -- :: -=| state-1 +=| [%2 ~] =* state - :: %+ verb | %- agent:dbug ^- agent:gall -=< - |_ =bowl:gall - +* this . - do ~(. +> bowl) - def ~(. (default-agent this %|) bowl) - :: - ++ on-init - ^- (quip card _this) - :_ this - :~ [%pass /submissions %agent [our.bowl %link-store] %watch /submissions] - [%pass /discussions %agent [our.bowl %link-store] %watch /discussions] - [%pass /seen %agent [our.bowl %link-store] %watch /seen] - :: - =+ [%invite-action !>([%create /link])] - [%pass /invitatory/create %agent [our.bowl %invite-store] %poke -] - :: - =+ /invitatory/link - [%pass - %agent [our.bowl %invite-store] %watch -] - :* %pass /srv %agent [our.bowl %file-server] - %poke %file-server-action - !>([%serve-dir /'~link' /app/landscape %.n %.y]) - == - == - :: - ++ on-save !>(state) - ++ on-load - |= old-vase=vase - ^- (quip card _this) - =/ old !<(versioned-state old-vase) - ?- -.old - %1 [~ this] - %0 - :_ this(state [%1 ~]) - :- [%pass /connect %arvo %e %disconnect [~ /'~link']] - :~ :* %pass /srv %agent [our.bowl %file-server] - %poke %file-server-action - !>([%serve-dir /'~link' /app/landscape %.n %.y]) - == == - == - :: - ++ on-poke - |= [=mark =vase] - ^- (quip card _this) - ?> (team:title our.bowl src.bowl) - :_ this - ?+ mark (on-poke:def mark vase) - %link-action - [(handle-action:do !<(action:store vase)) ~] - :: - %link-view-action - (handle-view-action:do !<(action:view vase)) - == - :: - ++ on-watch - |= =path - ^- (quip card _this) - ?: ?=([%json %seen ~] path) - [~ this] - ?: ?=([%tile ~] path) - :_ this - ~[give-tile-data:do] - ?. ?=([%json @ @ *] path) - (on-watch:def path) - =/ p=@ud (slav %ud i.t.path) - ?+ t.t.path (on-watch:def path) - [%submissions ~] - :_ this - (give-initial-submissions:do p ~) - :: - [%submissions ^] - :_ this - (give-initial-submissions:do p t.t.t.path) - :: - [%submission @ ^] - :_ this - (give-specific-submission:do p (break-discussion-path:store t.t.t.path)) - :: - [%discussions @ ^] - :_ this - (give-initial-discussions:do p (break-discussion-path:store t.t.t.path)) - == - :: - ++ on-agent - |= [=wire =sign:agent:gall] - ^- (quip card _this) - ?+ -.sign (on-agent:def wire sign) - %poke-ack - ?. ?=([%join-group @ @ @ @ ~] wire) - (on-agent:def wire sign) - ?^ p.sign - (on-agent:def wire sign) - =/ rid=resource - (de-path:resource t.t.wire) - =/ host=ship - (slav %p i.t.wire) - :_ this - (joined-group:do host rid) - :: - %kick - :_ this - =/ app=term - ?: ?=([%invites *] wire) - %invite-store - %link-store - [%pass wire %agent [our.bowl app] %watch wire]~ - :: - %fact - =* mark p.cage.sign - =* vase q.cage.sign - ?+ mark (on-agent:def wire sign) - %invite-update [(handle-invite-update:do !<(invite-update vase)) this] - %link-initial [~ this] - :: - %link-update - :_ this - :- (send-update:do !<(update:store vase)) - ?: =(/discussions wire) ~ - ~[give-tile-data:do] - == - == - :: - ++ on-arvo - |= [=wire =sign-arvo] - ^- (quip card _this) - ?. ?=([%e %bound *] sign-arvo) - (on-arvo:def wire sign-arvo) - ~? !accepted.sign-arvo - [dap.bowl "bind rejected!" binding.sign-arvo] - [~ this] - :: - ++ on-peek on-peek:def - ++ on-leave on-leave:def - ++ on-fail on-fail:def - -- -:: -~% %link-view-logic ..card ~ |_ =bowl:gall -+* md ~(. metadata bowl) - grp ~(. grpl bowl) ++* this . + def ~(. (default-agent this %|) bowl) :: -++ page-size 25 -++ get-paginated - |* [page=(unit @ud) list=(list)] - ^- [total=@ud pages=@ud page=_list] - =/ l=@ud (lent list) - :+ l - %+ add (div l page-size) - (min 1 (mod l page-size)) - ?~ page list - %+ swag - [(mul u.page page-size) page-size] - list +++ on-init [~ this] +++ on-save !>(state) +++ on-load + |= old-vase=vase + ^- (quip card _this) + :_ this(state [%2 ~]) + [%pass /connect %arvo %e %disconnect [~ /'~link']]~ :: -++ page-to-json - =, enjs:format - |* $: page-number=@ud - [total-items=@ud total-pages=@ud page=(list)] - item-to-json=$-(* json) - == - ^- json - %- pairs - :~ 'totalItems'^(numb total-items) - 'totalPages'^(numb total-pages) - 'pageNumber'^(numb page-number) - 'page'^a+(turn page item-to-json) - == -++ do-poke - |= [app=term =mark =vase] - ^- card - [%pass /create/[app]/[mark] %agent [our.bowl app] %poke mark vase] -:: -++ joined-group - |= [host=ship rid=resource] - ^- (list card) - =/ =path - (en-path:resource rid) - :~ - :: sync the group - :: - %^ do-poke %group-pull-hook - %pull-hook-action - !> ^- action:pull-hook - [%add host rid] - :: - :: sync the metadata - :: - %^ do-poke %metadata-hook - %metadata-hook-action - !> ^- metadata-hook-action:metadata-hook - [%add-synced host path] - :: - :: sync the collection - :: - %^ do-poke %link-listen-hook - %link-listen-action - !> ^- action:listen-hook - [%watch ~[name.rid]] - == -:: -++ handle-invite-update - |= upd=invite-update - ^- (list card) - ?. ?=(%accepted -.upd) ~ - ?. =(/link path.upd) ~ - =/ rid=resource - (de-path:resource path.invite.upd) - :~ :: add self - :* %pass - [%join-group (scot %p ship.invite.upd) path.invite.upd] - %agent [entity.rid %group-push-hook] - %poke %group-update - !> ^- action:group-store - [%add-members rid (sy our.bowl ~)] - == == -:: -++ handle-action - |= =action:store - ^- card - [%pass /action %agent [our.bowl %link-store] %poke %link-action !>(action)] -:: -++ handle-view-action - |= act=action:view - ^- (list card) - ?- -.act - %create (handle-create +.act) - %delete (handle-delete +.act) - %invite (handle-invite +.act) - == -:: -++ handle-create - |= [=path title=@t description=@t members=create-members:view real-group=?] - ^- (list card) - =/ group-path=^path - ?- -.members - %group path.members - :: - %ships - [%ship (scot %p our.bowl) path] - == - =; group-setup=(list card) - %+ weld group-setup - :~ :: add collection to metadata-store - :: - %^ do-poke %metadata-hook - %metadata-action - !> ^- metadata-action:md - :^ %add group-path - [%link path] - %* . *metadata:md - title title - description description - date-created now.bowl - creator our.bowl - == - :: - :: expose the metadata - :: - %^ do-poke %metadata-hook - %metadata-hook-action - !> ^- metadata-hook-action:metadata-hook - [%add-owned group-path] - :: - :: watch the collection ourselves - :: - %^ do-poke %link-listen-hook - %link-listen-action - !> ^- action:listen-hook - [%watch path] - == - ?: ?=(%group -.members) ~ - :: if the group is "real", make contact-view do the heavy lifting - =/ rid=resource - (de-path:resource group-path) - ?: real-group - :- %^ do-poke %contact-view - %contact-view-action - !> ^- contact-view-action:contact-view - [%groupify rid title description] - %+ turn ~(tap in ships.members) - |= =ship - ^- card - %^ do-poke %invite-hook - %invite-action - !> ^- invite-action - :^ %invite /link - (sham group-path eny.bowl) - :* our.bowl - %group-hook - group-path - ship - title - == - :: for "unmanaged" groups, do it ourselves - :: - =/ =policy - [%invite ships.members] - :* :: create the new group - :: - %^ do-poke %group-store - %group-action - !> ^- action:group-store - [%add-group rid policy %.y] - :: - :: send invites - :: - %+ turn ~(tap in ships.members) - |= =ship - ^- card - %^ do-poke %invite-hook - %invite-action - !> ^- invite-action - :^ %invite /link - (sham group-path eny.bowl) - :* our.bowl - %group-hook - group-path - ship - title - == - == -:: -++ handle-delete - |= =path - ^- (list card) - =/ groups=(list ^path) - (groups-from-resource:md [%link path]) - %- zing - %+ turn groups - |= =group=^path - =/ rid=resource - (de-path:resource group-path) - %+ snoc - ^- (list card) - :: if it's a real group, we can't/shouldn't unsync it. this leaves us with - :: no way to stop propagation of collection deletion. - :: - ?. ?=([%'~' ^] group-path) ~ - :: if it's an unmanaged group, we just stop syncing the group & metadata, - :: and clean up the group (after un-hooking it, to not push deletion). - :: - :~ %^ do-poke %group-hook - %group-hook-action - !> ^- action:group-hook - [%remove rid] - :: - %^ do-poke %metadata-hook - %metadata-hook-action - !> ^- metadata-hook-action:metadata-hook - [%remove group-path] - :: - %^ do-poke %group-store - %group-action - !> ^- action:group-store - [%remove-group rid ~] - == - :: remove collection from metadata-store - :: - %^ do-poke %metadata-store - %metadata-action - !> ^- metadata-action:md - [%remove group-path [%link path]] -:: -++ handle-invite - |= [=path ships=(set ship)] - ^- (list card) - %- zing - %+ turn (groups-from-resource:md %link path) - |= =group=^path - ^- (list card) - =/ rid=resource - (de-path:resource group-path) - =/ =group - (need (scry-group:grp rid)) - %- zing - :~ - ?. ?=(%invite -.policy.group) - ~ - :~ %^ do-poke %group-store - %group-action - !> ^- action:group-store - [%change-policy rid %invite %add-invites ships] - == - :: - %+ turn ~(tap in ships) - |= =ship - ^- card - %^ do-poke %invite-hook - %invite-action - !> ^- invite-action - :^ %invite /link - (sham group-path eny.bowl) - :* our.bowl - %group-pull-hook - group-path - ship - (rsh 3 1 (spat path)) - == - == -:: +give-tile-data: total unread count as json object -:: -::NOTE the full recalc of totals here probably isn't the end of the world. -:: but in case it is, well, here it is. -:: -++ give-tile-data - ^- card - =; =json - [%give %fact ~[/tile] %json !>(json)] - %+ frond:enjs:format 'unseen' - %- numb:enjs:format - %- %~ rep in - (scry-for (jug path url) /unseen) - |= [[=path unseen=(set url)] total=@ud] - %+ add total - ~(wyt in unseen) -:: -:: +give-initial-submissions: page of submissions on path -:: -:: for the / path, give page for every path -:: -:: result is in the shape of: { -:: "/some/path": { -:: totalItems: 1, -:: totalPages: 1, -:: pageNumber: 0, -:: page: [ -:: { commentCount: 1, ...restOfTheSubmission } -:: ] -:: }, -:: "/maybe/more": { etc } -:: } -:: -++ give-initial-submissions - ~/ %link-view-initial-submissions - |= [p=@ud =requested=path] - ^- (list card) - :_ :: only keep the base case alive (for updates), kick all others - :: - ?: &(=(0 p) ?=(~ requested-path)) ~ - [%give %kick ~ ~]~ - =; =json - [%give %fact ~ %json !>(json)] - %+ frond:enjs:format 'link-update' - %+ frond:enjs:format 'initial-submissions' - %- pairs:enjs:format - %+ turn - %~ tap by - %+ scry-for (map path submissions) - [%submissions requested-path] - |= [=path =submissions] - ^- [@t json] - :- (spat path) - =; =json - :: add unseen count - :: - ?> ?=(%o -.json) - :- %o - %+ ~(put by p.json) 'unseenCount' - %- numb:enjs:format - %~ wyt in - %+ scry-for (set url) - [%unseen path] - ?: &(=(0 p) ?=(~ requested-path)) - :: for a broad-scope initial result, only give total counts - :: - =, enjs:format - %- pairs - =+ l=(lent submissions) - :~ 'totalItems'^(numb l) - 'totalPages'^(numb (div l page-size)) - == - %^ page-to-json p - %+ get-paginated `p - submissions - |= =submission - ^- json - =/ =json (submission:enjs:store submission) - ?> ?=([%o *] json) - :: add in seen status - :: - =. p.json - %+ ~(put by p.json) 'seen' - :- %b - %+ scry-for ? - [%seen (build-discussion-path:store path url.submission)] - :: add in comment count - :: - =; comment-count=@ud - :- %o - %+ ~(put by p.json) 'commentCount' - (numb:enjs:format comment-count) - %- lent - ~| [path url.submission] - ^- comments - =- (~(got by (~(got by -) path)) url.submission) - %+ scry-for (per-path-url comments) - :- %discussions - (build-discussion-path:store path url.submission) -:: -++ give-specific-submission - |= [n=@ud =path =url] - :_ [%give %kick ~ ~]~ - =; =json - [%give %fact ~ %json !>(json)] - %+ frond:enjs:format 'link-update' - %+ frond:enjs:format 'submission' - ^- json - =; sub=(unit submission) - ?~ sub ~ - (submission:enjs:store u.sub) - =/ =submissions - =- (~(got by -) path) - %+ scry-for (map ^path submissions) - [%submissions path] - |- - ?~ submissions ~ - =* sub i.submissions - ?. =(url.sub url) - $(submissions t.submissions) - ?: =(0 n) `sub - $(n (dec n), submissions t.submissions) -:: -++ give-initial-discussions - |= [p=@ud =path =url] - ^- (list card) - :_ ?: =(0 p) ~ - [%give %kick ~ ~]~ - =; =json - [%give %fact ~ %json !>(json)] - %+ frond:enjs:format 'link-update' - %+ frond:enjs:format 'initial-discussions' - %^ page-to-json p - %+ get-paginated `p - =- (~(got by (~(got by -) path)) url) - %+ scry-for (per-path-url comments) - [%discussions (build-discussion-path:store path url)] - comment:enjs:store -:: -++ send-update - |= =update:store - ^- card - ?+ -.update ~|([dap.bowl %unexpected-update -.update] !!) - %submissions - %+ give-json - %+ frond:enjs:format 'link-update' - (update:enjs:store update) - :~ /json/0/submissions - (weld /json/0/submissions path.update) - == - :: - %discussions - %+ give-json - %+ frond:enjs:format 'link-update' - (update:enjs:store update) - :_ ~ - %+ weld /json/0/discussions - (build-discussion-path:store [path url]:update) - :: - %observation - %+ give-json - %+ frond:enjs:format 'link-update' - (update:enjs:store update) - ~[/json/seen] - == -:: -++ give-json - |= [=json paths=(list path)] - ^- card - [%give %fact paths %json !>(json)] -:: -++ scry-for - |* [=mold =path] - .^ mold - %gx - (scot %p our.bowl) - %link-store - (scot %da now.bowl) - (snoc `^path`path %noun) - == +++ on-poke on-poke:def +++ on-watch on-watch:def +++ on-agent on-agent:def +++ on-arvo on-arvo:def +++ on-peek on-peek:def +++ on-leave on-leave:def +++ on-fail on-fail:def -- diff --git a/pkg/arvo/app/spider.hoon b/pkg/arvo/app/spider.hoon index a6286ba8c..6a974f500 100644 --- a/pkg/arvo/app/spider.hoon +++ b/pkg/arvo/app/spider.hoon @@ -1,5 +1,5 @@ /- spider -/+ libstrand=strand, default-agent, verb +/+ libstrand=strand, default-agent, verb, server =, strand=strand:libstrand |% +$ card card:agent:gall @@ -17,15 +17,25 @@ $: starting=(map yarn [=trying =vase]) running=trie tid=(map tid yarn) + serving=(map tid [@ta =mark]) == :: +$ clean-slate-any $^ clean-slate-ket $% clean-slate-sig + clean-slate-1 clean-slate == :: +$ clean-slate + $: %2 + starting=(map yarn [=trying =vase]) + running=(list yarn) + tid=(map tid yarn) + serving=(map tid [@ta =mark]) + == +:: ++$ clean-slate-1 $: %1 starting=(map yarn [=trying =vase]) running=(list yarn) @@ -133,7 +143,10 @@ sc ~(. spider-core bowl) def ~(. (default-agent this %|) bowl) :: - ++ on-init on-init:def + ++ on-init + ^- (quip card _this) + :_ this + ~[bind-eyre:sc] ++ on-save clean-state:sc ++ on-load |^ @@ -141,7 +154,9 @@ =+ !<(any=clean-slate-any old-state) =? any ?=(^ -.any) (old-to-1 any) =? any ?=(~ -.any) (old-to-1 any) - ?> ?=(%1 -.any) + =^ upgrade-cards any + (old-to-2 any) + ?> ?=(%2 -.any) :: =. tid.state tid.any =/ yarns=(list yarn) @@ -154,12 +169,26 @@ (handle-stop-thread:sc (yarn-to-tid i.yarns) |) =^ cards-2 this $(yarns t.yarns) - [(weld cards-1 cards-2) this] + [:(weld upgrade-cards cards-1 cards-2) this] :: ++ old-to-1 |= old=clean-slate-ket - ^- clean-slate + ^- clean-slate-1 1+old(starting (~(run by starting.old) |=([* v=vase] none+v))) + :: + ++ old-to-2 + |= old=clean-slate-any + ^- (quip card clean-slate) + ?> ?=(?(%1 %2) -.old) + ?: ?=(%2 -.old) + `old + :- ~[bind-eyre:sc] + :* %2 + starting.old + running.old + tid.old + ~ + == -- :: ++ on-poke @@ -172,6 +201,9 @@ %spider-input (on-poke-input:sc !<(input vase)) %spider-start (handle-start-thread:sc !<(start-args vase)) %spider-stop (handle-stop-thread:sc !<([tid ?] vase)) + :: + %handle-http-request + (handle-http-request:sc !<([@ta =inbound-request:eyre] vase)) == [cards this] :: @@ -182,6 +214,7 @@ ?+ path (on-watch:def path) [%thread @ *] (on-watch:sc t.path) [%thread-result @ ~] (on-watch-result:sc i.t.path) + [%http-response *] `state == [cards this] :: @@ -216,6 +249,7 @@ ?+ wire (on-arvo:def wire sign-arvo) [%thread @ *] (handle-sign:sc i.t.wire t.t.wire sign-arvo) [%build @ ~] (handle-build:sc i.t.wire sign-arvo) + [%bind ~] `state == [cards this] :: On unexpected failure, kill all outstanding strands @@ -228,6 +262,41 @@ -- :: |_ =bowl:gall +:: +++ bind-eyre + ^- card + [%pass /bind %arvo %e %connect [~ /spider] %spider] +:: +++ handle-http-request + |= [eyre-id=@ta =inbound-request:eyre] + ^- (quip card _state) + ?> authenticated.inbound-request + =/ url + (parse-request-line:server url.request.inbound-request) + ?> ?=([%spider @t @t @t ~] site.url) + =* input-mark i.t.site.url + =* thread i.t.t.site.url + =* output-mark i.t.t.t.site.url + =/ =tid + (scot %uv (sham eny.bowl)) + =. serving.state + (~(put by serving.state) tid [eyre-id output-mark]) + =+ .^ + =tube:clay + %cc + /(scot %p our.bowl)/[q.byk.bowl]/(scot %da now.bowl)/json/[input-mark] + == + ?> ?=(^ body.request.inbound-request) + =/ body=json + (need (de-json:html q.u.body.request.inbound-request)) + =/ input=vase + (tube !>(body)) + =/ =start-args + [~ `tid thread input] + =^ cards state + (handle-start-thread start-args) + [cards state] +:: ++ on-poke-input |= input =/ yarn (~(got by tid.state) tid) @@ -394,6 +463,25 @@ :~ [%give %fact ~[/thread-result/[tid]] %thread-fail !>([term tang])] [%give %kick ~[/thread-result/[tid]] ~] == +++ thread-http-fail + |= [=tid =term =tang] + ^- (quip card ^state) + =- (fall - `state) + %+ bind + (~(get by serving.state) tid) + |= [eyre-id=@ta output=mark] + :_ state(serving (~(del by serving.state) tid)) + %+ give-simple-payload:app:server eyre-id + ^- simple-payload:http + :_ ~ :_ ~ + ?. ?=(http-error:spider term) + ((slog tang) 500) + ?- term + %bad-request 400 + %forbidden 403 + %nonexistent 404 + %offline 504 + == :: ++ thread-fail |= [=yarn =term =tang] @@ -402,7 +490,24 @@ =/ =tid (yarn-to-tid yarn) =/ fail-cards (thread-say-fail tid term tang) =^ cards state (thread-clean yarn) - [(weld fail-cards cards) state] + =^ http-cards state (thread-http-fail tid term tang) + [:(weld fail-cards cards http-cards) state] +:: +++ thread-http-response + |= [=tid =vase] + ^- (quip card ^state) + =- (fall - `state) + %+ bind + (~(get by serving.state) tid) + |= [eyre-id=@ta output=mark] + =+ .^ + =tube:clay + %cc + /(scot %p our.bowl)/[q.byk.bowl]/(scot %da now.bowl)/[output]/json + == + :_ state(serving (~(del by serving.state) tid)) + %+ give-simple-payload:app:server eyre-id + (json-response:gen:server !<(json (tube vase))) :: ++ thread-done |= [=yarn =vase] @@ -413,8 +518,10 @@ :~ [%give %fact ~[/thread-result/[tid]] %thread-done vase] [%give %kick ~[/thread-result/[tid]] ~] == + =^ http-cards state + (thread-http-response tid vase) =^ cards state (thread-clean yarn) - [(weld done-cards cards) state] + [:(weld done-cards cards http-cards) state] :: ++ thread-clean |= =yarn @@ -474,5 +581,5 @@ :: ++ clean-state !> ^- clean-slate - 1+state(running (turn (tap-yarn running.state) head)) + 2+state(running (turn (tap-yarn running.state) head)) -- diff --git a/pkg/arvo/lib/graph-view.hoon b/pkg/arvo/lib/graph-view.hoon new file mode 100644 index 000000000..2d0b473bb --- /dev/null +++ b/pkg/arvo/lib/graph-view.hoon @@ -0,0 +1,61 @@ +/- sur=graph-view +/+ resource, group-store +^? +=< [sur .] +=, sur +|% +++ dejs + =, dejs:format + |% + ++ action + |^ + ^- $-(json ^action) + %- of + :~ create+create + delete+delete + join+join + leave+leave + groupify+groupify + ::invite+invite + == + :: + ++ create + %- ou + :~ resource+(un dejs:resource) + title+(un so) + description+(un so) + mark+(uf ~ (mu so)) + associated+(un associated) + == + :: + ++ leave + %- ot + :~ resource+dejs:resource + == + :: + ++ delete + %- ot + :~ resource+dejs:resource + == + :: + ++ join + %- ot + :~ resource+dejs:resource + ship+(su ;~(pfix sig fed:ag)) + == + :: + ++ groupify + %- ou + :~ resource+(un dejs:resource) + to+(uf ~ (mu dejs:resource)) + == + ++ invite !! + :: + ++ associated + %- of + :~ group+dejs:resource + policy+policy:dejs:group-store + == + -- + -- +-- diff --git a/pkg/arvo/lib/graph.hoon b/pkg/arvo/lib/graph.hoon index 395c0554f..2e2bcdf1e 100644 --- a/pkg/arvo/lib/graph.hoon +++ b/pkg/arvo/lib/graph.hoon @@ -13,12 +13,24 @@ :: ++ get-graph |= res=resource - ^- marked-graph:store - %+ scry-for marked-graph:store + ^- update:store + %+ scry-for update:store /graph/(scot %p entity.res)/[name.res] :: -++ peek-log +++ get-update-log + |= rid=resource + ^- update-log:store + %+ scry-for update-log:store + /update-log/(scot %p entity.rid)/[name.rid] +:: +++ peek-update-log |= res=resource ^- (unit time) (scry-for (unit time) /peek-update-log/(scot %p entity.res)/[name.res]) +:: +++ get-update-log-subset + |= [res=resource start=@da] + ^- update-log:store + %+ scry-for update-log:store + /update-log-subset/(scot %p entity.res)/[name.res]/(scot %da start)/'~' -- diff --git a/pkg/arvo/lib/group.hoon b/pkg/arvo/lib/group.hoon index 880ab048a..9644fcc58 100644 --- a/pkg/arvo/lib/group.hoon +++ b/pkg/arvo/lib/group.hoon @@ -48,6 +48,13 @@ ^- ? =- (~(has in -) ship) (members-from-path group-path) +:: +++ is-admin + |= [=ship =group-path] + ^- ? + =/ tags tags:(fall (scry-group-path group-path) *group) + =/ admins=(set ^ship) (~(gut by tags) %admin ~) + (~(has in admins) ship) :: +role-for-ship: get role for user :: :: Returns ~ if no such group exists or user is not @@ -77,6 +84,7 @@ ?: (~(has in members.group) ship) [~ ~] ~ +:: ++ can-join-from-path |= [=path =ship] %+ scry-for diff --git a/pkg/arvo/lib/hood/drum.hoon b/pkg/arvo/lib/hood/drum.hoon index e7b91cfbe..a41c917a7 100644 --- a/pkg/arvo/lib/hood/drum.hoon +++ b/pkg/arvo/lib/hood/drum.hoon @@ -105,6 +105,8 @@ %file-server %glob %graph-store + %graph-pull-hook + %graph-push-hook == :: ++ deft-fish :: default connects @@ -207,7 +209,7 @@ == :: ++ on-load - |= [hood-version=?(%1 %2 %3 %4 %5 %6 %7 %8 %9) old=any-state] + |= [hood-version=?(%1 %2 %3 %4 %5 %6 %7 %8 %9 %10) old=any-state] =< se-abet =< se-view =. sat old =. dev (~(gut by bin) ost *source) @@ -236,6 +238,9 @@ (se-born | %home %group-pull-hook) =? ..on-load (lte hood-version %9) (se-born | %home %graph-store) + =? ..on-load (lte hood-version %10) + => (se-born | %home %graph-push-hook) + (se-born | %home %graph-pull-hook) ..on-load :: ++ reap-phat :: ack connect diff --git a/pkg/arvo/lib/strandio.hoon b/pkg/arvo/lib/strandio.hoon index 71299cb8b..80c4178a1 100644 --- a/pkg/arvo/lib/strandio.hoon +++ b/pkg/arvo/lib/strandio.hoon @@ -241,6 +241,16 @@ ;< our=@p bind:m get-our (watch wire [our term] path) :: +++ scry + |* [=mold =path] + =/ m (strand ,mold) + ^- form:m + ?> ?=(^ path) + ?> ?=(^ t.path) + ;< =bowl:spider bind:m get-bowl + %- pure:m + .^(mold i.path (scot %p our.bowl) i.t.path (scot %da now.bowl) t.t.path) +:: ++ leave |= [=wire =dock] =/ m (strand ,~) @@ -285,6 +295,20 @@ [%pass /wait/(scot %da until) %arvo %b %wait until] (send-raw-card card) :: +++ map-err + |* computation-result=mold + =/ m (strand ,computation-result) + |= [f=$-([term tang] [term tang]) computation=form:m] + ^- form:m + |= tin=strand-input:strand + =* loop $ + =/ c-res (computation tin) + ?: ?=(%cont -.next.c-res) + c-res(self.next ..loop(computation self.next.c-res)) + ?. ?=(%fail -.next.c-res) + c-res + c-res(err.next (f err.next.c-res)) +:: ++ set-timeout |* computation-result=mold =/ m (strand ,computation-result) @@ -478,6 +502,17 @@ `[%skip ~] `[%done +>.sign-arvo.u.in.tin] == +:: +check-online: require that peer respond before timeout +:: +++ check-online + |= [who=ship lag=@dr] + =/ m (strand ,~) + ^- form:m + %+ (map-err ,~) |=(* [%offline *tang]) + %+ (set-timeout ,~) lag + ;< ~ bind:m + (poke [who %hood] %helm-hi !>(~)) + (pure:m ~) :: :: Queue on skip, try next on fail %ignore :: diff --git a/pkg/arvo/mar/graph/update.hoon b/pkg/arvo/mar/graph/update.hoon index d5f0f4abe..1cd2a2068 100644 --- a/pkg/arvo/mar/graph/update.hoon +++ b/pkg/arvo/mar/graph/update.hoon @@ -1,7 +1,9 @@ /+ *graph-store |_ upd=update +++ grad %noun ++ grow |% + ++ noun upd ++ json (update:enjs upd) -- :: diff --git a/pkg/arvo/mar/graph/validator/link.hoon b/pkg/arvo/mar/graph/validator/link.hoon new file mode 100644 index 000000000..02de528df --- /dev/null +++ b/pkg/arvo/mar/graph/validator/link.hoon @@ -0,0 +1,27 @@ +/- *post +|_ i=indexed-post +++ grow + |% + ++ noun i + -- +++ grab + |% + ++ noun + |= p=* + =/ ip ;;(indexed-post p) + ?+ index.p.ip ~|(index+index.p.ip !!) + :: top-level link post; title and url + :: + [@ ~] + ?> ?=([[%text @] [%url @] ~] contents.p.ip) + ip + :: + :: comment on link post; comment text + :: + [@ @ ~] + ?> ?=([[%text @] ~] contents.p.ip) + ip + == + -- +++ grad %noun +-- diff --git a/pkg/arvo/mar/graph/view-action.hoon b/pkg/arvo/mar/graph/view-action.hoon new file mode 100644 index 000000000..ab8b0ef12 --- /dev/null +++ b/pkg/arvo/mar/graph/view-action.hoon @@ -0,0 +1,13 @@ +/+ *graph-view +|_ act=action +++ grad %noun +++ grow + |% + ++ noun act + -- +++ grab + |% + ++ noun action + ++ json action:dejs + -- +-- diff --git a/pkg/arvo/sur/graph-view.hoon b/pkg/arvo/sur/graph-view.hoon new file mode 100644 index 000000000..6514f2a49 --- /dev/null +++ b/pkg/arvo/sur/graph-view.hoon @@ -0,0 +1,45 @@ +/- *group, store=graph-store +/+ resource +^? +|% +:: $associated: A group to associate, or a policy if it is unmanaged +:: ++$ associated + $% [%group rid=resource] + [%policy =policy] + == +:: +:: $error: An error from a graph-view poke +:: +:: %offline: Ship is offline +:: %bad-perms: Not permitted +:: %unknown: Anything not described above +:: ++$ error + ?(%offline %bad-perms %unknown) +:: $action: A semantic action on graphs +:: +:: %create: Create a graph and associated metadata +:: %delete: Delete a graph +:: %join: Join a graph +:: %invite: Invite users to a graph +:: %groupify: Make graph into managed group +:: ++$ action + $% + $: %create + rid=resource + title=@t + description=@t + mark=(unit mark) + =associated + == + [%delete rid=resource] + [%leave rid=resource] + [%join rid=resource =ship] + ::[%invite rid=resource ships=(set ship)] + [%groupify rid=resource to=(unit resource)] + [%forward rid=resource =update:store] + == +-- + diff --git a/pkg/arvo/sur/spider.hoon b/pkg/arvo/sur/spider.hoon index f9035a91e..5fe7a7626 100644 --- a/pkg/arvo/sur/spider.hoon +++ b/pkg/arvo/sur/spider.hoon @@ -5,4 +5,10 @@ +$ input [=tid =cage] +$ tid tid:strand +$ bowl bowl:strand ++$ http-error + $? %bad-request :: 400 + %forbidden :: 403 + %nonexistent :: 404 + %offline :: 504 + == -- diff --git a/pkg/arvo/ted/graph/create.hoon b/pkg/arvo/ted/graph/create.hoon new file mode 100644 index 000000000..580d26019 --- /dev/null +++ b/pkg/arvo/ted/graph/create.hoon @@ -0,0 +1,60 @@ +/- spider, graph=graph-store, *metadata-store, *group, group-store +/+ strandio, resource, graph-view +=> +|% +++ strand strand:spider +++ poke poke:strandio +++ poke-our poke-our:strandio +:: +++ handle-group + |= [rid=resource =associated:graph-view] + =/ m (strand ,resource) + ?: ?=(%group -.associated) + (pure:m rid.associated) + =/ =action:group-store + [%add-group rid policy.associated %&] + ;< ~ bind:m (poke-our %group-store %group-action !>(action)) + ;< ~ bind:m + (poke-our %group-push-hook %push-hook-action !>([%add rid])) + (pure:m rid) +-- +:: +=, strand=strand:spider +^- thread:spider +|= arg=vase +=/ m (strand ,vase) +^- form:m +=+ !<(=action:graph-view arg) +?> ?=(%create -.action) +;< =bowl:spider bind:m get-bowl:strandio +:: Add graph to graph-store +:: +?. =(our.bowl entity.rid.action) + (strand-fail:strandio %bad-request ~) +=/ =update:graph + [%0 now.bowl %add-graph rid.action *graph:graph mark.action] +;< ~ bind:m + (poke-our %graph-store graph-update+!>(update)) +;< ~ bind:m + (poke-our %graph-push-hook %push-hook-action !>([%add rid.action])) +:: Add group, if graph is unmanaged +:: +;< group=resource bind:m + (handle-group rid.action associated.action) +=/ group-path=path + (en-path:resource group) +:: Setup metadata +:: +=/ =metadata + %* . *metadata + title title.action + description description.action + date-created now.bowl + creator our.bowl + == +=/ act=metadata-action + [%add group-path graph+(en-path:resource rid.action) metadata] +;< ~ bind:m (poke-our %metadata-hook %metadata-action !>(act)) +;< ~ bind:m + (poke-our %metadata-hook %metadata-hook-action !>([%add-owned group-path])) +(pure:m !>(~)) diff --git a/pkg/arvo/ted/graph/delete.hoon b/pkg/arvo/ted/graph/delete.hoon new file mode 100644 index 000000000..96e8524a7 --- /dev/null +++ b/pkg/arvo/ted/graph/delete.hoon @@ -0,0 +1,70 @@ +/- spider, graph-view, graph=graph-store, *metadata-store, *group +/+ strandio, resource +=> +|% +++ strand strand:spider +++ poke poke:strandio +++ poke-our poke-our:strandio +:: +++ scry-metadata + |= rid=resource + =/ m (strand ,(unit resource)) + ;< paxs=(unit (set path)) bind:m + %+ scry:strandio ,(unit (set path)) + ;: weld + /gx/metadata-store/resource/graph + (en-path:resource rid) + /noun + == + ?~ paxs (pure:m ~) + ?~ u.paxs (pure:m ~) + (pure:m `(de-path:resource n.u.paxs)) +:: +++ scry-group + |= rid=resource + =/ m (strand ,group) + ;< ugroup=(unit group) bind:m + %+ scry:strandio ,(unit group) + ;: weld + /gx/group-store/groups + (en-path:resource rid) + /noun + == + (pure:m (need ugroup)) +:: +++ delete-graph + |= rid=resource + =/ m (strand ,~) + ^- form:m + ;< =bowl:spider bind:m get-bowl:strandio + ;< ~ bind:m + (poke-our %graph-store %graph-update !>([%0 now.bowl %remove-graph rid])) + ;< ~ bind:m + (poke-our %graph-push-hook %push-hook-action !>([%remove rid])) + (pure:m ~) +-- +:: +^- thread:spider +|= arg=vase +=/ m (strand ,vase) +^- form:m +=+ !<(=action:graph-view arg) +?> ?=(%delete -.action) +;< =bowl:spider bind:m get-bowl:strandio +?. =(our.bowl entity.rid.action) + (strand-fail:strandio %bad-request ~) +;< ugroup-rid=(unit resource) bind:m + (scry-metadata rid.action) +?~ ugroup-rid !! +;< =group bind:m + (scry-group u.ugroup-rid) +?. hidden.group + ;< ~ bind:m + (delete-graph rid.action) + (pure:m !>(~)) +;< ~ bind:m + (poke-our %group-store %group-action !>([%remove-group rid.action ~])) +;< ~ bind:m + (poke-our %group-push-hook %push-hook-action !>([%remove rid.action])) +;< ~ bind:m (delete-graph rid.action) +(pure:m !>(~)) diff --git a/pkg/arvo/ted/graph/groupify.hoon b/pkg/arvo/ted/graph/groupify.hoon new file mode 100644 index 000000000..ca9c26edf --- /dev/null +++ b/pkg/arvo/ted/graph/groupify.hoon @@ -0,0 +1,74 @@ +/- spider, graph-view, graph=graph-store, *metadata-store, *group, *metadata-store +/+ strandio, resource +=> +|% +++ strand strand:spider +++ poke poke:strandio +++ poke-our poke-our:strandio +:: +++ check-live + |= who=ship + =/ m (strand ,~) + ^- form:m + %+ (set-timeout:strandio ,~) ~s20 + ;< ~ bind:m + (poke [who %hood] %helm-hi !>(~)) + (pure:m ~) +:: +++ scry-group + |= rid=resource + =/ m (strand ,group) + ^- form:m + ;< ugroup=(unit group) bind:m + %+ scry:strandio (unit group) + %+ weld /gx/group-store/groups + (snoc (en-path:resource rid) %noun) + ?> ?=(^ ugroup) + (pure:m u.ugroup) +:: +++ scry-metadatum + |= rid=resource + =/ m (strand ,metadata) + ^- form:m + =/ enc-path=@t + (scot %t (spat (en-path:resource rid))) + ;< umeta=(unit metadata) bind:m + %+ scry:strandio (unit metadata) + %+ weld /gx/metadata-store/metadata + /[enc-path]/graph/[enc-path]/noun + ?> ?=(^ umeta) + (pure:m u.umeta) +-- +:: +^- thread:spider +|= arg=vase +=/ m (strand ,vase) +^- form:m +=+ !<(=action:graph-view arg) +?> ?=(%groupify -.action) +;< =group bind:m (scry-group rid.action) +?. hidden.group + (strand-fail:strandio %bad-request ~) +;< =metadata bind:m + (scry-metadatum rid.action) +?~ to.action + ;< ~ bind:m + %+ poke-our %contact-view + contact-view-action+!>([%groupify rid.action title.metadata description.metadata]) + (pure:m !>(~)) +;< new=^group bind:m (scry-group u.to.action) +?< hidden.new +=/ new-path + (en-path:resource u.to.action) +=/ app-path + (en-path:resource rid.action) +=/ add-md=metadata-action + [%add new-path graph+app-path metadata] +;< ~ bind:m + (poke-our %metadata-store metadata-action+!>(add-md)) +;< ~ bind:m + %+ poke-our %metadata-store + metadata-action+!>([%remove app-path graph+app-path]) +;< ~ bind:m + (poke-our %group-store %group-update !>([%remove-group rid.action])) +(pure:m !>(~)) diff --git a/pkg/arvo/ted/graph/join.hoon b/pkg/arvo/ted/graph/join.hoon new file mode 100644 index 000000000..59b48ec92 --- /dev/null +++ b/pkg/arvo/ted/graph/join.hoon @@ -0,0 +1,61 @@ +/- spider, graph-view, graph=graph-store, *metadata-store, *group +/+ strandio, resource +=> +|% +++ strand strand:spider +++ fail strand-fail:strand +++ poke poke:strandio +++ poke-our poke-our:strandio +:: +++ scry-metadata + |= rid=resource + =/ m (strand ,(unit resource)) + ^- form:m + ;< pax=(unit (set path)) bind:m + %+ scry:strandio ,(unit (set path)) + ;: weld + /gx/metadata-store/resource/graph + (en-path:resource rid) + /noun + == + %- pure:m + ?~ pax ~ + ?~ u.pax ~ + `(de-path:resource n.u.pax) +-- +:: +^- thread:spider +|= arg=vase +=/ m (strand ,vase) +^- form:m +=+ !<(=action:graph-view arg) +?> ?=(%join -.action) +;< =bowl:spider bind:m get-bowl:strandio +?: =(our.bowl entity.rid.action) + (fail %bad-request ~) +;< group=(unit resource) bind:m (scry-metadata rid.action) +?^ group + :: We have group, graph is managed + ;< ~ bind:m + %+ poke-our %graph-pull-hook + pull-hook-action+!>([%add ship.action rid.action]) + (pure:m !>(~)) +:: Else, add group then join +;< ~ bind:m + %+ (map-err:strandio ,~) |=(* [%forbidden ~]) + %+ poke + [ship.action %group-push-hook] + group-update+!>([%add-members rid.action (sy our.bowl ~)]) +:: +;< ~ bind:m + %+ poke-our %group-pull-hook + pull-hook-action+!>([%add ship.action rid.action]) +:: +;< ~ bind:m + %+ poke-our %metadata-hook + metadata-hook-action+!>([%add-synced ship.action rid.action]) +:: +;< ~ bind:m + %+ poke-our %graph-pull-hook + pull-hook-action+!>([%add ship.action rid.action]) +(pure:m !>(~)) diff --git a/pkg/arvo/ted/graph/leave.hoon b/pkg/arvo/ted/graph/leave.hoon new file mode 100644 index 000000000..e85359d84 --- /dev/null +++ b/pkg/arvo/ted/graph/leave.hoon @@ -0,0 +1,67 @@ +/- spider, graph-view, graph=graph-store, *metadata-store, *group +/+ strandio, resource +=> +|% +++ strand strand:spider +++ poke poke:strandio +++ poke-our poke-our:strandio +:: +++ scry-metadata + |= rid=resource + =/ m (strand ,resource) + ^- form:m + ;< pax=(unit (set path)) bind:m + %+ scry:strandio ,(unit (set path)) + ;: weld + /gx/metadata-store/resource/graph + (en-path:resource rid) + /noun + == + ?> ?=(^ pax) + ?> ?=(^ u.pax) + (pure:m (de-path:resource n.u.pax)) +:: +++ scry-group + |= rid=resource + =/ m (strand ,group) + ^- form:m + ;< ugroup=(unit group) bind:m + %+ scry:strandio ,(unit group) + ;: weld + /gx/group-store/resource/graph + (en-path:resource rid) + /noun + == + (pure:m (need ugroup)) +:: +++ delete-graph + |= rid=resource + =/ m (strand ,~) + ^- form:m + ;< ~ bind:m + (poke-our %graph-pull-hook %pull-hook-action !>([%remove rid])) + ;< ~ bind:m + (poke-our %graph-store %graph-update !>([%remove-graph rid])) + (pure:m ~) +-- +:: +^- thread:spider +|= arg=vase +=/ m (strand ,vase) +^- form:m +=+ !<([=action:graph-view ~] arg) +?> ?=(%leave -.action) +;< =bowl:spider bind:m get-bowl:strandio +?: =(our.bowl entity.rid.action) + (strand-fail:strandio %bad-request ~) +;< group-rid=resource bind:m (scry-metadata rid.action) +;< g=group bind:m (scry-group group-rid) +?. hidden.g + ;< ~ bind:m (delete-graph rid.action) + (pure:m !>(~)) +;< ~ bind:m + (poke-our %group-push-hook %pull-hook-action !>([%remove rid.action])) +;< ~ bind:m + (poke-our %group-store %group-action !>([%remove-group rid.action ~])) +;< ~ bind:m (delete-graph rid.action) +(pure:m !>(~)) diff --git a/pkg/interface/src/logic/api/base.ts b/pkg/interface/src/logic/api/base.ts index b18a34516..da17a9ad1 100644 --- a/pkg/interface/src/logic/api/base.ts +++ b/pkg/interface/src/logic/api/base.ts @@ -9,7 +9,6 @@ export default class BaseApi { unsubscribe(id: number) { this.channel.unsubscribe(id); - } subscribe(path: Path, method, ship = this.ship, app: string, success, fail, quit) { @@ -37,19 +36,20 @@ export default class BaseApi { ); } - action(appl: string, mark: string, data: any): Promise { + action( + appl: string, + mark: string, + data: any, + ship = (window as any).ship + ): Promise { return new Promise((resolve, reject) => { this.channel.poke( - (window as any).ship, + ship, appl, mark, data, - (json) => { - resolve(json); - }, - (err) => { - reject(err); - } + (json) => { resolve(json); }, + (err) => { reject(err); } ); }); } @@ -57,4 +57,14 @@ export default class BaseApi { scry(app: string, path: Path): Promise { return fetch(`/~/scry/${app}${path}.json`).then(r => r.json() as Promise); } + + async spider(inputMark: string, outputMark: string, threadName: string, body: any): Promise { + const res = await fetch(`/spider/${inputMark}/${threadName}/${outputMark}.json`, { + method: 'POST', + body: JSON.stringify(body) + }); + + return res.json(); + } + } diff --git a/pkg/interface/src/logic/api/graph.ts b/pkg/interface/src/logic/api/graph.ts index d58332202..bc5abc77a 100644 --- a/pkg/interface/src/logic/api/graph.ts +++ b/pkg/interface/src/logic/api/graph.ts @@ -1,7 +1,10 @@ import BaseApi from './base'; import { StoreState } from '../store/type'; import { Patp, Path, PatpNoSig } from '~/types/noun'; - +import _ from 'lodash'; +import {makeResource, resourceFromPath} from '../lib/group'; +import {GroupPolicy, Enc, Post} from '~/types'; +import { deSig } from '~/logic/lib/util'; export const createPost = (contents: Object[], parentIndex: string = '') => { return { @@ -20,8 +23,93 @@ export default class GraphApi extends BaseApi { return this.action('graph-store', 'graph-update', action) } + private viewAction(threadName: string, action: any) { + return this.spider('graph-view-action', 'json', threadName, action); + } + + private hookAction(ship: Patp, action: any): Promise { + return this.action('graph-push-hook', 'graph-update', action, deSig(ship)); + } + + createManagedGraph( + name: string, + title: string, + description: string, + group: Path + ) { + const associated = { group: resourceFromPath(group) }; + const resource = makeResource(`~${window.ship}`, name); + + return this.viewAction('graph-create', { + "create": { + resource, + title, + description, + associated + } + }); + } + + createUnmanagedGraph( + name: string, + title: string, + description: string, + policy: Enc + ) { + const resource = makeResource(`~${window.ship}`, name); + + return this.viewAction('graph-create', { + "create": { + resource, + title, + description, + associated: { policy } + } + }); + } + + joinGraph(ship: Patp, name: string) { + const resource = makeResource(ship, name); + return this.viewAction('graph-join', { + join: { + resource, + ship, + } + }); + } + + deleteGraph(name: string) { + const resource = makeResource(`~${window.ship}`, name); + return this.viewAction('graph-delete', { + "delete": { + resource + } + }); + } + + leaveGraph(ship: Patp, name: string) { + const resource = makeResource(ship, name); + return this.viewAction('graph-leave', { + "leave": { + resource + } + }); + } + + groupifyGraph(ship: Patp, name: string, toPath?: string) { + const resource = makeResource(ship, name); + const to = toPath && resourceFromPath(toPath); + + return this.viewAction('graph-groupify', { + groupify: { + resource, + to + } + }); + } + addGraph(ship: Patp, name: string, graph: any, mark: any) { - this.storeAction({ + return this.storeAction({ 'add-graph': { resource: { ship, name }, graph, @@ -30,31 +118,24 @@ export default class GraphApi extends BaseApi { }); } - removeGraph(ship: Patp, name: string) { - this.storeAction({ - 'remove-graph': { - resource: { ship, name } - } - }); - } - - addPost(ship: Patp, name: string, post: Object) { + addPost(ship: Patp, name: string, post: Post) { let nodes = {}; + const resource = { ship, name }; nodes[post.index] = { post, children: { empty: null } }; - this.storeAction({ + return this.hookAction(ship, { 'add-nodes': { - resource: { ship, name }, + resource, nodes } }); } addNodes(ship: Patp, name: string, nodes: Object) { - this.storeAction({ + this.hookAction(ship, { 'add-nodes': { resource: { ship, name }, nodes @@ -63,7 +144,7 @@ export default class GraphApi extends BaseApi { } removeNodes(ship: Patp, name: string, indices: string[]) { - this.storeAction({ + return this.hookAction(ship, { 'remove-nodes': { resource: { ship, name }, indices @@ -107,7 +188,7 @@ export default class GraphApi extends BaseApi { }); } - getGraphSubset(ship: string, resource: string, start: string, end: start) { + getGraphSubset(ship: string, resource: string, start: string, end: string) { this.scry( 'graph-store', `/graph-subset/${ship}/${resource}/${end}/${start}` diff --git a/pkg/interface/src/logic/lib/group.ts b/pkg/interface/src/logic/lib/group.ts index 64b035b51..bd4939c2f 100644 --- a/pkg/interface/src/logic/lib/group.ts +++ b/pkg/interface/src/logic/lib/group.ts @@ -13,3 +13,8 @@ export function resourceFromPath(path: Path): Resource { const [, , ship, name] = path.split('/'); return { ship, name } } + +export function makeResource(ship: string, name:string) { + return { ship, name }; +} + diff --git a/pkg/interface/src/logic/reducers/graph-update.js b/pkg/interface/src/logic/reducers/graph-update.js index c8253f229..54047e72a 100644 --- a/pkg/interface/src/logic/reducers/graph-update.js +++ b/pkg/interface/src/logic/reducers/graph-update.js @@ -140,10 +140,21 @@ const addNodes = (json, state) => { }; const removeNodes = (json, state) => { + const _remove = (graph, index) => { + if (index.length === 1) { + graph.delete(index[0]); + } else { + const child = graph.get(index[0]); + _remove(child.children, index.slice(1)); + graph.set(index[0], child); + } + }; const data = _.get(json, 'remove-nodes', false); if (data) { console.log(data); - if (!(data.resource in state.graphs)) { return; } + const { ship, name } = data.resource; + const res = `${ship}/${name}`; + if (!(res in state.graphs)) { return; } data.indices.forEach((index) => { console.log(index); @@ -151,13 +162,7 @@ const removeNodes = (json, state) => { let indexArr = index.split('/').slice(1).map((ind) => { return parseInt(ind, 10); }); - - if (indexArr.length === 1) { - state.graphs[data.resource].delete(indexArr[0]); - } else { - // TODO: recursive - } - + _remove(state.graphs[res], indexArr); }); } }; diff --git a/pkg/interface/src/logic/store/store.ts b/pkg/interface/src/logic/store/store.ts index 8bb5794d9..aab7823db 100644 --- a/pkg/interface/src/logic/store/store.ts +++ b/pkg/interface/src/logic/store/store.ts @@ -66,7 +66,7 @@ export default class GlobalStore extends BaseStore { associations: { chat: {}, contacts: {}, - link: {}, + graph: {}, publish: {} }, groups: {}, diff --git a/pkg/interface/src/logic/store/type.ts b/pkg/interface/src/logic/store/type.ts index ffb989fa5..6e94da33d 100644 --- a/pkg/interface/src/logic/store/type.ts +++ b/pkg/interface/src/logic/store/type.ts @@ -12,6 +12,7 @@ import { LaunchState, WeatherState } from '~/types/launch-update'; import { LinkComments, LinkCollections, LinkSeen } from '~/types/link-update'; import { ConnectionStatus } from '~/types/connection'; import { BackgroundConfig, LocalUpdateRemoteContentPolicy } from '~/types/local-update'; +import {Graphs} from '~/types/graph-update'; export interface StoreState { // local state @@ -36,7 +37,7 @@ export interface StoreState { groupKeys: Set; permissions: Permissions; s3: S3State; - graphs: Object; + graphs: Graphs; graphKeys: Set; diff --git a/pkg/interface/src/types/graph-update.ts b/pkg/interface/src/types/graph-update.ts new file mode 100644 index 000000000..c9fb6a19c --- /dev/null +++ b/pkg/interface/src/types/graph-update.ts @@ -0,0 +1,30 @@ +import {Patp} from "./noun"; + + +export interface TextContent { text: string; }; +export interface UrlContent { url: string; } +export interface CodeContent { expresssion: string; output: string; }; +export interface ReferenceContent { uid: string; } +export type Content = TextContent | UrlContent | CodeContent | ReferenceContent; + +export interface Post { + author: Patp; + contents: Content[]; + hash?: string; + index: string; + pending?: boolean; + signatures: string[]; + 'time-sent': number; +} + + +export interface GraphNode { + children: Graph; + post: Post; +} + +export type Graph = Map; + +export type Graphs = { [rid: string]: Graph }; + + diff --git a/pkg/interface/src/types/index.ts b/pkg/interface/src/types/index.ts index 3f7cda54c..44a608d9c 100644 --- a/pkg/interface/src/types/index.ts +++ b/pkg/interface/src/types/index.ts @@ -5,6 +5,7 @@ export * from './connection'; export * from './contact-update'; export * from './global'; export * from './group-update'; +export * from './graph-update'; export * from './invite-update'; export * from './launch-update'; export * from './link-listen-update'; diff --git a/pkg/interface/src/views/apps/links/app.js b/pkg/interface/src/views/apps/links/app.js index 080aefae7..ad9fb29b8 100644 --- a/pkg/interface/src/views/apps/links/app.js +++ b/pkg/interface/src/views/apps/links/app.js @@ -10,26 +10,20 @@ import { Skeleton } from './components/skeleton'; import { NewScreen } from './components/new'; import { SettingsScreen } from './components/settings'; import { MessageScreen } from './components/lib/message-screen'; -import { Links } from './components/links-list'; -import { LinkDetail } from './components/link'; +import { LinkList } from './components/link-list'; +import { LinkDetail } from './components/link-detail'; import { - makeRoutePath, amOwnerOfGroup, base64urlDecode } from '~/logic/lib/util'; -export class LinksApp extends Component { - constructor(props) { - super(props); - } +export class LinksApp extends Component { componentDidMount() { // preload spinner asset new Image().src = '/~landscape/img/Spinner.png'; - this.props.api.links.getPage('', 0); - this.props.subscription.startApp('link'); this.props.subscription.startApp('graph'); if (!this.props.sidebarShown) { this.props.api.local.sidebarToggle(); @@ -37,7 +31,6 @@ export class LinksApp extends Component { } componentWillUnmount() { - this.props.subscription.stopApp('link'); this.props.subscription.stopApp('graph'); } @@ -45,26 +38,20 @@ export class LinksApp extends Component { const { props } = this; const contacts = props.contacts ? props.contacts : {}; const groups = props.groups ? props.groups : {}; - const associations = props.associations ? props.associations : { link: {}, contacts: {} }; - const links = props.links ? props.links : {}; - const comments = props.linkComments ? props.linkComments : {}; - const seen = props.linksSeen ? props.linksSeen : {}; - const totalUnseen = _.reduce( - links, - (acc, collection) => acc + collection.unseenCount, - 0 - ); + const associations = + props.associations ? props.associations : { graph: {}, contacts: {} }; + const graphKeys = props.graphKeys || new Set([]); + const graphs = props.graphs || {}; const invites = props.invites ? props.invites : {}; - const listening = props.linkListening; const { api, sidebarShown, hideAvatars, hideNicknames, s3, remoteContentPolicy } = this.props; return ( <> - {totalUnseen > 0 ? `(${totalUnseen}) ` : ''}OS1 - Links + OS1 - Links + graphKeys={graphKeys}> ); @@ -94,29 +79,28 @@ export class LinksApp extends Component { invites={invites} groups={groups} sidebarShown={sidebarShown} - links={links} - listening={listening} api={api} - > + graphKeys={graphKeys}> ); }} /> - { - const resourcePath = '/' + props.match.params.resource; + const resource = + `${props.match.params.ship}/${props.match.params.name}`; const autoJoin = () => { try { - api.links.joinCollection(resourcePath); - props.history.push(makeRoutePath(resourcePath)); + // TODO: graph join + props.history.push(`/~link/${resource}`); } catch(err) { setTimeout(autoJoin, 2000); } @@ -124,51 +108,15 @@ export class LinksApp extends Component { autoJoin(); }} /> - { - const popout = props.match.url.includes('/popout/'); - const resourcePath = '/' + props.match.params.resource; - const resource = associations.link[resourcePath] || { metadata: {} }; - - const contactDetails = contacts[resource['group-path']] || {}; - const group = groups[resource['group-path']] || new Set([]); - const amOwner = amOwnerOfGroup(resource['group-path']); - - return ( - - - - ); - }} - /> - { + const resourcePath = + `${props.match.params.ship}/${props.match.params.name}`; const popout = props.match.url.includes('/popout/'); - const resourcePath = '/' + props.match.params.resource; - const resource = associations.link[resourcePath] || false; + const metPath = `/ship/~${resourcePath}`; + const resource = + associations.graph[metPath] ? + associations.graph[metPath] : { metadata: {} }; const contactDetails = contacts[resource['group-path']] || {}; const group = groups[resource['group-path']] || new Set([]); @@ -182,149 +130,125 @@ export class LinksApp extends Component { selected={resourcePath} sidebarShown={sidebarShown} popout={popout} - links={links} - listening={listening} - api={api} - > + graphKeys={graphKeys} + api={api}> + + ); + }} + /> + { + const resourcePath = + `${props.match.params.ship}/${props.match.params.name}`; + const resource = + associations.graph[resourcePath] ? + associations.graph[resourcePath] : { metadata: {} }; + const contactDetails = contacts[resource['group-path']] || {}; + const popout = props.match.url.includes('/popout/'); + const graph = graphs[resourcePath] || null; + + if (!graph) { + api.graph.getGraph( + `~${props.match.params.ship}`, + props.match.params.name + ); + } + + return ( + + ); }} /> - { - const resourcePath = '/' + props.match.params.resource; - const resource = associations.link[resourcePath] || { metadata: {} }; + { + const resourcePath = + `${props.match.params.ship}/${props.match.params.name}`; + const resource = + associations.graph[resourcePath] ? + associations.graph[resourcePath] : { metadata: {} }; + const popout = props.match.url.includes('/popout/'); + const contactDetails = contacts[resource['group-path']] || {}; - const amOwner = amOwnerOfGroup(resource['group-path']); + const indexArr = props.match.params.index.split('-'); + const graph = graphs[resourcePath] || null; - const contactDetails = contacts[resource['group-path']] || {}; + if (indexArr.length <= 1) { + return
Malformed URL
; + } - const page = props.match.params.page || 0; + const index = parseInt(indexArr[1], 10); + const node = !!graph ? graph.get(index) : null; - const popout = props.match.url.includes('/popout/'); - - const channelLinks = links[resourcePath] - ? links[resourcePath] - : { local: {} }; - - const channelComments = comments[resourcePath] - ? comments[resourcePath] - : {}; - - const channelSeen = seen[resourcePath] - ? seen[resourcePath] - : {}; - - return ( - - - + if (!graph) { + api.graph.getGraph( + `~${props.match.params.ship}`, + props.match.params.name ); - }} - /> - { - const resourcePath = '/' + props.match.params.resource; - const resource = associations.link[resourcePath] || { metadata: {} }; - - const amOwner = amOwnerOfGroup(resource['group-path']); - - const popout = props.match.url.includes('/popout/'); - - const contactDetails = contacts[resource['group-path']] || {}; - - const index = props.match.params.index || 0; - const page = props.match.params.page || 0; - const url = base64urlDecode(props.match.params.encodedUrl); - - const data = links[resourcePath] - ? links[resourcePath][page] - ? links[resourcePath][page][index] - : {} - : {}; - const coms = !comments[resourcePath] - ? undefined - : comments[resourcePath][url]; - - const commentPage = props.match.params.commentpage || 0; - - return ( - - + - - ); - }} - /> + remoteContentPolicy={remoteContentPolicy} /> + + ); + }} + />
); diff --git a/pkg/interface/src/views/apps/links/components/lib/channels-item.js b/pkg/interface/src/views/apps/links/components/lib/channel-item.js similarity index 83% rename from pkg/interface/src/views/apps/links/components/lib/channels-item.js rename to pkg/interface/src/views/apps/links/components/lib/channel-item.js index 735cc79e8..9ed136232 100644 --- a/pkg/interface/src/views/apps/links/components/lib/channels-item.js +++ b/pkg/interface/src/views/apps/links/components/lib/channel-item.js @@ -1,8 +1,8 @@ import React, { Component } from 'react'; import { Link } from 'react-router-dom'; -import { makeRoutePath } from '~/logic/lib/util'; -export class ChannelsItem extends Component { + +export class ChannelItem extends Component { render() { const { props } = this; @@ -15,7 +15,7 @@ export class ChannelsItem extends Component { : null; return ( - +

{props.name}

diff --git a/pkg/interface/src/views/apps/links/components/lib/channel-sidebar.js b/pkg/interface/src/views/apps/links/components/lib/channel-sidebar.js index 6b36e9115..e1bb9f9f0 100644 --- a/pkg/interface/src/views/apps/links/components/lib/channel-sidebar.js +++ b/pkg/interface/src/views/apps/links/components/lib/channel-sidebar.js @@ -6,120 +6,105 @@ import SidebarInvite from '~/views/components/SidebarInvite'; import { Welcome } from './welcome'; import { alphabetiseAssociations } from '~/logic/lib/util'; -export class ChannelsSidebar extends Component { - // drawer to the left - - render() { - const { props } = this; - - const sidebarInvites = Object.keys(props.invites) - .map((uid) => { - return ( - props.api.invite.accept('/link', uid)} - onDecline={() => props.api.invite.decline('/link', uid)} - /> - ); - }); - - const associations = props.associations.contacts ? alphabetiseAssociations(props.associations.contacts) : {}; - - const groupedChannels = {}; - [...props.listening].map((path) => { - const groupPath = props.associations.link[path] ? - props.associations.link[path]['group-path'] : ''; - - - if (groupPath in associations) { - if (groupedChannels[groupPath]) { - const array = groupedChannels[groupPath]; - array.push(path); - groupedChannels[groupPath] = array; - } else { - groupedChannels[groupPath] = [path]; - } - } else { - if (groupedChannels['/~/']) { - const array = groupedChannels['/~/']; - array.push(path); - groupedChannels['/~/'] = array; - } else { - groupedChannels['/~/'] = [path]; - }; - } - }); - - let i = -1; - const groupedItems = Object.keys(associations) - .map((each) => { - const channels = groupedChannels[each]; - if (!channels || channels.length === 0) - return; - i++; - if (groupedChannels['/~/'] && groupedChannels['/~/'].length !== 0) { - i++; - } - +export const ChannelSidebar = (props) => { + const sidebarInvites = Object.keys(props.invites) + .map((uid) => { return ( - props.api.invite.accept('/link', uid)} + onDecline={() => props.api.invite.decline('/link', uid)} /> ); }); - if (groupedChannels['/~/'] && groupedChannels['/~/'].length !== 0) { - groupedItems.unshift( - - ); - } - const activeClasses = (props.active === 'collections') ? ' ' : 'dn-s '; + const associations = props.associations.contacts ? + alphabetiseAssociations(props.associations.contacts) : {}; - let hiddenClasses = true; + const graphAssoc = props.associations.graph || {}; + + const groupedChannels = {}; + [...props.graphKeys].map((gKey) => { + const path = `/ship/~${gKey.split('/')[0]}/${gKey.split('/')[1]}`; + const groupPath = graphAssoc[path] ? graphAssoc[path]['group-path'] : ''; + + if (groupPath in associations) { + // managed + + if (groupedChannels[groupPath]) { + const array = groupedChannels[groupPath]; + array.push(path); + groupedChannels[groupPath] = array; + } else { + groupedChannels[groupPath] = [path]; + } - if (props.popout) { - hiddenClasses = false; } else { - hiddenClasses = props.sidebarShown; + // unmanaged + + if (groupedChannels['/~/']) { + const array = groupedChannels['/~/']; + array.push(path); + groupedChannels['/~/'] = array; + } else { + groupedChannels['/~/'] = [path]; + } } + }); + + const groupedItems = Object.keys(associations).map((each, i) => { + const channels = groupedChannels[each]; + if (!channels || channels.length === 0) { return; } return ( -

-
-
- - New Collection - -
- - {sidebarInvites} - {groupedItems} -
-
+ + ); + }); + + if (groupedChannels['/~/'] && groupedChannels['/~/'].length !== 0) { + groupedItems.push( + ); } -} + + const activeClasses = (props.active === 'collections') ? ' ' : 'dn-s '; + const hiddenClasses = !!props.popout ? false : props.sidebarShown; + + return ( +
+
+
+ + New Collection + +
+ + {sidebarInvites} + {groupedItems} +
+
+ ); +}; diff --git a/pkg/interface/src/views/apps/links/components/lib/comment-item.js b/pkg/interface/src/views/apps/links/components/lib/comment-item.js index dfe9b3403..1f37e045b 100644 --- a/pkg/interface/src/views/apps/links/components/lib/comment-item.js +++ b/pkg/interface/src/views/apps/links/components/lib/comment-item.js @@ -5,66 +5,41 @@ import moment from 'moment'; import { Box, Text, Row } from '@tlon/indigo-react'; import RichText from '~/views/components/RichText'; -export class CommentItem extends Component { - constructor(props) { - super(props); - this.state = { - timeSinceComment: this.getTimeSinceComment() - }; - } +export const CommentItem = (props) => { + const content = props.post.contents[0].text; + const timeSent = + moment.unix(props.post['time-sent'] / 1000).format('hh:mm a'); - componentDidMount() { - this.updateTimeSinceNewestMessageInterval = setInterval( () => { - this.setState({ timeSinceComment: this.getTimeSinceComment() }); - }, 60000); - } - - componentWillUnmount() { - if (this.updateTimeSinceNewestMessageInterval) { - clearInterval(this.updateTimeSinceNewestMessageInterval); - this.updateTimeSinceNewestMessageInterval = null; - } - } - - getTimeSinceComment() { - return this.props.time ? - moment.unix(this.props.time / 1000).from(moment.utc()) - : ''; - } - - render() { - const props = this.props; - - const member = props.member || false; - - const showAvatar = props.avatar && !props.hideAvatars; - const showNickname = props.nickname && !props.hideNicknames; - const img = showAvatar - ? - : + : ; + color={`#${props.color}`} + classes={(!!props.member ? 'mix-blend-diff' : '')} + />; - return ( - - - {img} - - - {showNickname ? props.nickname : cite(props.ship)} - - - {this.state.timeSinceComment} - - + return ( + + + {img} + + + {showNickname ? props.nickname : cite(props.post.author)} + + {timeSent} - {props.content} - - ); - } + + + + + {content} + + + + + ); } -export default CommentItem; diff --git a/pkg/interface/src/views/apps/links/components/lib/comment-submit.js b/pkg/interface/src/views/apps/links/components/lib/comment-submit.js new file mode 100644 index 000000000..ea613f99e --- /dev/null +++ b/pkg/interface/src/views/apps/links/components/lib/comment-submit.js @@ -0,0 +1,83 @@ +import React, { Component } from 'react'; +import { Spinner } from '~/views/components/Spinner'; +import { createPost } from '~/logic/api/graph'; + + +export class CommentSubmit extends Component { + constructor(props) { + super(props); + + this.state = { + comment: '', + commentFocus: false, + disabled: false + }; + } + + onClickPost() { + const parentIndex = this.props.parentIndex || ''; + let post = createPost([ + { text: this.state.comment }, + ], parentIndex); + + this.setState({ disabled: true }, () => { + this.props.api.graph.addPost( + `~${this.props.ship}`, + this.props.name, + post + ).then((r) => { + this.setState({ + disabled: false, + comment: '' + }); + }); + }); + } + + setComment(event) { + this.setState({ comment: event.target.value }); + } + + render() { + const { state, props } = this; + const focus = (state.commentFocus) + ? 'b--black b--white-d' + : 'b--gray4 b--gray2-d'; + + const activeClasses = state.comment + ? 'black white-d pointer' + : 'gray2 b--gray2'; + + return ( +
+