From 3a859ef585cbb77a28a8a3c6cf85f30aa93b09ca Mon Sep 17 00:00:00 2001 From: Fang Date: Tue, 19 Nov 2019 22:56:29 +0100 Subject: [PATCH] link: add minimal link-server-hook and link-webext link-server-hook exposes (parts of) the link-store over eyre, on the condition that the client is authenticated as the host ship. link-webext as committed is a very minimal web extension. When its toolbar button is clicked, it saves the current webpage to /private in the link-store. In the future, this should support choosing a target to save to, highlighting already-saved pages, and many other features. --- .gitignore | 1 + pkg/arvo/app/link-server-hook.hoon | 230 ++++++++++++++++++ pkg/arvo/app/link-store.hoon | 2 +- pkg/interface/link-webext/background.js | 92 +++++++ .../link-webext/browserAction/index.html | 11 + .../link-webext/browserAction/script.js | 1 + .../link-webext/browserAction/style.css | 3 + pkg/interface/link-webext/icons/icon.png | Bin 0 -> 979 bytes pkg/interface/link-webext/manifest.json | 39 +++ pkg/interface/link-webext/options/index.html | 22 ++ pkg/interface/link-webext/options/script.js | 21 ++ pkg/interface/link-webext/options/style.css | 3 + pkg/interface/link-webext/storage.js | 20 ++ 13 files changed, 444 insertions(+), 1 deletion(-) create mode 100644 pkg/arvo/app/link-server-hook.hoon create mode 100644 pkg/interface/link-webext/background.js create mode 100755 pkg/interface/link-webext/browserAction/index.html create mode 100755 pkg/interface/link-webext/browserAction/script.js create mode 100755 pkg/interface/link-webext/browserAction/style.css create mode 100644 pkg/interface/link-webext/icons/icon.png create mode 100755 pkg/interface/link-webext/manifest.json create mode 100755 pkg/interface/link-webext/options/index.html create mode 100755 pkg/interface/link-webext/options/script.js create mode 100755 pkg/interface/link-webext/options/style.css create mode 100644 pkg/interface/link-webext/storage.js diff --git a/.gitignore b/.gitignore index 38a470ec5..fa6ff5e3c 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ release/ **/*.swp **/*.swo **/*-min.js +pkg/interface/link-webext/web-ext-artifacts diff --git a/pkg/arvo/app/link-server-hook.hoon b/pkg/arvo/app/link-server-hook.hoon new file mode 100644 index 000000000..ebec35bf9 --- /dev/null +++ b/pkg/arvo/app/link-server-hook.hoon @@ -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) + == +-- \ No newline at end of file diff --git a/pkg/arvo/app/link-store.hoon b/pkg/arvo/app/link-store.hoon index 945585b02..cd237ba6e 100644 --- a/pkg/arvo/app/link-store.hoon +++ b/pkg/arvo/app/link-store.hoon @@ -31,7 +31,7 @@ =| state-0 =* state - :: -%+ verb & +%+ verb | ^- agent:gall =< |_ =bowl:gall diff --git a/pkg/interface/link-webext/background.js b/pkg/interface/link-webext/background.js new file mode 100644 index 000000000..bef42b9d0 --- /dev/null +++ b/pkg/interface/link-webext/background.js @@ -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; + } +}); diff --git a/pkg/interface/link-webext/browserAction/index.html b/pkg/interface/link-webext/browserAction/index.html new file mode 100755 index 000000000..fc7571809 --- /dev/null +++ b/pkg/interface/link-webext/browserAction/index.html @@ -0,0 +1,11 @@ + + + + + + + +

My browser action

+ + + diff --git a/pkg/interface/link-webext/browserAction/script.js b/pkg/interface/link-webext/browserAction/script.js new file mode 100755 index 000000000..80333ffad --- /dev/null +++ b/pkg/interface/link-webext/browserAction/script.js @@ -0,0 +1 @@ +console.log('script.js firing'); \ No newline at end of file diff --git a/pkg/interface/link-webext/browserAction/style.css b/pkg/interface/link-webext/browserAction/style.css new file mode 100755 index 000000000..001d5c5dc --- /dev/null +++ b/pkg/interface/link-webext/browserAction/style.css @@ -0,0 +1,3 @@ +h1 { + font-style: italic; +} diff --git a/pkg/interface/link-webext/icons/icon.png b/pkg/interface/link-webext/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a5dcb632da606a8d3da192bd188c2b7bd814f8c1 GIT binary patch literal 979 zcmV;^11$WBP)004R= z004l4008;_004mL004C`008P>0026e000+nl3&F}00009a7bBm000XU000XU0RWnu z7ytkRSxH1eRCwCenOjIyK^VtH%ewra1 zT%nm}TDinE9x#ZDpVlOVg8suWT{KaN4b5P+T;>}tH3^~MCuze@xd82N$LLm(U>*O3 zbHeWqG#hW2H4|VRKTNd(TtlOYG4q*V6+c9SBECX501^RK@or8kb-sp11K-VmNUH+6 z6yX{+?wGF#*8lo)WdRk2wLl=js(?;qyhex0su&zM!A%acl`NK#!CLlENRa}%gnb-j z##}%!_$XB@B~f7M9(9lRDnN+gLqnQ0vQO|j9(_@q%$6sWj5I$!sC^W!N^PB-* zbB`opog|TfaUs4mD16S=_`wg+#{3r@IC0Y&jkDO7&FW|SifTj@UznUd+UA!OYAFU5E zzia7ZkY3722?3ug+m7^GkF0@AMm#>V+@g$ZivrRFQGsA7zAtUylh`ECA*}! z_ng5tWZxsZq=6#8SkU3O`K(jIujI1y6Dhq*WMx>>yJD-z*8p+~y&fi;p5) literal 0 HcmV?d00001 diff --git a/pkg/interface/link-webext/manifest.json b/pkg/interface/link-webext/manifest.json new file mode 100755 index 000000000..bdf30a2ba --- /dev/null +++ b/pkg/interface/link-webext/manifest.json @@ -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" + } + } +} \ No newline at end of file diff --git a/pkg/interface/link-webext/options/index.html b/pkg/interface/link-webext/options/index.html new file mode 100755 index 000000000..91986a3ae --- /dev/null +++ b/pkg/interface/link-webext/options/index.html @@ -0,0 +1,22 @@ + + + + + + + + +
+ + + +
+ + + + + + diff --git a/pkg/interface/link-webext/options/script.js b/pkg/interface/link-webext/options/script.js new file mode 100755 index 000000000..62cf914dc --- /dev/null +++ b/pkg/interface/link-webext/options/script.js @@ -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); \ No newline at end of file diff --git a/pkg/interface/link-webext/options/style.css b/pkg/interface/link-webext/options/style.css new file mode 100755 index 000000000..001d5c5dc --- /dev/null +++ b/pkg/interface/link-webext/options/style.css @@ -0,0 +1,3 @@ +h1 { + font-style: italic; +} diff --git a/pkg/interface/link-webext/storage.js b/pkg/interface/link-webext/storage.js new file mode 100644 index 000000000..19d9db91b --- /dev/null +++ b/pkg/interface/link-webext/storage.js @@ -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); + }); + }); +}