mirror of
https://github.com/urbit/shrub.git
synced 2024-11-28 13:54:20 +03:00
Merge branch 'link-full' (#1970)
* origin/link-full: link-server-hook: disable verb by default link: make network comms work link: add minimal link-server-hook and link-webext server: properly defined request-line type link: social bookmarking core implementation group-store: create, add, remove generators Signed-off-by: Jared Tobin <jared@tlon.io>
This commit is contained in:
commit
5febd033cf
1
.gitignore
vendored
1
.gitignore
vendored
@ -16,3 +16,4 @@ release/
|
||||
**/*.swp
|
||||
**/*.swo
|
||||
**/*-min.js
|
||||
pkg/interface/link-webext/web-ext-artifacts
|
||||
|
231
pkg/arvo/app/link-listen-hook.hoon
Normal file
231
pkg/arvo/app/link-listen-hook.hoon
Normal file
@ -0,0 +1,231 @@
|
||||
:: link-listen-hook: get your friends' bookmarks
|
||||
::
|
||||
:: on-init, subscribes to all groups on this ship.
|
||||
:: for every ship in a group, we subscribe to their link's local-pages
|
||||
:: at the group path (through link-proxy-hook),
|
||||
:: and forwards all entries into our link as submissions.
|
||||
::
|
||||
/- *link, group-store
|
||||
/+ default-agent, verb
|
||||
::
|
||||
|%
|
||||
+$ state-0
|
||||
$: %0
|
||||
~
|
||||
::NOTE this means we could get away with just producing cards everywhere,
|
||||
:: never producing new state outside of the agent interface core.
|
||||
:: we opt to keep ^-(quip card _state) in place for most logic arms
|
||||
:: because it doesn't cost much, results in unsurprising code, and
|
||||
:: makes adding any state in the future easier.
|
||||
==
|
||||
::
|
||||
+$ card card:agent:gall
|
||||
--
|
||||
::
|
||||
=| state-0
|
||||
=* state -
|
||||
::
|
||||
%+ verb |
|
||||
^- agent:gall
|
||||
=<
|
||||
|_ =bowl:gall
|
||||
+* this .
|
||||
do ~(. +> bowl)
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
::
|
||||
++ on-init
|
||||
^- (quip card _this)
|
||||
:_ this
|
||||
[watch-groups:do]~
|
||||
::
|
||||
++ on-save !>(state)
|
||||
++ on-load
|
||||
|= old=vase
|
||||
^- (quip card _this)
|
||||
[~ this(state !<(state-0 old))]
|
||||
::
|
||||
++ on-agent
|
||||
|= [=wire =sign:agent:gall]
|
||||
^- (quip card _this)
|
||||
?: ?=([%groups ~] wire)
|
||||
=^ cards state
|
||||
(take-groups-sign:do sign)
|
||||
[cards this]
|
||||
?: ?=([%links @ ^] wire)
|
||||
=^ cards state
|
||||
(take-links-sign:do (slav %p i.t.wire) t.t.wire sign)
|
||||
[cards this]
|
||||
?: ?=([%forward ^] wire)
|
||||
=^ cards state
|
||||
(take-forward-sign:do t.wire sign)
|
||||
[cards this]
|
||||
~| [dap.bowl %weird-wire wire]
|
||||
!!
|
||||
::
|
||||
++ on-arvo
|
||||
|= [=wire =sign-arvo]
|
||||
^- (quip card _this)
|
||||
?. ?=([%g %done *] sign-arvo)
|
||||
(on-arvo:def wire sign-arvo)
|
||||
?~ error.sign-arvo [~ this]
|
||||
=/ =tank leaf+"{(trip dap.bowl)}'s message went wrong!"
|
||||
%- (slog tank tang.u.error.sign-arvo)
|
||||
[~ this]
|
||||
::
|
||||
++ on-poke on-poke:def
|
||||
++ on-peek on-peek:def
|
||||
++ on-watch on-watch:def
|
||||
++ on-leave on-leave:def
|
||||
++ on-fail on-fail:def
|
||||
--
|
||||
::
|
||||
::
|
||||
|_ =bowl:gall
|
||||
::
|
||||
:: groups subscription
|
||||
::
|
||||
++ watch-groups
|
||||
^- card
|
||||
[%pass /groups %agent [our.bowl %group-store] %watch /all]
|
||||
::
|
||||
++ 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
|
||||
~& [dap.bowl %fact mark]
|
||||
?+ mark ~|([dap.bowl %unexpected-mark mark] !!)
|
||||
%group-initial (handle-group-initial !<(groups:group-store vase))
|
||||
%group-update (handle-group-update !<(group-update:group-store vase))
|
||||
==
|
||||
==
|
||||
::
|
||||
++ handle-group-initial
|
||||
|= =groups:group-store
|
||||
^- (quip card _state)
|
||||
=| cards=(list card)
|
||||
=/ groups=(list [=path =group:group-store])
|
||||
~(tap by groups)
|
||||
|-
|
||||
?~ groups [cards state]
|
||||
=^ caz state
|
||||
%- handle-group-update
|
||||
[%add [group path]:i.groups]
|
||||
$(cards (weld cards caz), groups t.groups)
|
||||
::
|
||||
++ handle-group-update
|
||||
|= upd=group-update:group-store
|
||||
^- (quip card _state)
|
||||
:_ state
|
||||
?+ -.upd ~
|
||||
?(%path %add %remove)
|
||||
=/ whos=(list ship) ~(tap in members.upd)
|
||||
|- ^- (list card)
|
||||
?~ whos ~
|
||||
:: no need to subscribe to ourselves
|
||||
::
|
||||
?: =(our.bowl i.whos)
|
||||
$(whos t.whos)
|
||||
:_ $(whos t.whos)
|
||||
%. [i.whos pax.upd]
|
||||
?: ?=(%remove -.upd)
|
||||
end-link-subscription
|
||||
start-link-subscription
|
||||
==
|
||||
::
|
||||
:: link subscriptions
|
||||
::
|
||||
++ start-link-subscription
|
||||
|= [who=ship where=path]
|
||||
^- card
|
||||
:* %pass
|
||||
[%links (scot %p who) where]
|
||||
%agent
|
||||
[who %link-proxy-hook]
|
||||
%watch
|
||||
[%local-pages where]
|
||||
==
|
||||
::
|
||||
++ end-link-subscription
|
||||
|= [who=ship where=path]
|
||||
^- card
|
||||
:* %pass
|
||||
[%links (scot %p who) where]
|
||||
%agent
|
||||
[who %link-proxy-hook]
|
||||
%leave
|
||||
~
|
||||
==
|
||||
::
|
||||
++ take-links-sign
|
||||
|= [who=ship where=path =sign:agent:gall]
|
||||
^- (quip card _state)
|
||||
?- -.sign
|
||||
%poke-ack ~|([dap.bowl %unexpected-poke-ack /links who where] !!)
|
||||
%kick [[(start-link-subscription who where)]~ state]
|
||||
::
|
||||
%watch-ack
|
||||
?~ p.sign [~ state]
|
||||
:: our subscription request got rejected for whatever reason,
|
||||
:: (most likely difference in group membership,)
|
||||
:: so we don't try again.
|
||||
::TODO but now the only way to retry is to remove from group and re-add...
|
||||
:: this is a problem because our and their group may not update
|
||||
:: simultaneously...
|
||||
[~ state]
|
||||
::
|
||||
%fact
|
||||
=* mark p.cage.sign
|
||||
=* vase q.cage.sign
|
||||
?+ mark ~|([dap.bowl %unexpected-mark mark] !!)
|
||||
%link-update (handle-link-update who where !<(update vase))
|
||||
==
|
||||
==
|
||||
::
|
||||
++ handle-link-update
|
||||
|= [who=ship where=path =update]
|
||||
^- (quip card _state)
|
||||
?> ?=(%local-pages -.update)
|
||||
?> =(src.bowl who)
|
||||
:_ state
|
||||
%+ turn pages.update
|
||||
|= =page
|
||||
^- card
|
||||
:* %pass
|
||||
[%forward (scot %p who) where]
|
||||
%agent
|
||||
[our.bowl %link-store]
|
||||
%poke
|
||||
%link-action
|
||||
!>([%hear where src.bowl page])
|
||||
==
|
||||
::
|
||||
++ 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]
|
||||
--
|
231
pkg/arvo/app/link-proxy-hook.hoon
Normal file
231
pkg/arvo/app/link-proxy-hook.hoon
Normal file
@ -0,0 +1,231 @@
|
||||
:: link-proxy-hook: 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 adopts a very primitive view of groups-store as containing only
|
||||
:: groups of interesting (rather than uninteresting) ships. it sets the
|
||||
:: permission condition to be that ship must be in group matching the path
|
||||
:: it's subscribing to.
|
||||
:: we check this on-watch, but also subscribe to 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.
|
||||
::
|
||||
/- *link, group-store
|
||||
/+ default-agent, verb
|
||||
|%
|
||||
+$ 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))
|
||||
==
|
||||
::
|
||||
+$ card card:agent:gall
|
||||
--
|
||||
::
|
||||
=| state-0
|
||||
=* state -
|
||||
::
|
||||
%+ verb |
|
||||
^- agent:gall
|
||||
=<
|
||||
|_ =bowl:gall
|
||||
+* this .
|
||||
do ~(. +> bowl)
|
||||
def ~(. (default-agent this %&) bowl)
|
||||
::
|
||||
++ on-init
|
||||
^- (quip card _this)
|
||||
:_ this
|
||||
[watch-groups:do]~
|
||||
::
|
||||
++ on-save !>(state)
|
||||
++ on-load
|
||||
|= old=vase
|
||||
^- (quip card _this)
|
||||
[~ this(state !<(state-0 old))]
|
||||
::
|
||||
++ 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
|
||||
++ permitted
|
||||
|= [who=ship =path]
|
||||
^- ?
|
||||
:: we only expose /local-pages, and only to ships in the relevant group
|
||||
::
|
||||
?. ?=([%local-pages ^] path) |
|
||||
=; group
|
||||
?& ?=(^ group)
|
||||
(~(has in u.group) who)
|
||||
==
|
||||
.^ (unit group:group-store)
|
||||
%gx
|
||||
(scot %p our.bowl)
|
||||
%group-store
|
||||
(scot %da now.bowl)
|
||||
(snoc t.path %noun)
|
||||
==
|
||||
::
|
||||
:: 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 /all]
|
||||
::
|
||||
++ 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
|
||||
~& [dap.bowl %fact mark]
|
||||
?+ mark ~|([dap.bowl %unexpected-mark mark] !!)
|
||||
%group-initial [~ state]
|
||||
%group-update (handle-group-update !<(group-update:group-store vase))
|
||||
==
|
||||
==
|
||||
::
|
||||
++ handle-group-update
|
||||
|= upd=group-update:group-store
|
||||
^- (quip card _state)
|
||||
:_ state
|
||||
?. ?=(%remove -.upd) ~
|
||||
=/ whos=(list ship) ~(tap in members.upd)
|
||||
|- ^- (list card)
|
||||
?~ whos ~
|
||||
:: no need to remove to ourselves
|
||||
::
|
||||
?: =(our.bowl i.whos)
|
||||
$(whos t.whos)
|
||||
:_ $(whos t.whos)
|
||||
::NOTE this depends kind of unfortunately on the fact that we only accept
|
||||
:: subscriptions to /local-pages/* paths. it'd be more correct if we
|
||||
:: "just" looked at all paths in the map, and found the matching ones.
|
||||
(kick-proxy i.whos [%local-pages pax.upd])
|
||||
::
|
||||
:: proxy subscriptions
|
||||
::
|
||||
++ kick-proxy
|
||||
|= [who=ship =path]
|
||||
^- card
|
||||
[%give %kick `path `who]
|
||||
::
|
||||
++ handle-proxy-sign
|
||||
|= [=path =sign:agent:gall]
|
||||
^- (quip card _state)
|
||||
?- -.sign
|
||||
%poke-ack ~|([dap.bowl %unexpected-poke-ack path] !!)
|
||||
%fact [[%give %fact `path cage.sign]~ state]
|
||||
%kick [[(proxy-pass-link-store path %watch path)]~ 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=update
|
||||
[%local-pages path .^(pages %gx path)]
|
||||
[%give %fact ~ %link-update !>(initial)]
|
||||
::
|
||||
++ 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 ~)]~
|
||||
--
|
230
pkg/arvo/app/link-server-hook.hoon
Normal file
230
pkg/arvo/app/link-server-hook.hoon
Normal file
@ -0,0 +1,230 @@
|
||||
:: link-server: accessing link-store via eyre
|
||||
::
|
||||
:: only accepts requests authenticated as the host ship.
|
||||
::
|
||||
:: GET requests:
|
||||
:: /~link/local-pages/[some-path].json?p=0
|
||||
:: our submissions on path, with optional pagination
|
||||
::
|
||||
:: POST requests:
|
||||
:: /~link/add/[some-path]
|
||||
:: send {title url} json, will save link at path
|
||||
::
|
||||
/+ *link, *server, default-agent, verb
|
||||
::
|
||||
|%
|
||||
+$ state-0
|
||||
$: %0
|
||||
~
|
||||
::NOTE this means we could get away with just producing cards everywhere,
|
||||
:: never producing new state outside of the agent interface core.
|
||||
:: we opt to keep ^-(quip card _state) in place for most logic arms
|
||||
:: because it doesn't cost much, results in unsurprising code, and
|
||||
:: makes adding any state in the future easier.
|
||||
==
|
||||
::
|
||||
+$ card card:agent:gall
|
||||
--
|
||||
::
|
||||
=| state-0
|
||||
=* state -
|
||||
::
|
||||
%+ verb |
|
||||
^- agent:gall
|
||||
=<
|
||||
|_ =bowl:gall
|
||||
+* this .
|
||||
do ~(. +> bowl)
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
::
|
||||
++ on-init
|
||||
^- (quip card _this)
|
||||
:_ this
|
||||
[start-serving:do]~
|
||||
::
|
||||
++ on-save !>(state)
|
||||
++ on-load
|
||||
|= old=vase
|
||||
^- (quip card _this)
|
||||
[~ this(state !<(state-0 old))]
|
||||
::
|
||||
++ on-watch
|
||||
|= =path
|
||||
^- (quip card _this)
|
||||
?: ?=([%http-response *] path)
|
||||
[~ this]
|
||||
(on-watch:def path)
|
||||
::
|
||||
++ on-poke
|
||||
|= [=mark =vase]
|
||||
^- (quip card _this)
|
||||
?. ?=(%handle-http-request mark)
|
||||
(on-poke:def mark vase)
|
||||
:_ this
|
||||
=+ !<([eyre-id=@ta =inbound-request:eyre] vase)
|
||||
(handle-http-request:do eyre-id inbound-request)
|
||||
::
|
||||
++ on-arvo
|
||||
|= [=wire =sign-arvo]
|
||||
^- (quip card _this)
|
||||
?. ?=(%bound +<.sign-arvo)
|
||||
(on-arvo:def wire sign-arvo)
|
||||
[~ this]
|
||||
::
|
||||
++ on-agent
|
||||
|= [=wire =sign:agent:gall]
|
||||
^- (quip card _this)
|
||||
?. ?=(%poke-ack -.sign)
|
||||
(on-agent:def wire sign)
|
||||
?~ p.sign [~ this]
|
||||
=/ =tank
|
||||
leaf+"{(trip dap.bowl)} failed writing to %link-store"
|
||||
%- (slog tank u.p.sign)
|
||||
[~ this]
|
||||
::
|
||||
++ on-peek on-peek:def
|
||||
++ on-leave on-leave:def
|
||||
++ on-fail on-fail:def
|
||||
--
|
||||
::
|
||||
|_ =bowl:gall
|
||||
::
|
||||
++ start-serving
|
||||
^- card
|
||||
[%pass / %arvo %e %connect [~ /'~link'] dap.bowl]
|
||||
::
|
||||
++ do-action
|
||||
|= =action
|
||||
^- card
|
||||
[%pass / %agent [our.bowl %link-store] %poke %link-action !>(action)]
|
||||
::
|
||||
++ do-add
|
||||
|= [=path title=@t =url]
|
||||
^- card
|
||||
(do-action %add path title url)
|
||||
::
|
||||
++ handle-http-request
|
||||
|= [eyre-id=@ta =inbound-request:eyre]
|
||||
^- (list card)
|
||||
::NOTE we don't use +require-authorization because it's too restrictive
|
||||
:: on the flow we want here.
|
||||
::
|
||||
?. ?& authenticated.inbound-request
|
||||
=(src.bowl our.bowl)
|
||||
==
|
||||
::TODO `*octs -> ~ everywhere once no-data bug is fixed
|
||||
(give-simple-payload:app eyre-id [[403 ~] `*octs])
|
||||
:: request-line: parsed url + params
|
||||
::
|
||||
=/ =request-line
|
||||
%- parse-request-line
|
||||
url.request.inbound-request
|
||||
=* req-head header-list.request.inbound-request
|
||||
=- ::TODO =; [cards=(list card) =simple-payload:http]
|
||||
%+ weld cards
|
||||
(give-simple-payload:app eyre-id simple-payload)
|
||||
^- [cards=(list card) =simple-payload:http]
|
||||
?+ method.request.inbound-request [~ not-found:gen]
|
||||
%'OPTIONS'
|
||||
[~ (include-cors-headers req-head [[200 ~] `*octs])]
|
||||
::
|
||||
%'GET'
|
||||
[~ (handle-get req-head request-line)]
|
||||
::
|
||||
%'POST'
|
||||
(handle-post req-head request-line body.request.inbound-request)
|
||||
==
|
||||
::
|
||||
++ handle-post
|
||||
|= [request-headers=header-list:http =request-line body=(unit octs)]
|
||||
^- [(list card) simple-payload:http]
|
||||
=- ::TODO =; [success=? cards=(list card)]
|
||||
:- cards
|
||||
%+ include-cors-headers
|
||||
request-headers
|
||||
::TODO it would be more correct to wait for the %poke-ack instead of
|
||||
:: sending this response right away... but link-store pokes can't
|
||||
:: actually fail right now, so it's fine.
|
||||
[[?:(success 200 400) ~] `*octs]
|
||||
^- [success=? cards=(list card)]
|
||||
?~ body [| ~]
|
||||
?+ request-line [| ~]
|
||||
[[~ [%'~link' %add ^]] ~]
|
||||
^- [? (list card)]
|
||||
=/ jon=(unit json) (de-json:html q.u.body)
|
||||
?~ jon [| ~]
|
||||
=/ page=(unit [title=@t =url])
|
||||
%. u.jon
|
||||
(ot title+so url+so ~):dejs-soft:format
|
||||
?~ page [| ~]
|
||||
[& [(do-add t.t.site.request-line [title url]:u.page) ~]]
|
||||
==
|
||||
::
|
||||
++ handle-get
|
||||
|= [request-headers=header-list:http =request-line]
|
||||
%+ include-cors-headers
|
||||
request-headers
|
||||
^- simple-payload:http
|
||||
:: args: map of params
|
||||
:: p: pagination index
|
||||
::
|
||||
=/ args
|
||||
%- ~(gas by *(map @t @t))
|
||||
args.request-line
|
||||
=/ p=(unit @ud)
|
||||
%+ biff (~(get by args) 'p')
|
||||
(curr rush dim:ag)
|
||||
?+ request-line not-found:gen
|
||||
::TODO expose submissions, other data
|
||||
:: local links by recency as json
|
||||
::
|
||||
[[[~ %json] [%'~link' %local-pages ^]] *]
|
||||
%- json-response:gen
|
||||
%- json-to-octs ::TODO include in +json-response:gen
|
||||
^- json
|
||||
:- %a
|
||||
%+ turn
|
||||
`pages`(get-pages t.t.site.request-line p)
|
||||
`$-(page json)`page:en-json
|
||||
==
|
||||
::
|
||||
++ include-cors-headers
|
||||
|= [request-headers=header-list:http =simple-payload:http]
|
||||
^+ simple-payload
|
||||
=* out-heads headers.response-header.simple-payload
|
||||
=; =header-list:http
|
||||
|-
|
||||
?~ header-list simple-payload
|
||||
=* new-head i.header-list
|
||||
=. out-heads
|
||||
(set-header:http key.new-head value.new-head out-heads)
|
||||
$(header-list t.header-list)
|
||||
=/ origin=@t
|
||||
=/ headers=(map @t @t)
|
||||
(~(gas by *(map @t @t)) request-headers)
|
||||
(~(gut by headers) 'origin' '*')
|
||||
:~ 'Access-Control-Allow-Origin'^origin
|
||||
'Access-Control-Allow-Credentials'^'true'
|
||||
'Access-Control-Request-Method'^'OPTIONS, GET, POST'
|
||||
'Access-Control-Allow-Methods'^'OPTIONS, GET, POST'
|
||||
'Access-Control-Allow-Headers'^'content-type'
|
||||
==
|
||||
::
|
||||
++ page-size 25
|
||||
++ get-pages
|
||||
|= [=path p=(unit @ud)]
|
||||
^- pages
|
||||
=; =pages
|
||||
?~ p pages
|
||||
%+ scag page-size
|
||||
%+ slag (mul u.p page-size)
|
||||
pages
|
||||
.^ pages
|
||||
%gx
|
||||
(scot %p our.bowl)
|
||||
%link-store
|
||||
(scot %da now.bowl)
|
||||
%local-pages
|
||||
(snoc path %noun)
|
||||
==
|
||||
--
|
172
pkg/arvo/app/link-store.hoon
Normal file
172
pkg/arvo/app/link-store.hoon
Normal file
@ -0,0 +1,172 @@
|
||||
:: link: social bookmarking
|
||||
::
|
||||
:: the paths under which links are submitted are generally expected to
|
||||
:: correspond to existing group paths. for strictly-local collections of
|
||||
:: links, arbitrary paths are probably fair game, but could trip up
|
||||
:: primitive ui implementations.
|
||||
::
|
||||
:: scry and subscription paths:
|
||||
::
|
||||
:: /local-pages/[some-group] all pages we saved by recency
|
||||
:: /submissions/[some-group] all submissions by recency
|
||||
::
|
||||
/+ *link, default-agent, verb
|
||||
::
|
||||
|%
|
||||
+$ state-0
|
||||
$: %0
|
||||
by-group=(map path links)
|
||||
by-site=(map site (list [path submission]))
|
||||
==
|
||||
::
|
||||
+$ links
|
||||
$: ::NOTE all lists by recency
|
||||
=submissions
|
||||
ours=pages
|
||||
==
|
||||
::
|
||||
+$ card card:agent:gall
|
||||
--
|
||||
::
|
||||
=| state-0
|
||||
=* state -
|
||||
::
|
||||
%+ 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:de-json !<(json vase)))
|
||||
%link-action (do-action:do !<(action 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))
|
||||
==
|
||||
::
|
||||
++ on-watch
|
||||
|= =path
|
||||
^- (quip card _this)
|
||||
?> (team:title [our src]:bowl) ::TODO /lib/store
|
||||
:_ this
|
||||
|^ ?+ path (on-watch:def path)
|
||||
[%local-pages ^]
|
||||
%+ give %link-update
|
||||
[%local-pages t.path (get-local-pages:do t.path)]
|
||||
::
|
||||
[%submissions ^]
|
||||
%+ give %link-update
|
||||
[%submissions t.path (get-submissions:do t.path)]
|
||||
==
|
||||
::
|
||||
++ give
|
||||
|* [=mark =noun]
|
||||
^- (list 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
|
||||
::
|
||||
:: writing
|
||||
::
|
||||
++ do-action
|
||||
|= =action
|
||||
^- (quip card _state)
|
||||
?- -.action
|
||||
%add (add-page +.action)
|
||||
%hear (hear-submission +.action)
|
||||
==
|
||||
:: +add-page: save a page ourselves
|
||||
::
|
||||
++ add-page
|
||||
|= [=path title=@t =url]
|
||||
^- (quip card _state)
|
||||
?< =(~ path)
|
||||
:: 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
|
||||
::
|
||||
=^ cards state
|
||||
(hear-submission path [our.bowl page])
|
||||
:: send updates to subscribers
|
||||
::
|
||||
:_ state
|
||||
:_ cards
|
||||
:+ %give %fact
|
||||
:+ `[%local-pages path]
|
||||
%link-update
|
||||
!>([%local-pages path [page]~])
|
||||
:: +hear-submission: record page someone else saved
|
||||
::
|
||||
++ hear-submission
|
||||
|= [=path =submission]
|
||||
^- (quip card _state)
|
||||
?< =(~ path)
|
||||
:: add link to group submissions
|
||||
::
|
||||
=/ =links (~(gut by by-group) path *links)
|
||||
=. submissions.links [submission submissions.links]
|
||||
=. by-group (~(put by by-group) path links)
|
||||
:: add submission to global sites
|
||||
::
|
||||
=/ =site (site-from-url url.submission)
|
||||
=. by-site (~(add ja by-site) site [path submission])
|
||||
:: send updates to subscribers
|
||||
::
|
||||
:_ state
|
||||
:_ ~
|
||||
:+ %give %fact
|
||||
:+ `[%submissions path]
|
||||
%link-update
|
||||
!>([%submissions path [submission]~])
|
||||
::
|
||||
:: reading
|
||||
::
|
||||
++ get-local-pages
|
||||
|= =path
|
||||
^- pages
|
||||
ours:(~(gut by by-group) path *links)
|
||||
::
|
||||
++ get-submissions
|
||||
|= =path
|
||||
^- submissions
|
||||
submissions:(~(gut by by-group) path *links)
|
||||
--
|
10
pkg/arvo/gen/group-store/add.hoon
Normal file
10
pkg/arvo/gen/group-store/add.hoon
Normal file
@ -0,0 +1,10 @@
|
||||
:: group-store|add: add members to a group
|
||||
::
|
||||
/- *group-store
|
||||
:- %say
|
||||
|= $: [now=@da eny=@uvJ =beak]
|
||||
[[=path members=(list ship) ~] ~]
|
||||
==
|
||||
:- %group-action
|
||||
^- group-action
|
||||
[%add (sy members) path]
|
10
pkg/arvo/gen/group-store/create.hoon
Normal file
10
pkg/arvo/gen/group-store/create.hoon
Normal file
@ -0,0 +1,10 @@
|
||||
:: group-store|create: initialize a group
|
||||
::
|
||||
/- *group-store
|
||||
:- %say
|
||||
|= $: [now=@da eny=@uvJ =beak]
|
||||
[[=path ~] ~]
|
||||
==
|
||||
:- %group-action
|
||||
^- group-action
|
||||
[%bundle path]
|
10
pkg/arvo/gen/group-store/remove.hoon
Normal file
10
pkg/arvo/gen/group-store/remove.hoon
Normal file
@ -0,0 +1,10 @@
|
||||
:: group-store|remove: remove members from a group
|
||||
::
|
||||
/- *group-store
|
||||
:- %say
|
||||
|= $: [now=@da eny=@uvJ =beak]
|
||||
[[=path members=(list ship) ~] ~]
|
||||
==
|
||||
:- %group-action
|
||||
^- group-action
|
||||
[%remove (sy members) path]
|
10
pkg/arvo/gen/link-store/add.hoon
Normal file
10
pkg/arvo/gen/link-store/add.hoon
Normal file
@ -0,0 +1,10 @@
|
||||
:: link-store|add: save a link to a path
|
||||
::
|
||||
/- *link
|
||||
:- %say
|
||||
|= $: [now=@da eny=@uvJ =beak]
|
||||
[[=path title=@t =url ~] ~]
|
||||
==
|
||||
:- %link-action
|
||||
^- action
|
||||
[%add path title url]
|
49
pkg/arvo/lib/link.hoon
Normal file
49
pkg/arvo/lib/link.hoon
Normal file
@ -0,0 +1,49 @@
|
||||
:: link: social bookmarking
|
||||
::
|
||||
/- *link
|
||||
::
|
||||
|%
|
||||
++ site-from-url
|
||||
|= =url
|
||||
^- site
|
||||
=/ murl=(unit purl:eyre)
|
||||
(de-purl:html url)
|
||||
?~ murl 'http://example.com'
|
||||
%^ cat 3
|
||||
:: render protocol
|
||||
::
|
||||
=* sec p.p.u.murl
|
||||
?:(sec 'https://' 'http://')
|
||||
:: render host
|
||||
::
|
||||
=* host r.p.u.murl
|
||||
?- -.host
|
||||
%& (roll (join '.' p.host) (cury cat 3))
|
||||
%| (rsh 3 1 (scot %if p.host))
|
||||
==
|
||||
::
|
||||
++ en-json
|
||||
=, enjs:format
|
||||
|%
|
||||
++ page
|
||||
|= =^page
|
||||
^- json
|
||||
%- pairs
|
||||
:~ 'title'^s+title.page
|
||||
'url'^s+url.page
|
||||
'timestamp'^(time time.page)
|
||||
==
|
||||
--
|
||||
::
|
||||
++ de-json
|
||||
=, dejs:format
|
||||
|%
|
||||
++ action
|
||||
|= =json
|
||||
^- ^action
|
||||
?> ?=([%o [%add *] ~ ~] json)
|
||||
:- %add ::TODO +of doesn't please type system?
|
||||
%. q.n.p.json
|
||||
(ot 'path'^pa 'title'^so 'url'^so ~)
|
||||
--
|
||||
--
|
@ -1,11 +1,14 @@
|
||||
=, eyre
|
||||
|%
|
||||
::
|
||||
+$ request-line
|
||||
$: [ext=(unit @ta) site=(list @t)]
|
||||
args=(list [key=@t value=@t])
|
||||
==
|
||||
:: +parse-request-line: take a cord and parse out a url
|
||||
::
|
||||
++ parse-request-line
|
||||
|= url=@t
|
||||
^- [[ext=(unit @ta) site=(list @t)] args=(list [key=@t value=@t])]
|
||||
^- request-line
|
||||
(fall (rush url ;~(plug apat:de-purl:html yque:de-purl:html)) [[~ ~] ~])
|
||||
::
|
||||
++ manx-to-octs
|
||||
|
15
pkg/arvo/mar/link/update.hoon
Normal file
15
pkg/arvo/mar/link/update.hoon
Normal file
@ -0,0 +1,15 @@
|
||||
:: link: subscription updates
|
||||
::
|
||||
::TODO this should include json conversion once mark performance improves
|
||||
/- *link
|
||||
|_ =update
|
||||
++ grow
|
||||
|%
|
||||
++ noun update
|
||||
--
|
||||
::
|
||||
++ grab
|
||||
|%
|
||||
++ noun ^update
|
||||
--
|
||||
--
|
44
pkg/arvo/sur/link.hoon
Normal file
44
pkg/arvo/sur/link.hoon
Normal file
@ -0,0 +1,44 @@
|
||||
:: link: social bookmarking
|
||||
::
|
||||
:: link operates on the core structure of "pages", which are URLs saved at a
|
||||
:: specific time with a specific title.
|
||||
:: submissions, then, are pages received from a specific ship.
|
||||
::
|
||||
|%
|
||||
:: primitives
|
||||
::
|
||||
+$ url @t
|
||||
+$ site @t :: domain, host, etc.
|
||||
:: +page: a saved URL with timestamp and custom title
|
||||
::
|
||||
+$ page
|
||||
$: title=@t
|
||||
=url
|
||||
=time
|
||||
==
|
||||
:: +submission: a page saved by a ship
|
||||
::
|
||||
+$ submission
|
||||
$: =ship
|
||||
page
|
||||
==
|
||||
:: lists, reverse chronological / newest first
|
||||
::
|
||||
+$ pages (list page)
|
||||
+$ submissions (list submission)
|
||||
::
|
||||
:: +action: local actions
|
||||
::
|
||||
+$ action
|
||||
$% [%add =path title=@t =url]
|
||||
[%hear =path from=ship =page] ::TODO just =submission?
|
||||
==
|
||||
:: +update: local updates
|
||||
::
|
||||
::NOTE we include paths explicitly to support the "subscribed to all" case
|
||||
::
|
||||
+$ update
|
||||
$% [%local-pages =path =pages]
|
||||
[%submissions =path =submissions]
|
||||
==
|
||||
--
|
92
pkg/interface/link-webext/background.js
Normal file
92
pkg/interface/link-webext/background.js
Normal file
@ -0,0 +1,92 @@
|
||||
|
||||
const attemptPost = (endpoint, path, data) => {
|
||||
console.log('sending', data, JSON.stringify(data));
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch(`http://${endpoint}/~link${path}`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => {
|
||||
console.log('resp', response.status);
|
||||
resolve(response.status === 200);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('post failed', error);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const attemptGet = (endpoint, path, data) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch(`http://${endpoint}/~link{path}`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => {
|
||||
console.log('get response');
|
||||
console.log('response', response);
|
||||
resolve(true);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('fetch error', error);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const saveUrl = (endpoint, title, url) => {
|
||||
return attemptPost(endpoint, '/add/private', {title, url});
|
||||
}
|
||||
|
||||
const openOptions = () => {
|
||||
browser.tabs.create({
|
||||
url: browser.runtime.getURL('options/index.html')
|
||||
});
|
||||
}
|
||||
|
||||
const openLogin = (endpoint) => {
|
||||
browser.tabs.create({
|
||||
url: `http://${endpoint}/~/login`
|
||||
});
|
||||
}
|
||||
|
||||
const doSave = async () => {
|
||||
console.log('gonna do save!');
|
||||
// if no endpoint, refer to options page
|
||||
const endpoint = await getEndpoint();
|
||||
console.log('endpoint', endpoint);
|
||||
if (endpoint === null) {
|
||||
return openOptions();
|
||||
}
|
||||
|
||||
const tab = (await browser.tabs.query({currentWindow: true, active: true}))[0];
|
||||
//TODO figure out if we're viewing urbit page, turn into arvo:// url?
|
||||
const success = await saveUrl(endpoint, tab.title, tab.url);
|
||||
console.log('success', success);
|
||||
if (!success) {
|
||||
console.log('failed, opening login');
|
||||
openLogin(endpoint);
|
||||
} else {
|
||||
console.log('success!');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// perform save action when extension button is clicked
|
||||
//TODO want to do a pop-up instead of on-click action here latern
|
||||
//
|
||||
browser.browserAction.onClicked.addListener(doSave);
|
||||
|
||||
// open settings page on-install, user will need to set endpoint
|
||||
//
|
||||
browser.runtime.onInstalled.addListener(async ({ reason, temporary }) => {
|
||||
// if (temporary) return; // skip during development
|
||||
switch (reason) {
|
||||
case "install":
|
||||
browser.runtime.openOptionsPage();
|
||||
break;
|
||||
}
|
||||
});
|
11
pkg/interface/link-webext/browserAction/index.html
Executable file
11
pkg/interface/link-webext/browserAction/index.html
Executable file
@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link href="style.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="myHeading">My browser action</h1>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
1
pkg/interface/link-webext/browserAction/script.js
Executable file
1
pkg/interface/link-webext/browserAction/script.js
Executable file
@ -0,0 +1 @@
|
||||
console.log('script.js firing');
|
3
pkg/interface/link-webext/browserAction/style.css
Executable file
3
pkg/interface/link-webext/browserAction/style.css
Executable file
@ -0,0 +1,3 @@
|
||||
h1 {
|
||||
font-style: italic;
|
||||
}
|
BIN
pkg/interface/link-webext/icons/icon.png
Normal file
BIN
pkg/interface/link-webext/icons/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 979 B |
39
pkg/interface/link-webext/manifest.json
Executable file
39
pkg/interface/link-webext/manifest.json
Executable file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "link",
|
||||
"description": "Urbit Link",
|
||||
"version": "0.0.0",
|
||||
"icons": {
|
||||
"64": "icons/icon.png"
|
||||
},
|
||||
"browser_action": {
|
||||
"default_icon": {
|
||||
"64": "icons/icon.png"
|
||||
},
|
||||
"todo__default_popup": "browserAction/index.html",
|
||||
"default_title": "link"
|
||||
},
|
||||
"background": {
|
||||
"scripts": [
|
||||
"background.js",
|
||||
"storage.js"
|
||||
]
|
||||
},
|
||||
"options_ui": {
|
||||
"page": "options/index.html"
|
||||
},
|
||||
"web_accessible_resources": [
|
||||
"src/options/options.html"
|
||||
],
|
||||
|
||||
"permissions": [
|
||||
"storage", // storing config
|
||||
"activeTab" // viewing current page url & title
|
||||
],
|
||||
|
||||
"applications": {
|
||||
"gecko": {
|
||||
"id": "link-webext@tlon.io"
|
||||
}
|
||||
}
|
||||
}
|
22
pkg/interface/link-webext/options/index.html
Executable file
22
pkg/interface/link-webext/options/index.html
Executable file
@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link href="style.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<form>
|
||||
<label>
|
||||
Ship HTTP endpoint:
|
||||
<input id="endpoint" type="text" placeholder="your-ship.arvo.network" />
|
||||
</label>
|
||||
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
|
||||
<script src="../storage.js"></script>
|
||||
<script src="script.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
21
pkg/interface/link-webext/options/script.js
Executable file
21
pkg/interface/link-webext/options/script.js
Executable file
@ -0,0 +1,21 @@
|
||||
|
||||
function storeOptions(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// clean up endpoint address and store it
|
||||
let endpoint = document.querySelector("#endpoint").value
|
||||
.replace(/^.*:\/\//, '') // strip protocol
|
||||
.replace(/\/+$/, ''); // strip trailing slashes
|
||||
setEndpoint(endpoint);
|
||||
}
|
||||
|
||||
async function restoreOptions() {
|
||||
|
||||
const endpoint = await getEndpoint();
|
||||
console.log('prefilling with', endpoint);
|
||||
|
||||
document.querySelector("#endpoint").value = endpoint;
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", restoreOptions);
|
||||
document.querySelector("form").addEventListener("submit", storeOptions);
|
3
pkg/interface/link-webext/options/style.css
Executable file
3
pkg/interface/link-webext/options/style.css
Executable file
@ -0,0 +1,3 @@
|
||||
h1 {
|
||||
font-style: italic;
|
||||
}
|
20
pkg/interface/link-webext/storage.js
Normal file
20
pkg/interface/link-webext/storage.js
Normal file
@ -0,0 +1,20 @@
|
||||
// use synced storage if supported, fall back to local
|
||||
const storage = browser.storage.sync || browser.storage.local;
|
||||
|
||||
const setEndpoint = (endpoint) => {
|
||||
return storage.set({endpoint});
|
||||
}
|
||||
|
||||
const getEndpoint = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
storage.get("endpoint").then((res) => {
|
||||
if (res && res.endpoint) {
|
||||
resolve(res.endpoint);
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
}, (err) => {
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue
Block a user