mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-11-27 14:07:34 +03:00
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.
This commit is contained in:
parent
25d390d6b1
commit
3a859ef585
1
.gitignore
vendored
1
.gitignore
vendored
@ -16,3 +16,4 @@ release/
|
|||||||
**/*.swp
|
**/*.swp
|
||||||
**/*.swo
|
**/*.swo
|
||||||
**/*-min.js
|
**/*-min.js
|
||||||
|
pkg/interface/link-webext/web-ext-artifacts
|
||||||
|
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)
|
||||||
|
==
|
||||||
|
--
|
@ -31,7 +31,7 @@
|
|||||||
=| state-0
|
=| state-0
|
||||||
=* state -
|
=* state -
|
||||||
::
|
::
|
||||||
%+ verb &
|
%+ verb |
|
||||||
^- agent:gall
|
^- agent:gall
|
||||||
=<
|
=<
|
||||||
|_ =bowl:gall
|
|_ =bowl:gall
|
||||||
|
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